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
|
@@ -4,15 +4,19 @@ import path from 'node:path';
|
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import _ from 'lodash';
|
|
6
6
|
import { DEFAULTS, userConfigPath, userDir } from '../../core/config.js';
|
|
7
|
-
import { describeOverrides, isTrusted, LOCAL_CONFIG_NAME, normalizeOverrides, readLocalConfig, trustStorePath } from '../../core/local-config.js';
|
|
8
|
-
import {
|
|
7
|
+
import { allowedAnswer, allowStorePath, applyOverrides, describeOverrides, isTrusted, LOCAL_CONFIG_NAME, loosenedRequests, normalizeOverrides, OUTSIDE_FOLDER, outsideKey, outsideMappings, readLocalConfig, trustStorePath } from '../../core/local-config.js';
|
|
8
|
+
import { guestWorkspaceOf, previewGuest } from '../../core/workspace.js';
|
|
9
|
+
import { assertNoLinks, dirExists, expandPath, fail, plainText, safeText, sleep, writeJson } from '../../core/util.js';
|
|
10
|
+
import { humanBytes } from '../../core/disk.js';
|
|
11
|
+
import { AuditLog } from '../../host/audit/log.js';
|
|
9
12
|
import { readKnown, readRegistry } from '../../host/projects.js';
|
|
10
13
|
import { detectWsb } from '../../host/wsb.js';
|
|
11
14
|
import { describePrune, pruneStorage } from '../../host/cleanup.js';
|
|
12
15
|
import { doctorRows, hostClaude, insideSandbox, isSandboxRunning } from '../../host/platform.js';
|
|
13
16
|
import { readSandboxStatus } from '../../shared/rundir.js';
|
|
14
17
|
import { syncBack, watch } from '../../sync/watcher.js';
|
|
15
|
-
import { table } from './context.js';
|
|
18
|
+
import { printJson, table } from './context.js';
|
|
19
|
+
import { c } from '../colors.js';
|
|
16
20
|
export const sync = (ctx) => {
|
|
17
21
|
const running = isSandboxRunning();
|
|
18
22
|
const result = syncBack(ctx.dataPath, !running);
|
|
@@ -28,6 +32,8 @@ const newestLog = (dir) => {
|
|
|
28
32
|
};
|
|
29
33
|
export const logs = async (ctx) => {
|
|
30
34
|
const dir = path.join(ctx.dataPath, 'logs');
|
|
35
|
+
// The sandbox writes this folder: never read through a link it planted there.
|
|
36
|
+
assertNoLinks(ctx.dataPath, dir);
|
|
31
37
|
const file = newestLog(dir) ?? fail('No sandbox logs yet in ' + dir + '.', 'Logs appear after the first start (sandbox claude).');
|
|
32
38
|
console.log('== ' + file + ' ==');
|
|
33
39
|
let printed = 0;
|
|
@@ -102,8 +108,40 @@ const checkLocalConfig = (ctx) => {
|
|
|
102
108
|
const overrides = normalizeOverrides(file.data, dir, file.path);
|
|
103
109
|
const approved = isTrusted(trustStorePath(ctx.storageRoot), file.path, file.hash);
|
|
104
110
|
console.log(file.path + ': valid. ' + (approved ? 'Approved (this exact version).' : 'Not approved yet: the next launch asks you.') + '\n');
|
|
105
|
-
for (const line of describeOverrides(overrides))
|
|
111
|
+
for (const line of describeOverrides(overrides, dir))
|
|
106
112
|
console.log(' ' + safeText(line));
|
|
113
|
+
// What a launch from here would do with each folder and each looser setting, without launching.
|
|
114
|
+
const config = ctx.loaded.config;
|
|
115
|
+
const store = allowStorePath(ctx.storageRoot);
|
|
116
|
+
const workspaceGuest = guestWorkspaceOf(applyOverrides(config, overrides), dir).guest;
|
|
117
|
+
const outside = outsideMappings(dir, overrides.mappings);
|
|
118
|
+
const policy = config.projects.allowOutsideMappings;
|
|
119
|
+
const rows = (overrides.mappings ?? []).map((m) => {
|
|
120
|
+
const where = previewGuest(m, dir, workspaceGuest);
|
|
121
|
+
const status = !dirExists(m.host) ? (m.optional ? 'not on this PC: skipped' : c.red('the folder does not exist: create it, fix the path, or add "optional": true'))
|
|
122
|
+
: !outside.includes(m) ? (m.guest ? 'inside the project' : 'inside the project (already in the sandbox there)')
|
|
123
|
+
: policy === true ? 'outside the project, allowed (projects.allowOutsideMappings is true)'
|
|
124
|
+
: policy === false ? c.red('outside the project: refused (projects.allowOutsideMappings is false)')
|
|
125
|
+
: allowedAnswer(store, file.path, OUTSIDE_FOLDER, outsideKey(m)) === true ? 'outside the project, allowed (you said yes)'
|
|
126
|
+
: c.yellow('outside the project: the next interactive launch asks you');
|
|
127
|
+
return [(m.readOnly ? 'ro ' : 'rw ') + safeText(m.host), '-> ' + safeText(where.guest), status];
|
|
128
|
+
});
|
|
129
|
+
if (rows.length) {
|
|
130
|
+
console.log('\nFolders (where they appear in the sandbox):');
|
|
131
|
+
table(rows);
|
|
132
|
+
}
|
|
133
|
+
for (const m of overrides.mappings ?? []) {
|
|
134
|
+
const note = dirExists(m.host) ? previewGuest(m, dir, workspaceGuest).note : null;
|
|
135
|
+
if (note)
|
|
136
|
+
console.log(' ' + safeText(note));
|
|
137
|
+
}
|
|
138
|
+
const looser = loosenedRequests(config, overrides);
|
|
139
|
+
if (looser.length)
|
|
140
|
+
console.log('\nLooser than your settings (used only if you allow it for this project):');
|
|
141
|
+
for (const a of looser) {
|
|
142
|
+
const answer = allowedAnswer(store, file.path, a.key, a.wanted);
|
|
143
|
+
console.log(' ' + a.key + ' = ' + JSON.stringify(a.wanted) + ': ' + (answer === true ? 'allowed (you said yes)' : answer === false ? 'not used (you said no)' : 'the next interactive launch asks you'));
|
|
144
|
+
}
|
|
107
145
|
return 0;
|
|
108
146
|
};
|
|
109
147
|
export const config = (ctx) => {
|
|
@@ -152,3 +190,31 @@ export const doctor = (ctx) => {
|
|
|
152
190
|
};
|
|
153
191
|
export const claudeHost = (ctx) => hostClaude(ctx.storageRoot, ctx.options.personal, ctx.options.passthrough);
|
|
154
192
|
export const watcher = (ctx) => watch(ctx.dataPath);
|
|
193
|
+
const KINDS = ['proc', 'file', 'session', 'guard', 'note'];
|
|
194
|
+
const KIND_LABEL = { proc: 'program', file: 'file', session: 'session', guard: 'GUARD', note: 'note' };
|
|
195
|
+
/** `sandbox audit`: the audit log of the last days (audit.enabled), newest last. */
|
|
196
|
+
export const audit = async (ctx) => {
|
|
197
|
+
const settings = ctx.loaded.config.audit;
|
|
198
|
+
const kind = ctx.options.kind;
|
|
199
|
+
if (kind && !KINDS.includes(kind))
|
|
200
|
+
fail('Unknown kind "' + kind + '".', 'Use one of: ' + KINDS.join(', '));
|
|
201
|
+
const log = new AuditLog(AuditLog.dirFor(ctx.storageRoot, ctx.dataPath), settings);
|
|
202
|
+
const events = log.read({ days: ctx.options.days, kinds: kind ? [kind] : undefined, grep: ctx.options.grep });
|
|
203
|
+
if (printJson(ctx, events))
|
|
204
|
+
return 0;
|
|
205
|
+
if (!events.length) {
|
|
206
|
+
console.log(settings.enabled ? 'Nothing in the audit log for the last ' + ctx.options.days + ' day(s).' : 'The audit log is off. Turn it on in the menu (Settings > Security & privacy > Audit log) or with audit.enabled in your config.');
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
for (const e of events) {
|
|
210
|
+
// What the sandbox reported is text from the sandbox: no control codes reach your terminal.
|
|
211
|
+
const [p, cmd, parent, dir, act] = [e.p, e.c, e.pp, e.w, e.a].map((v) => plainText(v ?? ''));
|
|
212
|
+
const what = e.k === 'proc' ? (cmd || p) + (parent ? c.dim(' <- ' + path.win32.basename(parent)) : '')
|
|
213
|
+
: e.k === 'file' ? act + ' ' + p + c.dim(' in ' + dir)
|
|
214
|
+
: e.k === 'session' ? act + ' ' + p + c.dim(' ' + dir + ' ' + cmd)
|
|
215
|
+
: p;
|
|
216
|
+
console.log(c.dim(new Date(e.t).toLocaleString()) + ' ' + (e.k === 'guard' ? c.yellow(KIND_LABEL[e.k].padEnd(8)) : KIND_LABEL[e.k].padEnd(8)) + what + (e.n && e.n > 1 ? c.dim(' x' + e.n) : ''));
|
|
217
|
+
}
|
|
218
|
+
console.log(c.dim('\n' + events.length + ' event(s); the log takes ' + humanBytes(log.bytes()) + ' (' + AuditLog.dirFor(ctx.storageRoot, ctx.dataPath) + ').'));
|
|
219
|
+
return 0;
|
|
220
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SandboxStatus } from '../../core/types.js';
|
|
2
|
+
import { type Missing } from '../../core/project-policy.js';
|
|
2
3
|
import { type RegistryEntry } from '../../host/projects.js';
|
|
3
4
|
import type { ProjectSpecT } from '../../shared/schema.js';
|
|
4
5
|
import { type Context } from './context.js';
|
|
@@ -17,12 +18,15 @@ export interface Joined {
|
|
|
17
18
|
harness: string | null;
|
|
18
19
|
notes: string[];
|
|
19
20
|
}
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
/** restart.ts: the sandbox was restarted for a project in this command. */
|
|
22
|
+
export declare const markRestarted: () => void;
|
|
23
|
+
export declare const joinHere: (ctx: Context, status: SandboxStatus, o?: {
|
|
24
|
+
noRestart?: boolean;
|
|
25
|
+
}) => Promise<Joined>;
|
|
26
|
+
/** The command that restarts the running sandbox with this folder's boot wishes (consent given by --yes). */
|
|
27
|
+
export declare const restartCommand: (ctx: Context, host: string) => string;
|
|
28
|
+
/** Joined without what it wants at boot: what is missing, how to restart with it, and how to always have it. */
|
|
29
|
+
export declare const missingNotes: (ctx: Context, name: string, host: string, missing: Missing, shown?: boolean) => string[];
|
|
26
30
|
/** Waits until the sandbox has set a project up, with its progress on a spinner (plain lines without a terminal). */
|
|
27
31
|
export declare const provisionJoined: (ctx: Context, joined: Joined) => Promise<void>;
|
|
28
32
|
/** joinHere, then (for another project) waits until the sandbox has set it up. */
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
// these, so they all agree: `locateHere` only looks (attach, sessions, status, restart), `joinHere` also makes the
|
|
3
3
|
// folder a project of the running sandbox when it can without a restart (launch, start, exec, db).
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { select } from '@inquirer/prompts';
|
|
6
|
+
import { describeMissing, missingTip } from '../../core/project-policy.js';
|
|
5
7
|
import { askYesNo } from '../../core/prompt.js';
|
|
6
8
|
import { isInsideOrEqual } from '../../core/util.js';
|
|
7
9
|
import { resolveRun } from '../../core/workspace.js';
|
|
8
|
-
import { prepareProject } from '../../host/channel.js';
|
|
10
|
+
import { currentSessions, prepareProject } from '../../host/channel.js';
|
|
9
11
|
import { projectHere, readRegistry } from '../../host/projects.js';
|
|
10
12
|
import { joinRunning } from '../../host/join.js';
|
|
13
|
+
import { c } from '../colors.js';
|
|
11
14
|
import { working } from '../progress.js';
|
|
15
|
+
import { describeSessions } from './restart.js';
|
|
12
16
|
import { guestPathFor, sandboxAlive } from './context.js';
|
|
13
17
|
/** The folder the command is about: --workspace, else the current one. */
|
|
14
18
|
export const hereDir = (ctx) => path.resolve(ctx.options.workspace ?? process.cwd());
|
|
@@ -28,12 +32,16 @@ export const locateHere = (ctx, status) => {
|
|
|
28
32
|
* it): its .sandbox-config.json is approved like at a launch, its protections are checked, and it is made visible.
|
|
29
33
|
* Throws NeedsRestart (host/projects.ts) when that is impossible without a restart.
|
|
30
34
|
*/
|
|
31
|
-
|
|
35
|
+
let restartedOnce = false;
|
|
36
|
+
/** restart.ts: the sandbox was restarted for a project in this command. */
|
|
37
|
+
export const markRestarted = () => { restartedOnce = true; };
|
|
38
|
+
export const joinHere = async (ctx, status, o = {}) => {
|
|
32
39
|
const { options, loaded, packageRoot, storageRoot, dataPath } = ctx;
|
|
33
40
|
const dir = hereDir(ctx);
|
|
34
41
|
// A sandbox started before projects existed: only its launch folder (guestPathFor explains why otherwise).
|
|
35
42
|
if (!readRegistry(storageRoot))
|
|
36
43
|
return { project: null, guestDir: guestPathFor(dir, status.workspaceHost, status.guestWorkspace), spec: null, harness: null, notes: [] };
|
|
44
|
+
let shown = false;
|
|
37
45
|
const joined = await joinRunning({
|
|
38
46
|
storageRoot, dataPath, packageRoot, hostDir: dir, wsbShare: loaded.config.projects.wsbShare,
|
|
39
47
|
resolve: (root) => resolveRun({
|
|
@@ -41,10 +49,41 @@ export const joinHere = async (ctx, status) => {
|
|
|
41
49
|
noLocalConfig: options.noLocalConfig, trustConfig: options.trustConfig, scripts: options.scripts, databases: options.databases,
|
|
42
50
|
nodeVersion: options.node, pythonVersion: options.python, ask: askYesNo,
|
|
43
51
|
}),
|
|
52
|
+
// Something it wants only comes with a restart: --yes restarts, a terminal asks, anything else continues.
|
|
53
|
+
decide: async (name, missing) => {
|
|
54
|
+
// Once per command: what a restart could not give (see the launch notes) never restarts it again.
|
|
55
|
+
if (o.noRestart || restartedOnce)
|
|
56
|
+
return 'continue';
|
|
57
|
+
if (options.yes)
|
|
58
|
+
return 'restart';
|
|
59
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
60
|
+
return 'continue';
|
|
61
|
+
console.log(c.yellow(describeMissing(name, missing)));
|
|
62
|
+
shown = true;
|
|
63
|
+
const sessions = currentSessions(dataPath);
|
|
64
|
+
return select({
|
|
65
|
+
message: 'What now?',
|
|
66
|
+
choices: [
|
|
67
|
+
{ name: 'Continue anyway (join without them)', value: 'continue' },
|
|
68
|
+
{ name: 'Restart with them (' + (sessions.length ? describeSessions(sessions) + ' will end' : 'no session is running') + ')', value: 'restart' },
|
|
69
|
+
{ name: 'Cancel', value: 'cancel' },
|
|
70
|
+
],
|
|
71
|
+
default: 'continue',
|
|
72
|
+
});
|
|
73
|
+
},
|
|
44
74
|
});
|
|
45
75
|
const harness = joined.run && joined.run.harnessName !== 'none' ? joined.run.harnessName : null;
|
|
46
|
-
|
|
76
|
+
const notes = joined.missing ? [...joined.notes, ...missingNotes(ctx, joined.project.name, joined.project.host, joined.missing, shown)] : joined.notes;
|
|
77
|
+
return { project: joined.project, guestDir: joined.guestDir, spec: joined.spec, harness, notes };
|
|
47
78
|
};
|
|
79
|
+
/** The command that restarts the running sandbox with this folder's boot wishes (consent given by --yes). */
|
|
80
|
+
export const restartCommand = (ctx, host) => 'sandbox ' + (ctx.options.noAgent ? 'start' : ctx.options.harness ?? ctx.loaded.config.defaultHarness) + ' --workspace "' + host + '" --yes';
|
|
81
|
+
/** Joined without what it wants at boot: what is missing, how to restart with it, and how to always have it. */
|
|
82
|
+
export const missingNotes = (ctx, name, host, missing, shown = false) => [
|
|
83
|
+
...(shown ? [] : [c.yellow(describeMissing(name, missing))]),
|
|
84
|
+
'Joined without them. To restart the sandbox with them (its sessions end): ' + restartCommand(ctx, host),
|
|
85
|
+
c.dim(missingTip(missing)),
|
|
86
|
+
];
|
|
48
87
|
/** Waits until the sandbox has set a project up, with its progress on a spinner (plain lines without a terminal). */
|
|
49
88
|
export const provisionJoined = async (ctx, joined) => {
|
|
50
89
|
if (!joined.spec)
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// where the agent supports it) and then starts the new project's session.
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import _ from 'lodash';
|
|
8
|
+
import { missingTip } from '../../core/project-policy.js';
|
|
8
9
|
import { askYesNo } from '../../core/prompt.js';
|
|
9
10
|
import { fail, safeText, sleep } from '../../core/util.js';
|
|
10
11
|
import { currentSessions, execInSandbox } from '../../host/channel.js';
|
|
@@ -44,14 +45,20 @@ export const offerRestart = async (ctx, needs, then) => {
|
|
|
44
45
|
const registry = readRegistry(ctx.storageRoot) ?? fail('The running sandbox was started by an older version, so it cannot take another folder.', 'Stop it (sandbox stop) and start it again from ' + folders[0] + '.');
|
|
45
46
|
const primary = registry.projects.find((p) => p.primary) ?? fail('The running sandbox has no launch folder on record.', 'Stop it (sandbox stop) and start it again.');
|
|
46
47
|
const sessions = currentSessions(ctx.dataPath);
|
|
47
|
-
|
|
48
|
+
// A project that only wants boot settings or folders: you already chose the restart (or gave --yes) when asked.
|
|
49
|
+
const hidden = needs.filter((n) => !n.missing);
|
|
50
|
+
for (const need of hidden)
|
|
48
51
|
console.log(c.yellow('Windows Sandbox cannot add ' + need.hostDir + ' while it runs: ' + need.reason + '.'));
|
|
49
|
-
console.log('A restart maps ' + (
|
|
52
|
+
console.log((hidden.length ? 'A restart maps ' + (hidden.length === 1 ? 'it' : 'them') + ' as well' : 'Restarting with what ' + needs.map((n) => path.basename(n.hostDir)).join(', ') + ' wants') +
|
|
53
|
+
' (and keeps ' + registry.projects.map((p) => p.host).join(', ') + '). ' +
|
|
50
54
|
(sessions.length ? describeSessions(sessions) + ' will end' : 'No session is running') + '; your files, tools, databases and logins are kept. It takes about a minute.');
|
|
51
|
-
for (const parent of _.uniq(
|
|
55
|
+
for (const parent of _.uniq(hidden.map((n) => path.dirname(n.hostDir))))
|
|
52
56
|
console.log(c.dim('No restart next time for folders in ' + parent + ': sandbox projects roots add "' + parent + '"'));
|
|
57
|
+
for (const need of needs)
|
|
58
|
+
if (need.missing)
|
|
59
|
+
console.log(c.dim(missingTip(need.missing)));
|
|
53
60
|
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
54
|
-
if (!ctx.options.yes) {
|
|
61
|
+
if (!ctx.options.yes && hidden.length) {
|
|
55
62
|
if (!interactive)
|
|
56
63
|
fail('Not restarting without your consent.', 'Run it again with --yes to restart: ' + againWithYes(ctx));
|
|
57
64
|
if (!(await askYesNo('Restart the sandbox now?'))) {
|
|
@@ -62,9 +69,10 @@ export const offerRestart = async (ctx, needs, then) => {
|
|
|
62
69
|
const reopen = sessionsToReopen(registry, sessions);
|
|
63
70
|
const resume = reopen.length > 0 && (ctx.options.yes || (await askYesNo('Reopen the ' + reopen.length + ' session(s) afterwards (continuing their conversations where the agent supports it)?', true)));
|
|
64
71
|
await stopAndWait(ctx);
|
|
65
|
-
// Imported here: launch.ts imports this file.
|
|
72
|
+
// Imported here: launch.ts imports this file (and project.ts imports describeSessions).
|
|
66
73
|
const { launch, openSession, readySandbox } = await import('./launch.js');
|
|
67
|
-
const { joinHere } = await import('./project.js');
|
|
74
|
+
const { joinHere, markRestarted } = await import('./project.js');
|
|
75
|
+
markRestarted();
|
|
68
76
|
const keep = registry.projects.filter((p) => !p.primary && p.mechanism !== 'root').map((p) => p.host);
|
|
69
77
|
const relaunch = { ...ctx, options: { ...ctx.options, workspace: primary.host, harness: ctx.options.harness ?? ctx.options.sessions[0]?.harness, terminal: false, noAgent: false, newSession: true, passthrough: [], sessions: [] } };
|
|
70
78
|
await launch(relaunch, false, { extraProjects: [...keep, ...folders], autoSession: false, waitUntilReady: true });
|
|
@@ -73,7 +81,9 @@ export const offerRestart = async (ctx, needs, then) => {
|
|
|
73
81
|
for (const s of reopen) {
|
|
74
82
|
const at = { ...ctx, options: { ...ctx.options, workspace: s.hostDir, harness: s.harness, terminal: false, newSession: true, passthrough: [], sessions: [] } };
|
|
75
83
|
try {
|
|
76
|
-
const joined = await joinHere(at, ready);
|
|
84
|
+
const joined = await joinHere(at, ready, { noRestart: true });
|
|
85
|
+
for (const note of joined.notes)
|
|
86
|
+
console.log(note);
|
|
77
87
|
await openSession(at, ready, joined, { attach: false, harness: s.harness, args: ctx.loaded.config.harnesses[s.harness]?.resumeArguments ?? [], reuse: false });
|
|
78
88
|
}
|
|
79
89
|
catch (e) {
|
|
@@ -40,7 +40,7 @@ export const attach = async (ctx) => {
|
|
|
40
40
|
const projects = readRegistry(ctx.storageRoot)?.projects ?? [];
|
|
41
41
|
const chosen = await select({
|
|
42
42
|
message: 'Which session?' + (here.project ? ' (' + here.project.name + ' first)' : ''),
|
|
43
|
-
choices: running.map((s) => ({ value: s.id, name: (here.guestDir && isWithin(here.guestDir, s.cwd) ? '* ' : ' ') + safeText(s.harness).padEnd(9) + ' ' + projectOf(projects, s).padEnd(16) + ' ' + ago(s.startedAt).padEnd(11) + ' ' + s.id })),
|
|
43
|
+
choices: running.map((s) => ({ value: s.id, name: (here.guestDir && isWithin(here.guestDir, s.cwd) ? '* ' : ' ') + safeText(s.harness).padEnd(9) + ' ' + safeText(projectOf(projects, s)).padEnd(16) + ' ' + ago(s.startedAt).padEnd(11) + ' ' + s.id })),
|
|
44
44
|
});
|
|
45
45
|
return attachHere(ctx, pickSession(ctx.dataPath, chosen));
|
|
46
46
|
}
|
|
@@ -155,13 +155,14 @@ const printExecResult = (result) => {
|
|
|
155
155
|
if (result.stderr)
|
|
156
156
|
process.stderr.write(safeText(result.stderr));
|
|
157
157
|
if (!result.ok && !result.stdout && !result.stderr)
|
|
158
|
-
console.error(result.message);
|
|
158
|
+
console.error(safeText(result.message));
|
|
159
159
|
};
|
|
160
160
|
/** The guest folder for this folder, joining the running sandbox (and setting the project up) when needed. */
|
|
161
161
|
const guestFolderHere = async (ctx, what) => {
|
|
162
162
|
const ready = await readySandbox(ctx.dataPath);
|
|
163
163
|
try {
|
|
164
|
-
|
|
164
|
+
// A command needs no restart for what the project wants at boot: it runs without them (and says so).
|
|
165
|
+
const joined = await joinHere(ctx, ready, { noRestart: true });
|
|
165
166
|
for (const note of joined.notes)
|
|
166
167
|
console.error(note);
|
|
167
168
|
await provisionJoined(ctx, joined);
|
|
@@ -210,7 +211,7 @@ export const restart = async (ctx) => {
|
|
|
210
211
|
const scope = ctx.options.all || !here.project ? undefined : here.project.guest;
|
|
211
212
|
if (!scope && !ctx.options.all)
|
|
212
213
|
console.log(c.dim(hereDir(ctx) + ' is not a project of the running sandbox: restarting every session.'));
|
|
213
|
-
console.log((await working('Restarting the agents' + (scope ? ' of ' + here.project?.name : ''), () => restartSessions(ctx.dataPath, ctx.options.harness ?? null, sandboxAlive, scope))).message);
|
|
214
|
+
console.log(safeText((await working('Restarting the agents' + (scope ? ' of ' + here.project?.name : ''), () => restartSessions(ctx.dataPath, ctx.options.harness ?? null, sandboxAlive, scope))).message));
|
|
214
215
|
return 0;
|
|
215
216
|
};
|
|
216
217
|
export const stop = async (ctx) => {
|
|
@@ -6,7 +6,7 @@ import { parse } from 'jsonc-parser';
|
|
|
6
6
|
import _ from 'lodash';
|
|
7
7
|
import { userConfigPath } from '../../core/config.js';
|
|
8
8
|
import { fail } from '../../core/util.js';
|
|
9
|
-
import { editConfigText } from '../settings/
|
|
9
|
+
import { editConfigText } from '../menu/settings/config-file.js';
|
|
10
10
|
export const configFile = (ctx) => ctx.loaded.configPath ?? userConfigPath();
|
|
11
11
|
/** The value as written in your file (not merged with the defaults). */
|
|
12
12
|
export const fileSetting = (ctx, key, fallback) => {
|
package/dist/cli/index.js
CHANGED
|
@@ -5,11 +5,11 @@ import { buildProgram, parseCli } from './args.js';
|
|
|
5
5
|
import { commandSpec } from './manifest.js';
|
|
6
6
|
import { c } from './colors.js';
|
|
7
7
|
import { recordHistory } from './history.js';
|
|
8
|
-
import { menu } from './menu.js';
|
|
8
|
+
import { menu } from './menu/index.js';
|
|
9
9
|
import { validateOptions } from './validate.js';
|
|
10
10
|
import { loadConfig } from '../core/config.js';
|
|
11
11
|
import * as commands from './commands/index.js';
|
|
12
|
-
import { UserError, errMsg, fail } from '../core/util.js';
|
|
12
|
+
import { UserError, errMsg, fail, plainText } from '../core/util.js';
|
|
13
13
|
/** The package folder (this file is dist/cli/index.js). */
|
|
14
14
|
export const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
15
15
|
const entry = path.join(packageRoot, 'dist', 'bin', 'agent-sandbox.js');
|
|
@@ -30,6 +30,7 @@ const HANDLERS = {
|
|
|
30
30
|
projects: commands.projects,
|
|
31
31
|
sync: commands.sync,
|
|
32
32
|
logs: commands.logs,
|
|
33
|
+
audit: commands.audit,
|
|
33
34
|
clean: commands.clean,
|
|
34
35
|
config: commands.config,
|
|
35
36
|
doctor: commands.doctor,
|
|
@@ -106,9 +107,10 @@ export const report = (e) => {
|
|
|
106
107
|
if (e instanceof CommanderError)
|
|
107
108
|
return ['commander.helpDisplayed', 'commander.help', 'commander.version'].includes(e.code) ? 0 : e.exitCode || 1;
|
|
108
109
|
if (e instanceof UserError) {
|
|
109
|
-
|
|
110
|
+
// Messages can quote what the sandbox wrote (a session's status, an address): never with its control codes.
|
|
111
|
+
console.error(c.red('Error: ') + plainText(e.message));
|
|
110
112
|
if (e.fix)
|
|
111
|
-
console.error(' Fix: ' + e.fix);
|
|
113
|
+
console.error(' Fix: ' + plainText(e.fix));
|
|
112
114
|
return 1;
|
|
113
115
|
}
|
|
114
116
|
console.error(c.red('Unexpected error: ') + errMsg(e));
|
package/dist/cli/manifest.js
CHANGED
|
@@ -57,7 +57,7 @@ const SANDBOX_OPTIONS = [
|
|
|
57
57
|
bool('--window', 'show the sandbox window even if config says headless', { advanced: true }),
|
|
58
58
|
bool('--trust-config', 'approve the folder\'s .sandbox-config.json without asking', { advanced: true }),
|
|
59
59
|
bool('--no-local-config', 'ignore the folder\'s .sandbox-config.json', { advanced: true }),
|
|
60
|
-
bool('--yes', 'when this folder is not in the running sandbox
|
|
60
|
+
bool('--yes', 'when this folder is not in the running sandbox, or wants what it started without (vGPU, folders), restart without asking', { noMenu: true }),
|
|
61
61
|
];
|
|
62
62
|
const AGENT_OPTIONS = [
|
|
63
63
|
text('--harness', 'name', 'agent to run', { choices: 'harnesses' }),
|
|
@@ -93,6 +93,11 @@ export const COMMANDS = z.array(CommandSpec).parse([
|
|
|
93
93
|
{ name: 'data', summary: 'show what is kept between sandboxes (tools, databases, logins, logs), or delete some of it', group: 'Data', args: [{ name: 'action', description: 'show or clean', required: false, choices: ['show', 'clean'] }, { name: 'targets', description: 'what to clean, e.g. tools/go databases logs', required: false, variadic: true, when: 'clean' }], options: [bool('--yes', 'do not ask before deleting', { noMenu: true })], json },
|
|
94
94
|
{ name: 'clean', summary: 'delete old launch folders, logs and leftovers', group: 'Data', args: [], options: [{ flag: '--days', type: 'number', value: 'n', description: 'launch folders older than this many days', default: 7 }] },
|
|
95
95
|
{ name: 'sync', summary: 'copy conversation history back to this PC now', group: 'Data', args: [], options: [] },
|
|
96
|
+
{
|
|
97
|
+
name: 'audit', summary: 'what happened in your sandboxes: programs started, files changed, sessions, repo guard (audit.enabled)', group: 'Health', json,
|
|
98
|
+
args: [],
|
|
99
|
+
options: [{ flag: '--days', type: 'number', value: 'n', description: 'the last n days (default: today)', default: 1 }, text('--kind', 'kind', 'only proc, file, session or guard events', { choices: ['proc', 'file', 'session', 'guard'] }), text('--grep', 'text', 'only events containing this text')],
|
|
100
|
+
},
|
|
96
101
|
{ name: 'logs', summary: 'print the newest sandbox log', group: 'Health', args: [], options: [bool('--follow', 'keep printing new lines', { short: '-f' })] },
|
|
97
102
|
{ name: 'setup', summary: 'check this PC can run Windows Sandbox and offer to turn it on', group: 'Health', args: [], options: [] },
|
|
98
103
|
{ name: 'doctor', summary: 'check this PC and show what will be copied or downloaded', group: 'Health', args: [], options: [text('--harness', 'name', 'agent to check', { choices: 'harnesses' })] },
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { App } from '../tui/index.js';
|
|
2
|
+
import type { ConfigFile } from './settings/config-file.js';
|
|
3
|
+
export declare class AppsCatalogue {
|
|
4
|
+
private readonly app;
|
|
5
|
+
private readonly file;
|
|
6
|
+
private readonly dataPath;
|
|
7
|
+
constructor(app: App, file: ConfigFile, dataPath: string);
|
|
8
|
+
/** Downloaded already (into this data folder), so the next sandbox starts it without a download. */
|
|
9
|
+
private installed;
|
|
10
|
+
/** Is this entry on now (its setting, or listed in apps.install)? */
|
|
11
|
+
private chosen;
|
|
12
|
+
private item;
|
|
13
|
+
/** Shows the catalogue until you save (Enter) or leave it (Esc); returns the notice to show afterwards. */
|
|
14
|
+
run(): Promise<string | undefined>;
|
|
15
|
+
private save;
|
|
16
|
+
/** Your own programs: the apps.custom map, each entry a form (GitHub release or link, program, command line or not). */
|
|
17
|
+
private editCustom;
|
|
18
|
+
}
|
|
19
|
+
/** For the home screen: "12 apps chosen". */
|
|
20
|
+
export declare const appsSummary: (file: ConfigFile) => string;
|
|
21
|
+
export declare const appLabel: (id: string) => string;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// The apps catalogue in the menu: every app by category, searchable, ticked = installed in every sandbox. VS Code and
|
|
2
|
+
// Windows Terminal are listed too (they have settings of their own, which ticking changes). Your own programs
|
|
3
|
+
// (apps.custom) are edited as a list of small forms. Saved to your config file (or, for catalogue apps only, the
|
|
4
|
+
// project's .sandbox-config.json).
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import _ from 'lodash';
|
|
8
|
+
import { APP_CATEGORIES, APPS } from '../../shared/apps.js';
|
|
9
|
+
import { Text, Theme } from '../tui/index.js';
|
|
10
|
+
import { ValueEditor } from './settings/editors.js';
|
|
11
|
+
const CUSTOM = '@custom';
|
|
12
|
+
export class AppsCatalogue {
|
|
13
|
+
app;
|
|
14
|
+
file;
|
|
15
|
+
dataPath;
|
|
16
|
+
constructor(app, file, dataPath) {
|
|
17
|
+
this.app = app;
|
|
18
|
+
this.file = file;
|
|
19
|
+
this.dataPath = dataPath;
|
|
20
|
+
}
|
|
21
|
+
/** Downloaded already (into this data folder), so the next sandbox starts it without a download. */
|
|
22
|
+
installed(entry) {
|
|
23
|
+
if (entry.id === 'vscode')
|
|
24
|
+
return fs.existsSync(path.join(this.dataPath, 'tools', 'vscode', 'Code.exe'));
|
|
25
|
+
if (entry.id === 'windows-terminal')
|
|
26
|
+
return fs.existsSync(path.join(this.dataPath, 'tools', 'terminal', 'WindowsTerminal.exe'));
|
|
27
|
+
return fs.existsSync(path.join(this.dataPath, 'tools', 'apps', entry.id, '.agent-sandbox-app'));
|
|
28
|
+
}
|
|
29
|
+
/** Is this entry on now (its setting, or listed in apps.install)? */
|
|
30
|
+
chosen(entry, install) {
|
|
31
|
+
return entry.setting ? _.get(this.file.userConfig(), entry.setting) === true : install.includes(entry.id);
|
|
32
|
+
}
|
|
33
|
+
item(entry, install) {
|
|
34
|
+
const notes = [
|
|
35
|
+
Theme.muted('Download ') + '~' + entry.size + ' once, kept in C:\\AgentData\\tools\\' + (entry.setting ? '' : 'apps\\') + entry.id,
|
|
36
|
+
Theme.muted('Starts ') + (entry.cli ? 'from a terminal (on PATH)' : 'from the desktop or Start > Sandbox apps'),
|
|
37
|
+
...(entry.needs?.includes('mesa') ? [Theme.muted('Graphics ') + 'OpenGL on the CPU (Mesa) is added for it'] : []),
|
|
38
|
+
...(entry.fullGitOnly ? [Theme.warn('Needs provisioning.git.access "full" (it pushes with its own git).')] : []),
|
|
39
|
+
...(entry.setting ? [Theme.muted('Setting ') + entry.setting] : []),
|
|
40
|
+
];
|
|
41
|
+
return {
|
|
42
|
+
value: entry.id, label: entry.name, category: entry.category, description: entry.about, details: notes, keywords: entry.id,
|
|
43
|
+
hint: (this.installed(entry) ? Theme.ok('installed ') : '') + Theme.muted(entry.size), checked: this.chosen(entry, install),
|
|
44
|
+
...(this.file.isProject && entry.setting ? { disabled: 'A project cannot change this; it is in your own settings.' } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Shows the catalogue until you save (Enter) or leave it (Esc); returns the notice to show afterwards. */
|
|
48
|
+
async run() {
|
|
49
|
+
for (;;) {
|
|
50
|
+
const install = (this.file.isProject ? this.file.valueInFile(['apps', 'install']) : this.file.userConfig().apps.install) ?? [];
|
|
51
|
+
const custom = this.file.isProject ? {} : this.file.userConfig().apps.custom;
|
|
52
|
+
const entries = APPS.filter((a) => !(this.file.isProject && a.setting));
|
|
53
|
+
const picked = await this.app.list({
|
|
54
|
+
path: [this.file.isProject ? 'Project settings' : 'Apps'],
|
|
55
|
+
intro: 'Tick the apps every sandbox should have (space), then Enter to save. Each is downloaded once and kept; c = your own programs.',
|
|
56
|
+
multi: true,
|
|
57
|
+
categories: [...APP_CATEGORIES, ...(this.file.isProject ? [] : [{ id: 'custom', label: 'Your own programs', description: 'Programs you added yourself (c to add or change them).' }])],
|
|
58
|
+
items: [
|
|
59
|
+
...entries.map((a) => this.item(a, install)),
|
|
60
|
+
...Object.entries(custom).map(([id, a]) => ({ value: CUSTOM + id, label: a.name, category: 'custom', description: a.github ? 'From the latest ' + a.github + ' release.' : 'From ' + a.url, hint: Theme.muted(a.cli ? 'command line' : 'desktop'), checked: true, disabled: 'Your own programs are always installed; c to change or remove them.' })),
|
|
61
|
+
],
|
|
62
|
+
actions: this.file.isProject ? [] : [{ key: 'c', label: 'your own programs' }],
|
|
63
|
+
});
|
|
64
|
+
if (picked.kind === 'back')
|
|
65
|
+
return undefined;
|
|
66
|
+
if (picked.kind === 'action') {
|
|
67
|
+
const notice = await this.editCustom();
|
|
68
|
+
if (notice)
|
|
69
|
+
return notice;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (picked.kind !== 'multi')
|
|
73
|
+
continue;
|
|
74
|
+
return this.save(picked.values.filter((v) => !v.startsWith(CUSTOM)));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
save(ids) {
|
|
78
|
+
const problems = [];
|
|
79
|
+
let changed = 0;
|
|
80
|
+
for (const entry of APPS.filter((a) => a.setting && !this.file.isProject)) {
|
|
81
|
+
const on = ids.includes(entry.id);
|
|
82
|
+
if (on === this.chosen(entry, []))
|
|
83
|
+
continue;
|
|
84
|
+
const result = this.file.set(entry.setting.split('.'), on);
|
|
85
|
+
if (typeof result === 'object')
|
|
86
|
+
problems.push(...result.problems);
|
|
87
|
+
else if (result === 'saved')
|
|
88
|
+
changed++;
|
|
89
|
+
}
|
|
90
|
+
const install = APPS.filter((a) => !a.setting && ids.includes(a.id)).map((a) => a.id);
|
|
91
|
+
const result = this.file.set(['apps', 'install'], install.length || this.file.isProject ? install : undefined);
|
|
92
|
+
if (typeof result === 'object')
|
|
93
|
+
problems.push(...result.problems);
|
|
94
|
+
else if (result === 'saved')
|
|
95
|
+
changed++;
|
|
96
|
+
if (problems.length)
|
|
97
|
+
return Theme.error(Theme.glyph.cross + ' Not saved: ' + problems.join('; '));
|
|
98
|
+
if (!changed)
|
|
99
|
+
return Theme.muted('Apps unchanged.');
|
|
100
|
+
return Theme.ok(Theme.glyph.ok + ' Saved: ' + (install.length ? install.length + ' app' + (install.length === 1 ? '' : 's') : 'no apps') + '.') + Theme.muted(this.file.isProject ? ' The next launch asks you to approve the project file again.' : ' The next sandbox installs what it does not have yet.');
|
|
101
|
+
}
|
|
102
|
+
/** Your own programs: the apps.custom map, each entry a form (GitHub release or link, program, command line or not). */
|
|
103
|
+
async editCustom() {
|
|
104
|
+
const node = this.file.schema.child('apps')?.child('custom');
|
|
105
|
+
if (!node)
|
|
106
|
+
return undefined;
|
|
107
|
+
const edited = await new ValueEditor(this.app).edit(['Apps', 'Your own programs'], ['apps', 'custom'], node, this.file.userConfig().apps.custom);
|
|
108
|
+
if (!edited)
|
|
109
|
+
return undefined;
|
|
110
|
+
const result = this.file.set(['apps', 'custom'], edited.value);
|
|
111
|
+
if (typeof result === 'object')
|
|
112
|
+
return Theme.error(Theme.glyph.cross + ' Not saved: ' + result.problems.join('; '));
|
|
113
|
+
return result === 'saved' ? Theme.ok(Theme.glyph.ok + ' Saved your programs (' + Object.keys(edited.value).length + ').') : undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** For the home screen: "12 apps chosen". */
|
|
117
|
+
export const appsSummary = (file) => {
|
|
118
|
+
const config = file.userConfig();
|
|
119
|
+
const n = config.apps.install.length + Object.keys(config.apps.custom).length;
|
|
120
|
+
return n ? n + ' chosen' : 'none chosen';
|
|
121
|
+
};
|
|
122
|
+
export const appLabel = (id) => APPS.find((a) => a.id === id)?.name ?? Text.humanize(id);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Context } from '../commands/context.js';
|
|
2
|
+
import { type CommandSpecT, type OptionSpecT } from '../manifest.js';
|
|
3
|
+
import { App } from '../tui/index.js';
|
|
4
|
+
/** What an option accepts, checked while typing (undefined = fine). */
|
|
5
|
+
export declare const optionProblem: (o: OptionSpecT, v: string) => string | undefined;
|
|
6
|
+
export declare class CommandForm {
|
|
7
|
+
private readonly app;
|
|
8
|
+
private readonly ctx;
|
|
9
|
+
private readonly spec;
|
|
10
|
+
/** Answers by field id (arg:<name>, opt:<flag>, json, extra). */
|
|
11
|
+
private readonly values;
|
|
12
|
+
constructor(app: App, ctx: Context, spec: CommandSpecT, preset?: string[]);
|
|
13
|
+
/** Values a choices list names, filled in from config and the running sandbox. */
|
|
14
|
+
private choices;
|
|
15
|
+
private first;
|
|
16
|
+
private fields;
|
|
17
|
+
private id;
|
|
18
|
+
/** The command line the form stands for now. */
|
|
19
|
+
argv(): string[];
|
|
20
|
+
/** The name of a required argument that has no value yet. */
|
|
21
|
+
private missing;
|
|
22
|
+
private item;
|
|
23
|
+
/** Shows the form until you run the command (its argv) or go back (null). */
|
|
24
|
+
run(): Promise<string[] | null>;
|
|
25
|
+
private edit;
|
|
26
|
+
/** `data clean` targets, ticked from a list with their sizes (never "all": that is typed on purpose). */
|
|
27
|
+
private dataTargets;
|
|
28
|
+
}
|