@bitmagic/cli 0.1.12 → 0.1.13

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 CHANGED
@@ -132,6 +132,17 @@ build` stops with a message naming `bitmagic upgrade`, which vendors the file wi
132
132
  your game code. Run the printed install command afterwards if `upgrade` reports missing
133
133
  dependencies too.
134
134
 
135
+ ## Staying current
136
+
137
+ Two nudges, both advisory — nothing is ever blocked because something is out of date.
138
+
139
+ - **The CLI itself.** Commands print a line when a newer `@bitmagic/cli` is on npm. The check reads
140
+ a cache written by an earlier run, so it costs no time; the cache refreshes in the background at
141
+ most once a day. An unreachable registry, a prerelease build, or a first-ever run all stay quiet.
142
+ - **The vendored engine.** `bitmagic dev` and `bitmagic publish` say when a newer engine exists and
143
+ suggest `bitmagic upgrade`. Publishing is never refused for an old engine — the prompt belongs
144
+ where rebuilding is cheap, not at the moment you ship.
145
+
135
146
  ## Development
136
147
 
137
148
  ```bash
package/dist/bin.js CHANGED
@@ -1,13 +1,42 @@
1
1
  #!/usr/bin/env node
2
2
  import { runMain } from 'citty';
3
+ import { createRequire } from 'module';
3
4
  import { main } from './cli.js';
4
5
  import { CliError } from './errors.js';
6
+ import { pendingUpdateNotice, refreshUpdateCacheInBackground } from './update/check.js';
7
+ /**
8
+ * This file, not cli.ts, because cli.ts must stay free of side effects — cli.test.ts imports it
9
+ * for `subCommands` and would otherwise trigger a version check against the test runner.
10
+ */
11
+ function currentCliVersion() {
12
+ try {
13
+ // dist/bin.js -> ../package.json is this package's own manifest.
14
+ const pkg = createRequire(import.meta.url)('../package.json');
15
+ return typeof pkg.version === 'string' ? pkg.version : '';
16
+ }
17
+ catch {
18
+ return '';
19
+ }
20
+ }
21
+ // Printed BEFORE the command runs, and read purely from a cache written by a previous run — so it
22
+ // costs no network and no measurable time here. A command that exits early (or via process.exit
23
+ // on a CliError) has still shown it, which printing afterwards could not guarantee.
24
+ const version = currentCliVersion();
25
+ const notice = pendingUpdateNotice(version);
26
+ if (notice !== null)
27
+ console.error(notice);
28
+ // Deliberately NOT awaited: this is for the next invocation, not this one. Bounded by its own
29
+ // short timeout so a sulking registry cannot hold a finished command open.
30
+ void refreshUpdateCacheInBackground();
5
31
  // The actual executable entrypoint — this is what package.json's `bin` points at. It is
6
32
  // deliberately the only place that calls `runMain`, and it does so unconditionally: unlike the
7
33
  // old path-comparison guard in cli.ts (which broke when invoked through a symlink, e.g. every
8
34
  // `npm i -g` install), there is no heuristic here to get wrong. Importing `./cli.js` never runs
9
35
  // the CLI by itself, so this file is the sole source of the "actually run it" side effect.
10
- runMain(main).catch((error) => {
36
+ // The version is attached HERE rather than in cli.ts's `meta`, because that module is imported by
37
+ // cli.test.ts for `subCommands` and is deliberately free of side effects — reading package.json at
38
+ // its module scope would break that. citty renders `meta.version` for `--version` and in usage.
39
+ runMain({ ...main, meta: { ...main.meta, version } }).catch((error) => {
11
40
  if (error instanceof CliError) {
12
41
  console.error(error.message);
13
42
  process.exit(error.exitCode);
package/dist/bin.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,wFAAwF;AACxF,+FAA+F;AAC/F,8FAA8F;AAC9F,gGAAgG;AAChG,2FAA2F;AAC3F,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IACrC,IAAI,KAAK,YAAY,QAAQ,EAAE,CAAC;QAC9B,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC7B,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACtE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAChC,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,mBAAmB,EAAE,8BAA8B,EAAE,MAAM,mBAAmB,CAAC;AAExF;;;GAGG;AACH,SAAS,iBAAiB;IACxB,IAAI,CAAC;QACH,iEAAiE;QACjE,MAAM,GAAG,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,iBAAiB,CAA0B,CAAC;QACvF,OAAO,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,kGAAkG;AAClG,gGAAgG;AAChG,oFAAoF;AACpF,MAAM,OAAO,GAAG,iBAAiB,EAAE,CAAC;AACpC,MAAM,MAAM,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;AAC5C,IAAI,MAAM,KAAK,IAAI;IAAE,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;AAE3C,8FAA8F;AAC9F,2EAA2E;AAC3E,KAAK,8BAA8B,EAAE,CAAC;AAEtC,wFAAwF;AACxF,+FAA+F;AAC/F,8FAA8F;AAC9F,gGAAgG;AAChG,2FAA2F;AAC3F,kGAAkG;AAClG,mGAAmG;AACnG,gGAAgG;AAChG,OAAO,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,EAAE,GAAI,IAAI,CAAC,IAAgC,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC1G,IAAI,KAAK,YAAY,QAAQ,EAAE,CAAC;QAC9B,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC7B,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACtE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -9,6 +9,7 @@
9
9
  * So the prompt lives where acting on it is cheap — during development, before there is anything
10
10
  * to lose by rebuilding.
11
11
  */
12
+ import { isOlderDottedVersion } from './version-compare.js';
12
13
  /**
13
14
  * Compare two `3.<build>` engine versions.
14
15
  *
@@ -17,23 +18,7 @@
17
18
  * fires on every run is a nudge people learn to ignore.
18
19
  */
19
20
  export function isOlderEngine(projectVersion, currentVersion) {
20
- const parse = (v) => {
21
- const parts = v.trim().split('.');
22
- if (parts.length === 0 || parts.some((p) => !/^\d+$/.test(p)))
23
- return null;
24
- return parts.map(Number);
25
- };
26
- const a = parse(projectVersion);
27
- const b = parse(currentVersion);
28
- if (a === null || b === null)
29
- return null;
30
- for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
31
- const left = a[i] ?? 0;
32
- const right = b[i] ?? 0;
33
- if (left !== right)
34
- return left < right;
35
- }
36
- return false;
21
+ return isOlderDottedVersion(projectVersion, currentVersion);
37
22
  }
38
23
  /**
39
24
  * The line to print, or null to stay quiet.
@@ -1 +1 @@
1
- {"version":3,"file":"engine-update.js","sourceRoot":"","sources":["../../src/project/engine-update.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,cAAsB,EAAE,cAAsB;IAC1E,MAAM,KAAK,GAAG,CAAC,CAAS,EAAmB,EAAE;QAC3C,MAAM,KAAK,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;QAC3E,OAAO,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,CAAC,CAAC;IACF,MAAM,CAAC,GAAG,KAAK,CAAC,cAAc,CAAC,CAAC;IAChC,MAAM,CAAC,GAAG,KAAK,CAAC,cAAc,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC1C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACzD,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACvB,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACxB,IAAI,IAAI,KAAK,KAAK;YAAE,OAAO,IAAI,GAAG,KAAK,CAAC;IAC1C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,cAAsB;AACtB,gGAAgG;AAChG,8EAA8E;AAC9E,cAAyC;IAEzC,IAAI,OAAO,cAAc,KAAK,QAAQ,IAAI,cAAc,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAC7E,IAAI,OAAO,cAAc,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACpD,IAAI,aAAa,CAAC,cAAc,EAAE,cAAc,CAAC,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACxE,OAAO,gCAAgC,cAAc,wBAAwB,cAAc,2CAA2C,CAAC;AACzI,CAAC"}
1
+ {"version":3,"file":"engine-update.js","sourceRoot":"","sources":["../../src/project/engine-update.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAK5D;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,cAAsB,EAAE,cAAsB;IAC1E,OAAO,oBAAoB,CAAC,cAAc,EAAE,cAAc,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,cAAsB;AACtB,gGAAgG;AAChG,8EAA8E;AAC9E,cAAyC;IAEzC,IAAI,OAAO,cAAc,KAAK,QAAQ,IAAI,cAAc,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAC7E,IAAI,OAAO,cAAc,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACpD,IAAI,aAAa,CAAC,cAAc,EAAE,cAAc,CAAC,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACxE,OAAO,gCAAgC,cAAc,wBAAwB,cAAc,2CAA2C,CAAC;AACzI,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Numeric comparison of dot-separated versions, shared by the engine nudge and the CLI-update
3
+ * nudge.
4
+ *
5
+ * Shared rather than copied because both compare the same shape for the same purpose, and two
6
+ * copies of a comparison is how `3.1009` ends up ranked below `3.980` in one place and not the
7
+ * other. Every part must be all digits — `0.1.12`, `3.1009` — which is what both version streams
8
+ * actually produce.
9
+ *
10
+ * Returns null when either side is unparseable (a prerelease tag, a locally-built `worktree`
11
+ * engine, a git describe string). Callers treat null as "say nothing": an unrecognised version is
12
+ * not evidence that anything is out of date, and a nudge that fires on every run is one people
13
+ * learn to ignore.
14
+ */
15
+ export declare function isOlderDottedVersion(candidate: string, reference: string): boolean | null;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Numeric comparison of dot-separated versions, shared by the engine nudge and the CLI-update
3
+ * nudge.
4
+ *
5
+ * Shared rather than copied because both compare the same shape for the same purpose, and two
6
+ * copies of a comparison is how `3.1009` ends up ranked below `3.980` in one place and not the
7
+ * other. Every part must be all digits — `0.1.12`, `3.1009` — which is what both version streams
8
+ * actually produce.
9
+ *
10
+ * Returns null when either side is unparseable (a prerelease tag, a locally-built `worktree`
11
+ * engine, a git describe string). Callers treat null as "say nothing": an unrecognised version is
12
+ * not evidence that anything is out of date, and a nudge that fires on every run is one people
13
+ * learn to ignore.
14
+ */
15
+ export function isOlderDottedVersion(candidate, reference) {
16
+ const parse = (value) => {
17
+ if (typeof value !== 'string')
18
+ return null;
19
+ const parts = value.trim().split('.');
20
+ if (parts.length === 0 || parts.some((part) => !/^\d+$/.test(part)))
21
+ return null;
22
+ return parts.map(Number);
23
+ };
24
+ const a = parse(candidate);
25
+ const b = parse(reference);
26
+ if (a === null || b === null)
27
+ return null;
28
+ for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
29
+ // A missing trailing part reads as 0, so `3.1` is older than `3.1.1` rather than unparseable.
30
+ const left = a[i] ?? 0;
31
+ const right = b[i] ?? 0;
32
+ if (left !== right)
33
+ return left < right;
34
+ }
35
+ return false;
36
+ }
37
+ //# sourceMappingURL=version-compare.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-compare.js","sourceRoot":"","sources":["../../src/project/version-compare.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,oBAAoB,CAAC,SAAiB,EAAE,SAAiB;IACvE,MAAM,KAAK,GAAG,CAAC,KAAa,EAAmB,EAAE;QAC/C,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAC3C,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;QACjF,OAAO,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,CAAC,CAAC;IAEF,MAAM,CAAC,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC;IAC3B,MAAM,CAAC,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC;IAC3B,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAE1C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACzD,8FAA8F;QAC9F,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACvB,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACxB,IAAI,IAAI,KAAK,KAAK;YAAE,OAAO,IAAI,GAAG,KAAK,CAAC;IAC1C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,28 @@
1
+ import { type UpdateCheckCache } from './notice.js';
2
+ export declare function updateCachePath(baseDir?: string): string;
3
+ /**
4
+ * The last recorded answer, or null if there is none to trust.
5
+ *
6
+ * Missing, unreadable and malformed all collapse to null on purpose: the caller's next move is
7
+ * identical in every case — say nothing now, refresh in the background — and this is a hint, not
8
+ * a thing worth failing a command over.
9
+ */
10
+ export declare function readUpdateCache(baseDir?: string): UpdateCheckCache | null;
11
+ export declare function writeUpdateCache(cache: UpdateCheckCache, baseDir?: string): void;
12
+ /** Ask npm what the newest published version is. Null on any failure. */
13
+ export declare function fetchLatestVersion(fetchImpl?: typeof globalThis.fetch): Promise<string | null>;
14
+ /**
15
+ * The notice to print right now, decided entirely from the cache.
16
+ *
17
+ * Nothing here awaits the network. A stale cache is refreshed by `refreshUpdateCacheInBackground`
18
+ * for the NEXT run, which is why a first-ever invocation is silent — the alternative is making
19
+ * every command wait on npm to be told something it could just as well be told tomorrow.
20
+ */
21
+ export declare function pendingUpdateNotice(currentVersion: string, baseDir?: string): string | null;
22
+ /**
23
+ * Refresh the cache if it is stale. Fire-and-forget: callers must not await this.
24
+ *
25
+ * Returns a promise only so tests can. In production the process may well exit first, which is
26
+ * fine — the next run reads whatever was written, and an unwritten cache simply stays stale.
27
+ */
28
+ export declare function refreshUpdateCacheInBackground(now?: number, baseDir?: string, fetchImpl?: typeof globalThis.fetch): Promise<void>;
@@ -0,0 +1,92 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { defaultBaseDir } from '../config/credentials.js';
4
+ import { cliUpdateNotice, isCacheStale } from './notice.js';
5
+ /** Public registry metadata for the published `latest` tag. No auth, no scope needed. */
6
+ const NPM_LATEST_URL = 'https://registry.npmjs.org/@bitmagic/cli/latest';
7
+ /**
8
+ * Cap on the background refresh.
9
+ *
10
+ * A pending fetch keeps Node alive, so an unbounded request would hold a finished command open
11
+ * for as long as the network sulks. Two seconds is generous for a registry metadata read and
12
+ * short enough that nobody notices when it is not.
13
+ */
14
+ const REFRESH_TIMEOUT_MS = 2000;
15
+ export function updateCachePath(baseDir = defaultBaseDir()) {
16
+ return path.join(baseDir, 'update-check.json');
17
+ }
18
+ function isCache(value) {
19
+ if (typeof value !== 'object' || value === null)
20
+ return false;
21
+ const c = value;
22
+ return (c.latest === null || typeof c.latest === 'string') && typeof c.checkedAt === 'number';
23
+ }
24
+ /**
25
+ * The last recorded answer, or null if there is none to trust.
26
+ *
27
+ * Missing, unreadable and malformed all collapse to null on purpose: the caller's next move is
28
+ * identical in every case — say nothing now, refresh in the background — and this is a hint, not
29
+ * a thing worth failing a command over.
30
+ */
31
+ export function readUpdateCache(baseDir = defaultBaseDir()) {
32
+ try {
33
+ const parsed = JSON.parse(fs.readFileSync(updateCachePath(baseDir), 'utf-8'));
34
+ return isCache(parsed) ? parsed : null;
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ export function writeUpdateCache(cache, baseDir = defaultBaseDir()) {
41
+ try {
42
+ fs.mkdirSync(baseDir, { recursive: true });
43
+ fs.writeFileSync(updateCachePath(baseDir), `${JSON.stringify(cache)}\n`);
44
+ }
45
+ catch {
46
+ // An unwritable home directory costs a cached hint, nothing else. The command proceeds.
47
+ }
48
+ }
49
+ /** Ask npm what the newest published version is. Null on any failure. */
50
+ export async function fetchLatestVersion(fetchImpl = globalThis.fetch) {
51
+ try {
52
+ const response = await fetchImpl(NPM_LATEST_URL, {
53
+ signal: AbortSignal.timeout(REFRESH_TIMEOUT_MS),
54
+ headers: { accept: 'application/json' },
55
+ });
56
+ if (!response.ok)
57
+ return null;
58
+ const body = await response.json();
59
+ if (typeof body !== 'object' || body === null)
60
+ return null;
61
+ const version = body.version;
62
+ return typeof version === 'string' ? version : null;
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ /**
69
+ * The notice to print right now, decided entirely from the cache.
70
+ *
71
+ * Nothing here awaits the network. A stale cache is refreshed by `refreshUpdateCacheInBackground`
72
+ * for the NEXT run, which is why a first-ever invocation is silent — the alternative is making
73
+ * every command wait on npm to be told something it could just as well be told tomorrow.
74
+ */
75
+ export function pendingUpdateNotice(currentVersion, baseDir) {
76
+ return cliUpdateNotice(currentVersion, readUpdateCache(baseDir)?.latest ?? null);
77
+ }
78
+ /**
79
+ * Refresh the cache if it is stale. Fire-and-forget: callers must not await this.
80
+ *
81
+ * Returns a promise only so tests can. In production the process may well exit first, which is
82
+ * fine — the next run reads whatever was written, and an unwritten cache simply stays stale.
83
+ */
84
+ export async function refreshUpdateCacheInBackground(now = Date.now(), baseDir, fetchImpl = globalThis.fetch) {
85
+ if (!isCacheStale(readUpdateCache(baseDir), now))
86
+ return;
87
+ const latest = await fetchLatestVersion(fetchImpl);
88
+ // Recorded even when the fetch failed, so an offline machine backs off for a day rather than
89
+ // retrying npm on every single command.
90
+ writeUpdateCache({ latest, checkedAt: now }, baseDir);
91
+ }
92
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/update/check.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * How stale a cached npm answer may be before it is refreshed.
3
+ *
4
+ * The check never blocks a command: the notice is printed from the cache, and a stale cache is
5
+ * refreshed in the background for next time. So this is only "how far behind the notice can be",
6
+ * not "how long a command waits".
7
+ */
8
+ export declare const UPDATE_CHECK_INTERVAL_MS: number;
9
+ /** What is remembered between runs, so no command pays for the network. */
10
+ export interface UpdateCheckCache {
11
+ /** The newest version npm reported, or null if the last check could not reach it. */
12
+ latest: string | null;
13
+ /** Epoch ms of that check. */
14
+ checkedAt: number;
15
+ }
16
+ export declare function isCacheStale(cache: UpdateCheckCache | null, now: number): boolean;
17
+ /**
18
+ * The line to print, or null to stay quiet.
19
+ *
20
+ * Quiet for everything except a confirmed newer release: an unreachable npm, an unparseable
21
+ * version on either side, and a CLI somehow ahead of the registry all produce nothing.
22
+ */
23
+ export declare function cliUpdateNotice(currentVersion: string, latestVersion: string | null | undefined): string | null;
@@ -0,0 +1,34 @@
1
+ import { isOlderDottedVersion } from '../project/version-compare.js';
2
+ /**
3
+ * How stale a cached npm answer may be before it is refreshed.
4
+ *
5
+ * The check never blocks a command: the notice is printed from the cache, and a stale cache is
6
+ * refreshed in the background for next time. So this is only "how far behind the notice can be",
7
+ * not "how long a command waits".
8
+ */
9
+ export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
10
+ export function isCacheStale(cache, now) {
11
+ if (cache === null)
12
+ return true;
13
+ // A clock that moved backwards (a resumed VM, a corrected clock) would otherwise pin the cache
14
+ // as fresh forever.
15
+ if (cache.checkedAt > now)
16
+ return true;
17
+ return now - cache.checkedAt >= UPDATE_CHECK_INTERVAL_MS;
18
+ }
19
+ /**
20
+ * The line to print, or null to stay quiet.
21
+ *
22
+ * Quiet for everything except a confirmed newer release: an unreachable npm, an unparseable
23
+ * version on either side, and a CLI somehow ahead of the registry all produce nothing.
24
+ */
25
+ export function cliUpdateNotice(currentVersion, latestVersion) {
26
+ if (typeof latestVersion !== 'string' || latestVersion === '')
27
+ return null;
28
+ if (typeof currentVersion !== 'string' || currentVersion === '')
29
+ return null;
30
+ if (isOlderDottedVersion(currentVersion, latestVersion) !== true)
31
+ return null;
32
+ return `A newer bitmagic CLI is available: ${latestVersion} (you have ${currentVersion}). Run \`npm i -g @bitmagic/cli\` to update.`;
33
+ }
34
+ //# sourceMappingURL=notice.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notice.js","sourceRoot":"","sources":["../../src/update/notice.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAErE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAU5D,MAAM,UAAU,YAAY,CAAC,KAA8B,EAAE,GAAW;IACtE,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAChC,+FAA+F;IAC/F,oBAAoB;IACpB,IAAI,KAAK,CAAC,SAAS,GAAG,GAAG;QAAE,OAAO,IAAI,CAAC;IACvC,OAAO,GAAG,GAAG,KAAK,CAAC,SAAS,IAAI,wBAAwB,CAAC;AAC3D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,cAAsB,EAAE,aAAwC;IAC9F,IAAI,OAAO,aAAa,KAAK,QAAQ,IAAI,aAAa,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAC3E,IAAI,OAAO,cAAc,KAAK,QAAQ,IAAI,cAAc,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAC7E,IAAI,oBAAoB,CAAC,cAAc,EAAE,aAAa,CAAC,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC9E,OAAO,sCAAsC,aAAa,cAAc,cAAc,8CAA8C,CAAC;AACvI,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitmagic/cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Build Bitmagic games from your own agent tool",
5
5
  "type": "module",
6
6
  "bin": {