windows-development-sandbox 1.1.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/config.example.json +38 -5
- package/dist/cli/args.d.ts +3 -0
- package/dist/cli/args.js +3 -1
- 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.js +36 -3
- package/dist/cli/commands/maintenance.d.ts +2 -0
- package/dist/cli/commands/maintenance.js +35 -2
- package/dist/cli/commands/sessions.js +3 -3
- package/dist/cli/commands/user-config.js +1 -1
- package/dist/cli/index.js +6 -4
- package/dist/cli/manifest.js +5 -0
- 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/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 +9 -6
- package/dist/core/local-config.d.ts +15 -0
- package/dist/core/local-config.js +63 -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 +66 -1
- 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/sandbox.d.ts +28 -0
- package/dist/core/schema/sandbox.js +28 -0
- package/dist/core/schema/toolchains.d.ts +3 -0
- package/dist/core/schema/toolchains.js +6 -1
- package/dist/core/types.d.ts +6 -0
- package/dist/core/util.d.ts +2 -0
- package/dist/core/util.js +2 -0
- package/dist/core/workspace.js +22 -5
- 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 +1 -0
- package/dist/guest/env.js +3 -1
- package/dist/guest/helpers.d.ts +1 -1
- package/dist/guest/main.js +15 -2
- 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/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/generate.d.ts +6 -0
- package/dist/host/generate.js +10 -0
- package/dist/host/launch/mappings.js +5 -1
- 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/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 +1319 -552
- package/dist/runtime/db.js +168 -13
- package/dist/runtime/main.js +2898 -608
- package/dist/runtime/netproxy.js +25936 -0
- package/dist/runtime/view.js +272 -23
- package/dist/selftest/audit.test.d.ts +2 -0
- package/dist/selftest/audit.test.js +57 -0
- package/dist/selftest/channel.test.js +7 -0
- package/dist/selftest/cli.test.js +47 -17
- package/dist/selftest/fixtures.d.ts +45 -0
- package/dist/selftest/fixtures.js +3 -1
- package/dist/selftest/generate.test.js +7 -2
- package/dist/selftest/index.js +5 -2
- package/dist/selftest/live-network.test.d.ts +2 -0
- package/dist/selftest/live-network.test.js +63 -0
- package/dist/selftest/local-config.test.js +16 -3
- package/dist/selftest/network.test.js +12 -0
- package/dist/selftest/projects.test.js +1 -1
- 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/netpolicy.d.ts +27 -0
- package/dist/shared/netpolicy.js +51 -0
- package/dist/shared/pty-client.d.ts +9 -4
- package/dist/shared/pty-client.js +139 -17
- package/dist/shared/rundir.d.ts +2 -0
- package/dist/shared/rundir.js +2 -0
- package/dist/shared/schema.d.ts +10 -0
- package/dist/shared/schema.js +2 -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 +105 -7
- package/schemas/config.schema.json +303 -2
- package/schemas/sandbox-config.schema.json +126 -12
- 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
|
@@ -32,6 +32,24 @@
|
|
|
32
32
|
"type": "boolean",
|
|
33
33
|
"description": "Map this project read-only."
|
|
34
34
|
},
|
|
35
|
+
"workspaceFilter": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"enabled": {
|
|
39
|
+
"type": "boolean",
|
|
40
|
+
"description": "Keep what your .gitignore lists (.env files, node_modules, build output ...) out of the sandbox. The sandbox works on a copy of the project without those files, kept in step with it both ways within about a second; what it creates that is ignored (its own node_modules, builds) stays in the sandbox. Git works as usual. Off = the project folder itself is mapped."
|
|
41
|
+
},
|
|
42
|
+
"exclude": {
|
|
43
|
+
"type": "array",
|
|
44
|
+
"items": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"maxLength": 500
|
|
47
|
+
},
|
|
48
|
+
"description": "Also keep these out, as .gitignore patterns, e.g. secrets/, *.pem, data/private/."
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"additionalProperties": false
|
|
52
|
+
},
|
|
35
53
|
"mappings": {
|
|
36
54
|
"type": "array",
|
|
37
55
|
"items": {
|
|
@@ -62,7 +80,7 @@
|
|
|
62
80
|
],
|
|
63
81
|
"additionalProperties": false
|
|
64
82
|
},
|
|
65
|
-
"description": "Extra folders (relative to this file)."
|
|
83
|
+
"description": "Extra folders inside this project (relative to this file), e.g. a read-only data folder at another place in the sandbox."
|
|
66
84
|
},
|
|
67
85
|
"scripts": {
|
|
68
86
|
"type": "array",
|
|
@@ -137,6 +155,10 @@
|
|
|
137
155
|
"headless": {
|
|
138
156
|
"type": "boolean",
|
|
139
157
|
"description": "Hide the sandbox window (sbx-show brings it back). A headless sandbox shuts down when its last session ends."
|
|
158
|
+
},
|
|
159
|
+
"keepAwake": {
|
|
160
|
+
"type": "boolean",
|
|
161
|
+
"description": "Keep this PC from sleeping while a sandbox runs, so agents left working overnight keep going (the screen may still turn off; closing a laptop lid still sleeps it)."
|
|
140
162
|
}
|
|
141
163
|
},
|
|
142
164
|
"additionalProperties": false
|
|
@@ -145,7 +167,17 @@
|
|
|
145
167
|
"type": "object",
|
|
146
168
|
"properties": {
|
|
147
169
|
"blockPrivateRanges": {
|
|
148
|
-
"type": "boolean"
|
|
170
|
+
"type": "boolean",
|
|
171
|
+
"description": "Block your LAN (10/8, 172.16/12, 192.168/16, SMB) from inside the sandbox."
|
|
172
|
+
},
|
|
173
|
+
"blockHosts": {
|
|
174
|
+
"type": "array",
|
|
175
|
+
"items": {
|
|
176
|
+
"type": "string",
|
|
177
|
+
"maxLength": 253,
|
|
178
|
+
"pattern": "^([a-zA-Z0-9-]+\\.)*[a-zA-Z0-9-]+$"
|
|
179
|
+
},
|
|
180
|
+
"description": "Never reachable from the sandbox, e.g. production servers or telemetry (each exact name; the sandbox's hosts file sends them nowhere)."
|
|
149
181
|
}
|
|
150
182
|
},
|
|
151
183
|
"additionalProperties": false
|
|
@@ -701,6 +733,22 @@
|
|
|
701
733
|
"enabled": {
|
|
702
734
|
"type": "boolean",
|
|
703
735
|
"description": "Portable VS Code inside the sandbox (code on PATH, desktop shortcut); its settings and extensions persist."
|
|
736
|
+
},
|
|
737
|
+
"setup": {
|
|
738
|
+
"type": "boolean",
|
|
739
|
+
"description": "Point VS Code at the sandbox's own tools (JDK, Python, Go, compilers, .NET, Git, PowerShell 7 and Git Bash terminals) and install the language extensions for the toolchains in use."
|
|
740
|
+
},
|
|
741
|
+
"recommended": {
|
|
742
|
+
"type": "boolean",
|
|
743
|
+
"description": "Also install the extensions a project recommends in its .vscode/extensions.json."
|
|
744
|
+
},
|
|
745
|
+
"extensions": {
|
|
746
|
+
"type": "array",
|
|
747
|
+
"items": {
|
|
748
|
+
"type": "string",
|
|
749
|
+
"pattern": "^[\\w-]+\\.[\\w-]+$"
|
|
750
|
+
},
|
|
751
|
+
"description": "More extensions to install in every sandbox, e.g. eamodio.gitlens, ms-azuretools.vscode-docker."
|
|
704
752
|
}
|
|
705
753
|
},
|
|
706
754
|
"additionalProperties": false
|
|
@@ -920,20 +968,86 @@
|
|
|
920
968
|
},
|
|
921
969
|
"additionalProperties": false
|
|
922
970
|
},
|
|
923
|
-
"
|
|
971
|
+
"apps": {
|
|
924
972
|
"type": "object",
|
|
925
973
|
"properties": {
|
|
926
|
-
"
|
|
927
|
-
"type": "boolean",
|
|
928
|
-
"description": "Copy selected Claude settings from this PC into the sandbox."
|
|
929
|
-
},
|
|
930
|
-
"include": {
|
|
974
|
+
"install": {
|
|
931
975
|
"type": "array",
|
|
932
976
|
"items": {
|
|
933
977
|
"type": "string",
|
|
934
|
-
"
|
|
978
|
+
"enum": [
|
|
979
|
+
"notepadpp",
|
|
980
|
+
"sublime",
|
|
981
|
+
"neovim",
|
|
982
|
+
"dbeaver",
|
|
983
|
+
"pgadmin",
|
|
984
|
+
"heidisql",
|
|
985
|
+
"sqlitebrowser",
|
|
986
|
+
"compass",
|
|
987
|
+
"redisinsight",
|
|
988
|
+
"dbgate",
|
|
989
|
+
"beekeeper",
|
|
990
|
+
"dynamodb-admin",
|
|
991
|
+
"bruno",
|
|
992
|
+
"insomnia",
|
|
993
|
+
"sourcegit",
|
|
994
|
+
"githubdesktop",
|
|
995
|
+
"lazygit",
|
|
996
|
+
"7zip",
|
|
997
|
+
"winmerge",
|
|
998
|
+
"everything",
|
|
999
|
+
"sysinternals",
|
|
1000
|
+
"imhex",
|
|
1001
|
+
"chrome",
|
|
1002
|
+
"firefox",
|
|
1003
|
+
"godot",
|
|
1004
|
+
"blender",
|
|
1005
|
+
"love",
|
|
1006
|
+
"defold",
|
|
1007
|
+
"pixelorama",
|
|
1008
|
+
"tiled",
|
|
1009
|
+
"kubectl",
|
|
1010
|
+
"helm",
|
|
1011
|
+
"k9s",
|
|
1012
|
+
"terraform",
|
|
1013
|
+
"azure-cli",
|
|
1014
|
+
"gcloud",
|
|
1015
|
+
"aws-cli",
|
|
1016
|
+
"drawio",
|
|
1017
|
+
"pandoc",
|
|
1018
|
+
"mitmproxy",
|
|
1019
|
+
"xh",
|
|
1020
|
+
"grpcurl",
|
|
1021
|
+
"k6",
|
|
1022
|
+
"jq",
|
|
1023
|
+
"yq",
|
|
1024
|
+
"ripgrep",
|
|
1025
|
+
"fd",
|
|
1026
|
+
"bat",
|
|
1027
|
+
"fzf",
|
|
1028
|
+
"delta",
|
|
1029
|
+
"just",
|
|
1030
|
+
"hyperfine",
|
|
1031
|
+
"watchexec",
|
|
1032
|
+
"protoc",
|
|
1033
|
+
"ninja",
|
|
1034
|
+
"ffmpeg",
|
|
1035
|
+
"sqlite",
|
|
1036
|
+
"shellcheck",
|
|
1037
|
+
"gitleaks"
|
|
1038
|
+
]
|
|
935
1039
|
},
|
|
936
|
-
"description": "
|
|
1040
|
+
"description": "Apps from the catalogue to install in every sandbox (the Apps screen in the menu lists them by category). Each is downloaded once and kept."
|
|
1041
|
+
}
|
|
1042
|
+
},
|
|
1043
|
+
"additionalProperties": false
|
|
1044
|
+
},
|
|
1045
|
+
"claudeProfile": {
|
|
1046
|
+
"type": "object",
|
|
1047
|
+
"properties": {
|
|
1048
|
+
"enabled": {
|
|
1049
|
+
"type": "boolean",
|
|
1050
|
+
"description": "Copy selected Claude settings from this PC into the sandbox."
|
|
937
1051
|
},
|
|
938
1052
|
"overwrite": {
|
|
939
1053
|
"type": "boolean",
|
|
@@ -945,11 +1059,11 @@
|
|
|
945
1059
|
},
|
|
946
1060
|
"syncHistory": {
|
|
947
1061
|
"type": "boolean",
|
|
948
|
-
"description": "Bring
|
|
1062
|
+
"description": "Bring conversation history in, and sync new history back (path-translated)."
|
|
949
1063
|
},
|
|
950
1064
|
"syncClaudeMem": {
|
|
951
1065
|
"type": "boolean",
|
|
952
|
-
"description": "Bring claude-mem data
|
|
1066
|
+
"description": "Bring claude-mem data into the sandbox (its settings and backups stay on this PC)."
|
|
953
1067
|
}
|
|
954
1068
|
},
|
|
955
1069
|
"additionalProperties": false
|
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>;
|