@shardflux/cli 0.11.0 → 0.13.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +85 -15
  3. package/dist/account-args.js +2 -0
  4. package/dist/account-commands.js +39 -0
  5. package/dist/agent-commands.d.ts +8 -0
  6. package/dist/agent-commands.js +223 -0
  7. package/dist/agent-config-files.d.ts +23 -0
  8. package/dist/agent-config-files.js +203 -0
  9. package/dist/agent-config-mcp.d.ts +36 -0
  10. package/dist/agent-config-mcp.js +225 -0
  11. package/dist/agent-config-sync.d.ts +58 -0
  12. package/dist/agent-config-sync.js +366 -0
  13. package/dist/agent-credentials.d.ts +14 -0
  14. package/dist/agent-credentials.js +93 -0
  15. package/dist/agent-git-commands.d.ts +10 -0
  16. package/dist/agent-git-commands.js +294 -0
  17. package/dist/agent-git.d.ts +47 -0
  18. package/dist/agent-git.js +284 -0
  19. package/dist/agent-reachability.d.ts +4 -0
  20. package/dist/agent-reachability.js +13 -0
  21. package/dist/agent-repository.d.ts +13 -0
  22. package/dist/agent-repository.js +182 -0
  23. package/dist/agent-state.d.ts +16 -0
  24. package/dist/agent-state.js +63 -0
  25. package/dist/agent-target-commands.d.ts +10 -0
  26. package/dist/agent-target-commands.js +171 -0
  27. package/dist/agent-targets.d.ts +33 -0
  28. package/dist/agent-targets.js +267 -0
  29. package/dist/agent-terminal.d.ts +35 -0
  30. package/dist/agent-terminal.js +209 -0
  31. package/dist/args.d.ts +8 -4
  32. package/dist/args.js +120 -10
  33. package/dist/assisted-setup.d.ts +24 -0
  34. package/dist/assisted-setup.js +77 -0
  35. package/dist/browser-commands.d.ts +3 -0
  36. package/dist/browser-commands.js +83 -0
  37. package/dist/browser-profile-copy.d.ts +2 -0
  38. package/dist/browser-profile-copy.js +2 -0
  39. package/dist/browser-readers.d.ts +13 -0
  40. package/dist/browser-readers.js +249 -0
  41. package/dist/browser-sites.d.ts +1 -0
  42. package/dist/browser-sites.js +13 -0
  43. package/dist/claude-cloud-login.d.ts +9 -0
  44. package/dist/claude-cloud-login.js +143 -0
  45. package/dist/command-center-actions.d.ts +6 -0
  46. package/dist/command-center-actions.js +132 -0
  47. package/dist/command-center-keys.d.ts +6 -0
  48. package/dist/command-center-keys.js +38 -0
  49. package/dist/command-center-model.d.ts +29 -0
  50. package/dist/command-center-model.js +90 -0
  51. package/dist/command-center-renderer.d.ts +25 -0
  52. package/dist/command-center-renderer.js +100 -0
  53. package/dist/command-center.d.ts +18 -0
  54. package/dist/command-center.js +320 -0
  55. package/dist/commands.d.ts +8 -2
  56. package/dist/commands.js +123 -8
  57. package/dist/credential-prompts.d.ts +22 -0
  58. package/dist/credential-prompts.js +193 -0
  59. package/dist/filesystem-commands.d.ts +2 -0
  60. package/dist/filesystem-commands.js +63 -0
  61. package/dist/filesystem-state.d.ts +3 -0
  62. package/dist/filesystem-state.js +12 -0
  63. package/dist/git-remote-bin.d.ts +2 -0
  64. package/dist/git-remote-bin.js +37 -0
  65. package/dist/git-remote.d.ts +14 -0
  66. package/dist/git-remote.js +136 -0
  67. package/dist/handlers.js +9 -1
  68. package/dist/index.d.ts +2 -0
  69. package/dist/index.js +1 -0
  70. package/dist/input.d.ts +5 -0
  71. package/dist/main.js +21 -5
  72. package/dist/menubar.d.ts +32 -0
  73. package/dist/menubar.js +180 -0
  74. package/dist/project-identity.d.ts +14 -0
  75. package/dist/project-identity.js +61 -0
  76. package/dist/project-sync-manifest.d.ts +27 -0
  77. package/dist/project-sync-manifest.js +225 -0
  78. package/dist/project-sync-transfer.d.ts +14 -0
  79. package/dist/project-sync-transfer.js +240 -0
  80. package/dist/project-sync.d.ts +38 -0
  81. package/dist/project-sync.js +243 -0
  82. package/dist/project-template-setup.d.ts +17 -0
  83. package/dist/project-template-setup.js +89 -0
  84. package/dist/vault.d.ts +16 -0
  85. package/dist/vault.js +153 -0
  86. package/package.json +11 -3
package/dist/commands.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { agentReachability } from "./agent-reachability.js";
2
+ import { vaultType, vaultValue, SECRET_SECURITY_NOTE } from "./vault.js";
1
3
  /**
2
4
  * Command implementations of the project-key commands (workspaces, files, operations, secrets, templates, usage).
3
5
  * Everything goes through @shardflux/sdk (the same client, retries, idempotency keys, operation polling and
@@ -6,6 +8,7 @@
6
8
  * joins both tables.
7
9
  */
8
10
  import { computerStreamCache } from "./computer-stream-cache.js";
11
+ import { agentConfigLikeSecrets, syncAgentConfig } from "./agent-config-sync.js";
9
12
  import { randomUUID } from 'node:crypto';
10
13
  import { access, readFile, writeFile } from 'node:fs/promises';
11
14
  import { basename, dirname, resolve as resolvePath } from 'node:path';
@@ -17,7 +20,7 @@ import { updateCheckDisabled } from "./update-check.js";
17
20
  import { initTemplateYaml, recipeYaml } from "./template-yaml.js";
18
21
  import { BuildTimer, buildTimingJson, buildTimingText, progressLine } from "./timing.js";
19
22
  import { timingJson } from "./timing.js";
20
- import { bindingsText, buildTable, buildText, changesTable, diffSummaryText, diffTable, draftText, draftStateTable, executionJson, executionSummary, fileInfoText, fileTable, languageTable, json, kv, memoryGrowText, burstText, modeText, operationDetail, pairs, resizeJson, resizeText, operationTable, packageTable, searchText, secretAccessTable, secretDetail, secretTable, secretVersionTable, sessionTable, durableText, exposedPortText, portTable, suspendRequestText, suspendWhenIdleText, templateDetailText, templateEntryText, templateFileTable, templateTable, time, usageEstimateText, usageSeriesText, usageText, workspaceDetail, workspaceTable, } from "./format.js";
23
+ import { table, bindingsText, buildTable, buildText, changesTable, diffSummaryText, diffTable, draftText, draftStateTable, executionJson, executionSummary, fileInfoText, fileTable, languageTable, json, kv, memoryGrowText, burstText, modeText, operationDetail, pairs, resizeJson, resizeText, operationTable, packageTable, searchText, secretAccessTable, secretDetail, secretTable, secretVersionTable, sessionTable, durableText, exposedPortText, portTable, suspendRequestText, suspendWhenIdleText, templateDetailText, templateEntryText, templateFileTable, templateTable, time, usageEstimateText, usageSeriesText, usageText, workspaceDetail, workspaceTable, } from "./format.js";
21
24
  const DEFAULT_WAIT_MS = 300_000;
22
25
  const MAX_JSON_OUTPUT = 16 * 1024 * 1024;
23
26
  const MAX_STDIN = 1024 * 1024;
@@ -54,6 +57,23 @@ function withTiming(ctx, machine) {
54
57
  export function out(ctx, human, machine) {
55
58
  ctx.io.stdout.write(ctx.json ? json(withTiming(ctx, machine)) : human);
56
59
  }
60
+ export function openVolumes(values) {
61
+ if (values.length === 0)
62
+ return undefined;
63
+ const mounts = {};
64
+ for (const value of values) {
65
+ const match = /^([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?):(\/[^:]+)(?::(ro|rw))?$/.exec(value);
66
+ if (!match)
67
+ throw new UsageError('--volume expects name:/absolute/path[:ro]');
68
+ const [, name, path, mode] = match;
69
+ if (Object.hasOwn(mounts, path))
70
+ throw new UsageError(`--volume names mount ${path} twice`);
71
+ if (Object.values(mounts).some((v) => (typeof v === 'string' ? v : v.name) === name))
72
+ throw new UsageError(`--volume names volume ${name} twice`);
73
+ mounts[path] = mode ? { name: name, mode: mode } : name;
74
+ }
75
+ return mounts;
76
+ }
57
77
  function caps(values) {
58
78
  const c = {};
59
79
  const cpu = str(values['cpu-millis']);
@@ -105,7 +125,7 @@ function resizeParams(values) {
105
125
  * The caps of `workspaces open` and `workspaces fork` (0.8.0): the resource caps plus the memory allocation. Given caps
106
126
  * replace the stored ones, so they are sent only when a flag was given.
107
127
  */
108
- function workspaceCaps(values) {
128
+ export function workspaceCaps(values) {
109
129
  const c = caps(values) ?? {};
110
130
  const mode = oneOf(values, 'allocation-mode', ALLOCATION_MODES);
111
131
  const held = str(values['memory-held']);
@@ -651,9 +671,23 @@ function imageOpts(values) {
651
671
  function lookOpts(values) {
652
672
  const shotTo = str(values.screenshot);
653
673
  const settle = str(values.settle);
674
+ const pixelText = str(values['pixel-predicate']);
675
+ let pixelPredicate;
676
+ if (pixelText !== undefined) {
677
+ try {
678
+ pixelPredicate = JSON.parse(pixelText);
679
+ }
680
+ catch {
681
+ throw new UsageError('--pixel-predicate must be a JSON object');
682
+ }
683
+ if (!pixelPredicate || typeof pixelPredicate !== 'object' || Array.isArray(pixelPredicate))
684
+ throw new UsageError('--pixel-predicate must be a JSON object');
685
+ if (shotTo === undefined || settle !== undefined || str(values.format) === 'jpeg')
686
+ throw new UsageError('--pixel-predicate requires --screenshot, PNG and no --settle');
687
+ }
654
688
  if (settle !== undefined && (!/^\d+$/.test(settle.trim()) || Number(settle) > 10_000))
655
689
  throw new UsageError(`--settle must be 0 to 10000 milliseconds (got "${settle}")`);
656
- return { shotTo, req: { screenshot: shotTo !== undefined, ...(settle !== undefined ? { settle_ms: Number(settle) } : {}), ...imageOpts(values) } };
690
+ return { shotTo, req: { screenshot: shotTo !== undefined, ...(pixelPredicate ? { pixel_predicate: pixelPredicate } : {}), ...(settle !== undefined ? { settle_ms: Number(settle) } : {}), ...imageOpts(values) } };
657
691
  }
658
692
  /** A screen coordinate argument (pixels from the top-left). */
659
693
  function pixel(text, name) {
@@ -687,7 +721,9 @@ async function runComputer(ctx, ref, actions, look) {
687
721
  });
688
722
  const tail = `cursor ${r.cursor[0]},${r.cursor[1]} on a ${r.display.width}x${r.display.height} screen${look.shotTo !== undefined && r.screenshot ? `; screenshot written to ${look.shotTo}` : ''}`;
689
723
  out(ctx, `${lines.join('\n')}${lines.length ? '\n' : ''}${tail}\n`, r);
690
- return r.results.every((x) => x.ok) ? 0 : 1;
724
+ if (r.pixel_completion && !r.pixel_completion.matched)
725
+ ctx.io.stderr.write(`Pixel condition failed: ${r.pixel_completion.reason}; input results above say which actions ran.\n`);
726
+ return r.results.every((x) => x.ok) && r.pixel_completion?.matched !== false ? 0 : 1;
691
727
  }
692
728
  async function computerWorkspace(ctx, ref) {
693
729
  const ws = await resolveWorkspace(ctx, ref);
@@ -748,7 +784,41 @@ function lifecycleText(verb, w, op) {
748
784
  return `${verb} ${w.key !== undefined ? `${w.key} (${w.id})` : w.id}: operation ${op.id} ${op.kind} ${op.state}${op.state_reason ? ` (${op.state_reason})` : ''}\n${finished ? durableText(op) : `wait with: shard operations wait ${op.id}\n`}`;
749
785
  }
750
786
  /** The project-key commands; handlers.ts adds the account plane's. */
787
+ function agentGrace(text) {
788
+ if (!/^[0-9]+$/.test(text) || Number(text) > 3600)
789
+ throw new UsageError('--agent-idle-grace-seconds must be 0–3600');
790
+ return Number(text);
791
+ }
751
792
  export const KEY_HANDLERS = {
793
+ async 'agents list'(ctx, values) {
794
+ const state = str(values.state);
795
+ if (state && !['working', 'waiting', 'exited'].includes(state))
796
+ throw new UsageError('--state must be working, waiting or exited');
797
+ const labels = {};
798
+ for (const pair of strs(values.label)) {
799
+ const i = pair.indexOf('=');
800
+ if (i < 1)
801
+ throw new UsageError('--label must be key=value');
802
+ labels[pair.slice(0, i)] = pair.slice(i + 1);
803
+ }
804
+ const opts = {
805
+ ...(state ? { state: state } : {}), labels,
806
+ ...(str(values.limit) ? { limit: parsePositiveInt(str(values.limit), 'limit', 200) } : {}),
807
+ };
808
+ const rows = [];
809
+ let cursor = str(values.cursor);
810
+ let next;
811
+ do {
812
+ const page = await ctx.cloud().agents.list({ ...opts, ...(cursor ? { cursor } : {}) });
813
+ rows.push(...page.data);
814
+ next = page.nextCursor;
815
+ cursor = next ?? undefined;
816
+ } while (values.all === true && cursor);
817
+ out(ctx, rows.length ? table(['WORKSPACE', 'APP', 'STATE', 'TITLE'], rows.map(a => [a.workspace_key, a.app, a.state, a.title])) : 'No agent sessions.\n', { data: rows, next_cursor: next });
818
+ if (!ctx.json && next)
819
+ ctx.io.stderr.write(`more: shard agents list --cursor ${next}\n`);
820
+ return 0;
821
+ },
752
822
  async login(ctx) {
753
823
  const p = await principal(ctx);
754
824
  const where = ctx.apiKey().source === 'env'
@@ -977,8 +1047,10 @@ export const KEY_HANDLERS = {
977
1047
  },
978
1048
  async 'workspaces open'(ctx, values, [key]) {
979
1049
  const template = str(values.template);
980
- if (!template)
981
- throw new UsageError('--template <slug> is required for "shard workspaces open"');
1050
+ const like = str(values.like);
1051
+ if (!template && !like)
1052
+ throw new UsageError('--template <slug> or --like <id|key> is required for "shard workspaces open"');
1053
+ const volumes = openVolumes(strs(values.volume));
982
1054
  const c = workspaceCaps(values);
983
1055
  const bind = secretNames(strs(values.secret), '--secret');
984
1056
  const lifetime = oneOf(values, 'lifetime', LIFETIMES);
@@ -994,14 +1066,19 @@ export const KEY_HANDLERS = {
994
1066
  const cloud = ctx.cloud();
995
1067
  // The SDK's waited open: the server holds the request until the workspace is ready (Prefer: wait), then the SDK
996
1068
  // polls for what is left of --timeout; one trace (ws.lastTiming) covers all of it.
1069
+ const openSecrets = bind.length ? bind : like ? await agentConfigLikeSecrets(ctx, like) : undefined;
997
1070
  const ws = await waited(ctx, 'the open continues server side (open the key again to keep waiting).', () => cloud.workspaces.open({
998
1071
  key: key,
999
- template,
1072
+ ...(template ? { template } : {}),
1073
+ ...(like ? { like } : {}),
1074
+ ...(volumes ? { volumes } : {}),
1000
1075
  ...(str(values.labels) !== undefined ? { labels: labelMap(str(values.labels)) } : {}),
1001
1076
  ...(str(values['delete-after-idle']) !== undefined ? { retention: openRetentionDays(str(values['delete-after-idle'])) } : {}),
1077
+ ...agentReachability(values),
1078
+ ...(str(values['agent-idle-grace-seconds']) !== undefined ? { agentIdleGraceSeconds: agentGrace(str(values['agent-idle-grace-seconds'])) } : {}),
1002
1079
  ...(str(values['idle-policy']) ? { idlePolicy: str(values['idle-policy']) } : {}),
1003
1080
  ...(c ? { caps: c } : {}),
1004
- ...(bind.length ? { secrets: bind } : {}),
1081
+ ...(openSecrets ? { secrets: openSecrets } : {}),
1005
1082
  ...(inputs ? { inputs } : {}),
1006
1083
  ...(lifetime ? { lifetime } : {}),
1007
1084
  ...(mode ? { mode } : {}),
@@ -1010,6 +1087,8 @@ export const KEY_HANDLERS = {
1010
1087
  wait: values.wait === false ? false : { timeoutMs, signal: ctx.signal },
1011
1088
  }));
1012
1089
  const op = ws.activeOperation;
1090
+ if (ws.ready && ws.mode === 'processful' && ws.labels.shard_project)
1091
+ await syncAgentConfig(ctx, ws);
1013
1092
  const view = ws.data;
1014
1093
  const human = ws.ready
1015
1094
  ? `Workspace ${ws.key} (${ws.id}) is ready.\n${workspaceDetail(view)}`
@@ -1055,6 +1134,8 @@ export const KEY_HANDLERS = {
1055
1134
  },
1056
1135
  async 'workspaces get'(ctx, _values, [ref]) {
1057
1136
  const ws = await resolveWorkspace(ctx, ref);
1137
+ if (!UUID.test(ref))
1138
+ await ws.refresh();
1058
1139
  out(ctx, workspaceDetail(ws.data), ws.data);
1059
1140
  return 0;
1060
1141
  },
@@ -1177,6 +1258,14 @@ export const KEY_HANDLERS = {
1177
1258
  }
1178
1259
  return code;
1179
1260
  },
1261
+ async 'files read-many'(ctx, values, [ref]) {
1262
+ const paths = Array.isArray(values.path) ? values.path.map(String) : typeof values.path === 'string' ? [values.path] : [];
1263
+ const ws = await resolveWorkspace(ctx, ref);
1264
+ const result = await cellOf(ctx, ws).files.readSealedMany(paths, { generation: str(values.generation) });
1265
+ const wire = { ...result, items: result.items.map(item => item.data === undefined ? item : { ...item, data: Buffer.from(item.data).toString('base64') }) };
1266
+ ctx.io.stdout.write(json(withTiming(ctx, wire)));
1267
+ return result.complete ? 0 : 1;
1268
+ },
1180
1269
  async 'files read'(ctx, values, [ref, path]) {
1181
1270
  const ws = await resolveWorkspace(ctx, ref);
1182
1271
  const bytes = await cellOf(ctx, ws).files.read(path);
@@ -1985,6 +2074,13 @@ export const KEY_HANDLERS = {
1985
2074
  throw new UsageError('<text> must not be empty');
1986
2075
  return runComputer(ctx, ref, [{ action: 'type', text }], lookOpts(values));
1987
2076
  },
2077
+ async 'workspaces computer paste-text'(ctx, values, [ref, source]) {
2078
+ const text = source === '-' ? Buffer.from(await readInput(ctx, '-', 256 << 10, 'text')).toString('utf8') : source;
2079
+ if (text === '')
2080
+ throw new UsageError('<text> must not be empty');
2081
+ const chord = str(values['paste-chord']);
2082
+ return runComputer(ctx, ref, [{ action: 'pasteText', text, ...(chord !== undefined ? { paste_chord: chord } : {}) }], lookOpts(values));
2083
+ },
1988
2084
  async 'workspaces computer key'(ctx, values, [ref, keys]) {
1989
2085
  const repeat = str(values.repeat) !== undefined ? parsePositiveInt(str(values.repeat), 'repeat', 100) : undefined;
1990
2086
  return runComputer(ctx, ref, [{ action: 'key', text: keys, ...(repeat !== undefined ? { repeat } : {}) }], lookOpts(values));
@@ -2101,6 +2197,25 @@ export const KEY_HANDLERS = {
2101
2197
  out(ctx, bindingsText(b), b);
2102
2198
  return 0;
2103
2199
  },
2200
+ async 'secrets import'(ctx, values, names) {
2201
+ const from = vaultType(str(values.from));
2202
+ const alias = str(values.as);
2203
+ const keychain = str(values.keychain);
2204
+ if (alias && names.length !== 1)
2205
+ throw new UsageError('--as requires exactly one item');
2206
+ if (keychain && from !== 'keychain')
2207
+ throw new UsageError('--keychain applies to the keychain vault');
2208
+ const targets = secretNames(names.map(n => alias ?? n), '--as');
2209
+ const project = (await principal(ctx)).project_id;
2210
+ const imported = [];
2211
+ for (const [i, name] of names.entries()) {
2212
+ const value = await vaultValue(from, name, { signal: ctx.signal, ...(keychain ? { keychain } : {}) });
2213
+ ctx.addSecret(value);
2214
+ imported.push(await ctx.cloud().secrets.create(project, { name: targets[i], value }));
2215
+ }
2216
+ out(ctx, `Imported ${imported.map(s => s.name).join(', ')}. ${SECRET_SECURITY_NOTE}\n`, { data: imported, security_note: SECRET_SECURITY_NOTE });
2217
+ return 0;
2218
+ },
2104
2219
  async 'secrets create'(ctx, values, [name]) {
2105
2220
  if (!SECRET_NAME.test(name))
2106
2221
  throw new UsageError(`<name> must be an environment variable name like OPENAI_API_KEY (^[A-Z_][A-Z0-9_]*$), got "${name}"`);
@@ -0,0 +1,22 @@
1
+ import type { CredentialRequest, Workspace } from '@shardflux/sdk';
2
+ import type { Ctx, Handler } from './commands.js';
3
+ import type { Vault } from './vault.js';
4
+ type Selection = {
5
+ vault: Vault;
6
+ item: string;
7
+ always: boolean;
8
+ fields: CredentialRequest['fields'];
9
+ };
10
+ export declare class SiteVaultStore {
11
+ readonly file: string;
12
+ constructor(directory: string);
13
+ key(api: string, project: string, site: string): string;
14
+ read(): Record<string, Selection>;
15
+ get(api: string, project: string, site: string): Selection | undefined;
16
+ save(api: string, project: string, site: string, selection: Selection): void;
17
+ }
18
+ export declare function approveCredential(ctx: Ctx, request: CredentialRequest, selection: Selection, remember?: boolean): Promise<void>;
19
+ export declare function configureCredentialFill(ctx: Ctx, ws: Workspace): Promise<boolean>;
20
+ export declare function watchCredentialPrompts(ctx: Ctx, ws: Workspace, ask: (message: string) => Promise<string>, signal: AbortSignal): Promise<void>;
21
+ export declare const CREDENTIAL_HANDLERS: Record<string, Handler>;
22
+ export {};
@@ -0,0 +1,193 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { ShardfluxApiError, sealCredential } from '@shardflux/sdk';
4
+ import { resolveWorkspace } from "./commands.js";
5
+ import { str } from "./commands.js";
6
+ import { UsageError } from "./errors.js";
7
+ import { REMEMBERED_SECURITY_NOTE, VAULT_SECURITY_NOTE, vaultCredential, vaultType } from "./vault.js";
8
+ export class SiteVaultStore {
9
+ file;
10
+ constructor(directory) {
11
+ this.file = join(directory, 'site-vaults.json');
12
+ }
13
+ key(api, project, site) {
14
+ return JSON.stringify([api.replace(/\/$/, ''), project, site]);
15
+ }
16
+ read() {
17
+ if (!existsSync(this.file))
18
+ return {};
19
+ const raw = JSON.parse(readFileSync(this.file, 'utf8'));
20
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
21
+ throw new UsageError('Could not read site vault choices.');
22
+ return raw;
23
+ }
24
+ get(api, project, site) {
25
+ return this.read()[this.key(api, project, site)];
26
+ }
27
+ save(api, project, site, selection) {
28
+ const all = this.read();
29
+ all[this.key(api, project, site)] = selection;
30
+ mkdirSync(join(this.file, '..'), { recursive: true, mode: 0o700 });
31
+ const temp = `${this.file}.${process.pid}.tmp`;
32
+ writeFileSync(temp, JSON.stringify(all), { mode: 0o600, flag: 'wx' });
33
+ renameSync(temp, this.file);
34
+ chmodSync(this.file, 0o600);
35
+ }
36
+ }
37
+ export async function approveCredential(ctx, request, selection, remember = false) {
38
+ if (request.state !== 'pending' || Date.parse(request.expires_at) <= Date.now())
39
+ throw new UsageError(`Credential request ${request.id} has expired or ended. ${VAULT_SECURITY_NOTE}`);
40
+ const credential = await vaultCredential(selection.vault, selection.item, request.site, { signal: ctx.signal });
41
+ for (const value of Object.values(credential))
42
+ if (typeof value === 'string' && value)
43
+ ctx.addSecret(value);
44
+ const required = request.fields.map((f) => (f === 'totp' ? 'totp_seed' : f));
45
+ if (required.some((f) => !credential[f]))
46
+ throw new UsageError(`The item does not contain every requested field. ${VAULT_SECURITY_NOTE}`);
47
+ const subset = Object.fromEntries(required.map((f) => [f, credential[f]]));
48
+ const ciphertext = sealCredential(request.public_key, request, subset);
49
+ const answered = await ctx.cloud().credentialRequests.fulfil(request.id, ciphertext);
50
+ if (answered.state !== 'fulfilled')
51
+ throw new UsageError(`Credential request is ${answered.state}. ${VAULT_SECURITY_NOTE}`);
52
+ if (remember)
53
+ await ctx.cloud().credentialRequests.remember(request.workspace_id, request.site, credential);
54
+ new SiteVaultStore(ctx.store.dir).save(ctx.apiUrl, request.project_id, request.site, selection);
55
+ ctx.io.stderr.write(`Approved ${request.site}. ${remember ? REMEMBERED_SECURITY_NOTE : VAULT_SECURITY_NOTE}\n`);
56
+ }
57
+ export async function configureCredentialFill(ctx, ws) {
58
+ try {
59
+ await ctx.cloud().credentialRequests.configure(ws, { signal: ctx.signal });
60
+ return true;
61
+ }
62
+ catch (e) {
63
+ if (e instanceof ShardfluxApiError &&
64
+ (e.status === 403 ||
65
+ e.status === 404 ||
66
+ (e.status === 503 && e.details?.reason === 'credential_requests_unavailable')))
67
+ return false;
68
+ // Fill is optional while older guest images are still in use.
69
+ // The explicit credentials enable command continues to report the failure.
70
+ if (e instanceof Error && e.message.startsWith('Credential '))
71
+ return false;
72
+ throw e;
73
+ }
74
+ }
75
+ export async function watchCredentialPrompts(ctx, ws, ask, signal) {
76
+ const seen = new Set();
77
+ const store = new SiteVaultStore(ctx.store.dir);
78
+ while (!signal.aborted) {
79
+ const page = await ctx.cloud().credentialRequests.list({
80
+ workspaceId: ws.id,
81
+ state: 'pending',
82
+ limit: 200,
83
+ });
84
+ for (const r of page.data) {
85
+ if (signal.aborted)
86
+ return;
87
+ if (seen.has(r.id))
88
+ continue;
89
+ seen.add(r.id);
90
+ try {
91
+ const saved = store.get(ctx.apiUrl, r.project_id, r.site);
92
+ if (saved?.always && r.fields.every((f) => saved.fields.includes(f))) {
93
+ await approveCredential(ctx, r, saved);
94
+ continue;
95
+ }
96
+ const choice = (await ask(`${r.site} requests ${r.fields.join(', ')}. Allow once / Always for this project / Deny [once/always/deny]. ${VAULT_SECURITY_NOTE}\n`)).toLowerCase();
97
+ if (choice === 'deny') {
98
+ await ctx.cloud().credentialRequests.deny(r.id);
99
+ ctx.io.stderr.write(`Denied ${r.site}. ${VAULT_SECURITY_NOTE}\n`);
100
+ continue;
101
+ }
102
+ if (choice !== 'once' && choice !== 'always')
103
+ throw new UsageError(`Choose once, always or deny. ${VAULT_SECURITY_NOTE}`);
104
+ const vault = vaultType(saved?.vault ?? (await ask(`Choose hsec, 1password, bitwarden or keychain. ${VAULT_SECURITY_NOTE}\n`)));
105
+ const item = saved?.item ?? (await ask(`Vault item name for ${r.site}. ${VAULT_SECURITY_NOTE}\n`));
106
+ const remember = (await ask(`Remember for unattended use? [yes/no]. ${REMEMBERED_SECURITY_NOTE}\n`)).toLowerCase() === 'yes';
107
+ await approveCredential(ctx, r, { vault, item, always: choice === 'always', fields: r.fields }, remember);
108
+ }
109
+ catch (e) {
110
+ if (signal.aborted)
111
+ return;
112
+ ctx.io.stderr.write(`Credential approval could not finish. ${VAULT_SECURITY_NOTE}\n`);
113
+ if (e instanceof ShardfluxApiError && e.status === 401)
114
+ throw e;
115
+ }
116
+ }
117
+ await new Promise((resolve) => {
118
+ const done = () => {
119
+ clearTimeout(timer);
120
+ signal.removeEventListener('abort', done);
121
+ resolve();
122
+ };
123
+ const timer = setTimeout(done, 5000);
124
+ signal.addEventListener('abort', done, { once: true });
125
+ if (signal.aborted)
126
+ done();
127
+ });
128
+ }
129
+ }
130
+ export const CREDENTIAL_HANDLERS = {
131
+ async 'secrets log'(ctx) {
132
+ const events = await ctx.cloud().credentialRequests.events({ limit: 200 });
133
+ ctx.io.stdout.write(ctx.json
134
+ ? JSON.stringify({ ...events, security_note: VAULT_SECURITY_NOTE }) + '\n'
135
+ : events.data
136
+ .map((e) => `${e.created_at} ${e.action} ${typeof e.details.site === 'string' ? e.details.site : ''}. ${VAULT_SECURITY_NOTE}\n`)
137
+ .join(''));
138
+ return 0;
139
+ },
140
+ async 'credentials list'(ctx) {
141
+ const page = await ctx.cloud().credentialRequests.list({ state: 'pending', limit: 200 });
142
+ ctx.io.stdout.write(ctx.json
143
+ ? JSON.stringify({ ...page, security_note: VAULT_SECURITY_NOTE }) + '\n'
144
+ : page.data.map((r) => `${r.id} ${r.site} ${r.fields.join(', ')}. ${VAULT_SECURITY_NOTE}\n`).join(''));
145
+ return 0;
146
+ },
147
+ async 'credentials enable'(ctx, _values, [ref]) {
148
+ const ws = await resolveWorkspace(ctx, ref);
149
+ await ctx.cloud().credentialRequests.configure(ws, { signal: ctx.signal });
150
+ ctx.io.stdout.write(ctx.json
151
+ ? JSON.stringify({
152
+ enabled: true,
153
+ workspace_id: ws.id,
154
+ security_note: VAULT_SECURITY_NOTE,
155
+ }) + '\n'
156
+ : `Credential fill enabled for ${ws.key}. ${VAULT_SECURITY_NOTE}\n`);
157
+ return 0;
158
+ },
159
+ async 'credentials approve'(ctx, values, [id]) {
160
+ const { request } = await ctx.cloud().credentialRequests.get(id);
161
+ const vault = vaultType(str(values.from));
162
+ const item = str(values.item);
163
+ if (!item)
164
+ throw new UsageError('--item names a vault login');
165
+ await approveCredential(ctx, request, { vault, item, always: values.always === true, fields: request.fields }, values.remember === true);
166
+ if (ctx.json)
167
+ ctx.io.stdout.write(JSON.stringify({
168
+ approved: true,
169
+ request_id: request.id,
170
+ security_note: values.remember === true ? REMEMBERED_SECURITY_NOTE : VAULT_SECURITY_NOTE,
171
+ }) + '\n');
172
+ return 0;
173
+ },
174
+ async 'credentials deny'(ctx, _values, [id]) {
175
+ const r = await ctx.cloud().credentialRequests.deny(id);
176
+ ctx.io.stdout.write(ctx.json
177
+ ? JSON.stringify({ ...r, security_note: VAULT_SECURITY_NOTE }) + '\n'
178
+ : `Credential request ${r.state}. ${VAULT_SECURITY_NOTE}\n`);
179
+ return 0;
180
+ },
181
+ async 'credentials forget'(ctx, _values, [ref, site]) {
182
+ const ws = await resolveWorkspace(ctx, ref);
183
+ await ctx.cloud().credentialRequests.forget(ws.id, site);
184
+ ctx.io.stdout.write(ctx.json
185
+ ? JSON.stringify({
186
+ forgotten: true,
187
+ site,
188
+ security_note: REMEMBERED_SECURITY_NOTE,
189
+ }) + '\n'
190
+ : `Removed remembered credential for ${site}. ${REMEMBERED_SECURITY_NOTE}\n`);
191
+ return 0;
192
+ },
193
+ };
@@ -0,0 +1,2 @@
1
+ import type { Handler } from './commands.js';
2
+ export declare const FILESYSTEM_HANDLERS: Record<string, Handler>;
@@ -0,0 +1,63 @@
1
+ import { UsageError } from "./errors.js";
2
+ import { projectIdentity, normalizeRemote } from "./project-identity.js";
3
+ import { AGENT_HANDLERS } from "./agent-commands.js";
4
+ import { syncAgentConfig } from "./agent-config-sync.js";
5
+ function remote() {
6
+ const project = projectIdentity('./');
7
+ return { project, repoRemote: project.remote ?? normalizeRemote(`file://${project.folder}`) };
8
+ }
9
+ function output(ctx, state) {
10
+ ctx.io.stdout.write(ctx.json ? JSON.stringify(state) + '\n' : `${state.level}: ${state.state}; version ${state.version}\n` +
11
+ (state.pending ? `Saving version ${state.pending.version}. Run shard ${state.level} --json for progress.\n` : '') +
12
+ (state.failure ? `${state.failure.message}\n` : '') +
13
+ (state.template ? `${state.template.slug}@${state.template.version}\n` : 'No saved version yet.\n'));
14
+ return 0;
15
+ }
16
+ const rollbackTo = (value) => {
17
+ if (value === undefined)
18
+ return undefined;
19
+ const n = Number(value);
20
+ if (!Number.isSafeInteger(n) || n < 1)
21
+ throw new UsageError('--to must be a saved version number');
22
+ return n;
23
+ };
24
+ export const FILESYSTEM_HANDLERS = {
25
+ async machine(ctx) {
26
+ const cloud = ctx.cloud();
27
+ const state = await cloud.machine.get({ signal: ctx.signal });
28
+ if (ctx.json || !ctx.io.stdin.isTTY)
29
+ return output(ctx, state);
30
+ const key = `machine/${state.organization_id}/v${state.version}`;
31
+ const ws = await cloud.workspaces.open({ key, template: state.template ? `${state.template.slug}@${state.template.version}` : 'coding',
32
+ labels: { shard_layer: 'machine', shard_machine: state.organization_id, repo_path: '/home/user', app: 'claude', agent: 'machine' },
33
+ wait: { timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal } });
34
+ if (state.version === 0)
35
+ await syncAgentConfig(ctx, ws);
36
+ return AGENT_HANDLERS['agents open'](ctx, {}, [ws.key]);
37
+ },
38
+ async 'machine save'(ctx) {
39
+ const cloud = ctx.cloud();
40
+ const state = await cloud.machine.get({ signal: ctx.signal });
41
+ const ws = await cloud.workspaces.findByKey(`machine/${state.organization_id}/v${state.version}`, { signal: ctx.signal }) ??
42
+ (state.workspace_id ? await cloud.workspaces.get(state.workspace_id) : null);
43
+ if (!ws)
44
+ throw new UsageError('Open shard machine, change it, then run shard machine save');
45
+ return output(ctx, await cloud.machine.save({ workspaceId: ws.id }, { signal: ctx.signal }));
46
+ },
47
+ async 'machine rollback'(ctx, values) {
48
+ return output(ctx, await ctx.cloud().machine.rollback({ to: rollbackTo(values.to) }, { signal: ctx.signal }));
49
+ },
50
+ async template(ctx) {
51
+ return output(ctx, await ctx.cloud().templates.getProject(remote().repoRemote, { signal: ctx.signal }));
52
+ },
53
+ async 'template save'(ctx) {
54
+ const { project, repoRemote } = remote();
55
+ const ws = await ctx.cloud().workspaces.findByKey(`${project.slug}/main`, { signal: ctx.signal });
56
+ if (!ws)
57
+ throw new UsageError('Run shard ./ for this project before saving its template');
58
+ return output(ctx, await ctx.cloud().templates.saveProject({ repoRemote, workspaceId: ws.id }, { signal: ctx.signal }));
59
+ },
60
+ async 'template rollback'(ctx, values) {
61
+ return output(ctx, await ctx.cloud().templates.rollbackProject({ repoRemote: remote().repoRemote, to: rollbackTo(values.to) }, { signal: ctx.signal }));
62
+ },
63
+ };
@@ -0,0 +1,3 @@
1
+ import type { AgentFilesystem } from '@shardflux/sdk';
2
+ import type { Ctx } from './commands.js';
3
+ export declare function savedMachine(ctx: Ctx): Promise<AgentFilesystem | undefined>;
@@ -0,0 +1,12 @@
1
+ import { ShardfluxApiError } from '@shardflux/sdk';
2
+ export async function savedMachine(ctx) {
3
+ try {
4
+ return await ctx.cloud().machine.get({ signal: ctx.signal });
5
+ }
6
+ catch (err) {
7
+ // Older APIs have no filesystem route; unmigrated APIs report its dependency.
8
+ if (err instanceof ShardfluxApiError && (err.status === 404 || err.details?.reason === 'filesystem_unavailable'))
9
+ return undefined;
10
+ throw err;
11
+ }
12
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import { Shardflux } from '@shardflux/sdk';
3
+ import { CredentialStore, configDir } from "./credentials.js";
4
+ import { resolveApiUrl } from "./args.js";
5
+ import { remoteHelper } from "./git-remote.js";
6
+ import { redact } from "./errors.js";
7
+ const controller = new AbortController();
8
+ const abort = () => { controller.abort(); process.stdin.destroy(); };
9
+ process.once('SIGINT', abort);
10
+ process.once('SIGTERM', abort);
11
+ let cell;
12
+ let key = '';
13
+ try {
14
+ const apiUrl = resolveApiUrl(undefined, process.env.SHARDFLUX_API_URL ?? process.env.SHARDFLUX_BASE_URL);
15
+ const store = new CredentialStore(configDir(process.env));
16
+ process.exitCode = await remoteHelper(process.argv[3] ?? process.argv[2] ?? '', process.stdin, process.stdout, process.stderr, async (remote) => {
17
+ key = process.env.SHARDFLUX_API_KEY?.trim() || store.profile(apiUrl).api_key?.token || '';
18
+ if (!key)
19
+ throw new Error('sign in and save a project key with shard setup');
20
+ const cloud = new Shardflux({ apiKey: key, baseUrl: apiUrl, versionCheck: false });
21
+ const ws = await cloud.workspaces.findByKey(remote.key, { includeDeleted: false, signal: controller.signal });
22
+ if (!ws)
23
+ throw new Error(`workspace ${remote.key} does not exist`);
24
+ cell = ws.cell({ agentLabel: 'git-remote-shard', transitionTimeoutMs: 120_000 });
25
+ return cell.exec;
26
+ }, controller.signal);
27
+ }
28
+ catch (error) {
29
+ process.stderr.write(`git-remote-shard: ${redact(error instanceof Error ? error.message : String(error), key)}\n`);
30
+ process.exitCode = controller.signal.aborted ? 130 : 1;
31
+ }
32
+ finally {
33
+ cell?.close();
34
+ process.stdin.destroy();
35
+ process.removeListener('SIGINT', abort);
36
+ process.removeListener('SIGTERM', abort);
37
+ }
@@ -0,0 +1,14 @@
1
+ import type { CellClient } from '@shardflux/sdk';
2
+ import type { Readable, Writable } from 'node:stream';
3
+ export interface ShardRemote {
4
+ key: string;
5
+ path: string;
6
+ }
7
+ /** Keys have two segments (K9). Everything after the agent is an absolute guest path. */
8
+ export declare function parseShardRemote(url: string): ShardRemote;
9
+ export declare function shardRemote(key: string, path: string): string;
10
+ export type GitExec = Pick<CellClient['exec'], 'start' | 'input' | 'output' | 'cancel'>;
11
+ /** No text decoding of the pack stream; offsets are acknowledged bytes, including partial stdin writes. */
12
+ export declare function bridgeGit(exec: GitExec, service: string, path: string, input: AsyncIterable<Uint8Array>, stdout: Writable, stderr: Writable, signal: AbortSignal): Promise<number>;
13
+ /** Git's `connect` capability switches from newline commands to a full-duplex smart protocol. */
14
+ export declare function remoteHelper(url: string, stdin: Readable, stdout: Writable, stderr: Writable, connect: (remote: ShardRemote) => Promise<GitExec>, signal: AbortSignal): Promise<number>;