@inthedark/wb 0.0.4 → 0.0.6
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 +12 -1
- package/StoreCommandRunner.mjs +10 -2
- package/WorkbenchBootstrap.mjs +25 -64
- package/WorkbenchBootstrapCommand.mjs +32 -2
- package/WorkbenchPackageRegistration.mjs +184 -0
- package/WorkbenchToolchain.mjs +185 -0
- package/index.mjs +0 -8
- package/package.json +6 -1
- package/vite-plus-installer.mjs +52 -0
- package/workbench-data-root.d.mts +11 -0
- package/workbench-data-root.mjs +20 -0
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`
|
|
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
|
package/StoreCommandRunner.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
|
|
9
9
|
const MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
10
10
|
const PACKAGED_WB = path.join(path.dirname(fileURLToPath(import.meta.url)), "wb");
|
|
11
|
+
const DANGEROUS_ACCESS = "DANGEROUSLY_ACCESS_ENV_VARS";
|
|
11
12
|
|
|
12
13
|
export default class StoreCommandRunner {
|
|
13
14
|
constructor({ maxOutputBytes = MAX_OUTPUT_BYTES, packagedWb = PACKAGED_WB } = {}) {
|
|
@@ -19,9 +20,16 @@ export default class StoreCommandRunner {
|
|
|
19
20
|
async run(argv, { signal } = {}) {
|
|
20
21
|
signal?.throwIfAborted();
|
|
21
22
|
// `wb` means this package's own bootstrap so lookups work without PATH setup.
|
|
22
|
-
const
|
|
23
|
+
const packaged = argv[0] === "wb";
|
|
24
|
+
const [executable, ...args] = packaged ? [process.execPath, this.packagedWb, ...argv.slice(1)] : argv;
|
|
23
25
|
return await new Promise((resolve, reject) => {
|
|
24
|
-
const child = spawn(executable, args, {
|
|
26
|
+
const child = spawn(executable, args, {
|
|
27
|
+
env: packaged ? { ...process.env, [DANGEROUS_ACCESS]: "1" } : process.env,
|
|
28
|
+
shell: false,
|
|
29
|
+
signal,
|
|
30
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
31
|
+
windowsHide: true,
|
|
32
|
+
});
|
|
25
33
|
const chunks = [];
|
|
26
34
|
let length = 0;
|
|
27
35
|
let overflow = false;
|
package/WorkbenchBootstrap.mjs
CHANGED
|
@@ -11,21 +11,29 @@ 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({
|
|
17
19
|
home = os.homedir(),
|
|
18
20
|
packageRoot = path.dirname(fileURLToPath(import.meta.url)),
|
|
19
21
|
environment = process.env,
|
|
22
|
+
platform = process.platform,
|
|
20
23
|
prompt = new WorkbenchInstallPrompt(),
|
|
21
|
-
commands = new WorkbenchBootstrapCommand(),
|
|
24
|
+
commands = new WorkbenchBootstrapCommand({ environment }),
|
|
25
|
+
toolchain = new WorkbenchToolchain({ home, environment, commands }),
|
|
26
|
+
registration = new WorkbenchPackageRegistration({ home, environment, packageRoot }),
|
|
22
27
|
write = text => process.stdout.write(text),
|
|
23
28
|
} = {}) {
|
|
24
29
|
this.home = home;
|
|
25
30
|
this.packageRoot = packageRoot;
|
|
26
31
|
this.environment = environment;
|
|
32
|
+
this.platform = platform;
|
|
27
33
|
this.prompt = prompt;
|
|
28
34
|
this.commands = commands;
|
|
35
|
+
this.toolchain = toolchain;
|
|
36
|
+
this.registration = registration;
|
|
29
37
|
this.write = write;
|
|
30
38
|
this.registry = path.join(home, ".workbench", "installation.json");
|
|
31
39
|
}
|
|
@@ -35,7 +43,7 @@ export default class WorkbenchBootstrap {
|
|
|
35
43
|
const manifest = JSON.parse(await fs.readFile(path.join(root, "package.json"), "utf8"));
|
|
36
44
|
if (manifest.name !== "workbench-root") return false;
|
|
37
45
|
await fs.access(path.join(root, "wb"));
|
|
38
|
-
await fs.access(path.join(root, "
|
|
46
|
+
await fs.access(path.join(root, "scripts", "install.mjs"));
|
|
39
47
|
await fs.access(path.join(root, "cli", "dispatch.mjs"));
|
|
40
48
|
return true;
|
|
41
49
|
} catch (error) {
|
|
@@ -88,47 +96,6 @@ export default class WorkbenchBootstrap {
|
|
|
88
96
|
}
|
|
89
97
|
}
|
|
90
98
|
|
|
91
|
-
async discardIncomplete(record) {
|
|
92
|
-
const root = path.resolve(record.root);
|
|
93
|
-
if (root !== record.root || !["wb", "workbench"].includes(path.basename(root).toLowerCase())) {
|
|
94
|
-
throw new Error(`Cannot verify failed install destination ${record.root}; it was preserved.`);
|
|
95
|
-
}
|
|
96
|
-
let stat;
|
|
97
|
-
try { stat = await fs.lstat(root); }
|
|
98
|
-
catch (error) {
|
|
99
|
-
if (error.code !== "ENOENT") throw error;
|
|
100
|
-
await fs.unlink(this.registry);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
104
|
-
throw new Error(`Failed install destination ${root} is not an ordinary directory; it was preserved.`);
|
|
105
|
-
}
|
|
106
|
-
if (await fs.realpath(root) !== root) {
|
|
107
|
-
throw new Error(`Failed install destination ${root} resolves elsewhere; it was preserved.`);
|
|
108
|
-
}
|
|
109
|
-
if ((await fs.readdir(root)).length) {
|
|
110
|
-
if (!await this.checkoutExists(root)) {
|
|
111
|
-
throw new Error(`Failed install destination ${root} is not a complete Workbench checkout; it was preserved.`);
|
|
112
|
-
}
|
|
113
|
-
const git = await fs.lstat(path.join(root, ".git")).catch(error => {
|
|
114
|
-
if (error.code === "ENOENT") return null;
|
|
115
|
-
throw error;
|
|
116
|
-
});
|
|
117
|
-
if (!git?.isDirectory() || git.isSymbolicLink()) {
|
|
118
|
-
throw new Error(`Failed install destination ${root} is not a regular Git clone; it was preserved.`);
|
|
119
|
-
}
|
|
120
|
-
let changes = "";
|
|
121
|
-
await this.commands.run("git", ["status", "--porcelain=v1", "--untracked-files=all"], {
|
|
122
|
-
cwd: root,
|
|
123
|
-
output: { write: bytes => { changes += bytes.toString(); } },
|
|
124
|
-
errorOutput: { write() {} },
|
|
125
|
-
});
|
|
126
|
-
if (changes.trim()) throw new Error(`Failed install destination ${root} has changes; it was preserved.`);
|
|
127
|
-
}
|
|
128
|
-
await fs.rm(root, { recursive: true });
|
|
129
|
-
await fs.unlink(this.registry);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
99
|
async run(args) {
|
|
133
100
|
const managed = this.environment.WORKBENCH_THREAD_ID || this.environment.CODEX_THREAD_ID;
|
|
134
101
|
const view = args[0] === "view" && (args.length === 1 || args.length === 2 && ["daemon", "app"].includes(args[1]));
|
|
@@ -151,12 +118,11 @@ export default class WorkbenchBootstrap {
|
|
|
151
118
|
}
|
|
152
119
|
await this.requireTool("git");
|
|
153
120
|
await this.requireTool("bash");
|
|
154
|
-
await this.requireTool("vp");
|
|
155
121
|
const warning = installed?.phase === "ready"
|
|
156
122
|
? `\nNote: Previous installation at ${installed.root} no longer exists and will be recreated.`
|
|
157
|
-
: installed ? `\
|
|
123
|
+
: installed ? `\nResuming incomplete install at ${installed.root}.` : "";
|
|
158
124
|
const choice = await this.prompt.choose(
|
|
159
|
-
`Workbench is not installed. This command will clone the repository
|
|
125
|
+
`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
126
|
["Let's go!", "Cancel"],
|
|
161
127
|
);
|
|
162
128
|
if (choice !== "Let's go!") throw new DOMException("Setup cancelled.", "AbortError");
|
|
@@ -189,30 +155,25 @@ export default class WorkbenchBootstrap {
|
|
|
189
155
|
if (latest?.root !== installed?.root || latest?.phase !== installed?.phase) {
|
|
190
156
|
throw new Error("The selected Workbench installation changed while setup was open. Run wb again.");
|
|
191
157
|
}
|
|
192
|
-
|
|
193
|
-
await this.requireEmptyDestination(root);
|
|
194
|
-
|
|
195
|
-
await this.writeInstallation(
|
|
158
|
+
const resume = latest?.root === root && await this.checkoutExists(root);
|
|
159
|
+
if (!resume) await this.requireEmptyDestination(root);
|
|
160
|
+
// Recording before provisioning keeps the chosen location as the next default if anything fails.
|
|
161
|
+
await this.writeInstallation({ version: 1, root, phase: resume ? "setup" : "cloning" });
|
|
162
|
+
await this.registration.prepare();
|
|
163
|
+
await this.toolchain.ensure();
|
|
196
164
|
await this.prompt.withHeader(async () => {
|
|
197
|
-
this.write(
|
|
198
|
-
|
|
165
|
+
this.write(`${resume ? "Resuming" : "Installing"} Workbench at ${root}\n`);
|
|
166
|
+
if (!resume) {
|
|
199
167
|
// Install sandboxes substitute a local snapshot; ordinary installs clone the public repository.
|
|
200
168
|
const repository = this.environment.WORKBENCH_INSTALL_REPOSITORY || "https://github.com/ErisInTheDark/workbench.git";
|
|
201
169
|
await this.commands.run("git", ["clone", "--progress", "--branch", "main", "--single-branch", repository, root]);
|
|
202
170
|
if (!await this.checkoutExists(root)) throw new Error("Clone did not produce a Workbench checkout.");
|
|
203
|
-
|
|
204
|
-
await this.writeInstallation(record);
|
|
205
|
-
await this.commands.run(process.execPath, [path.join(root, "installation", "install.mjs"), "--prepare"], { cwd: root });
|
|
206
|
-
} catch (error) {
|
|
207
|
-
try { await this.discardIncomplete(record); }
|
|
208
|
-
catch (cleanupError) {
|
|
209
|
-
throw new Error(`${error.message} Incomplete checkout was preserved at ${root}: ${cleanupError.message}`, { cause: error });
|
|
210
|
-
}
|
|
211
|
-
throw error;
|
|
171
|
+
await this.writeInstallation({ version: 1, root, phase: "setup" });
|
|
212
172
|
}
|
|
173
|
+
await this.commands.run(process.execPath, [path.join(root, "scripts", "install.mjs"), "--prepare"], { cwd: root });
|
|
174
|
+
await this.registration.register(root);
|
|
213
175
|
});
|
|
214
|
-
|
|
215
|
-
await this.writeInstallation(record);
|
|
176
|
+
await this.writeInstallation({ version: 1, root, phase: "ready" });
|
|
216
177
|
} finally {
|
|
217
178
|
// Closing the connection also releases the OS lock after normal completion;
|
|
218
179
|
// process death releases it without a stale PID or lock-file recovery path.
|
|
@@ -220,7 +181,7 @@ export default class WorkbenchBootstrap {
|
|
|
220
181
|
}
|
|
221
182
|
// Persist readiness before optional platform actions: missing native artifacts
|
|
222
183
|
// must not turn a usable source installation into an endless setup loop.
|
|
223
|
-
await this.commands.run(process.execPath, [path.join(root, "
|
|
184
|
+
await this.commands.run(process.execPath, [path.join(root, "scripts", "install.mjs"),
|
|
224
185
|
args[0] === "connect" ? "--connect" : "--welcome"], { cwd: root, interactive: true });
|
|
225
186
|
}
|
|
226
187
|
|
|
@@ -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 (
|
|
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:
|
|
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/index.mjs
CHANGED
|
@@ -17,10 +17,6 @@ function warnDefault(message) {
|
|
|
17
17
|
process.emitWarning(message, "WorkbenchStoreWarning");
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function isManagedAgentProcess() {
|
|
21
|
-
return Boolean(process.env.WORKBENCH_THREAD_ID?.trim() || process.env.CODEX_THREAD_ID?.trim());
|
|
22
|
-
}
|
|
23
|
-
|
|
24
20
|
function stripTerminalLineEnding(value) {
|
|
25
21
|
return value.endsWith("\r\n") ? value.slice(0, -2) : value.endsWith("\n") ? value.slice(0, -1) : value;
|
|
26
22
|
}
|
|
@@ -58,10 +54,6 @@ async function resolveReferences(parsed, definitions, { runner, signal, warn })
|
|
|
58
54
|
}
|
|
59
55
|
}
|
|
60
56
|
if (!lookups.size) return parsed;
|
|
61
|
-
if (isManagedAgentProcess()) {
|
|
62
|
-
warn("Store lookups are disabled in Workbench-managed agent processes. Store references were left unchanged.");
|
|
63
|
-
return parsed;
|
|
64
|
-
}
|
|
65
57
|
const results = new Map(await Promise.all(Array.from(lookups, async ([id, lookup]) => {
|
|
66
58
|
const result = await runner.run(lookup.argv, { signal });
|
|
67
59
|
if (!result.ok) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inthedark/wb",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
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
|
+
}
|