@shardflux/cli 0.11.1 → 0.13.1
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 +64 -1
- package/README.md +83 -15
- package/dist/account-args.js +2 -0
- package/dist/account-commands.js +39 -0
- package/dist/agent-commands.d.ts +8 -0
- package/dist/agent-commands.js +223 -0
- package/dist/agent-config-files.d.ts +23 -0
- package/dist/agent-config-files.js +203 -0
- package/dist/agent-config-mcp.d.ts +36 -0
- package/dist/agent-config-mcp.js +225 -0
- package/dist/agent-config-sync.d.ts +58 -0
- package/dist/agent-config-sync.js +366 -0
- package/dist/agent-credentials.d.ts +14 -0
- package/dist/agent-credentials.js +93 -0
- package/dist/agent-git-commands.d.ts +10 -0
- package/dist/agent-git-commands.js +294 -0
- package/dist/agent-git.d.ts +47 -0
- package/dist/agent-git.js +285 -0
- package/dist/agent-reachability.d.ts +4 -0
- package/dist/agent-reachability.js +13 -0
- package/dist/agent-repository.d.ts +13 -0
- package/dist/agent-repository.js +182 -0
- package/dist/agent-state.d.ts +16 -0
- package/dist/agent-state.js +63 -0
- package/dist/agent-target-commands.d.ts +10 -0
- package/dist/agent-target-commands.js +171 -0
- package/dist/agent-targets.d.ts +33 -0
- package/dist/agent-targets.js +267 -0
- package/dist/agent-terminal.d.ts +35 -0
- package/dist/agent-terminal.js +209 -0
- package/dist/args.d.ts +8 -4
- package/dist/args.js +120 -10
- package/dist/assisted-setup.d.ts +24 -0
- package/dist/assisted-setup.js +77 -0
- package/dist/browser-commands.d.ts +3 -0
- package/dist/browser-commands.js +83 -0
- package/dist/browser-profile-copy.d.ts +2 -0
- package/dist/browser-profile-copy.js +2 -0
- package/dist/browser-readers.d.ts +13 -0
- package/dist/browser-readers.js +249 -0
- package/dist/browser-sites.d.ts +1 -0
- package/dist/browser-sites.js +13 -0
- package/dist/claude-cloud-login.d.ts +9 -0
- package/dist/claude-cloud-login.js +143 -0
- package/dist/command-center-actions.d.ts +6 -0
- package/dist/command-center-actions.js +132 -0
- package/dist/command-center-keys.d.ts +6 -0
- package/dist/command-center-keys.js +38 -0
- package/dist/command-center-model.d.ts +29 -0
- package/dist/command-center-model.js +90 -0
- package/dist/command-center-renderer.d.ts +25 -0
- package/dist/command-center-renderer.js +100 -0
- package/dist/command-center.d.ts +18 -0
- package/dist/command-center.js +320 -0
- package/dist/commands.d.ts +8 -2
- package/dist/commands.js +123 -8
- package/dist/credential-prompts.d.ts +22 -0
- package/dist/credential-prompts.js +193 -0
- package/dist/filesystem-commands.d.ts +2 -0
- package/dist/filesystem-commands.js +63 -0
- package/dist/filesystem-state.d.ts +3 -0
- package/dist/filesystem-state.js +12 -0
- package/dist/git-remote-bin.d.ts +2 -0
- package/dist/git-remote-bin.js +37 -0
- package/dist/git-remote-helper.d.ts +8 -0
- package/dist/git-remote-helper.js +31 -0
- package/dist/git-remote.d.ts +14 -0
- package/dist/git-remote.js +136 -0
- package/dist/handlers.js +9 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/input.d.ts +5 -0
- package/dist/main.js +21 -5
- package/dist/menubar.d.ts +32 -0
- package/dist/menubar.js +180 -0
- package/dist/project-identity.d.ts +14 -0
- package/dist/project-identity.js +61 -0
- package/dist/project-sync-manifest.d.ts +27 -0
- package/dist/project-sync-manifest.js +225 -0
- package/dist/project-sync-transfer.d.ts +14 -0
- package/dist/project-sync-transfer.js +240 -0
- package/dist/project-sync.d.ts +38 -0
- package/dist/project-sync.js +243 -0
- package/dist/project-template-setup.d.ts +17 -0
- package/dist/project-template-setup.js +89 -0
- package/dist/vault.d.ts +16 -0
- package/dist/vault.js +153 -0
- package/package.json +9 -2
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.
|
|
15
|
+
export const CLI_VERSION = '0.13.1';
|
|
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).' },
|
|
@@ -67,6 +67,7 @@ const SLUG = { name: 'slug', required: true };
|
|
|
67
67
|
/** The screenshot after a computer action or batch (0.10.0+). */
|
|
68
68
|
const COMPUTER_LOOK = {
|
|
69
69
|
screenshot: { type: 'string', value: '<file>', description: 'Take a screenshot after it and write it here.' },
|
|
70
|
+
'pixel-predicate': { type: 'string', value: '<json>', description: 'Exact caller-supplied RGBA region hashes on the full PNG; requires --screenshot and no --settle.' },
|
|
70
71
|
settle: { type: 'string', value: '<ms>', description: 'Wait this long before that screenshot when the screen may change (0-10000, default 250).' },
|
|
71
72
|
format: { type: 'string', value: '<png|jpeg>', description: 'Screenshot format (default png).' },
|
|
72
73
|
quality: { type: 'string', value: '<1-100>', description: 'JPEG quality (default 80).' },
|
|
@@ -139,6 +140,16 @@ const SECRET_PERMISSIONS = {
|
|
|
139
140
|
'all-projects': { type: 'boolean', description: 'Organization secrets: every project of the organization may use it.' },
|
|
140
141
|
};
|
|
141
142
|
const KEY_COMMANDS = [
|
|
143
|
+
...['machine', 'template'].flatMap((level) => [
|
|
144
|
+
{ path: [level], summary: level === 'machine' ? 'Open your machine agent, or show its saved filesystem with --json' : 'Show this project’s template filesystem', positionals: [], options: {} },
|
|
145
|
+
{ path: [level, 'save'], summary: `Save the ${level} filesystem as a new private version`, positionals: [], options: {} },
|
|
146
|
+
{ path: [level, 'rollback'], summary: `Restore a saved ${level} version`, positionals: [], options: { to: { type: 'string', value: '<version>', description: 'Version to restore (default previous saved version).' } } },
|
|
147
|
+
]),
|
|
148
|
+
{ path: ['agents', 'list'], summary: 'List coding agent sessions.', positionals: [], options: {
|
|
149
|
+
...PAGE,
|
|
150
|
+
state: { type: 'string', value: '<working|waiting|exited>', description: 'Agent state filter.' },
|
|
151
|
+
label: { type: 'string', multiple: true, value: '<key=value>', description: 'Exact workspace label match.' },
|
|
152
|
+
} },
|
|
142
153
|
{
|
|
143
154
|
path: ['login'],
|
|
144
155
|
summary: 'Verify the project API key (SHARDFLUX_API_KEY, else the saved one) and show its organization, project and tools',
|
|
@@ -220,6 +231,10 @@ const KEY_COMMANDS = [
|
|
|
220
231
|
template: { type: 'string', value: '<slug>', description: 'Template slug (required), e.g. python-node-browser.' },
|
|
221
232
|
labels: { type: 'string', value: '<json>', description: 'Replace workspace labels with a JSON object of strings.' },
|
|
222
233
|
'delete-after-idle': { type: 'string', value: '<days>d', description: 'Delete this persistent workspace after 1..3650 idle days (e.g. 14d).' },
|
|
234
|
+
like: { type: 'string', value: '<id|key>', description: 'Open a fresh workspace with the source’s template and configuration.' },
|
|
235
|
+
volume: { type: 'string', multiple: true, value: '<name:/path[:ro]>', description: 'Replace the named volume mounts; repeat for each mount.' },
|
|
236
|
+
reachable: { type: 'string', value: '<duration>', description: 'Keep waiting agents reachable (default 30m; 0 off; maximum 24h).' },
|
|
237
|
+
'agent-idle-grace-seconds': { type: 'string', value: '<0-3600>', description: 'Grace after a coding agent waits or exits (default 300 seconds).' },
|
|
223
238
|
'idle-policy': { type: 'string', value: '<policy>', description: 'adaptive, never or fixed:<seconds>.' },
|
|
224
239
|
secret: {
|
|
225
240
|
type: 'string',
|
|
@@ -589,6 +604,14 @@ const KEY_COMMANDS = [
|
|
|
589
604
|
options: { ...COMPUTER_LOOK },
|
|
590
605
|
examples: ['shard ws computer type acme/demo "hello world"', 'cat notes.txt | shard ws computer type acme/demo -'],
|
|
591
606
|
},
|
|
607
|
+
{
|
|
608
|
+
path: ['workspaces', 'computer', 'paste-text'],
|
|
609
|
+
summary: 'Intentionally replace the clipboard and paste text (0.12.0+)',
|
|
610
|
+
description: 'Replaces CLIPBOARD with <text> and presses the paste chord (default ctrl+v). The former clipboard is never restored, including cancellation after acquisition. Requires released keys. Up to 20,000 characters; - reads standard input.',
|
|
611
|
+
positionals: [REF, { name: 'text|-', required: true }],
|
|
612
|
+
options: { 'paste-chord': { type: 'string', value: '<keys>', description: 'One paste chord, e.g. shift+Insert or ctrl+shift+v.' }, ...COMPUTER_LOOK },
|
|
613
|
+
examples: ['cat notes.txt | shard ws computer paste-text acme/demo - --paste-chord shift+Insert --screenshot after.png'],
|
|
614
|
+
},
|
|
592
615
|
{
|
|
593
616
|
path: ['workspaces', 'computer', 'key'],
|
|
594
617
|
summary: 'Press keys on the workspace desktop (0.10.0+)',
|
|
@@ -721,6 +744,12 @@ const KEY_COMMANDS = [
|
|
|
721
744
|
options: {},
|
|
722
745
|
examples: ['shard templates computer-use my-desktop-agent on'],
|
|
723
746
|
},
|
|
747
|
+
{
|
|
748
|
+
path: ['files', 'read-many'],
|
|
749
|
+
summary: 'Read complete files from one sealed suspended generation without waking',
|
|
750
|
+
positionals: [REF],
|
|
751
|
+
options: { path: { type: 'string', multiple: true, value: '<path>', description: 'Absolute file path (repeat for 1–8 ordered files).' }, generation: { type: 'string', value: '<id>', description: 'Require this exact sealed generation.' } },
|
|
752
|
+
},
|
|
724
753
|
{
|
|
725
754
|
path: ['files', 'read'],
|
|
726
755
|
summary: 'Print (or save) a file from a workspace',
|
|
@@ -808,6 +837,25 @@ const KEY_COMMANDS = [
|
|
|
808
837
|
},
|
|
809
838
|
examples: ['shard executions get acme/agent-7 build-42', 'shard ws executions get acme/agent-7 ex-6f1c2a9e-0d7b-4c1e-9a53-2b8e1f0c4d7a --wait 10m --json'],
|
|
810
839
|
},
|
|
840
|
+
{ path: ['secrets', 'log'], summary: 'Show credential request and fill audit events', positionals: [], options: {} },
|
|
841
|
+
{ path: ['credentials', 'list'], summary: 'Show pending login requests', positionals: [], options: {} },
|
|
842
|
+
{ path: ['credentials', 'enable'], summary: 'Enable credential fill in a workspace', positionals: [REF], options: {} },
|
|
843
|
+
{ path: ['credentials', 'deny'], summary: 'Deny a login request', positionals: [{ name: 'request-id', required: true }], options: {} },
|
|
844
|
+
{ path: ['credentials', 'forget'], summary: 'Remove a remembered site credential', positionals: [REF, { name: 'site', required: true }], options: {} },
|
|
845
|
+
{ path: ['credentials', 'approve'], summary: 'Approve a login directly from your vault', positionals: [{ name: 'request-id', required: true }], options: {
|
|
846
|
+
from: { type: 'string', description: 'hsec, 1password, bitwarden or keychain.' }, item: { type: 'string', description: 'Vault login item name.' },
|
|
847
|
+
always: { type: 'boolean', description: 'Always allow these fields for this project on this laptop.' }, remember: { type: 'boolean', description: 'Remember for unattended use, encrypted with AWS KMS.' },
|
|
848
|
+
} },
|
|
849
|
+
{
|
|
850
|
+
path: ['secrets', 'import'],
|
|
851
|
+
summary: 'Import named items from your vault as encrypted project secrets',
|
|
852
|
+
positionals: [{ name: 'name', required: true, variadic: true }],
|
|
853
|
+
options: {
|
|
854
|
+
from: { type: 'string', value: '<hsec|1password|bitwarden|keychain>', description: 'Vault to read; values go straight into the encrypted secrets request.' },
|
|
855
|
+
as: { type: 'string', value: '<secret-name>', description: 'Secret name for a single imported item.' },
|
|
856
|
+
keychain: { type: 'string', value: '<path>', description: 'Read from this Keychain (keychain source only).' },
|
|
857
|
+
},
|
|
858
|
+
},
|
|
811
859
|
{
|
|
812
860
|
path: ['secrets', 'create'],
|
|
813
861
|
summary: 'Create a secret; the value is read from standard input or --from-file, never from arguments',
|
|
@@ -1078,9 +1126,58 @@ const KEY_COMMANDS = [
|
|
|
1078
1126
|
},
|
|
1079
1127
|
},
|
|
1080
1128
|
];
|
|
1129
|
+
/** Copied sessions are project-key commands; --json is the normal global output option. */
|
|
1130
|
+
const AGENT_CREDENTIAL_COMMANDS = [
|
|
1131
|
+
...['sync', 'status', 'forget'].map((action) => ({ path: ['agents', 'credentials', action], summary: `${action} Codex copies and the account's cloud Claude login (contents never printed)`, positionals: [], options: {} })),
|
|
1132
|
+
{ path: ['agents', 'credentials', 'login'], summary: 'Sign in once to cloud Claude for this account', positionals: [{ name: 'app', required: true }], options: { workspace: { type: 'string', value: '<id|key>', description: 'Use this workspace for the cloud sign-in.' } } },
|
|
1133
|
+
];
|
|
1134
|
+
const AGENT_OPTIONS = {
|
|
1135
|
+
reachable: { type: 'string', value: '<duration>', description: 'Keep waiting agents reachable (default 30m; 0 off; maximum 24h).' },
|
|
1136
|
+
approvals: { type: 'boolean', description: 'Keep the agent’s permission prompts.' },
|
|
1137
|
+
};
|
|
1138
|
+
export const PROJECT_COMMAND = {
|
|
1139
|
+
path: ['<path>'], summary: 'Open the project’s cloud agent command center',
|
|
1140
|
+
description: 'Claude is the default; add codex to use Codex. Detach with Ctrl-] then d; /exit ends the agent session.',
|
|
1141
|
+
positionals: [{ name: 'path', required: true }, { name: 'claude|codex' }], options: AGENT_OPTIONS,
|
|
1142
|
+
};
|
|
1143
|
+
export const CENTER_COMMAND = { path: ['<center>'], summary: 'Open the cloud agent command center', positionals: [], options: {} };
|
|
1144
|
+
const AGENT_COMMANDS = [
|
|
1145
|
+
{ path: ['agents', 'targets'], summary: 'Detect apps or configure managed desktop connections', positionals: [{ name: 'agent' }], options: { remove: { type: 'boolean', description: 'Remove this agent’s managed entries, or all entries for this API URL.' } } },
|
|
1146
|
+
...['claude-desktop', 'codex-app', 'web'].map(target => ({ path: ['agents', 'open', target], summary: `Open the agent in ${target}`, positionals: [{ name: 'agent' }], options: { reachable: AGENT_OPTIONS.reachable } })),
|
|
1147
|
+
{ path: ['ssh-proxy'], summary: 'Forward SSH standard IO to a managed workspace', positionals: [{ name: 'host', required: true }], options: {} },
|
|
1148
|
+
...['worktree', 'merge', 'keep', 'discard', 'restore'].map((action) => ({
|
|
1149
|
+
path: ['agents', action], summary: { worktree: 'Open agent work in a local worktree', merge: 'Merge agent work into the current branch and clean up', keep: 'Keep agent work as a branch and clean up', discard: 'Delete an agent and its local work', restore: 'Restore archived work as a local branch within 30 days' }[action],
|
|
1150
|
+
positionals: [{ name: 'agent', required: true }], options: {
|
|
1151
|
+
path: { type: 'string', value: '<path>', description: 'Local repository path (default current repository).' },
|
|
1152
|
+
...(['worktree', 'merge', 'keep'].includes(action) ? { 'remote-path': { type: 'string', value: '<path>', description: 'Absolute repository path in the workspace (default /home/user/<project-slug>).' } } : {}),
|
|
1153
|
+
...(action === 'keep' ? { push: { type: 'boolean', description: 'Push the kept branch to origin and open a PR when gh is installed.' } } : {}),
|
|
1154
|
+
},
|
|
1155
|
+
})),
|
|
1156
|
+
{ path: ['agents', 'open'], summary: 'Attach an agent in the terminal', description: 'Detach with Ctrl-] then d; /exit ends the agent session.', positionals: [{ name: 'agent', required: true }], options: { ...AGENT_OPTIONS, app: { type: 'string', value: '<claude|codex>', description: 'Agent app (default remembered choice).' }, in: { type: 'string', value: '<terminal>', description: 'Where to open the session (default terminal).' } } },
|
|
1157
|
+
{ path: ['agents', 'new'], summary: 'Start a session or a named agent', positionals: [{ name: 'agent' }], options: {
|
|
1158
|
+
...AGENT_OPTIONS,
|
|
1159
|
+
app: { type: 'string', value: '<claude|codex>', description: 'Agent app (default remembered choice).' },
|
|
1160
|
+
path: { type: 'string', value: '<path>', description: 'Local project path for first launch.' },
|
|
1161
|
+
name: { type: 'string', value: '<name>', description: 'New agent name; copies the project main workspace with --like.' },
|
|
1162
|
+
like: { type: 'string', value: '<id|key>', description: 'Copy the workspace configuration onto a fresh disk.' },
|
|
1163
|
+
} },
|
|
1164
|
+
{ path: ['agents', 'fork'], summary: 'Fork an agent’s memory and disk', positionals: [{ name: 'agent', required: true }, { name: 'name', required: true }], options: WAIT },
|
|
1165
|
+
{ path: ['agents', 'park'], summary: 'Suspend an agent when idle', positionals: [{ name: 'agent', required: true }], options: { 'when-idle': { type: 'string', value: '<duration>', description: 'Idle grace before suspend (default 1s).' } } },
|
|
1166
|
+
{ path: ['agents', 'logs'], summary: 'Tail an agent PTY from its output offset', positionals: [{ name: 'agent', required: true }], options: {
|
|
1167
|
+
app: { type: 'string', value: '<claude|codex>', description: 'Agent app (default remembered choice).' },
|
|
1168
|
+
offset: { type: 'string', value: '<bytes>', description: 'Read from this output byte offset (default last 64 KiB).' },
|
|
1169
|
+
follow: { type: 'boolean', description: 'Follow output until interrupted.' },
|
|
1170
|
+
} },
|
|
1171
|
+
];
|
|
1081
1172
|
/** Every command: the account plane first (sign in, set up), then the project-key commands. */
|
|
1082
|
-
|
|
1173
|
+
const MENUBAR_COMMANDS = ['start', 'stop', 'status'].map(action => ({
|
|
1174
|
+
path: ['menubar', action], summary: `${action === 'start' ? 'Install and start' : action === 'stop' ? 'Stop' : 'Show'} the macOS menu bar app`,
|
|
1175
|
+
positionals: [], options: { login: { type: 'string', value: '<on|off>', description: 'Enable or disable launch at login.' } },
|
|
1176
|
+
}));
|
|
1177
|
+
export const COMMANDS = [...ACCOUNT_COMMANDS.filter((c) => c.path[0] !== 'templates'), ...AGENT_COMMANDS, ...AGENT_CREDENTIAL_COMMANDS, ...MENUBAR_COMMANDS, ...KEY_COMMANDS, ...ACCOUNT_COMMANDS.filter((c) => c.path[0] === 'templates')];
|
|
1083
1178
|
const GROUPS = {
|
|
1179
|
+
menubar: 'macOS menu bar app',
|
|
1180
|
+
agents: 'Cloud agent sessions and copied credentials',
|
|
1084
1181
|
auth: 'Create an account and sign in (the session is saved in the credentials file); passwords, email, two-factor authentication and sessions',
|
|
1085
1182
|
'auth password': 'Change or reset your password',
|
|
1086
1183
|
'auth email': 'Change your sign-in email',
|
|
@@ -1100,6 +1197,7 @@ const GROUPS = {
|
|
|
1100
1197
|
files: 'Read, write, list, search and patch files in a workspace',
|
|
1101
1198
|
operations: 'Inspect and wait for lifecycle operations',
|
|
1102
1199
|
executions: 'Fetch the results of file-first executions (the commands "shard ws exec" runs in file-first workspaces)',
|
|
1200
|
+
credentials: 'Approve browser login requests from your vault',
|
|
1103
1201
|
secrets: 'Manage secrets (values are write-only) and bind them to workspaces',
|
|
1104
1202
|
templates: 'Build templates from template.yaml, test and export versions, browse templates (file tree, diff), develop them in a draft, and publish or archive versions',
|
|
1105
1203
|
};
|
|
@@ -1111,7 +1209,6 @@ const ALIASES = {
|
|
|
1111
1209
|
execution: 'executions',
|
|
1112
1210
|
file: 'files',
|
|
1113
1211
|
secret: 'secrets',
|
|
1114
|
-
template: 'templates',
|
|
1115
1212
|
org: 'orgs',
|
|
1116
1213
|
organization: 'orgs',
|
|
1117
1214
|
organizations: 'orgs',
|
|
@@ -1131,7 +1228,7 @@ const FOLDED = new Set(['files', 'operations', 'executions']);
|
|
|
1131
1228
|
/** Top-level shortcuts for a whole command path (0.10.0+): `shard exec` is `shard workspaces exec`. */
|
|
1132
1229
|
const SHORTCUTS = { exec: ['workspaces', 'exec'] };
|
|
1133
1230
|
export function normalizeWords(words) {
|
|
1134
|
-
const w = words.flatMap((x, i) => (i === 0 ? SHORTCUTS[x] : undefined) ?? [ALIASES[x] ?? x]);
|
|
1231
|
+
const w = words.flatMap((x, i) => (i === 0 ? SHORTCUTS[x] : undefined) ?? [i === 1 && ['projects', 'project'].includes(words[0]) && x === 'template' ? x : ALIASES[x] ?? x]);
|
|
1135
1232
|
if (w[0] === 'workspaces' && w[1] !== undefined && FOLDED.has(w[1]))
|
|
1136
1233
|
return w.slice(1);
|
|
1137
1234
|
return w;
|
|
@@ -1245,9 +1342,9 @@ function toParseArgsOptions(opts) {
|
|
|
1245
1342
|
}
|
|
1246
1343
|
/**
|
|
1247
1344
|
* Parses argv (without node/script). Throws UsageError for anything invalid;
|
|
1248
|
-
* returns help text for --help, bare groups and
|
|
1345
|
+
* returns help text for --help, bare groups and non-TTY bare `shard`.
|
|
1249
1346
|
*/
|
|
1250
|
-
export function parseCommandLine(argv) {
|
|
1347
|
+
export function parseCommandLine(argv, options = {}) {
|
|
1251
1348
|
const terminator = argv.indexOf('--');
|
|
1252
1349
|
const head = terminator < 0 ? argv : argv.slice(0, terminator);
|
|
1253
1350
|
const leading = [];
|
|
@@ -1294,7 +1391,7 @@ export function parseCommandLine(argv) {
|
|
|
1294
1391
|
rest.push(argv[(i += 1)]);
|
|
1295
1392
|
continue;
|
|
1296
1393
|
}
|
|
1297
|
-
if (!sawPositional &&
|
|
1394
|
+
if (!sawPositional && isPrefix([...words, a])) {
|
|
1298
1395
|
words.push(a);
|
|
1299
1396
|
continue;
|
|
1300
1397
|
}
|
|
@@ -1302,7 +1399,14 @@ export function parseCommandLine(argv) {
|
|
|
1302
1399
|
rest.push(a);
|
|
1303
1400
|
}
|
|
1304
1401
|
const wantsHelp = head.includes('--help') || head.includes('-h');
|
|
1305
|
-
|
|
1402
|
+
let command = findCommand(words);
|
|
1403
|
+
if (!command && words.length === 0) {
|
|
1404
|
+
const path = rest.find((r, i) => !r.startsWith('-') && !GLOBAL_VALUE_FLAGS.has(rest[i - 1] ?? ''));
|
|
1405
|
+
if (path !== undefined && (/^(?:\.|\/|~\/)/.test(path) || options.isProjectPath?.(path)))
|
|
1406
|
+
command = PROJECT_COMMAND;
|
|
1407
|
+
else if (path === undefined && !wantsHelp && !head.includes('--version') && !head.includes('-V') && (options.tty || head.includes('--json')))
|
|
1408
|
+
command = CENTER_COMMAND;
|
|
1409
|
+
}
|
|
1306
1410
|
if (!command) {
|
|
1307
1411
|
if (words.length === 0) {
|
|
1308
1412
|
if (head.includes('--version') || head.includes('-V'))
|
|
@@ -1341,6 +1445,8 @@ export function parseCommandLine(argv) {
|
|
|
1341
1445
|
if (values.version === true)
|
|
1342
1446
|
return { kind: 'version' };
|
|
1343
1447
|
const positionals = parsed.positionals;
|
|
1448
|
+
if (command === PROJECT_COMMAND && positionals[1] !== undefined && !['claude', 'codex'].includes(positionals[1]))
|
|
1449
|
+
throw new UsageError('agent app must be claude or codex');
|
|
1344
1450
|
const specs = command.positionals;
|
|
1345
1451
|
const variadic = specs.at(-1)?.variadic === true;
|
|
1346
1452
|
const required = specs.filter((s) => s.required && !s.variadic).length;
|
|
@@ -1390,6 +1496,8 @@ function optionLines(opts) {
|
|
|
1390
1496
|
return rows.map(([l, d]) => ` ${l.padEnd(w)} ${d}`);
|
|
1391
1497
|
}
|
|
1392
1498
|
export function usageLine(c) {
|
|
1499
|
+
if (c === PROJECT_COMMAND)
|
|
1500
|
+
return 'shard [options] <path> [claude|codex]';
|
|
1393
1501
|
const pos = c.positionals.map((p) => (p.variadic ? (p.words ? `<${p.name}...>` : `-- <${p.name}> [args...]`) : p.required ? `<${p.name}>` : `[${p.name}]`));
|
|
1394
1502
|
const opts = Object.keys(c.options).length > 0 ? ' [options]' : '';
|
|
1395
1503
|
return `shard ${c.path.join(' ')}${opts}${pos.length ? ` ${pos.join(' ')}` : ''}`;
|
|
@@ -1427,9 +1535,9 @@ export const EXIT_CODE_HELP = [
|
|
|
1427
1535
|
/** The top help's sections: every command's group (first word) is in exactly one (args.test.ts checks). */
|
|
1428
1536
|
export const HELP_SECTIONS = [
|
|
1429
1537
|
{ 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'] },
|
|
1430
|
-
{ title: 'Workspaces (a project API key: SHARDFLUX_API_KEY, else the one shard setup saved)', groups: ['login', 'whoami', 'usage', 'workspaces', 'files', 'operations', 'executions', 'secrets', 'volumes', 'egress'] },
|
|
1538
|
+
{ 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'] },
|
|
1431
1539
|
{ title: 'Templates (publish and archive use the session; the rest the API key)', groups: ['templates'] },
|
|
1432
|
-
{ title: 'Other', groups: ['feedback', 'version'] },
|
|
1540
|
+
{ title: 'Other', groups: ['menubar', 'feedback', 'version'] },
|
|
1433
1541
|
];
|
|
1434
1542
|
export function topHelp() {
|
|
1435
1543
|
const w = Math.max(...COMMANDS.map((c) => usageLine(c).length));
|
|
@@ -1438,6 +1546,8 @@ export function topHelp() {
|
|
|
1438
1546
|
`shard ${CLI_VERSION} — the Shardflux command line (built on @shardflux/sdk)`,
|
|
1439
1547
|
'',
|
|
1440
1548
|
'Usage: shard [global options] <command> [options] [arguments]',
|
|
1549
|
+
' shard <path> [claude|codex] [--approvals]',
|
|
1550
|
+
'Terminal: detach with Ctrl-] then d; /exit ends the agent session.',
|
|
1441
1551
|
'',
|
|
1442
1552
|
'Feedback: shard feedback [options] <message...>',
|
|
1443
1553
|
...FEEDBACK_WHY.map((l) => ` ${l}`),
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { WorkspaceView } from '@shardflux/sdk';
|
|
2
|
+
import type { Ctx } from './commands.js';
|
|
3
|
+
export interface SetupProgress {
|
|
4
|
+
id: string;
|
|
5
|
+
deps_hash: string;
|
|
6
|
+
state: 'queued' | 'running' | 'checking' | 'saving' | 'ready' | 'failed';
|
|
7
|
+
tasks: {
|
|
8
|
+
name: string;
|
|
9
|
+
state: 'queued' | 'running' | 'ready' | 'failed';
|
|
10
|
+
attempts: number;
|
|
11
|
+
error?: {
|
|
12
|
+
code: string;
|
|
13
|
+
message: string;
|
|
14
|
+
retryable: boolean;
|
|
15
|
+
};
|
|
16
|
+
}[];
|
|
17
|
+
cache_scope: 'workspace' | 'account';
|
|
18
|
+
}
|
|
19
|
+
/** Read only while the command center is visible, from opted-in running VMs.
|
|
20
|
+
* A parked workspace is never woken just to display setup progress.
|
|
21
|
+
*/
|
|
22
|
+
export declare function readSetupProgress(ctx: Ctx, workspace: WorkspaceView, agentWorking?: boolean): Promise<SetupProgress | undefined>;
|
|
23
|
+
export declare function retrySetupStep(ctx: Ctx, workspace: WorkspaceView, step: string): Promise<void>;
|
|
24
|
+
export declare function setupLine(project: string, setup: SetupProgress): string | undefined;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { workspaceRepositoryPath } from "./agent-repository.js";
|
|
2
|
+
const settled = new WeakMap();
|
|
3
|
+
async function registered(ctx, workspace, progress) {
|
|
4
|
+
if (progress.state !== 'saving')
|
|
5
|
+
return progress;
|
|
6
|
+
try {
|
|
7
|
+
const binding = await ctx.cloud().projectTemplates.get(workspace.labels.shard_repo_remote, { signal: ctx.signal });
|
|
8
|
+
if (binding.deps_hash === progress.deps_hash && binding.version > 0)
|
|
9
|
+
progress.state = 'ready';
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (!(error instanceof Error && 'status' in error && error.status === 404))
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
return progress;
|
|
16
|
+
}
|
|
17
|
+
/** Read only while the command center is visible, from opted-in running VMs.
|
|
18
|
+
* A parked workspace is never woken just to display setup progress.
|
|
19
|
+
*/
|
|
20
|
+
export async function readSetupProgress(ctx, workspace, agentWorking = false) {
|
|
21
|
+
if (workspace.labels.shard_setup !== 'true')
|
|
22
|
+
return undefined;
|
|
23
|
+
const cloud = ctx.cloud();
|
|
24
|
+
let cache = settled.get(cloud);
|
|
25
|
+
if (!cache) {
|
|
26
|
+
cache = new Map();
|
|
27
|
+
settled.set(cloud, cache);
|
|
28
|
+
}
|
|
29
|
+
const previous = cache.get(workspace.id);
|
|
30
|
+
// A status exec counts as activity. Stop spawning them after setup settles
|
|
31
|
+
// so the command center cannot keep a waiting agent awake indefinitely.
|
|
32
|
+
if (previous && previous.deps_hash === workspace.labels.shard_deps_hash && (previous.state !== 'failed' || !agentWorking))
|
|
33
|
+
return registered(ctx, workspace, previous);
|
|
34
|
+
if (workspace.observed_state !== 'running')
|
|
35
|
+
return previous?.deps_hash === workspace.labels.shard_deps_hash ? previous : undefined;
|
|
36
|
+
const ws = await ctx.cloud().workspaces.get(workspace.id);
|
|
37
|
+
const cell = ws.cell({ wake: null, agentLabel: ctx.agentLabel, transitionTimeoutMs: 0 });
|
|
38
|
+
try {
|
|
39
|
+
const response = await cell.exec.run(['sf-agent', 'template', '--json'], { cwd: workspaceRepositoryPath(ws), timeoutMs: 3000, maxOutputBytes: 1024 * 1024, signal: ctx.signal });
|
|
40
|
+
if (response.exitCode !== 0)
|
|
41
|
+
return undefined; // Older guests have no setup status.
|
|
42
|
+
const progress = JSON.parse(response.stdout);
|
|
43
|
+
if (!progress || !/^[a-f0-9]{64}$/.test(progress.id) || !Array.isArray(progress.tasks))
|
|
44
|
+
throw new Error('guest omitted structured setup progress');
|
|
45
|
+
if (['saving', 'ready', 'failed'].includes(progress.state))
|
|
46
|
+
cache.set(workspace.id, progress);
|
|
47
|
+
await registered(ctx, workspace, progress);
|
|
48
|
+
return progress;
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
cell.close();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export async function retrySetupStep(ctx, workspace, step) {
|
|
55
|
+
settled.get(ctx.cloud())?.delete(workspace.id);
|
|
56
|
+
const ws = await ctx.cloud().workspaces.get(workspace.id);
|
|
57
|
+
const cell = ws.cell({ agentLabel: ctx.agentLabel, transitionTimeoutMs: ctx.wakeTimeoutMs });
|
|
58
|
+
try {
|
|
59
|
+
const result = await cell.exec.run(['sf-agent', 'template', 'retry', '--step', step, '--json'], { cwd: workspaceRepositoryPath(ws), timeoutMs: 3000, maxOutputBytes: 64 * 1024, signal: ctx.signal });
|
|
60
|
+
if (result.exitCode !== 0)
|
|
61
|
+
throw new Error(result.stderr.trim() || `setup retry exited ${result.exitCode}`);
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
cell.close();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function setupLine(project, setup) {
|
|
68
|
+
if (setup.state === 'ready')
|
|
69
|
+
return undefined;
|
|
70
|
+
const failed = setup.tasks.find(task => task.state === 'failed' && task.error?.code !== 'setup_dependency_failed');
|
|
71
|
+
if (failed)
|
|
72
|
+
return `Setting up ${project}: ${failed.name} failed: ${failed.error?.message ?? 'No error receipt'}${failed.error?.retryable ? ' · r retries once' : ''}`;
|
|
73
|
+
if (setup.state === 'saving')
|
|
74
|
+
return `Setting up ${project}: saving template`;
|
|
75
|
+
const running = setup.tasks.filter(task => task.state === 'running').map(task => task.name);
|
|
76
|
+
return `Setting up ${project}: ${running.join(', ') || 'detecting pinned tools and dependencies'}`;
|
|
77
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { resolveWorkspace } from "./commands.js";
|
|
3
|
+
import { UsageError } from "./errors.js";
|
|
4
|
+
import { browserSessionNote, workspaceBrowserSessionNote } from "./browser-profile-copy.js";
|
|
5
|
+
import { normalizeBrowserSite } from "./browser-sites.js";
|
|
6
|
+
const browsers = ['chrome', 'arc', 'brave', 'edge', 'safari', 'firefox'];
|
|
7
|
+
const text = (v) => typeof v === 'string' ? v : undefined;
|
|
8
|
+
export const browserProfileName = (site, scope) => `site-${createHash('sha256').update(`${site}\0${scope}`).digest('hex').slice(0, 24)}`;
|
|
9
|
+
const note = (workspace) => workspace ? workspaceBrowserSessionNote : browserSessionNote;
|
|
10
|
+
function summary(rows) {
|
|
11
|
+
return rows.length ? rows.flatMap(p => p.sites.map(s => `${s.site} · ${s.cookie_count} cookies · ${s.origins.length} storage origins · ${typeof p.scope === 'string' ? 'project' : p.scope.workspace_id}`)).join('\n') + '\n' : 'No browser sessions.\n';
|
|
12
|
+
}
|
|
13
|
+
export const BROWSER_HANDLERS = {
|
|
14
|
+
async 'browser import'(ctx, values, sites) {
|
|
15
|
+
if (values.project && values.workspace)
|
|
16
|
+
throw new UsageError('Choose --project or --workspace.');
|
|
17
|
+
const from = text(values.from) ?? 'chrome';
|
|
18
|
+
if (!browsers.includes(from))
|
|
19
|
+
throw new UsageError('--from must be chrome, arc, brave, edge, safari or firefox');
|
|
20
|
+
const browser = from;
|
|
21
|
+
const ws = text(values.workspace) ? await resolveWorkspace(ctx, text(values.workspace)) : undefined;
|
|
22
|
+
const { browserDirectory, readBrowserState } = await import("./browser-readers.js");
|
|
23
|
+
const directory = text(values['profile-dir']) ?? await browserDirectory(browser);
|
|
24
|
+
const states = await readBrowserState(browser, directory, sites);
|
|
25
|
+
const profiles = [];
|
|
26
|
+
for (const s of states) {
|
|
27
|
+
if (!s.storage_state.cookies.length && !s.storage_state.origins.some(o => o.localStorage.length))
|
|
28
|
+
throw new UsageError(`No browser session was found for ${s.site}. Sign in to that site in the selected browser, then retry.`);
|
|
29
|
+
profiles.push(await ctx.cloud().browserProfiles.put(browserProfileName(s.site, ws?.id ?? 'project'), { scope: ws ? { workspace_id: ws.id } : 'project', sites: [s] }, { signal: ctx.signal }));
|
|
30
|
+
}
|
|
31
|
+
if (ctx.json)
|
|
32
|
+
ctx.io.stdout.write(`${JSON.stringify({ profiles, security_note: note(!!ws) })}\n`);
|
|
33
|
+
else
|
|
34
|
+
ctx.io.stdout.write(`${summary(profiles)}${note(!!ws)}\n`);
|
|
35
|
+
return 0;
|
|
36
|
+
},
|
|
37
|
+
async 'browser list'(ctx) {
|
|
38
|
+
const profiles = await ctx.cloud().browserProfiles.list({ signal: ctx.signal });
|
|
39
|
+
ctx.io.stdout.write(ctx.json ? `${JSON.stringify(profiles)}\n` : summary(profiles));
|
|
40
|
+
return 0;
|
|
41
|
+
},
|
|
42
|
+
async 'browser remove'(ctx, values, [raw]) {
|
|
43
|
+
if (values.project && values.workspace)
|
|
44
|
+
throw new UsageError('Choose --project or --workspace.');
|
|
45
|
+
const site = normalizeBrowserSite(raw);
|
|
46
|
+
const ws = text(values.workspace) ? await resolveWorkspace(ctx, text(values.workspace)) : undefined;
|
|
47
|
+
const profiles = (await ctx.cloud().browserProfiles.list({ signal: ctx.signal })).filter(p => p.sites.some(s => s.site === site) && (ws ? typeof p.scope !== 'string' && p.scope.workspace_id === ws.id : p.scope === 'project'));
|
|
48
|
+
if (!profiles.length)
|
|
49
|
+
throw new UsageError(`No stored browser session was found for ${site} in this scope.`);
|
|
50
|
+
for (const p of profiles)
|
|
51
|
+
await ctx.cloud().browserProfiles.remove(p.name, { site, signal: ctx.signal });
|
|
52
|
+
ctx.io.stdout.write(ctx.json ? `${JSON.stringify({ removed: site })}\n` : `Removed ${site}.\n`);
|
|
53
|
+
return 0;
|
|
54
|
+
},
|
|
55
|
+
async 'browser save'(ctx, values, [raw]) {
|
|
56
|
+
const key = text(values.workspace);
|
|
57
|
+
if (!key)
|
|
58
|
+
throw new UsageError('browser save requires --workspace <key>');
|
|
59
|
+
const site = normalizeBrowserSite(raw), ws = await resolveWorkspace(ctx, key);
|
|
60
|
+
const cell = ws.cell({ agentLabel: ctx.agentLabel, transitionTimeoutMs: ctx.wakeTimeoutMs });
|
|
61
|
+
const filename = `/home/user/.local/share/shardflux/browser/export-${randomUUID()}.json`;
|
|
62
|
+
try {
|
|
63
|
+
const run = await cell.exec.run(['sf-guest-agent', 'browser-profile', 'export', site, filename], { env: { HOME: '/home/user' }, signal: ctx.signal, timeoutMs: 60_000, maxOutputBytes: 4096 });
|
|
64
|
+
if (run.exitCode !== 0 || run.truncated)
|
|
65
|
+
throw new UsageError('The workspace browser session could not be saved.');
|
|
66
|
+
let storage;
|
|
67
|
+
try {
|
|
68
|
+
storage = JSON.parse(await cell.files.readText(filename, { length: 4 * 1024 * 1024 + 1 }));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new UsageError('The workspace browser session could not be read.');
|
|
72
|
+
}
|
|
73
|
+
if (!storage.cookies?.length && !storage.origins?.some(o => o.localStorage.length))
|
|
74
|
+
throw new UsageError(`Sign in to ${site} through the workspace desktop, then retry save.`);
|
|
75
|
+
const profile = await ctx.cloud().browserProfiles.put(browserProfileName(site, ws.id), { scope: { workspace_id: ws.id }, sites: [{ site, storage_state: storage }] }, { signal: ctx.signal });
|
|
76
|
+
ctx.io.stdout.write(ctx.json ? `${JSON.stringify({ profile, security_note: note(true) })}\n` : `${summary([profile])}${note(true)}\n`);
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
await cell.files.remove(filename).catch(() => { });
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const browserSessionNote = "Stored encrypted with AWS KMS; only this project's workspaces can load it, and every load is logged.";
|
|
2
|
+
export declare const workspaceBrowserSessionNote = "Stored encrypted with AWS KMS; only this workspace can load it, and every load is logged.";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BrowserStorageState } from '@shardflux/sdk';
|
|
2
|
+
export type BrowserName = 'chrome' | 'arc' | 'brave' | 'edge' | 'safari' | 'firefox';
|
|
3
|
+
export { browserSessionNote } from './browser-profile-copy.js';
|
|
4
|
+
export { normalizeBrowserSite } from './browser-sites.js';
|
|
5
|
+
/** Called only by an explicit import. Tests pass a throwaway directory and synthetic key provider. */
|
|
6
|
+
export declare function browserDirectory(browser: BrowserName): Promise<string>;
|
|
7
|
+
export declare function safeStorageKey(browser: Exclude<BrowserName, 'safari' | 'firefox'>): Promise<Buffer>;
|
|
8
|
+
export declare function decryptChromiumCookie(encrypted: Uint8Array, password: Buffer, host: string, version: number): string;
|
|
9
|
+
export declare function readBrowserState(browser: BrowserName, directory: string, rawSites: string[], keyProvider?: typeof safeStorageKey): Promise<{
|
|
10
|
+
site: string;
|
|
11
|
+
storage_state: BrowserStorageState;
|
|
12
|
+
}[]>;
|
|
13
|
+
export declare function readSafariCookies(b: Buffer): BrowserStorageState['cookies'];
|