@shardflux/cli 0.19.1 → 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.
Files changed (115) hide show
  1. package/CHANGELOG.md +278 -1
  2. package/README.md +225 -73
  3. package/dist/account-args.js +7 -2
  4. package/dist/account-commands.js +56 -106
  5. package/dist/account-pool.d.ts +20 -15
  6. package/dist/account-pool.js +51 -97
  7. package/dist/account-scope.d.ts +65 -0
  8. package/dist/account-scope.js +246 -0
  9. package/dist/acp-login.d.ts +4 -3
  10. package/dist/acp-login.js +7 -5
  11. package/dist/acp-models.js +20 -15
  12. package/dist/acp.js +66 -13
  13. package/dist/agent-commands.d.ts +4 -1
  14. package/dist/agent-commands.js +86 -32
  15. package/dist/agent-config-files.d.ts +19 -0
  16. package/dist/agent-config-files.js +70 -1
  17. package/dist/agent-config-return.d.ts +194 -0
  18. package/dist/agent-config-return.js +724 -0
  19. package/dist/agent-config-sync.d.ts +10 -2
  20. package/dist/agent-config-sync.js +45 -51
  21. package/dist/agent-credentials.js +7 -3
  22. package/dist/agent-repository.d.ts +16 -3
  23. package/dist/agent-repository.js +149 -74
  24. package/dist/agent-setup.d.ts +14 -3
  25. package/dist/agent-setup.js +78 -36
  26. package/dist/agent-target-commands.js +24 -13
  27. package/dist/args.d.ts +1 -1
  28. package/dist/args.js +104 -55
  29. package/dist/bin.js +5 -1
  30. package/dist/browser.d.ts +13 -2
  31. package/dist/browser.js +30 -14
  32. package/dist/claude-cloud-login.d.ts +24 -2
  33. package/dist/claude-cloud-login.js +151 -11
  34. package/dist/cli-proxy-forward.d.ts +2 -0
  35. package/dist/cli-proxy-forward.js +9 -2
  36. package/dist/cli-proxy-hub.d.ts +69 -25
  37. package/dist/cli-proxy-hub.js +231 -54
  38. package/dist/cli-proxy.d.ts +70 -1
  39. package/dist/cli-proxy.js +298 -32
  40. package/dist/client-settings.d.ts +39 -0
  41. package/dist/client-settings.js +91 -0
  42. package/dist/codex-cloud.d.ts +5 -0
  43. package/dist/codex-cloud.js +88 -17
  44. package/dist/command-center-actions.js +5 -1
  45. package/dist/commands.js +171 -22
  46. package/dist/conversation-commands.d.ts +5 -0
  47. package/dist/conversation-commands.js +8 -5
  48. package/dist/conversation-terminal.d.ts +10 -2
  49. package/dist/conversation-terminal.js +81 -15
  50. package/dist/conversations.d.ts +77 -1
  51. package/dist/conversations.js +257 -23
  52. package/dist/device-bridge.d.ts +1 -1
  53. package/dist/device-bridge.js +6 -4
  54. package/dist/device-helper.js +52 -4
  55. package/dist/errors.d.ts +34 -1
  56. package/dist/errors.js +56 -5
  57. package/dist/filesystem-commands.d.ts +22 -3
  58. package/dist/filesystem-commands.js +132 -16
  59. package/dist/format.d.ts +2 -1
  60. package/dist/format.js +10 -1
  61. package/dist/handlers.js +4 -1
  62. package/dist/initial-prompt.d.ts +8 -0
  63. package/dist/initial-prompt.js +43 -1
  64. package/dist/large-media.d.ts +14 -0
  65. package/dist/large-media.js +100 -0
  66. package/dist/linux-terminal.d.ts +5 -3
  67. package/dist/linux-terminal.js +6 -20
  68. package/dist/local-sessions.d.ts +26 -2
  69. package/dist/local-sessions.js +113 -24
  70. package/dist/machine-setup.d.ts +190 -0
  71. package/dist/machine-setup.js +849 -0
  72. package/dist/machine-status.d.ts +67 -0
  73. package/dist/machine-status.js +140 -0
  74. package/dist/main.js +23 -13
  75. package/dist/move-worktrees-restore.py +144 -0
  76. package/dist/move-worktrees.d.ts +63 -0
  77. package/dist/move-worktrees.js +201 -0
  78. package/dist/move.d.ts +10 -1
  79. package/dist/move.js +116 -16
  80. package/dist/onboarding.d.ts +11 -7
  81. package/dist/onboarding.js +59 -27
  82. package/dist/prepared-repository.d.ts +202 -0
  83. package/dist/prepared-repository.js +860 -0
  84. package/dist/prepared-repository.py +1306 -0
  85. package/dist/project-preparation.d.ts +48 -0
  86. package/dist/project-preparation.js +304 -0
  87. package/dist/project-sync-manifest.d.ts +4 -0
  88. package/dist/project-sync-manifest.js +5 -1
  89. package/dist/project-sync-transfer.js +1 -1
  90. package/dist/project-sync.d.ts +19 -4
  91. package/dist/project-sync.js +31 -8
  92. package/dist/project-template-setup.d.ts +33 -1
  93. package/dist/project-template-setup.js +59 -2
  94. package/dist/secret-ask.d.ts +72 -0
  95. package/dist/secret-ask.js +351 -0
  96. package/dist/seed-phases.d.ts +13 -3
  97. package/dist/seed-phases.js +21 -3
  98. package/dist/setup-cache.d.ts +7 -0
  99. package/dist/setup-cache.js +32 -0
  100. package/dist/t3-launch.js +1 -4
  101. package/dist/t3-mode.d.ts +8 -2
  102. package/dist/t3-mode.js +19 -4
  103. package/dist/t3-phone.d.ts +4 -2
  104. package/dist/t3-phone.js +12 -4
  105. package/dist/terminal-apps.d.ts +157 -0
  106. package/dist/terminal-apps.js +561 -0
  107. package/dist/terminal-onboarding.d.ts +2 -0
  108. package/dist/terminal-onboarding.js +11 -1
  109. package/dist/this-computer.d.ts +11 -0
  110. package/dist/this-computer.js +36 -0
  111. package/dist/thread-on-computer.js +4 -1
  112. package/dist/update.js +11 -3
  113. package/dist/worktree-twin.d.ts +10 -2
  114. package/dist/worktree-twin.js +27 -9
  115. package/package.json +5 -5
@@ -54,13 +54,12 @@ export async function seedAgentRepository(ctx, ws, folder, options = {}) {
54
54
  const state = new AgentStateStore(ctx.store.dir);
55
55
  state.update(doc => { const seeds = (doc.git_seeding ?? {}); seeds[ws.id] = { api_url: ctx.apiUrl, folder: project.folder }; doc.git_seeding = seeds; });
56
56
  const cell = ws.cell({ agentLabel: ctx.agentLabel, transitionTimeoutMs: ctx.wakeTimeoutMs });
57
- await cell.files.mkdir(root, { parents: true });
58
57
  let initialReady = false;
59
58
  const initial = options.onInitialFiles ? { paths: new Set(options.initialFiles ?? []), ready: () => { if (!initialReady) {
60
59
  initialReady = true;
61
60
  options.onInitialFiles();
62
61
  } } } : undefined;
63
- await initializeProjectGit(cell, project.folder, root, ctx.signal, options.populate, retry, message => { ctx.io.stderr.write(`${message}\n`); }, initial);
62
+ await initializeProjectGit(cell, project.folder, root, ctx.signal, options.populate, retry, message => { ctx.io.stderr.write(`${message}\n`); }, initial, options.changes, options.background, options.beforeHistory);
64
63
  initial?.ready();
65
64
  state.update(doc => { const seeds = (doc.git_seeding ?? {}); delete seeds[ws.id]; doc.git_seeding = seeds; });
66
65
  }
@@ -80,6 +79,22 @@ async function optionalGit(folder, args, signal) {
80
79
  }
81
80
  }
82
81
  const tail = (text) => text.trim().split('\n').slice(-3).join(' ').slice(-400);
82
+ /** Another Git process in the VM holds this repository's index or config lock (the agent starting there, its hook). */
83
+ const LOCKED = /\.lock'?: File exists|could not lock config file/;
84
+ /**
85
+ * A Git step of the seed that writes the workspace repository's index, refs or config: run again, for up to ~5 s,
86
+ * while another Git process in the VM holds the lock (a conversation's agent starts as soon as its repository appears,
87
+ * and its own `git status` takes the index lock). Every step passed here is safe to repeat.
88
+ */
89
+ export async function runGitStep(cell, argv, options, attempts = 50) {
90
+ for (let attempt = 1;; attempt++) {
91
+ const result = await cell.exec.run(argv, options);
92
+ if (result.exitCode === 0 || attempt >= attempts || !LOCKED.test(result.stderr))
93
+ return result;
94
+ options.signal?.throwIfAborted();
95
+ await new Promise(resolve => setTimeout(resolve, 100));
96
+ }
97
+ }
83
98
  /** The repository's object store size, an upper bound of any one branch's history bundle. Reads sizes only. */
84
99
  export async function historyEstimate(folder, signal) {
85
100
  const counts = new Map((await optionalGit(folder, ['count-objects', '-v'], signal)).split('\n').map(line => line.split(': ')));
@@ -89,18 +104,37 @@ export async function historyEstimate(folder, signal) {
89
104
  // every commit and tree. Alongside it, a depth-1 fetch of the same ref brings the tip's files in one pack, moved in
90
105
  // beside the history, so the checkout reads them locally instead of asking for each blob. Prints the commit. A failed
91
106
  // history fetch leaves a plain repository, so the bundle fallback is not a partial clone of an origin it cannot reach.
107
+ // The VM's own prefetch of this worktree (a guest that starts one when its conversation waits for the seed, coding 53+;
108
+ // $HOME/.cache/shardflux/prefetch/<16 hex of sha256(worktree)>: history/ and tip/ bare repositories, remote, commit)
109
+ // is waited for and its packs adopted (0.21.1+): when its commit is $4 (this computer's upstream), nothing more is
110
+ // fetched; otherwise only what is newer, and the checkout fetches any missing tip files in one request.
92
111
  export const FETCH_ORIGIN = [
93
112
  'set -eu',
94
113
  'git init -q "$1"; git -C "$1" config remote.origin.url "$2"',
95
114
  'case "$3" in HEAD) dst=refs/shard-sync/origin ;; *) dst="refs/remotes/origin/${3#refs/heads/}" ;; esac',
96
- 'tip="$1/.git/shard-sync-tip"; rm -rf "$tip"; git init -q --bare "$tip"',
97
- 'git -C "$tip" -c core.hooksPath=/dev/null -c credential.interactive=false -c fetch.unpackLimit=1 fetch -q --no-tags --depth=1 "$2" "+$3:refs/tip" >/dev/null 2>&1 &',
98
- 'job=$!',
115
+ 'key=$(printf %s "$1" | sha256sum 2>/dev/null | cut -c1-16); pre=; have=',
116
+ 'case "$key" in [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]) pre="${HOME:-/home/user}/.cache/shardflux/prefetch/$key" ;; esac',
117
+ 'if [ -n "$pre" ] && { [ -e "$pre.lock" ] || [ -d "$pre" ]; }; then',
118
+ ' exec 8>"$pre.lock"; flock -w 30 8 || true',
119
+ ' if [ -d "$pre/history" ] && [ "$(cat "$pre/remote" 2>/dev/null)" = "$2" ] && have=$(cat "$pre/commit" 2>/dev/null) && [ -n "$have" ]; then',
120
+ ' for p in "$pre"/history/objects/pack/pack-* "$pre"/tip/objects/pack/pack-*; do if [ -f "$p" ]; then mv "$p" "$1/.git/objects/pack/"; fi; done',
121
+ ' git -C "$1" config remote.origin.promisor true; git -C "$1" config remote.origin.partialclonefilter blob:none',
122
+ ' git -C "$1" update-ref "$dst" "$have" || have=',
123
+ ' else have=; fi',
124
+ ' rm -rf "$pre" "$pre.lock"; exec 8>&-',
125
+ // No prefetch yet: one starting now would only fetch the same again (the guest's sweep removes this marker).
126
+ 'elif [ -n "$pre" ]; then mkdir -p "$pre" 2>/dev/null || true; fi',
127
+ 'if [ -n "$have" ] && [ "$have" = "${4:-}" ]; then git -C "$1" rev-parse --verify "$dst^{commit}"; exit 0; fi',
128
+ 'tip="$1/.git/shard-sync-tip"; rm -rf "$tip"; job=',
129
+ 'if [ -z "$have" ]; then git init -q --bare "$tip"',
130
+ ' git -C "$tip" -c core.hooksPath=/dev/null -c credential.interactive=false -c fetch.unpackLimit=1 fetch -q --no-tags --depth=1 "$2" "+$3:refs/tip" >/dev/null 2>&1 &',
131
+ ' job=$!; fi',
99
132
  'if ! git -C "$1" -c core.hooksPath=/dev/null -c credential.interactive=false fetch -q --no-tags --filter=blob:none origin "+$3:$dst" >&2; then',
100
- ' wait "$job" || true; rm -rf "$tip"',
133
+ ' if [ -n "$job" ]; then wait "$job" || true; fi; rm -rf "$tip"',
134
+ ' if [ -n "$have" ]; then git -C "$1" rev-parse --verify "$dst^{commit}"; exit 0; fi',
101
135
  ' git -C "$1" config --unset remote.origin.promisor || true; git -C "$1" config --unset remote.origin.partialclonefilter || true; exit 1',
102
136
  'fi',
103
- 'if wait "$job"; then for pack in "$tip"/objects/pack/pack-*.pack; do [ -f "$pack" ] || continue; base=${pack%.pack}',
137
+ 'if [ -n "$job" ] && wait "$job"; then for pack in "$tip"/objects/pack/pack-*.pack; do [ -f "$pack" ] || continue; base=${pack%.pack}',
104
138
  ' for ext in idx rev pack; do if [ -f "$base.$ext" ]; then mv "$base.$ext" "$1/.git/objects/pack/"; fi; done; done; fi',
105
139
  'rm -rf "$tip"',
106
140
  'git -C "$1" rev-parse --verify "$dst^{commit}"',
@@ -110,7 +144,8 @@ export const FETCH_ORIGIN = [
110
144
  export const BACKFILL = 'set -eu; cd "$1"; git -c core.hooksPath=/dev/null -c credential.interactive=false fetch -q --no-tags --no-write-fetch-head --refetch --no-filter origin "$2"; ' +
111
145
  'git config --unset remote.origin.partialclonefilter || true; git config --unset remote.origin.promisor || true';
112
146
  // Applies an optional bundle of HEAD (plus its branch and upstream) and points the workspace HEAD at the laptop's.
113
- const APPLY_HISTORY = 'set -eu; git init -q "$1"; if [ -n "$2" ]; then git -C "$1" -c core.hooksPath=/dev/null fetch -q --update-head-ok "$2" HEAD:refs/shard-sync/initial ' +
147
+ // $5: a shallow clone's boundary (its .git/shallow), written before the fetch so the bundle's cut history is accepted.
148
+ const APPLY_HISTORY = 'set -eu; git init -q "$1"; if [ -n "${5:-}" ]; then cp "$5" "$1/.git/shallow"; fi; if [ -n "$2" ]; then git -C "$1" -c core.hooksPath=/dev/null fetch -q --update-head-ok "$2" HEAD:refs/shard-sync/initial ' +
114
149
  '"+refs/heads/*:refs/heads/*" "+refs/tags/*:refs/tags/*" "+refs/remotes/*:refs/remotes/*"; fi; ' +
115
150
  'if [ -n "$4" ]; then git -C "$1" symbolic-ref HEAD "$4"; else git -C "$1" update-ref --no-deref HEAD "$3"; fi; git -C "$1" update-ref HEAD "$3"; git -C "$1" read-tree HEAD; ' +
116
151
  'for ref in refs/shard-sync/initial refs/shard-sync/origin; do if git -C "$1" rev-parse -q --verify "$ref" >/dev/null; then git -C "$1" update-ref -d "$ref"; fi; done';
@@ -131,7 +166,7 @@ if missing and git('config','--bool','--default','false','remote.origin.promisor
131
166
  absent={l[1:] for l in git('rev-list','--objects','--no-walk','--missing=print','HEAD').split(b'\n') if l.startswith(b'?')}
132
167
  want&=absent
133
168
  if want: git('-c','fetch.negotiationAlgorithm=noop','fetch','-q','--no-tags','--no-write-fetch-head','--recurse-submodules=no','--filter=blob:none','--stdin','origin',data=b'\n'.join(sorted(want))+b'\n')
134
- for at in range(0,len(missing),4096): git('checkout-index','-z','--stdin',data=b'\0'.join(missing[at:at+4096])+b'\0')
169
+ for at in range(0,len(missing),4096): git('-c','checkout.workers=0','checkout-index','-z','--stdin',data=b'\0'.join(missing[at:at+4096])+b'\0')
135
170
  print(len(missing))
136
171
  `;
137
172
  /** `git bundle create` streamed straight into the workspace in chunks: any size, no temporary file, no time limit. */
@@ -168,16 +203,19 @@ async function streamBundle(cell, folder, remote, revs, signal) {
168
203
  * from origin as a blobless partial clone (the workspace's own Git credentials), plus a bundle of the commits origin
169
204
  * lacks. Without a reachable origin the whole bundle is streamed in chunks: no history size refuses (0.19.0+).
170
205
  */
171
- export async function importProjectHistory(cell, folder, root, head, branch, origin, signal, originFirst = ORIGIN_FIRST_BYTES, report = () => undefined) {
206
+ export async function importProjectHistory(cell, folder, root, head, branch, origin, signal, originFirst = ORIGIN_FIRST_BYTES, report = () => undefined, configure) {
172
207
  const upstream = branch ? await optionalGit(folder, ['rev-parse', '--symbolic-full-name', `${branch.slice(11)}@{upstream}`], signal) : '';
173
208
  const tracked = upstream.startsWith('refs/remotes/') ? upstream : '';
174
209
  const tips = ['HEAD', ...(branch ? [branch] : []), ...(tracked ? [tracked] : [])];
175
210
  const remote = `/tmp/shard-sync-${randomUUID()}.bundle`;
211
+ let shallowFile = '', bundled = false;
176
212
  try {
177
213
  let revs = tips, partial = null;
178
214
  if (origin && await historyEstimate(folder, signal) > originFirst) {
179
215
  const ref = upstream.startsWith('refs/remotes/origin/') ? `refs/heads/${upstream.slice(20)}` : 'HEAD';
180
- const fetched = await cell.exec.run(['sh', '-c', FETCH_ORIGIN, 'shard-sync', root, origin, ref], { timeoutMs: 600_000, maxOutputBytes: 8192, env: { GIT_TERMINAL_PROMPT: '0' }, signal });
216
+ // This computer's view of that branch on origin: a prefetch at that commit needs no fetch at all.
217
+ const expected = ref !== 'HEAD' ? await optionalGit(folder, ['rev-parse', '--verify', `${upstream}^{commit}`], signal) : '';
218
+ const fetched = await cell.exec.run(['sh', '-c', FETCH_ORIGIN, 'shard-sync', root, origin, ref, expected], { timeoutMs: 600_000, maxOutputBytes: 8192, env: { GIT_TERMINAL_PROMPT: '0' }, signal });
181
219
  seedPhase('origin_fetch');
182
220
  const base = fetched.stdout.trim();
183
221
  if (fetched.exitCode === 0 && /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(base)) {
@@ -201,20 +239,75 @@ export async function importProjectHistory(cell, folder, root, head, branch, ori
201
239
  report(`Sending the history from this computer: the workspace could not fetch ${origin} (${tail(fetched.stderr)}).`);
202
240
  }
203
241
  if (revs) {
242
+ bundled = true;
204
243
  await streamBundle(cell, folder, remote, revs, signal);
205
244
  seedPhase('bundle');
206
245
  }
207
- const result = await cell.exec.run(['sh', '-c', APPLY_HISTORY, 'shard-sync', root, revs ? remote : '', head, branch], { timeoutMs: 600_000, maxOutputBytes: 4096, signal });
246
+ // A shallow clone (git clone --depth N) has no parents for its oldest commits: Git refuses its bundle ("did not send
247
+ // all necessary objects") unless the workspace repository has the same shallow boundary.
248
+ const boundary = revs ? await shallowBoundary(folder, signal) : null;
249
+ if (boundary) {
250
+ shallowFile = `${remote}.shallow`;
251
+ await cell.files.write(shallowFile, boundary, { mode: '0600' });
252
+ }
253
+ // With `configure`, the repository's config and exclude rules are set in the same request (0.21.1+).
254
+ const apply = ['sh', '-c', APPLY_HISTORY, 'shard-sync', root, revs ? remote : '', head, branch, shallowFile];
255
+ const result = configure
256
+ ? await runGitStep(cell, ['sh', '-c', APPLY_THEN_CONFIGURE, 'shard-sync', root, revs ? remote : '', head, branch, shallowFile, root, ...configure.settings], { stdin: configure.excludes, env: { SHARD_APPLY: APPLY_HISTORY, SHARD_CONFIGURE: CONFIGURE_GIT }, timeoutMs: 600_000, maxOutputBytes: 4096, ...(signal ? { signal } : {}) })
257
+ : await runGitStep(cell, apply, { timeoutMs: 600_000, maxOutputBytes: 4096, ...(signal ? { signal } : {}) });
208
258
  if (result.exitCode !== 0)
209
259
  throw new Error(`initial Git bundle failed: ${result.stderr.trim()}`);
210
260
  seedPhase('apply_history');
211
261
  return { partial };
212
262
  }
213
263
  finally {
214
- await cell.files.remove(remote).catch(() => undefined);
264
+ if (bundled)
265
+ await cell.files.remove(remote).catch(() => undefined);
266
+ if (shallowFile)
267
+ await cell.files.remove(shallowFile).catch(() => undefined);
268
+ }
269
+ }
270
+ /** A shallow clone's boundary commits (its shallow file), or null for a complete history. */
271
+ async function shallowBoundary(folder, signal) {
272
+ if (await optionalGit(folder, ['rev-parse', '--is-shallow-repository'], signal) !== 'true')
273
+ return null;
274
+ const file = await optionalGit(folder, ['rev-parse', '--git-path', 'shallow'], signal);
275
+ return file ? await readFile(resolve(folder, file)).catch(() => null) : null;
276
+ }
277
+ // APPLY_HISTORY ($1-$5), then CONFIGURE_GIT (the rest; the exclude rules on standard input), in one request.
278
+ const APPLY_THEN_CONFIGURE = 'set -eu; sh -c "$SHARD_APPLY" shard-sync "$1" "$2" "$3" "$4" "$5" </dev/null; shift 5; exec sh -c "$SHARD_CONFIGURE" shard-sync "$@"';
279
+ // Sets the workspace repository's Git config (key value pairs) and its exclude file (standard input) in one request.
280
+ const CONFIGURE_GIT = 'set -eu; umask 077; root=$1; shift; f="$root/.git/shard-sync-excludes"; cat > "$f"; git -C "$root" config core.excludesFile "$f"; ' +
281
+ 'while [ $# -ge 2 ]; do git -C "$root" config "$1" "$2"; shift 2; done';
282
+ /** Git's info/exclude and this computer's excludes file: selection metadata, never credentials or .git data. */
283
+ async function localExcludes(folder, signal) {
284
+ const readOptional = async (path) => {
285
+ try {
286
+ return await readFile(path, 'utf8');
287
+ }
288
+ catch (error) {
289
+ if (error.code === 'ENOENT')
290
+ return '';
291
+ throw error;
292
+ }
293
+ };
294
+ const info = (await projectGit(folder, ['rev-parse', '--git-path', 'info/exclude'], signal)).trim();
295
+ let global = '';
296
+ try {
297
+ global = (await projectGit(folder, ['config', '--path', '--get', 'core.excludesFile'], signal)).trim();
298
+ }
299
+ catch (error) {
300
+ if (signal?.aborted)
301
+ throw error;
215
302
  }
303
+ return await readOptional(resolve(folder, info)) + '\n' + await readOptional(global ? resolve(folder, global) : join(process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), 'git', 'ignore'));
304
+ }
305
+ async function configureWorkspaceGit(cell, root, settings, excludes, signal) {
306
+ const result = await runGitStep(cell, ['sh', '-c', CONFIGURE_GIT, 'shard-sync', root, ...settings], { stdin: excludes, timeoutMs: 10_000, maxOutputBytes: 4096, ...(signal ? { signal } : {}) });
307
+ if (result.exitCode !== 0)
308
+ throw new Error(`could not configure the workspace Git repository: ${tail(result.stderr)}`);
216
309
  }
217
- async function initializeProjectGit(cell, folder, root, signal, populate = false, retry = false, report = () => undefined, initial) {
310
+ async function initializeProjectGit(cell, folder, root, signal, populate = false, retry = false, report = () => undefined, initial, changes, background, beforeHistory) {
218
311
  let top;
219
312
  try {
220
313
  top = (await projectGit(folder, ['rev-parse', '--show-toplevel'], signal)).trim();
@@ -222,16 +315,20 @@ async function initializeProjectGit(cell, folder, root, signal, populate = false
222
315
  catch (error) {
223
316
  if (signal?.aborted || error.code !== 128)
224
317
  throw error;
318
+ await cell.files.mkdir(root, { parents: true });
225
319
  if (populate)
226
320
  await copyProjectFiles(cell, folder, root, await missingInWorkspace(cell, root, await selectedProjectPaths(folder, signal), signal), [], signal, report, initial, false);
227
321
  return;
228
322
  }
229
323
  if (top !== folder)
230
324
  throw new UsageError('project sync requires the Git repository root; run shard there');
231
- const existing = await cell.exec.run(['git', '-C', root, 'rev-parse', '--verify', 'HEAD'], { timeoutMs: 5000, signal });
325
+ // The workspace folder and whether it already has history, in one request.
326
+ const existing = await cell.exec.run(['sh', '-c', 'mkdir -p "$1" && exec git -C "$1" rev-parse --verify HEAD', 'shard-sync', root], { timeoutMs: 5000, signal });
232
327
  if (existing.exitCode !== 0 && existing.exitCode !== 128)
233
328
  throw new Error('could not check workspace Git history');
234
329
  if (existing.exitCode === 128 || retry) {
330
+ // What must be in the VM before its repository appears (a conversation's setup intent, read when its agent starts).
331
+ await beforeHistory;
235
332
  const importHistory = existing.exitCode === 128;
236
333
  let head = '', branch = '';
237
334
  try {
@@ -248,16 +345,9 @@ async function initializeProjectGit(cell, folder, root, signal, populate = false
248
345
  if (signal?.aborted)
249
346
  throw error;
250
347
  }
251
- let partial = null;
252
- if (head && importHistory) {
253
- const remote = projectIdentity(folder).remote;
254
- ({ partial } = await importProjectHistory(cell, folder, root, head, branch, remote && !remote.startsWith('file:') ? remote : null, signal, ORIGIN_FIRST_BYTES, report));
255
- }
256
- else if (importHistory) {
257
- const result = await cell.exec.run(['git', 'init', '-q', ...(branch.startsWith('refs/heads/') ? ['--initial-branch', branch.slice(11)] : []), root], { timeoutMs: 5000, signal });
258
- if (result.exitCode !== 0)
259
- throw new Error(`initial Git repository failed: ${result.stderr.trim()}`);
260
- }
348
+ // The author identity, origin, the branch's upstream (it keeps tracking its origin branch, as on this computer) and
349
+ // the exclude rules: set with the history's import, else in a request of their own.
350
+ const settings = [];
261
351
  for (const key of ['user.name', 'user.email']) {
262
352
  let value = '';
263
353
  try {
@@ -267,61 +357,50 @@ async function initializeProjectGit(cell, folder, root, signal, populate = false
267
357
  if (signal?.aborted)
268
358
  throw error;
269
359
  }
270
- if (value) {
271
- const result = await cell.exec.run(['git', '-C', root, 'config', key, value], { timeoutMs: 5000, signal });
272
- if (result.exitCode !== 0)
273
- throw new Error(`could not copy Git ${key}`);
274
- }
360
+ if (value)
361
+ settings.push(key, value);
275
362
  }
276
363
  const origin = projectIdentity(folder).remote;
277
364
  if (origin && !origin.startsWith('file:')) {
278
- const result = await cell.exec.run(['git', '-C', root, 'config', 'remote.origin.url', origin], { timeoutMs: 5000, signal });
279
- if (result.exitCode !== 0)
280
- throw new Error('could not copy Git origin');
281
- // The imported branch keeps tracking its origin branch, as on this computer.
365
+ settings.push('remote.origin.url', origin);
282
366
  const upstream = importHistory && branch ? await optionalGit(folder, ['rev-parse', '--symbolic-full-name', `${branch.slice(11)}@{upstream}`], signal) : '';
283
367
  if (upstream.startsWith('refs/remotes/origin/'))
284
- for (const [key, value] of [[`branch.${branch.slice(11)}.remote`, 'origin'], [`branch.${branch.slice(11)}.merge`, `refs/heads/${upstream.slice(20)}`]]) {
285
- const set = await cell.exec.run(['git', '-C', root, 'config', key, value], { timeoutMs: 5000, signal });
286
- if (set.exitCode !== 0)
287
- throw new Error('could not copy Git upstream');
288
- }
368
+ settings.push(`branch.${branch.slice(11)}.remote`, 'origin', `branch.${branch.slice(11)}.merge`, `refs/heads/${upstream.slice(20)}`);
369
+ }
370
+ const excludes = await localExcludes(folder, signal);
371
+ let partial = null;
372
+ if (head && importHistory) {
373
+ const remote = projectIdentity(folder).remote;
374
+ ({ partial } = await importProjectHistory(cell, folder, root, head, branch, remote && !remote.startsWith('file:') ? remote : null, signal, ORIGIN_FIRST_BYTES, report, { settings, excludes }));
289
375
  }
376
+ else {
377
+ if (importHistory) {
378
+ const result = await cell.exec.run(['git', 'init', '-q', ...(branch.startsWith('refs/heads/') ? ['--initial-branch', branch.slice(11)] : []), root], { timeoutMs: 5000, signal });
379
+ if (result.exitCode !== 0)
380
+ throw new Error(`initial Git repository failed: ${result.stderr.trim()}`);
381
+ }
382
+ await configureWorkspaceGit(cell, root, settings, excludes, signal);
383
+ }
384
+ seedPhase('git_config');
290
385
  if (head)
291
- await seedFromHead(cell, folder, root, populate, signal, report, initial);
386
+ await seedFromHead(cell, folder, root, populate, signal, report, initial, changes);
292
387
  else
293
388
  await seedUnborn(cell, folder, root, populate, signal, report, initial);
294
389
  // Optional work, after the start's own: the lowest priority, and a failure only leaves the clone partial.
295
- if (partial)
296
- await cell.exec.start({ argv: ['nice', '-n', '19', 'sh', '-c', BACKFILL, 'shard-backfill', root, partial], env: { GIT_TERMINAL_PROMPT: '0' }, timeout_ms: 3_600_000 }, signal).catch(() => undefined);
297
- }
298
- // Git's info/exclude and laptop-wide excludes are selection metadata, copied
299
- // into a dedicated Git exclude file without syncing credentials or .git data.
300
- const readOptional = async (path) => {
301
- try {
302
- return await readFile(path, 'utf8');
303
- }
304
- catch (error) {
305
- if (error.code === 'ENOENT')
306
- return '';
307
- throw error;
390
+ // A caller that collects background work (a conversation start) gets the request instead of waiting for it.
391
+ if (partial) {
392
+ const started = cell.exec.start({ argv: ['nice', '-n', '19', 'sh', '-c', BACKFILL, 'shard-backfill', root, partial], env: { GIT_TERMINAL_PROMPT: '0' }, timeout_ms: 3_600_000 }, signal).catch(() => undefined);
393
+ if (background)
394
+ background(started);
395
+ else
396
+ await started;
308
397
  }
309
- };
310
- const info = (await projectGit(folder, ['rev-parse', '--git-path', 'info/exclude'], signal)).trim();
311
- let global = '';
312
- try {
313
- global = (await projectGit(folder, ['config', '--path', '--get', 'core.excludesFile'], signal)).trim();
314
398
  }
315
- catch (error) {
316
- if (signal?.aborted)
317
- throw error;
399
+ else {
400
+ // An existing repository keeps its config; its exclude rules follow this computer's.
401
+ await configureWorkspaceGit(cell, root, [], await localExcludes(folder, signal), signal);
402
+ seedPhase('git_config');
318
403
  }
319
- const excludes = await readOptional(resolve(folder, info)) + '\n' + await readOptional(global ? resolve(folder, global) : join(process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), 'git', 'ignore'));
320
- await cell.files.write(`${root}/.git/shard-sync-excludes`, excludes, { mode: '0600' });
321
- const result = await cell.exec.run(['git', '-C', root, 'config', 'core.excludesFile', `${root}/.git/shard-sync-excludes`], { timeoutMs: 5000, signal });
322
- if (result.exitCode !== 0)
323
- throw new Error('could not apply project Git exclude rules');
324
- seedPhase('excludes');
325
404
  }
326
405
  const nul = (text) => text.split('\0').filter(Boolean);
327
406
  /** Git's NUL-separated output of any size, streamed (no output buffer or time bound). */
@@ -330,11 +409,6 @@ async function gitList(folder, args, signal) {
330
409
  await projectGitPaths(folder, args, path => { out.push(path); }, { signal });
331
410
  return out;
332
411
  }
333
- /**
334
- * What differs between HEAD and this computer's working tree, from Git's own status (0.19.0+): changed, added and
335
- * untracked files Git does not ignore, the .env files we always carry, Git LFS files (their content lives outside Git),
336
- * and deletions. Ignored files, nested repositories and submodule contents stay out.
337
- */
338
412
  export async function localChanges(folder, signal) {
339
413
  const [fields, untracked, env, lfs] = await Promise.all([
340
414
  gitList(folder, ['diff', '--name-status', '-z', '--no-renames', '--ignore-submodules=all', 'HEAD', '--'], signal),
@@ -374,7 +448,7 @@ export async function localChanges(folder, signal) {
374
448
  return { copy, remove, skipped };
375
449
  }
376
450
  /** HEAD's files come from the workspace's own Git; then only this computer's changes are copied. */
377
- async function seedFromHead(cell, folder, root, populate, signal, report, initial) {
451
+ async function seedFromHead(cell, folder, root, populate, signal, report, initial, precomputed) {
378
452
  if (!populate) {
379
453
  // Ignore files must exist before the first remote scan; otherwise a template's dependency outputs would look like
380
454
  // agent-created source.
@@ -382,7 +456,8 @@ async function seedFromHead(cell, folder, root, populate, signal, report, initia
382
456
  await copyProjectFiles(cell, folder, root, await missingInWorkspace(cell, root, ignores, signal), [], signal, report, undefined, false);
383
457
  return;
384
458
  }
385
- const changes = await localChanges(folder, signal);
459
+ // A conversation start reads them while its VM starts (0.21.1+).
460
+ const changes = await (precomputed ?? localChanges(folder, signal));
386
461
  seedPhase('local_changes');
387
462
  const skip = changes.remove.length ? `/tmp/shard-sync-${randomUUID()}.skip` : '';
388
463
  try {
@@ -5,25 +5,36 @@
5
5
  */
6
6
  export declare const AGENT_SHELL_VARIABLES: readonly ["CLAUDECODE", "CODEX_SANDBOX", "CODEX_THREAD_ID", "CURSOR_AGENT", "GEMINI_CLI"];
7
7
  export declare function codingAgentShell(env: Record<string, string | undefined>): boolean;
8
- export declare const SETUP_QUESTION = "Is Shardflux for a SaaS platform (your product runs VMs for its own customers through the API, SDK or MCP), or for your personal setup (your own cloud coding agents: each Claude or Codex conversation gets its own VM)?";
8
+ /**
9
+ * How the agent runs the setup (0.20.1+): it does the work, the person only decides. Onboarding testers (2026-10-12)
10
+ * found agents handing them commands, links and codes, writing paragraphs and asking open questions.
11
+ */
12
+ export declare const SETUP_RULES: readonly ["Do every permitted step yourself: run each command and open each link. Never ask your user to paste a secret or sign-in code into the chat.", "Your user answers your questions, agrees to the Terms, approves sign-ins in the browser tabs shard opens, and types secrets into the page shard opens (you never see them).", "If your permission system or auto-mode classifier refuses an action, stop that action. Do not change permissions, wrap it in a script or use another tool to bypass the refusal. Explain the reason and give your user the exact refused command to run in their own terminal; continue independent permitted steps.", "After a refused action, ask for only its non-secret result and resume permitted steps when your user confirms it. Never ask for credentials, credential files or raw logs. Plugin installation needs confirmation that the plugin is installed; machine opening needs its workspace key, not local files or environment variables.", "To check saved account state without printing credentials, use shard setup --guide --json: guide.signed_in.email and guide.api_key_saved. It is a local check, not proof that a key works. If reading setup output or checking state is refused, have your user run this command and confirm those fields; do not read redirected output or credential files instead.", "A command exiting successfully is not proof of setup. Check its non-secret result and the requested behavior; keep any blocked or unobserved step unverified.", "Ask one question at a time as multiple choice, recommended answer first (use your question tool if you have one).", "Keep each message to one or two short sentences: what you are doing or what you need, never how it works.", "Commands that wait for your user in the browser (sign-ins, --ask) take up to 10 minutes: run them with a long timeout or in the background, and tell your user once what to do there."];
13
+ export declare const SETUP_QUESTION = "What are you setting up Shardflux for?";
14
+ /** The answers to SETUP_QUESTION, the usual one first. */
15
+ export declare const SETUP_CHOICES: readonly ["Your own cloud coding agents: each Claude or Codex conversation gets its own VM", "Your product: it runs VMs for its customers through the API, SDK or MCP"];
9
16
  export declare const AGENT_SETUP_DOCS = "https://docs.shardflux.dev/guides/agent-setup.md";
10
- /** One step of a path: what it does, what to ask the user first (if anything), and the commands in order. */
17
+ /** One step of a path: what it does, the one question to ask first (if any) with its answers, and the commands. */
11
18
  export interface GuideStep {
12
19
  step: string;
13
20
  ask?: string;
21
+ /** Multiple-choice answers to `ask`, the recommended one first. */
22
+ choices?: string[];
14
23
  commands: string[];
15
24
  note?: string;
16
25
  }
17
26
  /** What `shard setup --guide --json` prints. */
18
27
  export interface SetupGuide {
28
+ rules: string[];
19
29
  question: string;
30
+ choices: string[];
20
31
  signed_in: {
21
32
  email: string | null;
22
33
  } | null;
23
34
  api_key_saved: boolean;
24
35
  account: GuideStep;
25
- saas: GuideStep[];
26
36
  personal: GuideStep[];
37
+ saas: GuideStep[];
27
38
  terms_url: string;
28
39
  privacy_url: string;
29
40
  docs: string;
@@ -1,13 +1,16 @@
1
1
  /**
2
- * Setting Shardflux up from a coding agent (0.17.1+): the one question the agent asks its user first, and the ordered
3
- * steps of both answers. `shard setup --guide` prints it; so does a bare `shard` without a terminal or in a coding
4
- * agent's shell, where the command center would wait for keys nobody presses. Local only: no network call and nothing
5
- * to answer, so it returns at once.
2
+ * Setting Shardflux up from a coding agent (0.17.1+): how the agent runs it, the one question it asks first, and the
3
+ * ordered steps of both answers. `shard setup --guide` prints it; so does a bare `shard` without a terminal or in a
4
+ * coding agent's shell, where the command center would wait for keys nobody presses. Local only: no network call and
5
+ * nothing to answer, so it returns at once.
6
6
  *
7
- * - SaaS platform: the user's product runs VMs for its own customers (API, SDK, MCP). An account, an API key, the
7
+ * - Own cloud coding agents (the usual answer): sign in, connect Claude and Codex, set the machine up from this
8
+ * computer, choose where conversations open, set up the project and open the first conversation (0.20.1+).
9
+ * - A product (SaaS platform) running VMs for its own customers (API, SDK, MCP): an account, an API key, the
8
10
  * integration and templates for the customers' workspaces; no agent sign-ins, menu bar or machine.
9
- * - Personal setup: the user's own cloud coding agents. Sign in, connect Claude and Codex, build the machine from what
10
- * the user asks for, set up the project and hand over `shard ./`.
11
+ *
12
+ * The agent does permitted steps; the person answers multiple-choice questions, approves browser sign-ins and types
13
+ * secrets into the page shard opens. A real permission refusal requires a precise user handoff (SETUP_RULES).
11
14
  */
12
15
  import { legalFields } from "./legal.js";
13
16
  /**
@@ -19,7 +22,27 @@ export const AGENT_SHELL_VARIABLES = ['CLAUDECODE', 'CODEX_SANDBOX', 'CODEX_THRE
19
22
  export function codingAgentShell(env) {
20
23
  return AGENT_SHELL_VARIABLES.some((name) => (env[name]?.trim() ?? '') !== '');
21
24
  }
22
- export const SETUP_QUESTION = 'Is Shardflux for a SaaS platform (your product runs VMs for its own customers through the API, SDK or MCP), or for your personal setup (your own cloud coding agents: each Claude or Codex conversation gets its own VM)?';
25
+ /**
26
+ * How the agent runs the setup (0.20.1+): it does the work, the person only decides. Onboarding testers (2026-10-12)
27
+ * found agents handing them commands, links and codes, writing paragraphs and asking open questions.
28
+ */
29
+ export const SETUP_RULES = [
30
+ 'Do every permitted step yourself: run each command and open each link. Never ask your user to paste a secret or sign-in code into the chat.',
31
+ 'Your user answers your questions, agrees to the Terms, approves sign-ins in the browser tabs shard opens, and types secrets into the page shard opens (you never see them).',
32
+ 'If your permission system or auto-mode classifier refuses an action, stop that action. Do not change permissions, wrap it in a script or use another tool to bypass the refusal. Explain the reason and give your user the exact refused command to run in their own terminal; continue independent permitted steps.',
33
+ 'After a refused action, ask for only its non-secret result and resume permitted steps when your user confirms it. Never ask for credentials, credential files or raw logs. Plugin installation needs confirmation that the plugin is installed; machine opening needs its workspace key, not local files or environment variables.',
34
+ 'To check saved account state without printing credentials, use shard setup --guide --json: guide.signed_in.email and guide.api_key_saved. It is a local check, not proof that a key works. If reading setup output or checking state is refused, have your user run this command and confirm those fields; do not read redirected output or credential files instead.',
35
+ 'A command exiting successfully is not proof of setup. Check its non-secret result and the requested behavior; keep any blocked or unobserved step unverified.',
36
+ 'Ask one question at a time as multiple choice, recommended answer first (use your question tool if you have one).',
37
+ 'Keep each message to one or two short sentences: what you are doing or what you need, never how it works.',
38
+ 'Commands that wait for your user in the browser (sign-ins, --ask) take up to 10 minutes: run them with a long timeout or in the background, and tell your user once what to do there.',
39
+ ];
40
+ export const SETUP_QUESTION = 'What are you setting up Shardflux for?';
41
+ /** The answers to SETUP_QUESTION, the usual one first. */
42
+ export const SETUP_CHOICES = [
43
+ 'Your own cloud coding agents: each Claude or Codex conversation gets its own VM',
44
+ 'Your product: it runs VMs for its customers through the API, SDK or MCP',
45
+ ];
23
46
  export const AGENT_SETUP_DOCS = 'https://docs.shardflux.dev/guides/agent-setup.md';
24
47
  const { terms_url: TERMS, privacy_url: PRIVACY } = legalFields();
25
48
  export function setupGuide(state) {
@@ -27,46 +50,61 @@ export function setupGuide(state) {
27
50
  ? { step: 'Account', commands: ['shard setup --json'], note: `Signed in${state.email ? ` as ${state.email}` : ''}. shard setup saves the organization, project and API key (safe to run again).` }
28
51
  : {
29
52
  step: 'Account',
30
- ask: `Do you agree to the Terms of Service (${TERMS}) and the Privacy Policy (${PRIVACY})? Continue once they do.`,
53
+ ask: `Do you agree to the Terms of Service (${TERMS}) and the Privacy Policy (${PRIVACY})?`,
54
+ choices: ['Yes, I agree', 'No'],
31
55
  commands: ['shard signup --accept-terms --json'],
32
- note: 'It signs up or signs in with this computer’s Codex login and saves an API key. Without a Codex login, or for an account they already have: shard auth login --browser --no-wait --json (give your user the link and the code), then shard auth login --browser --json (returns once they approved) and shard setup --json.',
56
+ note: 'Signs up or signs in with this computer’s Codex login. Without one, or for an existing account: shard auth login --browser --json opens the sign-in page; tell your user to approve it there (if your command times out, run it again: it waits for the same request), then shard setup --json.',
33
57
  };
34
58
  return {
59
+ rules: [...SETUP_RULES],
35
60
  question: SETUP_QUESTION,
61
+ choices: [...SETUP_CHOICES],
36
62
  signed_in: state.signedIn ? { email: state.email } : null,
37
63
  api_key_saved: state.apiKey,
38
64
  account,
39
- saas: [
40
- { step: 'API key for your product', commands: ['eval "$(shard env)"'], note: 'Exports SHARDFLUX_API_KEY for the SDKs, the MCP server and your product’s server. Keep it in server-side secrets, never in client code.' },
41
- {
42
- step: 'Integrate',
43
- commands: ['npm install @shardflux/sdk', 'pip install shardflux'],
44
- note: 'Or the HTTP API (https://api.shardflux.dev/v1) or the MCP server (https://mcp.shardflux.dev/mcp). Your customers’ workspaces start from a template: shard templates build template.yaml. Start at https://docs.shardflux.dev/quickstart.md',
45
- },
46
- ],
47
65
  personal: [
48
66
  {
49
67
  step: 'Connect Claude and Codex',
50
- ask: 'Which agents do you use: Claude, Codex or both?',
51
- commands: ['shard setup --inspect --connect --json --apps claude,codex'],
52
- note: 'Claude: your user runs shard agents credentials login claude in their own terminal, once (a claude.ai link and a code). Codex: codex login on this computer; --connect copies it and starts the account’s Codex remote device. Pass only the apps they use.',
68
+ ask: 'Which coding agents do you use?',
69
+ choices: ['Claude Code and Codex', 'Claude Code', 'Codex'],
70
+ commands: ['shard setup --inspect --json', 'shard agents credentials login claude --json', 'shard setup --inspect --connect --json --apps claude,codex'],
71
+ note: 'Recommend what setup --inspect finds on this computer. The Claude sign-in opens claude.ai and a page for its code in your user’s browser. Codex uses this computer’s Codex login: run codex login yourself if it is missing. Pass only the apps they use.',
72
+ },
73
+ {
74
+ step: 'Set up the machine',
75
+ ask: 'Set up your cloud machine from this computer? It copies your tools, runtimes, shell, git identity and dotfiles.',
76
+ choices: ['Yes, copy this computer', 'Not now'],
77
+ commands: ['shard machine setup --from-this-computer --json', 'shard machine setup --status --json'],
78
+ note: 'This computer’s Claude Code or Codex does it read-only and saves the machine, in up to 10 minutes: tell your user once, then wait. Their tools carry over once machine.state in --status is ready; when it is failed, tell them its line and run its machine.action.command. Anything else they want on it: shard machine open --json, shard ws exec <key> -- bash -lc \'<install>\', shard files write <key> <path> --ask for a secret file (your user types it in the page), then shard machine save --wait --json.',
53
79
  },
54
80
  {
55
- step: 'Build the machine',
56
- ask: 'What should every cloud agent start with? Languages and runtimes, CLIs and tools, dotfiles and shell, secrets and credentials, editor and agent config.',
57
- commands: ['shard machine open --json', "shard ws exec <key> -- bash -lc '<install commands>'", 'shard files write <key> <path> --from -', 'shard machine save --wait --json'],
58
- note: 'machine open prints the machine VM’s workspace key. Install with ws exec, write dotfiles and secret files with files write (contents on standard input, never in arguments), then save: every new conversation starts from it.',
81
+ step: 'Choose where conversations open',
82
+ ask: 'Where should your conversations open?',
83
+ choices: ['The terminal you use now', 'Claude app', 'Codex app', 'T3 Code'],
84
+ commands: ['shard agents targets --json', 'shard agents targets --open-in <target> --terminal <app> --json'],
85
+ note: 'Offer only what targets --json lists as installed, the terminal shard runs in first (for example Ghostty, iTerm2, Warp).',
59
86
  },
60
87
  {
61
88
  step: 'Set up the project',
62
- ask: 'Which repository, which project secrets, and what does a good first run look like (tests, a dev server)?',
63
- commands: ['shard ./ --json --prompt "Set up this project and run its tests"', 'shard conversations wait <conversation-id> --timeout 90', 'shard template --json'],
64
- note: 'Run it in the project folder (shard ./ codex … for Codex): the conversation gets its own VM, and setup installs the dependencies and saves the project template. Run the wait again while it says Still working. Project secrets: shard secrets import --from <hsec|1password|bitwarden|keychain> NAME, or printf %s "$VALUE" | shard secrets create NAME.',
89
+ ask: 'Does this project need secrets (API keys, tokens)?',
90
+ choices: ['No', 'Yes, I’ll type them in a secure page', 'Yes, from my vault (1Password, Bitwarden, Keychain, hsec)'],
91
+ commands: ['shard secrets create <NAME> --ask --json', 'shard secrets import --from <vault> <item> --as <NAME> --json', 'shard ./ --json --prompt "Set up this project and run its tests"', 'shard conversations wait <conversation-id> --timeout 90', 'shard template --json'],
92
+ note: 'secrets create --ask opens a page where your user types the value; you only see that it was saved. Run shard ./ in the project folder (shard ./ codex … for Codex): its VM installs the dependencies and saves the project template. Run the wait again while it says Still working.',
65
93
  },
66
94
  {
67
- step: 'Hand over',
95
+ step: 'Open the first conversation',
68
96
  commands: ['shard ./'],
69
- note: 'Your user runs it in their own terminal (shard ./ codex for Codex). Claude conversations continue on claude.ai and in the Claude app; Codex threads in the Codex app, on the web and on the phone.',
97
+ note: 'It opens in the app your user chose. Tell them, in one sentence, that shard ./ in any project folder starts the next one.',
98
+ },
99
+ ],
100
+ saas: [
101
+ { step: 'API key for your product', commands: ['eval "$(shard env)"'], note: 'Exports SHARDFLUX_API_KEY for the SDKs, the MCP server and your product’s server. Keep it in server-side secrets, never in client code.' },
102
+ {
103
+ step: 'Integrate',
104
+ ask: 'How will your product call Shardflux?',
105
+ choices: ['TypeScript SDK', 'Python SDK', 'HTTP API', 'MCP server'],
106
+ commands: ['npm install @shardflux/sdk', 'pip install shardflux'],
107
+ note: 'Or the HTTP API (https://api.shardflux.dev/v1) or the MCP server (https://mcp.shardflux.dev/mcp). Your customers’ workspaces start from a template: shard templates build template.yaml. Start at https://docs.shardflux.dev/quickstart.md',
70
108
  },
71
109
  ],
72
110
  terms_url: TERMS,
@@ -94,23 +132,27 @@ function stepText(n, s) {
94
132
  const head = `${n}. ${s.step}.${s.ask ? ` Ask: ${s.ask}` : ''}`;
95
133
  return [
96
134
  ` ${wrap(head, ' ')}`,
135
+ ...(s.choices ? [` ${wrap(`Choices: ${s.choices.join(' | ')}`, ' ', 96)}`] : []),
97
136
  ...s.commands.map((c) => ` ${c}`),
98
137
  ...(s.note ? [` ${wrap(s.note, ' ', 96)}`] : []),
99
138
  ].join('\n');
100
139
  }
101
140
  export function setupGuideText(g) {
102
141
  return [
103
- 'Set up Shardflux. Ask your user one question first:',
104
- ` ${wrap(g.question, ' ')}`,
142
+ 'Set up Shardflux for your user. How:',
143
+ ...g.rules.map((r) => ` - ${wrap(r, ' ', 98)}`),
105
144
  '',
106
- stepText(1, g.account),
145
+ 'First ask, as multiple choice:',
146
+ ` ${wrap(`${g.question} Choices: ${g.choices.join(' | ')}`, ' ')}`,
107
147
  '',
108
- 'SaaS platform:',
109
- ...g.saas.map((s, i) => stepText(i + 2, s)),
148
+ stepText(1, g.account),
110
149
  '',
111
- 'Personal setup:',
150
+ 'Own cloud coding agents:',
112
151
  ...g.personal.map((s, i) => stepText(i + 2, s)),
113
152
  '',
153
+ 'Product (SaaS platform):',
154
+ ...g.saas.map((s, i) => stepText(i + 2, s)),
155
+ '',
114
156
  `Every step in detail: ${g.docs}. Every command: shard --help.`,
115
157
  ].join('\n') + '\n';
116
158
  }