windows-development-sandbox 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.sandbox-config.example.json +2 -1
- package/config.example.json +1 -0
- package/dist/cli/boot-progress.d.ts +22 -0
- package/dist/cli/boot-progress.js +59 -0
- package/dist/cli/commands/launch.d.ts +5 -2
- package/dist/cli/commands/launch.js +77 -22
- package/dist/cli/commands/maintenance.js +36 -3
- package/dist/cli/commands/project.d.ts +10 -6
- package/dist/cli/commands/project.js +42 -3
- package/dist/cli/commands/restart.js +17 -7
- package/dist/cli/commands/sessions.js +2 -1
- package/dist/cli/manifest.js +1 -1
- package/dist/cli/progress.d.ts +15 -0
- package/dist/cli/progress.js +52 -0
- package/dist/core/config.js +1 -1
- package/dist/core/local-config.d.ts +16 -1
- package/dist/core/local-config.js +15 -9
- package/dist/core/project-policy.d.ts +50 -3
- package/dist/core/project-policy.js +90 -4
- package/dist/core/schema/config.d.ts +3 -2
- package/dist/core/schema/config.js +1 -1
- package/dist/core/schema/projects.d.ts +1 -0
- package/dist/core/schema/projects.js +1 -0
- package/dist/core/schema/sandbox.d.ts +1 -1
- package/dist/core/schema/sandbox.js +1 -1
- package/dist/core/types.d.ts +18 -1
- package/dist/core/workspace.d.ts +21 -4
- package/dist/core/workspace.js +87 -26
- package/dist/guest/env.d.ts +6 -2
- package/dist/guest/env.js +15 -3
- package/dist/guest/main.js +5 -1
- package/dist/guest/toolchains/index.js +29 -3
- package/dist/host/channel.d.ts +2 -2
- package/dist/host/channel.js +3 -2
- package/dist/host/generate.d.ts +1 -1
- package/dist/host/generate.js +13 -6
- package/dist/host/join.d.ts +7 -0
- package/dist/host/join.js +23 -9
- package/dist/host/launch/mappings.d.ts +7 -2
- package/dist/host/launch/mappings.js +64 -18
- package/dist/host/launch/projects.d.ts +2 -0
- package/dist/host/launch/projects.js +11 -5
- package/dist/host/projects.d.ts +25 -2
- package/dist/host/projects.js +54 -5
- package/dist/runtime/daemon.js +71 -9
- package/dist/runtime/db.js +33 -3
- package/dist/runtime/main.js +191 -30
- package/dist/runtime/netproxy.js +15 -3
- package/dist/runtime/view.js +262 -128
- package/dist/selftest/channel.test.js +38 -2
- package/dist/selftest/fixtures.d.ts +2 -1
- package/dist/selftest/index.js +2 -1
- package/dist/selftest/live-terminal.test.d.ts +2 -0
- package/dist/selftest/live-terminal.test.js +169 -0
- package/dist/selftest/local-config.test.js +28 -3
- package/dist/selftest/mirror.test.js +36 -7
- package/dist/selftest/projects.test.js +76 -7
- package/dist/shared/console-input.d.ts +11 -0
- package/dist/shared/console-input.js +61 -0
- package/dist/shared/download.d.ts +3 -0
- package/dist/shared/download.js +25 -2
- package/dist/shared/guest-paths.d.ts +27 -0
- package/dist/shared/guest-paths.js +68 -0
- package/dist/shared/projects.d.ts +27 -4
- package/dist/shared/projects.js +72 -13
- package/dist/shared/pty-client.d.ts +17 -0
- package/dist/shared/pty-client.js +65 -8
- package/dist/shared/schema.d.ts +24 -0
- package/dist/shared/schema.js +12 -0
- package/package.json +1 -1
- package/sandbox/ptyhost.cs +60 -3
- package/schemas/config.schema.json +15 -1
- package/schemas/sandbox-config.schema.json +4 -3
|
@@ -13,3 +13,5 @@ export declare const planBootProjects: (o: {
|
|
|
13
13
|
extraProjects?: string[];
|
|
14
14
|
knownProjects?: string[];
|
|
15
15
|
}) => BootProjects;
|
|
16
|
+
/** The launch folder is inside a mapped root and placed under it (core/workspace.ts): the root shows it, no own mapping. */
|
|
17
|
+
export declare const shownByRoot: (roots: RootMapping[], workspaceHost: string, guestWorkspace: string) => boolean;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { assertSafeMapping, assertSafeWorkspace } from '../../core/safe-paths.js';
|
|
6
6
|
import { dirExists, errMsg, fail } from '../../core/util.js';
|
|
7
|
-
import { isWithin, planProjectMappings } from '../../shared/projects.js';
|
|
7
|
+
import { guestDirFor, isWithin, planProjectMappings, projectForHostDir, usableRoots } from '../../shared/projects.js';
|
|
8
8
|
import { allProjectsToMap, readKnown } from '../projects.js';
|
|
9
9
|
/** More folders than this would make the .wsb (and every boot) unwieldy; the rest join with a restart later. */
|
|
10
10
|
export const MAX_BOOT_PROJECTS = 48;
|
|
@@ -21,7 +21,6 @@ export const planBootProjects = (o) => {
|
|
|
21
21
|
return errMsg(e);
|
|
22
22
|
}
|
|
23
23
|
};
|
|
24
|
-
const roots = [];
|
|
25
24
|
for (const root of config.projects.roots.map((r) => path.resolve(r))) {
|
|
26
25
|
if (!dirExists(root)) {
|
|
27
26
|
notes.push('Project root not found, skipped: ' + root);
|
|
@@ -30,9 +29,9 @@ export const planBootProjects = (o) => {
|
|
|
30
29
|
const refusal = safe(root);
|
|
31
30
|
if (refusal)
|
|
32
31
|
fail('projects.roots: ' + refusal, 'Use a folder that holds your projects, e.g. F:\\Code (not a drive, your profile or a system folder).');
|
|
33
|
-
if (!roots.some((r) => isWithin(r, root)))
|
|
34
|
-
roots.push(root);
|
|
35
32
|
}
|
|
33
|
+
// The same list resolveRun used to place a launch folder that is inside a root (core/workspace.ts).
|
|
34
|
+
const roots = usableRoots(config.projects.roots, dirExists);
|
|
36
35
|
const known = o.knownProjects ?? readKnown(storageRoot).map((k) => k.host);
|
|
37
36
|
const wanted = [...(o.extraProjects ?? []), ...(config.projects.all ? allProjectsToMap(known, { primary: workspaceHost, roots, exclude: config.projects.exclude }) : [])];
|
|
38
37
|
const projects = [];
|
|
@@ -52,5 +51,12 @@ export const planBootProjects = (o) => {
|
|
|
52
51
|
}
|
|
53
52
|
projects.push(folder);
|
|
54
53
|
}
|
|
55
|
-
|
|
54
|
+
// Projects mapped on their own sit where they would as the launch folder (C:\Workspace\<name>), never on top of it.
|
|
55
|
+
const taken = [config.guestWorkspace, ...config.mappings.flatMap((m) => (m.guest ? [m.guest] : []))];
|
|
56
|
+
return { ...planProjectMappings(roots, projects, Boolean(o.run.mirror), taken), notes };
|
|
57
|
+
};
|
|
58
|
+
/** The launch folder is inside a mapped root and placed under it (core/workspace.ts): the root shows it, no own mapping. */
|
|
59
|
+
export const shownByRoot = (roots, workspaceHost, guestWorkspace) => {
|
|
60
|
+
const root = projectForHostDir(roots, workspaceHost);
|
|
61
|
+
return root !== null && guestDirFor(root, workspaceHost).toLowerCase() === guestWorkspace.replace(/[\\/]+$/, '').toLowerCase();
|
|
56
62
|
};
|
package/dist/host/projects.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import type { Missing } from '../core/project-policy.js';
|
|
2
3
|
import { PROJECTS_GUEST, type ProjectEntry, type RootMapping } from '../shared/projects.js';
|
|
3
4
|
import { type Wsb } from './wsb.js';
|
|
4
5
|
export { PROJECTS_GUEST };
|
|
@@ -36,7 +37,17 @@ declare const Registry: z.ZodObject<{
|
|
|
36
37
|
}>;
|
|
37
38
|
registeredAt: z.ZodString;
|
|
38
39
|
}, z.core.$strict>>;
|
|
40
|
+
atBoot: z.ZodOptional<z.ZodObject<{
|
|
41
|
+
memoryMb: z.ZodNullable<z.ZodNumber>;
|
|
42
|
+
mappings: z.ZodArray<z.ZodObject<{
|
|
43
|
+
host: z.ZodString;
|
|
44
|
+
guest: z.ZodString;
|
|
45
|
+
readOnly: z.ZodBoolean;
|
|
46
|
+
}, z.core.$strict>>;
|
|
47
|
+
widened: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
48
|
+
}, z.core.$strict>>;
|
|
39
49
|
}, z.core.$strict>;
|
|
50
|
+
export type AtBoot = NonNullable<z.output<typeof Registry>['atBoot']>;
|
|
40
51
|
export type RegistryEntry = z.output<typeof Entry>;
|
|
41
52
|
export type ProjectRegistry = z.output<typeof Registry>;
|
|
42
53
|
export declare const readRegistry: (storageRoot: string) => ProjectRegistry | null;
|
|
@@ -47,12 +58,24 @@ export declare const startRegistry: (storageRoot: string, o: {
|
|
|
47
58
|
primary: RootMapping;
|
|
48
59
|
roots: RootMapping[];
|
|
49
60
|
boot: RootMapping[];
|
|
61
|
+
atBoot?: AtBoot;
|
|
50
62
|
}) => RegistryEntry;
|
|
51
|
-
/**
|
|
63
|
+
/** Reads the memory and mapped folders back from a .wsb this tool wrote (host-only launch folder; bounded). */
|
|
64
|
+
export declare const parseWsbBoot: (xml: string) => AtBoot;
|
|
65
|
+
/**
|
|
66
|
+
* What the running sandbox booted with. A sandbox started by 1.4.0 or older has no record: it is read from the .wsb in
|
|
67
|
+
* its launch folder (host-only storage) and recorded. null when neither is there.
|
|
68
|
+
*/
|
|
69
|
+
export declare const bootRecordOf: (storageRoot: string, launchPath: string | null) => AtBoot | null;
|
|
70
|
+
/**
|
|
71
|
+
* Why a folder cannot join the running sandbox without a restart (the caller offers one). With `missing`: it could
|
|
72
|
+
* join, but you chose to restart for what it wants at boot (core/project-policy.ts).
|
|
73
|
+
*/
|
|
52
74
|
export declare class NeedsRestart extends Error {
|
|
53
75
|
readonly hostDir: string;
|
|
54
76
|
readonly reason: string;
|
|
55
|
-
|
|
77
|
+
readonly missing?: Missing | undefined;
|
|
78
|
+
constructor(hostDir: string, reason: string, missing?: Missing | undefined);
|
|
56
79
|
}
|
|
57
80
|
export interface ProjectMapper {
|
|
58
81
|
/** The projects of the running sandbox (the launch folder first). */
|
package/dist/host/projects.js
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
// as folders join);
|
|
5
5
|
// - known-projects.json: every folder this tool has started a project in (for projects.all and `sandbox projects`).
|
|
6
6
|
// Everything about HOW a folder becomes visible is behind ProjectMapper, so the mechanisms can change independently.
|
|
7
|
+
import fs from 'node:fs';
|
|
7
8
|
import path from 'node:path';
|
|
8
9
|
import { z } from 'zod';
|
|
9
10
|
import { assertSafeWorkspace } from '../core/safe-paths.js';
|
|
10
11
|
import { dirExists, fail } from '../core/util.js';
|
|
11
12
|
import { Id } from '../shared/schema.js';
|
|
12
13
|
import { readValid, writeJsonAtomic } from '../shared/rundir.js';
|
|
13
|
-
import { guestDirFor, hubName, isWithin, PROJECTS_GUEST, projectForHostDir } from '../shared/projects.js';
|
|
14
|
+
import { guestDirFor, hubName, isWithin, ownGuestFolder, PROJECTS_GUEST, projectForHostDir } from '../shared/projects.js';
|
|
14
15
|
import { detectWsb, runningSandboxId, shareFolder } from './wsb.js';
|
|
15
16
|
export { PROJECTS_GUEST };
|
|
16
17
|
const AbsolutePath = z.string().max(1024).refine((p) => path.isAbsolute(p), 'must be an absolute path');
|
|
@@ -24,6 +25,13 @@ const Registry = z.strictObject({
|
|
|
24
25
|
protections: z.record(z.string(), z.union([z.boolean(), z.string()])),
|
|
25
26
|
roots: z.array(z.strictObject({ host: AbsolutePath, guest: z.string().max(1024) })).max(64),
|
|
26
27
|
projects: z.array(Entry).max(1024),
|
|
28
|
+
/** What the sandbox booted with that a joining project may also want (missing in registries of 1.4.0 and older). */
|
|
29
|
+
atBoot: z.strictObject({
|
|
30
|
+
memoryMb: z.number().int().positive().nullable(),
|
|
31
|
+
mappings: z.array(z.strictObject({ host: z.string().max(1024), guest: z.string().max(1024), readOnly: z.boolean() })).max(1024),
|
|
32
|
+
/** Boot settings turned on for a project mapped at boot (setting -> project): other projects share them. */
|
|
33
|
+
widened: z.record(z.string().max(100), z.string().max(100)),
|
|
34
|
+
}).optional(),
|
|
27
35
|
});
|
|
28
36
|
const registryFile = (storageRoot) => path.join(storageRoot, 'run', 'projects.json');
|
|
29
37
|
export const readRegistry = (storageRoot) => readValid(Registry, registryFile(storageRoot));
|
|
@@ -34,17 +42,57 @@ export const startRegistry = (storageRoot, o) => {
|
|
|
34
42
|
const primary = { name: hubName(o.primary.host, []), host: o.primary.host, guest: o.primary.guest, primary: true, mechanism: 'launch', registeredAt: at };
|
|
35
43
|
const names = [primary.name];
|
|
36
44
|
const boot = o.boot.map((b) => { const name = hubName(b.host, names); names.push(name); return { name, host: b.host, guest: b.guest, primary: false, mechanism: 'boot', registeredAt: at }; });
|
|
37
|
-
saveRegistry(storageRoot, { launchId: o.launchId, protections: o.protections, roots: o.roots, projects: [primary, ...boot] });
|
|
45
|
+
saveRegistry(storageRoot, { launchId: o.launchId, protections: o.protections, roots: o.roots, projects: [primary, ...boot], ...(o.atBoot ? { atBoot: o.atBoot } : {}) });
|
|
38
46
|
return primary;
|
|
39
47
|
};
|
|
40
|
-
/**
|
|
48
|
+
/** Reads the memory and mapped folders back from a .wsb this tool wrote (host-only launch folder; bounded). */
|
|
49
|
+
export const parseWsbBoot = (xml) => {
|
|
50
|
+
const text = xml.slice(0, 1_000_000);
|
|
51
|
+
const unescape = (s) => s.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '\'').replace(/&/g, '&');
|
|
52
|
+
const mappings = [...text.matchAll(/<MappedFolder><HostFolder>([^<]{1,1024})<\/HostFolder><SandboxFolder>([^<]{1,1024})<\/SandboxFolder><ReadOnly>(true|false)<\/ReadOnly><\/MappedFolder>/g)]
|
|
53
|
+
.slice(0, 1024).map((m) => ({ host: unescape(m[1]), guest: unescape(m[2]), readOnly: m[3] === 'true' }));
|
|
54
|
+
const memory = /<MemoryInMB>(\d{1,7})<\/MemoryInMB>/.exec(text);
|
|
55
|
+
return { memoryMb: memory ? Number(memory[1]) : null, mappings, widened: {} };
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* What the running sandbox booted with. A sandbox started by 1.4.0 or older has no record: it is read from the .wsb in
|
|
59
|
+
* its launch folder (host-only storage) and recorded. null when neither is there.
|
|
60
|
+
*/
|
|
61
|
+
export const bootRecordOf = (storageRoot, launchPath) => {
|
|
62
|
+
const registry = readRegistry(storageRoot);
|
|
63
|
+
if (!registry)
|
|
64
|
+
return null;
|
|
65
|
+
if (registry.atBoot)
|
|
66
|
+
return registry.atBoot;
|
|
67
|
+
if (!launchPath)
|
|
68
|
+
return null;
|
|
69
|
+
let xml;
|
|
70
|
+
try {
|
|
71
|
+
xml = fs.readFileSync(path.join(launchPath, 'agent.wsb'), 'utf8');
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const atBoot = parseWsbBoot(xml);
|
|
77
|
+
try {
|
|
78
|
+
saveRegistry(storageRoot, { ...registry, atBoot });
|
|
79
|
+
}
|
|
80
|
+
catch { /* the record is a convenience */ }
|
|
81
|
+
return atBoot;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Why a folder cannot join the running sandbox without a restart (the caller offers one). With `missing`: it could
|
|
85
|
+
* join, but you chose to restart for what it wants at boot (core/project-policy.ts).
|
|
86
|
+
*/
|
|
41
87
|
export class NeedsRestart extends Error {
|
|
42
88
|
hostDir;
|
|
43
89
|
reason;
|
|
44
|
-
|
|
90
|
+
missing;
|
|
91
|
+
constructor(hostDir, reason, missing) {
|
|
45
92
|
super(reason);
|
|
46
93
|
this.hostDir = hostDir;
|
|
47
94
|
this.reason = reason;
|
|
95
|
+
this.missing = missing;
|
|
48
96
|
}
|
|
49
97
|
}
|
|
50
98
|
/** Roots first (instant), then `wsb share` where the Windows Sandbox CLI exists, else NeedsRestart. */
|
|
@@ -84,7 +132,8 @@ export const projectMapper = (o) => {
|
|
|
84
132
|
const sandbox = runningSandboxId(wsb);
|
|
85
133
|
if (!sandbox.id)
|
|
86
134
|
throw new NeedsRestart(host, 'wsb share could not find the running sandbox: ' + sandbox.error);
|
|
87
|
-
|
|
135
|
+
// Where it is as a launch folder too (C:\Workspace\<name>), unless another project already uses that place.
|
|
136
|
+
const guest = ownGuestFolder(host, false, [...reg.projects.map((p) => p.guest), ...reg.roots.map((r) => r.guest)]);
|
|
88
137
|
const shared = shareFolder(wsb, sandbox.id, host, guest);
|
|
89
138
|
if (!shared.ok)
|
|
90
139
|
throw new NeedsRestart(host, 'wsb share failed (untested on this Windows): ' + shared.error);
|
package/dist/runtime/daemon.js
CHANGED
|
@@ -25320,7 +25320,7 @@ var ClaudeProfile = external_exports.strictObject({
|
|
|
25320
25320
|
var GuestWorkspace = external_exports.union([external_exports.literal("auto"), external_exports.literal("mirror"), GuestPath]).describe('Where the workspace appears inside the sandbox. "auto" = C:\\Workspace\\<folder name>. "mirror" = the same path as on this PC, so absolute paths in configs and build caches match (another drive letter such as F: is a subst drive onto C:\\Mirror\\F); paths the sandbox reserves (C:\\Users\\..., C:\\Program Files\\...) fall back to "auto".');
|
|
25321
25321
|
var Mapping = external_exports.strictObject({
|
|
25322
25322
|
host: external_exports.string().min(1).describe("Folder on this PC (relative paths are relative to the config file)."),
|
|
25323
|
-
guest:
|
|
25323
|
+
guest: external_exports.string().min(1).max(1024).optional().describe("Where it appears inside the sandbox, e.g. C:\\Assets, or on another drive letter (D: to Z:) such as F:\\SteamLibrary\\steamapps\\common\\Game (shown as F: at boot). Leave it out to use the same path as on this PC (a folder inside the project stays where it is in the project). A path the sandbox cannot use (C:\\Windows\\..., C:\\Users\\..., one already in use) goes to C:\\Mapped\\<drive>\\<path> instead, and the launch says so."),
|
|
25324
25324
|
readOnly: external_exports.boolean().describe("true = the agent can only read it."),
|
|
25325
25325
|
optional: external_exports.boolean().optional().describe("true = skip it quietly when the host folder does not exist.")
|
|
25326
25326
|
});
|
|
@@ -25608,6 +25608,7 @@ var Projects = external_exports.strictObject({
|
|
|
25608
25608
|
roots: external_exports.array(external_exports.string().min(1)).describe('Folders whose subfolders are your projects (e.g. "F:\\\\Code"), mapped read/write at every start. Any project inside one, including ones you create later, opens in the running sandbox at once, without a restart. Every agent in the sandbox can see all of them.'),
|
|
25609
25609
|
askToAddRoot: external_exports.boolean().describe("When you start a project whose parent folder is not a root yet, ask once whether to add it."),
|
|
25610
25610
|
declinedRoots: external_exports.array(external_exports.string().min(1)).describe("Parent folders you said no to (not asked again)."),
|
|
25611
|
+
allowOutsideMappings: external_exports.union([external_exports.literal("ask"), external_exports.boolean()]).describe(`A project's .sandbox-config.json may map folders from outside the project (e.g. a game folder on another drive). "ask" = an interactive launch asks once per project and set of folders, and remembers your answer; true = always allowed (still listed when you approve the file); false = never (the launch stops and says which folders). Only your own settings decide this, never a project file.`),
|
|
25611
25612
|
wsbShare: external_exports.boolean().describe("On Windows 11 24H2+ with the Windows Sandbox command line (wsb), add other folders to the running sandbox with `wsb share` (untested). Otherwise starting a project outside every root offers a restart.")
|
|
25612
25613
|
});
|
|
25613
25614
|
|
|
@@ -25650,7 +25651,7 @@ var LocalOverrides = external_exports.strictObject({
|
|
|
25650
25651
|
readOnly: external_exports.boolean().describe("Map this project read-only."),
|
|
25651
25652
|
// A project may turn the filter on and keep more out, never let ignored files in.
|
|
25652
25653
|
workspaceFilter: WorkspaceFilter.pick({ enabled: true, exclude: true }).partial(),
|
|
25653
|
-
mappings: external_exports.array(Mapping).describe("Extra folders
|
|
25654
|
+
mappings: external_exports.array(Mapping).describe("Extra folders (relative paths are relative to this file), e.g. a read-only data folder at another place in the sandbox. Folders outside the project are used only as your projects.allowOutsideMappings setting allows (by default you are asked once)."),
|
|
25654
25655
|
scripts: Strings.describe("Scripts (.ps1 / .mjs / .cmd, relative to this file) run inside the sandbox before the agent."),
|
|
25655
25656
|
// This PC's resources are yours to share: CPU and paging file (this PC's disk) are not a project's to set.
|
|
25656
25657
|
sandbox: Sandbox.omit({ cpuLimit: true, pageFileMb: true }).partial(),
|
|
@@ -25681,6 +25682,15 @@ var AttachEndpoint = external_exports.strictObject({
|
|
|
25681
25682
|
port: external_exports.number().int().min(1024).max(65535),
|
|
25682
25683
|
token: Text(256)
|
|
25683
25684
|
});
|
|
25685
|
+
var BootProgress = external_exports.strictObject({
|
|
25686
|
+
step: external_exports.number().int().min(1).max(32),
|
|
25687
|
+
steps: external_exports.number().int().min(1).max(32),
|
|
25688
|
+
title: Text(120),
|
|
25689
|
+
/** Being set up now; percent while it downloads (when the size is known). */
|
|
25690
|
+
active: external_exports.array(external_exports.strictObject({ name: Text(64), percent: external_exports.number().int().min(0).max(100).optional() })).max(8),
|
|
25691
|
+
done: external_exports.number().int().min(0).max(1e3),
|
|
25692
|
+
total: external_exports.number().int().min(0).max(1e3)
|
|
25693
|
+
});
|
|
25684
25694
|
var SandboxStatus = external_exports.object({
|
|
25685
25695
|
launchId: Text(64),
|
|
25686
25696
|
phase: external_exports.enum(["booting", "provisioning", "ready", "failed"]),
|
|
@@ -25690,7 +25700,9 @@ var SandboxStatus = external_exports.object({
|
|
|
25690
25700
|
guestWorkspace: Text(),
|
|
25691
25701
|
headless: external_exports.boolean(),
|
|
25692
25702
|
/** The sandbox's IPv4 addresses (for network.forwardPorts; the host only uses ones on its Hyper-V network). */
|
|
25693
|
-
addresses: external_exports.array(external_exports.string().max(45)).max(8).optional()
|
|
25703
|
+
addresses: external_exports.array(external_exports.string().max(45)).max(8).optional(),
|
|
25704
|
+
/** A malformed progress is dropped, never the status (the phase still counts). */
|
|
25705
|
+
progress: BootProgress.optional().catch(void 0)
|
|
25694
25706
|
});
|
|
25695
25707
|
var Version = external_exports.strictObject({ spec: Text(128), source: Text(256) });
|
|
25696
25708
|
var DatabasePlan = external_exports.strictObject({
|
|
@@ -25984,6 +25996,10 @@ var checksumRequired = false;
|
|
|
25984
25996
|
var setChecksumRequired = (on) => {
|
|
25985
25997
|
checksumRequired = on;
|
|
25986
25998
|
};
|
|
25999
|
+
var downloadWatcher = null;
|
|
26000
|
+
var watchDownloads = (watcher) => {
|
|
26001
|
+
downloadWatcher = watcher;
|
|
26002
|
+
};
|
|
25987
26003
|
var download = async (url2, destination, expected = null) => {
|
|
25988
26004
|
const sums = typeof expected === "string" || expected === null ? { sha256: expected } : expected;
|
|
25989
26005
|
if (checksumRequired && !sums.sha256?.trim() && !sums.sha512?.trim()) {
|
|
@@ -25996,8 +26012,25 @@ var download = async (url2, destination, expected = null) => {
|
|
|
25996
26012
|
const want = sums[kind]?.trim().toLowerCase();
|
|
25997
26013
|
return want ? [{ want, hash: crypto.createHash(kind) }] : [];
|
|
25998
26014
|
});
|
|
26015
|
+
const size = Number(response.headers.get("content-length")) || 0;
|
|
26016
|
+
const report = size > 0 ? downloadWatcher?.() ?? null : null;
|
|
26017
|
+
let received = 0;
|
|
26018
|
+
let tenth = 0;
|
|
26019
|
+
let reportedAt = Date.now();
|
|
25999
26020
|
const tap = new Transform({ transform(chunk, _encoding, done) {
|
|
26000
26021
|
for (const c of checks) c.hash.update(chunk);
|
|
26022
|
+
if (report) {
|
|
26023
|
+
received += chunk.length;
|
|
26024
|
+
const now2 = Math.floor(received * 10 / size);
|
|
26025
|
+
if (now2 !== tenth || Date.now() - reportedAt > 5e3) {
|
|
26026
|
+
tenth = now2;
|
|
26027
|
+
reportedAt = Date.now();
|
|
26028
|
+
try {
|
|
26029
|
+
report(Math.min(1, received / size));
|
|
26030
|
+
} catch {
|
|
26031
|
+
}
|
|
26032
|
+
}
|
|
26033
|
+
}
|
|
26001
26034
|
done(null, chunk);
|
|
26002
26035
|
} });
|
|
26003
26036
|
const partial2 = destination + ".part";
|
|
@@ -26145,16 +26178,25 @@ var mkdirp = (p) => {
|
|
|
26145
26178
|
fs5.mkdirSync(p, { recursive: true });
|
|
26146
26179
|
return p;
|
|
26147
26180
|
};
|
|
26148
|
-
var
|
|
26181
|
+
var progress;
|
|
26182
|
+
var setSandboxStatus = (phase, message, update) => {
|
|
26149
26183
|
try {
|
|
26184
|
+
if (update) progress = fitProgress({ step: 1, steps: 1, title: "", active: [], done: 0, total: 0, ...progress, ...update });
|
|
26150
26185
|
writeJsonAtomicChecked(phase, message);
|
|
26151
26186
|
} catch (e) {
|
|
26152
26187
|
warn("Could not update sandbox status: " + errMsg(e));
|
|
26153
26188
|
}
|
|
26154
26189
|
};
|
|
26190
|
+
var fitProgress = (p) => ({
|
|
26191
|
+
...p,
|
|
26192
|
+
title: p.title.slice(0, 120),
|
|
26193
|
+
done: Math.min(p.done, 1e3),
|
|
26194
|
+
total: Math.min(p.total, 1e3),
|
|
26195
|
+
active: p.active.slice(0, 8).map((a) => ({ name: a.name.slice(0, 64), ...a.percent === void 0 ? {} : { percent: Math.max(0, Math.min(100, Math.round(a.percent))) } }))
|
|
26196
|
+
});
|
|
26155
26197
|
var ownAddresses = () => Object.values(os3.networkInterfaces()).flat().filter((i) => Boolean(i) && i.family === "IPv4" && !i.internal).map((i) => i.address).slice(0, 8);
|
|
26156
26198
|
var writeJsonAtomicChecked = (phase, message) => {
|
|
26157
|
-
writeJsonAtomic(run.sandbox, { launchId: session.launchId, phase, message, updatedAt: now(), workspaceHost: session.workspaceHost, guestWorkspace: workspace, headless: session.headless, addresses: ownAddresses() });
|
|
26199
|
+
writeJsonAtomic(run.sandbox, { launchId: session.launchId, phase, message, updatedAt: now(), workspaceHost: session.workspaceHost, guestWorkspace: workspace, headless: session.headless, addresses: ownAddresses(), ...progress ? { progress } : {} });
|
|
26158
26200
|
};
|
|
26159
26201
|
var exec = (file3, args, options = {}) => {
|
|
26160
26202
|
const result = spawnSync(file3, args, { stdio: "inherit", windowsHide: true, ...options });
|
|
@@ -26273,6 +26315,7 @@ import path45 from "node:path";
|
|
|
26273
26315
|
// src/shared/projects.ts
|
|
26274
26316
|
import path8 from "node:path";
|
|
26275
26317
|
var PROJECTS_GUEST = "C:\\Projects";
|
|
26318
|
+
var WORKSPACES_GUEST = "C:\\Workspace";
|
|
26276
26319
|
var norm2 = (p) => path8.win32.resolve(p).replace(/[\\/]+$/, "").toLowerCase();
|
|
26277
26320
|
var isWithin = (parent, child) => {
|
|
26278
26321
|
const a = norm2(parent);
|
|
@@ -26286,7 +26329,7 @@ var hubName = (hostDir, taken) => {
|
|
|
26286
26329
|
for (let i = 2; ; i++) if (!used.has((base + "-" + i).toLowerCase())) return base + "-" + i;
|
|
26287
26330
|
};
|
|
26288
26331
|
var isBelow = (parent, child) => isWithin(parent, child) && norm2(parent) !== norm2(child);
|
|
26289
|
-
var insideProjects = (guestDir, roots = [], projectsDir = PROJECTS_GUEST) => isBelow(projectsDir, guestDir) || roots.some((r) => isWithin(r, guestDir));
|
|
26332
|
+
var insideProjects = (guestDir, roots = [], projectsDir = PROJECTS_GUEST) => isBelow(projectsDir, guestDir) || isBelow(WORKSPACES_GUEST, guestDir) || roots.some((r) => isWithin(r, guestDir));
|
|
26290
26333
|
|
|
26291
26334
|
// src/guest/daemon/state.ts
|
|
26292
26335
|
import path9 from "node:path";
|
|
@@ -26510,6 +26553,9 @@ var runHooksAsync = async (stage, env, target) => {
|
|
|
26510
26553
|
}
|
|
26511
26554
|
};
|
|
26512
26555
|
|
|
26556
|
+
// src/guest/toolchains/index.ts
|
|
26557
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
26558
|
+
|
|
26513
26559
|
// src/guest/toolchains/common.ts
|
|
26514
26560
|
import fs10 from "node:fs";
|
|
26515
26561
|
import path14 from "node:path";
|
|
@@ -28644,6 +28690,7 @@ var plan = (config2, versions, o) => {
|
|
|
28644
28690
|
|
|
28645
28691
|
// src/guest/toolchains/index.ts
|
|
28646
28692
|
var PARALLEL2 = 4;
|
|
28693
|
+
var currentStep = new AsyncLocalStorage2();
|
|
28647
28694
|
var locks = /* @__PURE__ */ new Map();
|
|
28648
28695
|
var lockKey = (name) => name.split(" ")[0].toLowerCase();
|
|
28649
28696
|
var exclusive = async (name, work) => {
|
|
@@ -28669,10 +28716,19 @@ var runSteps = async (steps, o) => {
|
|
|
28669
28716
|
const running = /* @__PURE__ */ new Set();
|
|
28670
28717
|
const waiting = [];
|
|
28671
28718
|
let done = 0;
|
|
28672
|
-
const
|
|
28719
|
+
const percent = /* @__PURE__ */ new Map();
|
|
28720
|
+
const active = () => [...running].map((name) => ({ name, ...percent.has(name) ? { percent: percent.get(name) } : {} }));
|
|
28721
|
+
const say = o.report ?? ((message) => setSandboxStatus("provisioning", message, { active: active(), done, total: steps.length }));
|
|
28673
28722
|
const report = () => {
|
|
28674
28723
|
if (running.size) say("Installing " + [...running].join(", ") + " (" + done + " of " + steps.length + " done)");
|
|
28675
28724
|
};
|
|
28725
|
+
if (!o.report) watchDownloads(() => {
|
|
28726
|
+
const name = currentStep.getStore();
|
|
28727
|
+
return name ? (fraction) => {
|
|
28728
|
+
percent.set(name, Math.round(fraction * 100));
|
|
28729
|
+
report();
|
|
28730
|
+
} : null;
|
|
28731
|
+
});
|
|
28676
28732
|
for (const s of steps) {
|
|
28677
28733
|
finished.set(s.name, (async () => {
|
|
28678
28734
|
await Promise.all((s.after ?? []).map((name) => finished.get(name)));
|
|
@@ -28686,16 +28742,22 @@ var runSteps = async (steps, o) => {
|
|
|
28686
28742
|
running.add(s.name);
|
|
28687
28743
|
report();
|
|
28688
28744
|
try {
|
|
28689
|
-
await exclusive(s.name, () => step(s.name, envs.get(s.name), async () => s.run(envs.get(s.name))));
|
|
28745
|
+
await currentStep.run(s.name, () => exclusive(s.name, () => step(s.name, envs.get(s.name), async () => s.run(envs.get(s.name)))));
|
|
28690
28746
|
} finally {
|
|
28691
28747
|
running.delete(s.name);
|
|
28748
|
+
percent.delete(s.name);
|
|
28692
28749
|
done++;
|
|
28693
28750
|
report();
|
|
28694
28751
|
waiting.shift()?.();
|
|
28695
28752
|
}
|
|
28696
28753
|
})());
|
|
28697
28754
|
}
|
|
28698
|
-
|
|
28755
|
+
try {
|
|
28756
|
+
await Promise.all(finished.values());
|
|
28757
|
+
} finally {
|
|
28758
|
+
if (!o.report) watchDownloads(null);
|
|
28759
|
+
}
|
|
28760
|
+
if (!o.report) setSandboxStatus("provisioning", "Set up " + steps.length + " toolchains and databases", { active: [], done, total: steps.length });
|
|
28699
28761
|
return envs;
|
|
28700
28762
|
};
|
|
28701
28763
|
var setupToolchains = async (config2, versions, options, run2 = {}) => {
|
package/dist/runtime/db.js
CHANGED
|
@@ -25316,7 +25316,7 @@ var ClaudeProfile = external_exports.strictObject({
|
|
|
25316
25316
|
var GuestWorkspace = external_exports.union([external_exports.literal("auto"), external_exports.literal("mirror"), GuestPath]).describe('Where the workspace appears inside the sandbox. "auto" = C:\\Workspace\\<folder name>. "mirror" = the same path as on this PC, so absolute paths in configs and build caches match (another drive letter such as F: is a subst drive onto C:\\Mirror\\F); paths the sandbox reserves (C:\\Users\\..., C:\\Program Files\\...) fall back to "auto".');
|
|
25317
25317
|
var Mapping = external_exports.strictObject({
|
|
25318
25318
|
host: external_exports.string().min(1).describe("Folder on this PC (relative paths are relative to the config file)."),
|
|
25319
|
-
guest:
|
|
25319
|
+
guest: external_exports.string().min(1).max(1024).optional().describe("Where it appears inside the sandbox, e.g. C:\\Assets, or on another drive letter (D: to Z:) such as F:\\SteamLibrary\\steamapps\\common\\Game (shown as F: at boot). Leave it out to use the same path as on this PC (a folder inside the project stays where it is in the project). A path the sandbox cannot use (C:\\Windows\\..., C:\\Users\\..., one already in use) goes to C:\\Mapped\\<drive>\\<path> instead, and the launch says so."),
|
|
25320
25320
|
readOnly: external_exports.boolean().describe("true = the agent can only read it."),
|
|
25321
25321
|
optional: external_exports.boolean().optional().describe("true = skip it quietly when the host folder does not exist.")
|
|
25322
25322
|
});
|
|
@@ -25573,6 +25573,7 @@ var Projects = external_exports.strictObject({
|
|
|
25573
25573
|
roots: external_exports.array(external_exports.string().min(1)).describe('Folders whose subfolders are your projects (e.g. "F:\\\\Code"), mapped read/write at every start. Any project inside one, including ones you create later, opens in the running sandbox at once, without a restart. Every agent in the sandbox can see all of them.'),
|
|
25574
25574
|
askToAddRoot: external_exports.boolean().describe("When you start a project whose parent folder is not a root yet, ask once whether to add it."),
|
|
25575
25575
|
declinedRoots: external_exports.array(external_exports.string().min(1)).describe("Parent folders you said no to (not asked again)."),
|
|
25576
|
+
allowOutsideMappings: external_exports.union([external_exports.literal("ask"), external_exports.boolean()]).describe(`A project's .sandbox-config.json may map folders from outside the project (e.g. a game folder on another drive). "ask" = an interactive launch asks once per project and set of folders, and remembers your answer; true = always allowed (still listed when you approve the file); false = never (the launch stops and says which folders). Only your own settings decide this, never a project file.`),
|
|
25576
25577
|
wsbShare: external_exports.boolean().describe("On Windows 11 24H2+ with the Windows Sandbox command line (wsb), add other folders to the running sandbox with `wsb share` (untested). Otherwise starting a project outside every root offers a restart.")
|
|
25577
25578
|
});
|
|
25578
25579
|
|
|
@@ -25615,7 +25616,7 @@ var LocalOverrides = external_exports.strictObject({
|
|
|
25615
25616
|
readOnly: external_exports.boolean().describe("Map this project read-only."),
|
|
25616
25617
|
// A project may turn the filter on and keep more out, never let ignored files in.
|
|
25617
25618
|
workspaceFilter: WorkspaceFilter.pick({ enabled: true, exclude: true }).partial(),
|
|
25618
|
-
mappings: external_exports.array(Mapping).describe("Extra folders
|
|
25619
|
+
mappings: external_exports.array(Mapping).describe("Extra folders (relative paths are relative to this file), e.g. a read-only data folder at another place in the sandbox. Folders outside the project are used only as your projects.allowOutsideMappings setting allows (by default you are asked once)."),
|
|
25619
25620
|
scripts: Strings.describe("Scripts (.ps1 / .mjs / .cmd, relative to this file) run inside the sandbox before the agent."),
|
|
25620
25621
|
// This PC's resources are yours to share: CPU and paging file (this PC's disk) are not a project's to set.
|
|
25621
25622
|
sandbox: Sandbox.omit({ cpuLimit: true, pageFileMb: true }).partial(),
|
|
@@ -25646,6 +25647,15 @@ var AttachEndpoint = external_exports.strictObject({
|
|
|
25646
25647
|
port: external_exports.number().int().min(1024).max(65535),
|
|
25647
25648
|
token: Text(256)
|
|
25648
25649
|
});
|
|
25650
|
+
var BootProgress = external_exports.strictObject({
|
|
25651
|
+
step: external_exports.number().int().min(1).max(32),
|
|
25652
|
+
steps: external_exports.number().int().min(1).max(32),
|
|
25653
|
+
title: Text(120),
|
|
25654
|
+
/** Being set up now; percent while it downloads (when the size is known). */
|
|
25655
|
+
active: external_exports.array(external_exports.strictObject({ name: Text(64), percent: external_exports.number().int().min(0).max(100).optional() })).max(8),
|
|
25656
|
+
done: external_exports.number().int().min(0).max(1e3),
|
|
25657
|
+
total: external_exports.number().int().min(0).max(1e3)
|
|
25658
|
+
});
|
|
25649
25659
|
var SandboxStatus = external_exports.object({
|
|
25650
25660
|
launchId: Text(64),
|
|
25651
25661
|
phase: external_exports.enum(["booting", "provisioning", "ready", "failed"]),
|
|
@@ -25655,7 +25665,9 @@ var SandboxStatus = external_exports.object({
|
|
|
25655
25665
|
guestWorkspace: Text(),
|
|
25656
25666
|
headless: external_exports.boolean(),
|
|
25657
25667
|
/** The sandbox's IPv4 addresses (for network.forwardPorts; the host only uses ones on its Hyper-V network). */
|
|
25658
|
-
addresses: external_exports.array(external_exports.string().max(45)).max(8).optional()
|
|
25668
|
+
addresses: external_exports.array(external_exports.string().max(45)).max(8).optional(),
|
|
25669
|
+
/** A malformed progress is dropped, never the status (the phase still counts). */
|
|
25670
|
+
progress: BootProgress.optional().catch(void 0)
|
|
25659
25671
|
});
|
|
25660
25672
|
var Version = external_exports.strictObject({ spec: Text(128), source: Text(256) });
|
|
25661
25673
|
var DatabasePlan = external_exports.strictObject({
|
|
@@ -25895,6 +25907,7 @@ var checksumRequired = false;
|
|
|
25895
25907
|
var setChecksumRequired = (on) => {
|
|
25896
25908
|
checksumRequired = on;
|
|
25897
25909
|
};
|
|
25910
|
+
var downloadWatcher = null;
|
|
25898
25911
|
var download = async (url2, destination, expected = null) => {
|
|
25899
25912
|
const sums = typeof expected === "string" || expected === null ? { sha256: expected } : expected;
|
|
25900
25913
|
if (checksumRequired && !sums.sha256?.trim() && !sums.sha512?.trim()) {
|
|
@@ -25907,8 +25920,25 @@ var download = async (url2, destination, expected = null) => {
|
|
|
25907
25920
|
const want = sums[kind]?.trim().toLowerCase();
|
|
25908
25921
|
return want ? [{ want, hash: crypto.createHash(kind) }] : [];
|
|
25909
25922
|
});
|
|
25923
|
+
const size = Number(response.headers.get("content-length")) || 0;
|
|
25924
|
+
const report = size > 0 ? downloadWatcher?.() ?? null : null;
|
|
25925
|
+
let received = 0;
|
|
25926
|
+
let tenth = 0;
|
|
25927
|
+
let reportedAt = Date.now();
|
|
25910
25928
|
const tap = new Transform({ transform(chunk, _encoding, done) {
|
|
25911
25929
|
for (const c of checks) c.hash.update(chunk);
|
|
25930
|
+
if (report) {
|
|
25931
|
+
received += chunk.length;
|
|
25932
|
+
const now2 = Math.floor(received * 10 / size);
|
|
25933
|
+
if (now2 !== tenth || Date.now() - reportedAt > 5e3) {
|
|
25934
|
+
tenth = now2;
|
|
25935
|
+
reportedAt = Date.now();
|
|
25936
|
+
try {
|
|
25937
|
+
report(Math.min(1, received / size));
|
|
25938
|
+
} catch {
|
|
25939
|
+
}
|
|
25940
|
+
}
|
|
25941
|
+
}
|
|
25912
25942
|
done(null, chunk);
|
|
25913
25943
|
} });
|
|
25914
25944
|
const partial2 = destination + ".part";
|