codealmanac 0.1.6 → 0.1.8
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/dist/chunk-3C5SY5SE.js +1239 -0
- package/dist/chunk-3C5SY5SE.js.map +1 -0
- package/dist/chunk-3LC55TG6.js +566 -0
- package/dist/chunk-3LC55TG6.js.map +1 -0
- package/dist/chunk-4CODZRHH.js +19 -0
- package/dist/chunk-4CODZRHH.js.map +1 -0
- package/dist/chunk-73A5TGBC.js +441 -0
- package/dist/chunk-73A5TGBC.js.map +1 -0
- package/dist/chunk-7JUX4ADQ.js +38 -0
- package/dist/chunk-7JUX4ADQ.js.map +1 -0
- package/dist/chunk-AXFPUHBN.js +227 -0
- package/dist/chunk-AXFPUHBN.js.map +1 -0
- package/dist/chunk-BJVZLP6O.js +145 -0
- package/dist/chunk-BJVZLP6O.js.map +1 -0
- package/dist/chunk-FM3VRDK7.js +20 -0
- package/dist/chunk-FM3VRDK7.js.map +1 -0
- package/dist/chunk-P3LDTCLB.js +34 -0
- package/dist/chunk-P3LDTCLB.js.map +1 -0
- package/dist/chunk-QHQ6YH7U.js +81 -0
- package/dist/chunk-QHQ6YH7U.js.map +1 -0
- package/dist/chunk-Z4MWLVS2.js +355 -0
- package/dist/chunk-Z4MWLVS2.js.map +1 -0
- package/dist/chunk-Z6MBJ3D2.js +203 -0
- package/dist/chunk-Z6MBJ3D2.js.map +1 -0
- package/dist/cli-HIXXCUSQ.js +393 -0
- package/dist/cli-HIXXCUSQ.js.map +1 -0
- package/dist/codealmanac.js +32 -5
- package/dist/codealmanac.js.map +1 -1
- package/dist/doctor-IS6N7V63.js +15 -0
- package/dist/doctor-IS6N7V63.js.map +1 -0
- package/dist/hook-CRJMWSSO.js +12 -0
- package/dist/hook-CRJMWSSO.js.map +1 -0
- package/dist/register-commands-JAPO3AUB.js +2647 -0
- package/dist/register-commands-JAPO3AUB.js.map +1 -0
- package/dist/uninstall-HE2Z2LN2.js +12 -0
- package/dist/uninstall-HE2Z2LN2.js.map +1 -0
- package/dist/update-IL243I4E.js +10 -0
- package/dist/update-IL243I4E.js.map +1 -0
- package/dist/wiki-EHZ7LG7R.js +238 -0
- package/dist/wiki-EHZ7LG7R.js.map +1 -0
- package/package.json +2 -2
- package/dist/cli-GTEC5PC7.js +0 -6237
- package/dist/cli-GTEC5PC7.js.map +0 -1
|
@@ -0,0 +1,227 @@
|
|
|
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
|
+
function defaultConfig() {
|
|
10
|
+
return { update_notifier: true };
|
|
11
|
+
}
|
|
12
|
+
function getConfigPath() {
|
|
13
|
+
return join(getGlobalAlmanacDir(), "config.json");
|
|
14
|
+
}
|
|
15
|
+
async function readConfig(path) {
|
|
16
|
+
const file = path ?? getConfigPath();
|
|
17
|
+
let raw;
|
|
18
|
+
try {
|
|
19
|
+
raw = await readFile(file, "utf8");
|
|
20
|
+
} catch {
|
|
21
|
+
return defaultConfig();
|
|
22
|
+
}
|
|
23
|
+
const trimmed = raw.trim();
|
|
24
|
+
if (trimmed.length === 0) return defaultConfig();
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(trimmed);
|
|
27
|
+
return {
|
|
28
|
+
update_notifier: typeof parsed.update_notifier === "boolean" ? parsed.update_notifier : true
|
|
29
|
+
};
|
|
30
|
+
} catch {
|
|
31
|
+
return defaultConfig();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function writeConfig(config, path) {
|
|
35
|
+
const file = path ?? getConfigPath();
|
|
36
|
+
await mkdir(dirname(file), { recursive: true });
|
|
37
|
+
const body = `${JSON.stringify(config, null, 2)}
|
|
38
|
+
`;
|
|
39
|
+
const tmp = `${file}.tmp`;
|
|
40
|
+
await writeFile(tmp, body, "utf8");
|
|
41
|
+
await rename(tmp, file);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/update/semver.ts
|
|
45
|
+
function parse(v) {
|
|
46
|
+
const trimmed = v.trim().replace(/^v/i, "");
|
|
47
|
+
const noBuild = trimmed.split("+")[0] ?? "";
|
|
48
|
+
const dashAt = noBuild.indexOf("-");
|
|
49
|
+
const core = dashAt === -1 ? noBuild : noBuild.slice(0, dashAt);
|
|
50
|
+
const pre = dashAt === -1 ? "" : noBuild.slice(dashAt + 1);
|
|
51
|
+
const parts = core.split(".").map((p) => Number.parseInt(p, 10));
|
|
52
|
+
if (parts.length === 0 || parts.some((n) => !Number.isFinite(n) || n < 0)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
major: parts[0] ?? 0,
|
|
57
|
+
minor: parts[1] ?? 0,
|
|
58
|
+
patch: parts[2] ?? 0,
|
|
59
|
+
pre
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function isNewer(latest, installed) {
|
|
63
|
+
const a = parse(latest);
|
|
64
|
+
const b = parse(installed);
|
|
65
|
+
if (a === null || b === null) return false;
|
|
66
|
+
if (a.major !== b.major) return a.major > b.major;
|
|
67
|
+
if (a.minor !== b.minor) return a.minor > b.minor;
|
|
68
|
+
if (a.patch !== b.patch) return a.patch > b.patch;
|
|
69
|
+
if (a.pre === b.pre) return false;
|
|
70
|
+
if (a.pre === "" && b.pre !== "") return true;
|
|
71
|
+
if (a.pre !== "" && b.pre === "") return false;
|
|
72
|
+
return a.pre > b.pre;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/update/state.ts
|
|
76
|
+
import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
|
|
77
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
78
|
+
function emptyState() {
|
|
79
|
+
return {
|
|
80
|
+
last_check_at: 0,
|
|
81
|
+
installed_version: "",
|
|
82
|
+
latest_version: "",
|
|
83
|
+
dismissed_versions: []
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function getStatePath() {
|
|
87
|
+
return join2(getGlobalAlmanacDir(), "update-state.json");
|
|
88
|
+
}
|
|
89
|
+
async function readState(path) {
|
|
90
|
+
const file = path ?? getStatePath();
|
|
91
|
+
let raw;
|
|
92
|
+
try {
|
|
93
|
+
raw = await readFile2(file, "utf8");
|
|
94
|
+
} catch {
|
|
95
|
+
return emptyState();
|
|
96
|
+
}
|
|
97
|
+
const trimmed = raw.trim();
|
|
98
|
+
if (trimmed.length === 0) return emptyState();
|
|
99
|
+
try {
|
|
100
|
+
const parsed = JSON.parse(trimmed);
|
|
101
|
+
return {
|
|
102
|
+
last_check_at: typeof parsed.last_check_at === "number" ? parsed.last_check_at : 0,
|
|
103
|
+
installed_version: typeof parsed.installed_version === "string" ? parsed.installed_version : "",
|
|
104
|
+
latest_version: typeof parsed.latest_version === "string" ? parsed.latest_version : "",
|
|
105
|
+
dismissed_versions: Array.isArray(parsed.dismissed_versions) ? parsed.dismissed_versions.filter(
|
|
106
|
+
(v) => typeof v === "string" && v.length > 0
|
|
107
|
+
) : [],
|
|
108
|
+
last_fetch_failed_at: typeof parsed.last_fetch_failed_at === "number" ? parsed.last_fetch_failed_at : void 0
|
|
109
|
+
};
|
|
110
|
+
} catch {
|
|
111
|
+
return emptyState();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function writeState(state, path) {
|
|
115
|
+
const file = path ?? getStatePath();
|
|
116
|
+
await mkdir2(dirname2(file), { recursive: true });
|
|
117
|
+
const body = `${JSON.stringify(state, null, 2)}
|
|
118
|
+
`;
|
|
119
|
+
const tmp = `${file}.tmp`;
|
|
120
|
+
await writeFile2(tmp, body, "utf8");
|
|
121
|
+
await rename2(tmp, file);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/update/check.ts
|
|
125
|
+
import { createRequire } from "module";
|
|
126
|
+
var DEFAULT_CACHE_SECONDS = 24 * 60 * 60;
|
|
127
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
128
|
+
var REGISTRY_URL = "https://registry.npmjs.org/codealmanac";
|
|
129
|
+
async function checkForUpdate(opts = {}) {
|
|
130
|
+
const now = opts.now ?? (() => Math.floor(Date.now() / 1e3));
|
|
131
|
+
const cacheSeconds = opts.cacheSeconds ?? DEFAULT_CACHE_SECONDS;
|
|
132
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
133
|
+
const fetchFn = opts.fetchFn ?? globalThis.fetch;
|
|
134
|
+
const installed = opts.installedVersion ?? readInstalledVersion();
|
|
135
|
+
const state = await readState(opts.statePath);
|
|
136
|
+
if (!opts.force && state.last_check_at > 0 && now() - state.last_check_at < cacheSeconds) {
|
|
137
|
+
return { state, fetched: false, fetchFailed: false };
|
|
138
|
+
}
|
|
139
|
+
let latest = null;
|
|
140
|
+
let failed = false;
|
|
141
|
+
try {
|
|
142
|
+
const ac = new AbortController();
|
|
143
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
144
|
+
try {
|
|
145
|
+
const res = await fetchFn(REGISTRY_URL, {
|
|
146
|
+
signal: ac.signal,
|
|
147
|
+
headers: { accept: "application/json" }
|
|
148
|
+
});
|
|
149
|
+
if (!res.ok) {
|
|
150
|
+
failed = true;
|
|
151
|
+
} else {
|
|
152
|
+
const body = await res.json();
|
|
153
|
+
const tag = body["dist-tags"]?.latest;
|
|
154
|
+
if (typeof tag === "string" && tag.length > 0) {
|
|
155
|
+
latest = tag;
|
|
156
|
+
} else {
|
|
157
|
+
failed = true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} finally {
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
failed = true;
|
|
165
|
+
}
|
|
166
|
+
if (failed || latest === null) {
|
|
167
|
+
const next2 = {
|
|
168
|
+
...state,
|
|
169
|
+
// We still bump last_check_at on a failed attempt; without this,
|
|
170
|
+
// every subsequent command would re-try the registry. A one-shot
|
|
171
|
+
// retry on the next invocation is enough; sustained failure gets
|
|
172
|
+
// retried on the 24h cadence like a success.
|
|
173
|
+
last_check_at: now(),
|
|
174
|
+
installed_version: installed,
|
|
175
|
+
last_fetch_failed_at: now()
|
|
176
|
+
};
|
|
177
|
+
try {
|
|
178
|
+
await writeState(next2, opts.statePath);
|
|
179
|
+
} catch {
|
|
180
|
+
}
|
|
181
|
+
return { state: next2, fetched: true, fetchFailed: true };
|
|
182
|
+
}
|
|
183
|
+
const next = {
|
|
184
|
+
last_check_at: now(),
|
|
185
|
+
installed_version: installed,
|
|
186
|
+
latest_version: latest,
|
|
187
|
+
dismissed_versions: state.dismissed_versions,
|
|
188
|
+
// Clear the failure marker on success.
|
|
189
|
+
last_fetch_failed_at: void 0
|
|
190
|
+
};
|
|
191
|
+
try {
|
|
192
|
+
await writeState(next, opts.statePath);
|
|
193
|
+
} catch {
|
|
194
|
+
}
|
|
195
|
+
return { state: next, fetched: true, fetchFailed: false };
|
|
196
|
+
}
|
|
197
|
+
function readInstalledVersion() {
|
|
198
|
+
try {
|
|
199
|
+
const require2 = createRequire(import.meta.url);
|
|
200
|
+
const pkg = require2("../../package.json");
|
|
201
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0) {
|
|
202
|
+
return pkg.version;
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
const require2 = createRequire(import.meta.url);
|
|
208
|
+
const pkg = require2("../package.json");
|
|
209
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0) {
|
|
210
|
+
return pkg.version;
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
return "unknown";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export {
|
|
218
|
+
getConfigPath,
|
|
219
|
+
readConfig,
|
|
220
|
+
writeConfig,
|
|
221
|
+
isNewer,
|
|
222
|
+
getStatePath,
|
|
223
|
+
readState,
|
|
224
|
+
writeState,
|
|
225
|
+
checkForUpdate
|
|
226
|
+
};
|
|
227
|
+
//# sourceMappingURL=chunk-AXFPUHBN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/update/config.ts","../src/update/semver.ts","../src/update/state.ts","../src/update/check.ts"],"sourcesContent":["import { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\n\nimport { getGlobalAlmanacDir } from \"../paths.js\";\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}\n\nexport function defaultConfig(): GlobalConfig {\n return { update_notifier: true };\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 return {\n update_notifier:\n typeof parsed.update_notifier === \"boolean\"\n ? parsed.update_notifier\n : true,\n };\n } catch {\n return defaultConfig();\n }\n}\n\nexport async function writeConfig(\n config: GlobalConfig,\n path?: string,\n): Promise<void> {\n const file = path ?? getConfigPath();\n await mkdir(dirname(file), { recursive: true });\n const body = `${JSON.stringify(config, null, 2)}\\n`;\n const tmp = `${file}.tmp`;\n await writeFile(tmp, body, \"utf8\");\n await rename(tmp, file);\n}\n","/**\n * Tiny semver comparator for update-version checks. We do NOT take a\n * dependency on `semver` — we only need a subset (compare two\n * well-formed `x.y.z` strings, optionally with a pre-release tag), and\n * adding a 400KB install for six lines of logic isn't a good trade.\n *\n * What we handle:\n * - Numeric `major.minor.patch` with or without a `-pre` tag.\n * - Missing parts default to 0 (`1.2` → `1.2.0`).\n * - Leading `v` is stripped.\n *\n * What we don't handle:\n * - Build metadata (`+sha.abcd`) — ignored.\n * - Pre-release precedence rules beyond \"tagged < untagged at same\n * numeric triple\". Good enough for codealmanac's linear release\n * cadence; if we ever publish `-rc.1` vs `-rc.2` and care which\n * comes first, revisit.\n */\n\ninterface Parsed {\n major: number;\n minor: number;\n patch: number;\n /** Empty string = no pre-release (counts as \"higher\" than a pre-release at the same triple). */\n pre: string;\n}\n\nfunction parse(v: string): Parsed | null {\n const trimmed = v.trim().replace(/^v/i, \"\");\n // Strip build metadata (everything from the first `+`).\n const noBuild = trimmed.split(\"+\")[0] ?? \"\";\n // Split off pre-release tag.\n const dashAt = noBuild.indexOf(\"-\");\n const core = dashAt === -1 ? noBuild : noBuild.slice(0, dashAt);\n const pre = dashAt === -1 ? \"\" : noBuild.slice(dashAt + 1);\n\n const parts = core.split(\".\").map((p) => Number.parseInt(p, 10));\n if (parts.length === 0 || parts.some((n) => !Number.isFinite(n) || n < 0)) {\n return null;\n }\n return {\n major: parts[0] ?? 0,\n minor: parts[1] ?? 0,\n patch: parts[2] ?? 0,\n pre,\n };\n}\n\n/**\n * Return `true` iff `latest` > `installed`. Returns `false` on unparseable\n * input rather than throwing — a bad version string must not be able to\n * crash the CLI's every-command banner path.\n */\nexport function isNewer(latest: string, installed: string): boolean {\n const a = parse(latest);\n const b = parse(installed);\n if (a === null || b === null) return false;\n\n if (a.major !== b.major) return a.major > b.major;\n if (a.minor !== b.minor) return a.minor > b.minor;\n if (a.patch !== b.patch) return a.patch > b.patch;\n\n // Same numeric triple. Empty pre-release beats a tagged pre-release\n // (1.2.3 > 1.2.3-rc.1). Two tagged pre-releases compare lexically.\n if (a.pre === b.pre) return false;\n if (a.pre === \"\" && b.pre !== \"\") return true;\n if (a.pre !== \"\" && b.pre === \"\") return false;\n return a.pre > b.pre;\n}\n","import { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\n\nimport { getGlobalAlmanacDir } from \"../paths.js\";\n\n/**\n * `~/.almanac/update-state.json` — the only piece of persistent state\n * the update system owns. Written by the background check worker (once\n * per 24h) and by `almanac update` / `almanac update --dismiss`; read\n * by the pre-command banner, `almanac doctor`, and the check path to\n * decide whether 24h have elapsed since the last check.\n *\n * Format is a single flat object with no schema version: fields are\n * only ever added (and read with defaults), never removed or reshaped.\n * If we ever need a breaking migration, the file is trivially\n * regenerable — the worst case is a single extra registry round-trip.\n *\n * Corruption handling: every read path tolerates missing or malformed\n * JSON as \"no state\" (empty defaults). We never propagate a parse\n * error up to the CLI banner, because a corrupt state file must not\n * be able to break every invocation.\n */\nexport interface UpdateState {\n /** Unix epoch seconds of the last registry query. */\n last_check_at: number;\n /** The codealmanac version running when we last wrote state. */\n installed_version: string;\n /** The newest version the registry has published at last check. */\n latest_version: string;\n /** Versions the user dismissed via `almanac update --dismiss`. */\n dismissed_versions: string[];\n /**\n * Epoch seconds of the last registry fetch attempt that FAILED. Used\n * by the check scheduler to back off (one failure shouldn't hammer\n * the registry on every command) — reads tolerate a missing field.\n */\n last_fetch_failed_at?: number;\n}\n\nexport function emptyState(): UpdateState {\n return {\n last_check_at: 0,\n installed_version: \"\",\n latest_version: \"\",\n dismissed_versions: [],\n };\n}\n\nexport function getStatePath(): string {\n return join(getGlobalAlmanacDir(), \"update-state.json\");\n}\n\n/**\n * Read the state file. Missing, empty, or malformed → empty state.\n * We deliberately swallow all errors here: the update system is\n * best-effort, and a read failure must not break any command.\n */\nexport async function readState(path?: string): Promise<UpdateState> {\n const file = path ?? getStatePath();\n let raw: string;\n try {\n raw = await readFile(file, \"utf8\");\n } catch {\n return emptyState();\n }\n const trimmed = raw.trim();\n if (trimmed.length === 0) return emptyState();\n try {\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(\n (v): v is string => typeof v === \"string\" && v.length > 0,\n )\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 emptyState();\n }\n}\n\n/**\n * Write the state file atomically (tmp + rename). Makes the concurrent\n * \"two commands ran their update checks at once\" race safe — one rename\n * wins, the other is dropped. Creates `~/.almanac/` if missing.\n */\nexport async function writeState(\n state: UpdateState,\n path?: string,\n): Promise<void> {\n const file = path ?? getStatePath();\n await mkdir(dirname(file), { recursive: true });\n const body = `${JSON.stringify(state, null, 2)}\\n`;\n const tmp = `${file}.tmp`;\n await writeFile(tmp, body, \"utf8\");\n await rename(tmp, file);\n}\n","import { createRequire } from \"node:module\";\n\nimport { readState, writeState, type UpdateState } from \"./state.js\";\n\n/**\n * Background update check. Called by the detached worker (`--internal-\n * check-updates`) after any normal command exits; also reachable via\n * `almanac update --check` for a synchronous \"am I current?\" readout.\n *\n * Contract:\n * - Reads `~/.almanac/update-state.json` if present.\n * - If the last check is older than `cacheSeconds` (default 24h), queries\n * the npm registry for `codealmanac`'s `dist-tags.latest` and writes a\n * new state file.\n * - Network timeout is 3s: registry flakes must not prevent a check\n * cycle on the next invocation.\n * - All errors are swallowed; the returned state is always a usable\n * snapshot (possibly the old one when the fetch failed).\n *\n * The fetch function is injectable. Tests pass a stub; production uses\n * the native `globalThis.fetch`. No dependency on `node-fetch`.\n */\n\nexport interface CheckOptions {\n /** Override the installed version (prod: read from package.json). */\n installedVersion?: string;\n /** Cache window; no registry call if last check is newer than this. */\n cacheSeconds?: number;\n /** Network timeout in ms (default 3000). */\n timeoutMs?: number;\n /** Clock. Tests inject to make \"24h ago\" deterministic. */\n now?: () => number;\n /** Fetch function (default `globalThis.fetch`). */\n fetchFn?: typeof fetch;\n /** Override the state file path (tests point it at a tmpdir). */\n statePath?: string;\n /** Force a registry call regardless of the cache. Used by `update --check`. */\n force?: boolean;\n}\n\nexport interface CheckResult {\n /** The state after the check (either refreshed or unchanged). */\n state: UpdateState;\n /** True when a registry call actually happened this run. */\n fetched: boolean;\n /** True when the registry call failed (network / timeout / parse). */\n fetchFailed: boolean;\n}\n\nconst DEFAULT_CACHE_SECONDS = 24 * 60 * 60;\nconst DEFAULT_TIMEOUT_MS = 3000;\nconst REGISTRY_URL = \"https://registry.npmjs.org/codealmanac\";\n\nexport async function checkForUpdate(\n opts: CheckOptions = {},\n): Promise<CheckResult> {\n const now = opts.now ?? (() => Math.floor(Date.now() / 1000));\n const cacheSeconds = opts.cacheSeconds ?? DEFAULT_CACHE_SECONDS;\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const fetchFn = opts.fetchFn ?? globalThis.fetch;\n const installed = opts.installedVersion ?? readInstalledVersion();\n\n const state = await readState(opts.statePath);\n\n // Cache gate. Skip the registry call when the previous check is\n // fresh enough, unless `force: true` (used by `update --check` so\n // the user can see real-time status without waiting out the window).\n if (\n !opts.force &&\n state.last_check_at > 0 &&\n now() - state.last_check_at < cacheSeconds\n ) {\n return { state, fetched: false, fetchFailed: false };\n }\n\n // Query the registry with a hard timeout. `AbortController` is the\n // idiomatic Node 20+ way to bound a fetch; `setTimeout` fires abort,\n // `clearTimeout` cancels if fetch resolves first.\n let latest: string | null = null;\n let failed = false;\n try {\n const ac = new AbortController();\n const timer = setTimeout(() => ac.abort(), timeoutMs);\n try {\n const res = await fetchFn(REGISTRY_URL, {\n signal: ac.signal,\n headers: { accept: \"application/json\" },\n });\n if (!res.ok) {\n failed = true;\n } else {\n const body = (await res.json()) as {\n [\"dist-tags\"]?: { latest?: unknown };\n };\n const tag = body[\"dist-tags\"]?.latest;\n if (typeof tag === \"string\" && tag.length > 0) {\n latest = tag;\n } else {\n failed = true;\n }\n }\n } finally {\n clearTimeout(timer);\n }\n } catch {\n failed = true;\n }\n\n if (failed || latest === null) {\n // Record the failure but DON'T clobber the previous latest_version —\n // an offline check shouldn't make us forget that 0.1.6 is out.\n const next: UpdateState = {\n ...state,\n // We still bump last_check_at on a failed attempt; without this,\n // every subsequent command would re-try the registry. A one-shot\n // retry on the next invocation is enough; sustained failure gets\n // retried on the 24h cadence like a success.\n last_check_at: now(),\n installed_version: installed,\n last_fetch_failed_at: now(),\n };\n try {\n await writeState(next, opts.statePath);\n } catch {\n // Even the state write failed (permissions, disk full). Return\n // whatever we have — the CLI doesn't care.\n }\n return { state: next, fetched: true, fetchFailed: true };\n }\n\n const next: UpdateState = {\n last_check_at: now(),\n installed_version: installed,\n latest_version: latest,\n dismissed_versions: state.dismissed_versions,\n // Clear the failure marker on success.\n last_fetch_failed_at: undefined,\n };\n try {\n await writeState(next, opts.statePath);\n } catch {\n // Silent — same rationale as above.\n }\n return { state: next, fetched: true, fetchFailed: false };\n}\n\n/**\n * Read the `version` field from `package.json`. Matches the same\n * lookup strategy as `readPackageVersion` in `cli.ts` and `doctor.ts`;\n * duplicated here to keep the update module self-contained and to\n * avoid a circular import at CLI startup.\n */\nfunction readInstalledVersion(): string {\n try {\n const require = createRequire(import.meta.url);\n const pkg = require(\"../../package.json\") as { version?: unknown };\n if (typeof pkg.version === \"string\" && pkg.version.length > 0) {\n return pkg.version;\n }\n } catch {\n // Fall through.\n }\n try {\n const require = createRequire(import.meta.url);\n const pkg = require(\"../package.json\") as { version?: unknown };\n if (typeof pkg.version === \"string\" && pkg.version.length > 0) {\n return pkg.version;\n }\n } catch {\n // Fall through.\n }\n return \"unknown\";\n}\n"],"mappings":"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","names":["mkdir","readFile","rename","writeFile","dirname","join","join","readFile","mkdir","dirname","writeFile","rename","next","require"]}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
IMPORT_LINE
|
|
4
|
+
} from "./chunk-3LC55TG6.js";
|
|
5
|
+
import {
|
|
6
|
+
runHookUninstall
|
|
7
|
+
} from "./chunk-Z4MWLVS2.js";
|
|
8
|
+
|
|
9
|
+
// src/commands/uninstall.ts
|
|
10
|
+
import { existsSync } from "fs";
|
|
11
|
+
import { readFile, rm, writeFile } from "fs/promises";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
import path from "path";
|
|
14
|
+
var BLUE = "\x1B[38;5;75m";
|
|
15
|
+
var DIM = "\x1B[2m";
|
|
16
|
+
var RST = "\x1B[0m";
|
|
17
|
+
async function runUninstall(options = {}) {
|
|
18
|
+
const out = options.stdout ?? process.stdout;
|
|
19
|
+
const isTTY = options.isTTY ?? process.stdin.isTTY === true;
|
|
20
|
+
const interactive = isTTY && options.yes !== true;
|
|
21
|
+
const claudeDir = options.claudeDir ?? path.join(homedir(), ".claude");
|
|
22
|
+
out.write("\n");
|
|
23
|
+
let removeHook = true;
|
|
24
|
+
if (options.keepHook === true) {
|
|
25
|
+
removeHook = false;
|
|
26
|
+
} else if (interactive) {
|
|
27
|
+
removeHook = await confirm(
|
|
28
|
+
out,
|
|
29
|
+
"Remove the SessionEnd hook from ~/.claude/settings.json?",
|
|
30
|
+
true
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (removeHook) {
|
|
34
|
+
const res = await runHookUninstall({
|
|
35
|
+
settingsPath: options.settingsPath,
|
|
36
|
+
hookScriptPath: options.hookScriptPath
|
|
37
|
+
});
|
|
38
|
+
if (res.exitCode !== 0) {
|
|
39
|
+
return { stdout: "", stderr: res.stderr, exitCode: res.exitCode };
|
|
40
|
+
}
|
|
41
|
+
out.write(` ${BLUE}\u25C7${RST} ${res.stdout.trim()}
|
|
42
|
+
`);
|
|
43
|
+
} else {
|
|
44
|
+
out.write(` ${DIM}\u25CB Hook kept${RST}
|
|
45
|
+
`);
|
|
46
|
+
}
|
|
47
|
+
let removeGuides = true;
|
|
48
|
+
if (options.keepGuides === true) {
|
|
49
|
+
removeGuides = false;
|
|
50
|
+
} else if (interactive) {
|
|
51
|
+
removeGuides = await confirm(
|
|
52
|
+
out,
|
|
53
|
+
"Remove the guides + CLAUDE.md import line?",
|
|
54
|
+
true
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if (removeGuides) {
|
|
58
|
+
const summary = await removeGuideFiles(claudeDir);
|
|
59
|
+
if (summary.anyChanges) {
|
|
60
|
+
out.write(
|
|
61
|
+
` ${BLUE}\u25C7${RST} Guides removed (${summary.filesTouched.join(", ")})
|
|
62
|
+
`
|
|
63
|
+
);
|
|
64
|
+
} else {
|
|
65
|
+
out.write(` ${DIM}\u25CB Guides not installed${RST}
|
|
66
|
+
`);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
out.write(` ${DIM}\u25CB Guides kept${RST}
|
|
70
|
+
`);
|
|
71
|
+
}
|
|
72
|
+
out.write(`
|
|
73
|
+
${BLUE}\u25C7${RST} ${BLUE}Uninstall complete${RST}
|
|
74
|
+
|
|
75
|
+
`);
|
|
76
|
+
return { stdout: "", stderr: "", exitCode: 0 };
|
|
77
|
+
}
|
|
78
|
+
async function removeGuideFiles(claudeDir) {
|
|
79
|
+
const touched = [];
|
|
80
|
+
const mini = path.join(claudeDir, "codealmanac.md");
|
|
81
|
+
const ref = path.join(claudeDir, "codealmanac-reference.md");
|
|
82
|
+
const claudeMd = path.join(claudeDir, "CLAUDE.md");
|
|
83
|
+
if (existsSync(mini)) {
|
|
84
|
+
await rm(mini, { force: true });
|
|
85
|
+
touched.push("codealmanac.md");
|
|
86
|
+
}
|
|
87
|
+
if (existsSync(ref)) {
|
|
88
|
+
await rm(ref, { force: true });
|
|
89
|
+
touched.push("codealmanac-reference.md");
|
|
90
|
+
}
|
|
91
|
+
if (existsSync(claudeMd)) {
|
|
92
|
+
const existing = await readFile(claudeMd, "utf8");
|
|
93
|
+
const { changed, body } = removeImportLine(existing);
|
|
94
|
+
if (changed) {
|
|
95
|
+
if (body.trim().length === 0) {
|
|
96
|
+
await rm(claudeMd, { force: true });
|
|
97
|
+
touched.push("CLAUDE.md (deleted)");
|
|
98
|
+
} else {
|
|
99
|
+
await writeFile(claudeMd, body, "utf8");
|
|
100
|
+
touched.push("CLAUDE.md");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { anyChanges: touched.length > 0, filesTouched: touched };
|
|
105
|
+
}
|
|
106
|
+
function removeImportLine(contents) {
|
|
107
|
+
const eol = contents.includes("\r\n") ? "\r\n" : "\n";
|
|
108
|
+
const lines = contents.split(/\r?\n/);
|
|
109
|
+
const indices = [];
|
|
110
|
+
for (let i = 0; i < lines.length; i++) {
|
|
111
|
+
if (lines[i].trim() === IMPORT_LINE) indices.push(i);
|
|
112
|
+
}
|
|
113
|
+
if (indices.length === 0) return { changed: false, body: contents };
|
|
114
|
+
for (let i = indices.length - 1; i >= 0; i--) {
|
|
115
|
+
lines.splice(indices[i], 1);
|
|
116
|
+
}
|
|
117
|
+
let body = lines.join(eol);
|
|
118
|
+
body = body.replace(/\n\n\n+/g, "\n\n");
|
|
119
|
+
return { changed: true, body };
|
|
120
|
+
}
|
|
121
|
+
function confirm(out, question, defaultYes) {
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
const hint = defaultYes ? "[Y/n]" : "[y/N]";
|
|
124
|
+
out.write(` ${BLUE}\u25C6${RST} ${question} ${DIM}${hint}${RST} `);
|
|
125
|
+
let buf = "";
|
|
126
|
+
const onData = (chunk) => {
|
|
127
|
+
buf += chunk.toString("utf8");
|
|
128
|
+
const nl = buf.indexOf("\n");
|
|
129
|
+
if (nl === -1) return;
|
|
130
|
+
process.stdin.removeListener("data", onData);
|
|
131
|
+
process.stdin.pause();
|
|
132
|
+
const answer = buf.slice(0, nl).trim().toLowerCase();
|
|
133
|
+
const accepted = answer.length === 0 ? defaultYes : answer === "y" || answer === "yes";
|
|
134
|
+
resolve(accepted);
|
|
135
|
+
};
|
|
136
|
+
process.stdin.resume();
|
|
137
|
+
process.stdin.on("data", onData);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export {
|
|
142
|
+
runUninstall,
|
|
143
|
+
removeImportLine
|
|
144
|
+
};
|
|
145
|
+
//# sourceMappingURL=chunk-BJVZLP6O.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/uninstall.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { readFile, rm, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\n\nimport { runHookUninstall } from \"./hook.js\";\nimport { IMPORT_LINE } from \"./setup.js\";\n\n/**\n * `almanac uninstall` — the reverse of `setup`.\n *\n * Idempotent and order-insensitive: each step is a no-op if that\n * artifact was never installed. We remove exactly the things setup added,\n * nothing else:\n *\n * 1. The `@~/.claude/codealmanac.md` line from `~/.claude/CLAUDE.md`.\n * Other content stays untouched. If removing our line leaves the\n * file empty, we delete the file so our fingerprint doesn't persist\n * as zero bytes.\n * 2. The guide files `~/.claude/codealmanac.md` and\n * `~/.claude/codealmanac-reference.md`.\n * 3. The SessionEnd hook entry (delegated to `runHookUninstall`, which\n * already knows how to leave foreign entries alone).\n *\n * Flags:\n * --yes skip confirmations; remove everything\n * --keep-hook leave the hook alone\n * --keep-guides leave the guides + CLAUDE.md import alone\n *\n * Non-interactive (no TTY) → behaves as if `--yes` was passed. Same\n * contract as `setup`.\n */\n\nexport interface UninstallOptions {\n yes?: boolean;\n keepHook?: boolean;\n keepGuides?: boolean;\n\n // ─── Injection points ────────────────────────────────────────────\n settingsPath?: string;\n hookScriptPath?: string;\n claudeDir?: string;\n isTTY?: boolean;\n stdout?: NodeJS.WritableStream;\n}\n\nexport interface UninstallResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\nconst BLUE = \"\\x1b[38;5;75m\";\nconst DIM = \"\\x1b[2m\";\nconst RST = \"\\x1b[0m\";\n\nexport async function runUninstall(\n options: UninstallOptions = {},\n): Promise<UninstallResult> {\n const out = options.stdout ?? process.stdout;\n const isTTY =\n options.isTTY ?? (process.stdin.isTTY === true);\n const interactive = isTTY && options.yes !== true;\n const claudeDir = options.claudeDir ?? path.join(homedir(), \".claude\");\n\n out.write(\"\\n\");\n\n // Hook removal.\n let removeHook = true;\n if (options.keepHook === true) {\n removeHook = false;\n } else if (interactive) {\n removeHook = await confirm(\n out,\n \"Remove the SessionEnd hook from ~/.claude/settings.json?\",\n true,\n );\n }\n if (removeHook) {\n const res = await runHookUninstall({\n settingsPath: options.settingsPath,\n hookScriptPath: options.hookScriptPath,\n });\n if (res.exitCode !== 0) {\n return { stdout: \"\", stderr: res.stderr, exitCode: res.exitCode };\n }\n out.write(` ${BLUE}\\u25c7${RST} ${res.stdout.trim()}\\n`);\n } else {\n out.write(` ${DIM}\\u25cb Hook kept${RST}\\n`);\n }\n\n // Guide + import removal.\n let removeGuides = true;\n if (options.keepGuides === true) {\n removeGuides = false;\n } else if (interactive) {\n removeGuides = await confirm(\n out,\n \"Remove the guides + CLAUDE.md import line?\",\n true,\n );\n }\n if (removeGuides) {\n const summary = await removeGuideFiles(claudeDir);\n if (summary.anyChanges) {\n out.write(\n ` ${BLUE}\\u25c7${RST} Guides removed (${summary.filesTouched.join(\", \")})\\n`,\n );\n } else {\n out.write(` ${DIM}\\u25cb Guides not installed${RST}\\n`);\n }\n } else {\n out.write(` ${DIM}\\u25cb Guides kept${RST}\\n`);\n }\n\n out.write(`\\n ${BLUE}\\u25c7${RST} ${BLUE}Uninstall complete${RST}\\n\\n`);\n\n return { stdout: \"\", stderr: \"\", exitCode: 0 };\n}\n\ninterface RemoveGuidesResult {\n anyChanges: boolean;\n filesTouched: string[];\n}\n\nasync function removeGuideFiles(\n claudeDir: string,\n): Promise<RemoveGuidesResult> {\n const touched: string[] = [];\n\n const mini = path.join(claudeDir, \"codealmanac.md\");\n const ref = path.join(claudeDir, \"codealmanac-reference.md\");\n const claudeMd = path.join(claudeDir, \"CLAUDE.md\");\n\n if (existsSync(mini)) {\n await rm(mini, { force: true });\n touched.push(\"codealmanac.md\");\n }\n if (existsSync(ref)) {\n await rm(ref, { force: true });\n touched.push(\"codealmanac-reference.md\");\n }\n\n if (existsSync(claudeMd)) {\n const existing = await readFile(claudeMd, \"utf8\");\n const { changed, body } = removeImportLine(existing);\n if (changed) {\n // If the file is now content-free, delete it outright so our\n // installation leaves no trace. A user who was using CLAUDE.md\n // before we touched it still has their content; only the case\n // where CLAUDE.md contained nothing but our line gets cleaned up.\n if (body.trim().length === 0) {\n await rm(claudeMd, { force: true });\n touched.push(\"CLAUDE.md (deleted)\");\n } else {\n await writeFile(claudeMd, body, \"utf8\");\n touched.push(\"CLAUDE.md\");\n }\n }\n }\n\n return { anyChanges: touched.length > 0, filesTouched: touched };\n}\n\n/**\n * Remove the import line from a CLAUDE.md body. Match is line-anchored\n * (trimmed equality) so we don't munge a line that happens to include\n * the token as part of a longer string. Returns the unchanged body (and\n * `changed: false`) if the line isn't present — this is what makes the\n * command safe to run repeatedly.\n */\nexport function removeImportLine(contents: string): {\n changed: boolean;\n body: string;\n} {\n const eol = contents.includes(\"\\r\\n\") ? \"\\r\\n\" : \"\\n\";\n const lines = contents.split(/\\r?\\n/);\n\n const indices: number[] = [];\n for (let i = 0; i < lines.length; i++) {\n if (lines[i]!.trim() === IMPORT_LINE) indices.push(i);\n }\n if (indices.length === 0) return { changed: false, body: contents };\n\n // Remove the line(s). Iterate from the end so earlier indices stay\n // valid as we splice.\n for (let i = indices.length - 1; i >= 0; i--) {\n lines.splice(indices[i]!, 1);\n }\n\n let body = lines.join(eol);\n\n // Cleanup: collapse any double-blank that our removal created at the\n // spot the line used to live. A best-effort tidy — we don't try to\n // normalize the whole file.\n body = body.replace(/\\n\\n\\n+/g, \"\\n\\n\");\n\n return { changed: true, body };\n}\n\nfunction confirm(\n out: NodeJS.WritableStream,\n question: string,\n defaultYes: boolean,\n): Promise<boolean> {\n return new Promise((resolve) => {\n const hint = defaultYes ? \"[Y/n]\" : \"[y/N]\";\n out.write(` ${BLUE}\\u25c6${RST} ${question} ${DIM}${hint}${RST} `);\n\n let buf = \"\";\n const onData = (chunk: Buffer): void => {\n buf += chunk.toString(\"utf8\");\n const nl = buf.indexOf(\"\\n\");\n if (nl === -1) return;\n process.stdin.removeListener(\"data\", onData);\n process.stdin.pause();\n\n const answer = buf.slice(0, nl).trim().toLowerCase();\n const accepted =\n answer.length === 0\n ? defaultYes\n : answer === \"y\" || answer === \"yes\";\n resolve(accepted);\n };\n\n process.stdin.resume();\n process.stdin.on(\"data\", onData);\n });\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/ansi.ts
|
|
4
|
+
var useColor = (process.stdout.isTTY ?? false) && !("NO_COLOR" in process.env);
|
|
5
|
+
var RST = useColor ? "\x1B[0m" : "";
|
|
6
|
+
var BOLD = useColor ? "\x1B[1m" : "";
|
|
7
|
+
var DIM = useColor ? "\x1B[2m" : "";
|
|
8
|
+
var GREEN = useColor ? "\x1B[38;5;35m" : "";
|
|
9
|
+
var RED = useColor ? "\x1B[38;5;167m" : "";
|
|
10
|
+
var BLUE = useColor ? "\x1B[38;5;75m" : "";
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
RST,
|
|
14
|
+
BOLD,
|
|
15
|
+
DIM,
|
|
16
|
+
GREEN,
|
|
17
|
+
RED,
|
|
18
|
+
BLUE
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=chunk-FM3VRDK7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ansi.ts"],"sourcesContent":["/**\n * Shared ANSI escape-code constants. TTY-aware: when stdout is not a\n * terminal or `NO_COLOR` is set, every constant resolves to the empty\n * string so formatted output degrades to plain text without per-call\n * checks at every use site.\n *\n * See https://no-color.org/ for the `NO_COLOR` convention.\n */\n\nconst useColor =\n (process.stdout.isTTY ?? false) && !(\"NO_COLOR\" in process.env);\n\nexport const RST = useColor ? \"\\x1b[0m\" : \"\";\nexport const BOLD = useColor ? \"\\x1b[1m\" : \"\";\nexport const DIM = useColor ? \"\\x1b[2m\" : \"\";\nexport const GREEN = useColor ? \"\\x1b[38;5;35m\" : \"\";\nexport const RED = useColor ? \"\\x1b[38;5;167m\" : \"\";\nexport const BLUE = useColor ? \"\\x1b[38;5;75m\" : \"\";\nexport const YELLOW = useColor ? \"\\x1b[33m\" : \"\";\nexport const WHITE_BOLD = useColor ? \"\\x1b[1;37m\" : \"\";\nexport const BLUE_DIM = useColor ? \"\\x1b[38;5;69m\" : \"\";\nexport const ACCENT_BG = useColor\n ? \"\\x1b[48;5;252m\\x1b[38;5;16m\"\n : \"\";\n"],"mappings":";;;AASA,IAAM,YACH,QAAQ,OAAO,SAAS,UAAU,EAAE,cAAc,QAAQ;AAEtD,IAAM,MAAM,WAAW,YAAY;AACnC,IAAM,OAAO,WAAW,YAAY;AACpC,IAAM,MAAM,WAAW,YAAY;AACnC,IAAM,QAAQ,WAAW,kBAAkB;AAC3C,IAAM,MAAM,WAAW,mBAAmB;AAC1C,IAAM,OAAO,WAAW,kBAAkB;","names":[]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/helpers.ts
|
|
4
|
+
function emit(result) {
|
|
5
|
+
if (result.stderr.length > 0) process.stderr.write(result.stderr);
|
|
6
|
+
if (result.stdout.length > 0) process.stdout.write(result.stdout);
|
|
7
|
+
if (result.exitCode !== 0) process.exitCode = result.exitCode;
|
|
8
|
+
}
|
|
9
|
+
function collectOption(value, previous) {
|
|
10
|
+
return [...previous, value];
|
|
11
|
+
}
|
|
12
|
+
function parsePositiveInt(value) {
|
|
13
|
+
const n = Number.parseInt(value, 10);
|
|
14
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
15
|
+
throw new Error(`invalid --limit "${value}" (expected a non-negative integer)`);
|
|
16
|
+
}
|
|
17
|
+
return n;
|
|
18
|
+
}
|
|
19
|
+
async function readStdin() {
|
|
20
|
+
if (process.stdin.isTTY === true) return "";
|
|
21
|
+
const chunks = [];
|
|
22
|
+
for await (const chunk of process.stdin) {
|
|
23
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
24
|
+
}
|
|
25
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
emit,
|
|
30
|
+
collectOption,
|
|
31
|
+
parsePositiveInt,
|
|
32
|
+
readStdin
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=chunk-P3LDTCLB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli/helpers.ts"],"sourcesContent":["export interface CommandResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\nexport function emit(result: CommandResult): void {\n if (result.stderr.length > 0) process.stderr.write(result.stderr);\n if (result.stdout.length > 0) process.stdout.write(result.stdout);\n if (result.exitCode !== 0) process.exitCode = result.exitCode;\n}\n\nexport function collectOption(value: string, previous: string[]): string[] {\n return [...previous, value];\n}\n\nexport function parsePositiveInt(value: string): number {\n const n = Number.parseInt(value, 10);\n if (!Number.isFinite(n) || n < 0) {\n throw new Error(`invalid --limit \"${value}\" (expected a non-negative integer)`);\n }\n return n;\n}\n\nexport async function readStdin(): Promise<string> {\n if (process.stdin.isTTY === true) return \"\";\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\n }\n return Buffer.concat(chunks).toString(\"utf8\");\n}\n"],"mappings":";;;AAMO,SAAS,KAAK,QAA6B;AAChD,MAAI,OAAO,OAAO,SAAS,EAAG,SAAQ,OAAO,MAAM,OAAO,MAAM;AAChE,MAAI,OAAO,OAAO,SAAS,EAAG,SAAQ,OAAO,MAAM,OAAO,MAAM;AAChE,MAAI,OAAO,aAAa,EAAG,SAAQ,WAAW,OAAO;AACvD;AAEO,SAAS,cAAc,OAAe,UAA8B;AACzE,SAAO,CAAC,GAAG,UAAU,KAAK;AAC5B;AAEO,SAAS,iBAAiB,OAAuB;AACtD,QAAM,IAAI,OAAO,SAAS,OAAO,EAAE;AACnC,MAAI,CAAC,OAAO,SAAS,CAAC,KAAK,IAAI,GAAG;AAChC,UAAM,IAAI,MAAM,oBAAoB,KAAK,qCAAqC;AAAA,EAChF;AACA,SAAO;AACT;AAEA,eAAsB,YAA6B;AACjD,MAAI,QAAQ,MAAM,UAAU,KAAM,QAAO;AACzC,QAAM,SAAmB,CAAC;AAC1B,mBAAiB,SAAS,QAAQ,OAAO;AACvC,WAAO,KAAK,OAAO,SAAS,KAAK,IAAI,QAAQ,OAAO,KAAK,KAAK,CAAC;AAAA,EACjE;AACA,SAAO,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM;AAC9C;","names":[]}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
checkForUpdate,
|
|
4
|
+
getConfigPath,
|
|
5
|
+
getStatePath
|
|
6
|
+
} from "./chunk-AXFPUHBN.js";
|
|
7
|
+
|
|
8
|
+
// src/update/schedule.ts
|
|
9
|
+
import { spawn } from "child_process";
|
|
10
|
+
import { readFileSync } from "fs";
|
|
11
|
+
function scheduleBackgroundUpdateCheck(argv) {
|
|
12
|
+
if (!shouldSchedule(argv)) return;
|
|
13
|
+
const scriptPath = argv[1];
|
|
14
|
+
const nodeBin = process.execPath;
|
|
15
|
+
if (scriptPath === void 0 || scriptPath.length === 0) return;
|
|
16
|
+
try {
|
|
17
|
+
const child = spawn(
|
|
18
|
+
nodeBin,
|
|
19
|
+
[scriptPath, "--internal-check-updates"],
|
|
20
|
+
{
|
|
21
|
+
detached: true,
|
|
22
|
+
stdio: "ignore"
|
|
23
|
+
// Windows: with `detached: true` and no `stdio`, Node opens a
|
|
24
|
+
// console window — `"ignore"` prevents that.
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
child.unref();
|
|
28
|
+
child.on("error", () => {
|
|
29
|
+
});
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function shouldSchedule(argv) {
|
|
34
|
+
if (process.env.CODEALMANAC_SKIP_UPDATE_CHECK === "1") return false;
|
|
35
|
+
if (process.env.NODE_ENV === "test") return false;
|
|
36
|
+
if (process.env.VITEST !== void 0) return false;
|
|
37
|
+
if (argv.slice(2).includes("--internal-check-updates")) return false;
|
|
38
|
+
if (!notifierEnabled()) return false;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
function notifierEnabled() {
|
|
42
|
+
try {
|
|
43
|
+
const raw = readFileSync(getConfigPath(), "utf8");
|
|
44
|
+
const parsed = JSON.parse(raw);
|
|
45
|
+
if (parsed.update_notifier === false) return false;
|
|
46
|
+
return true;
|
|
47
|
+
} catch {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function runInternalUpdateCheck() {
|
|
52
|
+
try {
|
|
53
|
+
await checkForUpdate({});
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function readStateForDoctor(path) {
|
|
58
|
+
const file = path ?? getStatePath();
|
|
59
|
+
try {
|
|
60
|
+
const raw = readFileSync(file, "utf8");
|
|
61
|
+
const trimmed = raw.trim();
|
|
62
|
+
if (trimmed.length === 0) return null;
|
|
63
|
+
const parsed = JSON.parse(trimmed);
|
|
64
|
+
return {
|
|
65
|
+
last_check_at: typeof parsed.last_check_at === "number" ? parsed.last_check_at : 0,
|
|
66
|
+
installed_version: typeof parsed.installed_version === "string" ? parsed.installed_version : "",
|
|
67
|
+
latest_version: typeof parsed.latest_version === "string" ? parsed.latest_version : "",
|
|
68
|
+
dismissed_versions: Array.isArray(parsed.dismissed_versions) ? parsed.dismissed_versions.filter((v) => typeof v === "string") : [],
|
|
69
|
+
last_fetch_failed_at: typeof parsed.last_fetch_failed_at === "number" ? parsed.last_fetch_failed_at : void 0
|
|
70
|
+
};
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export {
|
|
77
|
+
scheduleBackgroundUpdateCheck,
|
|
78
|
+
runInternalUpdateCheck,
|
|
79
|
+
readStateForDoctor
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=chunk-QHQ6YH7U.js.map
|
|
@@ -0,0 +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":";;;;;;;;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":[]}
|