@shardflux/cli 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +214 -1
- package/README.md +215 -75
- package/dist/account-args.js +7 -2
- package/dist/account-commands.js +56 -106
- package/dist/account-pool.d.ts +19 -15
- package/dist/account-pool.js +47 -96
- package/dist/account-scope.d.ts +65 -0
- package/dist/account-scope.js +246 -0
- package/dist/acp-login.d.ts +4 -3
- package/dist/acp-login.js +7 -5
- package/dist/acp-models.js +20 -15
- package/dist/acp.js +66 -13
- package/dist/agent-commands.d.ts +4 -1
- package/dist/agent-commands.js +86 -32
- package/dist/agent-config-return.d.ts +46 -2
- package/dist/agent-config-return.js +110 -4
- package/dist/agent-credentials.js +7 -3
- package/dist/agent-repository.d.ts +16 -3
- package/dist/agent-repository.js +149 -74
- package/dist/agent-setup.d.ts +14 -3
- package/dist/agent-setup.js +78 -36
- package/dist/agent-target-commands.js +24 -13
- package/dist/args.d.ts +1 -1
- package/dist/args.js +94 -54
- package/dist/bin.js +5 -1
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -14
- package/dist/claude-cloud-login.d.ts +24 -2
- package/dist/claude-cloud-login.js +151 -11
- package/dist/cli-proxy-forward.d.ts +2 -0
- package/dist/cli-proxy-forward.js +9 -2
- package/dist/cli-proxy-hub.d.ts +69 -25
- package/dist/cli-proxy-hub.js +231 -54
- package/dist/cli-proxy.d.ts +70 -1
- package/dist/cli-proxy.js +298 -32
- package/dist/client-settings.d.ts +39 -0
- package/dist/client-settings.js +91 -0
- package/dist/codex-cloud.d.ts +5 -0
- package/dist/codex-cloud.js +88 -17
- package/dist/command-center-actions.js +5 -1
- package/dist/commands.js +171 -22
- package/dist/conversation-commands.d.ts +5 -0
- package/dist/conversation-commands.js +8 -5
- package/dist/conversation-terminal.d.ts +10 -2
- package/dist/conversation-terminal.js +77 -13
- package/dist/conversations.d.ts +73 -1
- package/dist/conversations.js +257 -23
- package/dist/device-bridge.d.ts +1 -1
- package/dist/device-bridge.js +6 -4
- package/dist/device-helper.js +43 -22
- package/dist/errors.d.ts +27 -1
- package/dist/errors.js +45 -5
- package/dist/filesystem-commands.d.ts +16 -9
- package/dist/filesystem-commands.js +108 -54
- package/dist/format.d.ts +2 -1
- package/dist/format.js +10 -1
- package/dist/handlers.js +3 -1
- package/dist/initial-prompt.d.ts +8 -0
- package/dist/initial-prompt.js +43 -1
- package/dist/large-media.d.ts +14 -0
- package/dist/large-media.js +100 -0
- package/dist/linux-terminal.d.ts +5 -3
- package/dist/linux-terminal.js +6 -20
- package/dist/local-sessions.d.ts +26 -2
- package/dist/local-sessions.js +113 -24
- package/dist/machine-setup.d.ts +11 -1
- package/dist/machine-setup.js +69 -22
- package/dist/machine-status.d.ts +67 -0
- package/dist/machine-status.js +140 -0
- package/dist/main.js +11 -13
- package/dist/move-worktrees-restore.py +144 -0
- package/dist/move-worktrees.d.ts +63 -0
- package/dist/move-worktrees.js +201 -0
- package/dist/move.d.ts +10 -1
- package/dist/move.js +116 -16
- package/dist/onboarding.d.ts +11 -7
- package/dist/onboarding.js +59 -27
- package/dist/prepared-repository.d.ts +202 -0
- package/dist/prepared-repository.js +860 -0
- package/dist/prepared-repository.py +1306 -0
- package/dist/project-preparation.d.ts +48 -0
- package/dist/project-preparation.js +304 -0
- package/dist/project-sync-manifest.d.ts +4 -0
- package/dist/project-sync-manifest.js +5 -1
- package/dist/project-sync-transfer.js +1 -1
- package/dist/project-sync.d.ts +19 -4
- package/dist/project-sync.js +31 -8
- package/dist/project-template-setup.d.ts +33 -1
- package/dist/project-template-setup.js +59 -2
- package/dist/secret-ask.d.ts +72 -0
- package/dist/secret-ask.js +351 -0
- package/dist/seed-phases.d.ts +8 -1
- package/dist/seed-phases.js +17 -2
- package/dist/setup-cache.d.ts +7 -0
- package/dist/setup-cache.js +32 -0
- package/dist/t3-launch.js +1 -4
- package/dist/t3-mode.d.ts +8 -2
- package/dist/t3-mode.js +19 -4
- package/dist/t3-phone.d.ts +4 -2
- package/dist/t3-phone.js +12 -4
- package/dist/terminal-apps.d.ts +157 -0
- package/dist/terminal-apps.js +561 -0
- package/dist/terminal-onboarding.js +6 -1
- package/dist/this-computer.d.ts +11 -0
- package/dist/this-computer.js +36 -0
- package/dist/thread-on-computer.js +4 -1
- package/dist/update.js +11 -3
- package/dist/worktree-twin.d.ts +10 -2
- package/dist/worktree-twin.js +27 -9
- package/package.json +5 -5
package/dist/machine-setup.js
CHANGED
|
@@ -25,6 +25,7 @@ import { syncAgentConfig } from "./agent-config-sync.js";
|
|
|
25
25
|
import { cloudClaudeLogin } from "./claude-cloud-login.js";
|
|
26
26
|
import { MachineSetupError, UsageError } from "./errors.js";
|
|
27
27
|
import { openMachine } from "./filesystem-commands.js";
|
|
28
|
+
import { machineRow, readMachineRow, rowText, statusDoc } from "./machine-status.js";
|
|
28
29
|
import { installDirs } from "./onboarding.js";
|
|
29
30
|
import { progress as stderrProgress } from "./progress.js";
|
|
30
31
|
import { writeProgress } from "./seed-phases.js";
|
|
@@ -248,6 +249,14 @@ export function setupGuard(env, command, values, positionals) {
|
|
|
248
249
|
return 'machine setup reads files only to standard output';
|
|
249
250
|
return undefined;
|
|
250
251
|
}
|
|
252
|
+
function setupCapacity(ws) {
|
|
253
|
+
const disk = ws.grants?.disk_gib;
|
|
254
|
+
if (typeof disk !== 'number' || !Number.isFinite(disk) || disk <= 0)
|
|
255
|
+
return undefined;
|
|
256
|
+
const requested = ws.caps?.disk_gib ?? null;
|
|
257
|
+
const source = ws.ceilings?.disk_gib === disk ? ws.ceilings.sources.disk_gib : requested === disk ? 'cap' : null;
|
|
258
|
+
return { disk_gib: disk, requested_disk_gib: requested, source };
|
|
259
|
+
}
|
|
251
260
|
const CLIS = ['gh', 'git-lfs', 'docker', 'kubectl', 'helm', 'k9s', 'kind', 'terraform', 'tofu', 'pulumi', 'aws', 'gcloud', 'az', 'doctl', 'flyctl', 'vercel',
|
|
252
261
|
'netlify', 'heroku', 'supabase', 'wrangler', 'stripe', 'jq', 'yq', 'rg', 'fd', 'fzf', 'bat', 'eza', 'tree', 'htop', 'btop', 'tmux', 'nvim', 'vim', 'emacs',
|
|
253
262
|
'http', 'wget', 'make', 'cmake', 'just', 'direnv', 'starship', 'zoxide', 'atuin', 'lazygit', 'delta', 'tig', 'uv', 'pipx', 'poetry', 'pnpm', 'yarn', 'bun',
|
|
@@ -309,7 +318,10 @@ export async function collectInventory(env) {
|
|
|
309
318
|
catch { /* absent */ }
|
|
310
319
|
}
|
|
311
320
|
const os = osPlatform() === 'darwin' ? `macOS ${release()} (${arch()})` : `${osPlatform()} ${release()} (${arch()})`;
|
|
321
|
+
const dotfiles = DOTFILES.filter(f => exists(join(home, f)));
|
|
312
322
|
return {
|
|
323
|
+
source_home: home,
|
|
324
|
+
config_paths: dotfiles.map(f => join(home, f)),
|
|
313
325
|
os,
|
|
314
326
|
shell: env.SHELL ? env.SHELL.split('/').pop() : null,
|
|
315
327
|
homebrew: brew ? brew.split('\n').map(s => s.trim()).filter(Boolean).slice(0, 400) : null,
|
|
@@ -319,7 +331,7 @@ export async function collectInventory(env) {
|
|
|
319
331
|
clis: CLIS.filter(c => has(c)),
|
|
320
332
|
npm_globals: npmGlobals.slice(0, 200),
|
|
321
333
|
git: { ...(name ? { name } : {}), ...(email ? { email } : {}) },
|
|
322
|
-
dotfiles
|
|
334
|
+
dotfiles,
|
|
323
335
|
};
|
|
324
336
|
}
|
|
325
337
|
/** Skills this computer's config copy brings to the machine: ~/.claude/skills and ~/.codex/skills (not Codex's bundled .system). */
|
|
@@ -340,7 +352,7 @@ const SUMMARY_SHAPE = '{"tools":["gh","ripgrep"],"runtimes":["Node 24","Python 3
|
|
|
340
352
|
* The fixed task: agent-setup.ts's "Build the machine" step, extended to recreate this computer. `where` is 'local' for
|
|
341
353
|
* an agent on this computer (it reaches the VM through shard) or 'vm' for the cloud Claude inside the machine VM.
|
|
342
354
|
*/
|
|
343
|
-
export function setupTask(where, key, inventory) {
|
|
355
|
+
export function setupTask(where, key, inventory, capacity) {
|
|
344
356
|
const reach = where === 'local'
|
|
345
357
|
? [
|
|
346
358
|
`The machine VM's workspace key is ${key}. Run commands on it with:`,
|
|
@@ -349,6 +361,8 @@ export function setupTask(where, key, inventory) {
|
|
|
349
361
|
` shard files write ${key} /home/user/<path> --from - <<'EOF'`,
|
|
350
362
|
' <contents>',
|
|
351
363
|
' EOF',
|
|
364
|
+
...(inventory.source_home ? [`Read local configuration only under ${JSON.stringify(inventory.source_home)}. The inventory and those files are the source of truth. Do not infer the source home or git identity from your login shell, inspect another home, or read dotfiles there.`] : []),
|
|
365
|
+
...(inventory.config_paths ? [`The allowed local configuration paths are ${JSON.stringify(inventory.config_paths)}. Read only these paths (or their children for a listed directory); an empty list means there are no portable dotfiles to read. Claude/Codex configuration is copied separately by shard, not by you.`] : []),
|
|
352
366
|
'On this computer you may only read: the Read and Glob tools, and read-only commands (brew list, brew leaves, ls, which, git config --global --list, <tool> --version). Nothing on this computer may change. Run every shard command on its own, not in a pipeline.',
|
|
353
367
|
]
|
|
354
368
|
: [
|
|
@@ -362,8 +376,11 @@ export function setupTask(where, key, inventory) {
|
|
|
362
376
|
'Their computer (collected by shard; names and versions only):',
|
|
363
377
|
JSON.stringify(inventory, null, 1),
|
|
364
378
|
'',
|
|
379
|
+
...(capacity ? ['Machine disk granted by Shardflux (a requested cap is not additional space):', JSON.stringify(capacity), ''] : []),
|
|
380
|
+
'Before installing, run df -h / /home/user on the VM. Check package download/installed sizes before each small batch; use --no-install-recommends with apt-get. Leave room for package configuration and the saved machine. If a tool or matching runtime will not fit, skip it with the concrete disk-space reason in your JSON, and keep the package manager healthy. Never grow the disk, change caps or upgrade the plan. Do not describe skipped tools as installed.',
|
|
381
|
+
'',
|
|
365
382
|
'Do:',
|
|
366
|
-
'1. Dev tools and CLIs it uses (Homebrew formulae, the CLIs present, global npm packages): install the Linux equivalents with sudo apt-get install -y, the tool\'s official Linux install script or binary release, npm -g, pipx or uv tool. Check with command -v first and skip what the VM already has. Skip GUI apps, macOS-only tools and libraries that are only dependencies.',
|
|
383
|
+
'1. Dev tools and CLIs it uses (Homebrew formulae, the CLIs present, global npm packages): install the Linux equivalents with sudo apt-get install -y --no-install-recommends, the tool\'s official Linux install script or binary release, npm -g, pipx or uv tool. Check with command -v first and skip what the VM already has. Skip GUI apps, macOS-only tools and libraries that are only dependencies.',
|
|
367
384
|
'2. Language runtimes at the same major versions (Node, Python, Go, Rust, Ruby, Java…), with the version manager the computer uses when there is one (nvm, mise, asdf, pyenv, uv…).',
|
|
368
385
|
'3. Shell: the same login shell if it is zsh or fish (install it and chsh -s for user), with the framework and prompt it uses (oh-my-zsh, starship…). Bring the aliases, functions, environment variables and PATH entries from its rc files that make sense on Linux into the VM\'s rc files, in a block marked "# from your computer (shard machine setup)", keeping what is there. Leave out macOS-only lines (brew shellenv, /opt/homebrew, /Applications, pbcopy, open, osascript), paths that only exist on that computer, and every line that sets a token, key, password or other secret.',
|
|
369
386
|
'4. Git: the global identity (user.name, user.email) and global settings that are not machine specific (aliases, init.defaultBranch, pull.rebase, the global gitignore). No credential helper, no signing key.',
|
|
@@ -470,6 +487,21 @@ export function claimRun(store, apiUrl, org, ifNeeded, start) {
|
|
|
470
487
|
});
|
|
471
488
|
return result;
|
|
472
489
|
}
|
|
490
|
+
/** An explicit tool permission refusal must not be retried through a different agent. */
|
|
491
|
+
class SetupPermissionRefusal extends MachineSetupError {
|
|
492
|
+
}
|
|
493
|
+
function checkClaudePermissions(result, app) {
|
|
494
|
+
if (!Array.isArray(result?.permission_denials) || !result.permission_denials.length)
|
|
495
|
+
return;
|
|
496
|
+
const tools = [...new Set(result.permission_denials.flatMap(d => typeof d?.tool_name === 'string' ? [d.tool_name.slice(0, 40)] : []))].slice(0, 5);
|
|
497
|
+
throw new SetupPermissionRefusal(`${app} stopped: permission refused${tools.length ? ` for ${tools.join(', ')}` : '; the provider omitted the tool names'}`, 'Resolve the reported permission refusal, then run shard machine setup --from-this-computer again');
|
|
498
|
+
}
|
|
499
|
+
function setupOutcome(text, app) {
|
|
500
|
+
const summary = parseSummary(text);
|
|
501
|
+
if (!summary)
|
|
502
|
+
throw new MachineSetupError(`${app} did not return a setup report`, 'shard machine setup --from-this-computer runs it again');
|
|
503
|
+
return { summary, text };
|
|
504
|
+
}
|
|
473
505
|
/**
|
|
474
506
|
* Whether the agent reached the machine VM at all: every shard command it ran failed (a sandbox without network, a
|
|
475
507
|
* permission it was never given) means nothing was set up, whatever its reply says, and the next agent should try.
|
|
@@ -486,11 +518,13 @@ export class ShardReach {
|
|
|
486
518
|
this.firstError = firstLine(output).slice(0, 160);
|
|
487
519
|
}
|
|
488
520
|
check(app) {
|
|
521
|
+
if (this.ok === 0 && this.calls === 0)
|
|
522
|
+
throw new MachineSetupError(`${app} did not confirm access to your machine with shard`, 'shard machine setup --from-this-computer runs it again');
|
|
489
523
|
if (this.calls > 0 && this.ok === 0)
|
|
490
524
|
throw new MachineSetupError(`${app} could not reach your machine with shard${this.firstError ? `: ${this.firstError}` : ''}`, 'shard machine setup --from-this-computer runs it again');
|
|
491
525
|
}
|
|
492
526
|
}
|
|
493
|
-
const isShardCommand = (command) => /(^|[\s;&|('"])shard\s+(ws|workspaces|files)\s/.test(command);
|
|
527
|
+
const isShardCommand = (command) => /(^|[\s;&|('"])shard\s+(?:(?:--json|--wake|--no-wake)\s+|(?:--api-url|--agent-label|--wake-timeout)\s+(?:"[^"]*"|'[^']*'|[^\s]+)\s+)*(ws|workspaces|files)\s/.test(command);
|
|
494
528
|
const toolText = (content) => typeof content === 'string' ? content
|
|
495
529
|
: Array.isArray(content) ? content.map(c => (c && typeof c === 'object' && typeof c.text === 'string' ? c.text : '')).join(' ') : '';
|
|
496
530
|
/** A private folder for the run: the agent's working directory, and a `shard` on PATH that is this very CLI. */
|
|
@@ -539,6 +573,7 @@ async function runClaudeLocal(ctx, agent, key, task, timeoutMs, log) {
|
|
|
539
573
|
catch { /* not an event */ }
|
|
540
574
|
},
|
|
541
575
|
});
|
|
576
|
+
checkClaudePermissions(result, 'Claude');
|
|
542
577
|
if (run.timedOut)
|
|
543
578
|
throw new MachineSetupError(`Claude ran past the ${timeoutText(timeoutMs)} time box`, 'shard machine setup --from-this-computer runs it again');
|
|
544
579
|
if (!result)
|
|
@@ -546,7 +581,7 @@ async function runClaudeLocal(ctx, agent, key, task, timeoutMs, log) {
|
|
|
546
581
|
if (result.is_error || result.subtype !== 'success')
|
|
547
582
|
throw new MachineSetupError(`Claude stopped: ${(result.result ?? result.subtype ?? 'error').slice(0, 200)}`, 'shard machine setup --from-this-computer runs it again');
|
|
548
583
|
reach.check('Claude');
|
|
549
|
-
return
|
|
584
|
+
return setupOutcome(result.result ?? '', 'Claude');
|
|
550
585
|
}
|
|
551
586
|
finally {
|
|
552
587
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -587,7 +622,7 @@ async function runCodexLocal(ctx, agent, key, task, timeoutMs, log) {
|
|
|
587
622
|
if (run.code !== 0)
|
|
588
623
|
throw new MachineSetupError(`Codex stopped: ${failure || firstLine(run.stderr) || `exit ${run.code ?? run.signal}`}`.slice(0, 300), 'shard machine setup --from-this-computer runs it again');
|
|
589
624
|
reach.check('Codex');
|
|
590
|
-
return
|
|
625
|
+
return setupOutcome(last, 'Codex');
|
|
591
626
|
}
|
|
592
627
|
finally {
|
|
593
628
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -615,9 +650,10 @@ async function runCloud(ctx, ws, task, timeoutMs) {
|
|
|
615
650
|
doc = JSON.parse(run.stdout.trim().split('\n').pop() ?? '{}');
|
|
616
651
|
}
|
|
617
652
|
catch { /* below */ }
|
|
618
|
-
|
|
653
|
+
checkClaudePermissions(doc, 'Cloud Claude');
|
|
654
|
+
if (run.exitCode !== 0 || doc.is_error || (doc.subtype !== undefined && doc.subtype !== 'success') || !doc.result)
|
|
619
655
|
throw new MachineSetupError(`Claude in the machine VM stopped: ${(doc.result ?? firstLine(run.stderr) ?? `exit ${run.exitCode}`).slice(0, 200) || `exit ${run.exitCode}`}`, 'shard machine setup --from-this-computer runs it again');
|
|
620
|
-
return
|
|
656
|
+
return setupOutcome(doc.result, 'Cloud Claude');
|
|
621
657
|
}
|
|
622
658
|
finally {
|
|
623
659
|
await cell.files.remove('/tmp/shard-machine-setup', { recursive: true }).catch(() => undefined);
|
|
@@ -665,17 +701,19 @@ function output(ctx, doc, human) {
|
|
|
665
701
|
ctx.io.stdout.write(ctx.json ? `${JSON.stringify(doc)}\n` : `${human}\n`);
|
|
666
702
|
return 0;
|
|
667
703
|
}
|
|
668
|
-
function statusDoc(rec, machine) {
|
|
669
|
-
return { state: rec?.state ?? 'none', machine_version: machine.version, ...(rec ?? {}), ...(rec ? {} : { line: null }) };
|
|
670
|
-
}
|
|
671
704
|
export async function machineSetup(ctx, values, deps = {}) {
|
|
672
705
|
const cloud = ctx.cloud();
|
|
673
706
|
const store = new AgentStateStore(ctx.store.dir);
|
|
674
707
|
const machine = await cloud.machine.get({ signal: ctx.signal });
|
|
675
708
|
const org = machine.organization_id;
|
|
709
|
+
// One source of truth (0.20.2+): the API's machine decides, this computer's record adds a run under way or stopped.
|
|
710
|
+
const status = async () => {
|
|
711
|
+
const rec = readRecord(store, ctx.apiUrl, org), row = await readMachineRow(ctx, machine);
|
|
712
|
+
return { doc: statusDoc(row, rec), text: rowText(row) };
|
|
713
|
+
};
|
|
676
714
|
if (values.status === true) {
|
|
677
|
-
const
|
|
678
|
-
return output(ctx,
|
|
715
|
+
const { doc, text } = await status();
|
|
716
|
+
return output(ctx, doc, text);
|
|
679
717
|
}
|
|
680
718
|
if (values['from-this-computer'] !== true)
|
|
681
719
|
throw new UsageError('shard machine setup needs --from-this-computer', 'shard machine setup --from-this-computer');
|
|
@@ -687,8 +725,10 @@ export async function machineSetup(ctx, values, deps = {}) {
|
|
|
687
725
|
const startRec = { state: 'running', runner: null, pid: process.pid, started_at: new Date(started).toISOString(), finished_at: null, machine_version: null,
|
|
688
726
|
line: workingLine('claude'), summary: null, error: null, duration_ms: null };
|
|
689
727
|
const claim = claimRun(store, ctx.apiUrl, org, values['if-needed'] === true, startRec);
|
|
690
|
-
if (!claim.claimed)
|
|
691
|
-
|
|
728
|
+
if (!claim.claimed) {
|
|
729
|
+
const { doc, text } = await status();
|
|
730
|
+
return output(ctx, { ...doc, skipped: true }, claim.record.state === 'running' ? `${text} (already running)` : text);
|
|
731
|
+
}
|
|
692
732
|
const phase = phaseWriter(ctx);
|
|
693
733
|
const steps = stepLog(ctx);
|
|
694
734
|
let runner = null;
|
|
@@ -705,10 +745,16 @@ export async function machineSetup(ctx, values, deps = {}) {
|
|
|
705
745
|
phase('machine', workingLine(runner));
|
|
706
746
|
const inventory = await (deps.inventory ?? collectInventory)(env);
|
|
707
747
|
const { state, ws } = await (deps.open ?? ((c, s) => openMachine(c, s)))(ctx, machine);
|
|
748
|
+
const capacity = setupCapacity(ws);
|
|
749
|
+
if (capacity) {
|
|
750
|
+
const line = `Cloud machine disk: ${capacity.disk_gib} GiB${capacity.source === 'plan' ? ' (plan limit)' : ''}; checking which tools fit…`;
|
|
751
|
+
phase('capacity', line);
|
|
752
|
+
steps.log(line);
|
|
753
|
+
}
|
|
708
754
|
// A machine saved before (from another computer, or by hand) gets this computer's Claude and Codex config too;
|
|
709
755
|
// openMachine copies it into a first machine itself.
|
|
710
756
|
if (state.version > 0)
|
|
711
|
-
await syncAgentConfig(ctx, ws)
|
|
757
|
+
await syncAgentConfig(ctx, ws);
|
|
712
758
|
let outcome;
|
|
713
759
|
let spent = 0;
|
|
714
760
|
for (const candidate of chain) {
|
|
@@ -725,16 +771,16 @@ export async function machineSetup(ctx, values, deps = {}) {
|
|
|
725
771
|
const began = Date.now();
|
|
726
772
|
try {
|
|
727
773
|
outcome = runner === 'cloud'
|
|
728
|
-
? await runCloud(ctx, ws, setupTask('vm', ws.key, inventory), box)
|
|
774
|
+
? await runCloud(ctx, ws, setupTask('vm', ws.key, inventory, capacity), box)
|
|
729
775
|
: runner === 'claude'
|
|
730
|
-
? await runClaudeLocal(ctx, candidate.agent, ws.key, setupTask('local', ws.key, inventory), box, steps.log)
|
|
731
|
-
: await runCodexLocal(ctx, candidate.agent, ws.key, setupTask('local', ws.key, inventory), box, steps.log);
|
|
776
|
+
? await runClaudeLocal(ctx, candidate.agent, ws.key, setupTask('local', ws.key, inventory, capacity), box, steps.log)
|
|
777
|
+
: await runCodexLocal(ctx, candidate.agent, ws.key, setupTask('local', ws.key, inventory, capacity), box, steps.log);
|
|
732
778
|
attempts.push({ runner, ok: true, error: null, duration_ms: Date.now() - began });
|
|
733
779
|
break;
|
|
734
780
|
}
|
|
735
781
|
catch (error) {
|
|
736
782
|
// Only the agent's own failures move on to the next agent; the API, the network or an interrupt stop here.
|
|
737
|
-
if (!(error instanceof MachineSetupError) || ctx.signal.aborted)
|
|
783
|
+
if (error instanceof SetupPermissionRefusal || !(error instanceof MachineSetupError) || ctx.signal.aborted)
|
|
738
784
|
throw error;
|
|
739
785
|
attempts.push({ runner, ok: false, error: error.message, duration_ms: Date.now() - began });
|
|
740
786
|
steps.log(`agent ${runner} stopped: ${error.message}`);
|
|
@@ -751,10 +797,11 @@ export async function machineSetup(ctx, values, deps = {}) {
|
|
|
751
797
|
const skills = countSkills(env.HOME || homedir());
|
|
752
798
|
const line = readyLine(outcome.summary, skills);
|
|
753
799
|
const rec = { state: 'ready', runner, pid: null, started_at: startRec.started_at, finished_at: new Date().toISOString(), machine_version: saved.version,
|
|
754
|
-
line, summary: { ...
|
|
800
|
+
line, summary: { ...outcome.summary, skills, runner }, attempts, error: null, duration_ms: Date.now() - started };
|
|
755
801
|
writeRecord(store, ctx.apiUrl, org, rec);
|
|
756
802
|
phase('ready', line);
|
|
757
|
-
|
|
803
|
+
const row = machineRow(saved, rec);
|
|
804
|
+
return output(ctx, { ...statusDoc(row, rec), workspace_key: ws.key }, rowText(row));
|
|
758
805
|
}
|
|
759
806
|
catch (error) {
|
|
760
807
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { AgentFilesystem } from '@shardflux/sdk';
|
|
2
|
+
import type { Ctx } from './commands.js';
|
|
3
|
+
import { type BuildQueue } from './filesystem-commands.js';
|
|
4
|
+
import { type SaveFailure } from './errors.js';
|
|
5
|
+
import { type SetupRecord } from './machine-setup.js';
|
|
6
|
+
export type MachineRowState = 'ready' | 'saving' | 'failed' | 'none';
|
|
7
|
+
/** too_large and timeout: the save's build; save_failed: any other build failure; setup_stopped: this computer's agent. */
|
|
8
|
+
export type MachineFailure = 'too_large' | 'timeout' | 'save_failed' | 'setup_stopped';
|
|
9
|
+
export interface MachineAction {
|
|
10
|
+
id: 'save' | 'retry' | 'setup';
|
|
11
|
+
title: 'Save again' | 'Retry' | 'Set up';
|
|
12
|
+
command: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface MachineRow {
|
|
15
|
+
/** unknown: the check or response could not establish readiness; unavailable: the API has no machine route. */
|
|
16
|
+
state: MachineRowState | 'unknown' | 'unavailable';
|
|
17
|
+
/** The saved version in use; 0 when none is, null when the version could not be read. */
|
|
18
|
+
version: number | null;
|
|
19
|
+
/** One plain line of at most 80 characters. */
|
|
20
|
+
line: string;
|
|
21
|
+
reason?: MachineFailure;
|
|
22
|
+
/** The failure's full message. */
|
|
23
|
+
error?: string;
|
|
24
|
+
/** saving: the setup's agent (setup) or the save's build (save), with its version and place in the queue. */
|
|
25
|
+
progress?: {
|
|
26
|
+
phase: 'setup' | 'save';
|
|
27
|
+
version?: number;
|
|
28
|
+
queue?: BuildQueue;
|
|
29
|
+
};
|
|
30
|
+
action?: MachineAction;
|
|
31
|
+
}
|
|
32
|
+
/** `text` cut so that it and `suffix` fit one 80-character line. */
|
|
33
|
+
export declare function fitLine(text: string, suffix?: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* A build failure as people say it: too large, out of time, or the build's own first sentence. The failure's code
|
|
36
|
+
* (0.21.1+, from an API that reports it) decides first: resource_limit is too large, named with the plan's disk when it
|
|
37
|
+
* is that limit.
|
|
38
|
+
*/
|
|
39
|
+
export declare function saveFailure(message: string, failure?: SaveFailure): {
|
|
40
|
+
reason: MachineFailure;
|
|
41
|
+
why: string;
|
|
42
|
+
};
|
|
43
|
+
/** What `machineRow` reads besides the machine: whether the failed save's VM is still there, and a build's queue. */
|
|
44
|
+
export interface MachineRowFacts {
|
|
45
|
+
saveSourceExists?: boolean;
|
|
46
|
+
queue?: BuildQueue;
|
|
47
|
+
}
|
|
48
|
+
/** The row, from the API's machine and this computer's setup record (pure: the facts are read by `readMachineRow`). */
|
|
49
|
+
export declare function machineRow(machine: AgentFilesystem, rec: SetupRecord | undefined, facts?: MachineRowFacts): MachineRow;
|
|
50
|
+
/**
|
|
51
|
+
* The row for this account and computer: the machine (one API call), the setup record, and, only when they matter,
|
|
52
|
+
* whether a failed save's VM is still there and a saving build's queue. `machine` skips the first call when the caller
|
|
53
|
+
* has it.
|
|
54
|
+
*/
|
|
55
|
+
export declare function readMachineRow(ctx: Ctx, machine?: AgentFilesystem): Promise<MachineRow>;
|
|
56
|
+
/**
|
|
57
|
+
* `setup --inspect`'s row: an API without machines reports unavailable; a check that fails says so on the row
|
|
58
|
+
* (state unknown) and never fails the report.
|
|
59
|
+
*/
|
|
60
|
+
export declare function inspectMachineRow(ctx: Ctx): Promise<MachineRow | null>;
|
|
61
|
+
/**
|
|
62
|
+
* `machine setup --status` and a setup's result: this computer's record (runner, summary, attempts…) with the row's
|
|
63
|
+
* state and line. Saving reads as running for older apps; unknown and unavailable remain explicit.
|
|
64
|
+
*/
|
|
65
|
+
export declare function statusDoc(row: MachineRow, rec: SetupRecord | undefined): Record<string, unknown>;
|
|
66
|
+
/** The row as the terminal shows it: its line, and for a failure or none the command that recovers it. */
|
|
67
|
+
export declare function rowText(row: MachineRow): string;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The account's cloud machine as one row (0.20.2+), the one source of truth that `shard machine setup --status`, `shard
|
|
3
|
+
* setup --inspect` (onboarding.machine) and a setup run's result report, and that the menu bar, the Linux app and the
|
|
4
|
+
* terminal onboarding show. The API's machine (GET /v1/machine: what is saved, saving or failed) decides; this
|
|
5
|
+
* computer's setup record (agents.json) adds only what the API cannot know: its agent is setting the machine up right
|
|
6
|
+
* now, or its last setup stopped before anything was saved.
|
|
7
|
+
*
|
|
8
|
+
* - ready: a version is saved and in use. saving: the setup's agent works, or a save builds (with its place in the
|
|
9
|
+
* queue). failed: the newest save failed (its reason: too large, out of time, or the build's own message), or this
|
|
10
|
+
* computer's setup stopped and no version was saved since it started. none: nothing saved and nothing under way.
|
|
11
|
+
* - A failed row has one plain line and one recovery: Save again (`shard machine save --wait`) when the save failed and
|
|
12
|
+
* its machine VM is still there, else Retry (`shard machine setup --from-this-computer`).
|
|
13
|
+
*/
|
|
14
|
+
import { ShardfluxApiError, saveProgressText } from '@shardflux/sdk';
|
|
15
|
+
import { AgentStateStore } from "./agent-state.js";
|
|
16
|
+
import { machineSaveSource, pendingQueue } from "./filesystem-commands.js";
|
|
17
|
+
import { gib, planDiskLimit } from "./errors.js";
|
|
18
|
+
import { readRecord, readyLine, workingLine } from "./machine-setup.js";
|
|
19
|
+
const LIMIT = 80;
|
|
20
|
+
/** `text` cut so that it and `suffix` fit one 80-character line. */
|
|
21
|
+
export function fitLine(text, suffix = '') {
|
|
22
|
+
const flat = text.replace(/\s+/g, ' ').trim(), room = LIMIT - suffix.length;
|
|
23
|
+
return (flat.length <= room ? flat : `${flat.slice(0, room - 1).trimEnd()}…`) + suffix;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A build failure as people say it: too large, out of time, or the build's own first sentence. The failure's code
|
|
27
|
+
* (0.21.1+, from an API that reports it) decides first: resource_limit is too large, named with the plan's disk when it
|
|
28
|
+
* is that limit.
|
|
29
|
+
*/
|
|
30
|
+
export function saveFailure(message, failure) {
|
|
31
|
+
const disk = planDiskLimit(failure);
|
|
32
|
+
if (disk)
|
|
33
|
+
return { reason: 'too_large', why: `over the plan’s ${gib(disk.limit)} disk` };
|
|
34
|
+
if (failure?.code === 'resource_limit')
|
|
35
|
+
return { reason: 'too_large', why: 'it’s too large' };
|
|
36
|
+
if (failure?.code === 'timeout')
|
|
37
|
+
return { reason: 'timeout', why: 'it ran out of time' };
|
|
38
|
+
if (/too[ _]large|exceed\w* (the |its )?(\w+ )?(max|size|limit|quota)|max_\w*bytes|disk (is )?full|no space|insufficient_disk|quota_exceeded|above the .*(GiB|MiB)/i.test(message))
|
|
39
|
+
return { reason: 'too_large', why: 'it’s too large' };
|
|
40
|
+
if (/exceeded its \d+ ?(second|s)\b|time ?out|timed out|ran past|took too long|deadline/i.test(message))
|
|
41
|
+
return { reason: 'timeout', why: 'it ran out of time' };
|
|
42
|
+
const first = message.replace(/\s+/g, ' ').trim().split(/(?<=[.!?])\s/)[0]?.replace(/[.!?]$/, '') ?? '';
|
|
43
|
+
// The API's own fallback ("Filesystem save failed. Open its build status…") adds nothing to "not saved".
|
|
44
|
+
return { reason: 'save_failed', why: /^(filesystem )?save failed$/i.test(first) ? '' : first };
|
|
45
|
+
}
|
|
46
|
+
const SAVE_AGAIN = { id: 'save', title: 'Save again', command: ['shard', 'machine', 'save', '--wait'] };
|
|
47
|
+
const RETRY = { id: 'retry', title: 'Retry', command: ['shard', 'machine', 'setup', '--from-this-computer'] };
|
|
48
|
+
const SET_UP = { id: 'setup', title: 'Set up', command: ['shard', 'machine', 'setup', '--from-this-computer'] };
|
|
49
|
+
/** When the newest saved version was made (0 when none is). */
|
|
50
|
+
const savedAt = (m) => Math.max(0, ...m.history.map(h => Date.parse(h.created_at)).filter(Number.isFinite));
|
|
51
|
+
const inUse = (version) => (version > 0 ? ` · version ${version} stays in use` : ' · no saved version');
|
|
52
|
+
/** The row, from the API's machine and this computer's setup record (pure: the facts are read by `readMachineRow`). */
|
|
53
|
+
export function machineRow(machine, rec, facts = {}) {
|
|
54
|
+
const version = machine.version;
|
|
55
|
+
if (!Number.isSafeInteger(version) || version < 0)
|
|
56
|
+
return { state: 'unknown', version: null, line: 'Cloud machine readiness could not be confirmed: saved version unavailable' };
|
|
57
|
+
if (machine.state === 'building' && machine.pending) {
|
|
58
|
+
const q = facts.queue;
|
|
59
|
+
return { state: 'saving', version, line: fitLine(q ? `Saving machine v${machine.pending.version} · ${saveProgressText({ version: machine.pending.version, build_id: machine.pending.build_id, state: 'queued', queue: q })}` : `Saving machine v${machine.pending.version}…`, inUse(version)),
|
|
60
|
+
progress: { phase: 'save', version: machine.pending.version, ...(q ? { queue: q } : {}) } };
|
|
61
|
+
}
|
|
62
|
+
if (machine.state === 'building')
|
|
63
|
+
return { state: 'unknown', version, line: 'Cloud machine is saving; build progress is unavailable' };
|
|
64
|
+
if (rec?.state === 'running') {
|
|
65
|
+
return { state: 'saving', version, line: fitLine(rec.line || workingLine(rec.runner ?? 'claude')), progress: { phase: 'setup' } };
|
|
66
|
+
}
|
|
67
|
+
if (machine.state === 'failed') {
|
|
68
|
+
const message = machine.failure?.message || 'The API reported a failed save without build details.';
|
|
69
|
+
const { reason, why } = saveFailure(message, machine.failure);
|
|
70
|
+
return { state: 'failed', version, line: fitLine(`Cloud machine not saved${why ? `: ${why}` : ''}`, inUse(version)), reason, error: message,
|
|
71
|
+
action: facts.saveSourceExists === false ? RETRY : SAVE_AGAIN };
|
|
72
|
+
}
|
|
73
|
+
// A setup that stopped counts until a version is saved after it started (by its own save that outlived its wait, by
|
|
74
|
+
// Save again, or by hand).
|
|
75
|
+
const started = Date.parse(rec?.started_at ?? '');
|
|
76
|
+
if (rec?.state === 'failed' && (version === 0 || (Number.isFinite(started) && started > savedAt(machine)))) {
|
|
77
|
+
const error = rec.error ?? 'it didn’t finish';
|
|
78
|
+
return { state: 'failed', version, line: fitLine(`Cloud machine setup stopped: ${error}`, inUse(version)), reason: 'setup_stopped', error, action: RETRY };
|
|
79
|
+
}
|
|
80
|
+
if (machine.state === 'ready' && version > 0) {
|
|
81
|
+
const mine = rec?.state === 'ready' && rec.machine_version === version;
|
|
82
|
+
const line = mine ? (rec.line || (rec.summary ? readyLine(rec.summary, rec.summary.skills) : '')) : '';
|
|
83
|
+
return { state: 'ready', version, line: line ? fitLine(line, ` · version ${version}`) : `Cloud machine ready (version ${version})` };
|
|
84
|
+
}
|
|
85
|
+
if (machine.state !== 'empty' || version !== 0)
|
|
86
|
+
return { state: 'unknown', version, line: 'Cloud machine readiness could not be confirmed from this API response' };
|
|
87
|
+
return { state: 'none', version: 0, line: 'Cloud machine not set up yet', action: SET_UP };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The row for this account and computer: the machine (one API call), the setup record, and, only when they matter,
|
|
91
|
+
* whether a failed save's VM is still there and a saving build's queue. `machine` skips the first call when the caller
|
|
92
|
+
* has it.
|
|
93
|
+
*/
|
|
94
|
+
export async function readMachineRow(ctx, machine) {
|
|
95
|
+
const cloud = ctx.cloud();
|
|
96
|
+
machine ??= await cloud.machine.get({ signal: ctx.signal });
|
|
97
|
+
const rec = readRecord(new AgentStateStore(ctx.store.dir), ctx.apiUrl, machine.organization_id);
|
|
98
|
+
const facts = {};
|
|
99
|
+
if (machine.state === 'building' && machine.pending)
|
|
100
|
+
facts.queue = await pendingQueue(ctx, machine);
|
|
101
|
+
if (machine.state === 'failed' && machine.failure)
|
|
102
|
+
facts.saveSourceExists = await saveSourceExists(ctx, machine);
|
|
103
|
+
return machineRow(machine, rec, facts);
|
|
104
|
+
}
|
|
105
|
+
/** The VM `shard machine save` saves (machineSaveSource): Save again needs it; without it the setup runs again. */
|
|
106
|
+
async function saveSourceExists(ctx, machine) {
|
|
107
|
+
try {
|
|
108
|
+
return !!(await machineSaveSource(ctx, machine));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* `setup --inspect`'s row: an API without machines reports unavailable; a check that fails says so on the row
|
|
116
|
+
* (state unknown) and never fails the report.
|
|
117
|
+
*/
|
|
118
|
+
export async function inspectMachineRow(ctx) {
|
|
119
|
+
try {
|
|
120
|
+
return await readMachineRow(ctx);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (error instanceof ShardfluxApiError && (error.status === 404 || error.details?.reason === 'filesystem_unavailable'))
|
|
124
|
+
return { state: 'unavailable', version: null, line: 'This API does not report cloud machine readiness; update the server' };
|
|
125
|
+
return { state: 'unknown', version: null, line: fitLine(`Couldn’t check your cloud machine: ${error instanceof Error ? error.message : String(error)}`) };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* `machine setup --status` and a setup's result: this computer's record (runner, summary, attempts…) with the row's
|
|
130
|
+
* state and line. Saving reads as running for older apps; unknown and unavailable remain explicit.
|
|
131
|
+
*/
|
|
132
|
+
export function statusDoc(row, rec) {
|
|
133
|
+
const state = row.state === 'saving' ? 'running' : row.state;
|
|
134
|
+
return { ...(rec ?? {}), state, line: state === 'none' ? null : row.line, error: row.state === 'failed' ? row.error ?? null : null,
|
|
135
|
+
machine_version: row.version, machine: row };
|
|
136
|
+
}
|
|
137
|
+
/** The row as the terminal shows it: its line, and for a failure or none the command that recovers it. */
|
|
138
|
+
export function rowText(row) {
|
|
139
|
+
return row.action ? `${row.line}. ${row.action.title}: ${row.action.command.join(' ')}` : row.line;
|
|
140
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -10,10 +10,9 @@
|
|
|
10
10
|
* (step-up, password change, TOTP change) is saved at once; a saved session the API rejects (401) is removed.
|
|
11
11
|
* Every credential the command loaded is redacted from what it prints on failure.
|
|
12
12
|
*/
|
|
13
|
-
import { aboutWait } from "./filesystem-commands.js";
|
|
14
13
|
import { homedir, hostname } from 'node:os';
|
|
15
14
|
import { statSync } from 'node:fs';
|
|
16
|
-
import { SDK_VERSION, Shardflux, ShardfluxAccount, isSessionToken } from '@shardflux/sdk';
|
|
15
|
+
import { SDK_VERSION, Shardflux, ShardfluxAccount, aboutSeconds, isSessionToken } from '@shardflux/sdk';
|
|
17
16
|
import { CLI_VERSION, CENTER_COMMAND, GUIDE_COMMAND, ONBOARDING_COMMAND, PROJECT_COMMAND, parseCommandLine, resolveApiUrl, resolveWake, resolveWakeTimeout } from "./args.js";
|
|
18
17
|
import { commandCenter } from "./command-center.js";
|
|
19
18
|
import { launchProjectCommand } from "./conversation-terminal.js";
|
|
@@ -73,10 +72,8 @@ function hintFor(info, cred, file, command = '') {
|
|
|
73
72
|
return `keep waiting with: shard operations wait ${info.operation_id}`;
|
|
74
73
|
if (info.code === 'usage')
|
|
75
74
|
return 'run "shard --help" or "shard <command> --help"';
|
|
76
|
-
if (info.code === 'filesystem_save_failed')
|
|
77
|
-
|
|
78
|
-
return `the version in use is unchanged; fix what the message names in ${level === 'machine' ? 'shard machine' : 'the project agent'}, then run shard ${level} save --wait again`;
|
|
79
|
-
}
|
|
75
|
+
if (info.code === 'filesystem_save_failed')
|
|
76
|
+
return info.fix;
|
|
80
77
|
if (info.source === 'cell' && info.code === 'workspace_not_running')
|
|
81
78
|
return 'open or resume the workspace first: shard workspaces open <key> --template <slug>';
|
|
82
79
|
if (info.status === 401 && info.code === 'unauthenticated') {
|
|
@@ -118,7 +115,7 @@ function hintFor(info, cred, file, command = '') {
|
|
|
118
115
|
if (info.code === 'rate_limited' && reasonOf(info) === 'template_build_queue_full') {
|
|
119
116
|
const queued = typeof info.details?.queued === 'number' ? info.details.queued : undefined;
|
|
120
117
|
const retry = info.retry_after_seconds ?? (typeof info.details?.retry_after_seconds === 'number' ? info.details.retry_after_seconds : undefined);
|
|
121
|
-
return `${queued ?? 'Several'} build${queued === 1 ? '' : 's'} already wait for your plan's build slots; nothing was started. Run the same command again${retry ? ` in ${
|
|
118
|
+
return `${queued ?? 'Several'} build${queued === 1 ? '' : 's'} already wait for your plan's build slots; nothing was started. Run the same command again${retry ? ` in ${aboutSeconds(retry)}` : ' when one finishes'}`;
|
|
122
119
|
}
|
|
123
120
|
const policyHint = spendPolicyHint(info);
|
|
124
121
|
if (policyHint)
|
|
@@ -351,6 +348,8 @@ const FEEDBACK_EMAIL = 'shardflux@heliosone.fi';
|
|
|
351
348
|
// names the next step (upgrade, overage, the cap, the payment method), not a problem to report.
|
|
352
349
|
const NO_NUDGE_CODES = new Set(['usage', 'terms_not_accepted', 'interrupted', 'unauthenticated', 'validation_failed', 'mfa_required', 'step_up_required', 'email_unverified', 'timeout', 'checkout_ended', 'allowance_exhausted']);
|
|
353
350
|
const NO_NUDGE_REASONS = new Set([
|
|
351
|
+
// shard template delete with nothing saved (0.20.1+): the hint names the next step.
|
|
352
|
+
'filesystem_not_found',
|
|
354
353
|
'revision_mismatch',
|
|
355
354
|
'tree_revision_mismatch',
|
|
356
355
|
'edit_not_found',
|
|
@@ -383,6 +382,9 @@ function nudges(info, command) {
|
|
|
383
382
|
// A plan limit whose hint names the next step (0.9.0): busy working slots, or Retained state used up.
|
|
384
383
|
if (info.code === 'quota_exceeded' && quotaHint(info) !== undefined)
|
|
385
384
|
return false;
|
|
385
|
+
// A save the same VM cannot pass (0.21.1+), such as one larger than the plan's workspace disk: the hint names the fix.
|
|
386
|
+
if (info.code === 'filesystem_save_failed' && info.retryable === false)
|
|
387
|
+
return false;
|
|
386
388
|
const reason = reasonOf(info);
|
|
387
389
|
return !NO_NUDGE_CODES.has(info.code) && !(reason !== undefined && NO_NUDGE_REASONS.has(reason));
|
|
388
390
|
}
|
|
@@ -474,12 +476,8 @@ export async function run(argv, io, opts = {}) {
|
|
|
474
476
|
io.stdout.write(wantsJson ? json({ cli: CLI_VERSION, sdk: SDK_VERSION }) : `shard ${CLI_VERSION} (@shardflux/sdk ${SDK_VERSION})\n`);
|
|
475
477
|
return EXIT.ok;
|
|
476
478
|
}
|
|
477
|
-
// shard ./ attaches Claude Code or Codex to this terminal
|
|
478
|
-
//
|
|
479
|
-
if (parsed.command === PROJECT_COMMAND && !tty && !parsed.global.json) {
|
|
480
|
-
await startupHousekeeping({ io, store, signal });
|
|
481
|
-
return report(new UsageError('shard ./ opens Claude Code or Codex in a terminal, and this shell has none.', 'ask your user to run shard ./ in their own terminal, or start the conversation detached: shard ./ --json --prompt "<task>" (shard ./ codex --json ... for Codex)'));
|
|
482
|
-
}
|
|
479
|
+
// shard ./ attaches Claude Code or Codex to this terminal. Without one (or in a coding agent's shell) it opens in the
|
|
480
|
+
// person's chosen app (0.20.1+: a new tab of their terminal app, see launchProjectCommand), never one nobody can see.
|
|
483
481
|
command = parsed.command.path.join(' ');
|
|
484
482
|
let ctx;
|
|
485
483
|
let updates = null;
|