Migrate 5 non-test TS files in src/ from console.* to LogTape:
- src/env.ts → getLogger('sf.core.env')
- src/resource-loader.ts → getLogger('sf.core.resource-loader')
- src/web/undo-service.ts → getLogger('sf.web.undo-service')
- src/web/cleanup-service.ts → getLogger('sf.web.cleanup-service')
- src/web/auto-dashboard-service.ts → getLogger('sf.web.auto-dashboard-service')
console.error(err) → log.error(msg, {error: err})
console.warn(msg) → log.warn(msg)
All CLI-facing output preserved. typecheck, lint pass.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
290 lines
9.4 KiB
TypeScript
290 lines
9.4 KiB
TypeScript
import { homedir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { z } from "zod";
|
|
import { getLogger } from "./logger.js";
|
|
|
|
const log = getLogger("sf.core.env");
|
|
|
|
const optionalNonEmptyString = z.string().trim().min(1).optional();
|
|
|
|
const booleanOneZero = z
|
|
.enum(["0", "1"])
|
|
.optional()
|
|
.transform((value) => value === "1");
|
|
|
|
// Numeric values
|
|
const positiveInteger = z
|
|
.string()
|
|
.transform((v) => parseInt(v, 10))
|
|
.pipe(z.number().int().positive());
|
|
|
|
const optionalPositiveInteger = positiveInteger.optional();
|
|
|
|
/**
|
|
* Minimal schema (original, for backward compatibility).
|
|
* These variables are the only ones originally validated.
|
|
*/
|
|
export const sfEnvSchema = z.object({
|
|
SF_HOME: optionalNonEmptyString,
|
|
SF_AGENT_DIR: optionalNonEmptyString,
|
|
SF_CODING_AGENT_DIR: optionalNonEmptyString,
|
|
SF_STATE_DIR: optionalNonEmptyString,
|
|
SF_PROJECT_ID: z
|
|
.string()
|
|
.trim()
|
|
.regex(/^[A-Za-z0-9_-]+$/, {
|
|
message:
|
|
"SF_PROJECT_ID must contain only letters, numbers, hyphens, and underscores",
|
|
})
|
|
.optional(),
|
|
SF_BIN_PATH: optionalNonEmptyString,
|
|
SF_VERSION: optionalNonEmptyString,
|
|
SF_WEB_PROJECT_CWD: optionalNonEmptyString,
|
|
SF_WEB_DAEMON_MODE: booleanOneZero,
|
|
});
|
|
|
|
/**
|
|
* Comprehensive schema: all SF_* environment variables.
|
|
*
|
|
* Purpose: Provide type-safe, runtime-validated access to all SF_* environment
|
|
* variables with clear error messages for misconfiguration. Prevents silent failures
|
|
* caused by missing or invalid config.
|
|
*
|
|
* Design: Partitions variables into logical groups (core, directories, performance,
|
|
* debug, extensions, recovery, settings). Each variable is documented with its purpose,
|
|
* valid values, and defaults. Optional variables have sensible defaults where applicable.
|
|
*
|
|
* Consumer: loader.ts validates this on startup; modules access config via getCompleteSfEnv().
|
|
*/
|
|
export const completeSfEnvSchema = sfEnvSchema.extend({
|
|
// Core paths and metadata (set by loader.ts)
|
|
SF_PKG_ROOT: optionalNonEmptyString,
|
|
SF_WORKFLOW_PATH: optionalNonEmptyString,
|
|
SF_BUNDLED_EXTENSION_PATHS: optionalNonEmptyString,
|
|
|
|
// Directories with defaults
|
|
SF_WORKSPACE_BASE: optionalNonEmptyString,
|
|
SF_HISTORY_BASE: optionalNonEmptyString,
|
|
SF_NOTIFICATIONS_BASE: optionalNonEmptyString,
|
|
SF_SCHEDULE_FILE: optionalNonEmptyString,
|
|
SF_RECOVERY_BASE: optionalNonEmptyString,
|
|
SF_FORENSICS_BASE: optionalNonEmptyString,
|
|
SF_CLEANUP_BASE: optionalNonEmptyString,
|
|
SF_EXPORT_BASE: optionalNonEmptyString,
|
|
SF_CAPTURES_BASE: optionalNonEmptyString,
|
|
SF_UNDO_BASE: optionalNonEmptyString,
|
|
SF_SKILL_HEALTH_BASE: optionalNonEmptyString,
|
|
SF_DOCTOR_BASE: optionalNonEmptyString,
|
|
SF_SETTINGS_BASE: optionalNonEmptyString,
|
|
SF_PROJECT_ROOT: optionalNonEmptyString,
|
|
SF_NODE_BIN: optionalNonEmptyString,
|
|
|
|
// Performance tuning
|
|
SF_RTK_DISABLED: booleanOneZero,
|
|
SF_RTK_PATH: optionalNonEmptyString,
|
|
SF_RTK_REWRITE_TIMEOUT_MS: optionalPositiveInteger,
|
|
SF_CIRCUIT_BREAKER_OPEN_DURATION_MS: optionalPositiveInteger,
|
|
SF_CIRCUIT_BREAKER_FAILURE_THRESHOLD: optionalPositiveInteger,
|
|
SF_CIRCUIT_BREAKER_HALF_OPEN_MAX_ATTEMPTS: optionalPositiveInteger,
|
|
SF_HEADLESS_PROMPT_TRACE_CHARS: optionalPositiveInteger,
|
|
|
|
// Debug flags
|
|
SF_QUIET: booleanOneZero,
|
|
SF_DEBUG: booleanOneZero,
|
|
SF_DEBUG_EXTENSIONS: booleanOneZero,
|
|
SF_TRACE_ENABLED: booleanOneZero,
|
|
SF_HEADLESS: booleanOneZero,
|
|
SF_HEADLESS_PROMPT_TRACE: booleanOneZero,
|
|
SF_STARTUP_TIMING: booleanOneZero,
|
|
SF_SHOW_TOKEN_COST: booleanOneZero,
|
|
SF_FIRST_RUN_BANNER: booleanOneZero,
|
|
SF_DISABLE_STARTUP_DOCTOR: booleanOneZero,
|
|
SF_ENGINE_BYPASS: booleanOneZero,
|
|
SF_DISABLE_NATIVE_SF_PARSER: booleanOneZero,
|
|
SF_DISABLE_NATIVE_SF_GIT: booleanOneZero,
|
|
|
|
// Extensions
|
|
SF_SKILL_MANIFEST_STRICT: booleanOneZero,
|
|
SF_PERMISSION_LEVEL: z.enum(["full", "restricted", "sandbox"]).optional(),
|
|
SF_GEMINI_PERMISSION_MODE: z.enum(["ask", "auto", "deny"]).optional(),
|
|
SF_SESSION_BROWSER_DIR: optionalNonEmptyString,
|
|
SF_SESSION_BROWSER_CWD: optionalNonEmptyString,
|
|
SF_FETCH_ALLOWED_URLS: optionalNonEmptyString,
|
|
SF_ALLOWED_COMMAND_PREFIXES: optionalNonEmptyString,
|
|
|
|
// Recovery and dispatch
|
|
SF_RECOVERY_DOCTOR_MODULE: optionalNonEmptyString,
|
|
SF_RECOVERY_FORENSICS_MODULE: optionalNonEmptyString,
|
|
SF_RECOVERY_SCOPE: z.enum(["unit", "milestone", "global"]).optional(),
|
|
SF_RECOVERY_SESSION_FILE: optionalNonEmptyString,
|
|
SF_RECOVERY_ACTIVITY_DIR: optionalNonEmptyString,
|
|
SF_PARALLEL_WORKER: booleanOneZero,
|
|
SF_WORKER_MODEL: optionalNonEmptyString,
|
|
SF_MILESTONE_LOCK: optionalNonEmptyString,
|
|
SF_SLICE_LOCK: optionalNonEmptyString,
|
|
SF_WORKTREE: optionalNonEmptyString,
|
|
SF_CLI_WORKTREE: optionalNonEmptyString,
|
|
SF_CLI_WORKTREE_BASE: optionalNonEmptyString,
|
|
SF_CLEANUP_BRANCHES: booleanOneZero,
|
|
SF_CLEANUP_SNAPSHOTS: booleanOneZero,
|
|
SF_AUTO_WORKTREE: optionalNonEmptyString,
|
|
|
|
// Settings modules
|
|
SF_SETTINGS_BUDGET_MODULE: optionalNonEmptyString,
|
|
SF_SETTINGS_HISTORY_MODULE: optionalNonEmptyString,
|
|
SF_SETTINGS_METRICS_MODULE: optionalNonEmptyString,
|
|
SF_SETTINGS_PREFS_MODULE: optionalNonEmptyString,
|
|
SF_SETTINGS_ROUTER_MODULE: optionalNonEmptyString,
|
|
SF_WORKSPACE_MODULE: optionalNonEmptyString,
|
|
SF_SESSION_MANAGER_MODULE: optionalNonEmptyString,
|
|
|
|
// Miscellaneous
|
|
SF_TRIAGE_SUFFIX: optionalNonEmptyString,
|
|
SF_DOCTOR_SCOPE: z.enum(["fast", "normal", "deep"]).optional(),
|
|
SF_EXPORT_FORMAT: z.enum(["json", "csv", "markdown"]).optional(),
|
|
SF_TARGET_SESSION_NAME: optionalNonEmptyString,
|
|
SF_TARGET_SESSION_PATH: optionalNonEmptyString,
|
|
SF_VISUALIZER_BASE: optionalNonEmptyString,
|
|
SF_RECOVERY_UNIT_ID: optionalNonEmptyString,
|
|
SF_RECOVERY_UNIT_TYPE: optionalNonEmptyString,
|
|
});
|
|
|
|
export type SfEnv = z.infer<typeof sfEnvSchema>;
|
|
|
|
export type CompleteSfEnv = z.infer<typeof completeSfEnvSchema>;
|
|
|
|
/**
|
|
* Parse supported SF_* environment variables into a typed object.
|
|
*
|
|
* Purpose: give runtime code a shared contract for SF-specific environment
|
|
* variables instead of scattering ad hoc `process.env` parsing across entry
|
|
* points.
|
|
*
|
|
* Consumer: root CLI/headless modules and web bridge code that need stable SF
|
|
* path and mode values.
|
|
*/
|
|
export function parseSfEnv(env: NodeJS.ProcessEnv = process.env): SfEnv {
|
|
return sfEnvSchema.parse(env);
|
|
}
|
|
|
|
/**
|
|
* Parse all known SF_* environment variables (comprehensive validation).
|
|
*
|
|
* Purpose: Validate complete environment configuration at startup, catching
|
|
* misconfiguration early with clear error messages.
|
|
*
|
|
* Consumer: loader.ts calls this during initialization.
|
|
*
|
|
* Error handling: If validation fails, returns safe defaults rather than
|
|
* throwing (graceful degradation). Critical errors logged to stderr.
|
|
*/
|
|
export function parseCompleteSfEnv(
|
|
env: NodeJS.ProcessEnv = process.env,
|
|
): CompleteSfEnv {
|
|
const result = completeSfEnvSchema.safeParse(env);
|
|
|
|
if (!result.success) {
|
|
// Log validation errors for debugging (but don't crash)
|
|
const errors = result.error.issues.slice(0, 5); // First 5 errors
|
|
const message = errors
|
|
.map((e: z.ZodIssue) => ` ${e.path.join(".")}: ${e.message}`)
|
|
.join("\n");
|
|
|
|
if (process.env.SF_DEBUG) {
|
|
log.warn(
|
|
`Environment validation issues (first 5):\n${message}\n` +
|
|
`Set SF_QUIET=1 to suppress this check.\n`,
|
|
);
|
|
}
|
|
}
|
|
|
|
return result.data || ({} as CompleteSfEnv);
|
|
}
|
|
|
|
/**
|
|
* Return typed SF environment values with path defaults applied.
|
|
*
|
|
* Purpose: centralize default path behavior for SF_HOME and the managed agent
|
|
* directory while still validating user-provided overrides.
|
|
*
|
|
* Consumer: app-paths.ts, cli-logs.ts, headless-query.ts, and future env readers.
|
|
*/
|
|
export function getSfEnv(env: NodeJS.ProcessEnv = process.env) {
|
|
const parsed = parseSfEnv(env);
|
|
const sfHome = parsed.SF_HOME ?? join(homedir(), ".sf");
|
|
const agentDir =
|
|
parsed.SF_AGENT_DIR ?? parsed.SF_CODING_AGENT_DIR ?? join(sfHome, "agent");
|
|
return {
|
|
...parsed,
|
|
sfHome,
|
|
agentDir,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get all known SF_* environment values with defaults applied.
|
|
*
|
|
* Purpose: Single source of truth for complete environment configuration
|
|
* throughout the application. All modules needing environment access should
|
|
* use this function instead of direct process.env reads.
|
|
*
|
|
* Consumer: Extensions, dispatch loop, recovery modules.
|
|
*
|
|
* Design: Applies sensible defaults for optional variables. For example,
|
|
* STATE_DIR defaults to SF_HOME if not set. Relative paths are NOT converted
|
|
* to absolute (caller responsibility).
|
|
*/
|
|
export function getCompleteSfEnv(
|
|
env: NodeJS.ProcessEnv = process.env,
|
|
): CompleteSfEnv & {
|
|
sfHome: string;
|
|
agentDir: string;
|
|
stateDir: string;
|
|
} {
|
|
const parsed = parseCompleteSfEnv(env);
|
|
const sfHome = parsed.SF_HOME ?? join(homedir(), ".sf");
|
|
const agentDir =
|
|
parsed.SF_AGENT_DIR ?? parsed.SF_CODING_AGENT_DIR ?? join(sfHome, "agent");
|
|
const stateDir = parsed.SF_STATE_DIR ?? sfHome;
|
|
|
|
return {
|
|
...parsed,
|
|
sfHome,
|
|
agentDir,
|
|
stateDir,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get validation summary: which SF_* variables are set vs defaults applied.
|
|
*
|
|
* Purpose: For debugging and diagnostics. Shows which variables were explicitly
|
|
* set vs using built-in defaults.
|
|
*
|
|
* Consumer: startup doctor, debug utilities.
|
|
*/
|
|
export function getEnvValidationSummary(env: NodeJS.ProcessEnv = process.env): {
|
|
configured: string[];
|
|
defaults: string[];
|
|
total: number;
|
|
} {
|
|
const configured: string[] = [];
|
|
const defaults: string[] = [];
|
|
|
|
// List of all known SF_* variables
|
|
const knownVars = Object.keys(completeSfEnvSchema.shape);
|
|
|
|
for (const varName of knownVars) {
|
|
if (env[varName] !== undefined) {
|
|
configured.push(varName);
|
|
} else {
|
|
defaults.push(varName);
|
|
}
|
|
}
|
|
|
|
return {
|
|
configured,
|
|
defaults,
|
|
total: knownVars.length,
|
|
};
|
|
}
|