codealmanac 0.1.6 → 0.1.7
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-2JJTTN7P.js +539 -0
- package/dist/chunk-2JJTTN7P.js.map +1 -0
- package/dist/chunk-3C5SY5SE.js +1239 -0
- package/dist/chunk-3C5SY5SE.js.map +1 -0
- package/dist/chunk-4CODZRHH.js +19 -0
- package/dist/chunk-4CODZRHH.js.map +1 -0
- package/dist/chunk-7JUX4ADQ.js +38 -0
- package/dist/chunk-7JUX4ADQ.js.map +1 -0
- package/dist/chunk-A6PUCAVJ.js +145 -0
- package/dist/chunk-A6PUCAVJ.js.map +1 -0
- package/dist/chunk-AXFPUHBN.js +227 -0
- package/dist/chunk-AXFPUHBN.js.map +1 -0
- package/dist/chunk-FM3VRDK7.js +20 -0
- package/dist/chunk-FM3VRDK7.js.map +1 -0
- package/dist/chunk-H6WU6PYH.js +441 -0
- package/dist/chunk-H6WU6PYH.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-AIH5QQ5H.js +393 -0
- package/dist/cli-AIH5QQ5H.js.map +1 -0
- package/dist/codealmanac.js +32 -5
- package/dist/codealmanac.js.map +1 -1
- package/dist/doctor-6FN5JO5F.js +15 -0
- package/dist/doctor-6FN5JO5F.js.map +1 -0
- package/dist/hook-CRJMWSSO.js +12 -0
- package/dist/hook-CRJMWSSO.js.map +1 -0
- package/dist/register-commands-PZMQNGCH.js +2644 -0
- package/dist/register-commands-PZMQNGCH.js.map +1 -0
- package/dist/uninstall-NBEZNNKM.js +12 -0
- package/dist/uninstall-NBEZNNKM.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 +1 -1
- 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,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":[]}
|