@shardflux/cli 0.15.0 → 0.15.2
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/CHANGELOG.md +21 -0
- package/README.md +5 -3
- package/dist/account-args.js +3 -3
- package/dist/account-commands.d.ts +2 -0
- package/dist/account-commands.js +18 -3
- package/dist/agent-commands.js +22 -8
- package/dist/agent-target-commands.d.ts +14 -0
- package/dist/agent-target-commands.js +73 -32
- package/dist/args.d.ts +1 -1
- package/dist/args.js +5 -2
- package/dist/commands.d.ts +1 -1
- package/dist/format.js +3 -1
- package/dist/main.js +30 -12
- package/dist/menubar.d.ts +57 -0
- package/dist/menubar.js +264 -45
- package/dist/registry.d.ts +48 -0
- package/dist/registry.js +102 -0
- package/dist/update-check.d.ts +9 -1
- package/dist/update-check.js +55 -17
- package/dist/update.d.ts +2 -0
- package/dist/update.js +26 -8
- package/package.json +2 -2
package/dist/update-check.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `shard` tells its user when it is outdated or no longer supported (0.5.0+). The
|
|
3
|
-
*
|
|
2
|
+
* `shard` tells its user when it is outdated or no longer supported (0.5.0+). The latest version comes from the
|
|
3
|
+
* registry (0.15.2+: npm's `latest` dist-tag, see registry.ts); the API's GET /v1/client-versions names the oldest
|
|
4
|
+
* version it still serves and, optionally, a version to stop recommending (`hold`).
|
|
4
5
|
*
|
|
5
|
-
* - At most one
|
|
6
|
-
*
|
|
7
|
-
* recorded before the
|
|
8
|
-
* - Started in parallel with the command (
|
|
6
|
+
* - At most one check per 12 hours per API URL and package: the answer is cached in <config dir>/update-check.json
|
|
7
|
+
* (`checked_at`, `latest`, `minimum_supported`, `upgrade_command`, and what each source said, with the registry's
|
|
8
|
+
* ETag). The attempt is recorded before the requests, so an unreachable network is not asked again for 12 hours.
|
|
9
|
+
* - Started in parallel with the command (two requests in parallel, 3 s timeout each, never throws) and
|
|
9
10
|
* printed on stderr after it. When the command ends first, `shard` waits at most ~300 ms and otherwise skips the
|
|
10
11
|
* notice (the next run uses what this one cached). Never changes the exit code; never printed after a command that
|
|
11
12
|
* failed with --json (the error document stays the only stderr content).
|
|
@@ -14,13 +15,16 @@
|
|
|
14
15
|
* - Off with SHARDFLUX_NO_UPDATE_CHECK=1 (also true/yes/on) or NO_UPDATE_NOTIFIER (the SDK's rules);
|
|
15
16
|
* `shard version --check` still asks.
|
|
16
17
|
*/
|
|
17
|
-
import {
|
|
18
|
+
import { clientVersionStatus, versionCheckDisabledByEnv } from '@shardflux/sdk';
|
|
18
19
|
import { readFileSync } from 'node:fs';
|
|
19
20
|
import { join } from 'node:path';
|
|
20
21
|
import { CLI_VERSION } from "./args.js";
|
|
21
22
|
import { writePrivateFile } from "./credentials.js";
|
|
23
|
+
import { NPM_REGISTRY, npmLatest, withRegistryLatest } from "./registry.js";
|
|
22
24
|
export const UPDATE_CHECK_FILE = 'update-check.json';
|
|
23
|
-
export const CHECK_INTERVAL_MS =
|
|
25
|
+
export const CHECK_INTERVAL_MS = 12 * 3600 * 1000;
|
|
26
|
+
/** The catalog request's timeout (ms). */
|
|
27
|
+
const CATALOG_TIMEOUT_MS = 3_000;
|
|
24
28
|
/** How long the end of a command waits for a check that is still running. */
|
|
25
29
|
export const NOTICE_WAIT_MS = 300;
|
|
26
30
|
export const CLI_DISTRIBUTION = { package: '@shardflux/cli', version: CLI_VERSION };
|
|
@@ -44,7 +48,26 @@ function entryOf(doc, apiUrl, pkg) {
|
|
|
44
48
|
const e = doc.checks[apiUrl]?.[pkg];
|
|
45
49
|
if (!isObj(e) || typeof e.checked_at !== 'string' || Number.isNaN(Date.parse(e.checked_at)))
|
|
46
50
|
return null;
|
|
47
|
-
return {
|
|
51
|
+
return {
|
|
52
|
+
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),
|
|
53
|
+
// An entry written before 0.15.2 has no sources: its `latest` came from the catalog.
|
|
54
|
+
catalog_latest: 'catalog_latest' in e ? strOrNull(e.catalog_latest) : strOrNull(e.latest), hold: strOrNull(e.hold), registry_latest: strOrNull(e.registry_latest), registry_etag: strOrNull(e.registry_etag),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** GET /v1/client-versions, or null when the API did not answer with JSON (never throws). */
|
|
58
|
+
async function fetchCatalog(apiUrl, f, userAgent) {
|
|
59
|
+
try {
|
|
60
|
+
const res = await f(new URL('/v1/client-versions', `${apiUrl.replace(/\/+$/, '')}/`), { method: 'GET', headers: { accept: 'application/json', 'user-agent': userAgent }, signal: AbortSignal.timeout(CATALOG_TIMEOUT_MS) });
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
await res.body?.cancel().catch(() => undefined);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const body = JSON.parse(await res.text());
|
|
66
|
+
return isObj(body) && Array.isArray(body.clients) ? body : null;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
48
71
|
}
|
|
49
72
|
/** The status the cached answer gives this distribution (the same rules as the SDK; no request). */
|
|
50
73
|
export function statusFromEntry(entry, d) {
|
|
@@ -71,7 +94,7 @@ export class UpdateCheck {
|
|
|
71
94
|
const cached = entryOf(readCache(this.#file), this.#o.apiUrl, this.#o.distribution.package);
|
|
72
95
|
return cached ? { status: statusFromEntry(cached, this.#o.distribution), checkedAt: cached.checked_at } : null;
|
|
73
96
|
}
|
|
74
|
-
/** Starts the check: from the cache when it is younger than
|
|
97
|
+
/** Starts the check: from the cache when it is younger than 12 h (unless `force`), else the registry and the API. */
|
|
75
98
|
start(force = false) {
|
|
76
99
|
const { apiUrl, distribution: d } = this.#o;
|
|
77
100
|
const now = (this.#o.now ?? Date.now)();
|
|
@@ -82,14 +105,29 @@ export class UpdateCheck {
|
|
|
82
105
|
return;
|
|
83
106
|
}
|
|
84
107
|
const checkedAt = new Date(now).toISOString();
|
|
85
|
-
// Record the attempt first:
|
|
86
|
-
//
|
|
108
|
+
// Record the attempt first: requests that never finish (the command ended, the network is down) still count,
|
|
109
|
+
// and the previous answer stays until a new one arrives.
|
|
87
110
|
this.#write(doc, { ...(cached ?? { latest: null, minimum_supported: null, upgrade_command: null, release_notes_url: null }), checked_at: checkedAt });
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
111
|
+
const f = this.#o.fetch;
|
|
112
|
+
this.#pending = Promise.all([
|
|
113
|
+
fetchCatalog(apiUrl, f, this.#o.userAgent),
|
|
114
|
+
npmLatest(d.package, { fetch: f, registry: this.#o.registry ?? NPM_REGISTRY, etag: cached?.registry_latest ? cached.registry_etag : null, userAgent: this.#o.userAgent }),
|
|
115
|
+
]).then(([catalog, registry]) => {
|
|
116
|
+
const registryLatest = registry?.notModified ? (cached?.registry_latest ?? null) : (registry?.latest ?? null);
|
|
117
|
+
if (catalog === null && registryLatest === null) {
|
|
118
|
+
// Nobody answered: the last answer stands.
|
|
119
|
+
return { status: cached ? statusFromEntry(cached, d) : statusFromEntry(null, d), checkedAt: cached ? cached.checked_at : null };
|
|
120
|
+
}
|
|
121
|
+
// A catalog that did not answer this time: what it said last time (minimum_supported, hold) still applies.
|
|
122
|
+
const base = catalog ?? (cached ? { clients: [{ package: d.package, ecosystem: 'npm', latest: cached.catalog_latest ?? null, minimum_supported: cached.minimum_supported, upgrade_command: cached.upgrade_command, release_notes_url: cached.release_notes_url, hold: cached.hold ?? null }] } : null);
|
|
123
|
+
const merged = withRegistryLatest(base, registryLatest ? { [d.package]: registryLatest } : {});
|
|
124
|
+
const own = merged.clients.find((c) => c.ecosystem === 'npm' && c.package === d.package);
|
|
125
|
+
const status = clientVersionStatus(merged, { package: d.package, version: d.version, ecosystem: 'npm' });
|
|
126
|
+
const raw = base?.clients.find((c) => c.ecosystem === 'npm' && c.package === d.package);
|
|
127
|
+
this.#write(readCache(this.#file), {
|
|
128
|
+
checked_at: checkedAt, latest: own?.latest ?? null, minimum_supported: own?.minimum_supported ?? null, upgrade_command: own?.upgrade_command ?? null, release_notes_url: own?.release_notes_url ?? null,
|
|
129
|
+
catalog_latest: strOrNull(raw?.latest), hold: own?.hold ?? null, registry_latest: registryLatest, registry_etag: registry?.etag ?? cached?.registry_etag ?? null,
|
|
130
|
+
});
|
|
93
131
|
return { status, checkedAt };
|
|
94
132
|
});
|
|
95
133
|
}
|
package/dist/update.d.ts
CHANGED
|
@@ -52,6 +52,8 @@ export interface UpdateDeps {
|
|
|
52
52
|
run?: Runner;
|
|
53
53
|
menubar?: MenubarManager;
|
|
54
54
|
}
|
|
55
|
+
/** True when nothing is behind but the menu bar app, and the app the npm package carries is already that version. */
|
|
56
|
+
export declare function onlyAppToSwap(pending: Component[], packagedApp: string | null): boolean;
|
|
55
57
|
export declare function update(ctx: Ctx, values: Record<string, unknown>, deps?: UpdateDeps): Promise<number>;
|
|
56
58
|
/** Records where `shard` runs, for the menu bar's update button. Written on macOS, only when it changed. */
|
|
57
59
|
export declare function recordInstall(dir: string, runtime: Runtime, distribution: {
|
package/dist/update.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `shard update` (0.13.5+): brings this computer's Shardflux install up to date in one step. It checks every local
|
|
3
|
-
* piece against GET /v1/client-versions
|
|
4
|
-
* the CLI and SDK inside it, the macOS menu bar app and a globally installed `@shardflux/mcp` — then runs one install
|
|
3
|
+
* piece against the latest versions on npm (0.15.2+; GET /v1/client-versions adds minimum_supported and hold) — the
|
|
4
|
+
* package that runs `shard` (the `shardflux` bundle or `@shardflux/cli`), the CLI and SDK inside it, the macOS menu bar app and a globally installed `@shardflux/mcp` — then runs one install
|
|
5
5
|
* of the latest versions with the package manager that installed `shard`, checks the result with the new `shard`,
|
|
6
6
|
* and restarts the menu bar on its new app. `--check` only reports.
|
|
7
7
|
*
|
|
@@ -18,7 +18,7 @@ import { AgentStateStore } from "./agent-state.js";
|
|
|
18
18
|
import { writePrivateFile } from "./credentials.js";
|
|
19
19
|
import { CodedError } from "./errors.js";
|
|
20
20
|
import { json } from "./format.js";
|
|
21
|
-
import {
|
|
21
|
+
import { compareAppVersions, MenubarManager, packagedAppPath } from "./menubar.js";
|
|
22
22
|
export const INSTALL_FILE = 'install.json';
|
|
23
23
|
export const LAST_UPDATE_FILE = 'last-update.json';
|
|
24
24
|
const LOCK_FILE = 'update.lock';
|
|
@@ -175,6 +175,10 @@ function npmFor(node) {
|
|
|
175
175
|
const sibling = join(dirname(node), process.platform === 'win32' ? 'npm.cmd' : 'npm');
|
|
176
176
|
return existsSync(sibling) ? sibling : 'npm';
|
|
177
177
|
}
|
|
178
|
+
/** True when nothing is behind but the menu bar app, and the app the npm package carries is already that version. */
|
|
179
|
+
export function onlyAppToSwap(pending, packagedApp) {
|
|
180
|
+
return pending.every(c => c.package === MENUBAR && packagedApp !== null && c.latest !== null && compareAppVersions(packagedApp, c.latest) >= 0);
|
|
181
|
+
}
|
|
178
182
|
export async function update(ctx, values, deps = {}) {
|
|
179
183
|
const run = deps.run ?? runProcess;
|
|
180
184
|
const menubar = deps.menubar ?? menubarManager(ctx);
|
|
@@ -193,10 +197,15 @@ export async function update(ctx, values, deps = {}) {
|
|
|
193
197
|
: `Shardflux is up to date (${versions}).\n`);
|
|
194
198
|
return 0;
|
|
195
199
|
}
|
|
196
|
-
|
|
197
|
-
|
|
200
|
+
// The npm package may already carry the latest menu bar app (installed with npm, not yet swapped in): no install.
|
|
201
|
+
const packagedApp = pending.some(c => c.package === MENUBAR) ? await menubar.packagedVersion().catch(() => null) : null;
|
|
202
|
+
if (onlyAppToSwap(pending, packagedApp)) {
|
|
198
203
|
const refreshed = await refreshMenubar(ctx, menubar);
|
|
199
|
-
|
|
204
|
+
const changes = refreshed.status === 'restarted' ? pending.map(c => ({ package: c.package, label: c.label, from: c.current, to: refreshed.version ?? null })) : [];
|
|
205
|
+
if (changes.length)
|
|
206
|
+
writeLastUpdate(ctx.store.dir, { status: 'ok', changes, message: `Updated ${changesText(changes)}`, command });
|
|
207
|
+
ctx.io.stdout.write(ctx.json ? json({ ...view, updated: changes.length > 0, ...(changes.length ? { changes } : {}), menubar: refreshed.status })
|
|
208
|
+
: changes.length ? `Updated ${changesText(changes)}.\n` : `Shardflux is up to date (${versions}).\n`);
|
|
200
209
|
return 0;
|
|
201
210
|
}
|
|
202
211
|
const argv = installArgv(install, specs, runtime ? npmFor(runtime.node) : 'npm');
|
|
@@ -269,7 +278,7 @@ async function refreshMenubar(ctx, menubar, runtime, run, env) {
|
|
|
269
278
|
}
|
|
270
279
|
else {
|
|
271
280
|
const packaged = await menubar.packagedVersion().catch(() => null);
|
|
272
|
-
if (!packaged || !before.version ||
|
|
281
|
+
if (!packaged || !before.version || compareAppVersions(before.version, packaged) >= 0)
|
|
273
282
|
return { status: 'current' };
|
|
274
283
|
await menubar.start(ctx.store.dir, ctx.apiUrl);
|
|
275
284
|
}
|
|
@@ -283,7 +292,16 @@ export function recordInstall(dir, runtime, distribution) {
|
|
|
283
292
|
script = realpathSync(script);
|
|
284
293
|
}
|
|
285
294
|
catch { /* keep the path */ }
|
|
286
|
-
|
|
295
|
+
// The app this CLI's npm package carries (0.15.2+): the menu bar compares itself with it and swaps itself in.
|
|
296
|
+
let menubarApp = packagedAppPath();
|
|
297
|
+
try {
|
|
298
|
+
if (menubarApp)
|
|
299
|
+
menubarApp = realpathSync(menubarApp);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
menubarApp = null;
|
|
303
|
+
}
|
|
304
|
+
const doc = { version: 1, node: runtime.node, script, package: distribution.package, package_version: distribution.version, cli_version: CLI_VERSION, ...(menubarApp ? { menubar_app: menubarApp } : {}) };
|
|
287
305
|
const text = `${JSON.stringify(doc, null, 2)}\n`;
|
|
288
306
|
try {
|
|
289
307
|
if (readFileSync(join(dir, INSTALL_FILE), 'utf8') === text)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shardflux/cli",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"type": "module",
|
|
5
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 serverless agent workspaces.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"typescript-eslint": "8.70.1"
|
|
59
59
|
},
|
|
60
60
|
"optionalDependencies": {
|
|
61
|
-
"@shardflux/menubar-darwin": "^0.3.
|
|
61
|
+
"@shardflux/menubar-darwin": "^0.3.2"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
64
|
"build": "node scripts/build.mjs",
|