POSIX-style TZ strings like IST-2 cause a hard RangeError crash in formatMessages because Intl.DateTimeFormat only accepts IANA identifiers. - Add isValidTimezone/resolveTimezone helpers to src/timezone.ts - Make formatLocalTime fall back to UTC on invalid timezone - Validate TZ candidates in config.ts before accepting - Add timezone setup step to detect and prompt when autodetection fails - Use node:22-slim in Dockerfile (node:24-slim Trixie package renames) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
68 lines
2.2 KiB
TypeScript
68 lines
2.2 KiB
TypeScript
/**
|
|
* Step: timezone — Detect, validate, and persist the user's timezone.
|
|
* Writes TZ to .env if a valid IANA timezone is resolved.
|
|
* Emits NEEDS_USER_INPUT=true when autodetection fails.
|
|
*/
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import { isValidTimezone } from '../src/timezone.js';
|
|
import { logger } from '../src/logger.js';
|
|
import { emitStatus } from './status.js';
|
|
|
|
export async function run(args: string[]): Promise<void> {
|
|
const projectRoot = process.cwd();
|
|
const envFile = path.join(projectRoot, '.env');
|
|
|
|
// Check what's already in .env
|
|
let envFileTz: string | undefined;
|
|
if (fs.existsSync(envFile)) {
|
|
const content = fs.readFileSync(envFile, 'utf-8');
|
|
const match = content.match(/^TZ=(.+)$/m);
|
|
if (match) envFileTz = match[1].trim().replace(/^["']|["']$/g, '');
|
|
}
|
|
|
|
const systemTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
const envTz = process.env.TZ;
|
|
|
|
// Accept --tz flag from CLI (used when setup skill collects from user)
|
|
const tzFlagIdx = args.indexOf('--tz');
|
|
const userTz = tzFlagIdx !== -1 ? args[tzFlagIdx + 1] : undefined;
|
|
|
|
// Resolve: user-provided > .env > process.env > system autodetect
|
|
let resolvedTz: string | undefined;
|
|
for (const candidate of [userTz, envFileTz, envTz, systemTz]) {
|
|
if (candidate && isValidTimezone(candidate)) {
|
|
resolvedTz = candidate;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const needsUserInput = !resolvedTz;
|
|
|
|
if (resolvedTz && resolvedTz !== envFileTz) {
|
|
// Write/update TZ in .env
|
|
if (fs.existsSync(envFile)) {
|
|
let content = fs.readFileSync(envFile, 'utf-8');
|
|
if (/^TZ=/m.test(content)) {
|
|
content = content.replace(/^TZ=.*$/m, `TZ=${resolvedTz}`);
|
|
} else {
|
|
content = content.trimEnd() + `\nTZ=${resolvedTz}\n`;
|
|
}
|
|
fs.writeFileSync(envFile, content);
|
|
} else {
|
|
fs.writeFileSync(envFile, `TZ=${resolvedTz}\n`);
|
|
}
|
|
logger.info({ timezone: resolvedTz }, 'Set TZ in .env');
|
|
}
|
|
|
|
emitStatus('TIMEZONE', {
|
|
SYSTEM_TZ: systemTz || 'unknown',
|
|
ENV_TZ: envTz || 'unset',
|
|
ENV_FILE_TZ: envFileTz || 'unset',
|
|
RESOLVED_TZ: resolvedTz || 'none',
|
|
NEEDS_USER_INPUT: needsUserInput,
|
|
STATUS: needsUserInput ? 'needs_input' : 'success',
|
|
});
|
|
}
|