codealmanac 0.1.10 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -104
- package/dist/agents-A4II4YJC.js +15 -0
- package/dist/auth-S5DVUIUJ.js +18 -0
- package/dist/{chunk-Z4MWLVS2.js → chunk-447U3GQJ.js} +162 -5
- package/dist/chunk-447U3GQJ.js.map +1 -0
- package/dist/{chunk-QLHJP2XK.js → chunk-B2AGSRXL.js} +13 -9
- package/dist/{chunk-QLHJP2XK.js.map → chunk-B2AGSRXL.js.map} +1 -1
- package/dist/{chunk-AXFPUHBN.js → chunk-F53U6JQG.js} +8 -49
- package/dist/chunk-F53U6JQG.js.map +1 -0
- package/dist/{chunk-3C5SY5SE.js → chunk-KQUVMF27.js} +5 -2
- package/dist/chunk-KQUVMF27.js.map +1 -0
- package/dist/{chunk-BJVZLP6O.js → chunk-MX2EW5MR.js} +3 -3
- package/dist/{chunk-Z6MBJ3D2.js → chunk-QQHIVTXT.js} +6 -4
- package/dist/{chunk-Z6MBJ3D2.js.map → chunk-QQHIVTXT.js.map} +1 -1
- package/dist/chunk-R3URPHGH.js +194 -0
- package/dist/chunk-R3URPHGH.js.map +1 -0
- package/dist/chunk-SSYMRT4I.js +126 -0
- package/dist/chunk-SSYMRT4I.js.map +1 -0
- package/dist/{chunk-QHQ6YH7U.js → chunk-V3QOQSXI.js} +5 -3
- package/dist/{chunk-QHQ6YH7U.js.map → chunk-V3QOQSXI.js.map} +1 -1
- package/dist/chunk-WRUSDYYE.js +97 -0
- package/dist/chunk-WRUSDYYE.js.map +1 -0
- package/dist/{chunk-3LC55TG6.js → chunk-ZDJSJIB6.js} +77 -126
- package/dist/chunk-ZDJSJIB6.js.map +1 -0
- package/dist/{cli-W3OYVJYH.js → cli-MZEXRV6E.js} +238 -24
- package/dist/cli-MZEXRV6E.js.map +1 -0
- package/dist/codealmanac.js +1 -1
- package/dist/doctor-3BYSF3JD.js +17 -0
- package/dist/{hook-CRJMWSSO.js → hook-2NP3UE7U.js} +2 -2
- package/dist/{register-commands-JHC2OFKM.js → register-commands-DPH4ZWEE.js} +621 -60
- package/dist/register-commands-DPH4ZWEE.js.map +1 -0
- package/dist/uninstall-FDIOBAAR.js +15 -0
- package/dist/uninstall-FDIOBAAR.js.map +1 -0
- package/dist/update-RAF7QRYF.js +11 -0
- package/dist/update-RAF7QRYF.js.map +1 -0
- package/dist/{wiki-IPSRRGOT.js → wiki-IGNRNLUZ.js} +2 -2
- package/hooks/almanac-capture.sh +40 -7
- package/package.json +3 -2
- package/dist/chunk-3C5SY5SE.js.map +0 -1
- package/dist/chunk-3LC55TG6.js.map +0 -1
- package/dist/chunk-AXFPUHBN.js.map +0 -1
- package/dist/chunk-Z4MWLVS2.js.map +0 -1
- package/dist/cli-W3OYVJYH.js.map +0 -1
- package/dist/doctor-ODFNJUKH.js +0 -15
- package/dist/register-commands-JHC2OFKM.js.map +0 -1
- package/dist/uninstall-HE2Z2LN2.js +0 -12
- package/dist/update-IL243I4E.js +0 -10
- /package/dist/{doctor-ODFNJUKH.js.map → agents-A4II4YJC.js.map} +0 -0
- /package/dist/{hook-CRJMWSSO.js.map → auth-S5DVUIUJ.js.map} +0 -0
- /package/dist/{chunk-BJVZLP6O.js.map → chunk-MX2EW5MR.js.map} +0 -0
- /package/dist/{uninstall-HE2Z2LN2.js.map → doctor-3BYSF3JD.js.map} +0 -0
- /package/dist/{update-IL243I4E.js.map → hook-2NP3UE7U.js.map} +0 -0
- /package/dist/{wiki-IPSRRGOT.js.map → wiki-IGNRNLUZ.js.map} +0 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/agent/auth.ts
|
|
4
|
+
import { spawn, spawnSync } from "child_process";
|
|
5
|
+
import { createRequire } from "module";
|
|
6
|
+
import { dirname, join } from "path";
|
|
7
|
+
var AUTH_TIMEOUT_MS = 1e4;
|
|
8
|
+
function resolveClaudeExecutable() {
|
|
9
|
+
const result = spawnSync("sh", ["-lc", "command -v claude"], {
|
|
10
|
+
encoding: "utf8"
|
|
11
|
+
});
|
|
12
|
+
if (result.status !== 0) return void 0;
|
|
13
|
+
const found = result.stdout.trim().split("\n")[0]?.trim();
|
|
14
|
+
return found !== void 0 && found.length > 0 ? found : void 0;
|
|
15
|
+
}
|
|
16
|
+
function resolveCliJsPath() {
|
|
17
|
+
const require2 = createRequire(import.meta.url);
|
|
18
|
+
const entry = require2.resolve("@anthropic-ai/claude-agent-sdk");
|
|
19
|
+
return join(dirname(entry), "cli.js");
|
|
20
|
+
}
|
|
21
|
+
var defaultSpawnCli = (args) => {
|
|
22
|
+
const command = resolveClaudeExecutable() ?? "claude";
|
|
23
|
+
const child = spawn(command, args, {
|
|
24
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
25
|
+
});
|
|
26
|
+
return child;
|
|
27
|
+
};
|
|
28
|
+
var legacySdkSpawnCli = (args) => {
|
|
29
|
+
const cliPath = resolveCliJsPath();
|
|
30
|
+
const child = spawn(process.execPath, [cliPath, ...args], {
|
|
31
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
32
|
+
});
|
|
33
|
+
return child;
|
|
34
|
+
};
|
|
35
|
+
async function checkClaudeAuth(spawnCli = defaultSpawnCli) {
|
|
36
|
+
if (spawnCli === defaultSpawnCli) {
|
|
37
|
+
const status = await checkClaudeAuthWith(defaultSpawnCli);
|
|
38
|
+
if (status.loggedIn) return status;
|
|
39
|
+
return await checkClaudeAuthWith(legacySdkSpawnCli);
|
|
40
|
+
}
|
|
41
|
+
return await checkClaudeAuthWith(spawnCli);
|
|
42
|
+
}
|
|
43
|
+
async function checkClaudeAuthWith(spawnCli) {
|
|
44
|
+
let child;
|
|
45
|
+
try {
|
|
46
|
+
child = spawnCli(["auth", "status", "--json"]);
|
|
47
|
+
} catch {
|
|
48
|
+
return { loggedIn: false };
|
|
49
|
+
}
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
let stdout = "";
|
|
52
|
+
let stderr = "";
|
|
53
|
+
let settled = false;
|
|
54
|
+
const settle = (value) => {
|
|
55
|
+
if (settled) return;
|
|
56
|
+
settled = true;
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
resolve(value);
|
|
59
|
+
};
|
|
60
|
+
const timer = setTimeout(() => {
|
|
61
|
+
try {
|
|
62
|
+
child.kill("SIGTERM");
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
settle({ loggedIn: false });
|
|
66
|
+
}, AUTH_TIMEOUT_MS);
|
|
67
|
+
child.stdout.on("data", (data) => {
|
|
68
|
+
stdout += data.toString();
|
|
69
|
+
});
|
|
70
|
+
child.stderr.on("data", (data) => {
|
|
71
|
+
stderr += data.toString();
|
|
72
|
+
});
|
|
73
|
+
child.on("error", () => {
|
|
74
|
+
settle({ loggedIn: false });
|
|
75
|
+
});
|
|
76
|
+
child.on("close", (code) => {
|
|
77
|
+
if (code !== 0 && stdout.trim().length === 0) {
|
|
78
|
+
void stderr;
|
|
79
|
+
settle({ loggedIn: false });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
settle(parseClaudeAuthStatus(stdout.trim()));
|
|
84
|
+
} catch {
|
|
85
|
+
settle({ loggedIn: false });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function parseClaudeAuthStatus(raw) {
|
|
91
|
+
const parsed = JSON.parse(raw);
|
|
92
|
+
const loggedIn = parsed.loggedIn === true;
|
|
93
|
+
const out = { loggedIn };
|
|
94
|
+
if (typeof parsed.email === "string") out.email = parsed.email;
|
|
95
|
+
if (typeof parsed.subscriptionType === "string") {
|
|
96
|
+
out.subscriptionType = parsed.subscriptionType;
|
|
97
|
+
}
|
|
98
|
+
if (typeof parsed.authMethod === "string") {
|
|
99
|
+
out.authMethod = parsed.authMethod;
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
var UNAUTHENTICATED_MESSAGE = "not authenticated to Claude.\n\nOption 1 \u2014 use your Claude subscription (Pro/Max):\n claude auth login --claudeai\n\nOption 2 \u2014 use a pay-per-token API key:\n Get one at https://console.anthropic.com\n export ANTHROPIC_API_KEY=sk-ant-...\n\nVerify with: claude auth status";
|
|
104
|
+
async function assertClaudeAuth(spawnCli = defaultSpawnCli) {
|
|
105
|
+
const status = await checkClaudeAuth(spawnCli);
|
|
106
|
+
if (status.loggedIn) {
|
|
107
|
+
return status;
|
|
108
|
+
}
|
|
109
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
110
|
+
if (apiKey !== void 0 && apiKey.length > 0) {
|
|
111
|
+
return { loggedIn: true, authMethod: "apiKey" };
|
|
112
|
+
}
|
|
113
|
+
const err = new Error(UNAUTHENTICATED_MESSAGE);
|
|
114
|
+
err.code = "CLAUDE_AUTH_MISSING";
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export {
|
|
119
|
+
resolveClaudeExecutable,
|
|
120
|
+
defaultSpawnCli,
|
|
121
|
+
legacySdkSpawnCli,
|
|
122
|
+
checkClaudeAuth,
|
|
123
|
+
UNAUTHENTICATED_MESSAGE,
|
|
124
|
+
assertClaudeAuth
|
|
125
|
+
};
|
|
126
|
+
//# sourceMappingURL=chunk-SSYMRT4I.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/agent/auth.ts"],"sourcesContent":["import { spawn, spawnSync, type ChildProcess } from \"node:child_process\";\nimport { createRequire } from \"node:module\";\nimport { dirname, join } from \"node:path\";\n\n/**\n * Claude auth gate — accepts either an active Claude subscription login\n * OR an `ANTHROPIC_API_KEY` environment variable.\n *\n * Claude Code owns subscription OAuth credentials. Users who are logged in\n * via `claude auth login --claudeai` should be able to run bootstrap/capture\n * without exporting an API key. Conversely, users on pay-per-token API keys\n * shouldn't be required to go through the OAuth flow.\n *\n * Current Claude Agent SDK packages no longer ship the old private\n * `cli.js` entrypoint, so the primary probe is the public Claude Code CLI:\n * `claude auth status --json`. We keep the SDK `cli.js` probe as a legacy\n * fallback for older SDK layouts.\n */\n\nexport interface ClaudeAuthStatus {\n loggedIn: boolean;\n email?: string;\n subscriptionType?: string;\n authMethod?: string;\n}\n\nexport interface SpawnedProcess {\n stdout: { on: (event: \"data\", cb: (data: Buffer | string) => void) => void };\n stderr: { on: (event: \"data\", cb: (data: Buffer | string) => void) => void };\n on: (event: \"close\" | \"error\", cb: (arg: number | null | Error) => void) => void;\n kill: (signal?: string) => void;\n}\n\n/**\n * The subprocess spawner is injectable so tests can replace it with a\n * fake that emits canned JSON without touching the filesystem.\n */\nexport type SpawnCliFn = (args: string[]) => SpawnedProcess;\n\nconst AUTH_TIMEOUT_MS = 10_000;\n\n/**\n * Resolve the installed Claude Code executable from PATH. The Agent SDK can\n * accept this path via `pathToClaudeCodeExecutable`, and the auth probe uses\n * the same binary so CodeAlmanac agrees with `claude auth status`.\n */\nexport function resolveClaudeExecutable(): string | undefined {\n const result = spawnSync(\"sh\", [\"-lc\", \"command -v claude\"], {\n encoding: \"utf8\",\n });\n if (result.status !== 0) return undefined;\n const found = result.stdout.trim().split(\"\\n\")[0]?.trim();\n return found !== undefined && found.length > 0 ? found : undefined;\n}\n\n/**\n * Resolve legacy `cli.js` from older `@anthropic-ai/claude-agent-sdk`\n * installs. SDK 0.2.129+ no longer ships this file; callers must treat\n * failure as expected and fall back to the public `claude` binary.\n */\nfunction resolveCliJsPath(): string {\n const require = createRequire(import.meta.url);\n const entry = require.resolve(\"@anthropic-ai/claude-agent-sdk\");\n return join(dirname(entry), \"cli.js\");\n}\n\n/**\n * Default subprocess spawner for production use — invokes the installed\n * Claude Code CLI.\n */\nexport const defaultSpawnCli: SpawnCliFn = (args: string[]) => {\n const command = resolveClaudeExecutable() ?? \"claude\";\n const child = spawn(command, args, {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n return child as unknown as SpawnedProcess;\n};\n\nexport const legacySdkSpawnCli: SpawnCliFn = (args: string[]) => {\n const cliPath = resolveCliJsPath();\n const child = spawn(process.execPath, [cliPath, ...args], {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n return child as unknown as SpawnedProcess;\n};\n\n/**\n * Check whether the user is authenticated via Claude subscription OAuth.\n *\n * Spawns `claude auth status --json`, falling back to the legacy SDK CLI\n * layout when available. On any failure (spawn error, non-JSON stdout,\n * non-zero exit, timeout) we return `{ loggedIn: false }` rather than\n * propagating the error — the caller will fall back to the\n * `ANTHROPIC_API_KEY` path and, if that's also missing, produce a clean\n * two-option error message.\n *\n * The 10s timeout guards against the CLI hanging on a broken network or\n * keychain prompt. In practice `auth status` is a cheap local read.\n */\nexport async function checkClaudeAuth(\n spawnCli: SpawnCliFn = defaultSpawnCli,\n): Promise<ClaudeAuthStatus> {\n if (spawnCli === defaultSpawnCli) {\n const status = await checkClaudeAuthWith(defaultSpawnCli);\n if (status.loggedIn) return status;\n return await checkClaudeAuthWith(legacySdkSpawnCli);\n }\n return await checkClaudeAuthWith(spawnCli);\n}\n\nasync function checkClaudeAuthWith(\n spawnCli: SpawnCliFn,\n): Promise<ClaudeAuthStatus> {\n let child: SpawnedProcess;\n try {\n child = spawnCli([\"auth\", \"status\", \"--json\"]);\n } catch {\n return { loggedIn: false };\n }\n\n return new Promise<ClaudeAuthStatus>((resolve) => {\n let stdout = \"\";\n let stderr = \"\";\n let settled = false;\n\n const settle = (value: ClaudeAuthStatus): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve(value);\n };\n\n const timer = setTimeout(() => {\n try {\n child.kill(\"SIGTERM\");\n } catch {\n // Kill can fail if the process already exited; nothing we can do.\n }\n settle({ loggedIn: false });\n }, AUTH_TIMEOUT_MS);\n\n child.stdout.on(\"data\", (data) => {\n stdout += data.toString();\n });\n child.stderr.on(\"data\", (data) => {\n stderr += data.toString();\n });\n\n child.on(\"error\", () => {\n settle({ loggedIn: false });\n });\n\n child.on(\"close\", (code) => {\n // The SDK writes `{\"loggedIn\": false, ...}` to stdout with a zero\n // exit code when the user isn't signed in, so we only reject on\n // non-zero + empty stdout. An empty stdout with zero exit (shouldn't\n // happen in practice) also fails safely to `loggedIn: false`.\n if (code !== 0 && stdout.trim().length === 0) {\n // `stderr` isn't surfaced to the user here — the caller's error\n // message covers both auth paths — but it would be captured by\n // `stderr` if we ever wanted to log it for debugging.\n void stderr;\n settle({ loggedIn: false });\n return;\n }\n try {\n settle(parseClaudeAuthStatus(stdout.trim()));\n } catch {\n settle({ loggedIn: false });\n }\n });\n });\n}\n\nfunction parseClaudeAuthStatus(raw: string): ClaudeAuthStatus {\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n const loggedIn = parsed.loggedIn === true;\n const out: ClaudeAuthStatus = { loggedIn };\n if (typeof parsed.email === \"string\") out.email = parsed.email;\n if (typeof parsed.subscriptionType === \"string\") {\n out.subscriptionType = parsed.subscriptionType;\n }\n if (typeof parsed.authMethod === \"string\") {\n out.authMethod = parsed.authMethod;\n }\n return out;\n}\n\n/**\n * Human-readable error when neither auth path is available. The text is\n * deliberately verbose — users hitting this wall for the first time\n * deserve both options in front of them, not a terse hint.\n */\nexport const UNAUTHENTICATED_MESSAGE =\n \"not authenticated to Claude.\\n\\n\" +\n \"Option 1 — use your Claude subscription (Pro/Max):\\n\" +\n \" claude auth login --claudeai\\n\\n\" +\n \"Option 2 — use a pay-per-token API key:\\n\" +\n \" Get one at https://console.anthropic.com\\n\" +\n \" export ANTHROPIC_API_KEY=sk-ant-...\\n\\n\" +\n \"Verify with: claude auth status\";\n\n/**\n * Assert that at least one auth path is satisfied. Prefers subscription\n * auth (fewer surprises for Claude Pro/Max users) but accepts\n * `ANTHROPIC_API_KEY` as a fallback. On failure throws with\n * `code = \"CLAUDE_AUTH_MISSING\"` so callers can distinguish this from\n * other errors if they ever want to.\n *\n * Returns the resolved auth status so callers that want to display the\n * logged-in email in a preamble can do so without a second subprocess.\n */\nexport async function assertClaudeAuth(\n spawnCli: SpawnCliFn = defaultSpawnCli,\n): Promise<ClaudeAuthStatus> {\n const status = await checkClaudeAuth(spawnCli);\n if (status.loggedIn) {\n return status;\n }\n const apiKey = process.env.ANTHROPIC_API_KEY;\n if (apiKey !== undefined && apiKey.length > 0) {\n // Signal to callers that we're on the API-key path. Not \"loggedIn\"\n // in the OAuth sense, but the SDK will pick up the env var and\n // succeed — so we return a status that tells bootstrap/capture the\n // gate is open.\n return { loggedIn: true, authMethod: \"apiKey\" };\n }\n const err = new Error(UNAUTHENTICATED_MESSAGE);\n (err as { code?: string }).code = \"CLAUDE_AUTH_MISSING\";\n throw err;\n}\n\n// Internal re-export — helps keep the public type surface minimal while\n// still letting tests import the `ChildProcess` shape when needed.\nexport type { ChildProcess };\n"],"mappings":"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","names":["require"]}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
checkForUpdate,
|
|
4
|
-
getConfigPath,
|
|
5
4
|
getStatePath
|
|
6
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-F53U6JQG.js";
|
|
6
|
+
import {
|
|
7
|
+
getConfigPath
|
|
8
|
+
} from "./chunk-WRUSDYYE.js";
|
|
7
9
|
|
|
8
10
|
// src/update/schedule.ts
|
|
9
11
|
import { spawn } from "child_process";
|
|
@@ -78,4 +80,4 @@ export {
|
|
|
78
80
|
runInternalUpdateCheck,
|
|
79
81
|
readStateForDoctor
|
|
80
82
|
};
|
|
81
|
-
//# sourceMappingURL=chunk-
|
|
83
|
+
//# sourceMappingURL=chunk-V3QOQSXI.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/update/schedule.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { readFileSync } from \"node:fs\";\n\nimport { checkForUpdate } from \"./check.js\";\nimport { getConfigPath } from \"./config.js\";\nimport { getStatePath, type UpdateState } from \"./state.js\";\n\n/**\n * Post-command scheduler for the background update check.\n *\n * After any normal `almanac <command>` exits, we want a fresh check to\n * have happened by the next invocation. We achieve that by spawning a\n * detached copy of ourselves with the hidden `--internal-check-updates`\n * flag; that child does nothing but hit the registry and write\n * `~/.almanac/update-state.json`, then exits.\n *\n * Why detach rather than check inline:\n * - 3s network timeout in the foreground would feel sluggish on every\n * command.\n * - `npm test` and CI scripts shouldn't pay for a registry round-trip\n * (gated below via env).\n * - A detached child with `stdio: \"ignore\"` cannot leak output into\n * the parent's stdout/stderr — critical for pipelines.\n *\n * Hazards we accept:\n * - A Claude Code subprocess whose parent shell exits right after the\n * `almanac` call may kill the child before it finishes. That's\n * fine: a failed check just means we try again next invocation.\n * - Detached child survival on Windows isn't as robust as on Unix.\n * Same fallback: next invocation retries.\n */\n\nexport function scheduleBackgroundUpdateCheck(argv: string[]): void {\n if (!shouldSchedule(argv)) return;\n\n const scriptPath = argv[1];\n const nodeBin = process.execPath;\n if (scriptPath === undefined || scriptPath.length === 0) return;\n\n // Spawn with the current Node and the same script path. `detached:\n // true` + `stdio: \"ignore\"` + `unref()` detaches the child from our\n // event loop so the parent can exit independently.\n try {\n const child = spawn(\n nodeBin,\n [scriptPath, \"--internal-check-updates\"],\n {\n detached: true,\n stdio: \"ignore\",\n // Windows: with `detached: true` and no `stdio`, Node opens a\n // console window — `\"ignore\"` prevents that.\n },\n );\n child.unref();\n // Swallow any synchronous spawn errors (e.g. ENOENT in strange\n // installs) — never propagate to the foreground command.\n child.on(\"error\", () => {});\n } catch {\n // Last-resort swallow: background checks are best-effort.\n }\n}\n\n/**\n * Should we spawn the worker at all?\n *\n * - Respect the `update_notifier` config — no banner means no need\n * for the data that feeds it.\n * - Skip in test environments so `npm test` doesn't fork 300 copies\n * of itself into the background and hammer the registry.\n * - Skip on the worker invocation itself (prevents a fork bomb).\n * - Skip when the user doesn't own the install path (permission\n * weirdness) — detected by `~/.almanac` mkdir failing; simplest\n * to just rely on the worker's own error handling, so we don't\n * gate here.\n * - Skip when the argv contains `--help`/`--version`/nothing — these\n * commands are often run from scripts that care about clean exit;\n * though the inline banner still shows, we don't kick off a check.\n */\nfunction shouldSchedule(argv: string[]): boolean {\n if (process.env.CODEALMANAC_SKIP_UPDATE_CHECK === \"1\") return false;\n if (process.env.NODE_ENV === \"test\") return false;\n if (process.env.VITEST !== undefined) return false;\n\n // Already the worker. argv[2..] contains the internal flag.\n if (argv.slice(2).includes(\"--internal-check-updates\")) return false;\n\n if (!notifierEnabled()) return false;\n\n return true;\n}\n\nfunction notifierEnabled(): boolean {\n try {\n const raw = readFileSync(getConfigPath(), \"utf8\");\n const parsed = JSON.parse(raw) as { update_notifier?: unknown };\n if (parsed.update_notifier === false) return false;\n return true;\n } catch {\n return true; // missing / malformed → default-on\n }\n}\n\n/**\n * The worker body. Invoked when `--internal-check-updates` appears on\n * the argv. Must be fast and must never print: the parent spawned us\n * with `stdio: \"ignore\"` but a stray write could still surprise a\n * downstream debugger.\n *\n * We take a simple file lock at `~/.almanac/.update-check.lock` to\n * prevent two workers running at the same time (which could happen if\n * the user fires several commands in parallel). The lock is just the\n * existence of the file with our PID inside; if an existing lock is\n * stale (older than the 3s + cache-write budget), we steal it.\n */\nexport async function runInternalUpdateCheck(): Promise<void> {\n // The worker is intentionally minimal. Any error (network, fs,\n // JSON) is handled inside `checkForUpdate` and surfaces as a\n // swallowed return; we just need to await it and exit.\n try {\n await checkForUpdate({});\n } catch {\n // Defense-in-depth: nothing must escape the worker.\n }\n}\n\n/**\n * Read the current state snapshot for diagnostic surfaces (doctor, the\n * `update --check` command). Wraps the sync read so callers can grab\n * state without the `async readState` ceremony.\n */\nexport function readStateForDoctor(path?: string): UpdateState | null {\n const file = path ?? getStatePath();\n try {\n const raw = readFileSync(file, \"utf8\");\n const trimmed = raw.trim();\n if (trimmed.length === 0) return null;\n const parsed = JSON.parse(trimmed) as Partial<UpdateState>;\n return {\n last_check_at:\n typeof parsed.last_check_at === \"number\" ? parsed.last_check_at : 0,\n installed_version:\n typeof parsed.installed_version === \"string\"\n ? parsed.installed_version\n : \"\",\n latest_version:\n typeof parsed.latest_version === \"string\" ? parsed.latest_version : \"\",\n dismissed_versions: Array.isArray(parsed.dismissed_versions)\n ? parsed.dismissed_versions.filter((v): v is string => typeof v === \"string\")\n : [],\n last_fetch_failed_at:\n typeof parsed.last_fetch_failed_at === \"number\"\n ? parsed.last_fetch_failed_at\n : undefined,\n };\n } catch {\n return null;\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/update/schedule.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { readFileSync } from \"node:fs\";\n\nimport { checkForUpdate } from \"./check.js\";\nimport { getConfigPath } from \"./config.js\";\nimport { getStatePath, type UpdateState } from \"./state.js\";\n\n/**\n * Post-command scheduler for the background update check.\n *\n * After any normal `almanac <command>` exits, we want a fresh check to\n * have happened by the next invocation. We achieve that by spawning a\n * detached copy of ourselves with the hidden `--internal-check-updates`\n * flag; that child does nothing but hit the registry and write\n * `~/.almanac/update-state.json`, then exits.\n *\n * Why detach rather than check inline:\n * - 3s network timeout in the foreground would feel sluggish on every\n * command.\n * - `npm test` and CI scripts shouldn't pay for a registry round-trip\n * (gated below via env).\n * - A detached child with `stdio: \"ignore\"` cannot leak output into\n * the parent's stdout/stderr — critical for pipelines.\n *\n * Hazards we accept:\n * - A Claude Code subprocess whose parent shell exits right after the\n * `almanac` call may kill the child before it finishes. That's\n * fine: a failed check just means we try again next invocation.\n * - Detached child survival on Windows isn't as robust as on Unix.\n * Same fallback: next invocation retries.\n */\n\nexport function scheduleBackgroundUpdateCheck(argv: string[]): void {\n if (!shouldSchedule(argv)) return;\n\n const scriptPath = argv[1];\n const nodeBin = process.execPath;\n if (scriptPath === undefined || scriptPath.length === 0) return;\n\n // Spawn with the current Node and the same script path. `detached:\n // true` + `stdio: \"ignore\"` + `unref()` detaches the child from our\n // event loop so the parent can exit independently.\n try {\n const child = spawn(\n nodeBin,\n [scriptPath, \"--internal-check-updates\"],\n {\n detached: true,\n stdio: \"ignore\",\n // Windows: with `detached: true` and no `stdio`, Node opens a\n // console window — `\"ignore\"` prevents that.\n },\n );\n child.unref();\n // Swallow any synchronous spawn errors (e.g. ENOENT in strange\n // installs) — never propagate to the foreground command.\n child.on(\"error\", () => {});\n } catch {\n // Last-resort swallow: background checks are best-effort.\n }\n}\n\n/**\n * Should we spawn the worker at all?\n *\n * - Respect the `update_notifier` config — no banner means no need\n * for the data that feeds it.\n * - Skip in test environments so `npm test` doesn't fork 300 copies\n * of itself into the background and hammer the registry.\n * - Skip on the worker invocation itself (prevents a fork bomb).\n * - Skip when the user doesn't own the install path (permission\n * weirdness) — detected by `~/.almanac` mkdir failing; simplest\n * to just rely on the worker's own error handling, so we don't\n * gate here.\n * - Skip when the argv contains `--help`/`--version`/nothing — these\n * commands are often run from scripts that care about clean exit;\n * though the inline banner still shows, we don't kick off a check.\n */\nfunction shouldSchedule(argv: string[]): boolean {\n if (process.env.CODEALMANAC_SKIP_UPDATE_CHECK === \"1\") return false;\n if (process.env.NODE_ENV === \"test\") return false;\n if (process.env.VITEST !== undefined) return false;\n\n // Already the worker. argv[2..] contains the internal flag.\n if (argv.slice(2).includes(\"--internal-check-updates\")) return false;\n\n if (!notifierEnabled()) return false;\n\n return true;\n}\n\nfunction notifierEnabled(): boolean {\n try {\n const raw = readFileSync(getConfigPath(), \"utf8\");\n const parsed = JSON.parse(raw) as { update_notifier?: unknown };\n if (parsed.update_notifier === false) return false;\n return true;\n } catch {\n return true; // missing / malformed → default-on\n }\n}\n\n/**\n * The worker body. Invoked when `--internal-check-updates` appears on\n * the argv. Must be fast and must never print: the parent spawned us\n * with `stdio: \"ignore\"` but a stray write could still surprise a\n * downstream debugger.\n *\n * We take a simple file lock at `~/.almanac/.update-check.lock` to\n * prevent two workers running at the same time (which could happen if\n * the user fires several commands in parallel). The lock is just the\n * existence of the file with our PID inside; if an existing lock is\n * stale (older than the 3s + cache-write budget), we steal it.\n */\nexport async function runInternalUpdateCheck(): Promise<void> {\n // The worker is intentionally minimal. Any error (network, fs,\n // JSON) is handled inside `checkForUpdate` and surfaces as a\n // swallowed return; we just need to await it and exit.\n try {\n await checkForUpdate({});\n } catch {\n // Defense-in-depth: nothing must escape the worker.\n }\n}\n\n/**\n * Read the current state snapshot for diagnostic surfaces (doctor, the\n * `update --check` command). Wraps the sync read so callers can grab\n * state without the `async readState` ceremony.\n */\nexport function readStateForDoctor(path?: string): UpdateState | null {\n const file = path ?? getStatePath();\n try {\n const raw = readFileSync(file, \"utf8\");\n const trimmed = raw.trim();\n if (trimmed.length === 0) return null;\n const parsed = JSON.parse(trimmed) as Partial<UpdateState>;\n return {\n last_check_at:\n typeof parsed.last_check_at === \"number\" ? parsed.last_check_at : 0,\n installed_version:\n typeof parsed.installed_version === \"string\"\n ? parsed.installed_version\n : \"\",\n latest_version:\n typeof parsed.latest_version === \"string\" ? parsed.latest_version : \"\",\n dismissed_versions: Array.isArray(parsed.dismissed_versions)\n ? parsed.dismissed_versions.filter((v): v is string => typeof v === \"string\")\n : [],\n last_fetch_failed_at:\n typeof parsed.last_fetch_failed_at === \"number\"\n ? parsed.last_fetch_failed_at\n : undefined,\n };\n } catch {\n return null;\n }\n}\n"],"mappings":";;;;;;;;;;AAAA,SAAS,aAAa;AACtB,SAAS,oBAAoB;AA+BtB,SAAS,8BAA8B,MAAsB;AAClE,MAAI,CAAC,eAAe,IAAI,EAAG;AAE3B,QAAM,aAAa,KAAK,CAAC;AACzB,QAAM,UAAU,QAAQ;AACxB,MAAI,eAAe,UAAa,WAAW,WAAW,EAAG;AAKzD,MAAI;AACF,UAAM,QAAQ;AAAA,MACZ;AAAA,MACA,CAAC,YAAY,0BAA0B;AAAA,MACvC;AAAA,QACE,UAAU;AAAA,QACV,OAAO;AAAA;AAAA;AAAA,MAGT;AAAA,IACF;AACA,UAAM,MAAM;AAGZ,UAAM,GAAG,SAAS,MAAM;AAAA,IAAC,CAAC;AAAA,EAC5B,QAAQ;AAAA,EAER;AACF;AAkBA,SAAS,eAAe,MAAyB;AAC/C,MAAI,QAAQ,IAAI,kCAAkC,IAAK,QAAO;AAC9D,MAAI,QAAQ,IAAI,aAAa,OAAQ,QAAO;AAC5C,MAAI,QAAQ,IAAI,WAAW,OAAW,QAAO;AAG7C,MAAI,KAAK,MAAM,CAAC,EAAE,SAAS,0BAA0B,EAAG,QAAO;AAE/D,MAAI,CAAC,gBAAgB,EAAG,QAAO;AAE/B,SAAO;AACT;AAEA,SAAS,kBAA2B;AAClC,MAAI;AACF,UAAM,MAAM,aAAa,cAAc,GAAG,MAAM;AAChD,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,OAAO,oBAAoB,MAAO,QAAO;AAC7C,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAcA,eAAsB,yBAAwC;AAI5D,MAAI;AACF,UAAM,eAAe,CAAC,CAAC;AAAA,EACzB,QAAQ;AAAA,EAER;AACF;AAOO,SAAS,mBAAmB,MAAmC;AACpE,QAAM,OAAO,QAAQ,aAAa;AAClC,MAAI;AACF,UAAM,MAAM,aAAa,MAAM,MAAM;AACrC,UAAM,UAAU,IAAI,KAAK;AACzB,QAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,UAAM,SAAS,KAAK,MAAM,OAAO;AACjC,WAAO;AAAA,MACL,eACE,OAAO,OAAO,kBAAkB,WAAW,OAAO,gBAAgB;AAAA,MACpE,mBACE,OAAO,OAAO,sBAAsB,WAChC,OAAO,oBACP;AAAA,MACN,gBACE,OAAO,OAAO,mBAAmB,WAAW,OAAO,iBAAiB;AAAA,MACtE,oBAAoB,MAAM,QAAQ,OAAO,kBAAkB,IACvD,OAAO,mBAAmB,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ,IAC1E,CAAC;AAAA,MACL,sBACE,OAAO,OAAO,yBAAyB,WACnC,OAAO,uBACP;AAAA,IACR;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getGlobalAlmanacDir
|
|
4
|
+
} from "./chunk-7JUX4ADQ.js";
|
|
5
|
+
|
|
6
|
+
// src/update/config.ts
|
|
7
|
+
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
8
|
+
import { dirname, join } from "path";
|
|
9
|
+
var AGENT_PROVIDER_IDS = ["claude", "codex", "cursor"];
|
|
10
|
+
function isAgentProviderId(value) {
|
|
11
|
+
return AGENT_PROVIDER_IDS.includes(value);
|
|
12
|
+
}
|
|
13
|
+
function defaultConfig() {
|
|
14
|
+
return {
|
|
15
|
+
update_notifier: true,
|
|
16
|
+
agent: {
|
|
17
|
+
default: "claude",
|
|
18
|
+
models: {
|
|
19
|
+
claude: "claude-sonnet-4-6",
|
|
20
|
+
codex: null,
|
|
21
|
+
cursor: null
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function getConfigPath() {
|
|
27
|
+
return join(getGlobalAlmanacDir(), "config.json");
|
|
28
|
+
}
|
|
29
|
+
async function readConfig(path) {
|
|
30
|
+
const file = path ?? getConfigPath();
|
|
31
|
+
let raw;
|
|
32
|
+
try {
|
|
33
|
+
raw = await readFile(file, "utf8");
|
|
34
|
+
} catch {
|
|
35
|
+
return defaultConfig();
|
|
36
|
+
}
|
|
37
|
+
const trimmed = raw.trim();
|
|
38
|
+
if (trimmed.length === 0) return defaultConfig();
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(trimmed);
|
|
41
|
+
const defaults = defaultConfig();
|
|
42
|
+
const rawAgent = parsed.agent !== void 0 && parsed.agent !== null && typeof parsed.agent === "object" ? parsed.agent : {};
|
|
43
|
+
const rawDefault = typeof rawAgent.default === "string" && isAgentProviderId(rawAgent.default) ? rawAgent.default : defaults.agent.default;
|
|
44
|
+
const rawModels = rawAgent.models !== void 0 && rawAgent.models !== null && typeof rawAgent.models === "object" ? rawAgent.models : {};
|
|
45
|
+
const models = {
|
|
46
|
+
...defaults.agent.models
|
|
47
|
+
};
|
|
48
|
+
for (const id of AGENT_PROVIDER_IDS) {
|
|
49
|
+
const value = rawModels[id];
|
|
50
|
+
if (typeof value === "string" && value.length > 0) {
|
|
51
|
+
models[id] = value;
|
|
52
|
+
} else if (value === null) {
|
|
53
|
+
models[id] = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
update_notifier: typeof parsed.update_notifier === "boolean" ? parsed.update_notifier : true,
|
|
58
|
+
agent: {
|
|
59
|
+
default: rawDefault,
|
|
60
|
+
models
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
} catch {
|
|
64
|
+
return defaultConfig();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function writeConfig(config, path) {
|
|
68
|
+
const file = path ?? getConfigPath();
|
|
69
|
+
await mkdir(dirname(file), { recursive: true });
|
|
70
|
+
const body = `${JSON.stringify(normalizeConfig(config), null, 2)}
|
|
71
|
+
`;
|
|
72
|
+
const tmp = `${file}.tmp`;
|
|
73
|
+
await writeFile(tmp, body, "utf8");
|
|
74
|
+
await rename(tmp, file);
|
|
75
|
+
}
|
|
76
|
+
function normalizeConfig(config) {
|
|
77
|
+
const defaults = defaultConfig();
|
|
78
|
+
return {
|
|
79
|
+
update_notifier: typeof config.update_notifier === "boolean" ? config.update_notifier : defaults.update_notifier,
|
|
80
|
+
agent: {
|
|
81
|
+
default: config.agent !== void 0 && isAgentProviderId(config.agent.default) ? config.agent.default : defaults.agent.default,
|
|
82
|
+
models: {
|
|
83
|
+
...defaults.agent.models,
|
|
84
|
+
...config.agent?.models ?? {}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export {
|
|
91
|
+
AGENT_PROVIDER_IDS,
|
|
92
|
+
isAgentProviderId,
|
|
93
|
+
getConfigPath,
|
|
94
|
+
readConfig,
|
|
95
|
+
writeConfig
|
|
96
|
+
};
|
|
97
|
+
//# sourceMappingURL=chunk-WRUSDYYE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/update/config.ts"],"sourcesContent":["import { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\n\nimport { getGlobalAlmanacDir } from \"../paths.js\";\n\nexport const AGENT_PROVIDER_IDS = [\"claude\", \"codex\", \"cursor\"] as const;\nexport type AgentProviderId = (typeof AGENT_PROVIDER_IDS)[number];\n\nexport function isAgentProviderId(value: string): value is AgentProviderId {\n return (AGENT_PROVIDER_IDS as readonly string[]).includes(value);\n}\n\nexport interface AgentConfig {\n /** Default provider for bootstrap/capture. Default: \"claude\". */\n default: AgentProviderId;\n /** Optional per-provider model override. `null` means provider default. */\n models: Partial<Record<AgentProviderId, string | null>>;\n}\n\n/**\n * `~/.almanac/config.json` — global, cross-wiki configuration. Today\n * the only field is `update_notifier` (on/off toggle for the pre-command\n * banner); designed as an object so we can add more knobs without\n * breaking users who already have the file on disk.\n *\n * Missing or malformed → defaults. Same tolerance as `UpdateState`:\n * the CLI must not be able to fail because this file drifted.\n */\nexport interface GlobalConfig {\n /** When `false`, suppress the pre-command update-nag banner. Default: true. */\n update_notifier: boolean;\n /** Agent-provider settings for bootstrap/capture. */\n agent: AgentConfig;\n}\n\nexport function defaultConfig(): GlobalConfig {\n return {\n update_notifier: true,\n agent: {\n default: \"claude\",\n models: {\n claude: \"claude-sonnet-4-6\",\n codex: null,\n cursor: null,\n },\n },\n };\n}\n\nexport function getConfigPath(): string {\n return join(getGlobalAlmanacDir(), \"config.json\");\n}\n\nexport async function readConfig(path?: string): Promise<GlobalConfig> {\n const file = path ?? getConfigPath();\n let raw: string;\n try {\n raw = await readFile(file, \"utf8\");\n } catch {\n return defaultConfig();\n }\n const trimmed = raw.trim();\n if (trimmed.length === 0) return defaultConfig();\n try {\n const parsed = JSON.parse(trimmed) as Partial<GlobalConfig>;\n const defaults = defaultConfig();\n const rawAgent =\n parsed.agent !== undefined &&\n parsed.agent !== null &&\n typeof parsed.agent === \"object\"\n ? (parsed.agent as Partial<AgentConfig>)\n : {};\n const rawDefault =\n typeof rawAgent.default === \"string\" &&\n isAgentProviderId(rawAgent.default)\n ? rawAgent.default\n : defaults.agent.default;\n const rawModels =\n rawAgent.models !== undefined &&\n rawAgent.models !== null &&\n typeof rawAgent.models === \"object\"\n ? (rawAgent.models as Record<string, unknown>)\n : {};\n const models: Partial<Record<AgentProviderId, string | null>> = {\n ...defaults.agent.models,\n };\n for (const id of AGENT_PROVIDER_IDS) {\n const value = rawModels[id];\n if (typeof value === \"string\" && value.length > 0) {\n models[id] = value;\n } else if (value === null) {\n models[id] = null;\n }\n }\n return {\n update_notifier:\n typeof parsed.update_notifier === \"boolean\"\n ? parsed.update_notifier\n : true,\n agent: {\n default: rawDefault,\n models,\n },\n };\n } catch {\n return defaultConfig();\n }\n}\n\nexport async function writeConfig(\n config: GlobalConfig | Partial<GlobalConfig>,\n path?: string,\n): Promise<void> {\n const file = path ?? getConfigPath();\n await mkdir(dirname(file), { recursive: true });\n const body = `${JSON.stringify(normalizeConfig(config), null, 2)}\\n`;\n const tmp = `${file}.tmp`;\n await writeFile(tmp, body, \"utf8\");\n await rename(tmp, file);\n}\n\nfunction normalizeConfig(config: GlobalConfig | Partial<GlobalConfig>): GlobalConfig {\n const defaults = defaultConfig();\n return {\n update_notifier:\n typeof config.update_notifier === \"boolean\"\n ? config.update_notifier\n : defaults.update_notifier,\n agent: {\n default:\n config.agent !== undefined && isAgentProviderId(config.agent.default)\n ? config.agent.default\n : defaults.agent.default,\n models: {\n ...defaults.agent.models,\n ...(config.agent?.models ?? {}),\n },\n },\n };\n}\n"],"mappings":";;;;;;AAAA,SAAS,OAAO,UAAU,QAAQ,iBAAiB;AACnD,SAAS,SAAS,YAAY;AAIvB,IAAM,qBAAqB,CAAC,UAAU,SAAS,QAAQ;AAGvD,SAAS,kBAAkB,OAAyC;AACzE,SAAQ,mBAAyC,SAAS,KAAK;AACjE;AAyBO,SAAS,gBAA8B;AAC5C,SAAO;AAAA,IACL,iBAAiB;AAAA,IACjB,OAAO;AAAA,MACL,SAAS;AAAA,MACT,QAAQ;AAAA,QACN,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,QAAQ;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,gBAAwB;AACtC,SAAO,KAAK,oBAAoB,GAAG,aAAa;AAClD;AAEA,eAAsB,WAAW,MAAsC;AACrE,QAAM,OAAO,QAAQ,cAAc;AACnC,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,SAAS,MAAM,MAAM;AAAA,EACnC,QAAQ;AACN,WAAO,cAAc;AAAA,EACvB;AACA,QAAM,UAAU,IAAI,KAAK;AACzB,MAAI,QAAQ,WAAW,EAAG,QAAO,cAAc;AAC/C,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,OAAO;AACjC,UAAM,WAAW,cAAc;AAC/B,UAAM,WACJ,OAAO,UAAU,UACjB,OAAO,UAAU,QACjB,OAAO,OAAO,UAAU,WACnB,OAAO,QACR,CAAC;AACP,UAAM,aACJ,OAAO,SAAS,YAAY,YAC5B,kBAAkB,SAAS,OAAO,IAC9B,SAAS,UACT,SAAS,MAAM;AACrB,UAAM,YACJ,SAAS,WAAW,UACpB,SAAS,WAAW,QACpB,OAAO,SAAS,WAAW,WACtB,SAAS,SACV,CAAC;AACP,UAAM,SAA0D;AAAA,MAC9D,GAAG,SAAS,MAAM;AAAA,IACpB;AACA,eAAW,MAAM,oBAAoB;AACnC,YAAM,QAAQ,UAAU,EAAE;AAC1B,UAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAAG;AACjD,eAAO,EAAE,IAAI;AAAA,MACf,WAAW,UAAU,MAAM;AACzB,eAAO,EAAE,IAAI;AAAA,MACf;AAAA,IACF;AACA,WAAO;AAAA,MACL,iBACE,OAAO,OAAO,oBAAoB,YAC9B,OAAO,kBACP;AAAA,MACN,OAAO;AAAA,QACL,SAAS;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAAA,EACF,QAAQ;AACN,WAAO,cAAc;AAAA,EACvB;AACF;AAEA,eAAsB,YACpB,QACA,MACe;AACf,QAAM,OAAO,QAAQ,cAAc;AACnC,QAAM,MAAM,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9C,QAAM,OAAO,GAAG,KAAK,UAAU,gBAAgB,MAAM,GAAG,MAAM,CAAC,CAAC;AAAA;AAChE,QAAM,MAAM,GAAG,IAAI;AACnB,QAAM,UAAU,KAAK,MAAM,MAAM;AACjC,QAAM,OAAO,KAAK,IAAI;AACxB;AAEA,SAAS,gBAAgB,QAA4D;AACnF,QAAM,WAAW,cAAc;AAC/B,SAAO;AAAA,IACL,iBACE,OAAO,OAAO,oBAAoB,YAC9B,OAAO,kBACP,SAAS;AAAA,IACf,OAAO;AAAA,MACL,SACE,OAAO,UAAU,UAAa,kBAAkB,OAAO,MAAM,OAAO,IAChE,OAAO,MAAM,UACb,SAAS,MAAM;AAAA,MACrB,QAAQ;AAAA,QACN,GAAG,SAAS,MAAM;AAAA,QAClB,GAAI,OAAO,OAAO,UAAU,CAAC;AAAA,MAC/B;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runHookInstall
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-447U3GQJ.js";
|
|
5
|
+
import {
|
|
6
|
+
UNAUTHENTICATED_MESSAGE,
|
|
7
|
+
checkClaudeAuth
|
|
8
|
+
} from "./chunk-SSYMRT4I.js";
|
|
9
|
+
import {
|
|
10
|
+
isAgentProviderId,
|
|
11
|
+
readConfig,
|
|
12
|
+
writeConfig
|
|
13
|
+
} from "./chunk-WRUSDYYE.js";
|
|
5
14
|
|
|
6
15
|
// src/commands/setup.ts
|
|
7
16
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -11,135 +20,20 @@ import {
|
|
|
11
20
|
readFile,
|
|
12
21
|
writeFile
|
|
13
22
|
} from "fs/promises";
|
|
14
|
-
import { createRequire as
|
|
23
|
+
import { createRequire as createRequire2 } from "module";
|
|
15
24
|
import { homedir as homedir2 } from "os";
|
|
16
25
|
import path3 from "path";
|
|
17
26
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
18
27
|
|
|
19
|
-
// src/agent/auth.ts
|
|
20
|
-
import { spawn, spawnSync } from "child_process";
|
|
21
|
-
import { createRequire } from "module";
|
|
22
|
-
import { dirname, join } from "path";
|
|
23
|
-
var AUTH_TIMEOUT_MS = 1e4;
|
|
24
|
-
function resolveClaudeExecutable() {
|
|
25
|
-
const result = spawnSync("sh", ["-lc", "command -v claude"], {
|
|
26
|
-
encoding: "utf8"
|
|
27
|
-
});
|
|
28
|
-
if (result.status !== 0) return void 0;
|
|
29
|
-
const found = result.stdout.trim().split("\n")[0]?.trim();
|
|
30
|
-
return found !== void 0 && found.length > 0 ? found : void 0;
|
|
31
|
-
}
|
|
32
|
-
function resolveCliJsPath() {
|
|
33
|
-
const require2 = createRequire(import.meta.url);
|
|
34
|
-
const entry = require2.resolve("@anthropic-ai/claude-agent-sdk");
|
|
35
|
-
return join(dirname(entry), "cli.js");
|
|
36
|
-
}
|
|
37
|
-
var defaultSpawnCli = (args) => {
|
|
38
|
-
const command = resolveClaudeExecutable() ?? "claude";
|
|
39
|
-
const child = spawn(command, args, {
|
|
40
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
41
|
-
});
|
|
42
|
-
return child;
|
|
43
|
-
};
|
|
44
|
-
var legacySdkSpawnCli = (args) => {
|
|
45
|
-
const cliPath = resolveCliJsPath();
|
|
46
|
-
const child = spawn(process.execPath, [cliPath, ...args], {
|
|
47
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
48
|
-
});
|
|
49
|
-
return child;
|
|
50
|
-
};
|
|
51
|
-
async function checkClaudeAuth(spawnCli = defaultSpawnCli) {
|
|
52
|
-
if (spawnCli === defaultSpawnCli) {
|
|
53
|
-
const status = await checkClaudeAuthWith(defaultSpawnCli);
|
|
54
|
-
if (status.loggedIn) return status;
|
|
55
|
-
return await checkClaudeAuthWith(legacySdkSpawnCli);
|
|
56
|
-
}
|
|
57
|
-
return await checkClaudeAuthWith(spawnCli);
|
|
58
|
-
}
|
|
59
|
-
async function checkClaudeAuthWith(spawnCli) {
|
|
60
|
-
let child;
|
|
61
|
-
try {
|
|
62
|
-
child = spawnCli(["auth", "status", "--json"]);
|
|
63
|
-
} catch {
|
|
64
|
-
return { loggedIn: false };
|
|
65
|
-
}
|
|
66
|
-
return new Promise((resolve) => {
|
|
67
|
-
let stdout = "";
|
|
68
|
-
let stderr = "";
|
|
69
|
-
let settled = false;
|
|
70
|
-
const settle = (value) => {
|
|
71
|
-
if (settled) return;
|
|
72
|
-
settled = true;
|
|
73
|
-
clearTimeout(timer);
|
|
74
|
-
resolve(value);
|
|
75
|
-
};
|
|
76
|
-
const timer = setTimeout(() => {
|
|
77
|
-
try {
|
|
78
|
-
child.kill("SIGTERM");
|
|
79
|
-
} catch {
|
|
80
|
-
}
|
|
81
|
-
settle({ loggedIn: false });
|
|
82
|
-
}, AUTH_TIMEOUT_MS);
|
|
83
|
-
child.stdout.on("data", (data) => {
|
|
84
|
-
stdout += data.toString();
|
|
85
|
-
});
|
|
86
|
-
child.stderr.on("data", (data) => {
|
|
87
|
-
stderr += data.toString();
|
|
88
|
-
});
|
|
89
|
-
child.on("error", () => {
|
|
90
|
-
settle({ loggedIn: false });
|
|
91
|
-
});
|
|
92
|
-
child.on("close", (code) => {
|
|
93
|
-
if (code !== 0 && stdout.trim().length === 0) {
|
|
94
|
-
void stderr;
|
|
95
|
-
settle({ loggedIn: false });
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
try {
|
|
99
|
-
settle(parseClaudeAuthStatus(stdout.trim()));
|
|
100
|
-
} catch {
|
|
101
|
-
settle({ loggedIn: false });
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
function parseClaudeAuthStatus(raw) {
|
|
107
|
-
const parsed = JSON.parse(raw);
|
|
108
|
-
const loggedIn = parsed.loggedIn === true;
|
|
109
|
-
const out = { loggedIn };
|
|
110
|
-
if (typeof parsed.email === "string") out.email = parsed.email;
|
|
111
|
-
if (typeof parsed.subscriptionType === "string") {
|
|
112
|
-
out.subscriptionType = parsed.subscriptionType;
|
|
113
|
-
}
|
|
114
|
-
if (typeof parsed.authMethod === "string") {
|
|
115
|
-
out.authMethod = parsed.authMethod;
|
|
116
|
-
}
|
|
117
|
-
return out;
|
|
118
|
-
}
|
|
119
|
-
var UNAUTHENTICATED_MESSAGE = "not authenticated to Claude.\n\nOption 1 \u2014 use your Claude subscription (Pro/Max):\n claude auth login --claudeai\n\nOption 2 \u2014 use a pay-per-token API key:\n Get one at https://console.anthropic.com\n export ANTHROPIC_API_KEY=sk-ant-...\n\nVerify with: claude auth status";
|
|
120
|
-
async function assertClaudeAuth(spawnCli = defaultSpawnCli) {
|
|
121
|
-
const status = await checkClaudeAuth(spawnCli);
|
|
122
|
-
if (status.loggedIn) {
|
|
123
|
-
return status;
|
|
124
|
-
}
|
|
125
|
-
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
126
|
-
if (apiKey !== void 0 && apiKey.length > 0) {
|
|
127
|
-
return { loggedIn: true, authMethod: "apiKey" };
|
|
128
|
-
}
|
|
129
|
-
const err = new Error(UNAUTHENTICATED_MESSAGE);
|
|
130
|
-
err.code = "CLAUDE_AUTH_MISSING";
|
|
131
|
-
throw err;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
28
|
// src/commands/setup/install-path.ts
|
|
135
29
|
import { execFile } from "child_process";
|
|
136
|
-
import { createRequire
|
|
30
|
+
import { createRequire } from "module";
|
|
137
31
|
import { homedir } from "os";
|
|
138
32
|
import path from "path";
|
|
139
33
|
import { fileURLToPath } from "url";
|
|
140
34
|
function detectCurrentInstallPath() {
|
|
141
35
|
try {
|
|
142
|
-
const req =
|
|
36
|
+
const req = createRequire(import.meta.url);
|
|
143
37
|
const here = fileURLToPath(import.meta.url);
|
|
144
38
|
let dir = path.dirname(here);
|
|
145
39
|
for (let i = 0; i < 6; i++) {
|
|
@@ -330,6 +224,21 @@ async function runSetup(options = {}) {
|
|
|
330
224
|
const auth = await safeCheckAuth(options.spawnCli);
|
|
331
225
|
reportAuth(out, auth);
|
|
332
226
|
out.write(BAR + "\n");
|
|
227
|
+
const agentChoice = await chooseDefaultAgent({
|
|
228
|
+
out,
|
|
229
|
+
interactive,
|
|
230
|
+
requested: options.agent
|
|
231
|
+
});
|
|
232
|
+
if (!agentChoice.ok) {
|
|
233
|
+
return {
|
|
234
|
+
stdout: "",
|
|
235
|
+
stderr: `almanac: ${agentChoice.error}
|
|
236
|
+
`,
|
|
237
|
+
exitCode: 1
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
stepDone(out, `Default agent: ${WHITE_BOLD2}${agentChoice.provider}${RST2}`);
|
|
241
|
+
out.write(BAR + "\n");
|
|
333
242
|
const ephem = options.installPath !== void 0 ? options.installPath !== null ? detectEphemeral(options.installPath) : false : detectEphemeral(detectCurrentInstallPath());
|
|
334
243
|
if (ephem) {
|
|
335
244
|
let globalAction = "install";
|
|
@@ -367,13 +276,14 @@ async function runSetup(options = {}) {
|
|
|
367
276
|
} else if (interactive) {
|
|
368
277
|
hookAction = await confirm(
|
|
369
278
|
out,
|
|
370
|
-
"Install
|
|
279
|
+
"Install auto-capture hooks for Claude, Codex, and Cursor?",
|
|
371
280
|
true
|
|
372
281
|
);
|
|
373
282
|
}
|
|
374
283
|
let hookResultLine = "";
|
|
375
284
|
if (hookAction === "install") {
|
|
376
285
|
const res = await runHookInstall({
|
|
286
|
+
source: "all",
|
|
377
287
|
settingsPath: options.settingsPath,
|
|
378
288
|
hookScriptPath: options.hookScriptPath,
|
|
379
289
|
stableHooksDir: options.stableHooksDir
|
|
@@ -386,7 +296,7 @@ async function runSetup(options = {}) {
|
|
|
386
296
|
exitCode: res.exitCode
|
|
387
297
|
};
|
|
388
298
|
}
|
|
389
|
-
hookResultLine = res.stdout.includes("already installed") ? `
|
|
299
|
+
hookResultLine = res.stdout.includes("already installed") ? `Auto-capture hooks ${DIM2}already installed${RST2}` : `Auto-capture hooks installed`;
|
|
390
300
|
stepDone(out, hookResultLine);
|
|
391
301
|
} else {
|
|
392
302
|
stepSkipped(out, `SessionEnd hook ${DIM2}skipped${RST2}`);
|
|
@@ -437,6 +347,31 @@ async function safeCheckAuth(spawnCli) {
|
|
|
437
347
|
return { loggedIn: false };
|
|
438
348
|
}
|
|
439
349
|
}
|
|
350
|
+
async function chooseDefaultAgent(args) {
|
|
351
|
+
const config = await readConfig();
|
|
352
|
+
let selected = args.requested ?? config.agent.default;
|
|
353
|
+
if (args.interactive && args.requested === void 0) {
|
|
354
|
+
selected = await promptText(
|
|
355
|
+
args.out,
|
|
356
|
+
"Choose default agent: claude, codex, or cursor",
|
|
357
|
+
config.agent.default
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
if (!isAgentProviderId(selected)) {
|
|
361
|
+
return {
|
|
362
|
+
ok: false,
|
|
363
|
+
error: `unknown agent '${selected}'. Expected one of: claude, codex, cursor.`
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
await writeConfig({
|
|
367
|
+
...config,
|
|
368
|
+
agent: {
|
|
369
|
+
...config.agent,
|
|
370
|
+
default: selected
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
return { ok: true, provider: selected };
|
|
374
|
+
}
|
|
440
375
|
function reportAuth(out, auth) {
|
|
441
376
|
if (auth.loggedIn) {
|
|
442
377
|
const who = auth.email ?? "Claude account";
|
|
@@ -529,6 +464,25 @@ function confirm(out, question, defaultYes) {
|
|
|
529
464
|
process.stdin.on("data", onData);
|
|
530
465
|
});
|
|
531
466
|
}
|
|
467
|
+
function promptText(out, question, defaultValue) {
|
|
468
|
+
return new Promise((resolve) => {
|
|
469
|
+
out.write(
|
|
470
|
+
` ${BLUE2}\u25C6${RST2} ${question} ${DIM2}[${defaultValue}]${RST2} `
|
|
471
|
+
);
|
|
472
|
+
let buf = "";
|
|
473
|
+
const onData = (chunk) => {
|
|
474
|
+
buf += chunk.toString("utf8");
|
|
475
|
+
const nl = buf.indexOf("\n");
|
|
476
|
+
if (nl === -1) return;
|
|
477
|
+
process.stdin.removeListener("data", onData);
|
|
478
|
+
process.stdin.pause();
|
|
479
|
+
const answer = buf.slice(0, nl).trim().toLowerCase();
|
|
480
|
+
resolve(answer.length === 0 ? defaultValue : answer);
|
|
481
|
+
};
|
|
482
|
+
process.stdin.resume();
|
|
483
|
+
process.stdin.on("data", onData);
|
|
484
|
+
});
|
|
485
|
+
}
|
|
532
486
|
function resolveGuidesDir() {
|
|
533
487
|
const here = path3.dirname(fileURLToPath2(import.meta.url));
|
|
534
488
|
const candidates = [
|
|
@@ -542,7 +496,7 @@ function resolveGuidesDir() {
|
|
|
542
496
|
if (looksLikeGuidesDir(dir)) return dir;
|
|
543
497
|
}
|
|
544
498
|
try {
|
|
545
|
-
const require2 =
|
|
499
|
+
const require2 = createRequire2(import.meta.url);
|
|
546
500
|
const pkgJson = require2.resolve("codealmanac/package.json");
|
|
547
501
|
const guides = path3.join(path3.dirname(pkgJson), "guides");
|
|
548
502
|
if (looksLikeGuidesDir(guides)) return guides;
|
|
@@ -557,10 +511,7 @@ function looksLikeGuidesDir(dir) {
|
|
|
557
511
|
}
|
|
558
512
|
|
|
559
513
|
export {
|
|
560
|
-
resolveClaudeExecutable,
|
|
561
|
-
checkClaudeAuth,
|
|
562
|
-
assertClaudeAuth,
|
|
563
514
|
runSetup,
|
|
564
515
|
IMPORT_LINE
|
|
565
516
|
};
|
|
566
|
-
//# sourceMappingURL=chunk-
|
|
517
|
+
//# sourceMappingURL=chunk-ZDJSJIB6.js.map
|