@shardflux/cli 0.13.3 → 0.13.5
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 +22 -1
- package/README.md +22 -8
- package/dist/account-commands.js +6 -3
- package/dist/agent-commands.d.ts +1 -1
- package/dist/agent-commands.js +20 -5
- package/dist/agent-credentials.js +9 -2
- package/dist/agent-live.d.ts +1 -0
- package/dist/agent-live.js +3 -1
- package/dist/args.d.ts +1 -1
- package/dist/args.js +15 -2
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +5 -3
- package/dist/filesystem-commands.d.ts +4 -2
- package/dist/filesystem-commands.js +14 -3
- package/dist/handlers.js +2 -1
- package/dist/main.d.ts +3 -0
- package/dist/main.js +16 -2
- package/dist/menubar.d.ts +3 -0
- package/dist/menubar.js +5 -1
- package/dist/setup-cache.d.ts +5 -0
- package/dist/setup-cache.js +11 -0
- package/dist/update-check.d.ts +2 -1
- package/dist/update-check.js +3 -2
- package/dist/update.d.ts +61 -0
- package/dist/update.js +298 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.5
|
|
4
|
+
|
|
5
|
+
- `shard update` brings this computer's Shardflux install up to date in one step: it checks shard (the `shardflux`
|
|
6
|
+
package or `@shardflux/cli`), its SDK, the macOS menu bar app and a globally installed `@shardflux/mcp` against the
|
|
7
|
+
latest versions, runs one install with the package manager that installed shard (npm in its own prefix, pnpm, bun
|
|
8
|
+
or Volta), checks the result with the new shard and restarts the menu bar on its new app. `--check` only reports;
|
|
9
|
+
`--json` for both. A prefix npm cannot write to fails with `update_permission_denied` and the exact command to run.
|
|
10
|
+
- The menu bar's Update button runs `shard update`: each `shard` run on macOS records where it runs
|
|
11
|
+
(`install.json` in the config directory), and `shard update` records its outcome (`last-update.json`).
|
|
12
|
+
- `shard` replaces a running menu bar app that is older than the one its npm package carries (after
|
|
13
|
+
`npm i -g shardflux`, the old app used to keep running).
|
|
14
|
+
- The daily update notice and `shard version` say `Update: shard update`.
|
|
15
|
+
- `shard agents list` shows each session's latest message (LAST MESSAGE); `--json` returns it as `last_message`.
|
|
16
|
+
|
|
17
|
+
## 0.13.4
|
|
18
|
+
|
|
19
|
+
- Cloud Claude sign-in and reopening the machine editor preserve existing workspace labels while updating their own labels.
|
|
20
|
+
- `shard billing status` on the signup trial suggests `shard billing upgrade pro` when the live catalog offers Pro (the trial is 60 days of Pro from catalog live-v10); otherwise it points to `shard billing plans`.
|
|
21
|
+
- Reuse an account-private setup cache across projects after the agent starts; report cache scope with `shard template --json`.
|
|
22
|
+
- Machine and project filesystem status shows the current platform base and whether a rebase is pending.
|
|
23
|
+
|
|
3
24
|
## 0.13.3
|
|
4
25
|
|
|
5
26
|
- `shard ./` seeds the workspace repository with the checked-out branch and its upstream only, not every local branch, tag and stash. When that history is still above 256 MiB, the workspace fetches the branch from origin with its own Git credentials and receives only the commits origin does not have yet. The workspace branch tracks its origin branch.
|
|
@@ -13,7 +34,7 @@
|
|
|
13
34
|
- Git history above the first copy's 256 MiB limit fails with `repository_too_large` and its next step in the hint, not `internal_error`.
|
|
14
35
|
- A `revision_mismatch` suggests `--expected-revision` only for the file commands that take it; elsewhere it says to run the same command again.
|
|
15
36
|
- Config sync reports project entries keyed by paths on this computer (Codex `config.projects.*`) as one summary line.
|
|
16
|
-
- The command center does not show a setup step that `.shardflux/setup.yaml` marks `required: false` as the setup failure while the template saves, and reads skipped steps (coding template
|
|
37
|
+
- The command center does not show a setup step that `.shardflux/setup.yaml` marks `required: false` as the setup failure while the template saves, and reads skipped steps (coding template 10+).
|
|
17
38
|
|
|
18
39
|
## 0.13.2
|
|
19
40
|
|
package/README.md
CHANGED
|
@@ -264,9 +264,9 @@ plan only when Stripe confirms the payment, so `--wait` polls until the subscrip
|
|
|
264
264
|
open). An organization that already has a subscription changes plans in the billing portal: the
|
|
265
265
|
command prints the portal URL instead (exit 0).
|
|
266
266
|
|
|
267
|
-
On the signup trial (60 days
|
|
267
|
+
On the signup trial (60 days of Pro, no card), `billing status` shows when it ends and
|
|
268
268
|
what follows (0.6.2+): `plan trial, ends 2026-11-29 14:03:00Z, then free (trial, catalog ...)`, then
|
|
269
|
-
`keep these limits after the trial: shard billing upgrade
|
|
269
|
+
`keep these limits after the trial: shard billing upgrade pro` (0.13.4+). A subscription outranks the
|
|
270
270
|
trial and ends it. The trial is granted, never bought, so `billing plans` leaves it out.
|
|
271
271
|
|
|
272
272
|
`billing overage` shows and changes opt-in overage (owners and billing members). With it on,
|
|
@@ -283,7 +283,7 @@ already charged this period (`spend_cap_below_charges`); a plan without overage
|
|
|
283
283
|
```text
|
|
284
284
|
$ shard billing overage on --cap 9
|
|
285
285
|
Acme: overage is on, spend cap $9.00 per billing period ($1.00 to $9.00, the plan price).
|
|
286
|
-
Rates past the allowances: $0.
|
|
286
|
+
Rates past the allowances: $0.015 per RAM GiB-hour, $0.04 per CPU-hour, charged on the next invoice (this period: shard usage).
|
|
287
287
|
Saved (policy version 1).
|
|
288
288
|
```
|
|
289
289
|
|
|
@@ -346,12 +346,26 @@ and the session renewals of step-up and password changes).
|
|
|
346
346
|
- **Never printed:** the session and the saved key appear only in the file. Output shows key ids; error
|
|
347
347
|
output redacts anything shaped like a key or session token. `shard env` prints the key on purpose.
|
|
348
348
|
|
|
349
|
+
## Update
|
|
350
|
+
|
|
351
|
+
`shard update` **(0.13.5+)** brings everything Shardflux installed on this computer up to date: shard itself
|
|
352
|
+
(the `shardflux` package or `@shardflux/cli`), its SDK, the macOS menu bar app and a globally installed
|
|
353
|
+
`@shardflux/mcp`. It runs one install of the latest versions with the package manager that installed shard (npm,
|
|
354
|
+
pnpm, bun or Volta) and restarts the menu bar on its new app. The menu bar's **Update** button runs the same command.
|
|
355
|
+
|
|
356
|
+
```sh
|
|
357
|
+
shard update --check # what is behind, without installing
|
|
358
|
+
shard update
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Projects that depend on `@shardflux/sdk` or the Python `shardflux` package keep the versions their lockfiles pin.
|
|
362
|
+
|
|
349
363
|
## Version check
|
|
350
364
|
|
|
351
365
|
`shard` tells you when a newer version exists, on stderr after the command's output:
|
|
352
366
|
|
|
353
367
|
```text
|
|
354
|
-
@shardflux/cli 0.5
|
|
368
|
+
@shardflux/cli 0.13.5 is outdated: 0.14.0 is available. Update: shard update
|
|
355
369
|
```
|
|
356
370
|
|
|
357
371
|
or that the API no longer supports it (`... is no longer supported by the Shardflux API (minimum
|
|
@@ -828,12 +842,12 @@ amount:
|
|
|
828
842
|
```text
|
|
829
843
|
overage accruing (usage past the allowances is charged on the next invoice)
|
|
830
844
|
spend cap $9.00 (at most $9.00, the plan price)
|
|
831
|
-
overage charges $
|
|
845
|
+
overage charges $1.10 of $9.00 (12.22%), $7.90 left; billed on the next invoice
|
|
832
846
|
cap reached 2026-09-29 15:02:46Z (projected at this period's average use)
|
|
833
847
|
|
|
834
|
-
OVERAGE UNIT PAST ALLOWANCE BILLED RATE
|
|
835
|
-
ram_gib_hours gib_hours 70 60 $0.
|
|
836
|
-
cpu_hours hours 5 5 $0.
|
|
848
|
+
OVERAGE UNIT PAST ALLOWANCE BILLED RATE AMOUNT
|
|
849
|
+
ram_gib_hours gib_hours 70 60 $0.015 $0.90
|
|
850
|
+
cpu_hours hours 5 5 $0.04 $0.20
|
|
837
851
|
```
|
|
838
852
|
|
|
839
853
|
`--json` prints the API's summary, with `spend_cap` and `exhausted_reason`. Owners and billing members turn overage on
|
package/dist/account-commands.js
CHANGED
|
@@ -1403,12 +1403,15 @@ export const ACCOUNT_HANDLERS = {
|
|
|
1403
1403
|
const account = ctx.account();
|
|
1404
1404
|
const org = await resolveOrg(ctx, account, values);
|
|
1405
1405
|
const s = await account.billing.subscription(org.id);
|
|
1406
|
-
// The trial has
|
|
1406
|
+
// The trial has Pro's limits and allowances (catalog live-v10): Pro keeps them (a subscription outranks the trial and ends it).
|
|
1407
|
+
// Name Pro only while the live catalog sells it; a catalog without Pro gets the general hint.
|
|
1408
|
+
const trialToPro = !s.subscription && s.purchase_available && s.plan?.plan_key === 'trial'
|
|
1409
|
+
&& (await fetchBillingCatalog({ baseUrl: ctx.apiUrl, fetch: ctx.accountOptions().fetch ?? fetch }).catch(() => null))?.plans?.some((p) => p.plan_key === 'pro') === true;
|
|
1407
1410
|
const next = s.subscription
|
|
1408
1411
|
? 'manage it: shard billing portal'
|
|
1409
1412
|
: s.purchase_available
|
|
1410
|
-
?
|
|
1411
|
-
? 'keep these limits after the trial: shard billing upgrade
|
|
1413
|
+
? trialToPro
|
|
1414
|
+
? 'keep these limits after the trial: shard billing upgrade pro'
|
|
1412
1415
|
: 'buy a plan: shard billing plans, then shard billing upgrade <plan>'
|
|
1413
1416
|
: '';
|
|
1414
1417
|
out(ctx, `${billingStatusText(s, org)}${next ? `${next}\n` : ''}`, s);
|
package/dist/agent-commands.d.ts
CHANGED
|
@@ -3,6 +3,6 @@ import type { Ctx, Handler } from './commands.js';
|
|
|
3
3
|
/** `<slug>-<6 hex of sha256(identity)>`: the same repository gets the same key on every machine. */
|
|
4
4
|
export declare function disambiguatedSlug(slug: string, identity: string): string;
|
|
5
5
|
export declare function projectTemplate(ctx: Ctx, remote: string | null): Promise<string>;
|
|
6
|
-
export declare function agentPty(cell: CellClient, app: 'claude' | 'codex', agent: string, approvals: boolean, term: string, signal?: AbortSignal, project?: string, setupHash?: string, cwd?: string, ptyName?: string, onLaunch?: () => void): Promise<PtySession>;
|
|
6
|
+
export declare function agentPty(cell: CellClient, app: 'claude' | 'codex', agent: string, approvals: boolean, term: string, signal?: AbortSignal, project?: string, setupHash?: string, cwd?: string, ptyName?: string, onLaunch?: () => void, deferSetup?: boolean): Promise<PtySession>;
|
|
7
7
|
export declare const launchProject: Handler;
|
|
8
8
|
export declare const AGENT_HANDLERS: Record<string, Handler>;
|
package/dist/agent-commands.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { prepareSetupCache } from "./setup-cache.js";
|
|
1
2
|
import { agentReachability } from "./agent-reachability.js";
|
|
2
3
|
import { configureCredentialFill, watchCredentialPrompts } from "./credential-prompts.js";
|
|
3
4
|
import { createHash } from 'node:crypto';
|
|
@@ -39,11 +40,11 @@ export async function projectTemplate(ctx, remote) {
|
|
|
39
40
|
throw error;
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
|
-
export async function agentPty(cell, app, agent, approvals, term, signal, project, setupHash, cwd, ptyName, onLaunch) {
|
|
43
|
+
export async function agentPty(cell, app, agent, approvals, term, signal, project, setupHash, cwd, ptyName, onLaunch, deferSetup = false) {
|
|
43
44
|
const id = `shard-${ptyName || agent}-${app}`;
|
|
44
45
|
// Arrival and changed locks queue the same durable guest job even when the
|
|
45
46
|
// native agent is already alive. Setup never becomes its first user turn.
|
|
46
|
-
if (setupHash) {
|
|
47
|
+
if (setupHash && !deferSetup) {
|
|
47
48
|
const queued = await cell.exec.run(['sf-agent', 'setup-start', '--deps-hash', setupHash], { timeoutMs: 30_000, maxOutputBytes: 4096, signal, ...(cwd ? { cwd } : {}) });
|
|
48
49
|
if (queued.exitCode !== 0)
|
|
49
50
|
throw new Error(`project setup could not be queued: ${queued.stderr.trim() || `exit ${queued.exitCode}`}`);
|
|
@@ -59,7 +60,7 @@ export async function agentPty(cell, app, agent, approvals, term, signal, projec
|
|
|
59
60
|
}
|
|
60
61
|
onLaunch?.();
|
|
61
62
|
const launch = await cell.exec.run(['sf-agent', 'run', app, '--name', agent, '--detach', ...(ptyName ? ['--pty-name', ptyName] : []), ...(approvals ? ['--approvals'] : []), ...(setupHash ? ['--setup', '--setup-deps-hash', setupHash] : [])], {
|
|
62
|
-
env: { TERM: term, ...(project ? { SF_PROJECT_NAME: project } : {}) }, timeoutMs: 30_000, maxOutputBytes: 4096, signal, ...(cwd ? { cwd } : {}),
|
|
63
|
+
env: { TERM: term, ...(project ? { SF_PROJECT_NAME: project } : {}), ...(setupHash && deferSetup ? { SF_SETUP_DEFERRED: '1' } : {}) }, timeoutMs: 30_000, maxOutputBytes: 4096, signal, ...(cwd ? { cwd } : {}),
|
|
63
64
|
});
|
|
64
65
|
if (launch.exitCode !== 0)
|
|
65
66
|
throw new Error(`agent launch failed: ${launch.stderr.trim() || `exit ${launch.exitCode}`}`);
|
|
@@ -88,14 +89,25 @@ async function terminal(ctx, ws, app, agent, project, values, setupHash, cwd = w
|
|
|
88
89
|
try {
|
|
89
90
|
const credentialFill = await configureCredentialFill(ctx, ws);
|
|
90
91
|
// A cold launch is remembered before it starts, so the listing can show it before the projection does (agent-live.ts).
|
|
91
|
-
const session = await agentPty(cell, app, agent, values.approvals === true, ctx.io.env.TERM || 'xterm-256color', ctx.signal, ws.labels.shard_project || ws.key.split('/')[0], setupHash, cwd, ws.labels.agent_pty_name, () => { rememberLaunch(ctx, ws.id); progress(ctx, `Starting ${app === 'claude' ? 'Claude' : 'Codex'} in ${ws.key}…`); });
|
|
92
|
+
const session = await agentPty(cell, app, agent, values.approvals === true, ctx.io.env.TERM || 'xterm-256color', ctx.signal, ws.labels.shard_project || ws.key.split('/')[0], setupHash, cwd, ws.labels.agent_pty_name, () => { rememberLaunch(ctx, ws.id); progress(ctx, `Starting ${app === 'claude' ? 'Claude' : 'Codex'} in ${ws.key}…`); }, true);
|
|
93
|
+
// The real agent is running before any cache provisioning or mount wait.
|
|
94
|
+
const preparation = prepareSetupCache(ctx, ws).catch((error) => {
|
|
95
|
+
ctx.io.stderr.write(`Account setup cache unavailable: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
96
|
+
}).then(async () => {
|
|
97
|
+
if (!setupHash)
|
|
98
|
+
return;
|
|
99
|
+
const queued = await cell.exec.run(['sf-agent', 'setup-start', '--deps-hash', setupHash], { cwd, timeoutMs: 30_000, maxOutputBytes: 4096, signal: ctx.signal });
|
|
100
|
+
if (queued.exitCode !== 0)
|
|
101
|
+
throw new Error(`project setup could not be queued: ${queued.stderr.trim() || `exit ${queued.exitCode}`}`);
|
|
102
|
+
});
|
|
92
103
|
store.save(project, app);
|
|
93
104
|
if (!attach) {
|
|
105
|
+
await preparation;
|
|
94
106
|
ctx.io.stdout.write(json({ workspace: ws.data, session }));
|
|
95
107
|
return 0;
|
|
96
108
|
}
|
|
97
109
|
ctx.io.stderr.write(`Shardflux · ${ws.key}\nDetach: Ctrl-] then d\n`);
|
|
98
|
-
|
|
110
|
+
const attached = attachTerminal({
|
|
99
111
|
...(credentialFill ? { credentialPrompts: (ask, signal) => watchCredentialPrompts(ctx, ws, ask, signal) } : {}),
|
|
100
112
|
io: ctx.io, signal: ctx.signal, connect: cellConnector(cell, session.session_id),
|
|
101
113
|
wake: (signal) => ws.wake({ timeoutMs: ctx.wakeTimeoutMs, signal }),
|
|
@@ -108,8 +120,11 @@ async function terminal(ctx, ws, app, agent, project, values, setupHash, cwd = w
|
|
|
108
120
|
done();
|
|
109
121
|
}),
|
|
110
122
|
});
|
|
123
|
+
const [code] = await Promise.all([attached, preparation]);
|
|
124
|
+
return code;
|
|
111
125
|
}
|
|
112
126
|
finally {
|
|
127
|
+
cell.close();
|
|
113
128
|
await tunnels?.stop();
|
|
114
129
|
}
|
|
115
130
|
}
|
|
@@ -76,8 +76,15 @@ export const AGENT_CREDENTIAL_HANDLERS = {
|
|
|
76
76
|
}
|
|
77
77
|
requireClaudeTerminal(ctx);
|
|
78
78
|
const project = projectIdentity('./');
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
let ws;
|
|
80
|
+
if (typeof values.workspace === 'string')
|
|
81
|
+
ws = await resolveWorkspace(ctx, values.workspace);
|
|
82
|
+
else {
|
|
83
|
+
const cloud = ctx.cloud();
|
|
84
|
+
const key = `${project.slug}/main`;
|
|
85
|
+
const current = await cloud.workspaces.findByKey(key, { includeDeleted: false, signal: ctx.signal });
|
|
86
|
+
ws = await cloud.workspaces.open({ key, template: 'coding', labels: { ...current?.labels, ...projectLabels(project, 'main', 'claude') }, wait: { signal: ctx.signal, timeoutMs: ctx.wakeTimeoutMs } });
|
|
87
|
+
}
|
|
81
88
|
output(ctx, await loginClaudeInWorkspace(ctx, ws));
|
|
82
89
|
return 0;
|
|
83
90
|
},
|
package/dist/agent-live.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ interface GuestSession {
|
|
|
8
8
|
since_unix_ms: number;
|
|
9
9
|
title: string;
|
|
10
10
|
remote_control_url: string;
|
|
11
|
+
last_message?: string;
|
|
11
12
|
}
|
|
12
13
|
/** Parses `sf-agent list` stdout; anything malformed yields no sessions rather than a wrong row. */
|
|
13
14
|
export declare function guestSessions(stdout: string): GuestSession[];
|
package/dist/agent-live.js
CHANGED
|
@@ -61,7 +61,9 @@ export async function withLiveAgents(ctx, agents, filter, now = Date.now()) {
|
|
|
61
61
|
return [];
|
|
62
62
|
return guestSessions(result.stdout).map(s => ({
|
|
63
63
|
id: `${w.id}:${s.session_id}`, workspace_id: w.id, workspace_key: w.workspace_key, app: s.app, state: s.state,
|
|
64
|
-
since: new Date(s.since_unix_ms).toISOString(), title: s.title, remote_control_url: s.remote_control_url,
|
|
64
|
+
since: new Date(s.since_unix_ms).toISOString(), title: s.title, remote_control_url: s.remote_control_url,
|
|
65
|
+
// A guest from before the latest message (coding templates before it) lists none.
|
|
66
|
+
last_message: typeof s.last_message === 'string' ? s.last_message : '', labels: w.labels,
|
|
65
67
|
}));
|
|
66
68
|
}
|
|
67
69
|
catch (error) {
|
package/dist/args.d.ts
CHANGED
package/dist/args.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { parseArgs } from 'node:util';
|
|
13
13
|
import { ACCOUNT_COMMANDS } from "./account-args.js";
|
|
14
14
|
import { UsageError } from "./errors.js";
|
|
15
|
-
export const CLI_VERSION = '0.13.
|
|
15
|
+
export const CLI_VERSION = '0.13.5';
|
|
16
16
|
export const GLOBAL_OPTIONS = {
|
|
17
17
|
'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
|
|
18
18
|
json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
|
|
@@ -161,6 +161,19 @@ const KEY_COMMANDS = [
|
|
|
161
161
|
options: {},
|
|
162
162
|
},
|
|
163
163
|
{ path: ['whoami'], summary: 'Show the principal behind the project API key (SHARDFLUX_API_KEY, else the saved one)', positionals: [], options: {} },
|
|
164
|
+
{
|
|
165
|
+
path: ['update'],
|
|
166
|
+
summary: 'Update shard, the menu bar app and a global MCP server to their latest versions',
|
|
167
|
+
description: [
|
|
168
|
+
'Checks every local Shardflux piece against GET /v1/client-versions: shard (the shardflux package or @shardflux/cli),',
|
|
169
|
+
'its SDK, the macOS menu bar app and a globally installed @shardflux/mcp. When one is behind, runs one install of',
|
|
170
|
+
'the latest versions with the package manager that installed shard (npm, pnpm, bun or Volta), then restarts the',
|
|
171
|
+
'menu bar on its new app. --check only reports. Projects that depend on @shardflux/sdk or shardflux (Python) keep',
|
|
172
|
+
'their own versions.',
|
|
173
|
+
].join('\n'),
|
|
174
|
+
positionals: [],
|
|
175
|
+
options: { check: { type: 'boolean', description: 'Report what is behind without installing anything.' } },
|
|
176
|
+
},
|
|
164
177
|
{
|
|
165
178
|
path: ['version'],
|
|
166
179
|
summary: 'Print the CLI and SDK versions and whether a newer shard is available',
|
|
@@ -1540,7 +1553,7 @@ export const HELP_SECTIONS = [
|
|
|
1540
1553
|
{ title: 'Account (a user session: shard signup or shard auth login)', groups: ['signup', 'verify', 'auth', 'setup', 'env', 'orgs', 'projects', 'api-keys', 'members', 'invitations', 'billing', 'audit', 'account'] },
|
|
1541
1554
|
{ title: 'Workspaces (a project API key: SHARDFLUX_API_KEY, else the one shard setup saved)', groups: ['login', 'whoami', 'status', 'usage', 'machine', 'template', 'agents', 'ssh-proxy', 'workspaces', 'files', 'operations', 'executions', 'secrets', 'credentials', 'volumes', 'egress'] },
|
|
1542
1555
|
{ title: 'Templates (publish and archive use the session; the rest the API key)', groups: ['templates'] },
|
|
1543
|
-
{ title: 'Other', groups: ['menubar', 'feedback', 'version'] },
|
|
1556
|
+
{ title: 'Other', groups: ['menubar', 'update', 'feedback', 'version'] },
|
|
1544
1557
|
];
|
|
1545
1558
|
export function topHelp() {
|
|
1546
1559
|
const w = Math.max(...COMMANDS.map((c) => usageLine(c).length));
|
package/dist/commands.d.ts
CHANGED
|
@@ -64,6 +64,10 @@ export interface Ctx {
|
|
|
64
64
|
input: Input;
|
|
65
65
|
/** The package that runs the CLI (the version check's identity). */
|
|
66
66
|
distribution: Distribution;
|
|
67
|
+
/** The Node.js binary and script of this process; null when run in-process (tests, embedders). */
|
|
68
|
+
runtime: import('./update.js').Runtime | null;
|
|
69
|
+
/** GET /v1/client-versions (unauthenticated): the latest version of every client. */
|
|
70
|
+
clientVersions(): Promise<unknown>;
|
|
67
71
|
/** This machine's name (default API key name of `shard setup`). */
|
|
68
72
|
hostname(): string;
|
|
69
73
|
}
|
package/dist/commands.js
CHANGED
|
@@ -818,7 +818,9 @@ export const KEY_HANDLERS = {
|
|
|
818
818
|
// A complete listing also shows sessions the projection has not picked up yet (agent-live.ts).
|
|
819
819
|
if (!next && !str(values.cursor))
|
|
820
820
|
rows.splice(0, rows.length, ...await withLiveAgents(ctx, rows, { ...(state ? { state } : {}), labels }));
|
|
821
|
-
|
|
821
|
+
// LAST MESSAGE (0.13.4+): the session's latest message, from an API that reports it.
|
|
822
|
+
const message = (a) => !a.last_message ? '-' : a.last_message.length > 60 ? `${a.last_message.slice(0, 59)}…` : a.last_message;
|
|
823
|
+
out(ctx, rows.length ? table(['WORKSPACE', 'APP', 'STATE', 'TITLE', 'LAST MESSAGE'], rows.map(a => [a.workspace_key, a.app, a.state, a.title, message(a)])) : 'No agent sessions.\n', { data: rows, next_cursor: next });
|
|
822
824
|
if (!ctx.json && next)
|
|
823
825
|
ctx.io.stderr.write(`more: shard agents list --cursor ${next}\n`);
|
|
824
826
|
return 0;
|
|
@@ -2374,9 +2376,9 @@ function updateLine(u, d) {
|
|
|
2374
2376
|
case 'current':
|
|
2375
2377
|
return `up to date${pkg}: latest ${u.latest ?? '-'}${when}\n`;
|
|
2376
2378
|
case 'outdated':
|
|
2377
|
-
return `update available${pkg}: ${u.latest} (you have ${u.current}). Update:
|
|
2379
|
+
return `update available${pkg}: ${u.latest} (you have ${u.current}). Update: shard update\n`;
|
|
2378
2380
|
case 'unsupported':
|
|
2379
|
-
return `no longer supported${pkg}: the API needs ${u.minimum_supported} or later (latest ${u.latest ?? '-'}). Update:
|
|
2381
|
+
return `no longer supported${pkg}: the API needs ${u.minimum_supported} or later (latest ${u.latest ?? '-'}). Update: shard update\n`;
|
|
2380
2382
|
default:
|
|
2381
2383
|
return `update status unknown${pkg}${when} (the API could not be asked, or has no release of ${u.package} yet)\n`;
|
|
2382
2384
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { AgentFilesystem } from '@shardflux/sdk';
|
|
2
2
|
import type { Handler } from './commands.js';
|
|
3
|
-
/** One line per status: what is in use and what is saving or failed. */
|
|
4
|
-
export declare function filesystemLine(state: AgentFilesystem
|
|
3
|
+
/** One line per status: what is in use and what is saving or failed, its platform base and setup cache. */
|
|
4
|
+
export declare function filesystemLine(state: AgentFilesystem & {
|
|
5
|
+
cache_scope?: string;
|
|
6
|
+
}): string;
|
|
5
7
|
export declare const FILESYSTEM_HANDLERS: Record<string, Handler>;
|
|
@@ -2,14 +2,19 @@ import { waitTimeout } from "./commands.js";
|
|
|
2
2
|
import { UsageError } from "./errors.js";
|
|
3
3
|
import { projectIdentity, normalizeRemote } from "./project-identity.js";
|
|
4
4
|
import { AGENT_HANDLERS } from "./agent-commands.js";
|
|
5
|
+
import { readSetupProgress } from "./assisted-setup.js";
|
|
5
6
|
import { syncAgentConfig } from "./agent-config-sync.js";
|
|
6
7
|
import { progress } from "./progress.js";
|
|
7
8
|
function remote() {
|
|
8
9
|
const project = projectIdentity('./');
|
|
9
10
|
return { project, repoRemote: project.remote ?? normalizeRemote(`file://${project.folder}`) };
|
|
10
11
|
}
|
|
11
|
-
/** One line per status: what is in use and what is saving or failed. */
|
|
12
|
+
/** One line per status: what is in use and what is saving or failed, its platform base and setup cache. */
|
|
12
13
|
export function filesystemLine(state) {
|
|
14
|
+
const base = state.platform_base ? `; platform ${state.platform_base.slug}@${state.platform_base.version}${state.rebase_pending ? ', rebase pending' : ''}` : '';
|
|
15
|
+
return usageLine(state) + base + (state.cache_scope ? `; cache: ${state.cache_scope}` : '');
|
|
16
|
+
}
|
|
17
|
+
function usageLine(state) {
|
|
13
18
|
const current = state.version > 0 ? `version ${state.version}` : '';
|
|
14
19
|
if (state.state === 'building' && state.pending)
|
|
15
20
|
return `${state.level}: saving version ${state.pending.version}${current ? ` (${current} stays in use until it is ready)` : ''}`;
|
|
@@ -56,7 +61,7 @@ export const FILESYSTEM_HANDLERS = {
|
|
|
56
61
|
else if (existing.data.observed_state !== 'running')
|
|
57
62
|
progress(ctx, `Resuming your machine VM (${key})…`);
|
|
58
63
|
const ws = await cloud.workspaces.open({ key, template: state.template ? `${state.template.slug}@${state.template.version}` : 'coding',
|
|
59
|
-
labels: { shard_layer: 'machine', shard_machine: state.organization_id, repo_path: '/home/user', app: 'claude', agent: 'machine' },
|
|
64
|
+
labels: { ...existing?.labels, shard_layer: 'machine', shard_machine: state.organization_id, repo_path: '/home/user', app: 'claude', agent: 'machine' },
|
|
60
65
|
wait: { timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal } });
|
|
61
66
|
if (state.version === 0) {
|
|
62
67
|
progress(ctx, 'Syncing your Claude and Codex config into the machine…');
|
|
@@ -77,7 +82,13 @@ export const FILESYSTEM_HANDLERS = {
|
|
|
77
82
|
return output(ctx, await ctx.cloud().machine.rollback({ to: rollbackTo(values.to) }, { signal: ctx.signal }));
|
|
78
83
|
},
|
|
79
84
|
async template(ctx) {
|
|
80
|
-
|
|
85
|
+
const { project, repoRemote } = remote();
|
|
86
|
+
const state = await ctx.cloud().templates.getProject(repoRemote, { signal: ctx.signal });
|
|
87
|
+
const ws = await ctx.cloud().workspaces.findByKey(`${project.slug}/main`, { signal: ctx.signal });
|
|
88
|
+
const progress = ws ? await readSetupProgress(ctx, ws.data) : undefined;
|
|
89
|
+
const attachments = ws ? await ctx.cloud().volumes.listAttached(ws.id) : undefined;
|
|
90
|
+
const mounted = attachments?.data.some((a) => a.mount_path === '/mnt/shardflux-cache' && a.mounted && a.volume.name === 'shardflux-setup-cache' && a.volume.org_shared);
|
|
91
|
+
return output(ctx, { ...state, cache_scope: progress?.cache_scope ?? (mounted ? 'account' : 'workspace') });
|
|
81
92
|
},
|
|
82
93
|
async 'template save'(ctx, values) {
|
|
83
94
|
const { project, repoRemote } = remote();
|
package/dist/handlers.js
CHANGED
|
@@ -11,5 +11,6 @@ import { TARGET_HANDLERS } from "./agent-target-commands.js";
|
|
|
11
11
|
import { CENTER_HANDLERS } from "./command-center-actions.js";
|
|
12
12
|
import { AGENT_GIT_HANDLERS } from "./agent-git-commands.js";
|
|
13
13
|
import { MENUBAR_HANDLERS } from "./menubar.js";
|
|
14
|
+
import { UPDATE_HANDLERS } from "./update.js";
|
|
14
15
|
import { FILESYSTEM_HANDLERS } from "./filesystem-commands.js";
|
|
15
|
-
export const HANDLERS = { ...CREDENTIAL_HANDLERS, ...AGENT_CREDENTIAL_HANDLERS, ...KEY_HANDLERS, ...ACCOUNT_HANDLERS, ...AGENT_HANDLERS, ...CENTER_HANDLERS, ...TARGET_HANDLERS, ...AGENT_GIT_HANDLERS, ...MENUBAR_HANDLERS, ...FILESYSTEM_HANDLERS };
|
|
16
|
+
export const HANDLERS = { ...CREDENTIAL_HANDLERS, ...AGENT_CREDENTIAL_HANDLERS, ...KEY_HANDLERS, ...ACCOUNT_HANDLERS, ...AGENT_HANDLERS, ...CENTER_HANDLERS, ...TARGET_HANDLERS, ...AGENT_GIT_HANDLERS, ...MENUBAR_HANDLERS, ...FILESYSTEM_HANDLERS, ...UPDATE_HANDLERS };
|
package/dist/main.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Io } from './commands.js';
|
|
2
|
+
import type { Runtime } from './update.js';
|
|
2
3
|
import type { Distribution } from './update-check.js';
|
|
3
4
|
/** A sleep that ends early when the signal aborts (so Ctrl-C never waits out a poll interval). */
|
|
4
5
|
export declare function abortableSleep(signal: AbortSignal): (ms: number) => Promise<void>;
|
|
@@ -6,6 +7,8 @@ export interface RunOptions {
|
|
|
6
7
|
signal?: AbortSignal;
|
|
7
8
|
/** The package that runs the CLI, for the version check (default @shardflux/cli at CLI_VERSION). */
|
|
8
9
|
distribution?: Distribution;
|
|
10
|
+
/** The Node.js binary and script of this `shard` process (main() sets them; `shard update` needs them). */
|
|
11
|
+
runtime?: Runtime;
|
|
9
12
|
}
|
|
10
13
|
export declare function run(argv: readonly string[], io: Io, opts?: RunOptions): Promise<number>;
|
|
11
14
|
export interface MainOptions {
|
package/dist/main.js
CHANGED
|
@@ -17,6 +17,7 @@ import { CLI_VERSION, CENTER_COMMAND, PROJECT_COMMAND, commandHelp, parseCommand
|
|
|
17
17
|
import { commandCenter } from "./command-center.js";
|
|
18
18
|
import { housekeeping, startupHousekeeping } from "./agent-git-commands.js";
|
|
19
19
|
import { CredentialStore, configDir } from "./credentials.js";
|
|
20
|
+
import { recordInstall } from "./update.js";
|
|
20
21
|
import { AuthConfigError, EXIT, ExecutionPendingError, InterruptedError, UsageError, describeError, exitCodeFor, ownHint, redact } from "./errors.js";
|
|
21
22
|
import { errorText, feedbackCommand, json, time } from "./format.js";
|
|
22
23
|
import { HANDLERS } from "./handlers.js";
|
|
@@ -528,6 +529,15 @@ export async function run(argv, io, opts = {}) {
|
|
|
528
529
|
store,
|
|
529
530
|
input: new Input(io.stdin, io.stderr),
|
|
530
531
|
distribution,
|
|
532
|
+
runtime: opts.runtime ?? null,
|
|
533
|
+
async clientVersions() {
|
|
534
|
+
const res = await base(new URL('/v1/client-versions', `${apiUrl.replace(/\/+$/, '')}/`), {
|
|
535
|
+
headers: { accept: 'application/json', 'user-agent': userAgent }, signal: AbortSignal.any([signal, AbortSignal.timeout(10_000)]),
|
|
536
|
+
});
|
|
537
|
+
if (!res.ok)
|
|
538
|
+
throw new UsageError(`Could not read the latest versions from ${apiUrl} (HTTP ${res.status}).`);
|
|
539
|
+
return res.json();
|
|
540
|
+
},
|
|
531
541
|
hostname: () => {
|
|
532
542
|
try {
|
|
533
543
|
return hostname();
|
|
@@ -616,7 +626,7 @@ export async function run(argv, io, opts = {}) {
|
|
|
616
626
|
},
|
|
617
627
|
};
|
|
618
628
|
// The update check runs next to the command (not for `version`, which reports it itself).
|
|
619
|
-
if (!updateCheckDisabled(io.env) && !['version', 'ssh-proxy'].includes(parsed.command.path[0])) {
|
|
629
|
+
if (!updateCheckDisabled(io.env) && !['version', 'update', 'ssh-proxy'].includes(parsed.command.path[0])) {
|
|
620
630
|
try {
|
|
621
631
|
updates = new UpdateCheck({ dir: store.dir, apiUrl, distribution, fetch: base, userAgent });
|
|
622
632
|
updates.start();
|
|
@@ -684,7 +694,11 @@ export async function main(opts = {}) {
|
|
|
684
694
|
controller.abort(new Error('interrupted'));
|
|
685
695
|
});
|
|
686
696
|
const flush = (stream) => new Promise((resolve) => stream.write('', () => resolve()));
|
|
687
|
-
const
|
|
697
|
+
const runtime = { node: process.execPath, script: process.argv[1] ?? '' };
|
|
698
|
+
// The menu bar's update button runs this shard; it finds it here (macOS only, written when it changes).
|
|
699
|
+
if (process.platform === 'darwin' && runtime.script)
|
|
700
|
+
recordInstall(configDir(process.env), runtime, opts.distribution ?? CLI_DISTRIBUTION);
|
|
701
|
+
const code = await run(process.argv.slice(2), { stdout: process.stdout, stderr: process.stderr, stdin: process.stdin, env: process.env }, { signal: controller.signal, runtime, ...(opts.distribution ? { distribution: opts.distribution } : {}) });
|
|
688
702
|
await Promise.all([flush(process.stdout), flush(process.stderr)]);
|
|
689
703
|
process.exit(code);
|
|
690
704
|
}
|
package/dist/menubar.d.ts
CHANGED
|
@@ -21,6 +21,9 @@ export declare class MenubarManager {
|
|
|
21
21
|
packageApp?: () => string;
|
|
22
22
|
signal?: AbortSignal;
|
|
23
23
|
});
|
|
24
|
+
get platform(): NodeJS.Platform;
|
|
25
|
+
/** The version of the app this CLI's npm package carries (what `start` installs). */
|
|
26
|
+
packagedVersion(): Promise<string>;
|
|
24
27
|
status(): Promise<MenubarStatus>;
|
|
25
28
|
stop(): Promise<MenubarStatus>;
|
|
26
29
|
login(value: 'on' | 'off'): Promise<MenubarStatus>;
|
package/dist/menubar.js
CHANGED
|
@@ -49,8 +49,11 @@ export class MenubarManager {
|
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
|
+
get platform() { return this.#platform; }
|
|
52
53
|
#requireMac() { if (this.#platform !== 'darwin')
|
|
53
54
|
throw new UsageError('shard menubar requires macOS 14 or later.'); }
|
|
55
|
+
/** The version of the app this CLI's npm package carries (what `start` installs). */
|
|
56
|
+
async packagedVersion() { this.#requireMac(); return this.#version(this.#packageApp()); }
|
|
54
57
|
async #version(app) {
|
|
55
58
|
const file = join(app, 'Contents', 'Info.plist');
|
|
56
59
|
const id = (await this.#run('/usr/bin/plutil', ['-extract', 'CFBundleIdentifier', 'raw', '-o', '-', file])).trim();
|
|
@@ -158,7 +161,8 @@ export async function ensureMenubar(dir, apiUrl, manager = new MenubarManager(),
|
|
|
158
161
|
return undefined;
|
|
159
162
|
try {
|
|
160
163
|
const status = await manager.status();
|
|
161
|
-
|
|
164
|
+
// A running app stays unless this CLI carries a newer one (after `npm i -g shardflux`): then it is replaced.
|
|
165
|
+
if (status.running && !(status.version && compareBundleVersions(status.version, await manager.packagedVersion()) < 0))
|
|
162
166
|
return undefined;
|
|
163
167
|
const started = await manager.start(dir, apiUrl);
|
|
164
168
|
if (!started.running)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Workspace } from '@shardflux/sdk';
|
|
2
|
+
import type { Ctx } from './commands.js';
|
|
3
|
+
export declare const SETUP_CACHE_PATH = "/mnt/shardflux-cache";
|
|
4
|
+
/** Uses ordinary fenced shared-volume operations, after the managed PTY starts. */
|
|
5
|
+
export declare function prepareSetupCache(ctx: Ctx, workspace: Workspace): Promise<void>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const name = 'shardflux-setup-cache';
|
|
2
|
+
export const SETUP_CACHE_PATH = '/mnt/shardflux-cache';
|
|
3
|
+
/** Uses ordinary fenced shared-volume operations, after the managed PTY starts. */
|
|
4
|
+
export async function prepareSetupCache(ctx, workspace) {
|
|
5
|
+
const cloud = ctx.cloud();
|
|
6
|
+
const wait = { timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal };
|
|
7
|
+
const { volume } = await cloud.volumes.open({ name, projectId: workspace.data.project_id, wait });
|
|
8
|
+
if (volume.state !== 'available' || !volume.org_shared || volume.quota_gib !== 1)
|
|
9
|
+
throw new Error('account setup cache is not available');
|
|
10
|
+
await cloud.volumes.attach(workspace.id, { volumeId: volume.id, mountPath: SETUP_CACHE_PATH, wait });
|
|
11
|
+
}
|
package/dist/update-check.d.ts
CHANGED
|
@@ -59,6 +59,7 @@ export declare class UpdateCheck {
|
|
|
59
59
|
checkedAt: string | null;
|
|
60
60
|
} | null>;
|
|
61
61
|
}
|
|
62
|
-
/** The one-line notice for stderr, or null when there is nothing to say.
|
|
62
|
+
/** The one-line notice for stderr, or null when there is nothing to say. It names `shard update` (0.13.5+), which
|
|
63
|
+
* updates with the package manager that installed shard, rather than the catalog's npm command. */
|
|
63
64
|
export declare function noticeOf(s: ClientVersionStatus | undefined | null): string | null;
|
|
64
65
|
export {};
|
package/dist/update-check.js
CHANGED
|
@@ -120,9 +120,10 @@ export class UpdateCheck {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
-
/** The one-line notice for stderr, or null when there is nothing to say.
|
|
123
|
+
/** The one-line notice for stderr, or null when there is nothing to say. It names `shard update` (0.13.5+), which
|
|
124
|
+
* updates with the package manager that installed shard, rather than the catalog's npm command. */
|
|
124
125
|
export function noticeOf(s) {
|
|
125
126
|
if (!s || (s.status !== 'outdated' && s.status !== 'unsupported') || !s.message)
|
|
126
127
|
return null;
|
|
127
|
-
return `${s.message}\n`;
|
|
128
|
+
return `${s.message.replace(/ Update: .*$/, ' Update: shard update')}\n`;
|
|
128
129
|
}
|
package/dist/update.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ClientVersionStatusKind } from '@shardflux/sdk';
|
|
2
|
+
import type { Ctx, Handler } from './commands.js';
|
|
3
|
+
import { MenubarManager } from './menubar.js';
|
|
4
|
+
export declare const INSTALL_FILE = "install.json";
|
|
5
|
+
export declare const LAST_UPDATE_FILE = "last-update.json";
|
|
6
|
+
/** How this `shard` process runs: the Node.js binary and the script it was started with. */
|
|
7
|
+
export interface Runtime {
|
|
8
|
+
node: string;
|
|
9
|
+
script: string;
|
|
10
|
+
}
|
|
11
|
+
export type Manager = 'npm' | 'pnpm' | 'bun' | 'volta' | 'npx' | 'unmanaged';
|
|
12
|
+
export interface Install {
|
|
13
|
+
manager: Manager;
|
|
14
|
+
/** The installed package directory of the distribution, when found. */
|
|
15
|
+
packageDir: string | null;
|
|
16
|
+
/** The node_modules directory that holds it (the global root for a global install). */
|
|
17
|
+
root: string | null;
|
|
18
|
+
/** npm's global prefix (`<prefix>/lib/node_modules` is the root). */
|
|
19
|
+
prefix: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface Component {
|
|
22
|
+
package: string;
|
|
23
|
+
label: string;
|
|
24
|
+
current: string;
|
|
25
|
+
latest: string | null;
|
|
26
|
+
status: ClientVersionStatusKind;
|
|
27
|
+
}
|
|
28
|
+
/** Finds the package directory of `pkg` above `script` and how it was installed. */
|
|
29
|
+
export declare function detectInstall(script: string, pkg: string, platform?: NodeJS.Platform): Install;
|
|
30
|
+
/** The packages one install updates: the distribution, plus `@shardflux/mcp` when it is installed next to it. */
|
|
31
|
+
export declare function installSpecs(install: Install, pkg: string): string[];
|
|
32
|
+
/** The install command as argv (npm keeps the prefix it was installed in), or null when shard cannot run it. */
|
|
33
|
+
export declare function installArgv(install: Install, specs: string[], npm?: string): string[] | null;
|
|
34
|
+
/** The command a person types for the same update. */
|
|
35
|
+
export declare function displayCommand(install: Install, specs: string[]): string;
|
|
36
|
+
/** Every local piece and its status against the catalog. */
|
|
37
|
+
export declare function components(ctx: Ctx, install: Install, catalog: unknown, menubar?: MenubarManager): Promise<Component[]>;
|
|
38
|
+
/** One line per change: `shard 0.13.4 → 0.13.5 · menu bar 0.1.1 → 0.1.2`. */
|
|
39
|
+
export declare function changesText(list: {
|
|
40
|
+
label: string;
|
|
41
|
+
from: string;
|
|
42
|
+
to: string | null;
|
|
43
|
+
}[]): string;
|
|
44
|
+
export interface Exec {
|
|
45
|
+
code: number;
|
|
46
|
+
output: string;
|
|
47
|
+
}
|
|
48
|
+
export type Runner = (argv: string[], env: Record<string, string | undefined>, signal: AbortSignal) => Promise<Exec>;
|
|
49
|
+
/** Runs argv without a shell, keeping the last 64 KiB of output; never throws for a non-zero exit. */
|
|
50
|
+
export declare const runProcess: Runner;
|
|
51
|
+
export interface UpdateDeps {
|
|
52
|
+
run?: Runner;
|
|
53
|
+
menubar?: MenubarManager;
|
|
54
|
+
}
|
|
55
|
+
export declare function update(ctx: Ctx, values: Record<string, unknown>, deps?: UpdateDeps): Promise<number>;
|
|
56
|
+
/** Records where `shard` runs, for the menu bar's update button. Written on macOS, only when it changed. */
|
|
57
|
+
export declare function recordInstall(dir: string, runtime: Runtime, distribution: {
|
|
58
|
+
package: string;
|
|
59
|
+
version: string;
|
|
60
|
+
}): void;
|
|
61
|
+
export declare const UPDATE_HANDLERS: Record<string, Handler>;
|
package/dist/update.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
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 — the package that runs `shard` (the `shardflux` bundle or `@shardflux/cli`),
|
|
4
|
+
* the CLI and SDK inside it, the macOS menu bar app and a globally installed `@shardflux/mcp` — then runs one install
|
|
5
|
+
* of the latest versions with the package manager that installed `shard`, checks the result with the new `shard`,
|
|
6
|
+
* and restarts the menu bar on its new app. `--check` only reports.
|
|
7
|
+
*
|
|
8
|
+
* The menu bar app runs the same command: it finds `shard` through install.json (written by each `shard` run on
|
|
9
|
+
* macOS) and shows the outcome from last-update.json, also when the update replaced the app that started it.
|
|
10
|
+
*/
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import { closeSync, existsSync, openSync, readFileSync, realpathSync, rmSync, writeSync } from 'node:fs';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { basename, dirname, join, sep } from 'node:path';
|
|
15
|
+
import { clientVersionStatus, SDK_VERSION } from '@shardflux/sdk';
|
|
16
|
+
import { CLI_VERSION } from "./args.js";
|
|
17
|
+
import { AgentStateStore } from "./agent-state.js";
|
|
18
|
+
import { writePrivateFile } from "./credentials.js";
|
|
19
|
+
import { CodedError } from "./errors.js";
|
|
20
|
+
import { json } from "./format.js";
|
|
21
|
+
import { compareBundleVersions, MenubarManager } from "./menubar.js";
|
|
22
|
+
export const INSTALL_FILE = 'install.json';
|
|
23
|
+
export const LAST_UPDATE_FILE = 'last-update.json';
|
|
24
|
+
const LOCK_FILE = 'update.lock';
|
|
25
|
+
const MCP = '@shardflux/mcp';
|
|
26
|
+
const MENUBAR = '@shardflux/menubar-darwin';
|
|
27
|
+
/** Finds the package directory of `pkg` above `script` and how it was installed. */
|
|
28
|
+
export function detectInstall(script, pkg, platform = process.platform) {
|
|
29
|
+
let real = script;
|
|
30
|
+
try {
|
|
31
|
+
real = realpathSync(script);
|
|
32
|
+
}
|
|
33
|
+
catch { /* a missing script keeps its path */ }
|
|
34
|
+
let packageDir = null;
|
|
35
|
+
for (let dir = dirname(real); dir !== dirname(dir); dir = dirname(dir)) {
|
|
36
|
+
try {
|
|
37
|
+
if (JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).name === pkg) {
|
|
38
|
+
packageDir = dir;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch { /* no package.json here */ }
|
|
43
|
+
}
|
|
44
|
+
const none = { manager: 'unmanaged', packageDir, root: null, prefix: null };
|
|
45
|
+
if (!packageDir)
|
|
46
|
+
return none;
|
|
47
|
+
const root = pkg.startsWith('@') ? dirname(dirname(packageDir)) : dirname(packageDir);
|
|
48
|
+
if (basename(root) !== 'node_modules')
|
|
49
|
+
return none;
|
|
50
|
+
const has = (part) => root.split(sep).join('/').includes(part);
|
|
51
|
+
if (has('/_npx/'))
|
|
52
|
+
return { manager: 'npx', packageDir, root, prefix: null };
|
|
53
|
+
if (has('/.volta/'))
|
|
54
|
+
return { manager: 'volta', packageDir, root, prefix: null };
|
|
55
|
+
if (has('/.bun/install/global/'))
|
|
56
|
+
return { manager: 'bun', packageDir, root, prefix: null };
|
|
57
|
+
if (has('/pnpm/global/') || has('/.pnpm/'))
|
|
58
|
+
return { manager: 'pnpm', packageDir, root, prefix: null };
|
|
59
|
+
if (basename(dirname(root)) === 'lib')
|
|
60
|
+
return { manager: 'npm', packageDir, root, prefix: dirname(dirname(root)) };
|
|
61
|
+
if (platform === 'win32' && basename(dirname(root)) === 'npm')
|
|
62
|
+
return { manager: 'npm', packageDir, root, prefix: dirname(root) };
|
|
63
|
+
return none;
|
|
64
|
+
}
|
|
65
|
+
function versionAt(dir) {
|
|
66
|
+
try {
|
|
67
|
+
const v = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).version;
|
|
68
|
+
return typeof v === 'string' ? v : null;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** The packages one install updates: the distribution, plus `@shardflux/mcp` when it is installed next to it. */
|
|
75
|
+
export function installSpecs(install, pkg) {
|
|
76
|
+
const specs = [`${pkg}@latest`];
|
|
77
|
+
if (install.root && versionAt(join(install.root, MCP)))
|
|
78
|
+
specs.push(`${MCP}@latest`);
|
|
79
|
+
return specs;
|
|
80
|
+
}
|
|
81
|
+
/** The install command as argv (npm keeps the prefix it was installed in), or null when shard cannot run it. */
|
|
82
|
+
export function installArgv(install, specs, npm = 'npm') {
|
|
83
|
+
switch (install.manager) {
|
|
84
|
+
case 'npm': return [npm, 'install', '--global', ...(install.prefix ? ['--prefix', install.prefix] : []), ...specs, '--no-fund', '--no-audit'];
|
|
85
|
+
case 'pnpm': return ['pnpm', 'add', '--global', ...specs];
|
|
86
|
+
case 'bun': return ['bun', 'add', '--global', ...specs];
|
|
87
|
+
case 'volta': return ['volta', 'install', ...specs];
|
|
88
|
+
default: return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** The command a person types for the same update. */
|
|
92
|
+
export function displayCommand(install, specs) {
|
|
93
|
+
switch (install.manager) {
|
|
94
|
+
case 'pnpm': return `pnpm add -g ${specs.join(' ')}`;
|
|
95
|
+
case 'bun': return `bun add -g ${specs.join(' ')}`;
|
|
96
|
+
case 'volta': return `volta install ${specs.join(' ')}`;
|
|
97
|
+
case 'npx': return `npx ${specs[0]}`;
|
|
98
|
+
default: return `npm install -g ${specs.join(' ')}`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const LABELS = { shardflux: 'shardflux', '@shardflux/cli': 'shard', '@shardflux/sdk': 'SDK', [MENUBAR]: 'menu bar', [MCP]: 'MCP server' };
|
|
102
|
+
/** Every local piece and its status against the catalog. */
|
|
103
|
+
export async function components(ctx, install, catalog, menubar = menubarManager(ctx)) {
|
|
104
|
+
const local = [[ctx.distribution.package, ctx.distribution.version]];
|
|
105
|
+
if (ctx.distribution.package !== '@shardflux/cli')
|
|
106
|
+
local.push(['@shardflux/cli', CLI_VERSION]);
|
|
107
|
+
local.push(['@shardflux/sdk', SDK_VERSION]);
|
|
108
|
+
if (menubar.platform === 'darwin') {
|
|
109
|
+
const status = await menubar.status().catch(() => null);
|
|
110
|
+
if (status?.installed && status.version)
|
|
111
|
+
local.push([MENUBAR, status.version]);
|
|
112
|
+
}
|
|
113
|
+
if (install.root)
|
|
114
|
+
local.push([MCP, versionAt(join(install.root, MCP))]);
|
|
115
|
+
return local.filter((entry) => entry[1] !== null).map(([pkg, version]) => {
|
|
116
|
+
const s = clientVersionStatus(catalog, { package: pkg, version });
|
|
117
|
+
return { package: pkg, label: LABELS[pkg] ?? pkg, current: version, latest: s.latest, status: s.status };
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const outdated = (c) => c.status === 'outdated' || c.status === 'unsupported';
|
|
121
|
+
function menubarManager(ctx) {
|
|
122
|
+
return new MenubarManager({ home: ctx.io.env.HOME || homedir(), signal: ctx.signal });
|
|
123
|
+
}
|
|
124
|
+
/** One line per change: `shard 0.13.4 → 0.13.5 · menu bar 0.1.1 → 0.1.2`. */
|
|
125
|
+
export function changesText(list) {
|
|
126
|
+
return list.map(c => `${c.label} ${c.from} → ${c.to ?? '?'}`).join(' · ');
|
|
127
|
+
}
|
|
128
|
+
/** Runs argv without a shell, keeping the last 64 KiB of output; never throws for a non-zero exit. */
|
|
129
|
+
export const runProcess = (argv, env, signal) => new Promise((resolve, reject) => {
|
|
130
|
+
const child = spawn(argv[0], argv.slice(1), { env: env, stdio: ['ignore', 'pipe', 'pipe'], signal });
|
|
131
|
+
let output = '';
|
|
132
|
+
const keep = (chunk) => { output = (output + chunk.toString('utf8')).slice(-65536); };
|
|
133
|
+
child.stdout.on('data', keep);
|
|
134
|
+
child.stderr.on('data', keep);
|
|
135
|
+
child.on('error', reject);
|
|
136
|
+
child.on('close', code => resolve({ code: code ?? 1, output }));
|
|
137
|
+
});
|
|
138
|
+
function writeLastUpdate(dir, doc) {
|
|
139
|
+
try {
|
|
140
|
+
writePrivateFile(dir, join(dir, LAST_UPDATE_FILE), `${JSON.stringify({ version: 1, finished_at: new Date().toISOString(), ...doc }, null, 2)}\n`);
|
|
141
|
+
}
|
|
142
|
+
catch { /* the outcome is also printed; the menu bar then shows no result */ }
|
|
143
|
+
}
|
|
144
|
+
/** One update at a time per config directory; a lock left by a process that is gone is taken over. */
|
|
145
|
+
function lock(dir) {
|
|
146
|
+
const file = join(dir, LOCK_FILE);
|
|
147
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
148
|
+
try {
|
|
149
|
+
const fd = openSync(file, 'wx', 0o600);
|
|
150
|
+
writeSync(fd, String(process.pid));
|
|
151
|
+
closeSync(fd);
|
|
152
|
+
return () => rmSync(file, { force: true });
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (error.code !== 'EEXIST')
|
|
156
|
+
throw error;
|
|
157
|
+
const pid = Number(readFileSync(file, 'utf8'));
|
|
158
|
+
let alive = false;
|
|
159
|
+
try {
|
|
160
|
+
if (Number.isInteger(pid) && pid > 0) {
|
|
161
|
+
process.kill(pid, 0);
|
|
162
|
+
alive = true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch { /* gone */ }
|
|
166
|
+
if (alive)
|
|
167
|
+
throw new CodedError('update_in_progress', 'A Shardflux update is already running.', 'wait for it to finish, then run shard update --check');
|
|
168
|
+
rmSync(file, { force: true });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
throw new CodedError('update_in_progress', 'A Shardflux update is already running.', 'wait for it to finish, then run shard update --check');
|
|
172
|
+
}
|
|
173
|
+
/** npm next to the running Node.js (the same installation), else npm on PATH. */
|
|
174
|
+
function npmFor(node) {
|
|
175
|
+
const sibling = join(dirname(node), process.platform === 'win32' ? 'npm.cmd' : 'npm');
|
|
176
|
+
return existsSync(sibling) ? sibling : 'npm';
|
|
177
|
+
}
|
|
178
|
+
export async function update(ctx, values, deps = {}) {
|
|
179
|
+
const run = deps.run ?? runProcess;
|
|
180
|
+
const menubar = deps.menubar ?? menubarManager(ctx);
|
|
181
|
+
const runtime = ctx.runtime;
|
|
182
|
+
const install = runtime ? detectInstall(runtime.script, ctx.distribution.package) : { manager: 'unmanaged', packageDir: null, root: null, prefix: null };
|
|
183
|
+
const catalog = await ctx.clientVersions();
|
|
184
|
+
const list = await components(ctx, install, catalog, menubar);
|
|
185
|
+
const specs = installSpecs(install, ctx.distribution.package);
|
|
186
|
+
const command = displayCommand(install, specs);
|
|
187
|
+
const pending = list.filter(outdated);
|
|
188
|
+
const view = { update_available: pending.length > 0, components: list, install: { manager: install.manager, command } };
|
|
189
|
+
const versions = list.map(c => `${c.label} ${c.current}`).join(' · ');
|
|
190
|
+
if (values.check === true) {
|
|
191
|
+
ctx.io.stdout.write(ctx.json ? json(view) : pending.length
|
|
192
|
+
? `Update available: ${changesText(pending.map(c => ({ label: c.label, from: c.current, to: c.latest })))}\nRun: shard update\n`
|
|
193
|
+
: `Shardflux is up to date (${versions}).\n`);
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
if (!pending.length) {
|
|
197
|
+
// The npm package may already carry a newer menu bar app than the one running.
|
|
198
|
+
const refreshed = await refreshMenubar(ctx, menubar);
|
|
199
|
+
ctx.io.stdout.write(ctx.json ? json({ ...view, updated: false, menubar: refreshed }) : `Shardflux is up to date (${versions}).\n`);
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
const argv = installArgv(install, specs, runtime ? npmFor(runtime.node) : 'npm');
|
|
203
|
+
if (!argv || !runtime) {
|
|
204
|
+
const where = install.packageDir ?? runtime?.script ?? 'this process';
|
|
205
|
+
const message = install.manager === 'npx'
|
|
206
|
+
? 'npx runs the latest version the next time you start it.'
|
|
207
|
+
: `shard update does not manage the install at ${where}.`;
|
|
208
|
+
writeLastUpdate(ctx.store.dir, { status: 'failed', changes: [], message, command });
|
|
209
|
+
throw new CodedError('update_unmanaged', message, `run: ${command}`);
|
|
210
|
+
}
|
|
211
|
+
const unlock = lock(ctx.store.dir);
|
|
212
|
+
try {
|
|
213
|
+
if (!ctx.json)
|
|
214
|
+
ctx.io.stderr.write(`Updating ${changesText(pending.map(c => ({ label: c.label, from: c.current, to: c.latest })))} with ${install.manager}…\n`);
|
|
215
|
+
const env = { ...process.env, ...ctx.io.env, PATH: [dirname(runtime.node), ctx.io.env.PATH ?? process.env.PATH ?? ''].join(process.platform === 'win32' ? ';' : ':') };
|
|
216
|
+
const result = await run(argv, env, ctx.signal);
|
|
217
|
+
if (result.code !== 0) {
|
|
218
|
+
const denied = /\bEACCES\b|\bEPERM\b/.test(result.output);
|
|
219
|
+
const fix = denied ? `sudo ${command}` : command;
|
|
220
|
+
const message = denied
|
|
221
|
+
? `${install.manager} cannot write to ${install.prefix ?? install.root}.`
|
|
222
|
+
: `${install.manager} failed: ${result.output.trim().split('\n').slice(-3).join(' ').slice(0, 400)}`;
|
|
223
|
+
writeLastUpdate(ctx.store.dir, { status: 'failed', changes: [], message, command: fix });
|
|
224
|
+
throw new CodedError(denied ? 'update_permission_denied' : 'update_failed', message, `run: ${fix}`);
|
|
225
|
+
}
|
|
226
|
+
// The new shard reports what is installed now.
|
|
227
|
+
const after = await run([runtime.node, runtime.script, 'version', '--json'], { ...env, SHARDFLUX_NO_UPDATE_CHECK: '1' }, ctx.signal);
|
|
228
|
+
let now = {};
|
|
229
|
+
try {
|
|
230
|
+
now = JSON.parse(after.output.slice(after.output.indexOf('{')));
|
|
231
|
+
}
|
|
232
|
+
catch { /* reported as unknown below */ }
|
|
233
|
+
const installed = {
|
|
234
|
+
[ctx.distribution.package]: install.packageDir ? versionAt(install.packageDir) : null,
|
|
235
|
+
'@shardflux/cli': now.cli ?? null, '@shardflux/sdk': now.sdk ?? null,
|
|
236
|
+
[MCP]: install.root ? versionAt(join(install.root, MCP)) : null,
|
|
237
|
+
};
|
|
238
|
+
const restart = await refreshMenubar(ctx, menubar, runtime, run, env);
|
|
239
|
+
if (restart.version)
|
|
240
|
+
installed[MENUBAR] = restart.version;
|
|
241
|
+
const changes = list.filter(c => (installed[c.package] ?? c.current) !== c.current)
|
|
242
|
+
.map(c => ({ package: c.package, label: c.label, from: c.current, to: installed[c.package] ?? null }));
|
|
243
|
+
const message = changes.length ? `Updated ${changesText(changes)}` : 'Shardflux is up to date';
|
|
244
|
+
writeLastUpdate(ctx.store.dir, { status: 'ok', changes, message, command });
|
|
245
|
+
ctx.io.stdout.write(ctx.json ? json({ ...view, updated: true, changes, menubar: restart.status }) : `${message}.\n`);
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
unlock();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Restarts the menu bar on the app its npm package carries now, when that is newer than the installed one and the
|
|
254
|
+
* person has not turned the menu bar off. With `runtime`, the freshly installed shard does it (this process still
|
|
255
|
+
* runs the code from before the update).
|
|
256
|
+
*/
|
|
257
|
+
async function refreshMenubar(ctx, menubar, runtime, run, env) {
|
|
258
|
+
if (menubar.platform !== 'darwin')
|
|
259
|
+
return { status: 'off' };
|
|
260
|
+
if (new AgentStateStore(ctx.store.dir).read().menubar_stopped === true)
|
|
261
|
+
return { status: 'off' };
|
|
262
|
+
const before = await menubar.status().catch(() => null);
|
|
263
|
+
if (!before?.installed)
|
|
264
|
+
return { status: 'off' };
|
|
265
|
+
if (runtime && run && env) {
|
|
266
|
+
const r = await run([runtime.node, runtime.script, '--api-url', ctx.apiUrl, 'menubar', 'start', '--json'], { ...env, SHARDFLUX_NO_UPDATE_CHECK: '1', SHARDFLUX_CONFIG_DIR: ctx.store.dir }, ctx.signal);
|
|
267
|
+
if (r.code !== 0)
|
|
268
|
+
return { status: 'failed' };
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
const packaged = await menubar.packagedVersion().catch(() => null);
|
|
272
|
+
if (!packaged || !before.version || compareBundleVersions(before.version, packaged) >= 0)
|
|
273
|
+
return { status: 'current' };
|
|
274
|
+
await menubar.start(ctx.store.dir, ctx.apiUrl);
|
|
275
|
+
}
|
|
276
|
+
const after = await menubar.status().catch(() => null);
|
|
277
|
+
return after?.version && after.version !== before.version ? { status: 'restarted', version: after.version } : { status: 'current' };
|
|
278
|
+
}
|
|
279
|
+
/** Records where `shard` runs, for the menu bar's update button. Written on macOS, only when it changed. */
|
|
280
|
+
export function recordInstall(dir, runtime, distribution) {
|
|
281
|
+
let script = runtime.script;
|
|
282
|
+
try {
|
|
283
|
+
script = realpathSync(script);
|
|
284
|
+
}
|
|
285
|
+
catch { /* keep the path */ }
|
|
286
|
+
const doc = { version: 1, node: runtime.node, script, package: distribution.package, package_version: distribution.version, cli_version: CLI_VERSION };
|
|
287
|
+
const text = `${JSON.stringify(doc, null, 2)}\n`;
|
|
288
|
+
try {
|
|
289
|
+
if (readFileSync(join(dir, INSTALL_FILE), 'utf8') === text)
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
catch { /* first run */ }
|
|
293
|
+
try {
|
|
294
|
+
writePrivateFile(dir, join(dir, INSTALL_FILE), text);
|
|
295
|
+
}
|
|
296
|
+
catch { /* the menu bar then offers the terminal instead */ }
|
|
297
|
+
}
|
|
298
|
+
export const UPDATE_HANDLERS = { update: (ctx, values) => update(ctx, values) };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shardflux/cli",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.5",
|
|
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",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"smol-toml": "1.9.0",
|
|
48
48
|
"snappyjs": "0.7.0",
|
|
49
49
|
"yaml": "2.9.1",
|
|
50
|
-
"@shardflux/sdk": "^0.18.
|
|
50
|
+
"@shardflux/sdk": "^0.18.3"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@eslint/js": "10.0.1",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"typescript-eslint": "8.70.1"
|
|
59
59
|
},
|
|
60
60
|
"optionalDependencies": {
|
|
61
|
-
"@shardflux/menubar-darwin": "^0.1.
|
|
61
|
+
"@shardflux/menubar-darwin": "^0.1.2"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
64
|
"build": "node scripts/build.mjs",
|