@inthedark/wb 0.0.1 → 0.0.2
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 +60 -60
- package/WorkbenchBootstrap.mjs +168 -168
- package/WorkbenchBootstrapCommand.mjs +138 -138
- package/package.json +2 -1
- package/wb.schema.json +71 -0
package/README.md
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
|
|
2
|
-
# workbench
|
|
3
|
-
|
|
4
|
-
an agentic ide about the human
|
|
5
|
-
|
|
6
|
-
## how 2 use
|
|
7
|
-
```bash
|
|
8
|
-
# install & open
|
|
9
|
-
npx @inthedark/wb
|
|
10
|
-
|
|
11
|
-
# set up daemon on another device
|
|
12
|
-
npx @inthedark/wb connect
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## features
|
|
16
|
-
|
|
17
|
-
key features:
|
|
18
|
-
- fully customisable agent instructions with support for comments & imports
|
|
19
|
-
- strong thread transcript UX for when you need to keep an eye on a thread (command matching + html elements for agents)
|
|
20
|
-
- strong thread management UX for when you're managing LOTS of threads (thread status-sorted sidebar)
|
|
21
|
-
- better shared workspace support than any other agentic ide, because a lot of work is not worktree->pr shaped; agents claim the files they need and other agents don't freak out about unclaimed dirt
|
|
22
|
-
- near-identical agent behaviour across providers (codex, opencode 2, claude code) as if they're the same harness
|
|
23
|
-
- agents can orchestrate subagents working in threads across providers and projects
|
|
24
|
-
- customisable voice to text ("what if voice to text was like working in a rich text editor")
|
|
25
|
-
- git UI for doing your own work at the same time as other agents, allowing easy commits while agents have other files claimed
|
|
26
|
-
- quickly switch between saved profiles for harness/model/effort/window/fast mode/agent identity
|
|
27
|
-
- automatic compaction on turn start past configurable time & token thresholds (reduce cache write costs)
|
|
28
|
-
- agents can search repos with shell tools like they're on disk via vfs
|
|
29
|
-
|
|
30
|
-
setup/meta:
|
|
31
|
-
- built on top of other harnesses (codex, opencode 2, claude code) so you can use subscriptions
|
|
32
|
-
- tailscale integration
|
|
33
|
-
- daemon sleeps when idle, leaving only a light http wake service up
|
|
34
|
-
|
|
35
|
-
unpolished/unfinished/broken stuff (coming soon tm!!!!!):
|
|
36
|
-
- soon, autocomplete/suggestions extension for vscode & similar (highly configurable, able to be FAST)
|
|
37
|
-
- worktree->pr workflow is unsupported (gonna be honest i have been in a cave working solo on wb for so long that i have not needed this yet)
|
|
38
|
-
- browse AX & UX
|
|
39
|
-
- search UX
|
|
40
|
-
- i'd like more git ui
|
|
41
|
-
- many places around the app could use a UI polish pass
|
|
42
|
-
- the codebase itself could use a polish pass, esp. the react & tailwind
|
|
43
|
-
|
|
44
|
-
## tech
|
|
45
|
-
|
|
46
|
-
note: have tried to keep dependencies minimal for the most part
|
|
47
|
-
|
|
48
|
-
`/package` — the entrypoint/installer, what actually gets published to npm
|
|
49
|
-
`/daemon/host` — wake service
|
|
50
|
-
`/daemon/server` — workbench harness
|
|
51
|
-
`/daemon/voice` - vtt service (rust)
|
|
52
|
-
`/instructions` — instructions for agents, copied into the user's `~/.workbench` folder whenever read
|
|
53
|
-
`/app/server` — server for frontend app
|
|
54
|
-
`/app/client` — frontend SPA
|
|
55
|
-
`/app/tray` — system tray application for the frontend app (rust/tauri)
|
|
56
|
-
`/shared` — shared stuff between the projects
|
|
57
|
-
`/shared/network/native` — tailscale integration (go)
|
|
58
|
-
`/test` — test runner
|
|
59
|
-
`/scripts` — stuff used by package scripts plus anything i didn't notice an agent left behind :3
|
|
60
|
-
`/docs` — help agents find things and not break them without eating all their tokens
|
|
1
|
+
|
|
2
|
+
# workbench
|
|
3
|
+
|
|
4
|
+
an agentic ide about the human
|
|
5
|
+
|
|
6
|
+
## how 2 use
|
|
7
|
+
```bash
|
|
8
|
+
# install & open
|
|
9
|
+
npx @inthedark/wb
|
|
10
|
+
|
|
11
|
+
# set up daemon on another device
|
|
12
|
+
npx @inthedark/wb connect
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## features
|
|
16
|
+
|
|
17
|
+
key features:
|
|
18
|
+
- fully customisable agent instructions with support for comments & imports
|
|
19
|
+
- strong thread transcript UX for when you need to keep an eye on a thread (command matching + html elements for agents)
|
|
20
|
+
- strong thread management UX for when you're managing LOTS of threads (thread status-sorted sidebar)
|
|
21
|
+
- better shared workspace support than any other agentic ide, because a lot of work is not worktree->pr shaped; agents claim the files they need and other agents don't freak out about unclaimed dirt
|
|
22
|
+
- near-identical agent behaviour across providers (codex, opencode 2, claude code) as if they're the same harness
|
|
23
|
+
- agents can orchestrate subagents working in threads across providers and projects
|
|
24
|
+
- customisable voice to text ("what if voice to text was like working in a rich text editor")
|
|
25
|
+
- git UI for doing your own work at the same time as other agents, allowing easy commits while agents have other files claimed
|
|
26
|
+
- quickly switch between saved profiles for harness/model/effort/window/fast mode/agent identity
|
|
27
|
+
- automatic compaction on turn start past configurable time & token thresholds (reduce cache write costs)
|
|
28
|
+
- agents can search repos with shell tools like they're on disk via vfs
|
|
29
|
+
|
|
30
|
+
setup/meta:
|
|
31
|
+
- built on top of other harnesses (codex, opencode 2, claude code) so you can use subscriptions
|
|
32
|
+
- tailscale integration
|
|
33
|
+
- daemon sleeps when idle, leaving only a light http wake service up
|
|
34
|
+
|
|
35
|
+
unpolished/unfinished/broken stuff (coming soon tm!!!!!):
|
|
36
|
+
- soon, autocomplete/suggestions extension for vscode & similar (highly configurable, able to be FAST)
|
|
37
|
+
- worktree->pr workflow is unsupported (gonna be honest i have been in a cave working solo on wb for so long that i have not needed this yet)
|
|
38
|
+
- browse AX & UX
|
|
39
|
+
- search UX
|
|
40
|
+
- i'd like more git ui
|
|
41
|
+
- many places around the app could use a UI polish pass
|
|
42
|
+
- the codebase itself could use a polish pass, esp. the react & tailwind
|
|
43
|
+
|
|
44
|
+
## tech
|
|
45
|
+
|
|
46
|
+
note: have tried to keep dependencies minimal for the most part
|
|
47
|
+
|
|
48
|
+
`/package` — the entrypoint/installer, what actually gets published to npm
|
|
49
|
+
`/daemon/host` — wake service
|
|
50
|
+
`/daemon/server` — workbench harness
|
|
51
|
+
`/daemon/voice` - vtt service (rust)
|
|
52
|
+
`/instructions` — instructions for agents, copied into the user's `~/.workbench` folder whenever read
|
|
53
|
+
`/app/server` — server for frontend app
|
|
54
|
+
`/app/client` — frontend SPA
|
|
55
|
+
`/app/tray` — system tray application for the frontend app (rust/tauri)
|
|
56
|
+
`/shared` — shared stuff between the projects
|
|
57
|
+
`/shared/network/native` — tailscale integration (go)
|
|
58
|
+
`/test` — test runner
|
|
59
|
+
`/scripts` — stuff used by package scripts plus anything i didn't notice an agent left behind :3
|
|
60
|
+
`/docs` — help agents find things and not break them without eating all their tokens
|
package/WorkbenchBootstrap.mjs
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Exports:
|
|
3
|
-
* - WorkbenchBootstrap (default): locates or installs an editable checkout and delegates to it.
|
|
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 { randomUUID } from "node:crypto";
|
|
10
|
-
import { fileURLToPath } from "node:url";
|
|
11
|
-
import { DatabaseSync } from "node:sqlite";
|
|
1
|
+
/*
|
|
2
|
+
* Exports:
|
|
3
|
+
* - WorkbenchBootstrap (default): locates or installs an editable checkout and delegates to it.
|
|
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 { randomUUID } from "node:crypto";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { DatabaseSync } from "node:sqlite";
|
|
12
12
|
import WorkbenchBootstrapPrompt from "./WorkbenchBootstrapPrompt.mjs";
|
|
13
13
|
import WorkbenchBootstrapCommand from "./WorkbenchBootstrapCommand.mjs";
|
|
14
|
-
|
|
15
|
-
export default class WorkbenchBootstrap {
|
|
16
|
-
constructor({
|
|
17
|
-
home = os.homedir(),
|
|
18
|
-
packageRoot = path.dirname(fileURLToPath(import.meta.url)),
|
|
19
|
-
environment = process.env,
|
|
14
|
+
|
|
15
|
+
export default class WorkbenchBootstrap {
|
|
16
|
+
constructor({
|
|
17
|
+
home = os.homedir(),
|
|
18
|
+
packageRoot = path.dirname(fileURLToPath(import.meta.url)),
|
|
19
|
+
environment = process.env,
|
|
20
20
|
prompt = new WorkbenchBootstrapPrompt(),
|
|
21
21
|
commands = new WorkbenchBootstrapCommand(),
|
|
22
|
-
write = text => process.stdout.write(text),
|
|
23
|
-
} = {}) {
|
|
24
|
-
this.home = home;
|
|
25
|
-
this.packageRoot = packageRoot;
|
|
26
|
-
this.environment = environment;
|
|
27
|
-
this.prompt = prompt;
|
|
28
|
-
this.commands = commands;
|
|
29
|
-
this.write = write;
|
|
30
|
-
this.registry = path.join(home, ".workbench", "installation.json");
|
|
31
|
-
}
|
|
32
|
-
|
|
22
|
+
write = text => process.stdout.write(text),
|
|
23
|
+
} = {}) {
|
|
24
|
+
this.home = home;
|
|
25
|
+
this.packageRoot = packageRoot;
|
|
26
|
+
this.environment = environment;
|
|
27
|
+
this.prompt = prompt;
|
|
28
|
+
this.commands = commands;
|
|
29
|
+
this.write = write;
|
|
30
|
+
this.registry = path.join(home, ".workbench", "installation.json");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
33
|
async checkoutExists(root) {
|
|
34
34
|
try {
|
|
35
35
|
const manifest = JSON.parse(await fs.readFile(path.join(root, "package.json"), "utf8"));
|
|
36
36
|
if (manifest.name !== "workbench-root") return false;
|
|
37
|
-
await fs.access(path.join(root, "wb"));
|
|
37
|
+
await fs.access(path.join(root, "wb"));
|
|
38
38
|
await fs.access(path.join(root, "installation", "install.mjs"));
|
|
39
39
|
await fs.access(path.join(root, "cli", "dispatch.mjs"));
|
|
40
|
-
return true;
|
|
41
|
-
} catch (error) {
|
|
42
|
-
if (error.code === "ENOENT") return false;
|
|
40
|
+
return true;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error.code === "ENOENT") return false;
|
|
43
43
|
throw error;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
@@ -58,84 +58,84 @@ export default class WorkbenchBootstrap {
|
|
|
58
58
|
async readInstallation() {
|
|
59
59
|
try {
|
|
60
60
|
const record = JSON.parse(await fs.readFile(this.registry, "utf8"));
|
|
61
|
-
if (record.version !== 1 || typeof record.root !== "string" || !path.isAbsolute(record.root)
|
|
62
|
-
|| !["cloning", "setup", "ready"].includes(record.phase)) {
|
|
63
|
-
throw new Error(`Invalid Workbench installation record: ${this.registry}. Existing files were not changed.`);
|
|
64
|
-
}
|
|
65
|
-
return record;
|
|
66
|
-
} catch (error) {
|
|
67
|
-
if (error.code === "ENOENT") return null;
|
|
68
|
-
throw error;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async writeInstallation(record) {
|
|
73
|
-
const candidate = `${this.registry}.${randomUUID()}.tmp`;
|
|
74
|
-
await fs.writeFile(candidate, `${JSON.stringify(record, null, 2)}\n`, { flag: "wx", mode: 0o600 });
|
|
75
|
-
await fs.rename(candidate, this.registry);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async requireTool(command, cwd) {
|
|
79
|
-
try {
|
|
80
|
-
await this.commands.run(command, ["--version"], {
|
|
81
|
-
cwd,
|
|
82
|
-
output: { write() {} },
|
|
83
|
-
errorOutput: { write() {} },
|
|
84
|
-
});
|
|
85
|
-
} catch (error) {
|
|
86
|
-
if (error.code !== "ENOENT") throw error;
|
|
87
|
-
throw new Error(`${command} is required to install Workbench. Install it and make it available on PATH, then retry.`, { cause: error });
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
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
|
-
async run(args) {
|
|
133
|
-
const managed = this.environment.WORKBENCH_THREAD_ID || this.environment.CODEX_THREAD_ID;
|
|
134
|
-
const view = args[0] === "view" && (args.length === 1 || args.length === 2 && ["daemon", "app"].includes(args[1]));
|
|
135
|
-
const humanCommand = view || args.length === 0 || (args.length === 1 && ["connect", "disconnect", "shortcut"].includes(args[0]));
|
|
136
|
-
if (managed && humanCommand) throw new Error("Managed threads cannot install or launch Workbench services.");
|
|
137
|
-
const linked = path.resolve(this.packageRoot, "..");
|
|
138
|
-
if (await this.checkoutExists(linked)) {
|
|
61
|
+
if (record.version !== 1 || typeof record.root !== "string" || !path.isAbsolute(record.root)
|
|
62
|
+
|| !["cloning", "setup", "ready"].includes(record.phase)) {
|
|
63
|
+
throw new Error(`Invalid Workbench installation record: ${this.registry}. Existing files were not changed.`);
|
|
64
|
+
}
|
|
65
|
+
return record;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error.code === "ENOENT") return null;
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async writeInstallation(record) {
|
|
73
|
+
const candidate = `${this.registry}.${randomUUID()}.tmp`;
|
|
74
|
+
await fs.writeFile(candidate, `${JSON.stringify(record, null, 2)}\n`, { flag: "wx", mode: 0o600 });
|
|
75
|
+
await fs.rename(candidate, this.registry);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async requireTool(command, cwd) {
|
|
79
|
+
try {
|
|
80
|
+
await this.commands.run(command, ["--version"], {
|
|
81
|
+
cwd,
|
|
82
|
+
output: { write() {} },
|
|
83
|
+
errorOutput: { write() {} },
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error.code !== "ENOENT") throw error;
|
|
87
|
+
throw new Error(`${command} is required to install Workbench. Install it and make it available on PATH, then retry.`, { cause: error });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
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
|
+
async run(args) {
|
|
133
|
+
const managed = this.environment.WORKBENCH_THREAD_ID || this.environment.CODEX_THREAD_ID;
|
|
134
|
+
const view = args[0] === "view" && (args.length === 1 || args.length === 2 && ["daemon", "app"].includes(args[1]));
|
|
135
|
+
const humanCommand = view || args.length === 0 || (args.length === 1 && ["connect", "disconnect", "shortcut"].includes(args[0]));
|
|
136
|
+
if (managed && humanCommand) throw new Error("Managed threads cannot install or launch Workbench services.");
|
|
137
|
+
const linked = path.resolve(this.packageRoot, "..");
|
|
138
|
+
if (await this.checkoutExists(linked)) {
|
|
139
139
|
return await this.delegate(linked, args);
|
|
140
140
|
}
|
|
141
141
|
const installed = await this.readInstallation();
|
|
@@ -148,7 +148,7 @@ export default class WorkbenchBootstrap {
|
|
|
148
148
|
}
|
|
149
149
|
if (managed || !(args.length === 0 || (args.length === 1 && args[0] === "connect"))) {
|
|
150
150
|
throw new Error("Workbench is not installed. Run wb or wb connect in an interactive terminal first.");
|
|
151
|
-
}
|
|
151
|
+
}
|
|
152
152
|
await this.requireTool("git");
|
|
153
153
|
await this.requireTool("bash");
|
|
154
154
|
await this.requireTool("vp");
|
|
@@ -158,24 +158,24 @@ export default class WorkbenchBootstrap {
|
|
|
158
158
|
const choice = await this.prompt.choose(
|
|
159
159
|
`Workbench is not installed. This command will clone the repository and run build commands. It may take 1-2 minutes. Continue?${warning}`,
|
|
160
160
|
["Let's go!", "Cancel"],
|
|
161
|
-
);
|
|
162
|
-
if (choice !== "Let's go!") throw new DOMException("Setup cancelled.", "AbortError");
|
|
163
|
-
const defaultRoot = installed?.root ?? (process.platform === "win32"
|
|
164
|
-
? path.join(this.environment.LOCALAPPDATA || path.join(this.home, "AppData", "Local"), "Programs", "inthedark", "wb")
|
|
165
|
-
: path.join(this.home, ".local", "lib", "inthedark", "wb"));
|
|
166
|
-
const root = path.resolve(await this.prompt.location("Install location", defaultRoot));
|
|
167
|
-
await fs.mkdir(path.dirname(this.registry), { recursive: true });
|
|
168
|
-
const lockPath = `${this.registry}.lock.sqlite3`;
|
|
169
|
-
const lock = new DatabaseSync(lockPath);
|
|
170
|
-
try {
|
|
171
|
-
lock.exec("PRAGMA busy_timeout = 0; BEGIN EXCLUSIVE");
|
|
172
|
-
} catch (error) {
|
|
173
|
-
lock.close();
|
|
174
|
-
if (error.errcode === 5 || error.errcode === 6) {
|
|
175
|
-
throw new Error("Another Workbench installation is running. Let it finish before trying again.");
|
|
176
|
-
}
|
|
177
|
-
throw error;
|
|
178
|
-
}
|
|
161
|
+
);
|
|
162
|
+
if (choice !== "Let's go!") throw new DOMException("Setup cancelled.", "AbortError");
|
|
163
|
+
const defaultRoot = installed?.root ?? (process.platform === "win32"
|
|
164
|
+
? path.join(this.environment.LOCALAPPDATA || path.join(this.home, "AppData", "Local"), "Programs", "inthedark", "wb")
|
|
165
|
+
: path.join(this.home, ".local", "lib", "inthedark", "wb"));
|
|
166
|
+
const root = path.resolve(await this.prompt.location("Install location", defaultRoot));
|
|
167
|
+
await fs.mkdir(path.dirname(this.registry), { recursive: true });
|
|
168
|
+
const lockPath = `${this.registry}.lock.sqlite3`;
|
|
169
|
+
const lock = new DatabaseSync(lockPath);
|
|
170
|
+
try {
|
|
171
|
+
lock.exec("PRAGMA busy_timeout = 0; BEGIN EXCLUSIVE");
|
|
172
|
+
} catch (error) {
|
|
173
|
+
lock.close();
|
|
174
|
+
if (error.errcode === 5 || error.errcode === 6) {
|
|
175
|
+
throw new Error("Another Workbench installation is running. Let it finish before trying again.");
|
|
176
|
+
}
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
179
|
try {
|
|
180
180
|
// Another invocation may have finished while this one was accepting input.
|
|
181
181
|
const latest = await this.readInstallation();
|
|
@@ -192,49 +192,49 @@ export default class WorkbenchBootstrap {
|
|
|
192
192
|
if (latest && latest.phase !== "ready") await this.discardIncomplete(latest);
|
|
193
193
|
await this.requireEmptyDestination(root);
|
|
194
194
|
let record = { version: 1, root, phase: "cloning" };
|
|
195
|
-
await this.writeInstallation(record);
|
|
196
|
-
await this.prompt.withHeader(async () => {
|
|
197
|
-
this.write(`Installing Workbench at ${root}\n`);
|
|
198
|
-
try {
|
|
199
|
-
// Install sandboxes substitute a local snapshot; ordinary installs clone the public repository.
|
|
200
|
-
const repository = this.environment.WORKBENCH_INSTALL_REPOSITORY || "https://github.com/ErisInTheDark/workbench.git";
|
|
201
|
-
await this.commands.run("git", ["clone", "--progress", "--branch", "main", "--single-branch", repository, root]);
|
|
202
|
-
if (!await this.checkoutExists(root)) throw new Error("Clone did not produce a Workbench checkout.");
|
|
203
|
-
record = { ...record, phase: "setup" };
|
|
204
|
-
await this.writeInstallation(record);
|
|
195
|
+
await this.writeInstallation(record);
|
|
196
|
+
await this.prompt.withHeader(async () => {
|
|
197
|
+
this.write(`Installing Workbench at ${root}\n`);
|
|
198
|
+
try {
|
|
199
|
+
// Install sandboxes substitute a local snapshot; ordinary installs clone the public repository.
|
|
200
|
+
const repository = this.environment.WORKBENCH_INSTALL_REPOSITORY || "https://github.com/ErisInTheDark/workbench.git";
|
|
201
|
+
await this.commands.run("git", ["clone", "--progress", "--branch", "main", "--single-branch", repository, root]);
|
|
202
|
+
if (!await this.checkoutExists(root)) throw new Error("Clone did not produce a Workbench checkout.");
|
|
203
|
+
record = { ...record, phase: "setup" };
|
|
204
|
+
await this.writeInstallation(record);
|
|
205
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;
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
record = { ...record, phase: "ready" };
|
|
215
|
-
await this.writeInstallation(record);
|
|
216
|
-
} finally {
|
|
217
|
-
// Closing the connection also releases the OS lock after normal completion;
|
|
218
|
-
// process death releases it without a stale PID or lock-file recovery path.
|
|
219
|
-
lock.close();
|
|
220
|
-
}
|
|
221
|
-
// Persist readiness before optional platform actions: missing native artifacts
|
|
222
|
-
// must not turn a usable source installation into an endless setup loop.
|
|
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;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
record = { ...record, phase: "ready" };
|
|
215
|
+
await this.writeInstallation(record);
|
|
216
|
+
} finally {
|
|
217
|
+
// Closing the connection also releases the OS lock after normal completion;
|
|
218
|
+
// process death releases it without a stale PID or lock-file recovery path.
|
|
219
|
+
lock.close();
|
|
220
|
+
}
|
|
221
|
+
// Persist readiness before optional platform actions: missing native artifacts
|
|
222
|
+
// must not turn a usable source installation into an endless setup loop.
|
|
223
223
|
await this.commands.run(process.execPath, [path.join(root, "installation", "install.mjs"),
|
|
224
224
|
args[0] === "connect" ? "--connect" : "--welcome"], { cwd: root, interactive: true });
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
async delegate(root, args) {
|
|
228
228
|
return await this.commands.run(process.execPath, [path.join(root, "cli", "dispatch.mjs"), ...args], { interactive: true });
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async requireEmptyDestination(root) {
|
|
232
|
-
try {
|
|
233
|
-
if ((await fs.readdir(root)).length) throw new Error(`Installation destination must be empty: ${root}. Existing files were preserved.`);
|
|
234
|
-
} catch (error) {
|
|
235
|
-
if (error.code !== "ENOENT") throw error;
|
|
236
|
-
}
|
|
237
|
-
await fs.mkdir(path.dirname(root), { recursive: true });
|
|
238
|
-
await fs.access(path.dirname(root), constants.W_OK);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async requireEmptyDestination(root) {
|
|
232
|
+
try {
|
|
233
|
+
if ((await fs.readdir(root)).length) throw new Error(`Installation destination must be empty: ${root}. Existing files were preserved.`);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (error.code !== "ENOENT") throw error;
|
|
236
|
+
}
|
|
237
|
+
await fs.mkdir(path.dirname(root), { recursive: true });
|
|
238
|
+
await fs.access(path.dirname(root), constants.W_OK);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -1,140 +1,140 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Exports:
|
|
1
|
+
/*
|
|
2
|
+
* Exports:
|
|
3
3
|
* - WorkbenchBootstrapCommand (default): owns bootstrap child commands, Windows executable lookup (including Git Bash), and failures.
|
|
4
|
-
*/
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
import fs from "node:fs/promises";
|
|
7
|
-
import path from "node:path";
|
|
8
|
-
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
9
|
export default class WorkbenchBootstrapCommand {
|
|
10
|
-
constructor({ output = process.stdout, errorOutput = process.stderr, environment = process.env } = {}) {
|
|
11
|
-
this.output = output;
|
|
12
|
-
this.errorOutput = errorOutput;
|
|
13
|
-
this.environment = environment;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Environment variable by name; Windows names are case-insensitive, but copied env objects are not. */
|
|
17
|
-
variable(name) {
|
|
18
|
-
if (process.platform !== "win32") return this.environment[name];
|
|
19
|
-
const key = Object.keys(this.environment).find(candidate => candidate.toLowerCase() === name.toLowerCase());
|
|
20
|
-
return key === undefined ? undefined : this.environment[key];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async resolve(command) {
|
|
24
|
-
if (process.platform === "win32" && command === "bash") return [await this.resolveBash()];
|
|
25
|
-
if (process.platform !== "win32" || !["npm", "pnpm"].includes(command)) return [command];
|
|
26
|
-
const directories = [path.dirname(process.execPath), ...(this.variable("PATH") || "").split(path.delimiter)];
|
|
27
|
-
for (const directory of directories) {
|
|
28
|
-
if (!directory) continue;
|
|
29
|
-
const executable = path.join(directory, `${command}.exe`);
|
|
30
|
-
try {
|
|
31
|
-
await fs.access(executable);
|
|
32
|
-
return [executable];
|
|
33
|
-
} catch (error) {
|
|
34
|
-
if (error.code !== "ENOENT") throw error;
|
|
35
|
-
}
|
|
36
|
-
// npm/pnpm's Windows cmd wrappers cannot safely forward arbitrary paths.
|
|
37
|
-
// Invoke their installed Node entry instead of adding a command-shell layer.
|
|
38
|
-
const entry = command === "npm" ? "npm/bin/npm-cli.js" : "pnpm/bin/pnpm.cjs";
|
|
39
|
-
const script = path.join(directory, "node_modules", entry);
|
|
40
|
-
try {
|
|
41
|
-
await fs.access(script);
|
|
42
|
-
return [process.execPath, script];
|
|
43
|
-
} catch (error) {
|
|
44
|
-
if (error.code !== "ENOENT") throw error;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
throw new Error(`Cannot locate ${command}'s executable or Node entry. Install ${command} and ensure it is on PATH.`);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Windows bash that can run Workbench. Git for Windows usually puts only git on PATH, and the
|
|
52
|
-
* bash.exe Windows does find is often WSL's launcher, which runs scripts inside Linux.
|
|
53
|
-
*/
|
|
54
|
-
async resolveBash() {
|
|
55
|
-
const exists = async file => {
|
|
56
|
-
try { await fs.access(file); return true; }
|
|
57
|
-
catch (error) { if (error.code === "ENOENT") return false; throw error; }
|
|
58
|
-
};
|
|
59
|
-
const directories = (this.variable("PATH") || "").split(path.delimiter).filter(Boolean);
|
|
60
|
-
const local = this.variable("LOCALAPPDATA");
|
|
61
|
-
const launchers = [
|
|
62
|
-
path.join(this.variable("SystemRoot") || "C:\\Windows", "System32"),
|
|
63
|
-
...(local ? [path.join(local, "Microsoft", "WindowsApps")] : []),
|
|
64
|
-
].map(directory => path.resolve(directory).toLowerCase());
|
|
65
|
-
let sawLauncher = false;
|
|
66
|
-
for (const directory of directories) {
|
|
67
|
-
const bash = path.join(directory, "bash.exe");
|
|
68
|
-
if (!await exists(bash)) continue;
|
|
69
|
-
if (launchers.includes(path.resolve(directory).toLowerCase())) sawLauncher = true;
|
|
70
|
-
else return bash;
|
|
71
|
-
}
|
|
72
|
-
// git.exe lives in Git\cmd, Git\bin or Git\mingw64\bin; Git Bash is always Git\bin\bash.exe.
|
|
73
|
-
for (const directory of directories) {
|
|
74
|
-
if (!await exists(path.join(directory, "git.exe"))) continue;
|
|
75
|
-
const parent = path.dirname(directory);
|
|
76
|
-
const root = path.basename(parent).toLowerCase() === "mingw64" ? path.dirname(parent) : parent;
|
|
77
|
-
const bash = path.join(root, "bin", "bash.exe");
|
|
78
|
-
if (await exists(bash)) return bash;
|
|
79
|
-
}
|
|
80
|
-
for (const base of [this.variable("ProgramFiles"), local && path.join(local, "Programs")]) {
|
|
81
|
-
if (!base) continue;
|
|
82
|
-
const bash = path.join(base, "Git", "bin", "bash.exe");
|
|
83
|
-
if (await exists(bash)) return bash;
|
|
84
|
-
}
|
|
85
|
-
throw new Error(`Workbench needs Git Bash on Windows.${sawLauncher ? " The bash on PATH is WSL's, which cannot run Workbench." : ""} Install Git for Windows, then retry.`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async run(command, args, { cwd, signal, onOutput, environment = {}, output = this.output, errorOutput = this.errorOutput, interactive = false } = {}) {
|
|
89
|
-
signal?.throwIfAborted();
|
|
90
|
-
const [executable, ...prefix] = await this.resolve(command);
|
|
91
|
-
signal?.throwIfAborted();
|
|
92
|
-
return await new Promise((resolve, reject) => {
|
|
93
|
-
const child = spawn(executable, [...prefix, ...args], {
|
|
94
|
-
cwd,
|
|
95
|
-
env: { ...this.environment, ...environment },
|
|
96
|
-
windowsHide: true,
|
|
97
|
-
stdio: interactive ? "inherit" : ["ignore", "pipe", "pipe"],
|
|
98
|
-
signal,
|
|
99
|
-
});
|
|
100
|
-
let failure;
|
|
101
|
-
const relay = signal => { child.kill(signal); };
|
|
102
|
-
const interrupt = () => relay("SIGINT");
|
|
103
|
-
const terminate = () => relay("SIGTERM");
|
|
104
|
-
const hangup = () => relay("SIGHUP");
|
|
105
|
-
if (interactive) {
|
|
106
|
-
process.on("SIGINT", interrupt);
|
|
107
|
-
process.on("SIGTERM", terminate);
|
|
108
|
-
process.on("SIGHUP", hangup);
|
|
109
|
-
}
|
|
110
|
-
child.on("error", error => { failure = error; });
|
|
111
|
-
const forward = (stream, output) => {
|
|
112
|
-
stream.on("data", bytes => {
|
|
113
|
-
output.write(bytes);
|
|
114
|
-
if (onOutput) {
|
|
115
|
-
try { onOutput(bytes.toString()); }
|
|
116
|
-
catch (error) { failure = error; child.kill(); }
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
};
|
|
120
|
-
if (!interactive) {
|
|
121
|
-
forward(child.stdout, output);
|
|
122
|
-
forward(child.stderr, errorOutput);
|
|
123
|
-
}
|
|
124
|
-
child.once("close", (code, exitSignal) => {
|
|
125
|
-
if (interactive) {
|
|
126
|
-
process.off("SIGINT", interrupt);
|
|
127
|
-
process.off("SIGTERM", terminate);
|
|
128
|
-
process.off("SIGHUP", hangup);
|
|
129
|
-
}
|
|
130
|
-
if (failure?.code === "ENOENT") {
|
|
131
|
-
reject(Object.assign(new Error(`Cannot start ${command}: executable not found. Install it and make it available on PATH, then retry.`, { cause: failure }), {
|
|
132
|
-
code: "ENOENT",
|
|
133
|
-
}));
|
|
134
|
-
} else if (failure) reject(failure);
|
|
135
|
-
else if (code === 0) resolve();
|
|
136
|
-
else reject(Object.assign(new Error(`${command} failed with ${exitSignal ? `signal ${exitSignal}` : `status ${code ?? "unknown"}`}.`), { exitCode: code ?? 1 }));
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
10
|
+
constructor({ output = process.stdout, errorOutput = process.stderr, environment = process.env } = {}) {
|
|
11
|
+
this.output = output;
|
|
12
|
+
this.errorOutput = errorOutput;
|
|
13
|
+
this.environment = environment;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Environment variable by name; Windows names are case-insensitive, but copied env objects are not. */
|
|
17
|
+
variable(name) {
|
|
18
|
+
if (process.platform !== "win32") return this.environment[name];
|
|
19
|
+
const key = Object.keys(this.environment).find(candidate => candidate.toLowerCase() === name.toLowerCase());
|
|
20
|
+
return key === undefined ? undefined : this.environment[key];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async resolve(command) {
|
|
24
|
+
if (process.platform === "win32" && command === "bash") return [await this.resolveBash()];
|
|
25
|
+
if (process.platform !== "win32" || !["npm", "pnpm"].includes(command)) return [command];
|
|
26
|
+
const directories = [path.dirname(process.execPath), ...(this.variable("PATH") || "").split(path.delimiter)];
|
|
27
|
+
for (const directory of directories) {
|
|
28
|
+
if (!directory) continue;
|
|
29
|
+
const executable = path.join(directory, `${command}.exe`);
|
|
30
|
+
try {
|
|
31
|
+
await fs.access(executable);
|
|
32
|
+
return [executable];
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error.code !== "ENOENT") throw error;
|
|
35
|
+
}
|
|
36
|
+
// npm/pnpm's Windows cmd wrappers cannot safely forward arbitrary paths.
|
|
37
|
+
// Invoke their installed Node entry instead of adding a command-shell layer.
|
|
38
|
+
const entry = command === "npm" ? "npm/bin/npm-cli.js" : "pnpm/bin/pnpm.cjs";
|
|
39
|
+
const script = path.join(directory, "node_modules", entry);
|
|
40
|
+
try {
|
|
41
|
+
await fs.access(script);
|
|
42
|
+
return [process.execPath, script];
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error.code !== "ENOENT") throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`Cannot locate ${command}'s executable or Node entry. Install ${command} and ensure it is on PATH.`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Windows bash that can run Workbench. Git for Windows usually puts only git on PATH, and the
|
|
52
|
+
* bash.exe Windows does find is often WSL's launcher, which runs scripts inside Linux.
|
|
53
|
+
*/
|
|
54
|
+
async resolveBash() {
|
|
55
|
+
const exists = async file => {
|
|
56
|
+
try { await fs.access(file); return true; }
|
|
57
|
+
catch (error) { if (error.code === "ENOENT") return false; throw error; }
|
|
58
|
+
};
|
|
59
|
+
const directories = (this.variable("PATH") || "").split(path.delimiter).filter(Boolean);
|
|
60
|
+
const local = this.variable("LOCALAPPDATA");
|
|
61
|
+
const launchers = [
|
|
62
|
+
path.join(this.variable("SystemRoot") || "C:\\Windows", "System32"),
|
|
63
|
+
...(local ? [path.join(local, "Microsoft", "WindowsApps")] : []),
|
|
64
|
+
].map(directory => path.resolve(directory).toLowerCase());
|
|
65
|
+
let sawLauncher = false;
|
|
66
|
+
for (const directory of directories) {
|
|
67
|
+
const bash = path.join(directory, "bash.exe");
|
|
68
|
+
if (!await exists(bash)) continue;
|
|
69
|
+
if (launchers.includes(path.resolve(directory).toLowerCase())) sawLauncher = true;
|
|
70
|
+
else return bash;
|
|
71
|
+
}
|
|
72
|
+
// git.exe lives in Git\cmd, Git\bin or Git\mingw64\bin; Git Bash is always Git\bin\bash.exe.
|
|
73
|
+
for (const directory of directories) {
|
|
74
|
+
if (!await exists(path.join(directory, "git.exe"))) continue;
|
|
75
|
+
const parent = path.dirname(directory);
|
|
76
|
+
const root = path.basename(parent).toLowerCase() === "mingw64" ? path.dirname(parent) : parent;
|
|
77
|
+
const bash = path.join(root, "bin", "bash.exe");
|
|
78
|
+
if (await exists(bash)) return bash;
|
|
79
|
+
}
|
|
80
|
+
for (const base of [this.variable("ProgramFiles"), local && path.join(local, "Programs")]) {
|
|
81
|
+
if (!base) continue;
|
|
82
|
+
const bash = path.join(base, "Git", "bin", "bash.exe");
|
|
83
|
+
if (await exists(bash)) return bash;
|
|
84
|
+
}
|
|
85
|
+
throw new Error(`Workbench needs Git Bash on Windows.${sawLauncher ? " The bash on PATH is WSL's, which cannot run Workbench." : ""} Install Git for Windows, then retry.`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async run(command, args, { cwd, signal, onOutput, environment = {}, output = this.output, errorOutput = this.errorOutput, interactive = false } = {}) {
|
|
89
|
+
signal?.throwIfAborted();
|
|
90
|
+
const [executable, ...prefix] = await this.resolve(command);
|
|
91
|
+
signal?.throwIfAborted();
|
|
92
|
+
return await new Promise((resolve, reject) => {
|
|
93
|
+
const child = spawn(executable, [...prefix, ...args], {
|
|
94
|
+
cwd,
|
|
95
|
+
env: { ...this.environment, ...environment },
|
|
96
|
+
windowsHide: true,
|
|
97
|
+
stdio: interactive ? "inherit" : ["ignore", "pipe", "pipe"],
|
|
98
|
+
signal,
|
|
99
|
+
});
|
|
100
|
+
let failure;
|
|
101
|
+
const relay = signal => { child.kill(signal); };
|
|
102
|
+
const interrupt = () => relay("SIGINT");
|
|
103
|
+
const terminate = () => relay("SIGTERM");
|
|
104
|
+
const hangup = () => relay("SIGHUP");
|
|
105
|
+
if (interactive) {
|
|
106
|
+
process.on("SIGINT", interrupt);
|
|
107
|
+
process.on("SIGTERM", terminate);
|
|
108
|
+
process.on("SIGHUP", hangup);
|
|
109
|
+
}
|
|
110
|
+
child.on("error", error => { failure = error; });
|
|
111
|
+
const forward = (stream, output) => {
|
|
112
|
+
stream.on("data", bytes => {
|
|
113
|
+
output.write(bytes);
|
|
114
|
+
if (onOutput) {
|
|
115
|
+
try { onOutput(bytes.toString()); }
|
|
116
|
+
catch (error) { failure = error; child.kill(); }
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
if (!interactive) {
|
|
121
|
+
forward(child.stdout, output);
|
|
122
|
+
forward(child.stderr, errorOutput);
|
|
123
|
+
}
|
|
124
|
+
child.once("close", (code, exitSignal) => {
|
|
125
|
+
if (interactive) {
|
|
126
|
+
process.off("SIGINT", interrupt);
|
|
127
|
+
process.off("SIGTERM", terminate);
|
|
128
|
+
process.off("SIGHUP", hangup);
|
|
129
|
+
}
|
|
130
|
+
if (failure?.code === "ENOENT") {
|
|
131
|
+
reject(Object.assign(new Error(`Cannot start ${command}: executable not found. Install it and make it available on PATH, then retry.`, { cause: failure }), {
|
|
132
|
+
code: "ENOENT",
|
|
133
|
+
}));
|
|
134
|
+
} else if (failure) reject(failure);
|
|
135
|
+
else if (code === 0) resolve();
|
|
136
|
+
else reject(Object.assign(new Error(`${command} failed with ${exitSignal ? `signal ${exitSignal}` : `status ${code ?? "unknown"}`}.`), { exitCode: code ?? 1 }));
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inthedark/wb",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"index.d.ts",
|
|
32
32
|
"store-definitions.mjs",
|
|
33
33
|
"StoreCommandRunner.mjs",
|
|
34
|
+
"wb.schema.json",
|
|
34
35
|
"README.md"
|
|
35
36
|
],
|
|
36
37
|
"dependencies": {
|
package/wb.schema.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"$schema": {
|
|
6
|
+
"description": "Editor schema reference; ignored by Workbench.",
|
|
7
|
+
"type": "string"
|
|
8
|
+
},
|
|
9
|
+
"vis": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"css": {
|
|
13
|
+
"description": "Compiles CSS for .html and .svg vis files containing <link rel=\"workbench-css\">.",
|
|
14
|
+
"type": "object",
|
|
15
|
+
"properties": {
|
|
16
|
+
"command": {
|
|
17
|
+
"minItems": 1,
|
|
18
|
+
"maxItems": 64,
|
|
19
|
+
"type": "array",
|
|
20
|
+
"items": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"minLength": 1,
|
|
23
|
+
"maxLength": 4000
|
|
24
|
+
},
|
|
25
|
+
"description": "Argument vector run from the project root, inside the calling thread's sandbox. Its stdout is the result. {file}, {root} and {input} are substituted."
|
|
26
|
+
},
|
|
27
|
+
"input": {
|
|
28
|
+
"description": "Written to a temporary file named by {input}, for tools that read an entry file rather than arguments.",
|
|
29
|
+
"type": "string",
|
|
30
|
+
"maxLength": 16000
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"required": [
|
|
34
|
+
"command"
|
|
35
|
+
],
|
|
36
|
+
"additionalProperties": false
|
|
37
|
+
},
|
|
38
|
+
"build": {
|
|
39
|
+
"description": "Builds .tsx and .jsx vis files; prints JSON { document, inputs }.",
|
|
40
|
+
"type": "object",
|
|
41
|
+
"properties": {
|
|
42
|
+
"command": {
|
|
43
|
+
"minItems": 1,
|
|
44
|
+
"maxItems": 64,
|
|
45
|
+
"type": "array",
|
|
46
|
+
"items": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"minLength": 1,
|
|
49
|
+
"maxLength": 4000
|
|
50
|
+
},
|
|
51
|
+
"description": "Argument vector run from the project root, inside the calling thread's sandbox. Its stdout is the result. {file}, {root} and {input} are substituted."
|
|
52
|
+
},
|
|
53
|
+
"input": {
|
|
54
|
+
"description": "Written to a temporary file named by {input}, for tools that read an entry file rather than arguments.",
|
|
55
|
+
"type": "string",
|
|
56
|
+
"maxLength": 16000
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"required": [
|
|
60
|
+
"command"
|
|
61
|
+
],
|
|
62
|
+
"additionalProperties": false
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"additionalProperties": false,
|
|
66
|
+
"description": "Live visual drafts agents show with wb vis start."
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"additionalProperties": false,
|
|
70
|
+
"description": "Project-owned Workbench settings, read from .wb.json at the project root."
|
|
71
|
+
}
|