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,6 +4,7 @@ import fs from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { syncClaudeMem } from '../sync/claude-mem.js';
|
|
6
6
|
import { readSyncState, syncHistoryIn, syncHistoryOut, syncStatePath, translatePaths } from '../sync/history.js';
|
|
7
|
+
import { beatLife, endLife, explainStop, lastStopWarning, startLife } from '../host/stops.js';
|
|
7
8
|
export const syncSuite = {
|
|
8
9
|
name: 'sync',
|
|
9
10
|
title: 'history and claude-mem sync',
|
|
@@ -19,13 +20,17 @@ export const syncSuite = {
|
|
|
19
20
|
t.write(path.join(hostProject, 's1.jsonl'), '{"cwd":"D:\\\\Code\\\\my-app","t":1}\n');
|
|
20
21
|
t.write(path.join(hostDot, 'projects', 'C--Other', 's2.jsonl'), '{"x":1}\n');
|
|
21
22
|
t.write(path.join(hostDot, 'todos', 's1.json'), '[]');
|
|
22
|
-
t.write(path.join(hostDot, 'history.jsonl'), '{"display":"hi","timestamp":1,"project":"D:\\\\Code\\\\my-app"}\n');
|
|
23
|
+
t.write(path.join(hostDot, 'history.jsonl'), '{"display":"hi","timestamp":1,"project":"D:\\\\Code\\\\my-app"}\n{"display":"secret plan","timestamp":0,"project":"C:\\\\Other"}\n');
|
|
23
24
|
const sides = { hostClaudeDir: hostDot, guestClaudeDir: guestDot, hostWorkspace: hostWs, guestWorkspace: guestWs };
|
|
24
25
|
syncHistoryIn(sides);
|
|
25
26
|
t.includes(fs.readFileSync(path.join(guestProject, 's1.jsonl'), 'utf8'), 'C:\\\\Workspace\\\\my-app', 'history in: the current project is renamed and its paths translated');
|
|
26
|
-
t.check(fs.existsSync(path.join(guestDot, 'projects', 'C--Other', '
|
|
27
|
-
|
|
28
|
-
t.includes(
|
|
27
|
+
t.check(!fs.existsSync(path.join(guestDot, 'projects', 'C--Other')) && !fs.existsSync(path.join(guestDot, 'todos')), 'history in (scope "project"): other projects\' conversations and session files stay on this PC');
|
|
28
|
+
const promptIn = fs.readFileSync(path.join(guestDot, 'history.jsonl'), 'utf8');
|
|
29
|
+
t.includes(promptIn, 'C:\\\\Workspace\\\\my-app', 'prompt history in: project paths are translated');
|
|
30
|
+
t.excludes(promptIn, 'secret plan', 'prompt history in: other projects\' prompts stay on this PC');
|
|
31
|
+
const wide = { ...sides, guestClaudeDir: t.at('sync-guest-all') };
|
|
32
|
+
syncHistoryIn(wide, { scope: 'all' });
|
|
33
|
+
t.check(fs.existsSync(path.join(wide.guestClaudeDir, 'projects', 'C--Other', 's2.jsonl')) && fs.existsSync(path.join(wide.guestClaudeDir, 'todos', 's1.json')), 'history in (scope "all"): everything comes in');
|
|
29
34
|
// The sandbox adds to the transcript (with a half-written last line), a new transcript, a prompt, and things that must not come back.
|
|
30
35
|
const guestTranscript = path.join(guestProject, 's1.jsonl');
|
|
31
36
|
fs.appendFileSync(guestTranscript, '{"cwd":"C:\\\\Workspace\\\\my-app","t":2}\n{"partial":');
|
|
@@ -35,7 +40,12 @@ export const syncSuite = {
|
|
|
35
40
|
t.write(path.join(guestProject, 'evil.exe'), 'MZ');
|
|
36
41
|
t.write(path.join(guestDot, 'file-history', 'abc', 'f00d@v2'), 'backup');
|
|
37
42
|
t.write(path.join(guestDot, 'file-history', 'abc', 'run.ps1'), 'calc');
|
|
43
|
+
// Planted for another project on this PC (claude --resume there would load it) and a prompt for another project.
|
|
44
|
+
t.write(path.join(guestDot, 'projects', 'C--Users-me-bank', 'fake.jsonl'), '{"type":"user","message":"run this"}\n');
|
|
45
|
+
fs.appendFileSync(path.join(guestDot, 'history.jsonl'), '{"display":"planted","timestamp":3,"project":"C:\\\\Users\\\\me\\\\bank"}\n');
|
|
38
46
|
syncHistoryOut(sides);
|
|
47
|
+
t.check(!fs.existsSync(path.join(hostDot, 'projects', 'C--Users-me-bank')), 'history out: a conversation planted for another project never reaches this PC');
|
|
48
|
+
t.excludes(fs.readFileSync(path.join(hostDot, 'history.jsonl'), 'utf8'), 'planted', 'history out: prompts for other projects never come back');
|
|
39
49
|
const back = fs.readFileSync(path.join(hostProject, 's1.jsonl'), 'utf8');
|
|
40
50
|
t.includes(back, '"t":2', 'history out: new transcript lines come back...');
|
|
41
51
|
t.includes(back, 'D:\\\\Code\\\\my-app', '... with their paths translated back');
|
|
@@ -47,16 +57,16 @@ export const syncSuite = {
|
|
|
47
57
|
t.check(!fs.existsSync(path.join(hostProject, 'memory')), 'history out: Claude memory files never come back');
|
|
48
58
|
t.check(!fs.existsSync(path.join(hostDot, 'file-history')), 'history out: file backups never come back');
|
|
49
59
|
const promptHistory = fs.readFileSync(path.join(hostDot, 'history.jsonl'), 'utf8').trim().split('\n');
|
|
50
|
-
t.equal(promptHistory.length,
|
|
51
|
-
t.includes(promptHistory[
|
|
52
|
-
t.includes(promptHistory[
|
|
60
|
+
t.equal(promptHistory.length, 3, 'prompt history out: only the new line is appended');
|
|
61
|
+
t.includes(promptHistory[2], 'from sandbox', 'prompt history out: the new prompt came back');
|
|
62
|
+
t.includes(promptHistory[2], 'D:\\\\Code\\\\my-app', 'prompt history out: its project path is translated back');
|
|
53
63
|
syncHistoryOut(sides);
|
|
54
|
-
t.equal(fs.readFileSync(path.join(hostDot, 'history.jsonl'), 'utf8').trim().split('\n').length,
|
|
64
|
+
t.equal(fs.readFileSync(path.join(hostDot, 'history.jsonl'), 'utf8').trim().split('\n').length, 3, 'syncing again adds nothing (idempotent)');
|
|
55
65
|
t.section('history sync: what the sandbox may write into the prompt history');
|
|
56
66
|
const hostile = f.historySides('hostile-history', hostWs, guestWs);
|
|
57
|
-
t.write(path.join(hostile.guestClaudeDir, 'history.jsonl'), 'not json\n[1,2]\n{"display":"ok","timestamp":2}\n"text"\n');
|
|
67
|
+
t.write(path.join(hostile.guestClaudeDir, 'history.jsonl'), 'not json\n[1,2]\n{"display":"ok","timestamp":2,"project":"C:\\\\Workspace\\\\my-app"}\n{"display":"no project","timestamp":2}\n"text"\n');
|
|
58
68
|
syncHistoryOut(hostile);
|
|
59
|
-
t.equal(fs.readFileSync(path.join(hostile.hostClaudeDir, 'history.jsonl'), 'utf8'), '{"display":"ok","timestamp":2}\n', 'only JSON prompt-history entries come back from the sandbox, nothing else it wrote in that file');
|
|
69
|
+
t.equal(fs.readFileSync(path.join(hostile.hostClaudeDir, 'history.jsonl'), 'utf8'), '{"display":"ok","timestamp":2,"project":"D:\\\\Code\\\\my-app"}\n', 'only JSON prompt-history entries of this project come back from the sandbox, nothing else it wrote in that file');
|
|
60
70
|
t.section('sync state file');
|
|
61
71
|
const stateData = t.at('state-storage', 'data', 'd');
|
|
62
72
|
t.writeJson(syncStatePath(stateData), { hostClaudeDir: 'relative\\.claude', hostWorkspace: 'D:\\x', guestWorkspace: 'C:\\x' });
|
|
@@ -90,5 +100,35 @@ export const syncSuite = {
|
|
|
90
100
|
t.touch(t.write(hostDb, 'host-v3'), 30);
|
|
91
101
|
t.equal(syncClaudeMem(memHost, memGuest, memState, 'out'), 'conflict', 'claude-mem: both sides changed is reported as a conflict');
|
|
92
102
|
t.check(fs.readdirSync(t.temp).some((name) => name.startsWith('mem-host.conflict-')), 'claude-mem: this PC\'s database is kept as .conflict-<time> first', fs.readdirSync(t.temp).join(', '));
|
|
103
|
+
t.section('how the last sandbox ended');
|
|
104
|
+
const ev = (id, provider, at, process = '') => ({ id, provider, at, process });
|
|
105
|
+
t.equal(explainStop([ev(1074, 'User32', '2026-10-13T02:10:30Z', 'C:\\Windows\\servicing\\TrustedInstaller.exe (HOST)')])?.kind, 'updates', 'a restart by Windows Update is recognised');
|
|
106
|
+
t.equal(explainStop([ev(1074, 'User32', '2026-10-13T02:10:30Z', 'C:\\Windows\\System32\\winlogon.exe (HOST)')])?.text, 'this PC was restarted or shut down (by winlogon.exe)', 'another restart names the program');
|
|
107
|
+
t.equal(explainStop([ev(41, 'Microsoft-Windows-Kernel-Power', '2026-10-13T02:20:00Z'), ev(42, 'Microsoft-Windows-Kernel-Power', '2026-10-13T02:10:00Z')])?.kind, 'sleep', 'the first event counts (sleep, then a crash)');
|
|
108
|
+
t.equal(explainStop([ev(2004, 'Microsoft-Windows-Resource-Exhaustion-Detector', '2026-10-13T02:10:00Z')])?.kind, 'memory', 'low memory is recognised');
|
|
109
|
+
t.equal(explainStop([ev(41, 'SomethingElse', '2026-10-13T02:10:00Z')]), null, 'the same event id from another source is ignored');
|
|
110
|
+
// The message Hyper-V logged on 13 Oct 2026 when Windows inside the sandbox crashed.
|
|
111
|
+
const crashMessage = "'8abb707a-972e-42e2-9599-01d125d4595c' has encountered a fatal error. The guest operating system reported that it failed with the following error codes: ErrorCode0: 0x1A, ErrorCode1: 0x13003, ErrorCode2: 0x2346E5, ErrorCode3: 0x1";
|
|
112
|
+
t.equal(explainStop([ev(18590, 'Microsoft-Windows-Hyper-V-Worker', '2026-10-13T08:49:08Z', crashMessage)])?.text, 'Windows inside the sandbox crashed (stop code 0x1A, 0x13003)', 'a crash of Windows inside the sandbox is recognised, with its stop code');
|
|
113
|
+
t.equal(explainStop([ev(24, 'Microsoft-Windows-TerminalServices-LocalSessionManager', '2026-10-13T08:41:40Z', 'PC\\Admin')]), null, 'switching user is not taken for the cause (the sandbox keeps running)');
|
|
114
|
+
const lifeRoot = t.at('life-storage');
|
|
115
|
+
const lifeData = path.join(lifeRoot, 'data', 'testdata');
|
|
116
|
+
const noEvents = () => null;
|
|
117
|
+
t.equal(lastStopWarning(lifeRoot, lifeData, false, noEvents), null, 'no record: nothing to say');
|
|
118
|
+
startLife(lifeRoot, lifeData, true, new Date('2026-10-12T20:16:00Z'));
|
|
119
|
+
beatLife(lifeRoot, lifeData, new Date('2026-10-13T02:09:00Z'));
|
|
120
|
+
t.equal(lastStopWarning(lifeRoot, lifeData, true, noEvents), null, 'its watcher still runs: the sandbox has not ended');
|
|
121
|
+
let since = '';
|
|
122
|
+
const warning = lastStopWarning(lifeRoot, lifeData, false, (s) => { since = s; return { kind: 'updates', at: '2026-10-13T02:10:30Z', text: 'Windows restarted this PC to install updates' }; });
|
|
123
|
+
t.includes(warning ?? '', 'Windows restarted this PC to install updates', 'the PC stopped under a running sandbox: the next launch says why');
|
|
124
|
+
t.includes(warning ?? '', 'Pause updates', '... and what keeps it from happening again');
|
|
125
|
+
t.equal(since, '2026-10-13T02:09:00.000Z', 'the System log is read from the last time the sandbox was seen running');
|
|
126
|
+
t.equal(lastStopWarning(lifeRoot, lifeData, false, noEvents), null, 'said once only');
|
|
127
|
+
startLife(lifeRoot, lifeData, true);
|
|
128
|
+
endLife(lifeRoot, lifeData, null);
|
|
129
|
+
t.equal(lastStopWarning(lifeRoot, lifeData, false, noEvents), null, 'closed while this PC ran (by you): nothing to say');
|
|
130
|
+
startLife(lifeRoot, lifeData, true);
|
|
131
|
+
endLife(lifeRoot, lifeData, { kind: 'sleep', at: '2026-10-13T02:10:00Z', text: 'this PC went to sleep or hibernated' });
|
|
132
|
+
t.includes(lastStopWarning(lifeRoot, lifeData, false, noEvents) ?? '', 'sandbox.keepAwake', 'ended by sleep: the next launch names keepAwake');
|
|
93
133
|
},
|
|
94
134
|
};
|
|
@@ -5,6 +5,9 @@ import { javaMajorFrom, pickGoRelease, pickNodeVersion, pickPackageVersion, pick
|
|
|
5
5
|
import { parseReleasePage, parseReleaseTags } from '../shared/download.js';
|
|
6
6
|
import { decidePwsh, pcHasPwsh, PWSH_MAX_BYTES, readPwshScripts, usesPwsh } from '../core/detect-pwsh.js';
|
|
7
7
|
import { binstallAsset, CLI_TOOLS, decideCli, pwshAsset } from '../guest/toolchains/catalogue.js';
|
|
8
|
+
import { missingExtensions, vscodeExtensions, vscodeSettings } from '../guest/toolchains/vscode-profile.js';
|
|
9
|
+
import { APP_CATEGORIES, APPS, appsNeedMesa, appsToInstall, exeMatcher } from '../shared/apps.js';
|
|
10
|
+
import { Config, LocalOverrides } from '../core/schema.js';
|
|
8
11
|
export const toolchainsSuite = {
|
|
9
12
|
name: 'toolchains',
|
|
10
13
|
title: 'version pickers, release assets, PowerShell 7 and CLI tool decisions',
|
|
@@ -99,5 +102,32 @@ export const toolchainsSuite = {
|
|
|
99
102
|
const scanned = readPwshScripts(scanRoot);
|
|
100
103
|
t.equal(scanned.map((s) => s.path), ['moh.ps1', 'scripts/build.ps1'], 'pwsh scan: *.ps1 at the root and one folder down only (node_modules skipped)');
|
|
101
104
|
t.equal(scanned[1].text.length, PWSH_MAX_BYTES, 'pwsh scan: each script is read up to its cap');
|
|
105
|
+
t.section('VS Code set-up');
|
|
106
|
+
const vs = vscodeSettings({ javaHome: 'C:\\t\\java\\jdk-17.0.9+9', python: 'C:\\t\\python\\python.exe', pwsh: 'C:\\t\\pwsh\\pwsh.exe', gcc: 'C:\\t\\mingw\\bin\\gcc.exe', cl: 'C:\\t\\msvc\\cl.exe', dotnet: 'C:\\t\\dotnet\\dotnet.exe' });
|
|
107
|
+
t.equal([vs['java.jdt.ls.java.home'], vs['java.configuration.runtimes'][0].name, vs['python.defaultInterpreterPath'], vs['terminal.integrated.defaultProfile.windows'], vs['C_Cpp.default.compilerPath'], vs['dotnet.dotnetPath']], ['C:\\t\\java\\jdk-17.0.9+9', 'JavaSE-17', 'C:\\t\\python\\python.exe', 'PowerShell 7', 'C:\\t\\msvc\\cl.exe', 'C:\\t\\dotnet'], 'vscode: settings point at the sandbox\'s JDK (with its version), Python, PowerShell 7, compiler (MSVC first) and .NET');
|
|
108
|
+
t.check(!('go.goroot' in vs) && !('git.path' in vs), 'vscode: tools that are not installed are not set');
|
|
109
|
+
const ext = vscodeExtensions({ toolchains: ['node', 'java'], databases: ['postgres'], recommended: ['Foo.Bar', 'not an id'], extra: ['eamodio.gitlens', 'redhat.java'] });
|
|
110
|
+
t.check(ext.includes('redhat.java') && ext.includes('mtxr.sqltools-driver-pg') && ext.includes('foo.bar') && ext.includes('eamodio.gitlens') && !ext.includes('not an id') && ext.filter((e) => e === 'redhat.java').length === 1, 'vscode: extensions for the languages and databases in use, the project\'s and yours (no duplicates, nothing malformed)');
|
|
111
|
+
t.equal(missingExtensions(['golang.go', 'redhat.java'], ['golang.go-0.48.0', 'extensions.json']), ['redhat.java'], 'vscode: only extensions not installed yet are downloaded');
|
|
112
|
+
t.section('apps catalogue');
|
|
113
|
+
const ids = APPS.map((a) => a.id);
|
|
114
|
+
t.equal(ids.filter((id, i) => ids.indexOf(id) !== i), [], 'apps: every id is unique');
|
|
115
|
+
t.equal(APPS.filter((a) => !APP_CATEGORIES.some((c) => c.id === a.category)).map((a) => a.id), [], 'apps: every app is in a known category');
|
|
116
|
+
t.equal(APPS.filter((a) => 'github' in a.source && (() => { try {
|
|
117
|
+
new RegExp(a.source.asset);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return true;
|
|
122
|
+
} })()).map((a) => a.id), [], 'apps: every GitHub asset pattern compiles');
|
|
123
|
+
t.check(exeMatcher('/^Godot_v[\\d.]+-stable_win64\\.exe$/')('Godot_v4.7.2-stable_win64.exe') && !exeMatcher('/^Godot_v[\\d.]+-stable_win64\\.exe$/')('Godot_v4.7.2-stable_win64_console.exe') && exeMatcher('Code.exe')('code.EXE'), 'apps: the program is found by name or by pattern');
|
|
124
|
+
const chosen = appsToInstall({ install: ['firefox', 'jq', 'githubdesktop'], custom: { mine: { name: 'Mine', url: 'https://example.com/m.zip', exe: 'm.exe' } } }, 'readonly');
|
|
125
|
+
t.equal(chosen.entries.map((e) => e.id), ['7zip', 'firefox', 'jq', 'custom/mine'], 'apps: 7-Zip comes first when an installer needs it; your own programs follow');
|
|
126
|
+
t.equal(chosen.skipped.length, 1, 'apps: an app with its own git (GitHub Desktop) is skipped unless git access is "full"');
|
|
127
|
+
t.check(appsNeedMesa({ install: ['godot'], custom: {} }) && !appsNeedMesa({ install: ['jq'], custom: {} }), 'apps: game engines bring Mesa (OpenGL without a GPU) along');
|
|
128
|
+
const custom = (app) => Config.shape.apps.shape.custom.safeParse({ x: app }).success;
|
|
129
|
+
t.check(custom({ name: 'A', github: 'o/r', asset: 'win.*\\.zip$', exe: 'a.exe' }) && custom({ name: 'B', url: 'https://x.org/b.zip', exe: 'b.exe' }), 'apps: your own program comes from a GitHub release or an https link');
|
|
130
|
+
t.check(!custom({ name: 'C', url: 'http://x.org/c.zip', exe: 'c.exe' }) && !custom({ name: 'D', github: 'o/r', asset: '(', exe: 'd.exe' }) && !custom({ name: 'E', github: 'o/r', asset: 'x', url: 'https://x.org', exe: 'e.exe' }), 'apps: plain http, a broken pattern, or both sources are refused');
|
|
131
|
+
t.check(!LocalOverrides.safeParse({ apps: { custom: {} } }).success && LocalOverrides.safeParse({ apps: { install: ['jq'] } }).success, 'apps: a project file may add catalogue apps but never programs of its own');
|
|
102
132
|
},
|
|
103
133
|
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// workspaceFilter: the sandbox works on a copy of the project without what git ignores, kept in step both ways.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { IgnoreRules } from '../host/workspace-view/rules.js';
|
|
5
|
+
import { WorkspaceView } from '../host/workspace-view/view.js';
|
|
6
|
+
export const workspaceViewSuite = {
|
|
7
|
+
name: 'workspace-view',
|
|
8
|
+
title: 'git-ignored files kept out of the sandbox (filtered view, two-way sync)',
|
|
9
|
+
run: async (t, f) => {
|
|
10
|
+
t.section('a filtered launch');
|
|
11
|
+
const project = t.dir('filtered-app');
|
|
12
|
+
fs.mkdirSync(path.join(project, '.git'));
|
|
13
|
+
const filtered = { ...f.baseConfig, workspaceFilter: { enabled: true, exclude: [], include: [] } };
|
|
14
|
+
const gen = await f.launch({ cwd: project }, { config: filtered });
|
|
15
|
+
const workspace = gen.mappings.find((m) => m.guest.toLowerCase().endsWith('\\filtered-app'));
|
|
16
|
+
const git = gen.mappings.find((m) => m.guest.toLowerCase().endsWith('\\filtered-app.git'));
|
|
17
|
+
t.check(Boolean(workspace) && !workspace.host.toLowerCase().startsWith(project.toLowerCase()) && workspace.host.includes('views'), 'the sandbox maps the filtered view, not the project folder');
|
|
18
|
+
t.check(git?.host === path.join(project, '.git') && !git.readOnly, '... and the project\'s .git beside it, so git works');
|
|
19
|
+
const noGit = await f.launch({ cwd: project }, { config: { ...filtered, provisioning: { ...filtered.provisioning, git: { ...filtered.provisioning.git, access: 'none' } } } });
|
|
20
|
+
t.check(!noGit.mappings.some((m) => m.guest.toLowerCase().endsWith('.git')), 'with git access "none" no .git is mapped at all');
|
|
21
|
+
t.section('which files git ignores');
|
|
22
|
+
const rulesRoot = t.at('rules');
|
|
23
|
+
t.write(path.join(rulesRoot, '.gitignore'), 'node_modules/\n*.log\n!keep.log\n.env*\n!.env.example\ndist\n');
|
|
24
|
+
t.write(path.join(rulesRoot, 'sub', '.gitignore'), 'secret.txt\n!*.log\n');
|
|
25
|
+
t.write(path.join(rulesRoot, '.git', 'info', 'exclude'), 'local-notes.md\n');
|
|
26
|
+
const rules = new IgnoreRules(rulesRoot, ['*.pem'], ['dist/keep-me.js']);
|
|
27
|
+
const verdicts = Object.fromEntries(['src/a.ts', 'node_modules/x/index.js', 'a.log', 'keep.log', '.env', '.env.local', '.env.example', 'dist/out.js', 'sub/secret.txt', 'sub/b.log', 'local-notes.md', 'key.pem', 'dist/keep-me.js', '.git/config'].map((p) => [p, rules.isIgnored(p)]));
|
|
28
|
+
t.equal(verdicts, {
|
|
29
|
+
'src/a.ts': false, 'node_modules/x/index.js': true, 'a.log': true, 'keep.log': false, '.env': true, '.env.local': true, '.env.example': false,
|
|
30
|
+
'dist/out.js': true, 'sub/secret.txt': true, 'sub/b.log': false, 'local-notes.md': true, 'key.pem': true, 'dist/keep-me.js': false, '.git/config': true,
|
|
31
|
+
}, 'rules: nested .gitignore files, ! exceptions, .git/info/exclude, your own exclude and include patterns');
|
|
32
|
+
t.section('the view');
|
|
33
|
+
const host = t.at('project');
|
|
34
|
+
t.write(path.join(host, '.gitignore'), 'node_modules/\n.env\ndist/\n');
|
|
35
|
+
t.write(path.join(host, 'src', 'main.ts'), 'console.log(1);\n');
|
|
36
|
+
t.write(path.join(host, 'README.md'), '# hi\n');
|
|
37
|
+
t.write(path.join(host, '.env'), 'API_KEY=secret\n');
|
|
38
|
+
t.write(path.join(host, 'node_modules', 'pkg', 'index.js'), 'host build');
|
|
39
|
+
fs.mkdirSync(path.join(host, '.git'), { recursive: true });
|
|
40
|
+
const paths = WorkspaceView.pathsFor(t.at('storage'), host);
|
|
41
|
+
const view = new WorkspaceView({ ...paths, guestGitDir: 'C:\\Workspace\\project.git' });
|
|
42
|
+
view.prepare();
|
|
43
|
+
const inView = (rel) => fs.existsSync(path.join(paths.view, ...rel.split('/')));
|
|
44
|
+
t.check(inView('src/main.ts') && inView('README.md') && inView('.gitignore'), 'view: what git tracks is there');
|
|
45
|
+
t.check(!inView('.env') && !inView('node_modules'), 'view: ignored files (.env, node_modules) are not');
|
|
46
|
+
t.equal(fs.readFileSync(path.join(paths.view, '.git'), 'utf8'), 'gitdir: C:/Workspace/project.git\n', 'view: its .git points at the project\'s real .git (mapped separately)');
|
|
47
|
+
t.check(!paths.view.toLowerCase().startsWith(host.toLowerCase()), 'view: it lives outside the project');
|
|
48
|
+
t.section('both ways');
|
|
49
|
+
t.write(path.join(host, 'src', 'main.ts'), 'console.log(2);\n');
|
|
50
|
+
t.touch(path.join(host, 'src', 'main.ts'), 5);
|
|
51
|
+
view.syncChanged(['src/main.ts']);
|
|
52
|
+
t.equal(fs.readFileSync(path.join(paths.view, 'src', 'main.ts'), 'utf8'), 'console.log(2);\n', 'your edit reaches the sandbox');
|
|
53
|
+
t.write(path.join(paths.view, 'src', 'new.ts'), 'export {};\n');
|
|
54
|
+
t.write(path.join(paths.view, 'node_modules', 'pkg', 'index.js'), 'sandbox build');
|
|
55
|
+
t.write(path.join(paths.view, 'dist', 'bundle.js'), 'built');
|
|
56
|
+
view.syncAll();
|
|
57
|
+
t.check(fs.existsSync(path.join(host, 'src', 'new.ts')), 'a file the agent creates reaches your project');
|
|
58
|
+
t.equal(fs.readFileSync(path.join(host, 'node_modules', 'pkg', 'index.js'), 'utf8'), 'host build', 'the sandbox\'s own node_modules never overwrite yours');
|
|
59
|
+
t.check(!fs.existsSync(path.join(host, 'dist')), 'its build output stays in the sandbox');
|
|
60
|
+
fs.rmSync(path.join(paths.view, 'README.md'));
|
|
61
|
+
view.syncAll();
|
|
62
|
+
t.check(!fs.existsSync(path.join(host, 'README.md')), 'a deletion in the sandbox is repeated in your project');
|
|
63
|
+
fs.rmSync(path.join(host, 'src', 'new.ts'));
|
|
64
|
+
view.syncAll();
|
|
65
|
+
t.check(!inView('src/new.ts'), 'a deletion in your project is repeated in the sandbox');
|
|
66
|
+
t.section('conflicts and changing rules');
|
|
67
|
+
t.write(path.join(host, 'src', 'main.ts'), 'yours\n');
|
|
68
|
+
t.touch(path.join(host, 'src', 'main.ts'), 20);
|
|
69
|
+
t.write(path.join(paths.view, 'src', 'main.ts'), 'sandbox\n');
|
|
70
|
+
t.touch(path.join(paths.view, 'src', 'main.ts'), 30);
|
|
71
|
+
const stats = view.syncAll();
|
|
72
|
+
const conflicts = fs.readdirSync(path.join(host, 'src')).filter((n) => n.includes('.sandbox-conflict-'));
|
|
73
|
+
t.check(stats.conflicts === 1 && fs.readFileSync(path.join(host, 'src', 'main.ts'), 'utf8') === 'yours\n' && conflicts.length === 1 && fs.readFileSync(path.join(host, 'src', conflicts[0]), 'utf8') === 'sandbox\n', 'changed on both sides: yours stays, the sandbox\'s version is kept next to it');
|
|
74
|
+
t.equal(fs.readFileSync(path.join(paths.view, 'src', 'main.ts'), 'utf8'), 'yours\n', '... and the sandbox sees yours');
|
|
75
|
+
t.write(path.join(host, '.gitignore'), 'node_modules/\ndist/\n');
|
|
76
|
+
view.syncChanged(['.gitignore']);
|
|
77
|
+
t.check(inView('.env'), 'a file no longer ignored comes in when .gitignore changes');
|
|
78
|
+
t.equal(view.syncAll(), { toView: 0, toHost: 0, deleted: 0, conflicts: 0, skipped: 0 }, 'a pass with nothing changed copies nothing');
|
|
79
|
+
t.check(view.syncChanged(['../outside.txt', 'C:/Windows/x', '']).toHost === 0, 'paths from outside the project are ignored');
|
|
80
|
+
t.section('live');
|
|
81
|
+
const stop = view.watch(60000);
|
|
82
|
+
t.write(path.join(host, 'live.txt'), 'from you');
|
|
83
|
+
const arrived = await t.waitFor(() => inView('live.txt'), 5000);
|
|
84
|
+
t.write(path.join(paths.view, 'live-back.txt'), 'from the sandbox');
|
|
85
|
+
const back = await t.waitFor(() => fs.existsSync(path.join(host, 'live-back.txt')), 5000);
|
|
86
|
+
stop();
|
|
87
|
+
t.check(Boolean(arrived) && Boolean(back), 'changes on either side arrive within a second or two while it watches');
|
|
88
|
+
},
|
|
89
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Config, GitAccess } from '../core/types.js';
|
|
2
|
+
export type AppCategory = 'editors' | 'databases' | 'api' | 'git' | 'utilities' | 'browsers' | 'gamedev' | 'cloud' | 'diagrams' | 'network' | 'cli';
|
|
3
|
+
export declare const APP_CATEGORIES: {
|
|
4
|
+
id: AppCategory;
|
|
5
|
+
label: string;
|
|
6
|
+
description: string;
|
|
7
|
+
}[];
|
|
8
|
+
/** Where a download comes from. Resolvers (sublime, kubectl ...) are code in guest/apps, named here by id. */
|
|
9
|
+
export type AppSource = {
|
|
10
|
+
github: string;
|
|
11
|
+
asset: string;
|
|
12
|
+
} | {
|
|
13
|
+
url: string;
|
|
14
|
+
} | {
|
|
15
|
+
resolve: string;
|
|
16
|
+
} | {
|
|
17
|
+
npm: string;
|
|
18
|
+
};
|
|
19
|
+
export interface AppEntry {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
category: AppCategory;
|
|
23
|
+
about: string;
|
|
24
|
+
size: string;
|
|
25
|
+
source: AppSource;
|
|
26
|
+
/** How the download is unpacked: an archive (zip/7z/tar, the default), the file itself, an NSIS installer or an MSI. */
|
|
27
|
+
unpack?: 'archive' | 'file' | 'nsis' | 'msi';
|
|
28
|
+
/** The program to start (or the tool on PATH): a file name, or a regex (/.../) for names with a version in them. */
|
|
29
|
+
exe: string;
|
|
30
|
+
/** A command-line tool: its folder goes on PATH (no desktop shortcut). */
|
|
31
|
+
cli?: boolean;
|
|
32
|
+
/** Needs these first: 7-Zip to unpack an installer, Mesa for OpenGL without a GPU. */
|
|
33
|
+
needs?: ('7zip' | 'mesa')[];
|
|
34
|
+
/** Settings elsewhere that this entry stands for (VS Code, Windows Terminal): shown in the catalogue, not installed by it. */
|
|
35
|
+
setting?: string;
|
|
36
|
+
/** Only with full git access (it pushes on its own, past the sandbox's git guard). */
|
|
37
|
+
fullGitOnly?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export declare const APPS: AppEntry[];
|
|
40
|
+
export declare const appById: (id: string) => AppEntry | undefined;
|
|
41
|
+
/** A catalogue `exe` that is a regex ("/.../") matches names with a version in them. */
|
|
42
|
+
export declare const exeMatcher: (exe: string) => ((name: string) => boolean);
|
|
43
|
+
/** The apps to install, in order: 7-Zip first when anything needs it to unpack. */
|
|
44
|
+
export declare const appsToInstall: (apps: Config["apps"], gitAccess: GitAccess) => {
|
|
45
|
+
entries: AppEntry[];
|
|
46
|
+
skipped: string[];
|
|
47
|
+
};
|
|
48
|
+
/** Does any chosen app draw with OpenGL (so the Mesa step must run first)? */
|
|
49
|
+
export declare const appsNeedMesa: (apps: Config["apps"] | undefined) => boolean;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export const APP_CATEGORIES = [
|
|
2
|
+
{ id: 'editors', label: 'Editors & IDEs', description: 'Code and text editors (VS Code is a setting of its own and on by default).' },
|
|
3
|
+
{ id: 'databases', label: 'Database clients', description: 'GUIs for the databases the sandbox runs (PostgreSQL, MySQL, MongoDB, Redis, SQLite, DynamoDB).' },
|
|
4
|
+
{ id: 'api', label: 'API clients', description: 'Send HTTP, REST and GraphQL requests to what you are building.' },
|
|
5
|
+
{ id: 'git', label: 'Git tools', description: 'Git GUIs and terminal UIs. They use the sandbox\'s git, so its git access rules apply.' },
|
|
6
|
+
{ id: 'utilities', label: 'Utilities', description: 'Archives, comparing files, finding files, process tools, hex editing.' },
|
|
7
|
+
{ id: 'browsers', label: 'Browsers', description: 'Browsers for testing web apps (Edge is already in Windows).' },
|
|
8
|
+
{ id: 'gamedev', label: 'Game development', description: 'Engines and editors. They draw with OpenGL on the CPU (Mesa) when the sandbox has no GPU.' },
|
|
9
|
+
{ id: 'cloud', label: 'Cloud & Kubernetes', description: 'Command-line tools for clouds, Kubernetes and infrastructure as code.' },
|
|
10
|
+
{ id: 'diagrams', label: 'Diagrams & docs', description: 'Diagrams and document conversion.' },
|
|
11
|
+
{ id: 'network', label: 'Network & HTTP', description: 'Inspect and test HTTP traffic and APIs.' },
|
|
12
|
+
{ id: 'cli', label: 'Command-line tools', description: 'Small tools developers reach for: JSON and YAML, search, diff, builds, media, load testing ...' },
|
|
13
|
+
];
|
|
14
|
+
const gh = (github, asset) => ({ github, asset });
|
|
15
|
+
export const APPS = [
|
|
16
|
+
// ---- editors
|
|
17
|
+
{ id: 'vscode', name: 'Visual Studio Code', category: 'editors', about: 'Code editor with debugging, set up for the sandbox\'s languages.', size: '150 MB', source: { resolve: 'setting' }, exe: 'Code.exe', setting: 'toolchains.vscode.enabled' },
|
|
18
|
+
{ id: 'windows-terminal', name: 'Windows Terminal', category: 'utilities', about: 'The sandbox window: tabs, panes, PowerShell 7 profile.', size: '40 MB', source: { resolve: 'setting' }, exe: 'WindowsTerminal.exe', setting: 'provisioning.terminal.enabled' },
|
|
19
|
+
{ id: 'notepadpp', name: 'Notepad++', category: 'editors', about: 'Fast text and code editor.', size: '8 MB', source: gh('notepad-plus-plus/notepad-plus-plus', '^npp\\.[\\d.]+\\.portable\\.x64\\.zip$'), exe: 'notepad++.exe' },
|
|
20
|
+
{ id: 'sublime', name: 'Sublime Text', category: 'editors', about: 'Fast code editor (paid licence, free to evaluate).', size: '28 MB', source: { resolve: 'sublime' }, exe: 'sublime_text.exe' },
|
|
21
|
+
{ id: 'neovim', name: 'Neovim', category: 'editors', about: 'Terminal text editor (nvim).', size: '12 MB', source: gh('neovim/neovim', '^nvim-win64\\.zip$'), exe: 'nvim.exe', cli: true },
|
|
22
|
+
// ---- database clients
|
|
23
|
+
{ id: 'dbeaver', name: 'DBeaver Community', category: 'databases', about: 'Universal database client: PostgreSQL, MySQL, SQLite, SQL Server and more.', size: '120 MB', source: gh('dbeaver/dbeaver', '^dbeaver-ce-[\\d.]+-windows-x86_64\\.zip$'), exe: 'dbeaver.exe' },
|
|
24
|
+
{ id: 'pgadmin', name: 'pgAdmin 4', category: 'databases', about: 'The PostgreSQL admin GUI (from the PostgreSQL binaries zip).', size: '330 MB', source: { resolve: 'pgadmin' }, exe: 'pgAdmin4.exe' },
|
|
25
|
+
{ id: 'heidisql', name: 'HeidiSQL', category: 'databases', about: 'Light client for MySQL, MariaDB, PostgreSQL and SQL Server.', size: '28 MB', source: gh('HeidiSQL/HeidiSQL', '^HeidiSQL_[\\d.]+_64_Portable\\.zip$'), exe: 'heidisql.exe' },
|
|
26
|
+
{ id: 'sqlitebrowser', name: 'DB Browser for SQLite', category: 'databases', about: 'Open, browse and edit SQLite files.', size: '21 MB', source: gh('sqlitebrowser/sqlitebrowser', '^DB\\.Browser\\.for\\.SQLite-v?[\\d.]+-win64\\.zip$'), exe: 'DB Browser for SQLite.exe' },
|
|
27
|
+
{ id: 'compass', name: 'MongoDB Compass', category: 'databases', about: 'The MongoDB GUI.', size: '220 MB', source: gh('mongodb-js/compass', '^mongodb-compass-[\\d.]+-win32-x64\\.zip$'), exe: 'MongoDBCompass.exe' },
|
|
28
|
+
{ id: 'redisinsight', name: 'Redis Insight', category: 'databases', about: 'The Redis GUI.', size: '110 MB', source: gh('redis/RedisInsight', '^Redis-Insight-win-installer\\.exe$'), unpack: 'nsis', exe: 'Redis Insight.exe', needs: ['7zip'] },
|
|
29
|
+
{ id: 'dbgate', name: 'DbGate', category: 'databases', about: 'Database manager for SQL and NoSQL databases.', size: '180 MB', source: gh('dbgate/dbgate', '^dbgate-[\\d.]+-win_x64\\.zip$'), exe: 'DbGate.exe' },
|
|
30
|
+
{ id: 'beekeeper', name: 'Beekeeper Studio', category: 'databases', about: 'SQL editor and database manager.', size: '245 MB', source: gh('beekeeper-studio/beekeeper-studio', '^Beekeeper-Studio-[\\d.]+-portable\\.exe$'), unpack: 'nsis', exe: 'Beekeeper Studio.exe', needs: ['7zip'] },
|
|
31
|
+
{ id: 'dynamodb-admin', name: 'dynamodb-admin', category: 'databases', about: 'Web GUI for DynamoDB Local: run dynamodb-admin, then open http://localhost:8001.', size: '5 MB', source: { npm: 'dynamodb-admin' }, exe: 'dynamodb-admin.cmd', cli: true },
|
|
32
|
+
// ---- API clients
|
|
33
|
+
{ id: 'bruno', name: 'Bruno', category: 'api', about: 'Offline, git-friendly API client (collections are files in your repo).', size: '175 MB', source: gh('usebruno/bruno', '^bruno_[\\d.]+_x64_win\\.zip$'), exe: 'Bruno.exe' },
|
|
34
|
+
{ id: 'insomnia', name: 'Insomnia', category: 'api', about: 'REST, GraphQL and gRPC client.', size: '590 MB', source: gh('Kong/insomnia', '^insomnia-[\\d.]+-full\\.nupkg$'), exe: 'Insomnia.exe' },
|
|
35
|
+
// ---- git
|
|
36
|
+
{ id: 'sourcegit', name: 'SourceGit', category: 'git', about: 'Fast open-source Git GUI.', size: '26 MB', source: gh('sourcegit-scm/sourcegit', '^sourcegit_[\\d.]+\\.win-x64\\.zip$'), exe: 'SourceGit.exe' },
|
|
37
|
+
{ id: 'githubdesktop', name: 'GitHub Desktop', category: 'git', about: 'Simple Git GUI from GitHub. It has its own git, so it needs provisioning.git.access "full".', size: '300 MB', source: gh('desktop/desktop', '^GitHubDesktop-[\\d.]+-x64-full\\.nupkg$'), exe: 'GitHubDesktop.exe', fullGitOnly: true },
|
|
38
|
+
{ id: 'lazygit', name: 'lazygit', category: 'git', about: 'Git in the terminal.', size: '7 MB', source: gh('jesseduffield/lazygit', '^lazygit_[\\d.]+_windows_x86_64\\.zip$'), exe: 'lazygit.exe', cli: true },
|
|
39
|
+
// ---- utilities
|
|
40
|
+
{ id: '7zip', name: '7-Zip', category: 'utilities', about: 'Archive manager (7z, zip, rar, tar ...). Also unpacks installers for other apps.', size: '2 MB', source: { resolve: '7zip' }, exe: '7zFM.exe' },
|
|
41
|
+
{ id: 'winmerge', name: 'WinMerge', category: 'utilities', about: 'Compare and merge files and folders.', size: '23 MB', source: gh('WinMerge/winmerge', '^winmerge-[\\d.]+-x64-exe\\.zip$'), exe: 'WinMergeU.exe' },
|
|
42
|
+
{ id: 'everything', name: 'Everything', category: 'utilities', about: 'Finds files by name instantly.', size: '2 MB', source: { resolve: 'everything' }, exe: 'Everything.exe' },
|
|
43
|
+
{ id: 'sysinternals', name: 'Sysinternals Suite', category: 'utilities', about: 'Process Explorer, Process Monitor, Autoruns, TCPView and more (on PATH).', size: '200 MB', source: { url: 'https://download.sysinternals.com/files/SysinternalsSuite.zip' }, exe: 'procexp64.exe' },
|
|
44
|
+
{ id: 'imhex', name: 'ImHex', category: 'utilities', about: 'Hex editor for reverse engineering and binary formats.', size: '80 MB', source: gh('WerWolv/ImHex', '^imhex-[\\d.]+-Windows-Portable-NoGPU-x86_64\\.zip$'), exe: 'imhex-gui.exe' },
|
|
45
|
+
// ---- browsers
|
|
46
|
+
{ id: 'chrome', name: 'Chrome for Testing', category: 'browsers', about: 'Chrome build made for automated tests (with chromedriver on PATH).', size: '170 MB', source: { resolve: 'chrome' }, exe: 'chrome.exe' },
|
|
47
|
+
{ id: 'firefox', name: 'Firefox', category: 'browsers', about: 'Firefox for testing.', size: '75 MB', source: { resolve: 'firefox' }, unpack: 'nsis', exe: 'firefox.exe', needs: ['7zip'] },
|
|
48
|
+
// ---- game development
|
|
49
|
+
{ id: 'godot', name: 'Godot', category: 'gamedev', about: 'Open-source 2D and 3D game engine.', size: '80 MB', source: gh('godotengine/godot', '^Godot_v[\\d.]+-stable_win64\\.exe\\.zip$'), exe: '/^Godot_v[\\d.]+-stable_win64\\.exe$/', needs: ['mesa'] },
|
|
50
|
+
{ id: 'blender', name: 'Blender', category: 'gamedev', about: '3D modelling, animation and rendering.', size: '400 MB', source: { resolve: 'blender' }, exe: 'blender.exe', needs: ['mesa'] },
|
|
51
|
+
{ id: 'love', name: 'LÖVE', category: 'gamedev', about: 'Lua framework for 2D games.', size: '4 MB', source: gh('love2d/love', '^love-[\\d.]+-win64\\.zip$'), exe: 'love.exe', needs: ['mesa'] },
|
|
52
|
+
{ id: 'defold', name: 'Defold', category: 'gamedev', about: 'Free 2D/3D game engine and editor.', size: '330 MB', source: gh('defold/defold', '^Defold-x86_64-win32\\.zip$'), exe: 'Defold.exe', needs: ['mesa'] },
|
|
53
|
+
{ id: 'pixelorama', name: 'Pixelorama', category: 'gamedev', about: 'Pixel-art and sprite editor.', size: '40 MB', source: gh('Orama-Interactive/Pixelorama', '^Pixelorama-Windows-64bit\\.zip$'), exe: 'Pixelorama.exe', needs: ['mesa'] },
|
|
54
|
+
{ id: 'tiled', name: 'Tiled', category: 'gamedev', about: 'Tile map editor.', size: '22 MB', source: gh('mapeditor/tiled', '^Tiled-[\\d.]+_Windows-10\\+_x86_64\\.msi$'), unpack: 'msi', exe: 'tiled.exe' },
|
|
55
|
+
// ---- cloud
|
|
56
|
+
{ id: 'kubectl', name: 'kubectl', category: 'cloud', about: 'Kubernetes command line.', size: '60 MB', source: { resolve: 'kubectl' }, unpack: 'file', exe: 'kubectl.exe', cli: true },
|
|
57
|
+
{ id: 'helm', name: 'Helm', category: 'cloud', about: 'Kubernetes package manager.', size: '20 MB', source: { resolve: 'helm' }, exe: 'helm.exe', cli: true },
|
|
58
|
+
{ id: 'k9s', name: 'k9s', category: 'cloud', about: 'Kubernetes cluster UI in the terminal.', size: '40 MB', source: gh('derailed/k9s', '^k9s_Windows_amd64\\.zip$'), exe: 'k9s.exe', cli: true },
|
|
59
|
+
{ id: 'terraform', name: 'Terraform', category: 'cloud', about: 'Infrastructure as code.', size: '30 MB', source: { resolve: 'terraform' }, exe: 'terraform.exe', cli: true },
|
|
60
|
+
{ id: 'azure-cli', name: 'Azure CLI', category: 'cloud', about: 'Manage Azure (az).', size: '90 MB', source: { url: 'https://aka.ms/installazurecliwindowszipx64' }, exe: 'az.cmd', cli: true },
|
|
61
|
+
{ id: 'gcloud', name: 'Google Cloud CLI', category: 'cloud', about: 'Manage Google Cloud (gcloud, gsutil, bq).', size: '120 MB', source: { url: 'https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-windows-x86_64-bundled-python.zip' }, exe: 'gcloud.cmd', cli: true },
|
|
62
|
+
{ id: 'aws-cli', name: 'AWS CLI', category: 'cloud', about: 'Manage AWS (aws).', size: '50 MB', source: { url: 'https://awscli.amazonaws.com/AWSCLIV2.msi' }, unpack: 'msi', exe: 'aws.exe', cli: true },
|
|
63
|
+
// ---- diagrams and docs
|
|
64
|
+
{ id: 'drawio', name: 'draw.io', category: 'diagrams', about: 'Diagram editor (flowcharts, architecture, UML).', size: '170 MB', source: gh('jgraph/drawio-desktop', '^draw\\.io-[\\d.]+-windows\\.zip$'), exe: 'draw.io.exe' },
|
|
65
|
+
{ id: 'pandoc', name: 'Pandoc', category: 'diagrams', about: 'Convert documents between Markdown, HTML, Word, PDF ...', size: '35 MB', source: gh('jgm/pandoc', '^pandoc-[\\d.]+-windows-x86_64\\.zip$'), exe: 'pandoc.exe', cli: true },
|
|
66
|
+
// ---- network
|
|
67
|
+
{ id: 'mitmproxy', name: 'mitmproxy', category: 'network', about: 'Intercepting HTTPS proxy (mitmweb opens a web UI).', size: '90 MB', source: { resolve: 'mitmproxy' }, exe: 'mitmweb.exe', cli: true },
|
|
68
|
+
{ id: 'xh', name: 'xh', category: 'network', about: 'Friendly HTTP requests from the terminal (like HTTPie).', size: '5 MB', source: gh('ducaale/xh', '^xh-v[\\d.]+-x86_64-pc-windows-msvc\\.zip$'), exe: 'xh.exe', cli: true },
|
|
69
|
+
{ id: 'grpcurl', name: 'grpcurl', category: 'network', about: 'curl for gRPC servers.', size: '10 MB', source: gh('fullstorydev/grpcurl', '^grpcurl_[\\d.]+_windows_x86_64\\.zip$'), exe: 'grpcurl.exe', cli: true },
|
|
70
|
+
{ id: 'k6', name: 'k6', category: 'network', about: 'Load testing with JavaScript scripts.', size: '30 MB', source: gh('grafana/k6', '^k6-v[\\d.]+-windows-amd64\\.zip$'), exe: 'k6.exe', cli: true },
|
|
71
|
+
// ---- command-line tools
|
|
72
|
+
{ id: 'jq', name: 'jq', category: 'cli', about: 'Query and reshape JSON.', size: '1 MB', source: gh('jqlang/jq', '^jq-windows-amd64\\.exe$'), unpack: 'file', exe: 'jq.exe', cli: true },
|
|
73
|
+
{ id: 'yq', name: 'yq', category: 'cli', about: 'Query and edit YAML, JSON, XML and TOML.', size: '10 MB', source: gh('mikefarah/yq', '^yq_windows_amd64\\.exe$'), unpack: 'file', exe: 'yq.exe', cli: true },
|
|
74
|
+
{ id: 'ripgrep', name: 'ripgrep', category: 'cli', about: 'Very fast recursive search (rg).', size: '2 MB', source: gh('BurntSushi/ripgrep', '^ripgrep-[\\d.]+-x86_64-pc-windows-msvc\\.zip$'), exe: 'rg.exe', cli: true },
|
|
75
|
+
{ id: 'fd', name: 'fd', category: 'cli', about: 'Fast, friendly alternative to find.', size: '2 MB', source: gh('sharkdp/fd', '^fd-v[\\d.]+-x86_64-pc-windows-msvc\\.zip$'), exe: 'fd.exe', cli: true },
|
|
76
|
+
{ id: 'bat', name: 'bat', category: 'cli', about: 'cat with syntax highlighting and git changes.', size: '3 MB', source: gh('sharkdp/bat', '^bat-v[\\d.]+-x86_64-pc-windows-msvc\\.zip$'), exe: 'bat.exe', cli: true },
|
|
77
|
+
{ id: 'fzf', name: 'fzf', category: 'cli', about: 'Fuzzy finder for the terminal.', size: '2 MB', source: gh('junegunn/fzf', '^fzf-[\\d.]+-windows_amd64\\.zip$'), exe: 'fzf.exe', cli: true },
|
|
78
|
+
{ id: 'delta', name: 'delta', category: 'cli', about: 'Readable git diffs with syntax highlighting.', size: '3 MB', source: gh('dandavison/delta', '^delta-[\\d.]+-x86_64-pc-windows-msvc\\.zip$'), exe: 'delta.exe', cli: true },
|
|
79
|
+
{ id: 'just', name: 'just', category: 'cli', about: 'Command runner (a simpler make for project tasks).', size: '2 MB', source: gh('casey/just', '^just-[\\d.]+-x86_64-pc-windows-msvc\\.zip$'), exe: 'just.exe', cli: true },
|
|
80
|
+
{ id: 'hyperfine', name: 'hyperfine', category: 'cli', about: 'Benchmark commands.', size: '1 MB', source: gh('sharkdp/hyperfine', '^hyperfine-v[\\d.]+-x86_64-pc-windows-msvc\\.zip$'), exe: 'hyperfine.exe', cli: true },
|
|
81
|
+
{ id: 'watchexec', name: 'watchexec', category: 'cli', about: 'Run a command when files change.', size: '5 MB', source: gh('watchexec/watchexec', '^watchexec-[\\d.]+-x86_64-pc-windows-msvc\\.zip$'), exe: 'watchexec.exe', cli: true },
|
|
82
|
+
{ id: 'protoc', name: 'protoc', category: 'cli', about: 'Protocol Buffers compiler.', size: '3 MB', source: gh('protocolbuffers/protobuf', '^protoc-[\\d.]+-win64\\.zip$'), exe: 'protoc.exe', cli: true },
|
|
83
|
+
{ id: 'ninja', name: 'Ninja', category: 'cli', about: 'Small, fast build system (CMake\'s favourite generator).', size: '1 MB', source: gh('ninja-build/ninja', '^ninja-win\\.zip$'), exe: 'ninja.exe', cli: true },
|
|
84
|
+
{ id: 'ffmpeg', name: 'FFmpeg', category: 'cli', about: 'Convert and process audio and video.', size: '200 MB', source: gh('BtbN/FFmpeg-Builds', '^ffmpeg-master-latest-win64-gpl\\.zip$'), exe: 'ffmpeg.exe', cli: true },
|
|
85
|
+
{ id: 'sqlite', name: 'SQLite tools', category: 'cli', about: 'sqlite3 command-line shell.', size: '5 MB', source: { resolve: 'sqlite' }, exe: 'sqlite3.exe', cli: true },
|
|
86
|
+
{ id: 'shellcheck', name: 'ShellCheck', category: 'cli', about: 'Finds bugs in shell scripts.', size: '10 MB', source: gh('koalaman/shellcheck', '^shellcheck-v[\\d.]+\\.zip$'), exe: 'shellcheck.exe', cli: true },
|
|
87
|
+
{ id: 'gitleaks', name: 'Gitleaks', category: 'cli', about: 'Finds secrets committed to git.', size: '8 MB', source: gh('gitleaks/gitleaks', '^gitleaks_[\\d.]+_windows_x64\\.zip$'), exe: 'gitleaks.exe', cli: true },
|
|
88
|
+
];
|
|
89
|
+
export const appById = (id) => APPS.find((a) => a.id === id);
|
|
90
|
+
/** A catalogue `exe` that is a regex ("/.../") matches names with a version in them. */
|
|
91
|
+
export const exeMatcher = (exe) => {
|
|
92
|
+
const regex = /^\/(.+)\/$/.exec(exe);
|
|
93
|
+
if (regex) {
|
|
94
|
+
const r = new RegExp(regex[1], 'i');
|
|
95
|
+
return (name) => r.test(name);
|
|
96
|
+
}
|
|
97
|
+
return (name) => name.toLowerCase() === exe.toLowerCase();
|
|
98
|
+
};
|
|
99
|
+
/** Your own program as a catalogue entry (its folder is apps\custom\<id>). */
|
|
100
|
+
const customEntry = (id, app) => ({
|
|
101
|
+
id: 'custom/' + id, name: app.name, category: 'utilities', about: 'Your own program.', size: '?',
|
|
102
|
+
source: app.github && app.asset ? { github: app.github, asset: app.asset } : { url: app.url },
|
|
103
|
+
unpack: app.unpack, exe: app.exe, cli: app.cli,
|
|
104
|
+
});
|
|
105
|
+
/** The apps to install, in order: 7-Zip first when anything needs it to unpack. */
|
|
106
|
+
export const appsToInstall = (apps, gitAccess) => {
|
|
107
|
+
const chosen = apps.install.flatMap((id) => { const e = appById(id); return e && !e.setting ? [e] : []; });
|
|
108
|
+
const skipped = chosen.filter((e) => e.fullGitOnly && gitAccess !== 'full').map((e) => e.name + ' (needs provisioning.git.access "full")');
|
|
109
|
+
const allowed = [...chosen.filter((e) => !(e.fullGitOnly && gitAccess !== 'full')), ...Object.entries(apps.custom).map(([id, app]) => customEntry(id, app))];
|
|
110
|
+
const needs7zip = allowed.some((e) => e.needs?.includes('7zip') || e.unpack === 'nsis') && !allowed.some((e) => e.id === '7zip');
|
|
111
|
+
return { entries: [...(needs7zip ? [appById('7zip')] : []), ...allowed], skipped };
|
|
112
|
+
};
|
|
113
|
+
/** Does any chosen app draw with OpenGL (so the Mesa step must run first)? */
|
|
114
|
+
export const appsNeedMesa = (apps) => Boolean(apps?.install.some((id) => appById(id)?.needs?.includes('mesa')));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface VtInput {
|
|
2
|
+
/** true once the console passes the terminal's input through (mouse reports included); false if it could not. */
|
|
3
|
+
ready: Promise<boolean>;
|
|
4
|
+
/** Stops a change still in progress; resolves when nothing can change the console mode any more. */
|
|
5
|
+
stop: () => Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Turns on ENABLE_VIRTUAL_TERMINAL_INPUT for this process's console. Call it after stdin.setRawMode(true) (which
|
|
9
|
+
* overwrites the mode); setRawMode(false) turns it off again. No-op off Windows or without a console.
|
|
10
|
+
*/
|
|
11
|
+
export declare const enableVtInput: () => VtInput;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Windows: lets the terminal's own input sequences (mouse clicks and wheel, focus) reach a Node process in raw mode.
|
|
2
|
+
// Node reads the console as key records in raw mode and drops every mouse record, so an agent that turned mouse
|
|
3
|
+
// tracking on (its terminal then sends the wheel and clicks as mouse reports) got neither. With
|
|
4
|
+
// ENABLE_VIRTUAL_TERMINAL_INPUT the console hands over the terminal's input as it is: mouse reports, focus, keys.
|
|
5
|
+
// Node has no API for the console mode, so a PowerShell child sets it on the console both share (the input mode
|
|
6
|
+
// belongs to the console, not to a handle). Only built-ins here: view.js bundles this.
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { SYSTEM } from './system.js';
|
|
9
|
+
/**
|
|
10
|
+
* PowerShell starts in a console of its own, hidden (so it never retitles your tab or touches its modes), then switches
|
|
11
|
+
* to ours. Started without any console, powershell.exe does nothing at all.
|
|
12
|
+
*/
|
|
13
|
+
const script = (pid) => [
|
|
14
|
+
'Add-Type -TypeDefinition @"',
|
|
15
|
+
'using System; using System.Runtime.InteropServices;',
|
|
16
|
+
'public static class SbxVtInput {',
|
|
17
|
+
' [DllImport("kernel32.dll")] static extern bool FreeConsole();',
|
|
18
|
+
' [DllImport("kernel32.dll")] static extern bool AttachConsole(uint pid);',
|
|
19
|
+
' [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] static extern IntPtr CreateFileW(string name, uint access, uint share, IntPtr security, uint creation, uint flags, IntPtr template);',
|
|
20
|
+
' [DllImport("kernel32.dll")] static extern bool GetConsoleMode(IntPtr handle, out uint mode);',
|
|
21
|
+
' [DllImport("kernel32.dll")] static extern bool SetConsoleMode(IntPtr handle, uint mode);',
|
|
22
|
+
' [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);',
|
|
23
|
+
' public static int On(uint pid) {',
|
|
24
|
+
' FreeConsole();',
|
|
25
|
+
' if (!AttachConsole(pid)) return 2;',
|
|
26
|
+
' IntPtr input = CreateFileW("CONIN$", 0xC0000000, 3, IntPtr.Zero, 3, 0, IntPtr.Zero);',
|
|
27
|
+
' if (input == new IntPtr(-1)) return 3;',
|
|
28
|
+
' uint mode; bool ok = GetConsoleMode(input, out mode) && SetConsoleMode(input, mode | 0x200);',
|
|
29
|
+
' CloseHandle(input);',
|
|
30
|
+
' FreeConsole();',
|
|
31
|
+
' return ok ? 0 : 4;',
|
|
32
|
+
' }',
|
|
33
|
+
'}',
|
|
34
|
+
'"@',
|
|
35
|
+
'exit [SbxVtInput]::On(' + pid + ')',
|
|
36
|
+
].join('\n');
|
|
37
|
+
/**
|
|
38
|
+
* Turns on ENABLE_VIRTUAL_TERMINAL_INPUT for this process's console. Call it after stdin.setRawMode(true) (which
|
|
39
|
+
* overwrites the mode); setRawMode(false) turns it off again. No-op off Windows or without a console.
|
|
40
|
+
*/
|
|
41
|
+
export const enableVtInput = () => {
|
|
42
|
+
if (process.platform !== 'win32' || !process.stdin.isTTY)
|
|
43
|
+
return { ready: Promise.resolve(false), stop: () => Promise.resolve() };
|
|
44
|
+
let child = null;
|
|
45
|
+
let exited = Promise.resolve();
|
|
46
|
+
const ready = new Promise((resolve) => {
|
|
47
|
+
try {
|
|
48
|
+
child = spawn(SYSTEM.powershell(), ['-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', Buffer.from(script(process.pid), 'utf16le').toString('base64')], { stdio: 'ignore', windowsHide: true });
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
resolve(false);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
exited = new Promise((done) => {
|
|
55
|
+
child?.on('exit', (code) => { resolve(code === 0); done(); });
|
|
56
|
+
child?.on('error', () => { resolve(false); done(); });
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
return { ready, stop: () => { if (child && child.exitCode === null && child.signalCode === null)
|
|
60
|
+
child.kill(); return exited; } };
|
|
61
|
+
};
|
|
@@ -17,6 +17,9 @@ export interface Checksums {
|
|
|
17
17
|
}
|
|
18
18
|
/** downloads.requireChecksum: from then on download() refuses anything it has no expected hash for. The guest sets it from session.json. */
|
|
19
19
|
export declare const setChecksumRequired: (on: boolean) => void;
|
|
20
|
+
/** Who wants coarse download progress: called as a download starts (in the caller's async context), returns its reporter. */
|
|
21
|
+
export type DownloadWatcher = () => ((fraction: number) => void) | null;
|
|
22
|
+
export declare const watchDownloads: (watcher: DownloadWatcher | null) => void;
|
|
20
23
|
/**
|
|
21
24
|
* Streams a download to disk, hashing as it goes: big archives (PostgreSQL is ~330 MB) never sit in memory, and the
|
|
22
25
|
* event loop stays free so other toolchains keep downloading meanwhile. A mismatch (or any failure) leaves no file behind.
|
package/dist/shared/download.js
CHANGED
|
@@ -46,6 +46,8 @@ export const fetchText = async (url) => await (await fetchRetry(url)).text();
|
|
|
46
46
|
let checksumRequired = false;
|
|
47
47
|
/** downloads.requireChecksum: from then on download() refuses anything it has no expected hash for. The guest sets it from session.json. */
|
|
48
48
|
export const setChecksumRequired = (on) => { checksumRequired = on; };
|
|
49
|
+
let downloadWatcher = null;
|
|
50
|
+
export const watchDownloads = (watcher) => { downloadWatcher = watcher; };
|
|
49
51
|
/**
|
|
50
52
|
* Streams a download to disk, hashing as it goes: big archives (PostgreSQL is ~330 MB) never sit in memory, and the
|
|
51
53
|
* event loop stays free so other toolchains keep downloading meanwhile. A mismatch (or any failure) leaves no file behind.
|
|
@@ -64,8 +66,29 @@ export const download = async (url, destination, expected = null) => {
|
|
|
64
66
|
const want = sums[kind]?.trim().toLowerCase();
|
|
65
67
|
return want ? [{ want, hash: crypto.createHash(kind) }] : [];
|
|
66
68
|
});
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
// Progress for the boot screen: every 10% or 5 s at most, never per chunk; called here so it knows who is downloading.
|
|
70
|
+
const size = Number(response.headers.get('content-length')) || 0;
|
|
71
|
+
const report = size > 0 ? downloadWatcher?.() ?? null : null;
|
|
72
|
+
let received = 0;
|
|
73
|
+
let tenth = 0;
|
|
74
|
+
let reportedAt = Date.now();
|
|
75
|
+
const tap = new Transform({ transform(chunk, _encoding, done) {
|
|
76
|
+
for (const c of checks)
|
|
77
|
+
c.hash.update(chunk);
|
|
78
|
+
if (report) {
|
|
79
|
+
received += chunk.length;
|
|
80
|
+
const now = Math.floor((received * 10) / size);
|
|
81
|
+
if (now !== tenth || Date.now() - reportedAt > 5000) {
|
|
82
|
+
tenth = now;
|
|
83
|
+
reportedAt = Date.now();
|
|
84
|
+
try {
|
|
85
|
+
report(Math.min(1, received / size));
|
|
86
|
+
}
|
|
87
|
+
catch { /* progress never breaks a download */ }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
done(null, chunk);
|
|
91
|
+
} });
|
|
69
92
|
// Written to <destination>.part and moved into place only once complete and verified, so an interrupted download never
|
|
70
93
|
// leaves a partial file under the real name.
|
|
71
94
|
const partial = destination + '.part';
|
|
@@ -11,6 +11,33 @@ export declare const guestDrive: (guest: string) => string | null;
|
|
|
11
11
|
* no '..' or empty segments, no characters Windows rejects in paths.
|
|
12
12
|
*/
|
|
13
13
|
export declare const isValidGuestPath: (guest: string) => boolean;
|
|
14
|
+
/** Where a mapping lands when the path it asks for (or the PC path) cannot be used inside the sandbox. */
|
|
15
|
+
export declare const MAPPED_ROOT = "C:\\Mapped";
|
|
16
|
+
/** A guest path as typed, tidied: '/' -> '\', no doubled or trailing backslashes, upper-case drive letter. */
|
|
17
|
+
export declare const tidyGuestPath: (guest: string) => string;
|
|
18
|
+
/** Why a guest path cannot be used as it is (null: it can). Short, plain English. */
|
|
19
|
+
export declare const guestPathProblem: (guest: string) => string | null;
|
|
20
|
+
/** C:\Mapped\<drive>\<rest of path>: a safe place for a folder whose own path cannot be used (odd characters made safe). */
|
|
21
|
+
export declare const mappedFallback: (path: string) => string;
|
|
22
|
+
/** Two guest folders overlap where they really are (F:\x is C:\Mirror\F\x): the same, or one inside the other. */
|
|
23
|
+
export declare const guestOverlaps: (a: string, b: string) => boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Where an extra folder appears in the sandbox. `wanted` is the guest path asked for (or the one derived from the PC
|
|
26
|
+
* path); when the sandbox cannot use it (reserved, invalid, or overlapping a folder in `taken`), the folder goes to
|
|
27
|
+
* C:\Mapped\<drive>\<path> instead (made unique with -2, -3 ...), and `note` says where and why.
|
|
28
|
+
*/
|
|
29
|
+
export declare const placeGuest: (host: string, wanted: string, taken: {
|
|
30
|
+
guest: string;
|
|
31
|
+
what: string;
|
|
32
|
+
}[]) => {
|
|
33
|
+
guest: string;
|
|
34
|
+
note: string | null;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* The guest path a mapping asks for when it gives none: a folder inside the project at the same place in the project's
|
|
38
|
+
* guest folder, any other folder at the same path as on this PC (D: to Z: through C:\Mirror and subst).
|
|
39
|
+
*/
|
|
40
|
+
export declare const deriveGuest: (host: string, projectHost: string, projectGuest: string) => string;
|
|
14
41
|
/** Where a guest path really is inside the sandbox (the .wsb SandboxFolder): C: paths as-is, F:\x -> C:\Mirror\F\x. */
|
|
15
42
|
export declare const sandboxFolder: (guest: string) => string;
|
|
16
43
|
export interface SubstDrive {
|