@shardflux/cli 0.20.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +312 -1
- package/README.md +242 -75
- package/dist/account-args.js +7 -2
- package/dist/account-commands.js +56 -106
- package/dist/account-pool.d.ts +19 -15
- package/dist/account-pool.js +47 -96
- package/dist/account-scope.d.ts +65 -0
- package/dist/account-scope.js +246 -0
- package/dist/acp-journal.js +58 -6
- package/dist/acp-login.d.ts +13 -5
- package/dist/acp-login.js +37 -8
- package/dist/acp-models.d.ts +6 -1
- package/dist/acp-models.js +29 -19
- package/dist/acp.js +120 -14
- package/dist/agent-commands.d.ts +4 -1
- package/dist/agent-commands.js +110 -42
- package/dist/agent-config-return.d.ts +46 -2
- package/dist/agent-config-return.js +110 -4
- package/dist/agent-credentials.js +7 -3
- package/dist/agent-repository.d.ts +16 -3
- package/dist/agent-repository.js +149 -74
- package/dist/agent-setup.d.ts +14 -3
- package/dist/agent-setup.js +78 -36
- package/dist/agent-target-commands.d.ts +20 -1
- package/dist/agent-target-commands.js +73 -31
- package/dist/agent-targets.js +6 -1
- package/dist/args.d.ts +1 -1
- package/dist/args.js +95 -54
- package/dist/bin.js +5 -1
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -14
- package/dist/claude-cloud-login.d.ts +24 -2
- package/dist/claude-cloud-login.js +151 -11
- package/dist/cli-proxy-hub.d.ts +61 -25
- package/dist/cli-proxy-hub.js +202 -54
- package/dist/cli-proxy.d.ts +44 -1
- package/dist/cli-proxy.js +177 -28
- package/dist/client-settings.d.ts +39 -0
- package/dist/client-settings.js +91 -0
- package/dist/codex-cloud.d.ts +14 -4
- package/dist/codex-cloud.js +135 -31
- package/dist/codex-linux.d.ts +14 -0
- package/dist/codex-linux.js +69 -0
- package/dist/command-center-actions.js +18 -5
- package/dist/command-center-model.d.ts +2 -0
- package/dist/command-center-model.js +6 -1
- package/dist/command-center.js +3 -3
- package/dist/commands.js +173 -24
- package/dist/conversation-commands.d.ts +5 -0
- package/dist/conversation-commands.js +8 -5
- package/dist/conversation-terminal.d.ts +10 -2
- package/dist/conversation-terminal.js +78 -13
- package/dist/conversations.d.ts +86 -1
- package/dist/conversations.js +306 -23
- package/dist/desktop-linux.js +16 -3
- package/dist/device-bridge.d.ts +1 -1
- package/dist/device-bridge.js +6 -4
- package/dist/device-helper.js +43 -22
- package/dist/errors.d.ts +29 -2
- package/dist/errors.js +50 -7
- package/dist/filesystem-commands.d.ts +16 -9
- package/dist/filesystem-commands.js +133 -57
- package/dist/format.d.ts +2 -1
- package/dist/format.js +10 -1
- package/dist/handlers.js +3 -1
- package/dist/initial-prompt.d.ts +8 -0
- package/dist/initial-prompt.js +43 -1
- package/dist/large-media.d.ts +14 -0
- package/dist/large-media.js +100 -0
- package/dist/linux-terminal.d.ts +5 -3
- package/dist/linux-terminal.js +6 -20
- package/dist/local-sessions.d.ts +26 -2
- package/dist/local-sessions.js +113 -24
- package/dist/machine-setup.d.ts +11 -1
- package/dist/machine-setup.js +69 -22
- package/dist/machine-status.d.ts +67 -0
- package/dist/machine-status.js +140 -0
- package/dist/main.js +11 -13
- package/dist/menubar.d.ts +6 -5
- package/dist/menubar.js +38 -28
- package/dist/move-worktrees-restore.py +144 -0
- package/dist/move-worktrees.d.ts +69 -0
- package/dist/move-worktrees.js +252 -0
- package/dist/move.d.ts +10 -1
- package/dist/move.js +111 -16
- package/dist/onboarding.d.ts +11 -7
- package/dist/onboarding.js +63 -29
- package/dist/prepared-repository.d.ts +204 -0
- package/dist/prepared-repository.js +860 -0
- package/dist/prepared-repository.py +1307 -0
- package/dist/project-preparation.d.ts +62 -0
- package/dist/project-preparation.js +316 -0
- package/dist/project-sync-manifest.d.ts +4 -0
- package/dist/project-sync-manifest.js +5 -1
- package/dist/project-sync-transfer.js +1 -1
- package/dist/project-sync.d.ts +22 -4
- package/dist/project-sync.js +34 -8
- package/dist/project-template-setup.d.ts +33 -1
- package/dist/project-template-setup.js +59 -2
- package/dist/secret-ask.d.ts +72 -0
- package/dist/secret-ask.js +351 -0
- package/dist/seed-phases.d.ts +8 -1
- package/dist/seed-phases.js +17 -2
- package/dist/setup-cache.d.ts +7 -0
- package/dist/setup-cache.js +32 -0
- package/dist/t3-launch.d.ts +2 -1
- package/dist/t3-launch.js +12 -6
- package/dist/t3-mode.d.ts +8 -2
- package/dist/t3-mode.js +19 -4
- package/dist/t3-phone.d.ts +4 -2
- package/dist/t3-phone.js +12 -4
- package/dist/t3-proxy.d.ts +81 -0
- package/dist/t3-proxy.js +325 -0
- package/dist/terminal-apps.d.ts +168 -0
- package/dist/terminal-apps.js +574 -0
- package/dist/terminal-onboarding.js +6 -1
- package/dist/this-computer.d.ts +11 -0
- package/dist/this-computer.js +36 -0
- package/dist/thread-on-computer.js +4 -1
- package/dist/update.d.ts +2 -0
- package/dist/update.js +43 -9
- package/dist/worktree-twin.d.ts +10 -2
- package/dist/worktree-twin.js +27 -9
- package/package.json +5 -5
package/dist/move.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Ctx, Handler } from './commands.js';
|
|
|
3
3
|
import type { Values } from './args.js';
|
|
4
4
|
import type { Conversation } from './conversations.js';
|
|
5
5
|
import type { LocalSession, SessionHarness, SessionState } from './local-sessions.js';
|
|
6
|
+
import type { WorktreeMigration } from './move-worktrees.js';
|
|
6
7
|
type Env = Record<string, string | undefined>;
|
|
7
8
|
/** The project a folder belongs to: its Git repository's top, or the folder itself. */
|
|
8
9
|
export declare function projectRoot(folder: string): string;
|
|
@@ -15,10 +16,17 @@ type MoveCell = Pick<ReturnType<Workspace['cell']>, 'exec' | 'files'>;
|
|
|
15
16
|
* project space is mounted, as the conversation's durable copy with the same checkpoint metadata the guest writes
|
|
16
17
|
* (transcripts/<id>/meta.json), so the session survives the VM from its first minute.
|
|
17
18
|
*/
|
|
18
|
-
export declare function placeTranscript(cell: MoveCell, c: Pick<Conversation, 'id' | 'harness' | 'worktree_path'>, s: LocalSession, bytes: Buffer, folder: string, signal?: AbortSignal): Promise<{
|
|
19
|
+
export declare function placeTranscript(cell: MoveCell, c: Pick<Conversation, 'id' | 'harness' | 'worktree_path'>, s: LocalSession, bytes: Buffer, folder: string, signal?: AbortSignal, worktrees?: WorktreeMigration): Promise<{
|
|
19
20
|
native: string;
|
|
20
21
|
durable: boolean;
|
|
21
22
|
}>;
|
|
23
|
+
/** Every working directory the whole transcript names in its record metadata, never in message or tool content. */
|
|
24
|
+
export declare function transcriptCwds(bytes: Buffer): string[];
|
|
25
|
+
/**
|
|
26
|
+
* The project a move goes to: --path when the session's records confirm it worked there (placeTranscript checks), the
|
|
27
|
+
* current folder's project when it worked there, else the project of where it works now.
|
|
28
|
+
*/
|
|
29
|
+
export declare function moveFolder(s: Pick<LocalSession, 'cwd' | 'cwds'>, here: string, explicit: boolean): string;
|
|
22
30
|
export declare function moveSession(ctx: Ctx, values: Values): Promise<number>;
|
|
23
31
|
/** `shard move --list`: this project's sessions on this computer, and where each moved one continues. */
|
|
24
32
|
export declare function listMovable(ctx: Pick<Ctx, 'store' | 'apiUrl'>, values: Values, env: Env, now?: number): {
|
|
@@ -30,6 +38,7 @@ export declare function listMovable(ctx: Pick<Ctx, 'store' | 'apiUrl'>, values:
|
|
|
30
38
|
updated_at: string;
|
|
31
39
|
source: string;
|
|
32
40
|
cwd: string;
|
|
41
|
+
cwds: string[];
|
|
33
42
|
running: boolean;
|
|
34
43
|
turn: "working" | "idle";
|
|
35
44
|
moved: {
|
package/dist/move.js
CHANGED
|
@@ -31,8 +31,9 @@ import { openInChoices } from "./t3-mode.js";
|
|
|
31
31
|
import { openClaudeDesktopSession, remoteControlUrl } from "./agent-target-commands.js";
|
|
32
32
|
import { openUrl } from "./browser.js";
|
|
33
33
|
import { SYNC_CHUNK_BYTES } from "./project-sync-transfer.js";
|
|
34
|
-
import { completeRecords, continuedAfter, findLocalSession, homeRelativeTranscript, listLocalSessions, readTranscript, rewriteSessionPaths, sessionState } from "./local-sessions.js";
|
|
34
|
+
import { completeRecords, continuedAfter, findLocalSession, homeRelativeTranscript, listLocalSessions, readTranscript, recordCwd, rewriteSessionPaths, sessionState } from "./local-sessions.js";
|
|
35
35
|
import { installDivergenceNotice, movedKey, movedSession, movedSessions, rememberMoved } from "./moved-sessions.js";
|
|
36
|
+
import { captureWorktreeMigration, inspectWorktreeMigration, migrationScope, preserveWorktreeMigration, referencedWorktrees, rewriteWorktreeTranscript } from "./move-worktrees.js";
|
|
36
37
|
const VM_HOME = '/home/user';
|
|
37
38
|
const SPACE = '/shardflux/project';
|
|
38
39
|
/** A transcript above this moves with the repository's history bound. */
|
|
@@ -95,14 +96,27 @@ async function writeChunks(cell, path, bytes) {
|
|
|
95
96
|
* project space is mounted, as the conversation's durable copy with the same checkpoint metadata the guest writes
|
|
96
97
|
* (transcripts/<id>/meta.json), so the session survives the VM from its first minute.
|
|
97
98
|
*/
|
|
98
|
-
export async function placeTranscript(cell, c, s, bytes, folder, signal) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
99
|
+
export async function placeTranscript(cell, c, s, bytes, folder, signal, worktrees) {
|
|
100
|
+
// The project is confirmed by the session's own records (0.20.2+): any of them working in the folder, not only the
|
|
101
|
+
// first (a session started in a scratch folder that went on in this repository). Paths are moved for this folder;
|
|
102
|
+
// records of a folder outside it keep their bytes.
|
|
103
|
+
const here = confirmedCwds(s, bytes, folder, worktrees);
|
|
104
|
+
let moved = bytes;
|
|
105
|
+
if (worktrees) {
|
|
106
|
+
// Normalize linked spellings confirmed by record metadata before mapping
|
|
107
|
+
// longest registered prefixes. Scratch records outside the family stay intact.
|
|
108
|
+
for (const tree of [...worktrees.trees].sort((a, b) => b.source.length - a.source.length)) {
|
|
109
|
+
const spellings = here.filter(cwd => isInside(tree.source, realOr(cwd))).map(cwd => linkedSpelling(cwd, tree.source));
|
|
110
|
+
for (const spelled of new Set(spellings.filter((p) => !!p)))
|
|
111
|
+
moved = rewriteSessionPaths(moved, spelled, tree.source);
|
|
112
|
+
}
|
|
113
|
+
moved = rewriteWorktreeTranscript(moved, worktrees, c.worktree_path);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
moved = rewriteSessionPaths(moved, folder, c.worktree_path);
|
|
117
|
+
// A path recorded through a link (/tmp for /private/tmp): move that spelling too.
|
|
118
|
+
for (const spelled of new Set(here.map(cwd => linkedSpelling(cwd, folder)).filter((p) => !!p)))
|
|
119
|
+
moved = rewriteSessionPaths(moved, spelled, c.worktree_path);
|
|
106
120
|
}
|
|
107
121
|
const homeRelative = homeRelativeTranscript(s, c.worktree_path);
|
|
108
122
|
const native = `${VM_HOME}/${homeRelative}`;
|
|
@@ -125,6 +139,52 @@ const realOr = (path) => { try {
|
|
|
125
139
|
catch {
|
|
126
140
|
return resolve(path);
|
|
127
141
|
} };
|
|
142
|
+
const isInside = (folder, path) => path === folder || path.startsWith(folder.endsWith('/') ? folder : `${folder}/`);
|
|
143
|
+
/** The folder as `cwd` spells it through a link (/tmp/x for /private/tmp/x), or undefined when it spells it plainly. */
|
|
144
|
+
function linkedSpelling(cwd, folder) {
|
|
145
|
+
if (isInside(folder, cwd))
|
|
146
|
+
return undefined;
|
|
147
|
+
const inside = relative(folder, realOr(cwd));
|
|
148
|
+
if (inside && !cwd.endsWith(`/${inside}`))
|
|
149
|
+
return undefined;
|
|
150
|
+
return inside ? cwd.slice(0, cwd.length - inside.length - 1) : cwd;
|
|
151
|
+
}
|
|
152
|
+
/** Every working directory the whole transcript names in its record metadata, never in message or tool content. */
|
|
153
|
+
export function transcriptCwds(bytes) {
|
|
154
|
+
const out = new Set();
|
|
155
|
+
for (const line of bytes.toString('utf8').split('\n')) {
|
|
156
|
+
try {
|
|
157
|
+
const r = JSON.parse(line);
|
|
158
|
+
if (!r || typeof r !== 'object' || Array.isArray(r))
|
|
159
|
+
continue;
|
|
160
|
+
const cwd = recordCwd(r);
|
|
161
|
+
if (cwd)
|
|
162
|
+
out.add(cwd);
|
|
163
|
+
}
|
|
164
|
+
catch { /* a partial record */ }
|
|
165
|
+
}
|
|
166
|
+
return [...out];
|
|
167
|
+
}
|
|
168
|
+
function confirmedCwds(s, bytes, folder, worktrees) {
|
|
169
|
+
const roots = [folder, ...(worktrees?.trees.map(t => t.source) ?? [])];
|
|
170
|
+
const here = [...new Set([s.cwd, ...s.cwds, ...transcriptCwds(bytes)])].filter(cwd => roots.some(root => isInside(root, realOr(cwd))));
|
|
171
|
+
if (!here.length)
|
|
172
|
+
throw new UsageError('The session ran outside this project folder; run shard move in its project, or pass --path with the repository it worked in.');
|
|
173
|
+
return here;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The project a move goes to: --path when the session's records confirm it worked there (placeTranscript checks), the
|
|
177
|
+
* current folder's project when it worked there, else the project of where it works now.
|
|
178
|
+
*/
|
|
179
|
+
export function moveFolder(s, here, explicit) {
|
|
180
|
+
if (explicit)
|
|
181
|
+
return here;
|
|
182
|
+
const root = (cwd) => (existsSync(cwd) ? projectRoot(cwd) : here);
|
|
183
|
+
if (isInside(here, realOr(s.cwd)))
|
|
184
|
+
return root(s.cwd);
|
|
185
|
+
const inHere = [...s.cwds].reverse().find(cwd => isInside(here, realOr(cwd)));
|
|
186
|
+
return inHere ? root(inHere) : root(s.cwd);
|
|
187
|
+
}
|
|
128
188
|
async function alreadyMoved(ctx, m) {
|
|
129
189
|
try {
|
|
130
190
|
const c = await getConversation(ctx, m.conversation_id);
|
|
@@ -140,19 +200,49 @@ export async function moveSession(ctx, values) {
|
|
|
140
200
|
const env = { ...process.env, ...ctx.io.env };
|
|
141
201
|
const here = projectRoot(typeof values.path === 'string' ? values.path : process.cwd());
|
|
142
202
|
const s = chooseSession(values, env, here);
|
|
143
|
-
const folder = typeof values.path === 'string'
|
|
203
|
+
const folder = moveFolder(s, here, typeof values.path === 'string');
|
|
144
204
|
const prior = movedSession(ctx.store.dir, ctx.apiUrl, s.harness, s.session_id);
|
|
145
|
-
if (prior) {
|
|
205
|
+
if (prior && values.plan !== true) {
|
|
146
206
|
const live = await alreadyMoved(ctx, prior);
|
|
147
207
|
if (live) {
|
|
148
208
|
out(ctx, `This session already continues in the cloud as ${prior.key}. Open it: shard agents open ${prior.key}\n`, { already: true, moved: prior, conversation: live });
|
|
149
209
|
return 0;
|
|
150
210
|
}
|
|
151
211
|
}
|
|
152
|
-
|
|
212
|
+
// A normal move continues the session from this checkout, uncommitted edits included; the repository's other
|
|
213
|
+
// worktrees and local branches stay on this computer, untouched (as `this-worktree-only` says explicitly).
|
|
214
|
+
// --preserve-all-worktrees copies them too.
|
|
215
|
+
const preserveAll = values['preserve-all-worktrees'] === true;
|
|
216
|
+
if (preserveAll && values['this-worktree-only'] === true)
|
|
217
|
+
throw new UsageError('Choose --preserve-all-worktrees or --this-worktree-only.');
|
|
218
|
+
if (values.plan !== true) {
|
|
219
|
+
// A local MCP caller is still in its turn: an unrelated folder is refused while it can still answer.
|
|
220
|
+
if (!preserveAll)
|
|
221
|
+
confirmedCwds(s, completeRecords(readTranscript(s.file)), folder);
|
|
222
|
+
await waitForTurnEnd(ctx, s, env);
|
|
223
|
+
}
|
|
153
224
|
const bytes = completeRecords(readTranscript(s.file));
|
|
154
225
|
if (bytes.length > TRANSCRIPT_LIMIT)
|
|
155
226
|
throw new UsageError('This session’s transcript is above the 256 MiB move bound.');
|
|
227
|
+
const plan = preserveAll || values.plan === true ? await inspectWorktreeMigration(folder, bytes, ctx.signal) : null;
|
|
228
|
+
// Refuse before syncing credentials, creating a VM or snapshotting an unrelated
|
|
229
|
+
// folder. An explicit all-worktree scope also accepts record-confirmed linked owners.
|
|
230
|
+
confirmedCwds(s, bytes, folder, plan ?? undefined);
|
|
231
|
+
let elsewhere = [];
|
|
232
|
+
if (plan) {
|
|
233
|
+
await captureWorktreeMigration(plan, ctx.signal);
|
|
234
|
+
const scope = migrationScope(plan);
|
|
235
|
+
if (values.plan === true) {
|
|
236
|
+
out(ctx, `${JSON.stringify(scope, null, 2)}\n`, scope);
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
ctx.io.stderr.write(`Preserving ${plan.trees.length} registered worktrees, ${plan.files} files, ${plan.bytes} file bytes (archives add headers). Includes ignored files and Git metadata:\n${plan.trees.map(t => ` ${t.source}`).join('\n')}\nSymlink targets and unrelated sibling folders are not uploaded.\n`);
|
|
240
|
+
}
|
|
241
|
+
else if (values['this-worktree-only'] !== true) {
|
|
242
|
+
elsewhere = await referencedWorktrees(folder, bytes, ctx.signal);
|
|
243
|
+
if (elsewhere.length && !ctx.json)
|
|
244
|
+
ctx.io.stderr.write(`This session also worked in ${elsewhere.length === 1 ? 'another worktree' : `${elsewhere.length} other worktrees`} of this repository (${elsewhere.join(', ')}); ${elsewhere.length === 1 ? 'it stays' : 'they stay'} on this computer. shard move --preserve-all-worktrees copies every worktree.\n`);
|
|
245
|
+
}
|
|
156
246
|
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
157
247
|
if (!ctx.json)
|
|
158
248
|
ctx.io.stderr.write(`Moving ${s.harness === 'claude' ? 'Claude Code' : 'Codex'} session “${s.title}” to the cloud…\n`);
|
|
@@ -162,11 +252,16 @@ export async function moveSession(ctx, values) {
|
|
|
162
252
|
requireClaudeTerminal(ctx);
|
|
163
253
|
let created;
|
|
164
254
|
let placed;
|
|
255
|
+
let preserved;
|
|
165
256
|
try {
|
|
166
257
|
created = await createConversation(ctx, folder, s.harness, 'terminal', undefined, {}, {
|
|
167
258
|
session_id: s.session_id, title: s.title,
|
|
259
|
+
...(values['preserve-all-worktrees'] === true && plan ? { preserveWorktrees: true,
|
|
260
|
+
seedRepository: async (ws, c) => { preserved = await preserveWorktreeMigration(ws.cell(), plan, c, ctx.signal); } } : {}),
|
|
168
261
|
// The workspace's cell handle is shared with the seed that follows: it stays open.
|
|
169
|
-
beforeSeed: async (ws, c) => {
|
|
262
|
+
beforeSeed: async (ws, c) => {
|
|
263
|
+
placed = await placeTranscript(ws.cell(), c, s, bytes, folder, ctx.signal, values['preserve-all-worktrees'] === true && plan ? plan : undefined);
|
|
264
|
+
},
|
|
170
265
|
});
|
|
171
266
|
}
|
|
172
267
|
catch (e) {
|
|
@@ -198,7 +293,7 @@ export async function moveSession(ctx, values) {
|
|
|
198
293
|
if (!attach) {
|
|
199
294
|
const quiet = { ...ctx, io: { ...ctx.io, stdout: { write: () => true } } };
|
|
200
295
|
await conversationTerminal(quiet, ws, c, place, { ...values, detach: true });
|
|
201
|
-
out(ctx, `Moved to the cloud as ${c.key}: the same session, with its history. Open it: shard agents open ${c.key}\n`, { moved, conversation: c, workspace: ws.data, ...(placed ? { transcript: placed } : {}) });
|
|
296
|
+
out(ctx, `Moved to the cloud as ${c.key}: the same session, with its history. Open it: shard agents open ${c.key}\n`, { moved, conversation: c, workspace: ws.data, ...(placed ? { transcript: placed } : {}), ...(preserved ? { preserved } : {}), ...(elsewhere.length ? { other_worktrees: elsewhere } : {}) });
|
|
202
297
|
return 0;
|
|
203
298
|
}
|
|
204
299
|
const where = openInChoices(ctx, project.slug)[s.harness];
|
|
@@ -208,7 +303,7 @@ export async function moveSession(ctx, values) {
|
|
|
208
303
|
if (where === 'claude-desktop')
|
|
209
304
|
return openClaudeDesktopSession(ctx, ws, c);
|
|
210
305
|
const url = await remoteControlUrl(ctx, ws, c);
|
|
211
|
-
if (!openUrl(url, ctx.io.env))
|
|
306
|
+
if (!(await openUrl(url, ctx.io.env)))
|
|
212
307
|
throw new Error(`could not open ${url}`);
|
|
213
308
|
out(ctx, `${url}\n`, { moved, conversation: c, url });
|
|
214
309
|
return 0;
|
|
@@ -223,7 +318,7 @@ export function listMovable(ctx, values, env, now = Date.now()) {
|
|
|
223
318
|
const sessions = listLocalSessions(folder, env, { harness: harnessOf(values), limit: 10 }).map(s => {
|
|
224
319
|
const m = moved[movedKey(s.harness, s.session_id)];
|
|
225
320
|
const state = sessionState(s, env, now);
|
|
226
|
-
return { harness: s.harness, session_id: s.session_id, title: s.title, updated_at: s.updated_at, source: s.source, cwd: s.cwd,
|
|
321
|
+
return { harness: s.harness, session_id: s.session_id, title: s.title, updated_at: s.updated_at, source: s.source, cwd: s.cwd, cwds: s.cwds,
|
|
227
322
|
running: state.running, turn: state.turn,
|
|
228
323
|
moved: m ? { conversation_id: m.conversation_id, key: m.key, moved_at: m.moved_at, diverged: continuedAfter(s, m.bytes) } : null };
|
|
229
324
|
});
|
package/dist/onboarding.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Ctx } from './commands.js';
|
|
2
2
|
import type { Values } from './args.js';
|
|
3
3
|
import type { CodexConnectorView, CodexHealth } from './codex-cloud.js';
|
|
4
|
+
import type { MachineRow } from './machine-status.js';
|
|
4
5
|
/**
|
|
5
6
|
* The first-run demo's task: it shows the agent's own computer at work. The user copies it and pastes it into the
|
|
6
7
|
* launcher themselves; Daylight's own CLAUDE.md and AGENTS.md say how to run it and test it in the desktop browser.
|
|
@@ -13,7 +14,7 @@ export type AgentApp = 'claude' | 'codex';
|
|
|
13
14
|
* rest (Claude Code's installer uses ~/.local/bin).
|
|
14
15
|
*/
|
|
15
16
|
export declare function installDirs(home: string): string[];
|
|
16
|
-
/** Claude Code or Codex is on this
|
|
17
|
+
/** Claude Code or Codex is on this computer: its home folder (~/.claude, ~/.codex) or its command on PATH or in an install folder. */
|
|
17
18
|
export declare function foundApps(env: NodeJS.ProcessEnv): Promise<AgentApp[]>;
|
|
18
19
|
/** --apps claude,codex (repeatable or comma separated); unknown names are refused. */
|
|
19
20
|
export declare function parseApps(value: unknown): AgentApp[] | undefined;
|
|
@@ -71,32 +72,35 @@ export interface T3SignInRow {
|
|
|
71
72
|
* appears in T3 Code and the T3 app by itself. `startSignIn` (`--t3-connect`) starts that sign-in. Null when T3 Code
|
|
72
73
|
* is not installed.
|
|
73
74
|
*/
|
|
74
|
-
export declare function t3Connection(ctx: Ctx, signedIn: boolean, appDirs?: string[], platform?: NodeJS.Platform, startSignIn?: boolean): Promise<Connection | null>;
|
|
75
|
+
export declare function t3Connection(ctx: Ctx, signedIn: boolean, appDirs?: string[], platform?: NodeJS.Platform, startSignIn?: boolean, scope?: Values): Promise<Connection | null>;
|
|
75
76
|
/** Claude: the account login is ready and Claude itself accepts it right now. */
|
|
76
77
|
export declare function claudeConnection(row: CredentialRow | undefined): Omit<Connection, 'id' | 'picked' | 'found'>;
|
|
77
|
-
/** Codex: this
|
|
78
|
+
/** Codex: this computer's Codex login is copied and the account's Codex remote device (one executor) is connected. */
|
|
78
79
|
export declare function codexConnection(local: boolean, synced: boolean, connector: CodexConnectorView | null, health: CodexHealth): Omit<Connection, 'id' | 'picked' | 'found'>;
|
|
79
80
|
/**
|
|
80
81
|
* The onboarding report, for an agent setting itself up from chat and for the menu bar app. It verifies every
|
|
81
82
|
* connection live: the Shardflux account, Claude (Claude itself accepts the login) and Codex (the login is copied and
|
|
82
|
-
* the account's Codex remote device is connected), for the apps picked (--apps, else saved, else found on this
|
|
83
|
+
* the account's Codex remote device is connected), for the apps picked (--apps, else saved, else found on this computer).
|
|
83
84
|
* With --connect it also does what needs no sign-in: copies the Codex login and enables the Codex remote device.
|
|
84
85
|
*/
|
|
85
|
-
export declare function onboardingReport(ctx: Ctx, values: Values): Promise<{
|
|
86
|
+
export declare function onboardingReport(ctx: Ctx, values: Values, platform?: NodeJS.Platform): Promise<{
|
|
86
87
|
version: number;
|
|
87
88
|
api_url: string;
|
|
88
89
|
apps: AgentApp[];
|
|
89
90
|
found_apps: AgentApp[];
|
|
90
91
|
connections: Connection[];
|
|
91
92
|
ready: boolean;
|
|
93
|
+
connections_ready: boolean;
|
|
94
|
+
machine_ready: boolean | null;
|
|
92
95
|
account_error: string | null;
|
|
96
|
+
machine: MachineRow | null;
|
|
93
97
|
demo: {
|
|
94
98
|
prompt: string;
|
|
95
99
|
project: string;
|
|
96
100
|
prepare: string[];
|
|
97
101
|
};
|
|
98
102
|
activity: {
|
|
99
|
-
state: "running" | "failed" | "
|
|
103
|
+
state: "running" | "failed" | "creating" | "starting" | "suspending" | "suspended" | "resuming" | "forking" | "stopping" | "deleting" | "deleted";
|
|
100
104
|
workspace: {
|
|
101
105
|
id: string;
|
|
102
106
|
key: string;
|
|
@@ -180,7 +184,7 @@ interface ProcessRow {
|
|
|
180
184
|
export declare function activeCommands(processes: ProcessRow[]): string[];
|
|
181
185
|
/** Commands and listening ports of a running VM. Never wakes a parked computer. */
|
|
182
186
|
export declare function liveActivity(ctx: Ctx, ref: string): Promise<{
|
|
183
|
-
state: "running" | "failed" | "
|
|
187
|
+
state: "running" | "failed" | "creating" | "starting" | "suspending" | "suspended" | "resuming" | "forking" | "stopping" | "deleting" | "deleted";
|
|
184
188
|
workspace: {
|
|
185
189
|
id: string;
|
|
186
190
|
key: string;
|
package/dist/onboarding.js
CHANGED
|
@@ -6,12 +6,15 @@ import { resolveWorkspace } from "./commands.js";
|
|
|
6
6
|
import { AgentStateStore } from "./agent-state.js";
|
|
7
7
|
import { describeError } from "./errors.js";
|
|
8
8
|
import { t3PhoneLink } from "./t3-phone.js";
|
|
9
|
+
import { t3NeedsProject, t3SignInProject } from "./account-scope.js";
|
|
9
10
|
import { readAgentCredentials, syncAgentCredentials } from "./agent-credentials.js";
|
|
10
11
|
import { getConversation } from "./conversations.js";
|
|
11
12
|
import { codexHealth, readCodexConnector, restartCodexConnector } from "./codex-cloud.js";
|
|
12
13
|
import { applicationDirs } from "./agent-targets.js";
|
|
13
14
|
import { configureT3Provider } from "./t3-provider.js";
|
|
14
15
|
import { t3Install } from "./t3-mode.js";
|
|
16
|
+
import { inspectMachineRow, rowText } from "./machine-status.js";
|
|
17
|
+
import { launcherShortcut } from "./this-computer.js";
|
|
15
18
|
/**
|
|
16
19
|
* The first-run demo's task: it shows the agent's own computer at work. The user copies it and pastes it into the
|
|
17
20
|
* launcher themselves; Daylight's own CLAUDE.md and AGENTS.md say how to run it and test it in the desktop browser.
|
|
@@ -37,7 +40,7 @@ export function installDirs(home) {
|
|
|
37
40
|
return [join(home, '.local', 'bin'), join(home, '.claude', 'local'), join(home, '.npm-global', 'bin'), join(home, '.bun', 'bin'),
|
|
38
41
|
join(home, '.volta', 'bin'), join(home, 'Library', 'pnpm')];
|
|
39
42
|
}
|
|
40
|
-
/** Claude Code or Codex is on this
|
|
43
|
+
/** Claude Code or Codex is on this computer: its home folder (~/.claude, ~/.codex) or its command on PATH or in an install folder. */
|
|
41
44
|
export async function foundApps(env) {
|
|
42
45
|
const home = env.HOME ?? homedir();
|
|
43
46
|
const onPath = async (name) => {
|
|
@@ -69,7 +72,7 @@ export function parseApps(value) {
|
|
|
69
72
|
* appears in T3 Code and the T3 app by itself. `startSignIn` (`--t3-connect`) starts that sign-in. Null when T3 Code
|
|
70
73
|
* is not installed.
|
|
71
74
|
*/
|
|
72
|
-
export async function t3Connection(ctx, signedIn, appDirs = applicationDirs(ctx.io.env), platform = process.platform, startSignIn = false) {
|
|
75
|
+
export async function t3Connection(ctx, signedIn, appDirs = applicationDirs(ctx.io.env), platform = process.platform, startSignIn = false, scope = {}) {
|
|
73
76
|
// The same install every T3 command uses (macOS and Linux): a build with ACP providers first.
|
|
74
77
|
const app = t3Install(ctx.io.env, platform, appDirs);
|
|
75
78
|
if (!app)
|
|
@@ -90,37 +93,52 @@ export async function t3Connection(ctx, signedIn, appDirs = applicationDirs(ctx.
|
|
|
90
93
|
// Stable T3: one T3 Connect sign-in per person (the API's, per account), then each project appears by itself. `next`
|
|
91
94
|
// is for scripts: a project is set up for T3 Code with it.
|
|
92
95
|
const ssh = { ...base, mode: 'ssh', next: shard(['agents', 'new', '--in', 't3', '--path', '<project>']) };
|
|
96
|
+
const selection = ['org', 'project'].flatMap(key => typeof scope[key] === 'string' ? [`--${key}`, scope[key]] : []);
|
|
93
97
|
const offer = { ...ssh, state: 'action', fact: 'Your projects appear in T3 Code and the T3 app once connected.',
|
|
94
|
-
action: { title: 'Connect T3 Code', command: shard(['setup', '--inspect', '--t3-connect', '--json']) } };
|
|
98
|
+
action: { title: 'Connect T3 Code', command: shard(['setup', '--inspect', '--t3-connect', '--json', ...selection]) } };
|
|
95
99
|
if (!signedIn)
|
|
96
100
|
return offer;
|
|
97
|
-
|
|
101
|
+
let projectId;
|
|
98
102
|
let status;
|
|
99
103
|
try {
|
|
100
|
-
|
|
104
|
+
// Status and sign-in must use the same person and organization. The key may belong to another organization
|
|
105
|
+
// (or another member); its connected state cannot decide whether this person's selected sign-in is connected.
|
|
106
|
+
if (ctx.sessionSource() !== null) {
|
|
107
|
+
projectId = await t3SignInProject(ctx, scope);
|
|
108
|
+
status = await ctx.account().t3.status(projectId ? { projectId } : {});
|
|
109
|
+
}
|
|
110
|
+
else
|
|
111
|
+
status = await ctx.cloud().t3.status();
|
|
101
112
|
}
|
|
102
113
|
catch {
|
|
103
114
|
return { ...ssh, state: 'unknown', fact: 'T3 Connect could not be checked just now.', action: { title: 'Retry', retry: true } };
|
|
104
115
|
}
|
|
105
116
|
// Remembered for `agents targets`: with the sign-in, every project can start in T3 Code (t3Status).
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
// A one-off --org/--project check must not replace the default target's remembered status.
|
|
118
|
+
if (selection.length === 0) {
|
|
119
|
+
try {
|
|
120
|
+
new AgentStateStore(ctx.store.dir).saveT3Connected(ctx.apiUrl, status.connected);
|
|
121
|
+
}
|
|
122
|
+
catch { /* the next check writes it */ }
|
|
108
123
|
}
|
|
109
|
-
catch { /* the next check writes it */ }
|
|
110
124
|
if (!status.connected) {
|
|
111
125
|
if (!startSignIn)
|
|
112
126
|
return offer;
|
|
113
|
-
const signIn = await startT3SignIn(ctx);
|
|
127
|
+
const signIn = await startT3SignIn(ctx, projectId);
|
|
114
128
|
if (signIn === 'connected')
|
|
115
|
-
return t3Connection(ctx, signedIn, appDirs, platform, false);
|
|
129
|
+
return t3Connection(ctx, signedIn, appDirs, platform, false, scope);
|
|
116
130
|
if (signIn.needs_login)
|
|
117
131
|
return { ...offer, fact: 'Sign in to Shardflux on this computer to connect T3 Code.', t3_link: signIn };
|
|
118
132
|
return { ...offer, fact: 'Confirm the code on the page that opened. This updates on its own.', t3_link: signIn,
|
|
119
133
|
action: { title: 'Connect T3 Code', url: signIn.sign_in.verification_uri } };
|
|
120
134
|
}
|
|
121
135
|
const connected = { ...ssh, state: 'verified', fact: 'Your projects appear in T3 Code and the T3 app.' };
|
|
136
|
+
// Explicit account selection is a status check, not consent to relink the default computer's last workspace.
|
|
137
|
+
if (selection.length > 0)
|
|
138
|
+
return connected;
|
|
122
139
|
// The newest project set up for T3 Code from this computer (agents.json; a targets.json entry from an older CLI still
|
|
123
|
-
// counts): where it is. One set up before T3 Connect
|
|
140
|
+
// counts): where it is. One set up before T3 Connect is linked now. An existing error stays visible until the
|
|
141
|
+
// person explicitly requests another link; a periodic inspection must not restart permanent refusals.
|
|
124
142
|
const recorded = new AgentStateStore(ctx.store.dir).t3Environments(ctx.apiUrl).at(-1);
|
|
125
143
|
const manifest = await jsonFile(join(ctx.store.dir, 'targets.json'));
|
|
126
144
|
const entries = Array.isArray(manifest?.entries) ? manifest.entries : [];
|
|
@@ -131,7 +149,9 @@ export async function t3Connection(ctx, signedIn, appDirs = applicationDirs(ctx.
|
|
|
131
149
|
try {
|
|
132
150
|
const ws = await resolveWorkspace(ctx, key);
|
|
133
151
|
const known = status.links.find(l => l.workspace_id === ws.id);
|
|
134
|
-
|
|
152
|
+
if (known?.status === 'error')
|
|
153
|
+
return { ...ssh, state: 'info', fact: known.last_error ?? 'T3 Connect could not link this workspace. Check T3 Connect, then open this project in T3 Code again to retry.' };
|
|
154
|
+
const link = !known || known.status === 'needs_sign_in' ? (await t3PhoneLink(ctx, ws.id, ws.data.project_id)).link : known;
|
|
135
155
|
// Linked to the person's T3 account: T3 Code and the T3 app list it once they are signed in to that account.
|
|
136
156
|
if (link.status === 'linked')
|
|
137
157
|
return { ...connected, fact: `${link.label} is linked to your T3 account.` };
|
|
@@ -149,19 +169,21 @@ export async function t3Connection(ctx, signedIn, appDirs = applicationDirs(ctx.
|
|
|
149
169
|
}
|
|
150
170
|
/**
|
|
151
171
|
* Starts the person's T3 Connect sign-in (a pending one comes back again). It needs this computer's Shardflux session,
|
|
152
|
-
* not only an API key. `connected` when the account already has one (409 `t3_signed_in`).
|
|
172
|
+
* not only an API key. `connected` when the account already has one (409 `t3_signed_in`). The sign-in is per
|
|
173
|
+
* organization: it is scoped by --project/--org, else the API key's project, else the saved default (0.20.2+; before,
|
|
174
|
+
* a person in two organizations got 422 project_id is required).
|
|
153
175
|
*/
|
|
154
|
-
async function startT3SignIn(ctx) {
|
|
155
|
-
if (ctx.
|
|
176
|
+
async function startT3SignIn(ctx, projectId) {
|
|
177
|
+
if (ctx.sessionSource() === null)
|
|
156
178
|
return { status: 'needs_sign_in', label: '', needs_login: true };
|
|
157
179
|
try {
|
|
158
|
-
const flow = await ctx.account().t3.signIn();
|
|
180
|
+
const flow = await ctx.account().t3.signIn(projectId ? { projectId } : {});
|
|
159
181
|
return { status: 'needs_sign_in', label: '', sign_in: { verification_uri: flow.verification_uri, user_code: flow.user_code, expires_at: flow.expires_at } };
|
|
160
182
|
}
|
|
161
183
|
catch (error) {
|
|
162
184
|
if (error instanceof ShardfluxApiError && describeError(error).code === 't3_signed_in')
|
|
163
185
|
return 'connected';
|
|
164
|
-
throw error;
|
|
186
|
+
throw t3NeedsProject(error) ?? error;
|
|
165
187
|
}
|
|
166
188
|
}
|
|
167
189
|
const shard = (words) => ['shard', ...words];
|
|
@@ -176,7 +198,7 @@ export function claudeConnection(row) {
|
|
|
176
198
|
return { state: 'action', fact: 'Claude no longer accepts this sign-in.', action: { ...signIn, title: 'Sign in to Claude again' } };
|
|
177
199
|
return { state: 'verified', fact: 'Signed in. Works in every conversation, on web and phone.' };
|
|
178
200
|
}
|
|
179
|
-
/** Codex: this
|
|
201
|
+
/** Codex: this computer's Codex login is copied and the account's Codex remote device (one executor) is connected. */
|
|
180
202
|
export function codexConnection(local, synced, connector, health) {
|
|
181
203
|
const enable = { title: 'Connect Codex', command: shard(['agents', 'codex', 'enable']) };
|
|
182
204
|
if (!synced) {
|
|
@@ -198,14 +220,14 @@ export function codexConnection(local, synced, connector, health) {
|
|
|
198
220
|
/**
|
|
199
221
|
* The onboarding report, for an agent setting itself up from chat and for the menu bar app. It verifies every
|
|
200
222
|
* connection live: the Shardflux account, Claude (Claude itself accepts the login) and Codex (the login is copied and
|
|
201
|
-
* the account's Codex remote device is connected), for the apps picked (--apps, else saved, else found on this
|
|
223
|
+
* the account's Codex remote device is connected), for the apps picked (--apps, else saved, else found on this computer).
|
|
202
224
|
* With --connect it also does what needs no sign-in: copies the Codex login and enables the Codex remote device.
|
|
203
225
|
*/
|
|
204
|
-
export async function onboardingReport(ctx, values) {
|
|
226
|
+
export async function onboardingReport(ctx, values, platform = process.platform) {
|
|
205
227
|
const saved = await jsonFile(resolve(ctx.store.dir, 'onboarding.json'));
|
|
206
228
|
const state = saved?.api_url === ctx.apiUrl ? saved : undefined;
|
|
207
229
|
const found = await foundApps(ctx.io.env);
|
|
208
|
-
// An empty saved pick means nothing was chosen yet: what is found on this
|
|
230
|
+
// An empty saved pick means nothing was chosen yet: what is found on this computer is pre-ticked.
|
|
209
231
|
const savedApps = Array.isArray(state?.apps) && state.apps.length ? parseApps(state.apps.filter(v => typeof v === 'string')) : undefined;
|
|
210
232
|
const apps = parseApps(values.apps) ?? savedApps ?? found;
|
|
211
233
|
const connect = values.connect === true;
|
|
@@ -256,7 +278,7 @@ export async function onboardingReport(ctx, values) {
|
|
|
256
278
|
let read = await readCodexConnector(ctx);
|
|
257
279
|
health = codexHealth(read.connector, read.workspaceState);
|
|
258
280
|
const synced = credentials.some(row => row.app === 'codex');
|
|
259
|
-
// One executor per account, enabled as `shard setup` does when this
|
|
281
|
+
// One executor per account, enabled as `shard setup` does when this computer is signed in to Codex. --retry-codex starts
|
|
260
282
|
// a failed or stuck one again, once.
|
|
261
283
|
if (connect && synced && (health === 'missing' || health === 'disabled')) {
|
|
262
284
|
read = { connector: await ctx.cloud().codex.enable({ signal: ctx.signal }), workspaceState: null };
|
|
@@ -279,24 +301,33 @@ export async function onboardingReport(ctx, values) {
|
|
|
279
301
|
const claude = { id: 'claude', picked: apps.includes('claude'), found: found.includes('claude'),
|
|
280
302
|
...(!connected ? signIn : credentialError !== undefined ? failure(credentialError) : claudeConnection(credentials.find(row => row.app === 'claude'))) };
|
|
281
303
|
const codex = { id: 'codex', picked: apps.includes('codex'), found: found.includes('codex'),
|
|
282
|
-
...(!connected ? signIn :
|
|
283
|
-
:
|
|
284
|
-
|
|
304
|
+
...(!connected ? signIn : codexError instanceof ShardfluxApiError && codexError.reason === 'codex_chatgpt_login_required'
|
|
305
|
+
? { state: 'action', fact: codexError.message, action: { title: 'Sign in with ChatGPT', command: ['codex', 'login'] } }
|
|
306
|
+
: (credentialError ?? codexError) !== undefined ? failure(credentialError ?? codexError)
|
|
307
|
+
: codexConnection(localCodex, credentials.some(row => row.app === 'codex'), connector, health)) };
|
|
308
|
+
const t3 = await t3Connection(ctx, connected || ctx.sessionSource() !== null, undefined, undefined, values['t3-connect'] === true, values);
|
|
285
309
|
const connections = [account, claude, codex, ...(t3 ? [t3] : [])];
|
|
310
|
+
// The cloud machine (0.20.2+), the same row `shard machine setup --status` reports: whether what is installed carries
|
|
311
|
+
// over to new conversations. It never decides `ready`: conversations start without a saved machine too.
|
|
312
|
+
const machine = connected ? await inspectMachineRow(ctx) : null;
|
|
313
|
+
const shortcut = launcherShortcut(ctx.store.dir, platform);
|
|
314
|
+
const press = (what) => (shortcut ? `Press ${shortcut}, ${what}` : `Open a new conversation, ${what}`);
|
|
286
315
|
// Ready: signed in and at least one agent app connected; the other can be connected later (Settings, or this again).
|
|
287
316
|
const ready = account.state === 'verified' && [claude, codex].some(c => c.state === 'verified');
|
|
288
317
|
const done = (value) => value === true || (typeof value === 'string' && value.length > 0);
|
|
289
318
|
return {
|
|
290
|
-
version: 3, api_url: ctx.apiUrl, apps, found_apps: found, connections, ready,
|
|
319
|
+
version: 3, api_url: ctx.apiUrl, apps, found_apps: found, connections, ready, connections_ready: ready,
|
|
320
|
+
machine_ready: machine && !['unknown', 'unavailable'].includes(machine.state) ? machine.state === 'ready' && machine.version !== null && machine.version > 0 : null,
|
|
321
|
+
account_error: accountError, machine,
|
|
291
322
|
demo: { prompt: DEMO_PROMPT, project: 'Daylight (sample)', prepare: shard(['setup', '--prepare-sample', '--json']) },
|
|
292
323
|
activity: values.activity === true && typeof values.workspace === 'string' ? await liveActivity(ctx, values.workspace) : null,
|
|
293
324
|
steps: [
|
|
294
325
|
{ id: 'account', title: 'Sign in to Shardflux', status: connected ? 'complete' : 'available', command: shard(['signup']) },
|
|
295
326
|
{ id: 'connect', title: 'Connect Claude, Codex or both', status: ready ? 'complete' : 'available' },
|
|
296
327
|
{ id: 'demo', title: 'Try it once: your agent’s own computer', status: done(state?.demo_finished) ? 'complete' : 'available',
|
|
297
|
-
action:
|
|
328
|
+
action: `Copy the task. ${press('paste the task, then Start.')} The agent installs what it needs, starts it and tests it in its own browser.` },
|
|
298
329
|
{ id: 'own', title: 'Your own project', status: done(state?.own_key) ? 'complete' : 'available',
|
|
299
|
-
action: '
|
|
330
|
+
action: press('pick your project and describe what you want.') },
|
|
300
331
|
],
|
|
301
332
|
};
|
|
302
333
|
}
|
|
@@ -305,7 +336,10 @@ export function onboardingText(report) {
|
|
|
305
336
|
const names = { account: 'Shardflux', claude: 'Claude', codex: 'Codex', t3: 'T3 Code' };
|
|
306
337
|
const mark = { verified: '✓', action: '!', pending: '…', unknown: '?', info: '·' };
|
|
307
338
|
const lines = report.connections.filter(c => c.id === 'account' || c.picked || c.id === 't3').map(c => `${mark[c.state]} ${names[c.id]}: ${c.fact}${c.action?.command ? ` (${c.action.command.join(' ')})` : ''}`);
|
|
308
|
-
|
|
339
|
+
const machineMark = { ready: '✓', saving: '…', failed: '!', none: '·', unknown: '?', unavailable: '?' };
|
|
340
|
+
if (report.machine)
|
|
341
|
+
lines.push(`${machineMark[report.machine.state]} ${rowText(report.machine)}`);
|
|
342
|
+
return `${lines.join('\n')}\n${report.ready ? `Agent connections ready. Try it once: ${report.demo.prompt}` : 'Connections not ready yet: finish the steps above, then run this again.'}\n`;
|
|
309
343
|
}
|
|
310
344
|
/** What a person recognises, most telling first: installs, then how the app is run, then what it runs. */
|
|
311
345
|
const RANKS = [/^(sudo|apt|apt-get|dpkg)$/, /^(npm|npx|pnpm|yarn|bun)$/, /^(node|python[0-9.]*|uv|vite|next|deno|docker|cargo|go|make)$/, /^(pip[0-9.]*|inotifywait)$/];
|