windows-development-sandbox 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.sandbox-config.example.json +2 -1
- package/config.example.json +1 -0
- package/dist/cli/boot-progress.d.ts +22 -0
- package/dist/cli/boot-progress.js +59 -0
- package/dist/cli/commands/launch.d.ts +5 -2
- package/dist/cli/commands/launch.js +77 -22
- package/dist/cli/commands/maintenance.js +36 -3
- package/dist/cli/commands/project.d.ts +10 -6
- package/dist/cli/commands/project.js +42 -3
- package/dist/cli/commands/restart.js +17 -7
- package/dist/cli/commands/sessions.js +2 -1
- package/dist/cli/manifest.js +1 -1
- package/dist/cli/progress.d.ts +15 -0
- package/dist/cli/progress.js +52 -0
- package/dist/core/config.js +1 -1
- package/dist/core/local-config.d.ts +16 -1
- package/dist/core/local-config.js +15 -9
- package/dist/core/project-policy.d.ts +50 -3
- package/dist/core/project-policy.js +90 -4
- package/dist/core/schema/config.d.ts +3 -2
- package/dist/core/schema/config.js +1 -1
- package/dist/core/schema/projects.d.ts +1 -0
- package/dist/core/schema/projects.js +1 -0
- package/dist/core/schema/sandbox.d.ts +1 -1
- package/dist/core/schema/sandbox.js +1 -1
- package/dist/core/types.d.ts +18 -1
- package/dist/core/workspace.d.ts +21 -4
- package/dist/core/workspace.js +87 -26
- package/dist/guest/env.d.ts +6 -2
- package/dist/guest/env.js +15 -3
- package/dist/guest/main.js +5 -1
- package/dist/guest/toolchains/index.js +29 -3
- package/dist/host/channel.d.ts +2 -2
- package/dist/host/channel.js +3 -2
- package/dist/host/generate.d.ts +1 -1
- package/dist/host/generate.js +13 -6
- package/dist/host/join.d.ts +7 -0
- package/dist/host/join.js +23 -9
- package/dist/host/launch/mappings.d.ts +7 -2
- package/dist/host/launch/mappings.js +64 -18
- package/dist/host/launch/projects.d.ts +2 -0
- package/dist/host/launch/projects.js +11 -5
- package/dist/host/projects.d.ts +25 -2
- package/dist/host/projects.js +54 -5
- package/dist/runtime/daemon.js +71 -9
- package/dist/runtime/db.js +33 -3
- package/dist/runtime/main.js +191 -30
- package/dist/runtime/netproxy.js +15 -3
- package/dist/runtime/view.js +262 -128
- package/dist/selftest/channel.test.js +38 -2
- package/dist/selftest/fixtures.d.ts +2 -1
- package/dist/selftest/index.js +2 -1
- package/dist/selftest/live-terminal.test.d.ts +2 -0
- package/dist/selftest/live-terminal.test.js +169 -0
- package/dist/selftest/local-config.test.js +28 -3
- package/dist/selftest/mirror.test.js +36 -7
- package/dist/selftest/projects.test.js +76 -7
- package/dist/shared/console-input.d.ts +11 -0
- package/dist/shared/console-input.js +61 -0
- package/dist/shared/download.d.ts +3 -0
- package/dist/shared/download.js +25 -2
- package/dist/shared/guest-paths.d.ts +27 -0
- package/dist/shared/guest-paths.js +68 -0
- package/dist/shared/projects.d.ts +27 -4
- package/dist/shared/projects.js +72 -13
- package/dist/shared/pty-client.d.ts +17 -0
- package/dist/shared/pty-client.js +65 -8
- package/dist/shared/schema.d.ts +24 -0
- package/dist/shared/schema.js +12 -0
- package/package.json +1 -1
- package/sandbox/ptyhost.cs +60 -3
- package/schemas/config.schema.json +15 -1
- package/schemas/sandbox-config.schema.json +4 -3
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"rust": { "enabled": false }
|
|
9
9
|
},
|
|
10
10
|
"mappings": [
|
|
11
|
-
{ "host": "
|
|
11
|
+
{ "host": "./data", "guest": "C:\\Data", "readOnly": true, "optional": true },
|
|
12
|
+
{ "host": "D:\\Assets\\textures", "readOnly": true, "optional": true }
|
|
12
13
|
],
|
|
13
14
|
"scripts": []
|
|
14
15
|
}
|
package/config.example.json
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SandboxStatus } from '../core/types.js';
|
|
2
|
+
/** One progress line: `line` is shown, `key` changes only when the step or what runs changes, `what` names it. */
|
|
3
|
+
export interface Shown {
|
|
4
|
+
line: string;
|
|
5
|
+
key: string;
|
|
6
|
+
what: string;
|
|
7
|
+
}
|
|
8
|
+
/** "[3/8] Developer tools: downloading rust 40%, setting up go (2 of 6 done)". */
|
|
9
|
+
export declare const bootLine: (status: SandboxStatus | null, fallback?: string) => Shown;
|
|
10
|
+
/** Nothing new for a while: say so, and where the details are. */
|
|
11
|
+
export declare const stillWorking: (what: string, ms: number) => string;
|
|
12
|
+
export declare const readyIn: (ms: number) => string;
|
|
13
|
+
/** The wanted tools that are not downloaded yet (`has` says whether a folder in <data>\tools exists), with their rough size. */
|
|
14
|
+
export declare const pendingDownloads: (wanted: string[], has: (dir: string) => boolean) => {
|
|
15
|
+
names: string[];
|
|
16
|
+
mb: number;
|
|
17
|
+
};
|
|
18
|
+
/** The heads-up before the wait, or null when everything is already downloaded. first: nothing was ever installed. */
|
|
19
|
+
export declare const firstStartNotice: (first: boolean, pending: {
|
|
20
|
+
names: string[];
|
|
21
|
+
mb: number;
|
|
22
|
+
}) => string | null;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// What the terminal says while a sandbox starts: the first-start heads-up (what is downloaded, roughly how much) and
|
|
2
|
+
// one line of progress from the guest's status. Pure: the status is guest-written, so every bit of it is made safe
|
|
3
|
+
// and short here.
|
|
4
|
+
import { plainText } from '../core/util.js';
|
|
5
|
+
import { elapsed } from './progress.js';
|
|
6
|
+
const clean = (text, max) => plainText(text).replace(/\s+/g, ' ').trim().slice(0, max);
|
|
7
|
+
/** "[3/8] Developer tools: downloading rust 40%, setting up go (2 of 6 done)". */
|
|
8
|
+
export const bootLine = (status, fallback = 'Starting Windows Sandbox') => {
|
|
9
|
+
if (!status)
|
|
10
|
+
return { line: fallback, key: fallback, what: fallback.toLowerCase() };
|
|
11
|
+
const p = status.progress;
|
|
12
|
+
if (!p) {
|
|
13
|
+
const line = clean(status.message, 200) || fallback;
|
|
14
|
+
return { line, key: line, what: line };
|
|
15
|
+
}
|
|
16
|
+
const title = clean(p.title, 60);
|
|
17
|
+
const items = p.active.slice(0, 8).map((a) => ({ name: clean(a.name, 40), percent: a.percent }));
|
|
18
|
+
const loading = items.filter((a) => a.percent !== undefined && a.percent < 100);
|
|
19
|
+
const others = items.filter((a) => !loading.includes(a));
|
|
20
|
+
const parts = [
|
|
21
|
+
...(loading.length ? ['downloading ' + loading.map((a) => a.name + ' ' + a.percent + '%').join(', ')] : []),
|
|
22
|
+
...(others.length ? ['setting up ' + others.map((a) => a.name).join(', ')] : []),
|
|
23
|
+
];
|
|
24
|
+
const count = p.total > 0 && items.length ? ' (' + Math.min(p.done, p.total) + ' of ' + p.total + ' done)' : '';
|
|
25
|
+
const head = '[' + p.step + '/' + p.steps + '] ' + title;
|
|
26
|
+
return {
|
|
27
|
+
line: (head + (parts.length ? ': ' + parts.join('; ') : '') + count).slice(0, 240),
|
|
28
|
+
key: head + '|' + items.map((a) => a.name).sort().join(','),
|
|
29
|
+
what: items.length ? items.map((a) => a.name).join(', ') : title,
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
/** Nothing new for a while: say so, and where the details are. */
|
|
33
|
+
export const stillWorking = (what, ms) => 'Still working on ' + clean(what, 80) + ' (' + Math.max(1, Math.round(ms / 60000)) + ' min). Full log: sandbox logs -f';
|
|
34
|
+
export const readyIn = (ms) => 'Ready in ' + elapsed(ms) + '.';
|
|
35
|
+
// ---- the first-start heads-up ----------------------------------------------------------------------------------------
|
|
36
|
+
/** What a first start downloads, by setting name: its folder in <data>\tools, rough size and a readable name. */
|
|
37
|
+
const DOWNLOADS = {
|
|
38
|
+
node: { dir: 'nvm', mb: 30 }, python: { dir: 'python', mb: 30 }, rust: { dir: 'rust', mb: 300 }, msvc: { dir: 'msvc', mb: 1500, label: 'MSVC' },
|
|
39
|
+
cpp: { dir: 'mingw', mb: 270, label: 'C/C++' }, dotnet: { dir: 'dotnet', mb: 250, label: '.NET' }, go: { dir: 'go', mb: 80 }, java: { dir: 'java', mb: 200 },
|
|
40
|
+
ruby: { dir: 'ruby', mb: 100 }, php: { dir: 'php', mb: 30 }, deno: { dir: 'deno', mb: 40 }, pwsh: { dir: 'pwsh', mb: 100, label: 'PowerShell 7' },
|
|
41
|
+
vscode: { dir: 'vscode', mb: 150, label: 'VS Code' },
|
|
42
|
+
postgres: { dir: 'postgres', mb: 330, label: 'PostgreSQL' }, mysql: { dir: 'mariadb', mb: 90, label: 'MariaDB' }, mongodb: { dir: 'mongodb', mb: 450, label: 'MongoDB' },
|
|
43
|
+
redis: { dir: 'redis', mb: 10 }, dynamodb: { dir: 'dynamodb-local', mb: 80, label: 'DynamoDB Local' },
|
|
44
|
+
};
|
|
45
|
+
/** The wanted tools that are not downloaded yet (`has` says whether a folder in <data>\tools exists), with their rough size. */
|
|
46
|
+
export const pendingDownloads = (wanted, has) => {
|
|
47
|
+
const missing = [...new Set(wanted)].filter((w) => Object.hasOwn(DOWNLOADS, w) && !has(DOWNLOADS[w].dir));
|
|
48
|
+
return { names: missing.map((w) => DOWNLOADS[w].label ?? w), mb: missing.reduce((sum, w) => sum + DOWNLOADS[w].mb, 0) };
|
|
49
|
+
};
|
|
50
|
+
const roughSize = (mb) => (mb >= 950 ? (Math.round(mb / 100) / 10) + ' GB' : Math.max(10, Math.round(mb / 10) * 10) + ' MB');
|
|
51
|
+
/** The heads-up before the wait, or null when everything is already downloaded. first: nothing was ever installed. */
|
|
52
|
+
export const firstStartNotice = (first, pending) => {
|
|
53
|
+
if (!first && !pending.names.length)
|
|
54
|
+
return null;
|
|
55
|
+
const what = pending.names.length ? ' (about ' + roughSize(pending.mb) + ': ' + pending.names.slice(0, 8).join(', ') + (pending.names.length > 8 ? ', ...' : '') + ')' : '';
|
|
56
|
+
return first
|
|
57
|
+
? 'First start: the sandbox downloads the tools this project needs once' + what + '. This can take 5-20 minutes depending on your connection; later starts take about a minute.'
|
|
58
|
+
: 'New tools for this project: the sandbox downloads them once' + what + '. This can take a few minutes; later starts skip it.';
|
|
59
|
+
};
|
|
@@ -6,8 +6,11 @@ import { type Joined } from './project.js';
|
|
|
6
6
|
export declare const attachHere: (ctx: Context, session: Parameters<typeof attachTerminal>[0]) => Promise<number>;
|
|
7
7
|
/** The sandbox writes sandbox.json a few seconds into its boot; a second launch right after the first waits for it. */
|
|
8
8
|
export declare const runningSandboxStatus: (dataPath: string) => Promise<SandboxStatus>;
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Waits for the boot with its current step and what it sets up on one line (plain lines without a terminal).
|
|
11
|
+
* timed: say how long it took (a first start, where that is worth knowing).
|
|
12
|
+
*/
|
|
13
|
+
export declare const waitForBoot: (dataPath: string, launchId: string | null, timed?: boolean) => Promise<SandboxStatus>;
|
|
11
14
|
/** The running sandbox, once it is ready. */
|
|
12
15
|
export declare const readySandbox: (dataPath: string) => Promise<SandboxStatus>;
|
|
13
16
|
/**
|
|
@@ -12,7 +12,7 @@ import { generateSandbox } from '../../host/generate.js';
|
|
|
12
12
|
import { findSecretFiles } from '../../host/secrets.js';
|
|
13
13
|
import { checkRepo, saveRepoSnapshot } from '../../host/repo-guard.js';
|
|
14
14
|
import { assertCanLaunch, insideSandbox, isSandboxRunning, probeHost, spawnDetached, startSandbox } from '../../host/platform.js';
|
|
15
|
-
import { NeedsRestart, rememberProject, startRegistry } from '../../host/projects.js';
|
|
15
|
+
import { NeedsRestart, readKnown, rememberProject, startRegistry } from '../../host/projects.js';
|
|
16
16
|
import { ensureConpty, ensureTerminal } from '../../host/terminal.js';
|
|
17
17
|
import { planView, saveActiveViews, viewSkipped, WorkspaceView } from '../../host/workspace-view/index.js';
|
|
18
18
|
import { saveForwards } from '../../host/port-forward.js';
|
|
@@ -23,14 +23,18 @@ import { hideSandboxWindow, showSandboxWindow } from '../../host/window.js';
|
|
|
23
23
|
import { readSandboxStatus } from '../../shared/rundir.js';
|
|
24
24
|
import { isWithin } from '../../shared/projects.js';
|
|
25
25
|
import { syncBeforeLaunch, watcherRunning } from '../../sync/watcher.js';
|
|
26
|
-
import { protectionsOf } from '../../core/project-policy.js';
|
|
26
|
+
import { BOOT_SETTINGS, protectionsOf } from '../../core/project-policy.js';
|
|
27
|
+
import { LOCAL_CONFIG_NAME } from '../../core/local-config.js';
|
|
27
28
|
import { assertSafeMapping, assertSafeWorkspace } from '../../core/safe-paths.js';
|
|
28
|
-
import { errMsg, fail, isRootPath, sleep } from '../../core/util.js';
|
|
29
|
+
import { dirExists, errMsg, fail, fileExists, isRootPath, sleep } from '../../core/util.js';
|
|
30
|
+
import { planBootProjects } from '../../host/launch/projects.js';
|
|
29
31
|
import { syncAgentCommands } from './agent-commands.js';
|
|
30
32
|
import { askYesNo } from '../../core/prompt.js';
|
|
31
|
-
import { resolveRun } from '../../core/workspace.js';
|
|
33
|
+
import { autoGuestWorkspace, placeInRoot, resolveRun } from '../../core/workspace.js';
|
|
34
|
+
import { planMirrorWorkspace } from '../../shared/guest-paths.js';
|
|
32
35
|
import { c } from '../colors.js';
|
|
33
|
-
import { working } from '../progress.js';
|
|
36
|
+
import { follow, working } from '../progress.js';
|
|
37
|
+
import { bootLine, firstStartNotice, pendingDownloads, readyIn, stillWorking } from '../boot-progress.js';
|
|
34
38
|
import { requireOnPc, sandboxAlive, table } from './context.js';
|
|
35
39
|
import _ from 'lodash';
|
|
36
40
|
import { hereDir, joinHere, readyHere } from './project.js';
|
|
@@ -45,8 +49,11 @@ export const attachHere = (ctx, session) => {
|
|
|
45
49
|
};
|
|
46
50
|
/** Nothing has been installed into this data set yet: the first start downloads every toolchain once. */
|
|
47
51
|
const firstBoot = (dataPath) => !fs.existsSync(path.join(dataPath, 'tools', 'resolved-versions.json'));
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
/** The first-start heads-up (also when this project needs tools not downloaded yet), or null. */
|
|
53
|
+
const FIRST_BOOT = (dataPath, generated, config) => {
|
|
54
|
+
const wanted = ['node', ...Object.entries(generated.toolchains).filter(([, d]) => d.on).map(([name]) => name), ...(config.toolchains.vscode.enabled ? ['vscode'] : []), ...generated.databases.map((d) => d.database)];
|
|
55
|
+
return firstStartNotice(firstBoot(dataPath), pendingDownloads(wanted, (dir) => fs.existsSync(path.join(dataPath, 'tools', dir))));
|
|
56
|
+
};
|
|
50
57
|
/** The sandbox writes sandbox.json a few seconds into its boot; a second launch right after the first waits for it. */
|
|
51
58
|
export const runningSandboxStatus = async (dataPath) => {
|
|
52
59
|
for (let i = 0; i < 60; i++) {
|
|
@@ -57,10 +64,18 @@ export const runningSandboxStatus = async (dataPath) => {
|
|
|
57
64
|
}
|
|
58
65
|
return fail('A Windows Sandbox is running that this tool did not start (or that uses another storage.dataName).', 'Close that sandbox window first, then run sandbox claude again.');
|
|
59
66
|
};
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Waits for the boot with its current step and what it sets up on one line (plain lines without a terminal).
|
|
69
|
+
* timed: say how long it took (a first start, where that is worth knowing).
|
|
70
|
+
*/
|
|
71
|
+
export const waitForBoot = async (dataPath, launchId, timed = false) => {
|
|
72
|
+
let status = null;
|
|
73
|
+
const start = Date.now();
|
|
74
|
+
const ready = await follow(() => bootLine(status), () => waitForReady(dataPath, launchId, sandboxAlive, () => undefined, (s) => { if (s.phase !== 'failed')
|
|
75
|
+
status = s; }), { stall: stillWorking });
|
|
76
|
+
if (timed)
|
|
77
|
+
console.log(readyIn(Date.now() - start));
|
|
78
|
+
return ready;
|
|
64
79
|
};
|
|
65
80
|
/** The running sandbox, once it is ready. */
|
|
66
81
|
export const readySandbox = async (dataPath) => {
|
|
@@ -70,7 +85,8 @@ export const readySandbox = async (dataPath) => {
|
|
|
70
85
|
/** Waits for a session to start (its project's setup, the agent's install) with its progress on a spinner. */
|
|
71
86
|
const waitForStart = (ctx, requestId, what) => {
|
|
72
87
|
let step = 'starting';
|
|
73
|
-
|
|
88
|
+
const shown = () => ({ line: what + ': ' + step, key: step, what: step });
|
|
89
|
+
return follow(shown, () => waitForSession(ctx.dataPath, requestId, sandboxAlive, 60 * 60 * 1000, (m) => { step = m; }), { stall: stillWorking });
|
|
74
90
|
};
|
|
75
91
|
/**
|
|
76
92
|
* Starts (or picks back up) a session in the folder `joined` describes. attach: show it in this terminal.
|
|
@@ -216,6 +232,14 @@ const maybeAddRoot = async (ctx, run) => {
|
|
|
216
232
|
addToList(ctx, ['projects', 'roots'], parent);
|
|
217
233
|
run.config.projects.roots.push(parent);
|
|
218
234
|
console.log('Added ' + parent + ' to projects.roots (sandbox projects roots remove ' + parent + ' undoes it).');
|
|
235
|
+
// From now on this project is under the root's folder inside the sandbox, from this start on (one move, not two).
|
|
236
|
+
const before = run.config.guestWorkspace;
|
|
237
|
+
const derived = [autoGuestWorkspace(run.workspaceHost), planMirrorWorkspace(run.workspaceHost, autoGuestWorkspace(run.workspaceHost)).guest];
|
|
238
|
+
// In place: the caller holds run.config already.
|
|
239
|
+
if (derived.some((g) => g.toLowerCase() === before.toLowerCase()))
|
|
240
|
+
run.config.guestWorkspace = placeInRoot(run.config, run.workspaceHost, Boolean(run.mirror), run.notes).guestWorkspace;
|
|
241
|
+
if (run.config.guestWorkspace !== before)
|
|
242
|
+
console.log('Inside the sandbox this project is now at ' + run.config.guestWorkspace + ' (was ' + before + '): git worktrees made at the old place need `git worktree repair` once.');
|
|
219
243
|
}
|
|
220
244
|
if (answer === 'no')
|
|
221
245
|
addToList(ctx, ['projects', 'declinedRoots'], parent);
|
|
@@ -263,6 +287,32 @@ const prepare = async (ctx, run) => {
|
|
|
263
287
|
const filter = await prepareWorkspaceView(ctx, run);
|
|
264
288
|
return { account, terminal: terminal.message + '; pseudo console: ' + conpty, history, pruned: describePrune(pruneStorage(storageRoot)), filter };
|
|
265
289
|
};
|
|
290
|
+
/**
|
|
291
|
+
* The other projects mapped at boot (a restart's, --session's, projects.all's, and the known projects in your roots),
|
|
292
|
+
* with their effective configs: only approved .sandbox-config.json files and the looser values you allowed for each
|
|
293
|
+
* (nothing is asked here). The sandbox boots with what they want too (generate.ts).
|
|
294
|
+
*/
|
|
295
|
+
const bootWantsFor = async (ctx, run, extraProjects) => {
|
|
296
|
+
const { roots } = run.config.projects;
|
|
297
|
+
const planned = planBootProjects({ run, packageRoot: ctx.packageRoot, extraProjects }).projects.map((p) => p.host);
|
|
298
|
+
const inRoots = readKnown(run.storageRoot).map((k) => k.host).filter((h) => roots.some((r) => isWithin(path.resolve(r), h)));
|
|
299
|
+
const folders = _.uniqBy([...(extraProjects ?? []).map((p) => path.resolve(p)), ...planned, ...inRoots], (f) => f.toLowerCase())
|
|
300
|
+
.filter((f) => !isWithin(run.workspaceHost, f) && dirExists(f)).slice(0, 200);
|
|
301
|
+
const projects = [];
|
|
302
|
+
const notes = [];
|
|
303
|
+
for (const folder of folders) {
|
|
304
|
+
try {
|
|
305
|
+
const other = await resolveRun({ packageRoot: ctx.packageRoot, loaded: ctx.loaded, cwd: folder, workspace: folder, noAgent: true, noLocalConfig: false, trustConfig: false, scripts: [] });
|
|
306
|
+
projects.push({ name: path.basename(folder), config: other.config });
|
|
307
|
+
}
|
|
308
|
+
catch (e) {
|
|
309
|
+
if (fileExists(path.join(folder, LOCAL_CONFIG_NAME)))
|
|
310
|
+
notes.push('What ' + path.basename(folder) + ' asks for at boot is not included: ' + errMsg(e).split('\n')[0]);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const keepOff = BOOT_SETTINGS.filter((s) => _.get(ctx.loaded.config, s) === true && _.get(run.config, s) === false);
|
|
314
|
+
return { wants: { projects, keepOff }, notes };
|
|
315
|
+
};
|
|
266
316
|
export const launch = async (ctx, generateOnly = false, extras = {}) => {
|
|
267
317
|
const { options, loaded, packageRoot, dataPath, storageRoot } = ctx;
|
|
268
318
|
// attach.auto: an agent started from an interactive terminal attaches it, as with -t.
|
|
@@ -303,8 +353,11 @@ export const launch = async (ctx, generateOnly = false, extras = {}) => {
|
|
|
303
353
|
if (!generateOnly && !extras.extraProjects)
|
|
304
354
|
await maybeAddRoot(ctx, run);
|
|
305
355
|
const prepared = generateOnly ? null : await prepare(ctx, run);
|
|
306
|
-
const
|
|
307
|
-
|
|
356
|
+
const bootWants = await bootWantsFor(ctx, run, extras.extraProjects);
|
|
357
|
+
const generated = generateSandbox({ packageRoot, run, harnessArgs: options.passthrough, probe: probeHost(config, run.harnessName), extraProjects: extras.extraProjects, autoSession: extras.autoSession, bootWants: bootWants.wants });
|
|
358
|
+
// What the sandbox boots with (the launch folder's settings plus what the other projects at boot want).
|
|
359
|
+
const booted = generated.bootConfig;
|
|
360
|
+
for (const note of [...run.notes, ...bootWants.notes, ...generated.projects.notes, ...generated.mappingNotes])
|
|
308
361
|
console.log(note);
|
|
309
362
|
const secretFiles = config.secrets.warnFiles ? findSecretFiles(run.workspaceHost) : [];
|
|
310
363
|
if (secretFiles.length) {
|
|
@@ -314,7 +367,7 @@ export const launch = async (ctx, generateOnly = false, extras = {}) => {
|
|
|
314
367
|
const { roots, projects } = generated.projects;
|
|
315
368
|
table([
|
|
316
369
|
['Workspace', run.workspaceHost + ' -> ' + config.guestWorkspace + (config.readOnly ? ' (read-only)' : '')],
|
|
317
|
-
['Agent', run.harnessName + (prepared?.account ? ' (account: ' + prepared.account + ')' : '') + ' Memory: ' + generated.memoryMb + ' MB CPU: ' + (config.sandbox.cpuLimit === 'none' ? 'all' : 'max ' + config.sandbox.cpuLimit + '%') + ' Network: ' +
|
|
370
|
+
['Agent', run.harnessName + (prepared?.account ? ' (account: ' + prepared.account + ')' : '') + ' Memory: ' + generated.memoryMb + ' MB CPU: ' + (config.sandbox.cpuLimit === 'none' ? 'all' : 'max ' + config.sandbox.cpuLimit + '%') + ' Network: ' + booted.sandbox.networking + (booted.sandbox.vGpu ? ' vGPU' : '') + (config.sandbox.headless ? ' HEADLESS' : '')],
|
|
318
371
|
['Node', run.versions.node.spec + ' (' + run.versions.node.source + ') Python: ' + run.versions.python.spec + ' (' + run.versions.python.source + ')'],
|
|
319
372
|
...(prepared ? [['Ignored files', prepared.filter], ['Terminal', prepared.terminal], ['History', prepared.history], ['Cleanup', prepared.pruned]] : []),
|
|
320
373
|
['Toolchains', Object.entries(generated.toolchains).filter(([, d]) => d.on).map(([name, d]) => name + ' (' + d.reason + ')').join(', ') || 'Node only (nothing else used by this project)'],
|
|
@@ -326,7 +379,7 @@ export const launch = async (ctx, generateOnly = false, extras = {}) => {
|
|
|
326
379
|
['Secrets', !config.secrets.strip ? 'NOT stripped (secrets.strip is off)' : generated.profile.secretsRemoved.length ? 'kept on this PC: ' + generated.profile.secretsRemoved.join(', ') + ' (secrets.allow lets one through)' : 'none found in what is copied'],
|
|
327
380
|
...roots.map((r, i) => [i ? '' : 'Project roots', r.host + ' -> ' + r.guest + ' (every project in it opens without a restart)']),
|
|
328
381
|
...projects.map((p, i) => [i ? '' : 'Projects', p.host + ' -> ' + p.guest]),
|
|
329
|
-
...generated.mappings.slice(
|
|
382
|
+
...generated.mappings.slice(2).filter((m) => m.guest !== config.guestWorkspace && ![...roots, ...projects].some((p) => p.guest === m.guest)).map((m) => [' map ' + (m.readOnly ? 'ro' : 'rw'), m.host + ' -> ' + m.guest + (m.label ? ' (' + m.label + ')' : '')]),
|
|
330
383
|
['Generated', generated.wsbPath],
|
|
331
384
|
]);
|
|
332
385
|
if (generateOnly)
|
|
@@ -334,7 +387,8 @@ export const launch = async (ctx, generateOnly = false, extras = {}) => {
|
|
|
334
387
|
// Every folder the sandbox can write is snapshotted before it starts (the guard compares against this), and the
|
|
335
388
|
// projects it starts with are recorded host-side for the commands that run later.
|
|
336
389
|
saveRepoSnapshot(dataPath, run.workspaceHost, projects.map((p) => p.host), roots.map((r) => r.host));
|
|
337
|
-
startRegistry(storageRoot, { launchId: generated.launchId, protections: protectionsOf(
|
|
390
|
+
startRegistry(storageRoot, { launchId: generated.launchId, protections: protectionsOf(booted), primary: { host: run.workspaceHost, guest: config.guestWorkspace }, roots, boot: projects,
|
|
391
|
+
atBoot: { memoryMb: generated.memoryMb, mappings: generated.mappings, widened: generated.widened } });
|
|
338
392
|
rememberProject(storageRoot, run.workspaceHost);
|
|
339
393
|
startLife(storageRoot, dataPath, config.sandbox.keepAwake);
|
|
340
394
|
startSandbox(generated.wsbPath);
|
|
@@ -343,10 +397,11 @@ export const launch = async (ctx, generateOnly = false, extras = {}) => {
|
|
|
343
397
|
spawnDetached(ctx.entry, ['_watch', ...(options.config ? ['--config', path.resolve(options.config)] : [])]);
|
|
344
398
|
// Hiding runs alongside; a failure to find the window must never break the launch or an attach.
|
|
345
399
|
const hidden = config.sandbox.headless ? hideSandboxWindow(storageRoot).catch(() => false) : null;
|
|
400
|
+
const headsUp = FIRST_BOOT(dataPath, generated, config);
|
|
346
401
|
if (options.terminal || options.noAgent || extras.waitUntilReady) {
|
|
347
|
-
if (
|
|
348
|
-
console.log(
|
|
349
|
-
await waitForBoot(dataPath, generated.launchId);
|
|
402
|
+
if (headsUp)
|
|
403
|
+
console.log(headsUp);
|
|
404
|
+
await waitForBoot(dataPath, generated.launchId, Boolean(headsUp));
|
|
350
405
|
if (extras.waitUntilReady)
|
|
351
406
|
return 0;
|
|
352
407
|
// Without an agent there is no session to attach to: the sandbox itself is the result.
|
|
@@ -356,8 +411,8 @@ export const launch = async (ctx, generateOnly = false, extras = {}) => {
|
|
|
356
411
|
}
|
|
357
412
|
return attachHere(ctx, await waitForStart(ctx, generated.launchId, 'Starting ' + run.harnessName));
|
|
358
413
|
}
|
|
359
|
-
if (
|
|
360
|
-
console.log(
|
|
414
|
+
if (headsUp)
|
|
415
|
+
console.log(headsUp + '\nFollow along: sandbox logs -f');
|
|
361
416
|
if (hidden)
|
|
362
417
|
console.log(await hidden ? 'Sandbox running headless. Open the window: sbx-show Attach here: sandbox claude -t' : 'Could not find the sandbox window to hide (it may still be starting); run sandbox hide in a moment.');
|
|
363
418
|
else
|
|
@@ -4,8 +4,9 @@ import path from 'node:path';
|
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import _ from 'lodash';
|
|
6
6
|
import { DEFAULTS, userConfigPath, userDir } from '../../core/config.js';
|
|
7
|
-
import { describeOverrides, isTrusted, LOCAL_CONFIG_NAME, normalizeOverrides, readLocalConfig, trustStorePath } from '../../core/local-config.js';
|
|
8
|
-
import {
|
|
7
|
+
import { allowedAnswer, allowStorePath, applyOverrides, describeOverrides, isTrusted, LOCAL_CONFIG_NAME, loosenedRequests, normalizeOverrides, OUTSIDE_FOLDER, outsideKey, outsideMappings, readLocalConfig, trustStorePath } from '../../core/local-config.js';
|
|
8
|
+
import { guestWorkspaceOf, previewGuest } from '../../core/workspace.js';
|
|
9
|
+
import { assertNoLinks, dirExists, expandPath, fail, plainText, safeText, sleep, writeJson } from '../../core/util.js';
|
|
9
10
|
import { humanBytes } from '../../core/disk.js';
|
|
10
11
|
import { AuditLog } from '../../host/audit/log.js';
|
|
11
12
|
import { readKnown, readRegistry } from '../../host/projects.js';
|
|
@@ -107,8 +108,40 @@ const checkLocalConfig = (ctx) => {
|
|
|
107
108
|
const overrides = normalizeOverrides(file.data, dir, file.path);
|
|
108
109
|
const approved = isTrusted(trustStorePath(ctx.storageRoot), file.path, file.hash);
|
|
109
110
|
console.log(file.path + ': valid. ' + (approved ? 'Approved (this exact version).' : 'Not approved yet: the next launch asks you.') + '\n');
|
|
110
|
-
for (const line of describeOverrides(overrides))
|
|
111
|
+
for (const line of describeOverrides(overrides, dir))
|
|
111
112
|
console.log(' ' + safeText(line));
|
|
113
|
+
// What a launch from here would do with each folder and each looser setting, without launching.
|
|
114
|
+
const config = ctx.loaded.config;
|
|
115
|
+
const store = allowStorePath(ctx.storageRoot);
|
|
116
|
+
const workspaceGuest = guestWorkspaceOf(applyOverrides(config, overrides), dir).guest;
|
|
117
|
+
const outside = outsideMappings(dir, overrides.mappings);
|
|
118
|
+
const policy = config.projects.allowOutsideMappings;
|
|
119
|
+
const rows = (overrides.mappings ?? []).map((m) => {
|
|
120
|
+
const where = previewGuest(m, dir, workspaceGuest);
|
|
121
|
+
const status = !dirExists(m.host) ? (m.optional ? 'not on this PC: skipped' : c.red('the folder does not exist: create it, fix the path, or add "optional": true'))
|
|
122
|
+
: !outside.includes(m) ? (m.guest ? 'inside the project' : 'inside the project (already in the sandbox there)')
|
|
123
|
+
: policy === true ? 'outside the project, allowed (projects.allowOutsideMappings is true)'
|
|
124
|
+
: policy === false ? c.red('outside the project: refused (projects.allowOutsideMappings is false)')
|
|
125
|
+
: allowedAnswer(store, file.path, OUTSIDE_FOLDER, outsideKey(m)) === true ? 'outside the project, allowed (you said yes)'
|
|
126
|
+
: c.yellow('outside the project: the next interactive launch asks you');
|
|
127
|
+
return [(m.readOnly ? 'ro ' : 'rw ') + safeText(m.host), '-> ' + safeText(where.guest), status];
|
|
128
|
+
});
|
|
129
|
+
if (rows.length) {
|
|
130
|
+
console.log('\nFolders (where they appear in the sandbox):');
|
|
131
|
+
table(rows);
|
|
132
|
+
}
|
|
133
|
+
for (const m of overrides.mappings ?? []) {
|
|
134
|
+
const note = dirExists(m.host) ? previewGuest(m, dir, workspaceGuest).note : null;
|
|
135
|
+
if (note)
|
|
136
|
+
console.log(' ' + safeText(note));
|
|
137
|
+
}
|
|
138
|
+
const looser = loosenedRequests(config, overrides);
|
|
139
|
+
if (looser.length)
|
|
140
|
+
console.log('\nLooser than your settings (used only if you allow it for this project):');
|
|
141
|
+
for (const a of looser) {
|
|
142
|
+
const answer = allowedAnswer(store, file.path, a.key, a.wanted);
|
|
143
|
+
console.log(' ' + a.key + ' = ' + JSON.stringify(a.wanted) + ': ' + (answer === true ? 'allowed (you said yes)' : answer === false ? 'not used (you said no)' : 'the next interactive launch asks you'));
|
|
144
|
+
}
|
|
112
145
|
return 0;
|
|
113
146
|
};
|
|
114
147
|
export const config = (ctx) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SandboxStatus } from '../../core/types.js';
|
|
2
|
+
import { type Missing } from '../../core/project-policy.js';
|
|
2
3
|
import { type RegistryEntry } from '../../host/projects.js';
|
|
3
4
|
import type { ProjectSpecT } from '../../shared/schema.js';
|
|
4
5
|
import { type Context } from './context.js';
|
|
@@ -17,12 +18,15 @@ export interface Joined {
|
|
|
17
18
|
harness: string | null;
|
|
18
19
|
notes: string[];
|
|
19
20
|
}
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
/** restart.ts: the sandbox was restarted for a project in this command. */
|
|
22
|
+
export declare const markRestarted: () => void;
|
|
23
|
+
export declare const joinHere: (ctx: Context, status: SandboxStatus, o?: {
|
|
24
|
+
noRestart?: boolean;
|
|
25
|
+
}) => Promise<Joined>;
|
|
26
|
+
/** The command that restarts the running sandbox with this folder's boot wishes (consent given by --yes). */
|
|
27
|
+
export declare const restartCommand: (ctx: Context, host: string) => string;
|
|
28
|
+
/** Joined without what it wants at boot: what is missing, how to restart with it, and how to always have it. */
|
|
29
|
+
export declare const missingNotes: (ctx: Context, name: string, host: string, missing: Missing, shown?: boolean) => string[];
|
|
26
30
|
/** Waits until the sandbox has set a project up, with its progress on a spinner (plain lines without a terminal). */
|
|
27
31
|
export declare const provisionJoined: (ctx: Context, joined: Joined) => Promise<void>;
|
|
28
32
|
/** joinHere, then (for another project) waits until the sandbox has set it up. */
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
// these, so they all agree: `locateHere` only looks (attach, sessions, status, restart), `joinHere` also makes the
|
|
3
3
|
// folder a project of the running sandbox when it can without a restart (launch, start, exec, db).
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { select } from '@inquirer/prompts';
|
|
6
|
+
import { describeMissing, missingTip } from '../../core/project-policy.js';
|
|
5
7
|
import { askYesNo } from '../../core/prompt.js';
|
|
6
8
|
import { isInsideOrEqual } from '../../core/util.js';
|
|
7
9
|
import { resolveRun } from '../../core/workspace.js';
|
|
8
|
-
import { prepareProject } from '../../host/channel.js';
|
|
10
|
+
import { currentSessions, prepareProject } from '../../host/channel.js';
|
|
9
11
|
import { projectHere, readRegistry } from '../../host/projects.js';
|
|
10
12
|
import { joinRunning } from '../../host/join.js';
|
|
13
|
+
import { c } from '../colors.js';
|
|
11
14
|
import { working } from '../progress.js';
|
|
15
|
+
import { describeSessions } from './restart.js';
|
|
12
16
|
import { guestPathFor, sandboxAlive } from './context.js';
|
|
13
17
|
/** The folder the command is about: --workspace, else the current one. */
|
|
14
18
|
export const hereDir = (ctx) => path.resolve(ctx.options.workspace ?? process.cwd());
|
|
@@ -28,12 +32,16 @@ export const locateHere = (ctx, status) => {
|
|
|
28
32
|
* it): its .sandbox-config.json is approved like at a launch, its protections are checked, and it is made visible.
|
|
29
33
|
* Throws NeedsRestart (host/projects.ts) when that is impossible without a restart.
|
|
30
34
|
*/
|
|
31
|
-
|
|
35
|
+
let restartedOnce = false;
|
|
36
|
+
/** restart.ts: the sandbox was restarted for a project in this command. */
|
|
37
|
+
export const markRestarted = () => { restartedOnce = true; };
|
|
38
|
+
export const joinHere = async (ctx, status, o = {}) => {
|
|
32
39
|
const { options, loaded, packageRoot, storageRoot, dataPath } = ctx;
|
|
33
40
|
const dir = hereDir(ctx);
|
|
34
41
|
// A sandbox started before projects existed: only its launch folder (guestPathFor explains why otherwise).
|
|
35
42
|
if (!readRegistry(storageRoot))
|
|
36
43
|
return { project: null, guestDir: guestPathFor(dir, status.workspaceHost, status.guestWorkspace), spec: null, harness: null, notes: [] };
|
|
44
|
+
let shown = false;
|
|
37
45
|
const joined = await joinRunning({
|
|
38
46
|
storageRoot, dataPath, packageRoot, hostDir: dir, wsbShare: loaded.config.projects.wsbShare,
|
|
39
47
|
resolve: (root) => resolveRun({
|
|
@@ -41,10 +49,41 @@ export const joinHere = async (ctx, status) => {
|
|
|
41
49
|
noLocalConfig: options.noLocalConfig, trustConfig: options.trustConfig, scripts: options.scripts, databases: options.databases,
|
|
42
50
|
nodeVersion: options.node, pythonVersion: options.python, ask: askYesNo,
|
|
43
51
|
}),
|
|
52
|
+
// Something it wants only comes with a restart: --yes restarts, a terminal asks, anything else continues.
|
|
53
|
+
decide: async (name, missing) => {
|
|
54
|
+
// Once per command: what a restart could not give (see the launch notes) never restarts it again.
|
|
55
|
+
if (o.noRestart || restartedOnce)
|
|
56
|
+
return 'continue';
|
|
57
|
+
if (options.yes)
|
|
58
|
+
return 'restart';
|
|
59
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
60
|
+
return 'continue';
|
|
61
|
+
console.log(c.yellow(describeMissing(name, missing)));
|
|
62
|
+
shown = true;
|
|
63
|
+
const sessions = currentSessions(dataPath);
|
|
64
|
+
return select({
|
|
65
|
+
message: 'What now?',
|
|
66
|
+
choices: [
|
|
67
|
+
{ name: 'Continue anyway (join without them)', value: 'continue' },
|
|
68
|
+
{ name: 'Restart with them (' + (sessions.length ? describeSessions(sessions) + ' will end' : 'no session is running') + ')', value: 'restart' },
|
|
69
|
+
{ name: 'Cancel', value: 'cancel' },
|
|
70
|
+
],
|
|
71
|
+
default: 'continue',
|
|
72
|
+
});
|
|
73
|
+
},
|
|
44
74
|
});
|
|
45
75
|
const harness = joined.run && joined.run.harnessName !== 'none' ? joined.run.harnessName : null;
|
|
46
|
-
|
|
76
|
+
const notes = joined.missing ? [...joined.notes, ...missingNotes(ctx, joined.project.name, joined.project.host, joined.missing, shown)] : joined.notes;
|
|
77
|
+
return { project: joined.project, guestDir: joined.guestDir, spec: joined.spec, harness, notes };
|
|
47
78
|
};
|
|
79
|
+
/** The command that restarts the running sandbox with this folder's boot wishes (consent given by --yes). */
|
|
80
|
+
export const restartCommand = (ctx, host) => 'sandbox ' + (ctx.options.noAgent ? 'start' : ctx.options.harness ?? ctx.loaded.config.defaultHarness) + ' --workspace "' + host + '" --yes';
|
|
81
|
+
/** Joined without what it wants at boot: what is missing, how to restart with it, and how to always have it. */
|
|
82
|
+
export const missingNotes = (ctx, name, host, missing, shown = false) => [
|
|
83
|
+
...(shown ? [] : [c.yellow(describeMissing(name, missing))]),
|
|
84
|
+
'Joined without them. To restart the sandbox with them (its sessions end): ' + restartCommand(ctx, host),
|
|
85
|
+
c.dim(missingTip(missing)),
|
|
86
|
+
];
|
|
48
87
|
/** Waits until the sandbox has set a project up, with its progress on a spinner (plain lines without a terminal). */
|
|
49
88
|
export const provisionJoined = async (ctx, joined) => {
|
|
50
89
|
if (!joined.spec)
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// where the agent supports it) and then starts the new project's session.
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import _ from 'lodash';
|
|
8
|
+
import { missingTip } from '../../core/project-policy.js';
|
|
8
9
|
import { askYesNo } from '../../core/prompt.js';
|
|
9
10
|
import { fail, safeText, sleep } from '../../core/util.js';
|
|
10
11
|
import { currentSessions, execInSandbox } from '../../host/channel.js';
|
|
@@ -44,14 +45,20 @@ export const offerRestart = async (ctx, needs, then) => {
|
|
|
44
45
|
const registry = readRegistry(ctx.storageRoot) ?? fail('The running sandbox was started by an older version, so it cannot take another folder.', 'Stop it (sandbox stop) and start it again from ' + folders[0] + '.');
|
|
45
46
|
const primary = registry.projects.find((p) => p.primary) ?? fail('The running sandbox has no launch folder on record.', 'Stop it (sandbox stop) and start it again.');
|
|
46
47
|
const sessions = currentSessions(ctx.dataPath);
|
|
47
|
-
|
|
48
|
+
// A project that only wants boot settings or folders: you already chose the restart (or gave --yes) when asked.
|
|
49
|
+
const hidden = needs.filter((n) => !n.missing);
|
|
50
|
+
for (const need of hidden)
|
|
48
51
|
console.log(c.yellow('Windows Sandbox cannot add ' + need.hostDir + ' while it runs: ' + need.reason + '.'));
|
|
49
|
-
console.log('A restart maps ' + (
|
|
52
|
+
console.log((hidden.length ? 'A restart maps ' + (hidden.length === 1 ? 'it' : 'them') + ' as well' : 'Restarting with what ' + needs.map((n) => path.basename(n.hostDir)).join(', ') + ' wants') +
|
|
53
|
+
' (and keeps ' + registry.projects.map((p) => p.host).join(', ') + '). ' +
|
|
50
54
|
(sessions.length ? describeSessions(sessions) + ' will end' : 'No session is running') + '; your files, tools, databases and logins are kept. It takes about a minute.');
|
|
51
|
-
for (const parent of _.uniq(
|
|
55
|
+
for (const parent of _.uniq(hidden.map((n) => path.dirname(n.hostDir))))
|
|
52
56
|
console.log(c.dim('No restart next time for folders in ' + parent + ': sandbox projects roots add "' + parent + '"'));
|
|
57
|
+
for (const need of needs)
|
|
58
|
+
if (need.missing)
|
|
59
|
+
console.log(c.dim(missingTip(need.missing)));
|
|
53
60
|
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
54
|
-
if (!ctx.options.yes) {
|
|
61
|
+
if (!ctx.options.yes && hidden.length) {
|
|
55
62
|
if (!interactive)
|
|
56
63
|
fail('Not restarting without your consent.', 'Run it again with --yes to restart: ' + againWithYes(ctx));
|
|
57
64
|
if (!(await askYesNo('Restart the sandbox now?'))) {
|
|
@@ -62,9 +69,10 @@ export const offerRestart = async (ctx, needs, then) => {
|
|
|
62
69
|
const reopen = sessionsToReopen(registry, sessions);
|
|
63
70
|
const resume = reopen.length > 0 && (ctx.options.yes || (await askYesNo('Reopen the ' + reopen.length + ' session(s) afterwards (continuing their conversations where the agent supports it)?', true)));
|
|
64
71
|
await stopAndWait(ctx);
|
|
65
|
-
// Imported here: launch.ts imports this file.
|
|
72
|
+
// Imported here: launch.ts imports this file (and project.ts imports describeSessions).
|
|
66
73
|
const { launch, openSession, readySandbox } = await import('./launch.js');
|
|
67
|
-
const { joinHere } = await import('./project.js');
|
|
74
|
+
const { joinHere, markRestarted } = await import('./project.js');
|
|
75
|
+
markRestarted();
|
|
68
76
|
const keep = registry.projects.filter((p) => !p.primary && p.mechanism !== 'root').map((p) => p.host);
|
|
69
77
|
const relaunch = { ...ctx, options: { ...ctx.options, workspace: primary.host, harness: ctx.options.harness ?? ctx.options.sessions[0]?.harness, terminal: false, noAgent: false, newSession: true, passthrough: [], sessions: [] } };
|
|
70
78
|
await launch(relaunch, false, { extraProjects: [...keep, ...folders], autoSession: false, waitUntilReady: true });
|
|
@@ -73,7 +81,9 @@ export const offerRestart = async (ctx, needs, then) => {
|
|
|
73
81
|
for (const s of reopen) {
|
|
74
82
|
const at = { ...ctx, options: { ...ctx.options, workspace: s.hostDir, harness: s.harness, terminal: false, newSession: true, passthrough: [], sessions: [] } };
|
|
75
83
|
try {
|
|
76
|
-
const joined = await joinHere(at, ready);
|
|
84
|
+
const joined = await joinHere(at, ready, { noRestart: true });
|
|
85
|
+
for (const note of joined.notes)
|
|
86
|
+
console.log(note);
|
|
77
87
|
await openSession(at, ready, joined, { attach: false, harness: s.harness, args: ctx.loaded.config.harnesses[s.harness]?.resumeArguments ?? [], reuse: false });
|
|
78
88
|
}
|
|
79
89
|
catch (e) {
|
|
@@ -161,7 +161,8 @@ const printExecResult = (result) => {
|
|
|
161
161
|
const guestFolderHere = async (ctx, what) => {
|
|
162
162
|
const ready = await readySandbox(ctx.dataPath);
|
|
163
163
|
try {
|
|
164
|
-
|
|
164
|
+
// A command needs no restart for what the project wants at boot: it runs without them (and says so).
|
|
165
|
+
const joined = await joinHere(ctx, ready, { noRestart: true });
|
|
165
166
|
for (const note of joined.notes)
|
|
166
167
|
console.error(note);
|
|
167
168
|
await provisionJoined(ctx, joined);
|
package/dist/cli/manifest.js
CHANGED
|
@@ -57,7 +57,7 @@ const SANDBOX_OPTIONS = [
|
|
|
57
57
|
bool('--window', 'show the sandbox window even if config says headless', { advanced: true }),
|
|
58
58
|
bool('--trust-config', 'approve the folder\'s .sandbox-config.json without asking', { advanced: true }),
|
|
59
59
|
bool('--no-local-config', 'ignore the folder\'s .sandbox-config.json', { advanced: true }),
|
|
60
|
-
bool('--yes', 'when this folder is not in the running sandbox
|
|
60
|
+
bool('--yes', 'when this folder is not in the running sandbox, or wants what it started without (vGPU, folders), restart without asking', { noMenu: true }),
|
|
61
61
|
];
|
|
62
62
|
const AGENT_OPTIONS = [
|
|
63
63
|
text('--harness', 'name', 'agent to run', { choices: 'harnesses' }),
|
package/dist/cli/progress.d.ts
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
export declare const canAnimate: (env: NodeJS.ProcessEnv, isTTY: boolean) => boolean;
|
|
3
3
|
/** "12 s", "2 min 5 s". */
|
|
4
4
|
export declare const elapsed: (ms: number) => string;
|
|
5
|
+
/** One line of progress: `line` is shown, `key` changes when it is worth a new plain line, `what` names what runs. */
|
|
6
|
+
export interface Progressing {
|
|
7
|
+
line: string;
|
|
8
|
+
key: string;
|
|
9
|
+
what: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Follows long work (a sandbox's boot): on a terminal one line redrawn in place with the time so far; without one a
|
|
13
|
+
* plain line each time `key` changes. When nothing changes for `stallMs` it says so (stall(what, ms)). A failure first
|
|
14
|
+
* prints where it stopped, then rethrows.
|
|
15
|
+
*/
|
|
16
|
+
export declare const follow: <T>(show: () => Progressing, work: () => Promise<T>, o: {
|
|
17
|
+
stallMs?: number;
|
|
18
|
+
stall: (what: string, ms: number) => string;
|
|
19
|
+
}) => Promise<T>;
|
|
5
20
|
/**
|
|
6
21
|
* Shows `message` while work runs and removes the line afterwards. The spinner only moves while the event loop is free
|
|
7
22
|
* (waiting on the sandbox); for long synchronous work (measuring folders) the static line is what you see.
|