@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/handlers.js
CHANGED
|
@@ -24,4 +24,6 @@ import { ACCOUNT_POOL_HANDLERS } from "./account-pool.js";
|
|
|
24
24
|
import { MOVE_HANDLERS } from "./move.js";
|
|
25
25
|
import { AGENT_CONFIG_HANDLERS } from "./agent-config-sync.js";
|
|
26
26
|
import { MACHINE_SETUP_HANDLERS } from "./machine-setup.js";
|
|
27
|
-
|
|
27
|
+
import { SETTINGS_HANDLERS } from "./client-settings.js";
|
|
28
|
+
import { PROJECT_PREPARATION_HANDLERS } from "./project-preparation.js";
|
|
29
|
+
export const HANDLERS = { ...PROJECT_PREPARATION_HANDLERS, ...MOVE_HANDLERS, ...DEVICE_HANDLERS, ...CODEX_CLOUD_HANDLERS, ...CLI_PROXY_HANDLERS, ...THREAD_HANDLERS, ...CONVERSATION_HANDLERS, ...LOCAL_CONVERSATION_HANDLERS, ...TWIN_HANDLERS, ...CREDENTIAL_HANDLERS, ...AGENT_CREDENTIAL_HANDLERS, ...ACCOUNT_POOL_HANDLERS, ...KEY_HANDLERS, ...ACCOUNT_HANDLERS, ...AGENT_HANDLERS, ...CENTER_HANDLERS, ...TARGET_HANDLERS, ...AGENT_GIT_HANDLERS, ...MENUBAR_HANDLERS, ...FILESYSTEM_HANDLERS, ...MACHINE_SETUP_HANDLERS, ...SETTINGS_HANDLERS, ...UPDATE_HANDLERS, ...AGENT_CONFIG_HANDLERS };
|
package/dist/initial-prompt.d.ts
CHANGED
|
@@ -30,6 +30,14 @@ export declare const INBOX_MAX_CHARS = 65536;
|
|
|
30
30
|
* options. The guest's own first-message delivery reads the screen the same way (coding 39+).
|
|
31
31
|
*/
|
|
32
32
|
export declare function inputBoxReady(tail: string, glyph: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* The first prompt typed by the VM's own guest (0.21.1+, with a guest that has it): `sf-agent conversation first-prompt` waits for
|
|
35
|
+
* the input box, pastes, waits for the paste to show and presses Enter inside the VM, with the same checks as
|
|
36
|
+
* `submitInitialPrompt`, in one request from here instead of an attachment and three round trips. Nothing was typed
|
|
37
|
+
* when the result is `unsupported` (an older guest without the command: this terminal types it) or `not_ready` (the
|
|
38
|
+
* harness never showed its input box within the guest's wait: the conversation's inbox takes it, where there is one).
|
|
39
|
+
*/
|
|
40
|
+
export declare function guestFirstPrompt(cell: Pick<CellClient, 'exec'>, ptyId: string, app: 'claude' | 'codex', prompt: string, workspaceKey: string, signal: AbortSignal): Promise<'submitted' | 'unsupported' | 'not_ready'>;
|
|
33
41
|
/**
|
|
34
42
|
* Wait for the native terminal to enable paste before submitting one first turn. No blind timing or input replay.
|
|
35
43
|
* When nothing was typed yet and the terminal never became ready, `queue` hands the prompt to the conversation's guest
|
package/dist/initial-prompt.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { readInput, str, strs } from "./commands.js";
|
|
3
3
|
import { PtyOutputCursor } from "./agent-terminal.js";
|
|
4
4
|
import { CodedError, UsageError } from "./errors.js";
|
|
5
|
+
import { seedEvent, seedPhase } from "./seed-phases.js";
|
|
5
6
|
import { ShardfluxApiError } from '@shardflux/sdk';
|
|
6
7
|
import { parse, stringify } from 'smol-toml';
|
|
7
8
|
/** The user chose this repository. Trust exactly that new VM worktree before launching Codex's first turn. */
|
|
@@ -150,6 +151,41 @@ export function inputBoxReady(tail, glyph) {
|
|
|
150
151
|
const at = plain.lastIndexOf(glyph);
|
|
151
152
|
return at >= 0 && !/^[\s\u00a0]*\d+\./.test(plain.slice(at + glyph.length));
|
|
152
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* The first prompt typed by the VM's own guest (0.21.1+, with a guest that has it): `sf-agent conversation first-prompt` waits for
|
|
156
|
+
* the input box, pastes, waits for the paste to show and presses Enter inside the VM, with the same checks as
|
|
157
|
+
* `submitInitialPrompt`, in one request from here instead of an attachment and three round trips. Nothing was typed
|
|
158
|
+
* when the result is `unsupported` (an older guest without the command: this terminal types it) or `not_ready` (the
|
|
159
|
+
* harness never showed its input box within the guest's wait: the conversation's inbox takes it, where there is one).
|
|
160
|
+
*/
|
|
161
|
+
export async function guestFirstPrompt(cell, ptyId, app, prompt, workspaceKey, signal) {
|
|
162
|
+
const asked = Date.now();
|
|
163
|
+
const r = await cell.exec.run(['sf-agent', 'conversation', 'first-prompt', '--pty', ptyId, '--app', app], { stdin: prompt, timeoutMs: 30_000, maxOutputBytes: 8192, signal });
|
|
164
|
+
let result;
|
|
165
|
+
try {
|
|
166
|
+
result = JSON.parse(r.stdout.trim().split('\n').at(-1) ?? '');
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
result = undefined;
|
|
170
|
+
}
|
|
171
|
+
// The guest's own steps, on this computer's clock from when the request left (an upper bound by its transit).
|
|
172
|
+
for (const [name, ms] of [['guest_input_box', result?.ready_ms], ['guest_echo', result?.echo_ms], ['guest_submitted', result?.submitted_ms]]) {
|
|
173
|
+
if (typeof ms === 'number')
|
|
174
|
+
seedEvent(name, asked + ms);
|
|
175
|
+
}
|
|
176
|
+
// An older guest answers "unknown tool conversation_first-prompt" and prints no result.
|
|
177
|
+
if (!result || typeof result.pasted !== 'boolean')
|
|
178
|
+
return 'unsupported';
|
|
179
|
+
if (result.submitted === true) {
|
|
180
|
+
seedPhase('prompt_echo');
|
|
181
|
+
return 'submitted';
|
|
182
|
+
}
|
|
183
|
+
if (!result.pasted)
|
|
184
|
+
return 'not_ready';
|
|
185
|
+
// Typed into the input box: it must not be typed again.
|
|
186
|
+
const sent = typeof result.echo_ms === 'number' && result.echo_ms > 0;
|
|
187
|
+
throw new CodedError('initial_prompt_failed', `Created ${workspaceKey}, but the initial prompt ${sent ? 'could not be confirmed' : 'was not submitted'}. Open that conversation before starting another.`, undefined, { workspace_key: workspaceKey, submitted: sent });
|
|
188
|
+
}
|
|
153
189
|
/**
|
|
154
190
|
* Wait for the native terminal to enable paste before submitting one first turn. No blind timing or input replay.
|
|
155
191
|
* When nothing was typed yet and the terminal never became ready, `queue` hands the prompt to the conversation's guest
|
|
@@ -162,6 +198,7 @@ export async function submitInitialPrompt(connect, prompt, workspaceKey, signal,
|
|
|
162
198
|
let sent = false, pasted = false;
|
|
163
199
|
try {
|
|
164
200
|
connection = await connect(bounded);
|
|
201
|
+
seedPhase('attach');
|
|
165
202
|
let tail = '', pasteEnabled = false;
|
|
166
203
|
const cursor = new PtyOutputCursor();
|
|
167
204
|
for await (const message of connection.messages()) {
|
|
@@ -176,8 +213,10 @@ export async function submitInitialPrompt(connect, prompt, workspaceKey, signal,
|
|
|
176
213
|
pasteEnabled ||= tail.includes('\x1b[?2004h');
|
|
177
214
|
// eslint-disable-next-line no-control-regex -- Strip native terminal ANSI before matching prompt echo.
|
|
178
215
|
const plain = tail.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '').replace(/\s/g, '');
|
|
179
|
-
if (sent && bytes.length)
|
|
216
|
+
if (sent && bytes.length) {
|
|
217
|
+
seedPhase('prompt_echo');
|
|
180
218
|
return 'submitted';
|
|
219
|
+
}
|
|
181
220
|
if (pasted) {
|
|
182
221
|
// A long prompt overflows the input box: a 24-row terminal shows only its end, so either end confirms the paste.
|
|
183
222
|
const compact = prompt.replace(/\s/g, '');
|
|
@@ -188,6 +227,7 @@ export async function submitInitialPrompt(connect, prompt, workspaceKey, signal,
|
|
|
188
227
|
await write(enter);
|
|
189
228
|
else
|
|
190
229
|
connection.input(enter);
|
|
230
|
+
seedPhase('prompt_enter');
|
|
191
231
|
sent = true;
|
|
192
232
|
tail = '';
|
|
193
233
|
continue;
|
|
@@ -196,10 +236,12 @@ export async function submitInitialPrompt(connect, prompt, workspaceKey, signal,
|
|
|
196
236
|
if (!ready || !pasteEnabled)
|
|
197
237
|
continue;
|
|
198
238
|
const paste = Buffer.from(`\x1b[200~${prompt}\x1b[201~`);
|
|
239
|
+
seedPhase('input_box');
|
|
199
240
|
if (write)
|
|
200
241
|
await write(paste);
|
|
201
242
|
else
|
|
202
243
|
connection.input(paste);
|
|
244
|
+
seedPhase('prompt_paste');
|
|
203
245
|
pasted = true;
|
|
204
246
|
tail = '';
|
|
205
247
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Images, audio and video from this size on (marketing assets of a few hundred KB are the common case). */
|
|
2
|
+
export declare const LARGE_MEDIA_BYTES: number;
|
|
3
|
+
export interface OutsideFile {
|
|
4
|
+
path: string;
|
|
5
|
+
size: number;
|
|
6
|
+
mode: number;
|
|
7
|
+
meta: string;
|
|
8
|
+
}
|
|
9
|
+
/** The worktree's large untracked media, sorted; each file's sample is judged once per version (size, times, inode). */
|
|
10
|
+
export declare function largeUntrackedMedia(root: string, stateDir: string, id: string, signal?: AbortSignal): Promise<OutsideFile[]>;
|
|
11
|
+
/** Pathspecs that leave these files out of `git add -A`. */
|
|
12
|
+
export declare const outsideSpecs: (files: readonly OutsideFile[]) => string[];
|
|
13
|
+
/** The Git object id of a file's bytes with no filters (`git hash-object --no-filters`), streamed. */
|
|
14
|
+
export declare function blobId(path: string, format: 'sha1' | 'sha256', size: number): Promise<string>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Large untracked media (0.22.0+): untracked files of at least LARGE_MEDIA_BYTES whose bytes barely compress (video,
|
|
3
|
+
* images, audio, archives). Twin snapshots leave them out, so a start or a landing never hashes and stores hundreds of
|
|
4
|
+
* megabytes in Git; a start carries them as files beside the snapshot instead (prepared-repository.ts), and the VM's
|
|
5
|
+
* Git ignores them as the user's Git sees them: untracked. Text, however large, stays in the snapshot.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { constants, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { lstat, open } from 'node:fs/promises';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { deflateRawSync } from 'node:zlib';
|
|
12
|
+
import { projectGitPaths, validSyncPath } from "./project-sync-manifest.js";
|
|
13
|
+
/** Images, audio and video from this size on (marketing assets of a few hundred KB are the common case). */
|
|
14
|
+
export const LARGE_MEDIA_BYTES = 256 << 10;
|
|
15
|
+
/** A 64 KiB sample that deflates to at least this share of its size counts as incompressible. */
|
|
16
|
+
const INCOMPRESSIBLE = 0.92;
|
|
17
|
+
/** A path the VM can write and its Git can ignore with one exclude line (no line breaks, valid UTF-8). */
|
|
18
|
+
const carriable = (path) => validSyncPath(path) && !/[\n\r]/.test(path) && Buffer.from(path, 'utf8').toString('utf8') === path && !path.includes('�');
|
|
19
|
+
async function sampleIncompressible(path) {
|
|
20
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
21
|
+
try {
|
|
22
|
+
const sample = Buffer.allocUnsafe(64 << 10), { bytesRead } = await file.read(sample, 0, sample.length, 0);
|
|
23
|
+
return bytesRead > 0 && deflateRawSync(sample.subarray(0, bytesRead), { level: 1 }).length >= bytesRead * INCOMPRESSIBLE;
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
await file.close();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** The worktree's large untracked media, sorted; each file's sample is judged once per version (size, times, inode). */
|
|
30
|
+
export async function largeUntrackedMedia(root, stateDir, id, signal) {
|
|
31
|
+
const candidates = [];
|
|
32
|
+
await projectGitPaths(root, ['ls-files', '-z', '--others', '--exclude-standard'], path => { candidates.push(path); }, { signal });
|
|
33
|
+
const dir = join(stateDir, 'twin'), cachePath = join(dir, `${id}.media.json`);
|
|
34
|
+
let cache = {};
|
|
35
|
+
try {
|
|
36
|
+
cache = JSON.parse(readFileSync(cachePath, 'utf8'));
|
|
37
|
+
}
|
|
38
|
+
catch { /* first snapshot */ }
|
|
39
|
+
const next = {}, found = [];
|
|
40
|
+
let at = 0;
|
|
41
|
+
await Promise.all(Array.from({ length: Math.min(16, candidates.length) }, async () => {
|
|
42
|
+
for (let i = at++; i < candidates.length; i = at++) {
|
|
43
|
+
const path = candidates[i];
|
|
44
|
+
if (path.endsWith('/') || path.split('/').some(part => part === '.shard' || part.includes('.shard-tmp-')) || !carriable(path))
|
|
45
|
+
continue;
|
|
46
|
+
let st;
|
|
47
|
+
try {
|
|
48
|
+
st = await lstat(join(root, path));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (!st.isFile() || st.size < LARGE_MEDIA_BYTES)
|
|
54
|
+
continue;
|
|
55
|
+
const meta = `${st.size}:${st.mtimeMs}:${st.ctimeMs}:${st.ino}:${st.mode}`;
|
|
56
|
+
let media = cache[path]?.meta === meta ? cache[path].media : undefined;
|
|
57
|
+
if (media === undefined) {
|
|
58
|
+
try {
|
|
59
|
+
media = await sampleIncompressible(join(root, path));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
next[path] = { meta, media };
|
|
66
|
+
if (media)
|
|
67
|
+
found.push({ path, size: st.size, mode: st.mode & 0o777, meta });
|
|
68
|
+
}
|
|
69
|
+
}));
|
|
70
|
+
if (JSON.stringify(next) !== JSON.stringify(cache)) {
|
|
71
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
72
|
+
const temp = `${cachePath}.${process.pid}.tmp`;
|
|
73
|
+
writeFileSync(temp, JSON.stringify(next), { mode: 0o600 });
|
|
74
|
+
renameSync(temp, cachePath);
|
|
75
|
+
}
|
|
76
|
+
return found.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
77
|
+
}
|
|
78
|
+
/** Pathspecs that leave these files out of `git add -A`. */
|
|
79
|
+
export const outsideSpecs = (files) => files.map(f => `:(exclude,literal)${f.path}`);
|
|
80
|
+
/** The Git object id of a file's bytes with no filters (`git hash-object --no-filters`), streamed. */
|
|
81
|
+
export async function blobId(path, format, size) {
|
|
82
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW), hash = createHash(format).update(`blob ${size}\0`);
|
|
83
|
+
try {
|
|
84
|
+
const buffer = Buffer.allocUnsafe(4 << 20);
|
|
85
|
+
let read = 0;
|
|
86
|
+
for (;;) {
|
|
87
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, read);
|
|
88
|
+
if (!bytesRead)
|
|
89
|
+
break;
|
|
90
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
91
|
+
read += bytesRead;
|
|
92
|
+
}
|
|
93
|
+
if (read !== size)
|
|
94
|
+
throw new Error('changed while it was read');
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
await file.close();
|
|
98
|
+
}
|
|
99
|
+
return hash.digest('hex');
|
|
100
|
+
}
|
package/dist/linux-terminal.d.ts
CHANGED
|
@@ -3,9 +3,11 @@ import type { Values } from './args.js';
|
|
|
3
3
|
/** Linux terminals use the existing native Codex harness; explicit app routes keep Cloud. */
|
|
4
4
|
export declare function linuxTerminalSession(ctx: Pick<Ctx, 'io' | 'json'>, values: Values, platform?: NodeJS.Platform): boolean;
|
|
5
5
|
export declare const shellWord: (s: string) => string;
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The Linux default terminal running `argv` (in `cwd` when given): $TERMINAL, x-terminal-emulator, then gnome-terminal.
|
|
8
|
+
* The other terminal apps (Ghostty, kitty, WezTerm, Alacritty, Warp) open through terminal-apps.ts.
|
|
9
|
+
*/
|
|
10
|
+
export declare function linuxTerminalCommand(argv: string[], env: Record<string, string | undefined>, cwd?: string): {
|
|
7
11
|
command: string;
|
|
8
12
|
args: string[];
|
|
9
13
|
} | null;
|
|
10
|
-
/** Invoked before cloud work: the new terminal owns the attach, credentials and any prompts. */
|
|
11
|
-
export declare function openLinuxTerminal(ctx: Ctx, ref: string, app?: string): Promise<number>;
|
package/dist/linux-terminal.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { accessSync, constants } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
4
3
|
/** Linux terminals use the existing native Codex harness; explicit app routes keep Cloud. */
|
|
5
4
|
export function linuxTerminalSession(ctx, values, platform = process.platform) {
|
|
6
5
|
return platform === 'linux' && (values.in === 'terminal' || (values.in === undefined && !!ctx.io.stdin.isTTY && !ctx.json));
|
|
7
6
|
}
|
|
8
7
|
export const shellWord = (s) => `'${s.replaceAll("'", "'\\''")}'`;
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The Linux default terminal running `argv` (in `cwd` when given): $TERMINAL, x-terminal-emulator, then gnome-terminal.
|
|
10
|
+
* The other terminal apps (Ghostty, kitty, WezTerm, Alacritty, Warp) open through terminal-apps.ts.
|
|
11
|
+
*/
|
|
12
|
+
export function linuxTerminalCommand(argv, env, cwd) {
|
|
10
13
|
const executable = (name) => name.includes('/') ? [name] : (env.PATH ?? '').split(':').filter(Boolean).map(p => join(p, name));
|
|
11
14
|
const exists = (name) => executable(name).some(p => { try {
|
|
12
15
|
accessSync(p, constants.X_OK);
|
|
@@ -16,27 +19,10 @@ export function linuxTerminalCommand(argv, env) {
|
|
|
16
19
|
return false;
|
|
17
20
|
} });
|
|
18
21
|
// The shell keeps the window open on an attach error so the user can read and retry it.
|
|
19
|
-
const attach = ['sh', '-c', `${argv.map(shellWord).join(' ')}; status=$?; if [ "$status" -ne 0 ]; then printf '\nPress Enter to close.'; read answer; fi; exit "$status"`];
|
|
22
|
+
const attach = ['sh', '-c', `${cwd ? `cd ${shellWord(cwd)} 2>/dev/null; ` : ''}${argv.map(shellWord).join(' ')}; status=$?; if [ "$status" -ne 0 ]; then printf '\nPress Enter to close.'; read answer; fi; exit "$status"`];
|
|
20
23
|
for (const command of [...new Set([env.TERMINAL?.trim(), 'x-terminal-emulator', 'gnome-terminal'].filter((s) => !!s))]) {
|
|
21
24
|
if (exists(command))
|
|
22
25
|
return { command, args: [command === 'gnome-terminal' ? '--' : '-e', ...attach] };
|
|
23
26
|
}
|
|
24
27
|
return null;
|
|
25
28
|
}
|
|
26
|
-
/** Invoked before cloud work: the new terminal owns the attach, credentials and any prompts. */
|
|
27
|
-
export async function openLinuxTerminal(ctx, ref, app) {
|
|
28
|
-
const words = ['agents', 'open', ref, '--in', 'terminal', ...(app ? ['--app', app] : [])];
|
|
29
|
-
const argv = ctx.runtime ? [ctx.runtime.node, ctx.runtime.script, '--api-url', ctx.apiUrl, ...words] : ['shard', ...words];
|
|
30
|
-
const choice = linuxTerminalCommand(argv, ctx.io.env);
|
|
31
|
-
if (choice) {
|
|
32
|
-
const child = spawn(choice.command, choice.args, { env: ctx.io.env, cwd: process.cwd(), detached: true, stdio: 'ignore' });
|
|
33
|
-
try {
|
|
34
|
-
await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', reject); });
|
|
35
|
-
child.unref();
|
|
36
|
-
return 0;
|
|
37
|
-
}
|
|
38
|
-
catch { /* print a runnable command if the terminal could not start */ }
|
|
39
|
-
}
|
|
40
|
-
ctx.io.stdout.write(`${['shard', '--api-url', ctx.apiUrl, ...words].map(shellWord).join(' ')}\n`);
|
|
41
|
-
return 0;
|
|
42
|
-
}
|
package/dist/local-sessions.d.ts
CHANGED
|
@@ -4,8 +4,13 @@ export interface LocalSession {
|
|
|
4
4
|
session_id: string;
|
|
5
5
|
/** The transcript (Claude) or rollout (Codex) file. */
|
|
6
6
|
file: string;
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* The session's working directory (0.20.2+: the one most of its latest records name, ties to the newest; before, the
|
|
9
|
+
* first record's). A session started in a scratch folder that went on in a repository is that repository's.
|
|
10
|
+
*/
|
|
8
11
|
cwd: string;
|
|
12
|
+
/** Every working directory its records name (the transcript's head and tail), the latest used last (0.20.2+). */
|
|
13
|
+
cwds: string[];
|
|
9
14
|
title: string;
|
|
10
15
|
updated_at: string;
|
|
11
16
|
bytes: number;
|
|
@@ -28,10 +33,29 @@ export declare const codexHome: (env: Env) => string;
|
|
|
28
33
|
export declare function claudeProjectName(cwd: string): string;
|
|
29
34
|
/** Records appended after `offset` (whole lines only). */
|
|
30
35
|
export declare function recordsAfter(file: string, offset: number): Rec[];
|
|
36
|
+
/** A record's working directory: Claude's `cwd` on every record, Codex's on session_meta and turn_context. */
|
|
37
|
+
export declare function recordCwd(r: Rec): string;
|
|
38
|
+
/** The working directories records name, the latest used last. */
|
|
39
|
+
export declare function recordCwds(records: Rec[]): string[];
|
|
40
|
+
/**
|
|
41
|
+
* Where a session works: the working directory most of its latest records name (the newest wins a tie), so a session
|
|
42
|
+
* started in a scratch folder that went on in a repository belongs to the repository, and a brief `cd` elsewhere at the
|
|
43
|
+
* end does not move it. `recent` is the transcript's tail; `fallback` when it names none.
|
|
44
|
+
*/
|
|
45
|
+
export declare function workingDirectory(recent: Rec[], fallback: string): string;
|
|
31
46
|
/** Whether the transcript's last turn is still running, from its records (see the module comment). */
|
|
32
47
|
export declare function claudeTurnFromRecords(records: Rec[]): 'working' | 'idle';
|
|
33
48
|
export declare function codexTurnFromRecords(records: Rec[]): 'working' | 'idle';
|
|
34
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Whether a session worked in `folder` (or below it): any working directory its records name (0.20.2+; before, only
|
|
51
|
+
* the first). A session started in a scratch folder that went on in a repository is listed under the repository too.
|
|
52
|
+
*/
|
|
53
|
+
export declare function workedIn(s: Pick<LocalSession, 'cwd' | 'cwds'>, folder: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* A project's sessions on this computer, newest first: those that worked in the folder or below it. Claude keeps a
|
|
56
|
+
* transcript under the folder it started in, so one that started elsewhere (a scratch folder) and went on here is found
|
|
57
|
+
* by its records: the newest transcripts of other folders whose last 64 KiB name this folder are read.
|
|
58
|
+
*/
|
|
35
59
|
export declare function listLocalSessions(folder: string, env: Env, options?: {
|
|
36
60
|
harness?: SessionHarness;
|
|
37
61
|
limit?: number;
|
package/dist/local-sessions.js
CHANGED
|
@@ -93,6 +93,37 @@ const oneLine = (text, max = 80) => {
|
|
|
93
93
|
return line.length > max ? `${line.slice(0, max - 1).trimEnd()}…` : line;
|
|
94
94
|
};
|
|
95
95
|
// ---------------------------------------------------------------------------------------------------------------
|
|
96
|
+
// Working directories
|
|
97
|
+
/** A record's working directory: Claude's `cwd` on every record, Codex's on session_meta and turn_context. */
|
|
98
|
+
export function recordCwd(r) {
|
|
99
|
+
if (typeof r.cwd === 'string')
|
|
100
|
+
return r.cwd;
|
|
101
|
+
return r.type === 'session_meta' || r.type === 'turn_context' ? str(obj(r.payload).cwd) : '';
|
|
102
|
+
}
|
|
103
|
+
/** The working directories records name, the latest used last. */
|
|
104
|
+
export function recordCwds(records) {
|
|
105
|
+
const last = new Map();
|
|
106
|
+
records.forEach((r, i) => { const cwd = recordCwd(r); if (cwd)
|
|
107
|
+
last.set(cwd, i); });
|
|
108
|
+
return [...last.entries()].sort((a, b) => a[1] - b[1]).map(([cwd]) => cwd);
|
|
109
|
+
}
|
|
110
|
+
/** How many of the latest records with a working directory decide where a session works. */
|
|
111
|
+
const LATEST_CWDS = 100;
|
|
112
|
+
/**
|
|
113
|
+
* Where a session works: the working directory most of its latest records name (the newest wins a tie), so a session
|
|
114
|
+
* started in a scratch folder that went on in a repository belongs to the repository, and a brief `cd` elsewhere at the
|
|
115
|
+
* end does not move it. `recent` is the transcript's tail; `fallback` when it names none.
|
|
116
|
+
*/
|
|
117
|
+
export function workingDirectory(recent, fallback) {
|
|
118
|
+
const count = new Map(), last = new Map();
|
|
119
|
+
recent.map(recordCwd).filter(Boolean).slice(-LATEST_CWDS).forEach((cwd, i) => { count.set(cwd, (count.get(cwd) ?? 0) + 1); last.set(cwd, i); });
|
|
120
|
+
let best = '';
|
|
121
|
+
for (const [cwd, n] of count)
|
|
122
|
+
if (!best || n > count.get(best) || (n === count.get(best) && last.get(cwd) > last.get(best)))
|
|
123
|
+
best = cwd;
|
|
124
|
+
return best || fallback;
|
|
125
|
+
}
|
|
126
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
96
127
|
// Claude
|
|
97
128
|
function claudeText(content) {
|
|
98
129
|
if (typeof content === 'string')
|
|
@@ -153,10 +184,12 @@ function claudeSession(file) {
|
|
|
153
184
|
return null;
|
|
154
185
|
const titled = [...last].reverse().find(r => (r.type === 'custom-title' && str(r.customTitle)) || (r.type === 'ai-title' && str(r.aiTitle)) || (r.type === 'summary' && str(r.summary)));
|
|
155
186
|
const title = titled ? str(titled.customTitle) || str(titled.aiTitle) || str(titled.summary) : claudeText(obj(prompt.message).content);
|
|
156
|
-
// Where
|
|
187
|
+
// Where it works now (a session started in a scratch folder that went on in a repository is the repository's), and
|
|
188
|
+
// every folder it worked in.
|
|
157
189
|
const started = [...first, ...last].find(r => str(r.cwd)) ?? prompt;
|
|
158
|
-
|
|
159
|
-
|
|
190
|
+
const records = st.size <= (256 << 10) ? first : [...first, ...last];
|
|
191
|
+
return { harness: 'claude', session_id: id, file, cwd: workingDirectory(last, str(started.cwd)), cwds: recordCwds(records), title: oneLine(title),
|
|
192
|
+
updated_at: st.mtime.toISOString(), bytes: st.size, source: str(prompt.entrypoint) || 'cli' };
|
|
160
193
|
}
|
|
161
194
|
function claudeProcesses(env) {
|
|
162
195
|
const dir = join(claudeConfigDir(env), 'sessions');
|
|
@@ -209,22 +242,33 @@ export function codexTurnFromRecords(records) {
|
|
|
209
242
|
}
|
|
210
243
|
return 'idle';
|
|
211
244
|
}
|
|
212
|
-
|
|
245
|
+
/**
|
|
246
|
+
* A Codex rollout. `belongs` (a listing's folder test) is asked of its working directories: the head's first, and only
|
|
247
|
+
* when none matches, the tail's too (a thread that moved on to another folder in a later turn).
|
|
248
|
+
*/
|
|
249
|
+
function codexSession(file, belongs) {
|
|
213
250
|
const match = ROLLOUT.exec(basename(file));
|
|
214
251
|
if (!match)
|
|
215
252
|
return null;
|
|
216
253
|
const st = statSync(file);
|
|
217
254
|
const first = head(file);
|
|
218
255
|
const meta = first.find(r => r.type === 'session_meta');
|
|
219
|
-
const
|
|
220
|
-
if (!meta || !
|
|
256
|
+
const started = str(obj(meta?.payload).cwd);
|
|
257
|
+
if (!meta || !started)
|
|
258
|
+
return null;
|
|
259
|
+
if (belongs && !recordCwds(first).some(belongs) && (st.size <= (256 << 10) || !tailNames(file, st.size, belongs)))
|
|
221
260
|
return null;
|
|
222
261
|
const last = tail(file, st.size);
|
|
223
262
|
const prompt = [...first, ...last].map(codexPrompt).find(Boolean);
|
|
224
263
|
if (!prompt)
|
|
225
264
|
return null;
|
|
226
|
-
|
|
227
|
-
|
|
265
|
+
const records = st.size <= (256 << 10) ? first : [...first, ...last];
|
|
266
|
+
return { harness: 'codex', session_id: match[1], file, cwd: workingDirectory(last, started), cwds: recordCwds(records), title: oneLine(prompt),
|
|
267
|
+
updated_at: st.mtime.toISOString(), bytes: st.size, source: str(obj(meta.payload).originator) || 'codex' };
|
|
268
|
+
}
|
|
269
|
+
/** Whether the transcript's last 64 KiB name a working directory `belongs` takes, in record metadata only. */
|
|
270
|
+
function tailNames(file, size, belongs) {
|
|
271
|
+
return recordCwds(tail(file, size, 64 << 10)).some(belongs);
|
|
228
272
|
}
|
|
229
273
|
/** Every rollout file, newest first (one stat each; heads are read only for the ones a caller looks at). */
|
|
230
274
|
function codexRollouts(env) {
|
|
@@ -273,14 +317,29 @@ const real = (path) => { try {
|
|
|
273
317
|
catch {
|
|
274
318
|
return path;
|
|
275
319
|
} };
|
|
276
|
-
/**
|
|
320
|
+
/**
|
|
321
|
+
* Whether a session worked in `folder` (or below it): any working directory its records name (0.20.2+; before, only
|
|
322
|
+
* the first). A session started in a scratch folder that went on in a repository is listed under the repository too.
|
|
323
|
+
*/
|
|
324
|
+
export function workedIn(s, folder) {
|
|
325
|
+
const inside = within(real(folder));
|
|
326
|
+
return [s.cwd, ...s.cwds].some(cwd => inside(real(cwd)));
|
|
327
|
+
}
|
|
328
|
+
/** Claude transcripts stored under another folder's name that a listing looks at (the newest), each by its tail first. */
|
|
329
|
+
const FOREIGN_CLAUDE_LIMIT = 400;
|
|
330
|
+
/**
|
|
331
|
+
* A project's sessions on this computer, newest first: those that worked in the folder or below it. Claude keeps a
|
|
332
|
+
* transcript under the folder it started in, so one that started elsewhere (a scratch folder) and went on here is found
|
|
333
|
+
* by its records: the newest transcripts of other folders whose last 64 KiB name this folder are read.
|
|
334
|
+
*/
|
|
277
335
|
export function listLocalSessions(folder, env, options = {}) {
|
|
278
336
|
const root = real(folder), limit = options.limit ?? 10, inside = within(root), out = [];
|
|
337
|
+
const belongs = (cwd) => inside(real(cwd));
|
|
279
338
|
if (options.harness !== 'codex') {
|
|
280
339
|
const projects = join(claudeConfigDir(env), 'projects'), exact = claudeProjectName(root);
|
|
281
340
|
let dirs = [];
|
|
282
341
|
try {
|
|
283
|
-
dirs = readdirSync(projects)
|
|
342
|
+
dirs = readdirSync(projects);
|
|
284
343
|
}
|
|
285
344
|
catch { /* no Claude sessions */ }
|
|
286
345
|
const files = [];
|
|
@@ -292,22 +351,43 @@ export function listLocalSessions(folder, env, options = {}) {
|
|
|
292
351
|
catch {
|
|
293
352
|
continue;
|
|
294
353
|
}
|
|
354
|
+
const own = dir === exact || dir.startsWith(`${exact}-`);
|
|
295
355
|
for (const name of names)
|
|
296
356
|
if (name.endsWith('.jsonl')) {
|
|
297
357
|
const file = join(projects, dir, name);
|
|
298
358
|
try {
|
|
299
|
-
files.push({ file, mtime: statSync(file).mtimeMs });
|
|
359
|
+
files.push({ file, mtime: statSync(file).mtimeMs, own });
|
|
300
360
|
}
|
|
301
361
|
catch { /* removed */ }
|
|
302
362
|
}
|
|
303
363
|
}
|
|
304
|
-
let found = 0;
|
|
305
|
-
for (const { file } of files.sort((a, b) => b.mtime - a.mtime)) {
|
|
364
|
+
let found = 0, foreign = 0;
|
|
365
|
+
for (const { file, own } of files.sort((a, b) => b.mtime - a.mtime)) {
|
|
306
366
|
if (found >= limit)
|
|
307
367
|
break;
|
|
308
|
-
|
|
368
|
+
if (!own) {
|
|
369
|
+
if (foreign >= FOREIGN_CLAUDE_LIMIT)
|
|
370
|
+
continue;
|
|
371
|
+
foreign++;
|
|
372
|
+
let size;
|
|
373
|
+
try {
|
|
374
|
+
size = statSync(file).size;
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (!tailNames(file, size, belongs))
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
let s;
|
|
383
|
+
try {
|
|
384
|
+
s = claudeSession(file);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
309
389
|
// A folder named like another's prefix ("app" and "app-web") is told apart by the recorded cwd.
|
|
310
|
-
if (s &&
|
|
390
|
+
if (s && workedIn(s, root)) {
|
|
311
391
|
out.push(s);
|
|
312
392
|
found++;
|
|
313
393
|
}
|
|
@@ -318,7 +398,7 @@ export function listLocalSessions(folder, env, options = {}) {
|
|
|
318
398
|
for (const { file } of codexRollouts(env)) {
|
|
319
399
|
if (found >= limit)
|
|
320
400
|
break;
|
|
321
|
-
const s = codexSession(file,
|
|
401
|
+
const s = codexSession(file, belongs);
|
|
322
402
|
if (s) {
|
|
323
403
|
out.push(s);
|
|
324
404
|
found++;
|
|
@@ -397,25 +477,34 @@ export function rewriteSessionPaths(bytes, from, to) {
|
|
|
397
477
|
return bytes;
|
|
398
478
|
const needles = [from, JSON.stringify(from).slice(1, -1)];
|
|
399
479
|
const move = (path) => (path === from || path.startsWith(`${from}/`) ? to + path.slice(from.length) : path);
|
|
400
|
-
const
|
|
401
|
-
if (Array.isArray(value))
|
|
402
|
-
return value.map(rewrite);
|
|
403
|
-
if (!value || typeof value !== 'object')
|
|
404
|
-
return value;
|
|
480
|
+
const rewriteFields = (value) => {
|
|
405
481
|
return Object.fromEntries(Object.entries(value).map(([key, leaf]) => {
|
|
406
482
|
if (typeof leaf === 'string' && ['cwd', 'workdir', 'working_directory'].includes(key))
|
|
407
483
|
return [key, move(leaf)];
|
|
408
484
|
if (Array.isArray(leaf) && ['workspace_roots', 'runtime_workspace_roots', 'writable_roots'].includes(key))
|
|
409
|
-
return [key, leaf.map(v => (typeof v === 'string' ? move(v) :
|
|
410
|
-
return [key,
|
|
485
|
+
return [key, leaf.map(v => (typeof v === 'string' ? move(v) : v))];
|
|
486
|
+
return [key, leaf];
|
|
411
487
|
}));
|
|
412
488
|
};
|
|
489
|
+
const rewrite = (r) => {
|
|
490
|
+
const moved = rewriteFields(r);
|
|
491
|
+
if (r.type === 'session_meta' || r.type === 'turn_context') {
|
|
492
|
+
const payload = rewriteFields(obj(r.payload));
|
|
493
|
+
for (const key of ['sandbox_policy', 'permission_profile']) {
|
|
494
|
+
if (payload[key] && typeof payload[key] === 'object' && !Array.isArray(payload[key]))
|
|
495
|
+
payload[key] = rewriteFields(obj(payload[key]));
|
|
496
|
+
}
|
|
497
|
+
moved.payload = payload;
|
|
498
|
+
}
|
|
499
|
+
return moved;
|
|
500
|
+
};
|
|
413
501
|
const lines = bytes.toString('utf8').split('\n');
|
|
414
502
|
return Buffer.from(lines.map(line => {
|
|
415
503
|
if (!line || !needles.some(n => line.includes(n)))
|
|
416
504
|
return line;
|
|
417
505
|
try {
|
|
418
|
-
|
|
506
|
+
const parsed = JSON.parse(line);
|
|
507
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? JSON.stringify(rewrite(obj(parsed))) : line;
|
|
419
508
|
}
|
|
420
509
|
catch {
|
|
421
510
|
return line;
|
package/dist/machine-setup.d.ts
CHANGED
|
@@ -77,6 +77,10 @@ export declare function codexArgs(workDir: string, help: string): string[];
|
|
|
77
77
|
*/
|
|
78
78
|
export declare function setupGuard(env: Env, command: string, values: Values, positionals: string[]): string | undefined;
|
|
79
79
|
export interface Inventory {
|
|
80
|
+
/** The home whose files were inventoried; a local agent's login shell can use a different home. */
|
|
81
|
+
source_home?: string;
|
|
82
|
+
/** Exact portable dotfile paths in source_home; agent configuration is copied separately by shard. */
|
|
83
|
+
config_paths?: string[];
|
|
80
84
|
os: string;
|
|
81
85
|
shell: string | null;
|
|
82
86
|
homebrew: string[] | null;
|
|
@@ -91,6 +95,12 @@ export interface Inventory {
|
|
|
91
95
|
};
|
|
92
96
|
dotfiles: string[];
|
|
93
97
|
}
|
|
98
|
+
/** Requested caps are not grants: the plan can give this machine less disk than the requested cap. */
|
|
99
|
+
export interface SetupCapacity {
|
|
100
|
+
disk_gib: number;
|
|
101
|
+
requested_disk_gib: number | null;
|
|
102
|
+
source: string | null;
|
|
103
|
+
}
|
|
94
104
|
/** What is on this computer, without contents that could hold a secret: names, versions, the git identity. */
|
|
95
105
|
export declare function collectInventory(env: Env): Promise<Inventory>;
|
|
96
106
|
/** Skills this computer's config copy brings to the machine: ~/.claude/skills and ~/.codex/skills (not Codex's bundled .system). */
|
|
@@ -99,7 +109,7 @@ export declare function countSkills(home: string): number;
|
|
|
99
109
|
* The fixed task: agent-setup.ts's "Build the machine" step, extended to recreate this computer. `where` is 'local' for
|
|
100
110
|
* an agent on this computer (it reaches the VM through shard) or 'vm' for the cloud Claude inside the machine VM.
|
|
101
111
|
*/
|
|
102
|
-
export declare function setupTask(where: 'local' | 'vm', key: string, inventory: Inventory): string;
|
|
112
|
+
export declare function setupTask(where: 'local' | 'vm', key: string, inventory: Inventory, capacity?: SetupCapacity): string;
|
|
103
113
|
export interface SetupSummary {
|
|
104
114
|
tools: string[];
|
|
105
115
|
runtimes: string[];
|