diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d7f15f..9e24466 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: "22" + registry-url: https://registry.npmjs.org cache: npm - name: Install dependencies diff --git a/package-lock.json b/package-lock.json index ce39a1d..182f6dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@carboncode/cli", - "version": "0.1.2", + "version": "0.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@carboncode/cli", - "version": "0.1.2", + "version": "0.1.5", "license": "MIT", "dependencies": { "cli-highlight": "^2.1.11", @@ -2225,7 +2225,7 @@ }, "node_modules/@types/ws": { "version": "8.18.1", - "resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.18.1.tgz", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "dev": true, "license": "MIT", @@ -6891,7 +6891,7 @@ }, "node_modules/ws": { "version": "8.20.1", - "resolved": "https://registry.npmmirror.com/ws/-/ws-8.20.1.tgz", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "license": "MIT", "engines": { diff --git a/package.json b/package.json index 4aeaec7..7be76a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@carboncode/cli", - "version": "0.1.2", + "version": "0.1.5", "description": "Chinese-first DeepSeek-powered terminal coding agent for personal developer workflows.", "type": "module", "bin": { diff --git a/src/cli/index.ts b/src/cli/index.ts index 92b72a3..7403643 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -90,23 +90,25 @@ function resolveDashboardPort( : undefined; } -/** Resolution order: flag → REASONIX_DASHBOARD_HOST env → config.dashboard.host → undefined (server defaults to 127.0.0.1). */ +/** Resolution order: flag → CARBONCODE_DASHBOARD_HOST env → legacy REASONIX_DASHBOARD_HOST env → config.dashboard.host → undefined (server defaults to 127.0.0.1). */ function resolveDashboardHost( flagValue: string | undefined, noConfig: boolean, ): string | undefined { const fromFlag = flagValue?.trim(); if (fromFlag) return fromFlag; - const fromEnv = process.env.REASONIX_DASHBOARD_HOST?.trim(); + const fromEnv = + process.env.CARBONCODE_DASHBOARD_HOST?.trim() ?? process.env.REASONIX_DASHBOARD_HOST?.trim(); if (fromEnv) return fromEnv; if (noConfig) return undefined; const fromCfg = readConfig().dashboard?.host; return typeof fromCfg === "string" && fromCfg.trim() ? fromCfg.trim() : undefined; } -/** Resolution order: REASONIX_DASHBOARD_TOKEN env → config.dashboard.token → undefined (server mints a fresh per-boot token). Min 16 chars; shorter values are dropped with a warning to avoid trivially-guessable tokens. */ +/** Resolution order: CARBONCODE_DASHBOARD_TOKEN env → legacy REASONIX_DASHBOARD_TOKEN env → config.dashboard.token → undefined (server mints a fresh per-boot token). Min 16 chars; shorter values are dropped with a warning to avoid trivially-guessable tokens. */ function resolveDashboardToken(noConfig: boolean): string | undefined { - const fromEnv = process.env.REASONIX_DASHBOARD_TOKEN?.trim(); + const fromEnv = + process.env.CARBONCODE_DASHBOARD_TOKEN?.trim() ?? process.env.REASONIX_DASHBOARD_TOKEN?.trim(); const fromCfg = noConfig ? undefined : readConfig().dashboard?.token?.trim(); const candidate = fromEnv || fromCfg; if (!candidate) return undefined; diff --git a/src/cli/startup-profile.ts b/src/cli/startup-profile.ts index df3d0c4..31cd070 100644 --- a/src/cli/startup-profile.ts +++ b/src/cli/startup-profile.ts @@ -9,7 +9,7 @@ const marks: PhaseMark[] = []; let dumped = false; function envFlag(): boolean { - const v = process.env.REASONIX_PROFILE_STARTUP; + const v = process.env.CARBONCODE_PROFILE_STARTUP ?? process.env.REASONIX_PROFILE_STARTUP; return v === "1" || v === "true" || v === "yes"; } @@ -36,7 +36,7 @@ export function dumpStartupProfile(stream: NodeJS.WriteStream = process.stderr): prev = m.t; } lines.push( - `─── ${Math.round(totalMs)}ms total · last phase ${marks[marks.length - 1]!.name} · set REASONIX_PROFILE_STARTUP=0 to silence`, + `─── ${Math.round(totalMs)}ms total · last phase ${marks[marks.length - 1]!.name} · set CARBONCODE_PROFILE_STARTUP=0 to silence`, ); stream.write(`${lines.join("\n")}\n`); } diff --git a/src/cli/ui/App.tsx b/src/cli/ui/App.tsx index 4261884..6f5cb7e 100644 --- a/src/cli/ui/App.tsx +++ b/src/cli/ui/App.tsx @@ -294,10 +294,10 @@ export interface AppProps { * Throttle interval in ms. 50ms —20Hz —slow enough that cursor-up * repaints on winpty/MINTTY/ConEmu/tmux don't leave half-drawn frames, * fast enough that streaming text still reads as continuous. Override - * via `REASONIX_FLUSH_MS` if you want 60Hz on a terminal you trust. + * via `CARBONCODE_FLUSH_MS` if you want 60Hz on a terminal you trust. */ const FLUSH_INTERVAL_MS = (() => { - const raw = process.env.REASONIX_FLUSH_MS; + const raw = process.env.CARBONCODE_FLUSH_MS ?? process.env.REASONIX_FLUSH_MS; if (!raw) return 50; const parsed = Number(raw); if (!Number.isFinite(parsed) || parsed < 16 || parsed > 1000) return 50; @@ -393,7 +393,7 @@ export function App(props: AppProps): React.ReactElement { [props.session], ); const [themeName, setThemeName] = React.useState(() => - resolveThemePreference(loadTheme(), process.env.REASONIX_THEME), + resolveThemePreference(loadTheme(), process.env.CARBONCODE_THEME ?? process.env.REASONIX_THEME), ); const statusBar = React.useMemo((): StatusBarConfig => { const cfg = readConfig().statusBar ?? {}; @@ -2535,7 +2535,10 @@ function AppInner({ const handleQQThemePick = useCallback( (target: ThemeChoice): string => { saveTheme(target); - const active = resolveThemePreference(target, process.env.REASONIX_THEME); + const active = resolveThemePreference( + target, + process.env.CARBONCODE_THEME ?? process.env.REASONIX_THEME, + ); setThemeName(active); return `theme saved: ${target}\nactive now: ${active}`; }, @@ -4282,7 +4285,7 @@ function AppInner({ saveTheme(outcome.value); const active = resolveThemePreference( outcome.value, - process.env.REASONIX_THEME, + process.env.CARBONCODE_THEME ?? process.env.REASONIX_THEME, ); setThemeName(active); log.pushInfo(`theme saved: ${outcome.value}\n active now: ${active}`); diff --git a/src/cli/ui/Wizard.tsx b/src/cli/ui/Wizard.tsx index 9706f8b..459c163 100644 --- a/src/cli/ui/Wizard.tsx +++ b/src/cli/ui/Wizard.tsx @@ -94,13 +94,19 @@ export function Wizard({ useEffect(() => onLanguageChange(() => setLanguageVersion((v) => v + 1)), []); const [previewTheme, setPreviewTheme] = useState(() => - resolveThemePreference(initial?.theme ?? loadTheme(), process.env.REASONIX_THEME), + resolveThemePreference( + initial?.theme ?? loadTheme(), + process.env.CARBONCODE_THEME ?? process.env.REASONIX_THEME, + ), ); const [step, setStep] = useState("language"); const [data, setData] = useState(() => ({ language: getLanguage(), - theme: resolveThemePreference(initial?.theme ?? loadTheme(), process.env.REASONIX_THEME), + theme: resolveThemePreference( + initial?.theme ?? loadTheme(), + process.env.CARBONCODE_THEME ?? process.env.REASONIX_THEME, + ), apiKey: existingApiKey ?? "", preset: initial?.preset ?? "auto", selectedCatalog: deriveInitialCatalog(initial?.mcp ?? []), diff --git a/src/cli/ui/open-url.ts b/src/cli/ui/open-url.ts index 6185624..9e4d7e3 100644 --- a/src/cli/ui/open-url.ts +++ b/src/cli/ui/open-url.ts @@ -1,4 +1,4 @@ -/** Cross-platform URL opener; no-op under CI / when REASONIX_NO_OPEN is set. */ +/** Cross-platform URL opener; no-op under CI / when CARBONCODE_NO_OPEN or legacy REASONIX_NO_OPEN is set. */ import { spawn } from "node:child_process"; import { platform } from "node:os"; @@ -10,7 +10,9 @@ export interface OpenUrlResult { export function openUrl(url: string): OpenUrlResult { if (process.env.CI) return { opened: false, reason: "ci" }; - if (process.env.REASONIX_NO_OPEN) return { opened: false, reason: "disabled" }; + if (process.env.CARBONCODE_NO_OPEN ?? process.env.REASONIX_NO_OPEN) { + return { opened: false, reason: "disabled" }; + } const os = platform(); let cmd: string; diff --git a/src/cli/ui/slash/handlers/observability.ts b/src/cli/ui/slash/handlers/observability.ts index c28cb0f..3b26ac3 100644 --- a/src/cli/ui/slash/handlers/observability.ts +++ b/src/cli/ui/slash/handlers/observability.ts @@ -230,7 +230,10 @@ function estimateCost(userText: string, loop: import("@/loop.js").CacheFirstLoop } const feedback: SlashHandler = (_args, loop, ctx) => { - const themeName = resolveThemePreference(loadTheme(), process.env.REASONIX_THEME); + const themeName = resolveThemePreference( + loadTheme(), + process.env.CARBONCODE_THEME ?? process.env.REASONIX_THEME, + ); const diagnostic = buildFeedbackDiagnostic({ version: VERSION, latestVersion: ctx.latestVersion ?? undefined, diff --git a/src/cli/ui/slash/handlers/theme.ts b/src/cli/ui/slash/handlers/theme.ts index ed519af..a81402c 100644 --- a/src/cli/ui/slash/handlers/theme.ts +++ b/src/cli/ui/slash/handlers/theme.ts @@ -17,7 +17,10 @@ const theme: SlashHandler = (args) => { } saveTheme(next); - const active = resolveThemePreference(next, process.env.REASONIX_THEME); + const active = resolveThemePreference( + next, + process.env.CARBONCODE_THEME ?? process.env.REASONIX_THEME, + ); return { info: `theme saved: ${next}\nactive on next launch: ${active}` }; }; diff --git a/src/i18n/EN.ts b/src/i18n/EN.ts index 0ac9539..4fb77d0 100644 --- a/src/i18n/EN.ts +++ b/src/i18n/EN.ts @@ -487,7 +487,7 @@ export const EN: TranslationSchema = { footer: "↑↓ pick · ⏎ confirm · esc cancel", currentPref: "current preference", activeNow: "active now", - autoDesc: "use REASONIX_THEME or default", + autoDesc: "use CARBONCODE_THEME or default", }, planFlow: { approveCardTitle: "Approve plan", diff --git a/src/i18n/zh-CN.ts b/src/i18n/zh-CN.ts index 7afc2e3..a38f6f5 100644 --- a/src/i18n/zh-CN.ts +++ b/src/i18n/zh-CN.ts @@ -473,7 +473,7 @@ export const zhCN: TranslationSchema = { footer: "↑↓ 选择 · ⏎ 确认 · Esc 取消", currentPref: "当前偏好", activeNow: "当前生效", - autoDesc: "使用 REASONIX_THEME 或默认主题", + autoDesc: "使用 CARBONCODE_THEME 或默认主题", }, planFlow: { approveCardTitle: "确认计划", diff --git a/src/server/api/health.ts b/src/server/api/health.ts index 6583d11..d43fcea 100644 --- a/src/server/api/health.ts +++ b/src/server/api/health.ts @@ -1,9 +1,7 @@ import { existsSync, readdirSync, statSync } from "node:fs"; -import { homedir } from "node:os"; import { join } from "node:path"; -import { listSessions } from "../../memory/session.js"; import { VERSION } from "../../version.js"; -import type { DashboardContext } from "../context.js"; +import { type DashboardContext, resolveCarboncodeHome } from "../context.js"; import type { ApiResult } from "../router.js"; interface DirStat { @@ -56,6 +54,17 @@ function dirSize(path: string): DirStat { return { path, exists: true, fileCount, totalBytes }; } +function countSessionFiles(path: string): number { + if (!existsSync(path)) return 0; + try { + return readdirSync(path).filter( + (file) => file.endsWith(".jsonl") && !file.endsWith(".events.jsonl"), + ).length; + } catch { + return 0; + } +} + export async function handleHealth( method: string, _rest: string[], @@ -65,8 +74,7 @@ export async function handleHealth( if (method !== "GET") { return { status: 405, body: { error: "GET only" } }; } - const home = homedir(); - const carboncodeHome = join(home, ".carboncode"); + const carboncodeHome = resolveCarboncodeHome(ctx.configPath); const sessionsStat = dirSize(join(carboncodeHome, "sessions")); const memoryStat = dirSize(join(carboncodeHome, "memory")); @@ -81,8 +89,6 @@ export async function handleHealth( } } - const sessions = listSessions(); - return { status: 200, body: { @@ -91,7 +97,7 @@ export async function handleHealth( carboncodeHome, sessions: { path: sessionsStat.path, - count: sessions.length, + count: countSessionFiles(sessionsStat.path), totalBytes: sessionsStat.totalBytes, }, memory: { diff --git a/src/server/api/memory.ts b/src/server/api/memory.ts index e244ce5..28871d0 100644 --- a/src/server/api/memory.ts +++ b/src/server/api/memory.ts @@ -10,26 +10,25 @@ import { unlinkSync, writeFileSync, } from "node:fs"; -import { homedir } from "node:os"; import { basename, dirname, join, resolve as resolvePath } from "node:path"; import { PROJECT_MEMORY_FILE, findProjectMemoryPath, resolveProjectMemoryWritePath, } from "../../memory/project.js"; -import type { DashboardContext } from "../context.js"; +import { type DashboardContext, resolveCarboncodeHome } from "../context.js"; import type { ApiResult } from "../router.js"; function projectHash(rootDir: string): string { return createHash("sha1").update(resolvePath(rootDir)).digest("hex").slice(0, 16); } -function globalMemoryDir(): string { - return join(homedir(), ".carboncode", "memory", "global"); +function globalMemoryDir(carboncodeHome: string): string { + return join(carboncodeHome, "memory", "global"); } -function projectMemoryDir(rootDir: string): string { - return join(homedir(), ".carboncode", "memory", projectHash(rootDir)); +function projectMemoryDir(carboncodeHome: string, rootDir: string): string { + return join(carboncodeHome, "memory", projectHash(rootDir)); } interface WriteBody { @@ -74,8 +73,9 @@ export async function handleMemory( ctx: DashboardContext, ): Promise { const cwd = ctx.getCurrentCwd?.(); - const globalDir = globalMemoryDir(); - const projectMemDir = cwd ? projectMemoryDir(cwd) : ""; + const carboncodeHome = resolveCarboncodeHome(ctx.configPath); + const globalDir = globalMemoryDir(carboncodeHome); + const projectMemDir = cwd ? projectMemoryDir(carboncodeHome, cwd) : ""; if (method === "GET" && rest.length === 0) { const existingProjectMemory = cwd ? findProjectMemoryPath(cwd) : null; diff --git a/src/server/context.ts b/src/server/context.ts index 83fa7c8..50faefa 100644 --- a/src/server/context.ts +++ b/src/server/context.ts @@ -1,11 +1,20 @@ /** Callbacks (not refs) so endpoints read live loop state per request, not a frozen closure. */ +import { homedir } from "node:os"; +import { basename, dirname, join } from "node:path"; import type { McpServerSummary } from "../cli/ui/slash/types.js"; import type { EditMode } from "../config.js"; import type { CacheFirstLoop } from "../loop.js"; import type { ToolRegistry } from "../tools.js"; import type { JobRegistry } from "../tools/jobs.js"; +export function resolveCarboncodeHome(configPath: string): string { + if (!configPath.trim()) return join(homedir(), ".carboncode"); + const configDir = dirname(configPath); + if (basename(configDir).toLowerCase() === ".carboncode") return configDir; + return join(configDir, ".carboncode"); +} + export interface DashboardContext { /** Caller resolves via `defaultConfigPath()`; module deliberately avoids `homedir()` so tests can redirect. */ configPath: string; diff --git a/tests/cli-bare-routing.test.ts b/tests/cli-bare-routing.test.ts index 8c99a98..9556d7f 100644 --- a/tests/cli-bare-routing.test.ts +++ b/tests/cli-bare-routing.test.ts @@ -6,6 +6,7 @@ import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { writeConfig } from "../src/config.js"; +const HEAP_REEXEC_ENV = "REASONIX_HEAP_REEXEC"; const codeCommand = vi.fn(async () => {}); const chatCommand = vi.fn(async () => {}); const setupCommand = vi.fn(async () => {}); @@ -25,6 +26,7 @@ describe("bare CLI routing", () => { let cwd: string; const origHome = process.env.HOME; const origUserProfile = process.env.USERPROFILE; + const origHeapReexec = process.env[HEAP_REEXEC_ENV]; const origArgv = process.argv; const origCwd = process.cwd(); let stderr: ReturnType; @@ -38,6 +40,7 @@ describe("bare CLI routing", () => { cwd = realpathSync(mkdtempSync(join(tmpdir(), "carboncode-cli-cwd-"))); process.env.HOME = home; process.env.USERPROFILE = home; + process.env[HEAP_REEXEC_ENV] = "1"; process.chdir(cwd); codeCommand.mockClear(); chatCommand.mockClear(); @@ -63,6 +66,11 @@ describe("bare CLI routing", () => { } else { process.env.USERPROFILE = origUserProfile; } + if (origHeapReexec === undefined) { + delete process.env[HEAP_REEXEC_ENV]; + } else { + process.env[HEAP_REEXEC_ENV] = origHeapReexec; + } }); it("routes bare carboncode to code mode rooted at cwd", async () => { diff --git a/tests/helpers/codex-parity-harness.ts b/tests/helpers/codex-parity-harness.ts index 67e127c..2bb03c6 100644 --- a/tests/helpers/codex-parity-harness.ts +++ b/tests/helpers/codex-parity-harness.ts @@ -139,6 +139,7 @@ function runCommand( const child = spawn(command, [...args], { cwd, env: { ...process.env, CI: "1" }, + shell: process.platform === "win32", stdio: ["ignore", "pipe", "pipe"], }); const chunks: Buffer[] = []; diff --git a/tests/open-url.test.ts b/tests/open-url.test.ts new file mode 100644 index 0000000..4182e9b --- /dev/null +++ b/tests/open-url.test.ts @@ -0,0 +1,40 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { openUrl } from "../src/cli/ui/open-url.js"; + +describe("openUrl", () => { + const originalCi = process.env.CI; + const originalCarbonNoOpen = process.env.CARBONCODE_NO_OPEN; + const originalReasonixNoOpen = process.env.REASONIX_NO_OPEN; + + afterEach(() => { + if (originalCi === undefined) Reflect.deleteProperty(process.env, "CI"); + else process.env.CI = originalCi; + if (originalCarbonNoOpen === undefined) + Reflect.deleteProperty(process.env, "CARBONCODE_NO_OPEN"); + else process.env.CARBONCODE_NO_OPEN = originalCarbonNoOpen; + if (originalReasonixNoOpen === undefined) + Reflect.deleteProperty(process.env, "REASONIX_NO_OPEN"); + else process.env.REASONIX_NO_OPEN = originalReasonixNoOpen; + }); + + it("does not open URLs in CI", () => { + process.env.CI = "1"; + + expect(openUrl("https://example.com")).toEqual({ opened: false, reason: "ci" }); + }); + + it("respects CARBONCODE_NO_OPEN", () => { + Reflect.deleteProperty(process.env, "CI"); + process.env.CARBONCODE_NO_OPEN = "1"; + + expect(openUrl("https://example.com")).toEqual({ opened: false, reason: "disabled" }); + }); + + it("keeps legacy REASONIX_NO_OPEN compatibility", () => { + Reflect.deleteProperty(process.env, "CI"); + Reflect.deleteProperty(process.env, "CARBONCODE_NO_OPEN"); + process.env.REASONIX_NO_OPEN = "1"; + + expect(openUrl("https://example.com")).toEqual({ opened: false, reason: "disabled" }); + }); +}); diff --git a/tests/slash.test.ts b/tests/slash.test.ts index 135865b..4b45045 100644 --- a/tests/slash.test.ts +++ b/tests/slash.test.ts @@ -1231,21 +1231,29 @@ describe("handleSlash", () => { let tempHome: string; let originalHome: string | undefined; let originalUserProfile: string | undefined; + let originalCarbonTheme: string | undefined; let originalTheme: string | undefined; beforeEach(() => { tempHome = mkdtempSync(join(tmpdir(), "reasonix-theme-slash-")); originalHome = process.env.HOME; originalUserProfile = process.env.USERPROFILE; + originalCarbonTheme = process.env.CARBONCODE_THEME; originalTheme = process.env.REASONIX_THEME; process.env.HOME = tempHome; process.env.USERPROFILE = tempHome; + process.env.CARBONCODE_THEME = "github-dark"; process.env.REASONIX_THEME = "github-dark"; }); afterEach(() => { process.env.HOME = originalHome; process.env.USERPROFILE = originalUserProfile; + if (originalCarbonTheme === undefined) { + process.env.CARBONCODE_THEME = undefined; + } else { + process.env.CARBONCODE_THEME = originalCarbonTheme; + } if (originalTheme === undefined) { process.env.REASONIX_THEME = undefined; } else { @@ -1273,6 +1281,16 @@ describe("handleSlash", () => { expect(loadTheme()).toBe("auto"); }); + it("prefers CARBONCODE_THEME over legacy REASONIX_THEME", () => { + process.env.CARBONCODE_THEME = "tokyo-night"; + process.env.REASONIX_THEME = "github-light"; + + const r = handleSlash("theme", ["auto"], makeLoop()); + + expect(r.info).toMatch(/active on next launch: tokyo-night/); + expect(loadTheme()).toBe("auto"); + }); + it("rejects unknown theme names", () => { const r = handleSlash("theme", ["solarized"], makeLoop()); expect(r.info).toMatch(/unknown theme: solarized/); diff --git a/tests/startup-profile.test.ts b/tests/startup-profile.test.ts index e78c17e..82ea12c 100644 --- a/tests/startup-profile.test.ts +++ b/tests/startup-profile.test.ts @@ -19,19 +19,25 @@ function makeSink(): { stream: NodeJS.WriteStream; output: () => string } { } describe("startup-profile", () => { - const original = process.env.REASONIX_PROFILE_STARTUP; + const originalCarbon = process.env.CARBONCODE_PROFILE_STARTUP; + const originalReasonix = process.env.REASONIX_PROFILE_STARTUP; beforeEach(() => { _resetForTests(); }); afterEach(() => { - if (original === undefined) Reflect.deleteProperty(process.env, "REASONIX_PROFILE_STARTUP"); - else process.env.REASONIX_PROFILE_STARTUP = original; + if (originalCarbon === undefined) + Reflect.deleteProperty(process.env, "CARBONCODE_PROFILE_STARTUP"); + else process.env.CARBONCODE_PROFILE_STARTUP = originalCarbon; + if (originalReasonix === undefined) + Reflect.deleteProperty(process.env, "REASONIX_PROFILE_STARTUP"); + else process.env.REASONIX_PROFILE_STARTUP = originalReasonix; _resetForTests(); }); it("is disabled by default — markPhase + dumpStartupProfile are no-ops", () => { + Reflect.deleteProperty(process.env, "CARBONCODE_PROFILE_STARTUP"); Reflect.deleteProperty(process.env, "REASONIX_PROFILE_STARTUP"); expect(isStartupProfileEnabled()).toBe(false); markPhase("a"); @@ -43,15 +49,24 @@ describe("startup-profile", () => { it("recognizes 1 / true / yes as enable values", () => { for (const v of ["1", "true", "yes"]) { - process.env.REASONIX_PROFILE_STARTUP = v; + process.env.CARBONCODE_PROFILE_STARTUP = v; expect(isStartupProfileEnabled()).toBe(true); } - process.env.REASONIX_PROFILE_STARTUP = "0"; + process.env.CARBONCODE_PROFILE_STARTUP = "0"; expect(isStartupProfileEnabled()).toBe(false); }); - it("emits a formatted profile when enabled, with cumulative + delta per phase", () => { + it("prefers CARBONCODE_PROFILE_STARTUP over legacy REASONIX_PROFILE_STARTUP", () => { + process.env.CARBONCODE_PROFILE_STARTUP = "0"; process.env.REASONIX_PROFILE_STARTUP = "1"; + expect(isStartupProfileEnabled()).toBe(false); + + Reflect.deleteProperty(process.env, "CARBONCODE_PROFILE_STARTUP"); + expect(isStartupProfileEnabled()).toBe(true); + }); + + it("emits a formatted profile when enabled, with cumulative + delta per phase", () => { + process.env.CARBONCODE_PROFILE_STARTUP = "1"; markPhase("first"); markPhase("second"); markPhase("third"); @@ -64,10 +79,11 @@ describe("startup-profile", () => { expect(out).toContain("third"); expect(out).toMatch(/total/); expect(out).toMatch(/last phase third/); + expect(out).toContain("CARBONCODE_PROFILE_STARTUP=0"); }); it("dumpStartupProfile is idempotent — second call is silent", () => { - process.env.REASONIX_PROFILE_STARTUP = "1"; + process.env.CARBONCODE_PROFILE_STARTUP = "1"; markPhase("only"); const sink1 = makeSink(); dumpStartupProfile(sink1.stream); @@ -80,14 +96,14 @@ describe("startup-profile", () => { }); it("emits nothing when enabled but no phases were marked", () => { - process.env.REASONIX_PROFILE_STARTUP = "1"; + process.env.CARBONCODE_PROFILE_STARTUP = "1"; const sink = makeSink(); dumpStartupProfile(sink.stream); expect(sink.output()).toBe(""); }); it("each line shows ms + phase name + (+delta) suffix", () => { - process.env.REASONIX_PROFILE_STARTUP = "1"; + process.env.CARBONCODE_PROFILE_STARTUP = "1"; markPhase("alpha"); markPhase("beta"); const sink = makeSink();