@shardflux/cli 0.13.3 → 0.13.4

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.4
4
+
5
+ - Cloud Claude sign-in and reopening the machine editor preserve existing workspace labels while updating their own labels.
6
+ - `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`.
7
+ - Reuse an account-private setup cache across projects after the agent starts; report cache scope with `shard template --json`.
8
+ - Machine and project filesystem status shows the current platform base and whether a rebase is pending.
9
+
3
10
  ## 0.13.3
4
11
 
5
12
  - `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 +20,7 @@
13
20
  - 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
21
  - A `revision_mismatch` suggests `--expected-revision` only for the file commands that take it; elsewhere it says to run the same command again.
15
22
  - 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 8+).
23
+ - 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 9+).
17
24
 
18
25
  ## 0.13.2
19
26
 
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 at Developer limits, no card), `billing status` shows when it ends and
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 developer`. A subscription outranks the
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.03 per RAM GiB-hour, $0.12 per CPU-hour, charged on the next invoice (this period: shard usage).
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
 
@@ -828,12 +828,12 @@ amount:
828
828
  ```text
829
829
  overage accruing (usage past the allowances is charged on the next invoice)
830
830
  spend cap $9.00 (at most $9.00, the plan price)
831
- overage charges $2.40 of $9.00 (26.67%), $6.60 left; billed on the next invoice
831
+ overage charges $1.10 of $9.00 (12.22%), $7.90 left; billed on the next invoice
832
832
  cap reached 2026-09-29 15:02:46Z (projected at this period's average use)
833
833
 
834
- OVERAGE UNIT PAST ALLOWANCE BILLED RATE AMOUNT
835
- ram_gib_hours gib_hours 70 60 $0.03 $1.80
836
- cpu_hours hours 5 5 $0.12 $0.60
834
+ OVERAGE UNIT PAST ALLOWANCE BILLED RATE AMOUNT
835
+ ram_gib_hours gib_hours 70 60 $0.015 $0.90
836
+ cpu_hours hours 5 5 $0.04 $0.20
837
837
  ```
838
838
 
839
839
  `--json` prints the API's summary, with `spend_cap` and `exhausted_reason`. Owners and billing members turn overage on
@@ -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 Developer limits: Developer keeps them (a subscription outranks the trial and ends it).
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
- ? s.plan?.plan_key === 'trial'
1411
- ? 'keep these limits after the trial: shard billing upgrade developer'
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);
@@ -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>;
@@ -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
- return await attachTerminal({
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
- const ws = typeof values.workspace === 'string' ? await resolveWorkspace(ctx, values.workspace)
80
- : await ctx.cloud().workspaces.open({ key: `${project.slug}/main`, template: 'coding', labels: projectLabels(project, 'main', 'claude'), wait: { signal: ctx.signal, timeoutMs: ctx.wakeTimeoutMs } });
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/args.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const CLI_VERSION = "0.13.3";
1
+ export declare const CLI_VERSION = "0.13.4";
2
2
  export interface OptionSpec {
3
3
  type: 'string' | 'boolean';
4
4
  short?: string;
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.3';
15
+ export const CLI_VERSION = '0.13.4';
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).' },
@@ -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): string;
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
- return output(ctx, await ctx.cloud().templates.getProject(remote().repoRemote, { signal: ctx.signal }));
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();
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shardflux/cli",
3
- "version": "0.13.3",
3
+ "version": "0.13.4",
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.1"
50
+ "@shardflux/sdk": "^0.18.2"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@eslint/js": "10.0.1",