@inthedark/wb 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,15 @@
4
4
  an agentic ide about the human
5
5
 
6
6
  ## how 2 use
7
+
8
+ you need to install these yourself first:
9
+ 1. [git](https://git-scm.com/)
10
+ 2. node, through whatever you like. [vite+](https://viteplus.dev/) recommended
11
+
12
+ optional:
13
+ 1. [tailscale](https://tailscale.com/), if you want to connect to workbench agents on other devices
14
+
15
+ and then workbench itself is just:
7
16
  ```bash
8
17
  # install & open
9
18
  npx @inthedark/wb
@@ -48,7 +57,9 @@ note: have tried to keep dependencies minimal for the most part
48
57
  `/package` — the entrypoint/installer, what actually gets published to npm
49
58
  `/daemon/host` — wake service
50
59
  `/daemon/server` — workbench harness
51
- `/daemon/voice` - vtt service (rust)
60
+ `/daemon/voice` — vtt service (rust)
61
+ `/daemon/repo` — vfs service (go)
62
+ `/integration` — integrations in third party software
52
63
  `/instructions` — instructions for agents, copied into the user's `~/.workbench` folder whenever read
53
64
  `/app/server` — server for frontend app
54
65
  `/app/client` — frontend SPA
@@ -11,6 +11,8 @@ import { fileURLToPath } from "node:url";
11
11
  import { DatabaseSync } from "node:sqlite";
12
12
  import WorkbenchInstallPrompt from "./WorkbenchInstallPrompt.mjs";
13
13
  import WorkbenchBootstrapCommand from "./WorkbenchBootstrapCommand.mjs";
14
+ import WorkbenchToolchain from "./WorkbenchToolchain.mjs";
15
+ import WorkbenchPackageRegistration from "./WorkbenchPackageRegistration.mjs";
14
16
 
15
17
  export default class WorkbenchBootstrap {
16
18
  constructor({
@@ -18,7 +20,9 @@ export default class WorkbenchBootstrap {
18
20
  packageRoot = path.dirname(fileURLToPath(import.meta.url)),
19
21
  environment = process.env,
20
22
  prompt = new WorkbenchInstallPrompt(),
21
- commands = new WorkbenchBootstrapCommand(),
23
+ commands = new WorkbenchBootstrapCommand({ environment }),
24
+ toolchain = new WorkbenchToolchain({ home, environment, commands }),
25
+ registration = new WorkbenchPackageRegistration({ home, environment, packageRoot }),
22
26
  write = text => process.stdout.write(text),
23
27
  } = {}) {
24
28
  this.home = home;
@@ -26,6 +30,8 @@ export default class WorkbenchBootstrap {
26
30
  this.environment = environment;
27
31
  this.prompt = prompt;
28
32
  this.commands = commands;
33
+ this.toolchain = toolchain;
34
+ this.registration = registration;
29
35
  this.write = write;
30
36
  this.registry = path.join(home, ".workbench", "installation.json");
31
37
  }
@@ -35,7 +41,7 @@ export default class WorkbenchBootstrap {
35
41
  const manifest = JSON.parse(await fs.readFile(path.join(root, "package.json"), "utf8"));
36
42
  if (manifest.name !== "workbench-root") return false;
37
43
  await fs.access(path.join(root, "wb"));
38
- await fs.access(path.join(root, "installation", "install.mjs"));
44
+ await fs.access(path.join(root, "scripts", "install.mjs"));
39
45
  await fs.access(path.join(root, "cli", "dispatch.mjs"));
40
46
  return true;
41
47
  } catch (error) {
@@ -151,12 +157,11 @@ export default class WorkbenchBootstrap {
151
157
  }
152
158
  await this.requireTool("git");
153
159
  await this.requireTool("bash");
154
- await this.requireTool("vp");
155
160
  const warning = installed?.phase === "ready"
156
161
  ? `\nNote: Previous installation at ${installed.root} no longer exists and will be recreated.`
157
162
  : installed ? `\nNote: Incomplete install attempt at ${installed.root} will be removed.` : "";
158
163
  const choice = await this.prompt.choose(
159
- `Workbench is not installed. This command will clone the repository and run build commands. It may take 1-2 minutes. Continue?${warning}`,
164
+ `Workbench is not installed. This command will provision a private Vite+ toolchain if needed, clone the repository, run build commands, and register wb through your package manager. Continue?${warning}`,
160
165
  ["Let's go!", "Cancel"],
161
166
  );
162
167
  if (choice !== "Let's go!") throw new DOMException("Setup cancelled.", "AbortError");
@@ -191,6 +196,8 @@ export default class WorkbenchBootstrap {
191
196
  }
192
197
  if (latest && latest.phase !== "ready") await this.discardIncomplete(latest);
193
198
  await this.requireEmptyDestination(root);
199
+ await this.registration.prepare();
200
+ await this.toolchain.ensure();
194
201
  let record = { version: 1, root, phase: "cloning" };
195
202
  await this.writeInstallation(record);
196
203
  await this.prompt.withHeader(async () => {
@@ -202,7 +209,8 @@ export default class WorkbenchBootstrap {
202
209
  if (!await this.checkoutExists(root)) throw new Error("Clone did not produce a Workbench checkout.");
203
210
  record = { ...record, phase: "setup" };
204
211
  await this.writeInstallation(record);
205
- await this.commands.run(process.execPath, [path.join(root, "installation", "install.mjs"), "--prepare"], { cwd: root });
212
+ await this.commands.run(process.execPath, [path.join(root, "scripts", "install.mjs"), "--prepare"], { cwd: root });
213
+ await this.registration.register(root);
206
214
  } catch (error) {
207
215
  try { await this.discardIncomplete(record); }
208
216
  catch (cleanupError) {
@@ -220,7 +228,7 @@ export default class WorkbenchBootstrap {
220
228
  }
221
229
  // Persist readiness before optional platform actions: missing native artifacts
222
230
  // must not turn a usable source installation into an endless setup loop.
223
- await this.commands.run(process.execPath, [path.join(root, "installation", "install.mjs"),
231
+ await this.commands.run(process.execPath, [path.join(root, "scripts", "install.mjs"),
224
232
  args[0] === "connect" ? "--connect" : "--welcome"], { cwd: root, interactive: true });
225
233
  }
226
234
 
@@ -20,9 +20,39 @@ export default class WorkbenchBootstrapCommand {
20
20
  return key === undefined ? undefined : this.environment[key];
21
21
  }
22
22
 
23
+ environmentFor(overrides = {}) {
24
+ const environment = { ...this.environment };
25
+ for (const [name, value] of Object.entries(overrides)) {
26
+ for (const key of Object.keys(environment)) {
27
+ if (key === name || process.platform === "win32" && key.toLowerCase() === name.toLowerCase()) delete environment[key];
28
+ }
29
+ if (value !== undefined && value !== null) environment[name] = value;
30
+ }
31
+ return environment;
32
+ }
33
+
34
+ async findExecutable(command) {
35
+ const names = process.platform === "win32" ? [`${command}.exe`] : [command];
36
+ for (const directory of (this.variable("PATH") || "").split(path.delimiter).filter(Boolean)) {
37
+ for (const name of names) {
38
+ const candidate = path.resolve(directory, name);
39
+ try {
40
+ const stat = await fs.stat(candidate);
41
+ if (stat.isFile()) return candidate;
42
+ } catch (error) {
43
+ if (error.code !== "ENOENT" && error.code !== "ENOTDIR") throw error;
44
+ }
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+
23
50
  async resolve(command) {
24
51
  if (process.platform === "win32" && command === "bash") return [await this.resolveBash()];
25
- if (process.platform !== "win32" || !["npm", "pnpm"].includes(command)) return [command];
52
+ if (!["npm", "pnpm"].includes(command)) return [command];
53
+ const direct = await this.findExecutable(command);
54
+ if (direct) return [direct];
55
+ if (process.platform !== "win32") throw Object.assign(new Error(`${command} is unavailable on PATH.`), { code: "ENOENT" });
26
56
  const directories = [path.dirname(process.execPath), ...(this.variable("PATH") || "").split(path.delimiter)];
27
57
  for (const directory of directories) {
28
58
  if (!directory) continue;
@@ -92,7 +122,7 @@ export default class WorkbenchBootstrapCommand {
92
122
  return await new Promise((resolve, reject) => {
93
123
  const child = spawn(executable, [...prefix, ...args], {
94
124
  cwd,
95
- env: { ...this.environment, ...environment },
125
+ env: this.environmentFor(environment),
96
126
  windowsHide: true,
97
127
  stdio: interactive ? "inherit" : ["ignore", "pipe", "pipe"],
98
128
  signal,
@@ -0,0 +1,184 @@
1
+ /*
2
+ * Exports:
3
+ * - WorkbenchPackageRegistration (default): capture the invoking package manager and register wb in its public global bin.
4
+ */
5
+ import fs from "node:fs/promises";
6
+ import { constants } from "node:fs";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import WorkbenchBootstrapCommand from "./WorkbenchBootstrapCommand.mjs";
11
+
12
+ const managers = {
13
+ npm: {
14
+ binArgs: ["prefix", "--global"],
15
+ packageArgs: ["root", "--global"],
16
+ packagePaths: roots => roots.map(root => path.join(root, "@inthedark", "wb")),
17
+ bin: prefix => process.platform === "win32" ? prefix : path.join(prefix, "bin"),
18
+ install: (root, prefix) => ["install", "--global", "--prefix", prefix, path.join(root, "package")],
19
+ },
20
+ pnpm: {
21
+ binArgs: ["bin", "--global"],
22
+ packageArgs: ["list", "--global", "--parseable", "@inthedark/wb"],
23
+ packagePaths: roots => roots.slice(1),
24
+ bin: directory => directory,
25
+ install: root => ["add", "--global", path.join(root, "package")],
26
+ },
27
+ };
28
+
29
+ export default class WorkbenchPackageRegistration {
30
+ constructor({
31
+ environment = process.env, home = os.homedir(), nodePath = process.execPath,
32
+ packageRoot = path.dirname(fileURLToPath(import.meta.url)),
33
+ commands = new WorkbenchBootstrapCommand({ environment: { ...environment } }),
34
+ } = {}) {
35
+ this.commands = commands;
36
+ this.home = home;
37
+ this.nodePath = nodePath;
38
+ this.packageRoot = packageRoot;
39
+ }
40
+
41
+ async capture() {
42
+ const userAgent = this.commands.variable("npm_config_user_agent");
43
+ const identity = userAgent?.match(/^([a-z]+)(?:\/|\s|$)/iu)?.[1]?.toLowerCase();
44
+ const entry = this.commands.variable("npm_execpath");
45
+ const entryIdentity = entry && /(?:^|[/\\])(npm-cli\.js|npx-cli\.js|pnpm(?:\.cjs|\.js|\.exe)?)$/iu.exec(entry)?.[1];
46
+ const identified = entryIdentity && (entryIdentity.toLowerCase().startsWith("pnpm") ? "pnpm" : "npm");
47
+ if (identity && !Object.hasOwn(managers, identity)) {
48
+ throw new Error(`Automatic wb registration supports npm and pnpm, not ${identity}. Run npx @inthedark/wb or pnpm dlx @inthedark/wb.`);
49
+ }
50
+ if (identity && identified && identity !== identified) {
51
+ throw new Error("Invoking package-manager metadata conflicts. Run Workbench directly with npx or pnpm dlx.");
52
+ }
53
+ const name = identity ?? identified ?? "npm";
54
+ let command;
55
+ if (entry && identified === name) {
56
+ if (!path.isAbsolute(entry)) throw new Error("Invoking package-manager executable must be an absolute path.");
57
+ await fs.access(entry);
58
+ command = /\.(?:cjs|mjs|js)$/iu.test(entry) ? [this.nodePath, entry] : [entry];
59
+ } else {
60
+ command = await this.commands.resolve(name);
61
+ }
62
+ return { name, command };
63
+ }
64
+
65
+ async execute(selection, args, options = {}) {
66
+ const [command, ...prefix] = selection.command;
67
+ return this.commands.run(command, [...prefix, ...args], { cwd: this.home, ...options });
68
+ }
69
+
70
+ async reportPaths(selection, args) {
71
+ let output = "";
72
+ await this.execute(selection, args, { output: { write: bytes => { output += bytes.toString(); } } });
73
+ const locations = output.trim().split(/\r?\n/u);
74
+ if (locations.some(location => !location || !path.isAbsolute(location))) {
75
+ throw new Error(`${selection.name} did not report absolute global installation paths.`);
76
+ }
77
+ return locations;
78
+ }
79
+
80
+ async destination(selection) {
81
+ const locations = await this.reportPaths(selection, managers[selection.name].binArgs);
82
+ if (locations.length !== 1) throw new Error(`${selection.name} reported more than one global bin directory.`);
83
+ const [location] = locations;
84
+ return { prefix: location, bin: managers[selection.name].bin(location) };
85
+ }
86
+
87
+ async checkWritable(directory) {
88
+ let current = directory;
89
+ while (true) {
90
+ try {
91
+ await fs.access(current, constants.W_OK);
92
+ return;
93
+ } catch (error) {
94
+ if (error.code !== "ENOENT") throw new Error(`The package manager's global bin is not writable: ${directory}. Fix its permissions or configure a user-owned global prefix.`, { cause: error });
95
+ const parent = path.dirname(current);
96
+ if (parent === current) throw error;
97
+ current = parent;
98
+ }
99
+ }
100
+ }
101
+
102
+ samePath(left, right) {
103
+ const normalise = value => process.platform === "win32" ? path.resolve(value).toLowerCase() : path.resolve(value);
104
+ return normalise(left) === normalise(right);
105
+ }
106
+
107
+ async publicCommand(bin) {
108
+ for (const name of process.platform === "win32" ? ["wb.exe", "wb.cmd", "wb.bat", "wb"] : ["wb"]) {
109
+ const candidate = path.join(bin, name);
110
+ try {
111
+ if ((await fs.stat(candidate)).isFile()) return candidate;
112
+ } catch (error) {
113
+ if (error.code !== "ENOENT" && error.code !== "ENOTDIR") throw error;
114
+ }
115
+ }
116
+ return null;
117
+ }
118
+
119
+ async checkExistingCommand(bin) {
120
+ const command = await this.publicCommand(bin);
121
+ if (!command) return;
122
+ const manager = managers[this.selection.name];
123
+ const roots = await this.reportPaths(this.selection, manager.packageArgs);
124
+ for (const packageRoot of manager.packagePaths(roots)) {
125
+ try {
126
+ const manifest = JSON.parse(await fs.readFile(path.join(packageRoot, "package.json"), "utf8"));
127
+ if (manifest.name === "@inthedark/wb" && manifest.bin?.wb === "wb") {
128
+ await fs.access(path.join(packageRoot, "wb"));
129
+ return;
130
+ }
131
+ } catch (error) {
132
+ if (error.code !== "ENOENT") throw error;
133
+ }
134
+ }
135
+ throw new Error(`The global bin already contains ${command}, but ${this.selection.name} does not own a Workbench package there. Resolve that conflict before installing.`);
136
+ }
137
+
138
+ async checkPath(bin) {
139
+ const directories = (this.commands.variable("PATH") || "").split(path.delimiter).filter(Boolean);
140
+ if (!directories.some(directory => this.samePath(directory, bin))) {
141
+ throw new Error(`${bin} is ${this.selection.name}'s global bin but is not on PATH. Add that directory to your PATH, open a new terminal, and retry.`);
142
+ }
143
+ const names = process.platform === "win32" ? ["wb.exe", "wb.cmd", "wb.bat", "wb"] : ["wb"];
144
+ const ownEntry = await fs.realpath(path.join(this.packageRoot, "wb"));
145
+ for (const directory of directories) {
146
+ if (this.samePath(directory, bin)) {
147
+ await this.checkExistingCommand(bin);
148
+ return;
149
+ }
150
+ for (const name of names) {
151
+ const candidate = path.join(directory, name);
152
+ let target;
153
+ try { target = await fs.realpath(candidate); }
154
+ catch (error) { if (error.code === "ENOENT" || error.code === "ENOTDIR") continue; throw error; }
155
+ if (this.samePath(target, ownEntry)) continue;
156
+ // npm's npx Windows wrappers sit beside the package's node_modules entry.
157
+ const injected = path.join(directory, "..", "@inthedark", "wb", "wb");
158
+ try {
159
+ if (this.samePath(await fs.realpath(injected), ownEntry)) continue;
160
+ } catch (error) { if (error.code !== "ENOENT" && error.code !== "ENOTDIR") throw error; }
161
+ throw new Error(`Another wb command at ${candidate} precedes ${bin} on PATH. Resolve that conflict before installing; it was not changed.`);
162
+ }
163
+ }
164
+ }
165
+
166
+ async prepare() {
167
+ const selection = await this.capture();
168
+ const destination = await this.destination(selection);
169
+ this.selection = { ...selection, ...destination };
170
+ await this.checkPath(destination.bin);
171
+ await this.checkWritable(destination.bin);
172
+ }
173
+
174
+ async register(root) {
175
+ if (!this.selection) throw new Error("Package registration must be checked before installing Workbench.");
176
+ const current = await this.destination(this.selection);
177
+ if (!this.samePath(current.bin, this.selection.bin) || !this.samePath(current.prefix, this.selection.prefix)) {
178
+ throw new Error("The package manager's global destination changed during installation. Run Workbench again.");
179
+ }
180
+ await this.execute(this.selection, managers[this.selection.name].install(root, this.selection.prefix));
181
+ if (!await this.publicCommand(this.selection.bin)) throw new Error(`${this.selection.name} did not register the public wb command in ${this.selection.bin}.`);
182
+ await this.checkPath(this.selection.bin);
183
+ }
184
+ }
@@ -0,0 +1,185 @@
1
+ /*
2
+ * Exports:
3
+ * - WorkbenchToolchain (default): persist Vite+ selection and own isolated provisioning and pinned child environments.
4
+ */
5
+ import fs from "node:fs/promises";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { randomUUID } from "node:crypto";
9
+ import { DatabaseSync } from "node:sqlite";
10
+ import WorkbenchBootstrapCommand from "./WorkbenchBootstrapCommand.mjs";
11
+ import resolveWorkbenchDataRoot from "./workbench-data-root.mjs";
12
+ import installVitePlus from "./vite-plus-installer.mjs";
13
+
14
+ const directoryVariables = ["VP_HOME", "VP_BIN_DIR", "VP_DATA_DIR", "VP_CACHE_DIR",
15
+ "XDG_DATA_HOME", "XDG_CONFIG_HOME", "XDG_CACHE_HOME", "XDG_STATE_HOME"];
16
+ const versionVariables = ["VP_NODE_VERSION", "VP_PACKAGE_MANAGER", "VP_NPM_VERSION",
17
+ "VP_PNPM_VERSION", "VP_YARN_VERSION", "VP_BUN_VERSION", "VP_BYPASS"];
18
+
19
+ export default class WorkbenchToolchain {
20
+ constructor({
21
+ environment = process.env, home = os.homedir(),
22
+ dataRoot = resolveWorkbenchDataRoot({ environment, homeDirectory: home }),
23
+ commands = new WorkbenchBootstrapCommand({ environment }), install = installVitePlus,
24
+ } = {}) {
25
+ this.commands = commands;
26
+ this.environment = environment;
27
+ this.dataRoot = path.resolve(dataRoot);
28
+ this.directory = path.join(this.dataRoot, "installation");
29
+ this.receipt = path.join(this.directory, "toolchain.json");
30
+ this.managedHome = path.join(this.dataRoot, "toolchain");
31
+ this.install = install;
32
+ }
33
+
34
+ async read() {
35
+ let text;
36
+ try { text = await fs.readFile(this.receipt, "utf8"); }
37
+ catch (error) { if (error.code === "ENOENT") return null; throw error; }
38
+ let record;
39
+ try { record = JSON.parse(text); }
40
+ catch (error) { throw new Error(`Invalid toolchain receipt: ${this.receipt}. It was preserved.`, { cause: error }); }
41
+ if (record?.version !== 1 || typeof record.ready !== "boolean" || !["managed", "external"].includes(record.kind)
42
+ || record.kind === "external" && !record.ready
43
+ || typeof record.executable !== "string" || !path.isAbsolute(record.executable)
44
+ || !record.environment || typeof record.environment !== "object" || Array.isArray(record.environment)
45
+ || Object.keys(record.environment).some(name => !directoryVariables.includes(name))
46
+ || directoryVariables.some(name => !(record.environment[name] === null
47
+ || typeof record.environment[name] === "string" && path.isAbsolute(record.environment[name])))) {
48
+ throw new Error(`Invalid toolchain receipt: ${this.receipt}. It was preserved.`);
49
+ }
50
+ if (record.kind === "managed" && (record.environment.VP_HOME !== this.managedHome
51
+ || record.executable !== this.managedExecutable())) {
52
+ throw new Error(`Managed toolchain receipt points outside its owned location: ${this.receipt}.`);
53
+ }
54
+ return record;
55
+ }
56
+
57
+ managedExecutable() {
58
+ return path.join(this.managedHome, "bin", process.platform === "win32" ? "vp.exe" : "vp");
59
+ }
60
+
61
+ selection(executable, kind) {
62
+ const environment = Object.fromEntries(directoryVariables.map(name => {
63
+ const value = this.commands.variable(name)?.trim();
64
+ if (value && !path.isAbsolute(value) && name.startsWith("VP_")) {
65
+ throw new Error(`${name} must be an absolute path to persist the Vite+ environment.`);
66
+ }
67
+ return [name, value && path.isAbsolute(value) ? value : null];
68
+ }));
69
+ if (kind === "managed") {
70
+ for (const name of directoryVariables.filter(name => name.startsWith("VP_"))) environment[name] = null;
71
+ environment.VP_HOME = this.managedHome;
72
+ }
73
+ return { version: 1, kind, ready: kind === "external", executable, environment };
74
+ }
75
+
76
+ childEnvironment(record, pins) {
77
+ return {
78
+ ...Object.fromEntries(versionVariables.map(name => [name, undefined])),
79
+ ...record.environment,
80
+ WORKBENCH_DATA_ROOT: this.dataRoot,
81
+ ...(pins ? {
82
+ VP_NODE_VERSION: pins.node,
83
+ VP_PACKAGE_MANAGER: `${pins.manager}@${pins.packageManager}`,
84
+ VP_PNPM_VERSION: pins.packageManager,
85
+ } : {}),
86
+ };
87
+ }
88
+
89
+ async verify(record) {
90
+ try {
91
+ await this.commands.run(record.executable, ["--version"], {
92
+ environment: this.childEnvironment(record), output: { write() {} }, errorOutput: { write() {} },
93
+ });
94
+ } catch (error) {
95
+ throw new Error(`Recorded Vite+ is unavailable at ${record.executable}. ${record.kind === "external"
96
+ ? "Restore that installation; Workbench will not replace a user-owned toolchain."
97
+ : "Run wb repair to restore the managed toolchain."}`, { cause: error });
98
+ }
99
+ }
100
+
101
+ async discover() {
102
+ const executable = await this.commands.findExecutable("vp");
103
+ if (!executable) return null;
104
+ const record = this.selection(executable, "external");
105
+ await this.verify(record);
106
+ return record;
107
+ }
108
+
109
+ async resolve() {
110
+ const record = await this.read() ?? await this.discover();
111
+ if (!record) throw new Error("Workbench's Vite+ toolchain is unavailable. Run wb repair.");
112
+ if (!record.ready) throw new Error("Workbench toolchain setup is incomplete. Run wb repair.");
113
+ return record;
114
+ }
115
+
116
+ async ensure({ onOutput } = {}) {
117
+ if (this.environment.WORKBENCH_THREAD_ID || this.environment.CODEX_THREAD_ID) {
118
+ throw new Error("Managed threads cannot provision Workbench's toolchain.");
119
+ }
120
+ await fs.mkdir(this.directory, { recursive: true });
121
+ const lock = new DatabaseSync(path.join(this.directory, "toolchain.lock.sqlite3"));
122
+ try {
123
+ try { lock.exec("PRAGMA busy_timeout = 0; BEGIN EXCLUSIVE"); }
124
+ catch (error) {
125
+ if (error.errcode === 5 || error.errcode === 6) throw new Error("Another Workbench toolchain setup is running. Retry after it finishes.");
126
+ throw error;
127
+ }
128
+ let record = await this.read();
129
+ if (record?.ready) {
130
+ try { await this.verify(record); return record; }
131
+ catch (error) {
132
+ if (record.kind !== "managed") throw error;
133
+ if (error.cause?.code !== "ENOENT") throw error;
134
+ }
135
+ } else if (!record) {
136
+ record = await this.discover();
137
+ if (record) { await this.write(record); return record; }
138
+ record = this.selection(this.managedExecutable(), "managed");
139
+ await this.write(record);
140
+ }
141
+ if (record.ready) {
142
+ record = { ...record, ready: false };
143
+ await this.write(record);
144
+ }
145
+ onOutput?.(`Provisioning Workbench's managed Vite+ at ${this.managedHome}\n`);
146
+ await this.install({ home: this.managedHome, directory: this.directory, commands: this.commands, onOutput });
147
+ await this.verify(record);
148
+ record = { ...record, ready: true };
149
+ await this.write(record);
150
+ return record;
151
+ } finally { lock.close(); }
152
+ }
153
+
154
+ async write(record) {
155
+ const temporary = `${this.receipt}.${randomUUID()}.tmp`;
156
+ await fs.writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, { flag: "wx", mode: 0o600 });
157
+ await fs.rename(temporary, this.receipt);
158
+ }
159
+
160
+ async pins(root) {
161
+ const manifest = JSON.parse(await fs.readFile(path.join(root, "package.json"), "utf8"));
162
+ const runtime = manifest.devEngines?.runtime;
163
+ const manager = manifest.devEngines?.packageManager;
164
+ const exact = value => typeof value === "string" && /^\d+\.\d+\.\d+$/u.test(value);
165
+ if (runtime?.name !== "node" || manager?.name !== "pnpm" || !exact(runtime.version) || !exact(manager.version)) {
166
+ throw new Error(`Workbench must declare exact Node and pnpm devEngines pins: ${root}`);
167
+ }
168
+ return { node: runtime.version, manager: manager.name, packageManager: manager.version };
169
+ }
170
+
171
+ async run(args, { cwd, ...options }) {
172
+ const [record, pins] = await Promise.all([this.resolve(), this.pins(cwd)]);
173
+ return this.commands.run(record.executable, args, {
174
+ ...options, cwd, environment: { ...options.environment, ...this.childEnvironment(record, pins) },
175
+ });
176
+ }
177
+
178
+ async runNode(root, entry, args = [], { nodeOptions = [], ...options } = {}) {
179
+ const [record, pins] = await Promise.all([this.resolve(), this.pins(root)]);
180
+ return this.commands.run(record.executable, [
181
+ "env", "exec", "--node", pins.node, "--package-manager", `${pins.manager}@${pins.packageManager}`,
182
+ "node", ...nodeOptions, entry, ...args,
183
+ ], { ...options, environment: { ...options.environment, ...this.childEnvironment(record, pins) } });
184
+ }
185
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inthedark/wb",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,6 +26,11 @@
26
26
  "wb",
27
27
  "WorkbenchBootstrap.mjs",
28
28
  "WorkbenchBootstrapCommand.mjs",
29
+ "WorkbenchToolchain.mjs",
30
+ "WorkbenchPackageRegistration.mjs",
31
+ "vite-plus-installer.mjs",
32
+ "workbench-data-root.mjs",
33
+ "workbench-data-root.d.mts",
29
34
  "WorkbenchInstallPrompt.mjs",
30
35
  "installer-cube.mjs",
31
36
  "index.mjs",
@@ -0,0 +1,52 @@
1
+ /*
2
+ * Exports:
3
+ * - default installVitePlus: execute the pinned official installer without user shell integration.
4
+ */
5
+ import fs from "node:fs/promises";
6
+ import path from "node:path";
7
+ import { randomUUID } from "node:crypto";
8
+
9
+ const VITE_PLUS_VERSION = "1.1.0";
10
+
11
+ export default async function installVitePlus({
12
+ home, directory, commands, onOutput, fetchInstaller = fetch, platform = process.platform, files = fs,
13
+ }) {
14
+ const windows = platform === "win32";
15
+ const extension = windows ? "ps1" : "sh";
16
+ const url = `https://raw.githubusercontent.com/voidzero-dev/vite-plus/v${VITE_PLUS_VERSION}/packages/cli/install.${extension}`;
17
+ const response = await fetchInstaller(url);
18
+ if (!response.ok) {
19
+ await response.body?.cancel();
20
+ throw new Error(`Vite+ installer download failed (HTTP ${response.status}).`);
21
+ }
22
+ const script = await response.text();
23
+ if (!script.trim()) throw new Error("Vite+ installer download was empty.");
24
+ await files.mkdir(directory, { recursive: true });
25
+ const filename = path.join(directory, `vite-plus-${randomUUID()}.${extension}`);
26
+ await files.writeFile(filename, script, { flag: "wx", mode: 0o600 });
27
+ let failure;
28
+ try {
29
+ const environment = Object.fromEntries(Object.keys(commands.environment)
30
+ .filter(name => /^VP_/iu.test(name)).map(name => [name, undefined]));
31
+ Object.assign(environment, {
32
+ VP_HOME: home,
33
+ VP_VERSION: VITE_PLUS_VERSION,
34
+ VP_NODE_MANAGER: "yes",
35
+ VP_PM_MANAGER: "yes",
36
+ // Release-pinned self-setup honours this on Unix and Windows; CI alone still edits profiles.
37
+ VP_SELF_SETUP_NO_MODIFY_PATH: "1",
38
+ CI: "true",
39
+ });
40
+ await commands.run(windows ? "powershell.exe" : "bash",
41
+ windows ? ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", filename] : [filename],
42
+ { cwd: directory, environment, onOutput });
43
+ } catch (error) {
44
+ failure = error;
45
+ }
46
+ try { await files.unlink(filename); }
47
+ catch (error) {
48
+ if (failure) throw new AggregateError([failure, error], `Vite+ setup failed and its downloaded script could not be removed: ${filename}`, { cause: failure });
49
+ throw error;
50
+ }
51
+ if (failure) throw failure;
52
+ }
@@ -0,0 +1,11 @@
1
+ /*
2
+ * Exports:
3
+ * - WorkbenchDataRootOptions: environment, home and platform inputs for data storage.
4
+ * - default resolveWorkbenchDataRoot: dependency-free per-user data location.
5
+ */
6
+ export interface WorkbenchDataRootOptions {
7
+ environment?: NodeJS.ProcessEnv;
8
+ homeDirectory?: string;
9
+ platform?: NodeJS.Platform;
10
+ }
11
+ export default function resolveWorkbenchDataRoot(options?: WorkbenchDataRootOptions): string;
@@ -0,0 +1,20 @@
1
+ /*
2
+ * Exports:
3
+ * - default resolveWorkbenchDataRoot: resolve per-user OS data storage without installed dependencies.
4
+ */
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+
8
+ export default function resolveWorkbenchDataRoot({
9
+ environment = process.env, homeDirectory = os.homedir(), platform = process.platform,
10
+ } = {}) {
11
+ const paths = platform === "win32" ? path.win32 : path.posix;
12
+ const override = environment.WORKBENCH_DATA_ROOT?.trim();
13
+ if (override) return paths.resolve(override);
14
+ if (platform === "win32") {
15
+ return paths.resolve(environment.LOCALAPPDATA?.trim() || paths.join(homeDirectory, "AppData", "Local"), "inthedark", "wb");
16
+ }
17
+ if (platform === "darwin") return paths.resolve(homeDirectory, "Library", "Application Support", "inthedark", "wb");
18
+ const xdg = environment.XDG_DATA_HOME?.trim();
19
+ return paths.resolve(xdg && paths.isAbsolute(xdg) ? xdg : paths.join(homeDirectory, ".local", "share"), "inthedark", "wb");
20
+ }