@shardflux/cli 0.4.0 → 0.5.0

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.
@@ -0,0 +1,128 @@
1
+ /**
2
+ * `shard` tells its user when it is outdated or no longer supported (0.5.0+; contracts §30.4). The API's
3
+ * GET /v1/client-versions names the latest published version of every client and the oldest one it still serves.
4
+ *
5
+ * - At most one request per 24 hours per API URL and package: the answer is cached in
6
+ * <config dir>/update-check.json (`checked_at`, `latest`, `minimum_supported`, `upgrade_command`). The attempt is
7
+ * recorded before the request, so an unreachable API is not asked again for a day either.
8
+ * - Started in parallel with the command (the SDK's checkClientVersion: one request, 3 s timeout, never throws) and
9
+ * printed on stderr after it. When the command ends first, `shard` waits at most ~300 ms and otherwise skips the
10
+ * notice (the next run uses what this one cached). Never changes the exit code; never printed after a command that
11
+ * failed with --json (the error document stays the only stderr content).
12
+ * - Identity: `@shardflux/cli` at CLI_VERSION, or the distribution that runs the CLI (the `shardflux` npm bundle passes
13
+ * its own name and version to main()).
14
+ * - Off with SHARDFLUX_NO_UPDATE_CHECK=1 (also true/yes/on) or NO_UPDATE_NOTIFIER (the SDK's rules);
15
+ * `shard version --check` still asks.
16
+ */
17
+ import { checkClientVersion, clientVersionStatus, versionCheckDisabledByEnv } from '@shardflux/sdk';
18
+ import { readFileSync } from 'node:fs';
19
+ import { join } from 'node:path';
20
+ import { CLI_VERSION } from "./args.js";
21
+ import { writePrivateFile } from "./credentials.js";
22
+ export const UPDATE_CHECK_FILE = 'update-check.json';
23
+ export const CHECK_INTERVAL_MS = 24 * 3600 * 1000;
24
+ /** How long the end of a command waits for a check that is still running. */
25
+ export const NOTICE_WAIT_MS = 300;
26
+ export const CLI_DISTRIBUTION = { package: '@shardflux/cli', version: CLI_VERSION };
27
+ export function updateCheckDisabled(env) {
28
+ return versionCheckDisabledByEnv(env);
29
+ }
30
+ const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
31
+ const strOrNull = (v) => (typeof v === 'string' && v.length > 0 ? v : null);
32
+ function readCache(file) {
33
+ try {
34
+ const raw = JSON.parse(readFileSync(file, 'utf8'));
35
+ if (isObj(raw) && raw.version === 1 && isObj(raw.checks))
36
+ return raw;
37
+ }
38
+ catch {
39
+ // Missing or unreadable: a cache, so start over.
40
+ }
41
+ return { version: 1, checks: {} };
42
+ }
43
+ function entryOf(doc, apiUrl, pkg) {
44
+ const e = doc.checks[apiUrl]?.[pkg];
45
+ if (!isObj(e) || typeof e.checked_at !== 'string' || Number.isNaN(Date.parse(e.checked_at)))
46
+ return null;
47
+ return { checked_at: e.checked_at, latest: strOrNull(e.latest), minimum_supported: strOrNull(e.minimum_supported), upgrade_command: strOrNull(e.upgrade_command), release_notes_url: strOrNull(e.release_notes_url) };
48
+ }
49
+ /** The status the cached answer gives this distribution (the same rules as the SDK; no request). */
50
+ export function statusFromEntry(entry, d) {
51
+ const clients = entry && (entry.latest !== null || entry.minimum_supported !== null) ? [{ package: d.package, ecosystem: 'npm', ...entry }] : [];
52
+ return clientVersionStatus({ clients }, { package: d.package, version: d.version, ecosystem: 'npm' });
53
+ }
54
+ export function updateView(s, checkedAt) {
55
+ return { package: s.package, current: s.current, status: s.status, latest: s.latest, minimum_supported: s.minimumSupported, upgrade_command: s.upgradeCommand, checked_at: checkedAt };
56
+ }
57
+ /**
58
+ * One command's check: `start()` returns at once (a fresh cache answers without a request); `result(waitMs)` is the
59
+ * status, or null when the request has not finished within `waitMs`.
60
+ */
61
+ export class UpdateCheck {
62
+ #o;
63
+ #file;
64
+ #pending = null;
65
+ constructor(opts) {
66
+ this.#o = opts;
67
+ this.#file = join(opts.dir, UPDATE_CHECK_FILE);
68
+ }
69
+ /** The cached answer, whatever its age (no request). */
70
+ peek() {
71
+ const cached = entryOf(readCache(this.#file), this.#o.apiUrl, this.#o.distribution.package);
72
+ return cached ? { status: statusFromEntry(cached, this.#o.distribution), checkedAt: cached.checked_at } : null;
73
+ }
74
+ /** Starts the check: from the cache when it is younger than 24 h (unless `force`), else one request. */
75
+ start(force = false) {
76
+ const { apiUrl, distribution: d } = this.#o;
77
+ const now = (this.#o.now ?? Date.now)();
78
+ const doc = readCache(this.#file);
79
+ const cached = entryOf(doc, apiUrl, d.package);
80
+ if (!force && cached && now - Date.parse(cached.checked_at) < CHECK_INTERVAL_MS && Date.parse(cached.checked_at) <= now + 60_000) {
81
+ this.#pending = Promise.resolve({ status: statusFromEntry(cached, d), checkedAt: cached.checked_at });
82
+ return;
83
+ }
84
+ const checkedAt = new Date(now).toISOString();
85
+ // Record the attempt first: a request that never finishes (the command ended, the API is unreachable) still
86
+ // counts, and the previous answer stays until a new one arrives.
87
+ this.#write(doc, { ...(cached ?? { latest: null, minimum_supported: null, upgrade_command: null, release_notes_url: null }), checked_at: checkedAt });
88
+ this.#pending = checkClientVersion({ baseUrl: apiUrl, fetch: this.#o.fetch, package: d.package, version: d.version, ecosystem: 'npm', userAgent: this.#o.userAgent }).then((status) => {
89
+ const answered = status.latest !== null || status.minimumSupported !== null;
90
+ if (!answered)
91
+ return { status: cached ? statusFromEntry(cached, d) : status, checkedAt: cached ? cached.checked_at : null };
92
+ this.#write(readCache(this.#file), { checked_at: checkedAt, latest: status.latest, minimum_supported: status.minimumSupported, upgrade_command: status.upgradeCommand, release_notes_url: status.releaseNotesUrl });
93
+ return { status, checkedAt };
94
+ });
95
+ }
96
+ /** The status (with when it was fetched), or null when not started or still running after `waitMs`. */
97
+ async result(waitMs) {
98
+ if (!this.#pending)
99
+ return null;
100
+ let timer;
101
+ const late = new Promise((resolve) => {
102
+ timer = setTimeout(() => resolve(null), waitMs);
103
+ timer.unref();
104
+ });
105
+ try {
106
+ return await Promise.race([this.#pending.catch(() => null), late]);
107
+ }
108
+ finally {
109
+ clearTimeout(timer);
110
+ }
111
+ }
112
+ #write(doc, entry) {
113
+ try {
114
+ const { apiUrl, distribution: d } = this.#o;
115
+ doc.checks[apiUrl] = { ...(doc.checks[apiUrl] ?? {}), [d.package]: entry };
116
+ writePrivateFile(this.#o.dir, this.#file, `${JSON.stringify(doc, null, 2)}\n`);
117
+ }
118
+ catch {
119
+ // A cache: a read-only or missing config directory only means the next run asks again.
120
+ }
121
+ }
122
+ }
123
+ /** The one-line notice for stderr, or null when there is nothing to say. */
124
+ export function noticeOf(s) {
125
+ if (!s || (s.status !== 'outdated' && s.status !== 'unsupported') || !s.message)
126
+ return null;
127
+ return `${s.message}\n`;
128
+ }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@shardflux/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
- "description": "shard: the Shardflux command line. Open, run commands in, move files to, suspend, resume and fork persistent agent workspaces.",
5
+ "description": "shard: the Shardflux command line. Create an account, sign in, set up keys and billing, then open, run commands in, move files to, suspend, resume and fork persistent agent workspaces.",
6
6
  "license": "Apache-2.0",
7
7
  "homepage": "https://shardflux.dev",
8
8
  "keywords": [
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "yaml": "2.9.1",
43
- "@shardflux/sdk": "^0.7.0"
43
+ "@shardflux/sdk": "^0.9.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@eslint/js": "10.0.1",