windows-development-sandbox 1.1.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/README.md +3 -2
- package/config.example.json +39 -5
- package/dist/cli/args.d.ts +3 -0
- package/dist/cli/args.js +3 -1
- package/dist/cli/boot-progress.d.ts +22 -0
- package/dist/cli/boot-progress.js +59 -0
- package/dist/cli/commands/accounts.js +2 -2
- package/dist/cli/commands/agent-commands.js +1 -1
- package/dist/cli/commands/context.js +2 -1
- package/dist/cli/commands/data/clean.js +6 -1
- package/dist/cli/commands/data/collect.js +3 -3
- package/dist/cli/commands/index.d.ts +1 -1
- package/dist/cli/commands/index.js +1 -1
- package/dist/cli/commands/launch.d.ts +5 -2
- package/dist/cli/commands/launch.js +113 -25
- package/dist/cli/commands/maintenance.d.ts +2 -0
- package/dist/cli/commands/maintenance.js +70 -4
- 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 +5 -4
- package/dist/cli/commands/user-config.js +1 -1
- package/dist/cli/index.js +6 -4
- package/dist/cli/manifest.js +6 -1
- package/dist/cli/menu/apps.d.ts +21 -0
- package/dist/cli/menu/apps.js +122 -0
- package/dist/cli/menu/command-form.d.ts +28 -0
- package/dist/cli/menu/command-form.js +219 -0
- package/dist/cli/menu/index.d.ts +26 -0
- package/dist/cli/menu/index.js +180 -0
- package/dist/cli/menu/settings/categories.d.ts +9 -0
- package/dist/cli/menu/settings/categories.js +28 -0
- package/dist/cli/menu/settings/config-file.d.ts +40 -0
- package/dist/cli/menu/settings/config-file.js +71 -0
- package/dist/cli/menu/settings/editors.d.ts +30 -0
- package/dist/cli/menu/settings/editors.js +305 -0
- package/dist/cli/menu/settings/index.d.ts +32 -0
- package/dist/cli/menu/settings/index.js +136 -0
- package/dist/cli/menu/settings/schema-node.d.ts +62 -0
- package/dist/cli/menu/settings/schema-node.js +104 -0
- package/dist/cli/{menu-multi.d.ts → menu/several.d.ts} +10 -2
- package/dist/cli/menu/several.js +102 -0
- package/dist/cli/progress.d.ts +15 -0
- package/dist/cli/progress.js +52 -0
- package/dist/cli/tui/app.d.ts +39 -0
- package/dist/cli/tui/app.js +52 -0
- package/dist/cli/tui/dialogs.d.ts +57 -0
- package/dist/cli/tui/dialogs.js +162 -0
- package/dist/cli/tui/frame.d.ts +18 -0
- package/dist/cli/tui/frame.js +27 -0
- package/dist/cli/tui/index.d.ts +7 -0
- package/dist/cli/tui/index.js +8 -0
- package/dist/cli/tui/list-screen.d.ts +101 -0
- package/dist/cli/tui/list-screen.js +299 -0
- package/dist/cli/tui/terminal.d.ts +37 -0
- package/dist/cli/tui/terminal.js +89 -0
- package/dist/cli/tui/text.d.ts +20 -0
- package/dist/cli/tui/text.js +105 -0
- package/dist/cli/tui/theme.d.ts +45 -0
- package/dist/cli/tui/theme.js +30 -0
- package/dist/core/config.js +10 -7
- package/dist/core/local-config.d.ts +31 -1
- package/dist/core/local-config.js +72 -7
- package/dist/core/project-policy.d.ts +50 -3
- package/dist/core/project-policy.js +90 -4
- package/dist/core/schema/apps.d.ts +40 -0
- package/dist/core/schema/apps.js +27 -0
- package/dist/core/schema/config.d.ts +69 -3
- package/dist/core/schema/config.js +18 -6
- package/dist/core/schema/profile.d.ts +5 -0
- package/dist/core/schema/profile.js +4 -2
- package/dist/core/schema/projects.d.ts +1 -0
- package/dist/core/schema/projects.js +1 -0
- package/dist/core/schema/sandbox.d.ts +29 -1
- package/dist/core/schema/sandbox.js +29 -1
- package/dist/core/schema/toolchains.d.ts +3 -0
- package/dist/core/schema/toolchains.js +6 -1
- package/dist/core/types.d.ts +24 -1
- package/dist/core/util.d.ts +2 -0
- package/dist/core/util.js +2 -0
- package/dist/core/workspace.d.ts +21 -4
- package/dist/core/workspace.js +99 -21
- package/dist/guest/apps/index.d.ts +10 -0
- package/dist/guest/apps/index.js +206 -0
- package/dist/guest/apps/resolvers.d.ts +8 -0
- package/dist/guest/apps/resolvers.js +90 -0
- package/dist/guest/audit-parse.d.ts +7 -0
- package/dist/guest/audit-parse.js +24 -0
- package/dist/guest/audit.d.ts +4 -0
- package/dist/guest/audit.js +47 -0
- package/dist/guest/daemon/relays.d.ts +1 -0
- package/dist/guest/daemon/relays.js +18 -0
- package/dist/guest/daemon.js +4 -0
- package/dist/guest/env.d.ts +7 -2
- package/dist/guest/env.js +17 -3
- package/dist/guest/helpers.d.ts +1 -1
- package/dist/guest/main.js +20 -3
- package/dist/guest/netproxy.d.ts +1 -0
- package/dist/guest/netproxy.js +100 -0
- package/dist/guest/ptyhost.js +14 -4
- package/dist/guest/setup/claude.d.ts +0 -4
- package/dist/guest/setup/claude.js +48 -2
- package/dist/guest/setup/network.d.ts +9 -0
- package/dist/guest/setup/network.js +97 -4
- package/dist/guest/setup/user.js +4 -0
- package/dist/guest/toolchains/index.js +29 -3
- package/dist/guest/toolchains/native/mesa.js +13 -3
- package/dist/guest/toolchains/plan.d.ts +3 -1
- package/dist/guest/toolchains/plan.js +53 -9
- package/dist/guest/toolchains/python.js +26 -2
- package/dist/guest/toolchains/vscode-profile.d.ts +30 -0
- package/dist/guest/toolchains/vscode-profile.js +69 -0
- package/dist/guest/toolchains/vscode.d.ts +17 -0
- package/dist/guest/toolchains/vscode.js +87 -1
- package/dist/host/attach.js +2 -1
- package/dist/host/audit/collect.d.ts +34 -0
- package/dist/host/audit/collect.js +126 -0
- package/dist/host/audit/log.d.ts +46 -0
- package/dist/host/audit/log.js +143 -0
- package/dist/host/channel.d.ts +2 -2
- package/dist/host/channel.js +3 -2
- package/dist/host/generate.d.ts +7 -1
- package/dist/host/generate.js +23 -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 +68 -18
- package/dist/host/launch/projects.d.ts +2 -0
- package/dist/host/launch/projects.js +11 -5
- package/dist/host/launch/stage.js +1 -1
- package/dist/host/motw.d.ts +17 -0
- package/dist/host/motw.js +74 -0
- package/dist/host/platform.js +3 -0
- package/dist/host/port-forward.d.ts +8 -0
- package/dist/host/port-forward.js +37 -0
- package/dist/host/projects.d.ts +25 -2
- package/dist/host/projects.js +54 -5
- package/dist/host/repo-guard.d.ts +10 -3
- package/dist/host/repo-guard.js +142 -14
- package/dist/host/stops.d.ts +67 -0
- package/dist/host/stops.js +126 -0
- package/dist/host/terminal.d.ts +8 -0
- package/dist/host/terminal.js +45 -1
- package/dist/host/workspace-view/index.d.ts +20 -0
- package/dist/host/workspace-view/index.js +53 -0
- package/dist/host/workspace-view/rules.d.ts +26 -0
- package/dist/host/workspace-view/rules.js +93 -0
- package/dist/host/workspace-view/view.d.ts +50 -0
- package/dist/host/workspace-view/view.js +281 -0
- package/dist/runtime/daemon.js +1387 -558
- package/dist/runtime/db.js +199 -14
- package/dist/runtime/main.js +3082 -631
- package/dist/runtime/netproxy.js +25948 -0
- package/dist/runtime/view.js +531 -148
- package/dist/selftest/audit.test.d.ts +2 -0
- package/dist/selftest/audit.test.js +57 -0
- package/dist/selftest/channel.test.js +45 -2
- package/dist/selftest/cli.test.js +47 -17
- package/dist/selftest/fixtures.d.ts +47 -1
- package/dist/selftest/fixtures.js +3 -1
- package/dist/selftest/generate.test.js +7 -2
- package/dist/selftest/index.js +6 -2
- package/dist/selftest/live-network.test.d.ts +2 -0
- package/dist/selftest/live-network.test.js +63 -0
- 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 +42 -4
- package/dist/selftest/mirror.test.js +36 -7
- package/dist/selftest/network.test.js +12 -0
- package/dist/selftest/projects.test.js +77 -8
- package/dist/selftest/repo-guard.test.js +59 -1
- package/dist/selftest/sync.test.js +50 -10
- package/dist/selftest/toolchains.test.js +30 -0
- package/dist/selftest/workspace-view.test.d.ts +2 -0
- package/dist/selftest/workspace-view.test.js +89 -0
- package/dist/shared/apps.d.ts +49 -0
- package/dist/shared/apps.js +114 -0
- 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/netpolicy.d.ts +27 -0
- package/dist/shared/netpolicy.js +51 -0
- package/dist/shared/projects.d.ts +27 -4
- package/dist/shared/projects.js +72 -13
- package/dist/shared/pty-client.d.ts +26 -4
- package/dist/shared/pty-client.js +204 -25
- package/dist/shared/rundir.d.ts +2 -0
- package/dist/shared/rundir.js +2 -0
- package/dist/shared/schema.d.ts +34 -0
- package/dist/shared/schema.js +14 -0
- package/dist/shared/system.d.ts +1 -0
- package/dist/shared/system.js +1 -0
- package/dist/sync/claude-mem.js +3 -2
- package/dist/sync/history.d.ts +6 -0
- package/dist/sync/history.js +26 -11
- package/dist/sync/watcher.js +78 -6
- package/package.json +2 -1
- package/sandbox/cargoshim.cs +56 -0
- package/sandbox/cpucap.cs +33 -0
- package/sandbox/cpucap.ps1 +4 -35
- package/sandbox/logon.cmd +6 -0
- package/sandbox/memlog.ps1 +34 -0
- package/sandbox/pagefile.cs +31 -0
- package/sandbox/pagefile.ps1 +21 -0
- package/sandbox/ptyhost.cs +162 -7
- package/schemas/config.schema.json +318 -3
- package/schemas/sandbox-config.schema.json +129 -14
- package/dist/cli/menu-multi.js +0 -82
- package/dist/cli/menu.d.ts +0 -14
- package/dist/cli/menu.js +0 -290
- package/dist/cli/settings/edit.d.ts +0 -13
- package/dist/cli/settings/edit.js +0 -19
- package/dist/cli/settings/index.d.ts +0 -8
- package/dist/cli/settings/index.js +0 -131
- package/dist/cli/settings/prompts.d.ts +0 -7
- package/dist/cli/settings/prompts.js +0 -48
- package/dist/cli/settings/schema.d.ts +0 -33
- package/dist/cli/settings/schema.js +0 -29
package/dist/cli/menu-multi.js
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
// The menu's "Start several agents": projects, then agents for each, then options; it only builds a command line
|
|
2
|
-
// (sandbox launch --session <agent>@<folder> ...), so "repeat last" works and typing it does the same.
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { checkbox, confirm, input, select } from '@inquirer/prompts';
|
|
6
|
-
import { assertSafeWorkspace } from '../core/safe-paths.js';
|
|
7
|
-
import { expandPath, errMsg } from '../core/util.js';
|
|
8
|
-
import { readKnown } from '../host/projects.js';
|
|
9
|
-
import { isWithin } from '../shared/projects.js';
|
|
10
|
-
import { c, pageSizeFor, shortenMiddle, termWidth } from './colors.js';
|
|
11
|
-
import { commandLine } from './history.js';
|
|
12
|
-
const ADD = '@add';
|
|
13
|
-
/** Folders worth offering: this one, the projects started before, and the folders inside your roots. */
|
|
14
|
-
export const candidateFolders = (ctx, cwd = process.cwd()) => {
|
|
15
|
-
const roots = ctx.loaded.config.projects.roots.map((r) => expandPath(r, ctx.loaded.base));
|
|
16
|
-
const inRoots = roots.flatMap((r) => { try {
|
|
17
|
-
return fs.readdirSync(r, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.isSymbolicLink() && !e.name.startsWith('.')).slice(0, 200).map((e) => path.join(r, e.name));
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
return [];
|
|
21
|
-
} });
|
|
22
|
-
const all = [path.resolve(cwd), ...readKnown(ctx.storageRoot).map((k) => k.host), ...inRoots];
|
|
23
|
-
return all.filter((f, i) => all.findIndex((g) => g.toLowerCase() === f.toLowerCase()) === i && fs.existsSync(f));
|
|
24
|
-
};
|
|
25
|
-
/** The command line for several sessions: one --session per (agent, folder), with the shared options. */
|
|
26
|
-
export const severalAgentsArgv = (sessions, o) => ['launch', ...sessions.flatMap((s) => ['--session', s.harness + '@' + s.folder]), ...(o.headless ? ['--headless'] : []), ...(o.attachFirst ? ['-t'] : [])];
|
|
27
|
-
export const askSeveralAgents = async (ctx) => {
|
|
28
|
-
const here = path.resolve(process.cwd());
|
|
29
|
-
const width = Math.max(20, termWidth() - 14);
|
|
30
|
-
let folders = await checkbox({
|
|
31
|
-
message: 'Which projects? (each gets its sessions; all of them are mapped into one sandbox)',
|
|
32
|
-
pageSize: pageSizeFor(16),
|
|
33
|
-
choices: [
|
|
34
|
-
...candidateFolders(ctx).map((f) => ({ value: f, name: shortenMiddle(f, width) + (isWithin(f, here) && isWithin(here, f) ? c.dim(' (this folder)') : ''), checked: f.toLowerCase() === here.toLowerCase() })),
|
|
35
|
-
{ value: ADD, name: 'Add a folder...' },
|
|
36
|
-
],
|
|
37
|
-
});
|
|
38
|
-
if (folders.includes(ADD)) {
|
|
39
|
-
const extra = await input({
|
|
40
|
-
message: 'Folder to add:',
|
|
41
|
-
validate: (v) => {
|
|
42
|
-
const dir = path.resolve(v.trim());
|
|
43
|
-
if (!v.trim() || !fs.existsSync(dir) || !fs.statSync(dir).isDirectory())
|
|
44
|
-
return 'Not a folder: ' + dir;
|
|
45
|
-
try {
|
|
46
|
-
assertSafeWorkspace(dir, ctx.packageRoot, ctx.storageRoot);
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
catch (e) {
|
|
50
|
-
return errMsg(e);
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
folders = [...folders.filter((f) => f !== ADD), path.resolve(extra.trim())];
|
|
55
|
-
}
|
|
56
|
-
if (!folders.length)
|
|
57
|
-
return null;
|
|
58
|
-
const agents = Object.keys(ctx.loaded.config.harnesses);
|
|
59
|
-
const sessions = [];
|
|
60
|
-
for (const folder of folders) {
|
|
61
|
-
const picked = await checkbox({
|
|
62
|
-
message: 'Agents for ' + shortenMiddle(folder, width - 10) + ' (one session each):',
|
|
63
|
-
pageSize: pageSizeFor(agents.length),
|
|
64
|
-
choices: agents.map((a) => ({ value: a, name: a, checked: a === ctx.loaded.config.defaultHarness })),
|
|
65
|
-
required: true,
|
|
66
|
-
});
|
|
67
|
-
sessions.push(...picked.map((harness) => ({ harness, folder })));
|
|
68
|
-
}
|
|
69
|
-
const headless = await confirm({ message: 'Hide the sandbox window (headless)?', default: ctx.loaded.config.sandbox.headless });
|
|
70
|
-
const attach = await select({
|
|
71
|
-
message: 'Attach this terminal to:',
|
|
72
|
-
choices: [{ name: 'none (each has a tab inside; sandbox attach shows any of them)', value: -1 }, ...sessions.map((s, i) => ({ name: s.harness + ' in ' + shortenMiddle(s.folder, width - 20), value: i }))],
|
|
73
|
-
default: 0,
|
|
74
|
-
});
|
|
75
|
-
// The one to attach goes first: -t attaches the first session.
|
|
76
|
-
const ordered = attach >= 0 ? [sessions[attach], ...sessions.filter((_s, i) => i !== attach)] : sessions;
|
|
77
|
-
const projects = [...new Set(sessions.map((s) => path.basename(s.folder)))];
|
|
78
|
-
console.log('Start 1 sandbox with ' + sessions.length + ' session' + (sessions.length === 1 ? '' : 's') + ': ' + ordered.map((s) => s.harness + ' in ' + path.basename(s.folder)).join(', ') + ' - map ' + projects.join(' and '));
|
|
79
|
-
const argv = severalAgentsArgv(ordered, { headless, attachFirst: attach >= 0 });
|
|
80
|
-
console.log(c.dim(' ' + commandLine(argv)));
|
|
81
|
-
return await confirm({ message: 'Start them?', default: true }) ? argv : null;
|
|
82
|
-
};
|
package/dist/cli/menu.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { Context } from './commands/context.js';
|
|
2
|
-
import { splitArgs } from './args.js';
|
|
3
|
-
import { type CommandSpecT, type OptionSpecT } from './manifest.js';
|
|
4
|
-
/** Splits "a b 'c d'" like a shell would (enough for agent arguments and exec commands). */
|
|
5
|
-
export { splitArgs };
|
|
6
|
-
/** What this option accepts, checked while typing (undefined = anything). */
|
|
7
|
-
export declare const optionProblem: (o: OptionSpecT, v: string) => string | undefined;
|
|
8
|
-
/** Asks everything a command takes and returns its argv (or null when the user backs out). */
|
|
9
|
-
export declare const askCommand: (ctx: Context, spec: CommandSpecT, preset?: string[]) => Promise<string[] | null>;
|
|
10
|
-
/**
|
|
11
|
-
* Runs the menu until Exit, or until a command that takes over the terminal (launch, attach) has run. `run` executes
|
|
12
|
-
* an argv exactly like typing it.
|
|
13
|
-
*/
|
|
14
|
-
export declare const menu: (ctx: Context, run: (argv: string[]) => Promise<number>) => Promise<number>;
|
package/dist/cli/menu.js
DELETED
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
// The interactive menu (`sandbox` with no arguments). Built from manifest.ts: pick a command, answer one question per
|
|
2
|
-
// argument and option (on/off options as checkboxes), see the exact command line, run it. The command line it shows is
|
|
3
|
-
// what you would type yourself, and the last one is offered again at the top next time.
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { checkbox, confirm, input, select, Separator } from '@inquirer/prompts';
|
|
7
|
-
import { humanBytes } from '../core/disk.js';
|
|
8
|
-
import { errMsg, fail } from '../core/util.js';
|
|
9
|
-
import { listAccounts } from '../host/accounts.js';
|
|
10
|
-
import { runningSessions } from '../host/channel.js';
|
|
11
|
-
import { isSandboxRunning } from '../host/platform.js';
|
|
12
|
-
import { cleanTargetOf, collectDataEntries } from './commands/data/collect.js';
|
|
13
|
-
import { c, clearScreen, pageSizeFor, shorten, shortenMiddle, termWidth } from './colors.js';
|
|
14
|
-
import { commandLine, readHistory } from './history.js';
|
|
15
|
-
import { settingsMenu } from './settings/index.js';
|
|
16
|
-
import { splitArgs } from './args.js';
|
|
17
|
-
import { syncAgentCommands } from './commands/agent-commands.js';
|
|
18
|
-
import { loadConfig } from '../core/config.js';
|
|
19
|
-
import { askedWhen, COMMANDS, Group } from './manifest.js';
|
|
20
|
-
import { askSeveralAgents } from './menu-multi.js';
|
|
21
|
-
import { validNodeSpec, validPythonSpec } from './validate.js';
|
|
22
|
-
// ---- the screen ------------------------------------------------------------------------------------------------------
|
|
23
|
-
/** The settings screen may have switched agentCommands (Beta): apply it now, from the saved file. */
|
|
24
|
-
const applyAgentCommands = (ctx) => {
|
|
25
|
-
try {
|
|
26
|
-
syncAgentCommands(ctx, loadConfig(ctx.loaded.configPath ?? undefined).config);
|
|
27
|
-
}
|
|
28
|
-
catch (e) {
|
|
29
|
-
console.warn('Agent commands (Beta): ' + errMsg(e));
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
/** Waits for one key, so the output of a command stays on screen until you have read it. Ctrl+C leaves. */
|
|
33
|
-
const pressAnyKey = (message) => new Promise((resolve) => {
|
|
34
|
-
process.stdout.write('\n' + c.dim(message));
|
|
35
|
-
const { stdin } = process;
|
|
36
|
-
stdin.setRawMode(true);
|
|
37
|
-
stdin.resume();
|
|
38
|
-
stdin.once('data', (key) => {
|
|
39
|
-
stdin.setRawMode(false);
|
|
40
|
-
stdin.pause();
|
|
41
|
-
process.stdout.write('\n');
|
|
42
|
-
if (key[0] === 3)
|
|
43
|
-
process.exit(130);
|
|
44
|
-
resolve();
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
// ---- asking for one command ------------------------------------------------------------------------------------------
|
|
48
|
-
/** Values for a choices list the manifest names (filled in from config and the running sandbox). */
|
|
49
|
-
const choicesFor = (ctx, choices) => {
|
|
50
|
-
if (Array.isArray(choices))
|
|
51
|
-
return choices;
|
|
52
|
-
if (choices === 'harnesses')
|
|
53
|
-
return Object.keys(ctx.loaded.config.harnesses);
|
|
54
|
-
if (choices === 'accounts')
|
|
55
|
-
return listAccounts(ctx.storageRoot, ctx.dataPath).map((a) => a.name);
|
|
56
|
-
if (choices === 'sessions')
|
|
57
|
-
return isSandboxRunning() ? runningSessions(ctx.dataPath).map((s) => s.id) : [];
|
|
58
|
-
return [];
|
|
59
|
-
};
|
|
60
|
-
/** Splits "a b 'c d'" like a shell would (enough for agent arguments and exec commands). */
|
|
61
|
-
export { splitArgs };
|
|
62
|
-
/** What this option accepts, checked while typing (undefined = anything). */
|
|
63
|
-
export const optionProblem = (o, v) => {
|
|
64
|
-
if (v === '')
|
|
65
|
-
return undefined;
|
|
66
|
-
if (o.type === 'number' && !(Number(v) > 0))
|
|
67
|
-
return 'Enter a number greater than 0.';
|
|
68
|
-
if (o.flag === '--node' && !validNodeSpec(v))
|
|
69
|
-
return 'Use a number like 22 or 22.11.0, or lts, latest.';
|
|
70
|
-
if (o.flag === '--python' && !validPythonSpec(v))
|
|
71
|
-
return 'Use 3, 3.12 or 3.12.4.';
|
|
72
|
-
if (o.value === 'dir' && !fs.existsSync(path.resolve(v)))
|
|
73
|
-
return 'That folder does not exist.';
|
|
74
|
-
if (o.value === 'file') {
|
|
75
|
-
const missing = splitArgs(v).find((f) => !fs.existsSync(path.resolve(f)));
|
|
76
|
-
if (missing)
|
|
77
|
-
return missing + ' does not exist.';
|
|
78
|
-
}
|
|
79
|
-
return undefined;
|
|
80
|
-
};
|
|
81
|
-
/** Targets of `data clean` as a checkbox list with sizes (never "all", never --yes: those are typed on purpose). */
|
|
82
|
-
const askDataTargets = async (ctx) => {
|
|
83
|
-
console.log(c.dim('Measuring the saved data (big folders take a moment)...'));
|
|
84
|
-
const rows = collectDataEntries(ctx.dataPath, ctx.storageRoot).flatMap((e) => { const target = cleanTargetOf(e); return target ? [{ target, bytes: e.bytes }] : []; });
|
|
85
|
-
if (!rows.length) {
|
|
86
|
-
console.log('Nothing is saved yet, so there is nothing to delete.');
|
|
87
|
-
return [];
|
|
88
|
-
}
|
|
89
|
-
const width = Math.max(...rows.map((r) => r.target.length));
|
|
90
|
-
return checkbox({
|
|
91
|
-
message: 'Delete which? ' + c.dim('(space to tick, enter to continue; you confirm once more)'),
|
|
92
|
-
pageSize: pageSizeFor(16),
|
|
93
|
-
choices: rows.map((r) => ({ name: r.target.padEnd(width) + ' ' + humanBytes(r.bytes).padStart(8), value: r.target, short: r.target })),
|
|
94
|
-
validate: (picked) => (picked.length > 0 ? true : 'Tick at least one with space, or press Ctrl+C to go back.'),
|
|
95
|
-
});
|
|
96
|
-
};
|
|
97
|
-
const askArg = async (ctx, spec, arg) => {
|
|
98
|
-
if (spec.name === 'data' && arg.name === 'targets')
|
|
99
|
-
return askDataTargets(ctx);
|
|
100
|
-
const list = choicesFor(ctx, arg.choices);
|
|
101
|
-
if (list.length && !arg.variadic) {
|
|
102
|
-
const picked = await select({ message: arg.description, choices: [...list.map((v) => ({ name: v, value: v })), ...(arg.required ? [] : [{ name: c.dim('(skip)'), value: '' }])] });
|
|
103
|
-
return picked ? [picked] : [];
|
|
104
|
-
}
|
|
105
|
-
// A list the manifest fills in at run time (saved accounts, running sessions) that is empty: nothing to pick, so do not ask.
|
|
106
|
-
if (typeof arg.choices === 'string' && !arg.required)
|
|
107
|
-
return [];
|
|
108
|
-
if (typeof arg.choices === 'string')
|
|
109
|
-
fail('There is nothing to choose from: no ' + arg.choices + ' yet.', arg.choices === 'accounts' ? 'Sign in inside the sandbox, then run: sandbox account save <name>' : 'Start one first (sandbox claude).');
|
|
110
|
-
const answer = await input({ message: arg.description + (arg.required ? '' : c.dim(' (optional)')), validate: (v) => (!arg.required || v.trim() !== '' ? true : 'This is required.') });
|
|
111
|
-
return arg.variadic ? splitArgs(answer) : answer.trim() ? [answer.trim()] : [];
|
|
112
|
-
};
|
|
113
|
-
const askOptions = async (ctx, options) => {
|
|
114
|
-
const argv = [];
|
|
115
|
-
const flags = options.filter((o) => o.type === 'boolean');
|
|
116
|
-
if (flags.length)
|
|
117
|
-
argv.push(...await checkbox({ message: 'Options ' + c.dim('(space to toggle, enter to continue)'), choices: flags.map((o) => ({ name: o.flag + c.dim(' ' + shorten(o.description, Math.max(20, termWidth() - o.flag.length - 8))), value: o.flag })) }));
|
|
118
|
-
for (const o of options.filter((x) => x.type !== 'boolean')) {
|
|
119
|
-
const list = choicesFor(ctx, o.choices);
|
|
120
|
-
const label = o.flag + c.dim(' ' + o.description);
|
|
121
|
-
// A named list that is empty (no saved accounts) has nothing to pick: leave the option out.
|
|
122
|
-
if (typeof o.choices === 'string' && !list.length)
|
|
123
|
-
continue;
|
|
124
|
-
if (o.type === 'list' && list.length) {
|
|
125
|
-
for (const v of await checkbox({ message: label, choices: list.map((v) => ({ name: v, value: v })) }))
|
|
126
|
-
argv.push(o.flag, v);
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
const value = list.length
|
|
130
|
-
? await select({ message: label, choices: [{ name: c.dim('(default' + (o.choices === 'harnesses' ? ': ' + ctx.loaded.config.defaultHarness : o.choices === 'accounts' ? ': none chosen' : '') + ')'), value: '' }, ...list.map((v) => ({ name: v, value: v }))] })
|
|
131
|
-
: await input({ message: label + c.dim(o.default !== undefined ? ' (default ' + o.default + ')' : ' (empty = default)'), validate: (v) => optionProblem(o, v) ?? true });
|
|
132
|
-
if (!value)
|
|
133
|
-
continue;
|
|
134
|
-
for (const v of o.type === 'list' ? splitArgs(value) : [value])
|
|
135
|
-
argv.push(o.flag, v);
|
|
136
|
-
}
|
|
137
|
-
return argv;
|
|
138
|
-
};
|
|
139
|
-
/** Asks everything a command takes and returns its argv (or null when the user backs out). */
|
|
140
|
-
export const askCommand = async (ctx, spec, preset = []) => {
|
|
141
|
-
console.log('\n' + c.bold(spec.name) + c.dim(' ' + shorten(spec.summary, termWidth() - spec.name.length - 3)));
|
|
142
|
-
const argv = [spec.name, ...preset];
|
|
143
|
-
// The common case needs one answer: start with the settings you already have, or change them first.
|
|
144
|
-
if (spec.name === 'launch' && preset.length) {
|
|
145
|
-
const quick = await select({ message: 'Start ' + (preset[preset.indexOf('--harness') + 1] ?? 'the agent') + ' in ' + shortenMiddle(process.cwd(), Math.max(20, termWidth() - 30)), choices: [{ name: 'Start now', value: 'now' }, { name: 'Change options first...', value: 'custom' }, { name: c.dim('< back'), value: 'back' }] });
|
|
146
|
-
if (quick === 'back')
|
|
147
|
-
return null;
|
|
148
|
-
if (quick === 'now') {
|
|
149
|
-
console.log(c.dim(' ' + commandLine(argv)));
|
|
150
|
-
return argv;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
let first = '';
|
|
154
|
-
for (const [i, arg] of spec.args.entries()) {
|
|
155
|
-
if (!askedWhen(arg.when, first))
|
|
156
|
-
continue;
|
|
157
|
-
const answer = await askArg(ctx, spec, arg);
|
|
158
|
-
if (i === 0)
|
|
159
|
-
first = answer[0] ?? '';
|
|
160
|
-
argv.push(...answer);
|
|
161
|
-
}
|
|
162
|
-
const offered = (o) => !o.noMenu && askedWhen(o.when, first);
|
|
163
|
-
const basic = spec.options.filter((o) => !o.advanced && !preset.includes(o.flag) && offered(o));
|
|
164
|
-
const advanced = spec.options.filter((o) => o.advanced && offered(o));
|
|
165
|
-
argv.push(...await askOptions(ctx, basic));
|
|
166
|
-
if (advanced.length && await confirm({ message: 'Change advanced options (' + advanced.map((o) => o.flag).join(', ') + ')?', default: false }))
|
|
167
|
-
argv.push(...await askOptions(ctx, advanced));
|
|
168
|
-
if (spec.json && !(spec.name === 'data' && first === 'clean') && await confirm({ message: 'Machine-readable output (--json)?', default: false }))
|
|
169
|
-
argv.push('--json');
|
|
170
|
-
if (spec.passthrough) {
|
|
171
|
-
const extra = splitArgs(await input({ message: spec.passthrough === 'agent' ? 'Extra arguments for the agent ' + c.dim('(optional, e.g. --continue)') : 'Arguments for the command' + c.dim(' (optional)') }));
|
|
172
|
-
if (extra.length)
|
|
173
|
-
argv.push(...(spec.passthrough === 'agent' ? ['--', ...extra] : extra));
|
|
174
|
-
}
|
|
175
|
-
console.log(c.dim(' ' + commandLine(argv)));
|
|
176
|
-
return await confirm({ message: 'Run it?', default: true }) ? argv : null;
|
|
177
|
-
};
|
|
178
|
-
// ---- the menu --------------------------------------------------------------------------------------------------------
|
|
179
|
-
/** Is this agent already in the sandbox's persistent storage (so starting it needs no install)? */
|
|
180
|
-
const agentInstalled = (ctx, name) => {
|
|
181
|
-
const h = ctx.loaded.config.harnesses[name];
|
|
182
|
-
const dir = path.join(ctx.dataPath, 'tools', 'agents', name);
|
|
183
|
-
return Boolean(h) && [h.hostExecutable, h.npmBin && h.npmBin + '.cmd', h.uvBin && h.uvBin + '.exe'].some((f) => f && fs.existsSync(path.join(dir, f)));
|
|
184
|
-
};
|
|
185
|
-
const GROUP_TITLES = { Agents: 'Start an agent', Sandbox: 'Sandbox', Sessions: 'Sessions', Accounts: 'Claude accounts', Data: 'Saved data and cleanup', Health: 'Diagnostics and repair', Config: 'Configuration' };
|
|
186
|
-
const pickAgent = async (ctx) => {
|
|
187
|
-
const names = Object.keys(ctx.loaded.config.harnesses);
|
|
188
|
-
clearScreen();
|
|
189
|
-
const name = await select({
|
|
190
|
-
message: 'Which agent?',
|
|
191
|
-
pageSize: pageSizeFor(16),
|
|
192
|
-
choices: [
|
|
193
|
-
...names.map((n) => ({ name: n.padEnd(10) + (agentInstalled(ctx, n) ? c.green(' installed') : c.dim(' installs on first start')) + (n === ctx.loaded.config.defaultHarness ? c.dim(' (default)') : ''), value: n })),
|
|
194
|
-
new Separator(),
|
|
195
|
-
{ name: 'Claude on this PC, NOT sandboxed (host-claude)', value: '@host' },
|
|
196
|
-
{ name: c.dim('< back'), value: '' },
|
|
197
|
-
],
|
|
198
|
-
});
|
|
199
|
-
if (name === '@host')
|
|
200
|
-
return { kind: 'ask', spec: COMMANDS.find((s) => s.name === 'host-claude') };
|
|
201
|
-
return name ? { kind: 'ask', spec: COMMANDS.find((s) => s.name === 'launch'), preset: ['--harness', name] } : { kind: 'group', group: '' };
|
|
202
|
-
};
|
|
203
|
-
const mainMenu = async (ctx) => {
|
|
204
|
-
const last = readHistory()[0];
|
|
205
|
-
const running = isSandboxRunning();
|
|
206
|
-
clearScreen();
|
|
207
|
-
console.log(c.bold(c.cyan('Windows Development Sandbox')) + c.dim(' ' + shortenMiddle(process.cwd(), Math.max(20, termWidth() - 32))) + '\n' + (running ? c.green(' A sandbox is running') + c.dim(' (' + runningSessions(ctx.dataPath).length + ' session(s))') : c.dim(' No sandbox running')) + '\n');
|
|
208
|
-
return select({
|
|
209
|
-
message: 'What would you like to do?',
|
|
210
|
-
pageSize: pageSizeFor(14),
|
|
211
|
-
choices: [
|
|
212
|
-
...(last ? [{ name: 'Repeat last: ' + c.cyan(shorten(commandLine(last.argv), Math.max(20, termWidth() - 20))), value: { kind: 'run', argv: last.argv } }] : []),
|
|
213
|
-
{ name: 'Start ' + ctx.loaded.config.defaultHarness + ' in this folder', value: { kind: 'ask', spec: COMMANDS.find((s) => s.name === 'launch'), preset: ['--harness', ctx.loaded.config.defaultHarness] } },
|
|
214
|
-
{ name: 'Start several agents (one or more projects, attach to any)...', value: { kind: 'several' } },
|
|
215
|
-
{ name: 'Start another agent...', value: { kind: 'group', group: '@agents' } },
|
|
216
|
-
{ name: 'Start a sandbox without an agent', value: { kind: 'ask', spec: COMMANDS.find((s) => s.name === 'start') } },
|
|
217
|
-
new Separator(),
|
|
218
|
-
...Group.options.filter((g) => g !== 'Agents').map((g) => ({ name: GROUP_TITLES[g] + '...', value: { kind: 'group', group: g } })),
|
|
219
|
-
{ name: 'Settings ' + c.dim('(every setting, saved to your config file)'), value: { kind: 'group', group: '@settings' } },
|
|
220
|
-
new Separator(),
|
|
221
|
-
{ name: 'Exit', value: { kind: 'exit' } },
|
|
222
|
-
],
|
|
223
|
-
});
|
|
224
|
-
};
|
|
225
|
-
const groupMenu = async (group) => {
|
|
226
|
-
clearScreen();
|
|
227
|
-
return select({
|
|
228
|
-
message: GROUP_TITLES[group] ?? group,
|
|
229
|
-
pageSize: pageSizeFor(16),
|
|
230
|
-
choices: [
|
|
231
|
-
...COMMANDS.filter((s) => s.group === group && !s.hidden).map((s) => ({ name: s.name.padEnd(10), description: s.summary, value: { kind: 'ask', spec: s } })),
|
|
232
|
-
{ name: c.dim('< back'), value: { kind: 'group', group: '' } },
|
|
233
|
-
],
|
|
234
|
-
});
|
|
235
|
-
};
|
|
236
|
-
/**
|
|
237
|
-
* Runs the menu until Exit, or until a command that takes over the terminal (launch, attach) has run. `run` executes
|
|
238
|
-
* an argv exactly like typing it.
|
|
239
|
-
*/
|
|
240
|
-
export const menu = async (ctx, run) => {
|
|
241
|
-
if (!process.stdin.isTTY) {
|
|
242
|
-
console.log('The menu needs an interactive terminal. Run sandbox --help for the commands.');
|
|
243
|
-
return 1;
|
|
244
|
-
}
|
|
245
|
-
try {
|
|
246
|
-
for (let pick = await mainMenu(ctx);;) {
|
|
247
|
-
if (pick.kind === 'exit')
|
|
248
|
-
return 0;
|
|
249
|
-
if (pick.kind === 'group' && pick.group === '@settings') {
|
|
250
|
-
await settingsMenu(ctx.loaded.configPath);
|
|
251
|
-
applyAgentCommands(ctx);
|
|
252
|
-
pick = await mainMenu(ctx);
|
|
253
|
-
}
|
|
254
|
-
else if (pick.kind === 'group')
|
|
255
|
-
pick = pick.group === '' ? await mainMenu(ctx) : pick.group === '@agents' ? await pickAgent(ctx) : await groupMenu(pick.group);
|
|
256
|
-
else {
|
|
257
|
-
// Ctrl+C while answering the questions of one command goes back to the menu; on the menu itself it leaves.
|
|
258
|
-
let argv;
|
|
259
|
-
try {
|
|
260
|
-
clearScreen();
|
|
261
|
-
argv = pick.kind === 'run' ? pick.argv : pick.kind === 'several' ? await askSeveralAgents(ctx) : await askCommand(ctx, pick.spec, pick.preset);
|
|
262
|
-
}
|
|
263
|
-
catch (e) {
|
|
264
|
-
if (e instanceof Error && e.name === 'ExitPromptError')
|
|
265
|
-
argv = null;
|
|
266
|
-
else
|
|
267
|
-
throw e;
|
|
268
|
-
}
|
|
269
|
-
if (!argv) {
|
|
270
|
-
pick = await mainMenu(ctx);
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
clearScreen();
|
|
274
|
-
console.log(c.dim('$ ' + commandLine(argv)) + '\n');
|
|
275
|
-
const code = await run(argv);
|
|
276
|
-
if (COMMANDS.find((s) => s.name === argv[0])?.interactive)
|
|
277
|
-
return code;
|
|
278
|
-
await pressAnyKey((code === 0 ? c.green('Done.') : yellowCode(code)) + ' Press any key for the menu...');
|
|
279
|
-
pick = await mainMenu(ctx);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
catch (e) {
|
|
284
|
-
// Ctrl+C in a prompt: leave quietly.
|
|
285
|
-
if (e instanceof Error && e.name === 'ExitPromptError')
|
|
286
|
-
return 130;
|
|
287
|
-
throw e;
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
const yellowCode = (code) => c.yellow('Exit code ' + code);
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/** The user's own config, or a project's .sandbox-config.json (a smaller schema; nothing is merged with defaults). */
|
|
2
|
-
export type SettingsTarget = 'user' | 'project';
|
|
3
|
-
/** What the value is in the file (undefined when it is not set there). */
|
|
4
|
-
export declare const fileValue: (text: string, key: string[]) => unknown;
|
|
5
|
-
/**
|
|
6
|
-
* Applies one change (undefined = remove the line, back to the default) to the config text and returns the new text, or
|
|
7
|
-
* the problems that make the result invalid.
|
|
8
|
-
*/
|
|
9
|
-
export declare const editConfigText: (text: string, key: string[], value: unknown, target?: SettingsTarget) => {
|
|
10
|
-
text: string;
|
|
11
|
-
} | {
|
|
12
|
-
problems: string[];
|
|
13
|
-
};
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// Changing a config file's text: one setting at a time, through jsonc-parser so comments and formatting stay, and never
|
|
2
|
-
// into something invalid. Pure; the caller writes the file.
|
|
3
|
-
import { applyEdits, modify, parse } from 'jsonc-parser';
|
|
4
|
-
import _ from 'lodash';
|
|
5
|
-
import { DEFAULTS, mergeConfig } from '../../core/config.js';
|
|
6
|
-
import { Config, describeIssues, LocalOverrides } from '../../core/schema.js';
|
|
7
|
-
/** What the value is in the file (undefined when it is not set there). */
|
|
8
|
-
export const fileValue = (text, key) => _.get(parse(text) ?? {}, key);
|
|
9
|
-
/**
|
|
10
|
-
* Applies one change (undefined = remove the line, back to the default) to the config text and returns the new text, or
|
|
11
|
-
* the problems that make the result invalid.
|
|
12
|
-
*/
|
|
13
|
-
export const editConfigText = (text, key, value, target = 'user') => {
|
|
14
|
-
// Removing a setting the file does not have changes nothing (jsonc-parser throws on it).
|
|
15
|
-
const edits = value === undefined && fileValue(text || '{}', key) === undefined ? [] : modify(text || '{}', key, value, { formattingOptions: { insertSpaces: true, tabSize: 2, eol: '\n' } });
|
|
16
|
-
const next = applyEdits(text || '{}', edits);
|
|
17
|
-
const parsed = target === 'project' ? LocalOverrides.safeParse(parse(next) ?? {}) : Config.safeParse(mergeConfig(DEFAULTS, parse(next) ?? {}));
|
|
18
|
-
return parsed.success ? { text: next.endsWith('\n') ? next : next + '\n' } : { problems: describeIssues(parsed.error) };
|
|
19
|
-
};
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export { editConfigText } from './edit.js';
|
|
2
|
-
/** Titles of the top-level sections in the menu. A test makes sure every section of the schema has one. */
|
|
3
|
-
export declare const SECTION_TITLES: Record<string, string>;
|
|
4
|
-
/**
|
|
5
|
-
* The settings screen: first which file to edit (your own, or this folder's project file), then its settings. Your file
|
|
6
|
-
* is created when missing; so is the project file once a setting is saved.
|
|
7
|
-
*/
|
|
8
|
-
export declare const settingsMenu: (configPath: string | null, projectDir?: string) => Promise<void>;
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
// The settings screen in the menu: every setting, generated from the zod schemas (so nothing needs to be listed by
|
|
2
|
-
// hand), shown with its current value and description, edited with a fitting prompt and saved to a config file. Two
|
|
3
|
-
// files can be edited: your own (all projects) and this folder's .sandbox-config.json (only what a project may set).
|
|
4
|
-
// Edits keep comments and formatting, and nothing is written unless the whole file is still valid.
|
|
5
|
-
import fs from 'node:fs';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import { select, Separator } from '@inquirer/prompts';
|
|
8
|
-
import _ from 'lodash';
|
|
9
|
-
import { DEFAULTS, loadConfig, userConfigPath } from '../../core/config.js';
|
|
10
|
-
import { LOCAL_CONFIG_NAME, NEVER_LOOSER } from '../../core/local-config.js';
|
|
11
|
-
import { Config as ConfigSchema, LocalOverrides } from '../../core/schema.js';
|
|
12
|
-
import { c, clearScreen, pageSizeFor, shortenMiddle, termWidth } from '../colors.js';
|
|
13
|
-
import { editConfigText, fileValue } from './edit.js';
|
|
14
|
-
import { askValue } from './prompts.js';
|
|
15
|
-
import { childrenOf, describeSetting, isSection, unwrap } from './schema.js';
|
|
16
|
-
export { editConfigText } from './edit.js';
|
|
17
|
-
/** Titles of the top-level sections in the menu. A test makes sure every section of the schema has one. */
|
|
18
|
-
export const SECTION_TITLES = {
|
|
19
|
-
toolchains: 'Languages and tools', databases: 'Databases', sandbox: 'The sandbox machine (memory, window, GPU, network)', attach: 'Attaching (auto attach, auto hide)', agentCommands: 'Agent commands: claude, codex ... start in a sandbox (Beta)',
|
|
20
|
-
provisioning: 'Provisioning and git', downloads: 'Downloads (checksums)', secrets: 'Secrets', claudeProfile: 'Your Claude settings and history', storage: 'Storage',
|
|
21
|
-
network: 'Network', hooks: 'Hooks', personalization: 'Desktop look', projects: 'Several projects (project roots, adding folders while running)',
|
|
22
|
-
};
|
|
23
|
-
/** A value in one short line: maps as their keys, long lists shortened. */
|
|
24
|
-
const show = (value) => {
|
|
25
|
-
if (value === undefined)
|
|
26
|
-
return c.dim('(not set)');
|
|
27
|
-
if (typeof value === 'string')
|
|
28
|
-
return value;
|
|
29
|
-
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
30
|
-
const keys = Object.keys(value);
|
|
31
|
-
return keys.length ? '{ ' + _.truncate(keys.join(', '), { length: 60 }) + ' }' : '{}';
|
|
32
|
-
}
|
|
33
|
-
return _.truncate(JSON.stringify(value), { length: 70 });
|
|
34
|
-
};
|
|
35
|
-
const readText = (file) => (fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '');
|
|
36
|
-
/** The user's settings with the built-in defaults filled in (what a sandbox would use without a project file). */
|
|
37
|
-
const userConfig = (file) => loadConfig(fs.existsSync(file) ? file : undefined).config;
|
|
38
|
-
/** What is in effect now: for your own file the merged config, for a project file only what that file sets. */
|
|
39
|
-
const currentValue = (session, key, text, config) => session.target === 'project' ? fileValue(text, key) : _.get(config, key);
|
|
40
|
-
/** Edits one setting: shows it, asks, saves (or resets it to the default / removes it from the project file). */
|
|
41
|
-
const editSetting = async (session, schema, key, config) => {
|
|
42
|
-
const { file, target } = session;
|
|
43
|
-
const text = readText(file);
|
|
44
|
-
const inFile = fileValue(text, key);
|
|
45
|
-
const project = target === 'project';
|
|
46
|
-
console.log('\n' + c.cyan(key.join('.')) + (describeSetting(schema) ? '\n' + c.dim(describeSetting(schema)) : ''));
|
|
47
|
-
if (project && NEVER_LOOSER.includes(key.join('.')))
|
|
48
|
-
console.log(c.yellow('A project can only make this stricter than your own setting; a looser value is ignored.'));
|
|
49
|
-
console.log('Now: ' + (project ? show(inFile) + c.dim(inFile === undefined ? ' (not set in this project; yours: ' + show(_.get(config, key)) + ')' : ' (set in ' + file + ')') : show(_.get(config, key)) + (inFile === undefined ? c.dim(' (built-in default)') : c.dim(' (set in ' + file + ')'))));
|
|
50
|
-
const resetLabel = project ? 'Remove from this project file' : 'Reset to the default (' + show(_.get(DEFAULTS, key)) + ')';
|
|
51
|
-
const action = inFile === undefined ? 'change' : await select({ message: 'What now?', choices: [{ name: 'Change it', value: 'change' }, { name: resetLabel, value: 'reset' }, { name: c.dim('< back'), value: 'back' }] });
|
|
52
|
-
if (action === 'back')
|
|
53
|
-
return;
|
|
54
|
-
const answer = action === 'reset' ? { value: undefined } : await askValue(schema, key, project ? inFile ?? _.get(config, key) : _.get(config, key), config);
|
|
55
|
-
if (!answer)
|
|
56
|
-
return;
|
|
57
|
-
// In your own file the default needs no line; an unchanged value needs no save.
|
|
58
|
-
const value = !project && _.isEqual(answer.value, _.get(DEFAULTS, key)) ? undefined : answer.value;
|
|
59
|
-
if (value === inFile && (project || _.isEqual(answer.value, _.get(config, key)))) {
|
|
60
|
-
console.log(c.dim('Unchanged.'));
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
const result = editConfigText(text, key, value, target);
|
|
64
|
-
if ('problems' in result) {
|
|
65
|
-
console.log(c.red('Not saved: ') + result.problems.join('; '));
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
69
|
-
fs.writeFileSync(file, result.text, 'utf8');
|
|
70
|
-
console.log(c.green('Saved') + c.dim(' to ' + file + (project ? '.' : '. New sandboxes use it.')));
|
|
71
|
-
// The approval is tied to the file's exact contents, so any change makes the project file untrusted again.
|
|
72
|
-
if (project)
|
|
73
|
-
console.log(c.yellow('This project file is not approved any more: the next launch shows what it applies and asks you to approve it.'));
|
|
74
|
-
};
|
|
75
|
-
/** Browses one level of settings; sections open, leaves are edited. */
|
|
76
|
-
const browse = async (session, schema, key) => {
|
|
77
|
-
for (;;) {
|
|
78
|
-
const config = userConfig(session.target === 'user' ? session.file : userConfigPath());
|
|
79
|
-
const entries = childrenOf(schema);
|
|
80
|
-
const text = readText(session.file);
|
|
81
|
-
clearScreen();
|
|
82
|
-
console.log(c.dim('Editing ' + shortenMiddle(session.file, termWidth() - 10)) + '\n' + c.dim('Values in ') + c.yellow('yellow') + c.dim(' (marked *) are set in that file; the rest are defaults.') + '\n');
|
|
83
|
-
const pickName = await select({
|
|
84
|
-
message: key.length ? key.join('.') : 'Settings',
|
|
85
|
-
pageSize: pageSizeFor(18),
|
|
86
|
-
choices: [
|
|
87
|
-
...entries.map(([name, s]) => {
|
|
88
|
-
const full = [...key, name];
|
|
89
|
-
const changed = fileValue(text, full) !== undefined;
|
|
90
|
-
const shown = show(currentValue(session, full, text, config));
|
|
91
|
-
const label = isSection(s) ? (key.length ? name : SECTION_TITLES[name] ?? name) + c.dim(' ...') : name.padEnd(18) + ' ' + (changed ? c.yellow(shown) + ' *' : shown);
|
|
92
|
-
return { name: label, value: name, description: describeSetting(s) };
|
|
93
|
-
}),
|
|
94
|
-
new Separator(),
|
|
95
|
-
{ name: c.dim('< back'), value: '@back' },
|
|
96
|
-
],
|
|
97
|
-
});
|
|
98
|
-
if (pickName === '@back')
|
|
99
|
-
return;
|
|
100
|
-
const child = unwrap(schema).shape[pickName];
|
|
101
|
-
if (isSection(child))
|
|
102
|
-
await browse(session, child, [...key, pickName]);
|
|
103
|
-
else
|
|
104
|
-
await editSetting(session, child, [...key, pickName], config);
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
/**
|
|
108
|
-
* The settings screen: first which file to edit (your own, or this folder's project file), then its settings. Your file
|
|
109
|
-
* is created when missing; so is the project file once a setting is saved.
|
|
110
|
-
*/
|
|
111
|
-
export const settingsMenu = async (configPath, projectDir = process.cwd()) => {
|
|
112
|
-
const sessions = {
|
|
113
|
-
user: { target: 'user', file: configPath ?? userConfigPath(), schema: ConfigSchema },
|
|
114
|
-
project: { target: 'project', file: path.join(projectDir, LOCAL_CONFIG_NAME), schema: LocalOverrides },
|
|
115
|
-
};
|
|
116
|
-
for (;;) {
|
|
117
|
-
clearScreen();
|
|
118
|
-
const which = await select({
|
|
119
|
-
message: 'Which settings?',
|
|
120
|
-
choices: [
|
|
121
|
-
{ name: 'Your settings (all projects)', description: sessions.user.file, value: 'user' },
|
|
122
|
-
{ name: 'This project', description: sessions.project.file + ' (only what a project may set; needs your approval at launch)', value: 'project' },
|
|
123
|
-
new Separator(),
|
|
124
|
-
{ name: c.dim('< back'), value: '@back' },
|
|
125
|
-
],
|
|
126
|
-
});
|
|
127
|
-
if (which === '@back')
|
|
128
|
-
return;
|
|
129
|
-
await browse(sessions[which], sessions[which].schema, []);
|
|
130
|
-
}
|
|
131
|
-
};
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { Config } from '../../core/types.js';
|
|
2
|
-
import { type Schema } from './schema.js';
|
|
3
|
-
export type Answer = {
|
|
4
|
-
value: unknown;
|
|
5
|
-
} | undefined;
|
|
6
|
-
/** `key` is the setting's path; `config` (the user's settings) lets the default agent be picked from the configured ones. */
|
|
7
|
-
export declare const askValue: (schema: Schema, key: string[], current: unknown, config?: Config) => Promise<Answer>;
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// The prompt that fits a setting's type. Returns the new value, or undefined to change nothing.
|
|
2
|
-
import { input, select } from '@inquirer/prompts';
|
|
3
|
-
import { c } from '../colors.js';
|
|
4
|
-
import { jsonExample, kind, promptKind, unwrap } from './schema.js';
|
|
5
|
-
const CANCEL = '@cancel';
|
|
6
|
-
const cancel = { name: c.dim('< cancel'), value: CANCEL };
|
|
7
|
-
const pick = async (label, choices, current) => {
|
|
8
|
-
const v = await select({ message: label, choices: [...choices, cancel], default: current });
|
|
9
|
-
return v === CANCEL ? undefined : { value: v };
|
|
10
|
-
};
|
|
11
|
-
const typed = async (label, current, validate, convert) => {
|
|
12
|
-
const shown = current === undefined ? '' : typeof current === 'string' ? current : JSON.stringify(current);
|
|
13
|
-
const answer = await input({ message: label + c.dim(' (empty = cancel)'), default: shown, validate: (v) => (v === '' ? true : validate(v)) });
|
|
14
|
-
return answer === '' ? undefined : { value: convert(answer) };
|
|
15
|
-
};
|
|
16
|
-
const BOOLEANS = [{ name: 'true', value: true }, { name: 'false', value: false }];
|
|
17
|
-
/** `key` is the setting's path; `config` (the user's settings) lets the default agent be picked from the configured ones. */
|
|
18
|
-
export const askValue = async (schema, key, current, config) => {
|
|
19
|
-
const label = key.join('.');
|
|
20
|
-
const s = unwrap(schema);
|
|
21
|
-
// The default agent is one of the configured ones: a list, not free text.
|
|
22
|
-
if (label === 'defaultHarness' && config)
|
|
23
|
-
return pick(label, Object.keys(config.harnesses).map((h) => ({ name: h, value: h })), current);
|
|
24
|
-
switch (promptKind(s)) {
|
|
25
|
-
case 'boolean': return pick(label, BOOLEANS, current);
|
|
26
|
-
case 'enum': return pick(label, Object.values(s._zod.def.entries ?? {}).map((v) => ({ name: String(v), value: v })), current);
|
|
27
|
-
case 'union': {
|
|
28
|
-
const options = s._zod.def.options ?? [];
|
|
29
|
-
const literals = options.filter((o) => kind(o) === 'literal').flatMap((o) => unwrap(o)._zod.def.values ?? []);
|
|
30
|
-
const other = options.find((o) => !['literal', 'boolean'].includes(kind(o)));
|
|
31
|
-
const choices = [...(options.some((o) => kind(o) === 'boolean') ? BOOLEANS : []), ...literals.map((v) => ({ name: String(v), value: v }))];
|
|
32
|
-
if (!other)
|
|
33
|
-
return pick(label, choices, current);
|
|
34
|
-
const chosen = await pick(label, [...choices, { name: 'a ' + kind(other) + ' ...', value: '@other' }], current);
|
|
35
|
-
return chosen?.value === '@other' ? askValue(other, key, current) : chosen;
|
|
36
|
-
}
|
|
37
|
-
case 'number': return typed(label, current, (v) => (Number.isFinite(Number(v)) ? true : 'Enter a number, e.g. 8192.'), Number);
|
|
38
|
-
case 'string': return typed(label, current, () => true, (v) => v);
|
|
39
|
-
case 'string-list': return typed(label + c.dim(' (comma separated, [] = none)'), current, () => true, (v) => (v.trim() === '[]' ? [] : v.split(',').map((x) => x.trim()).filter(Boolean)));
|
|
40
|
-
default: return typed(label + c.dim(' (JSON, e.g. ' + jsonExample(key, s) + ')'), current, (v) => { try {
|
|
41
|
-
JSON.parse(v);
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return 'valid JSON';
|
|
46
|
-
} }, (v) => JSON.parse(v));
|
|
47
|
-
}
|
|
48
|
-
};
|