@stringsync/splates 0.0.53 → 0.0.55

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.
Files changed (63) hide show
  1. package/README.md +7 -24
  2. package/artifact.d.ts +3 -0
  3. package/budget.d.ts +3 -0
  4. package/creds.d.ts +3 -0
  5. package/flags.d.ts +37 -0
  6. package/handoff.d.ts +13 -0
  7. package/hook.d.ts +3 -0
  8. package/index.d.ts +2 -0
  9. package/index.js +1844 -0
  10. package/lane.d.ts +3 -0
  11. package/notes.d.ts +4 -0
  12. package/package.json +12 -14
  13. package/projects.d.ts +3 -0
  14. package/queue.d.ts +3 -0
  15. package/replica.d.ts +3 -0
  16. package/runners.d.ts +3 -0
  17. package/server.d.ts +6 -0
  18. package/setup.d.ts +22 -0
  19. package/slots.d.ts +3 -0
  20. package/splates.d.ts +10 -0
  21. package/task.d.ts +3 -0
  22. package/tunnel.d.ts +3 -0
  23. package/waiting.d.ts +3 -0
  24. package/worker.d.ts +3 -0
  25. package/artifact.ts +0 -120
  26. package/budget.ts +0 -117
  27. package/checkout.ts +0 -8
  28. package/creds.test.ts +0 -336
  29. package/creds.ts +0 -144
  30. package/dev/dev.ts +0 -86
  31. package/dev/eval.ts +0 -21
  32. package/dev/fix.ts +0 -63
  33. package/dev/index.ts +0 -1
  34. package/dev/prove.ts +0 -38
  35. package/dev/root.ts +0 -7
  36. package/dev/ship.ts +0 -46
  37. package/dev/test.ts +0 -154
  38. package/e2e.test.ts +0 -472
  39. package/flags.ts +0 -157
  40. package/hook.ts +0 -328
  41. package/index.ts +0 -18
  42. package/infra.ts +0 -107
  43. package/lane.test.ts +0 -29
  44. package/lane.ts +0 -172
  45. package/notes.ts +0 -19
  46. package/projects.test.ts +0 -191
  47. package/projects.ts +0 -37
  48. package/queue.ts +0 -82
  49. package/screenshots/annotate.png +0 -0
  50. package/screenshots/demo.png +0 -0
  51. package/screenshots/parallel.png +0 -0
  52. package/screenshots/questions.png +0 -0
  53. package/server.ts +0 -287
  54. package/setup.ts +0 -233
  55. package/slots.ts +0 -65
  56. package/splates.test.ts +0 -338
  57. package/splates.ts +0 -567
  58. package/task.ts +0 -354
  59. package/tunnel.ts +0 -106
  60. package/wait-journal.test.ts +0 -96
  61. package/waiting.ts +0 -59
  62. package/worker-relay.test.ts +0 -233
  63. package/worker.ts +0 -300
@@ -1,233 +0,0 @@
1
- import {
2
- afterEach,
3
- beforeEach,
4
- describe,
5
- expect,
6
- it,
7
- setDefaultTimeout,
8
- } from "bun:test";
9
- import { chmod } from "node:fs/promises";
10
- import { basename, dirname, join } from "node:path";
11
- import { type DevServer, dev } from "@stringsync/splates-server";
12
- import { printed } from "@stringsync/splates-server/agents/testing";
13
- import { relayDocument } from "@stringsync/splates-server/records";
14
- import { FsStorage } from "@stringsync/splates-server/storage";
15
- import { Testing } from "@stringsync/splates-server/testing";
16
- import type { RemoteWorker } from "@stringsync/splates-server/workers";
17
- import {
18
- DevWorkers,
19
- UnixWorkerSocket,
20
- WORKER_KEY,
21
- WorkerDaemon,
22
- workerCreds,
23
- } from "@stringsync/splates-server/workers";
24
- import { FakeSecretStore } from "webappwiz/creds/testing";
25
- import { color, MemoryLogger } from "webappwiz/log";
26
- import { NodePs, OpenPortProvider } from "webappwiz/system";
27
- import { Duration } from "webappwiz/time";
28
-
29
- const CLI = join(import.meta.dirname, "index.ts");
30
-
31
- // Real git, a real coordinator and worker, and a fresh CLI process per
32
- // command, as in the e2e tests.
33
- setDefaultTimeout(90_000);
34
-
35
- describe("WorkerRelay", () => {
36
- let env: Testing;
37
- let server: DevServer;
38
- let workers: DevWorkers;
39
- let daemon: WorkerDaemon;
40
- /** The worker, as the coordinator holds it. */
41
- let worker: RemoteWorker;
42
- /** A worker on another machine: a home of its own, and a clone with no `.git/splates`. */
43
- let home: string;
44
- let clone: string;
45
- let bin: string;
46
- let relay: string;
47
-
48
- beforeEach(async () => {
49
- env = await Testing.open();
50
- await env.commit(
51
- env.root,
52
- "splates.config.ts",
53
- `export default { preMerge: "true", remote: "origin" };\n`,
54
- "config",
55
- );
56
- env.config.remote = "origin";
57
- const base = dirname(env.root);
58
- // The central remote, which only the coordinator moves trunk on.
59
- const remote = join(base, "remote.git");
60
- await env.gitCli(base, "init", "--quiet", "--bare", "-b", "main", remote);
61
- await env.gitCli(env.root, "remote", "add", "origin", remote);
62
- await env.gitCli(env.root, "push", "--quiet", "origin", "main");
63
- const socket = join(base, "worker.sock");
64
- const log = new MemoryLogger();
65
- workers = new DevWorkers(log);
66
- server = await dev(env, {
67
- ports: OpenPortProvider.any(),
68
- workers,
69
- workerSocket: socket,
70
- });
71
- home = join(base, "worker", "home");
72
- clone = join(base, "worker", "repo");
73
- await env.gitCli(base, "clone", "--quiet", remote, clone);
74
- await env.gitCli(clone, "config", "user.email", "splates@example.com");
75
- await env.gitCli(clone, "config", "user.name", "splates");
76
- await env.gitCli(clone, "config", "commit.gpgsign", "false");
77
- // `splates add` runs no install of its own here.
78
- bin = join(base, "worker", "bin");
79
- await env.fs.mkdir(bin);
80
- await env.fs.write(join(bin, "bunx"), "#!/bin/sh\nexit 0\n");
81
- await chmod(join(bin, "bunx"), 0o755);
82
- const joined = new Promise<RemoteWorker>((resolve) =>
83
- workers.registry.events.on(
84
- "joined",
85
- (each) => each.id === "remote" && resolve(each),
86
- ),
87
- );
88
- daemon = new WorkerDaemon(
89
- { where: socket, dial: () => UnixWorkerSocket.connect(socket) },
90
- {
91
- id: "remote",
92
- slots: 2,
93
- ps: new NodePs(),
94
- fs: env.fs,
95
- log,
96
- backoff: { min: Duration.ms(20), max: Duration.ms(100) },
97
- relay: relayDocument(new FsStorage(home, env.fs)),
98
- },
99
- );
100
- void daemon.run();
101
- worker = await joined;
102
- relay = JSON.parse(await env.fs.read(`${home}/relay.json`)).url;
103
- });
104
-
105
- afterEach(async () => {
106
- await daemon.disposeAsync();
107
- await server.disposeAsync();
108
- await env.disposeAsync();
109
- });
110
-
111
- /** Runs the CLI in `cwd` on the worker, as an agent there would with `agentEnv`. */
112
- const splates = async (
113
- cwd: string,
114
- args: string[],
115
- agentEnv: Record<string, string> = {
116
- SPLATES_DEV: relay,
117
- SPLATES_PROJECT: basename(env.root),
118
- },
119
- ): Promise<string> => {
120
- const { exitCode, stdout, stderr } = await env.ps.spawnCapture(
121
- ["bun", CLI, ...args],
122
- {
123
- cwd,
124
- env: {
125
- PATH: `${bin}:${process.env.PATH}`,
126
- SPLATES_HOME: home,
127
- ...agentEnv,
128
- },
129
- },
130
- );
131
- if (exitCode !== 0) {
132
- throw new Error(`splates ${args.join(" ")}: ${stderr}`);
133
- }
134
- return color.strip(stdout);
135
- };
136
-
137
- /** What an agent the worker starts finds as `SPLATES_DEV`, told the coordinator's own. */
138
- const agentDev = (): Promise<string> =>
139
- printed(
140
- worker.start(["sh", "-c", 'printf "$SPLATES_DEV"'], clone, {
141
- SPLATES_DEV: "http://127.0.0.1:1",
142
- }),
143
- );
144
-
145
- it("points an agent the worker starts at the relay", async () => {
146
- expect(await agentDev()).toBe(relay);
147
- });
148
-
149
- it("finds the relay and the project by hand, with no env", async () => {
150
- await splates(clone, ["task", "add", "make it so", "--slug", "so"], {});
151
-
152
- expect(await env.tasks.all()).toMatchObject([{ subject: "make it so" }]);
153
- });
154
-
155
- it("reaches the coordinator directly by hand once no worker daemon runs", async () => {
156
- await daemon.disposeAsync();
157
- // As a daemon dialing a coordinator elsewhere leaves it once stopped.
158
- await relayDocument(new FsStorage(home, env.fs)).write({
159
- worker: "remote",
160
- coordinator: `http://127.0.0.1:${server.port}`,
161
- keyFile: true,
162
- });
163
- await env.fs.write(
164
- `${home}/worker.env`,
165
- "SPLATES_WORKER_ACCESS_ID=client.access\nSPLATES_WORKER_ACCESS_SECRET=secret\n",
166
- );
167
-
168
- await splates(clone, ["task", "add", "make it so", "--slug", "so"], {});
169
-
170
- expect(worker.connected).toBe(false);
171
- expect(await env.tasks.all()).toMatchObject([{ subject: "make it so" }]);
172
- });
173
-
174
- // scry-ignore simple-test-setup: minting a token and saving the creds it gets are what this test checks, and no other test here does either.
175
- it("joins with the token `worker add` mints, over the coordinator's own routes", async () => {
176
- const coordinator = `http://127.0.0.1:${server.port}`;
177
- const added = await env.ps.spawnCapture(["bun", CLI, "worker", "add"], {
178
- cwd: env.root,
179
- env: { SPLATES_DEV: coordinator },
180
- });
181
- const token = /splj_\S+/.exec(color.strip(added.stdout + added.stderr));
182
- expect(token).not.toBeNull();
183
-
184
- const store = new FakeSecretStore();
185
- const { key } = await workerCreds("box", {
186
- coordinator,
187
- slots: 1,
188
- id: "box",
189
- join: token?.[0],
190
- log: new MemoryLogger(),
191
- ps: new NodePs(),
192
- fs: env.fs,
193
- store,
194
- });
195
-
196
- expect(await store.get(WORKER_KEY)).toBe(key);
197
- expect(workers.records.verify("box", key)).toBe(true);
198
- });
199
-
200
- it("takes a task from add to merge on the coordinator's records", async () => {
201
- await env.tasks.add("make it so", null);
202
- relay = await agentDev();
203
- await splates(clone, ["lane", "add", "first"]);
204
- await splates(clone, ["add", "alpha", "--goal", "land it", "--task", "1"]);
205
- const tree = (await splates(clone, ["path", "alpha"])).trim();
206
-
207
- expect(await env.fs.exists(join(clone, ".git", "splates"))).toBe(false);
208
- expect((await env.service.find("alpha")).state).toMatchObject({
209
- status: "working",
210
- worktree: tree,
211
- });
212
- expect(await env.tasks.find(1)).toMatchObject({ takenBy: "alpha" });
213
-
214
- await splates(tree, ["claim", "alpha"]);
215
- await env.fs.write(join(clone, "..", "shot.png"), "png");
216
- await splates(tree, ["attach", join(clone, "..", "shot.png")]);
217
-
218
- await env.commit(tree, "work.txt", "done\n", "Do the work");
219
- await env.finish(tree);
220
- await splates(tree, ["merge"]);
221
-
222
- // Landed by the coordinator, on the remote and in its own checkout.
223
- const remote = join(dirname(env.root), "remote.git");
224
- expect(await env.gitCli(remote, "log", "-1", "--format=%s", "main")).toBe(
225
- "Do the work",
226
- );
227
- expect(await env.gitCli(env.root, "log", "-1", "--format=%s")).toBe(
228
- "Do the work",
229
- );
230
- expect((await env.service.find("alpha")).status).toBe("removed");
231
- expect(await env.tasks.all()).toEqual([]);
232
- });
233
- });
package/worker.ts DELETED
@@ -1,300 +0,0 @@
1
- import { splatesHome } from "@stringsync/splates-server/models";
2
- import type { SplatesDeps } from "@stringsync/splates-server/server";
3
- import {
4
- addWorker,
5
- drainWorker,
6
- FileLogger,
7
- listWorkers,
8
- removeWorker,
9
- setWorkerSlots,
10
- WORKER_JOIN,
11
- wakeWorker,
12
- workerJoin,
13
- workerRecipe,
14
- workerRun,
15
- } from "@stringsync/splates-server/workers";
16
- import { cli } from "webappwiz/cmd";
17
- import { CompositeLogger } from "webappwiz/log";
18
- import { z } from "zod";
19
- import { splatesWaiting } from "./waiting";
20
-
21
- /** `splates worker`, which the root cli mounts after `splates slots`. */
22
- export const splatesWorker = cli<SplatesDeps>("worker").description(
23
- "the machines that run tasks for `splates server`: each dials out to it and runs what it is given, agents, terminals and the git and process reads around them, on its own machine",
24
- );
25
-
26
- splatesWorker
27
- .command("run")
28
- .description(
29
- "run a worker on this machine until stopped: it dials `splates server`, by default the one on this machine through a socket in ~/.splates only you may open, and dials again whenever the link drops, while what it runs carries on; it stops reading a process's output while the server is away too long, so nothing is lost. Once its agents have moved off it, a server on another build has it restart on that build, through `bunx @stringsync/splates@<build>`, as it does when it refuses this worker for another protocol; one that says no build has it exit `outdated`",
30
- )
31
- .option("server", z.string(), {
32
- default: "",
33
- description:
34
- "the `splates server` to dial: its URL, whose /api/workers it opens, or unix:<path>; by default this machine's",
35
- })
36
- .option("coordinator", z.string(), {
37
- default: "",
38
- description:
39
- "the old name for --server, still read for workers installed before it, and passed to a build older than it",
40
- })
41
- .option("slots", z.coerce.number().int().min(0), {
42
- default: 0,
43
- description:
44
- "how many tasks this worker takes at once; by default one for each core and 4 GB of memory it has, whichever is fewer, as a VM such as WSL2 sees them",
45
- })
46
- .option("id", z.string(), {
47
- default: "",
48
- description:
49
- "what the server knows this worker by, kept across restarts so it picks up where it was; by default the id it joined as, kept beside its key, else the machine's host name",
50
- })
51
- .option("join", z.string(), {
52
- default: "",
53
- description:
54
- "a join token from `splates worker add` on the server, spent to join it before dialing; the key it answers is kept in this machine's system store, or read from SPLATES_WORKER_KEY on one without",
55
- })
56
- .option("ephemeral", z.boolean(), {
57
- default: false,
58
- description:
59
- "keep the key --join answers in memory alone, for a machine destroyed rather than restarted, as one a worker provider makes",
60
- })
61
- .option("key-file", z.boolean(), {
62
- default: false,
63
- description:
64
- "keep the key --join answers in ~/.splates/worker.env, which only this user may read, rather than the system store, for a machine whose store is locked or missing at boot, as WSL's is with no one signed in; read it from there on each run after",
65
- })
66
- .option("quiet", z.boolean(), {
67
- default: false,
68
- description:
69
- "say only what went wrong, not each time it joins or loses its server, as the worker `splates server` starts beside itself does; all of it still goes to worker.log in ~/.splates, the one before it kept as worker.log.1",
70
- })
71
- .action((opts, ctx) =>
72
- workerRun({
73
- coordinator: opts.server || opts.coordinator,
74
- slots: opts.slots,
75
- id: opts.id,
76
- join: opts.join || ctx.ps.env(WORKER_JOIN) || "",
77
- ephemeral: opts.ephemeral,
78
- keyFile: opts["key-file"],
79
- log: opts.quiet ? quietLog(ctx) : ctx.log,
80
- ps: ctx.ps,
81
- fs: ctx.fs,
82
- }),
83
- );
84
-
85
- splatesWorker
86
- .command("join")
87
- .description(
88
- "spend a join token from `splates worker add` and keep the key it answers, as `splates worker run --join` does, then exit, for an installer that leaves running the worker to a service",
89
- )
90
- .option("server", z.string(), {
91
- default: "",
92
- description: "the `splates server` to join: its URL",
93
- })
94
- .option("coordinator", z.string(), {
95
- default: "",
96
- description: "the old name for --server",
97
- })
98
- .option("join", z.string(), {
99
- default: "",
100
- description:
101
- "the join token `splates worker add` printed there; by default read from SPLATES_WORKER_JOIN, which keeps it out of the process list other users of the machine can read",
102
- })
103
- .option("id", z.string(), {
104
- default: "",
105
- description:
106
- "what the server knows this worker by, kept beside its key so a change of host name leaves it the same; by default the machine's host name",
107
- })
108
- .option("key-file", z.boolean(), {
109
- default: false,
110
- description:
111
- "keep the key in ~/.splates/worker.env, which only this user may read, rather than the system store, for `splates worker run --key-file` to read",
112
- })
113
- .action((opts, ctx) =>
114
- workerJoin({
115
- coordinator: opts.server || opts.coordinator,
116
- join: opts.join || ctx.ps.env(WORKER_JOIN) || "",
117
- id: opts.id,
118
- keyFile: opts["key-file"],
119
- log: ctx.log,
120
- ps: ctx.ps,
121
- fs: ctx.fs,
122
- }),
123
- );
124
-
125
- splatesWorker
126
- .command("recipe")
127
- .description(
128
- "print the shell script, run as root on a fresh Debian or Ubuntu machine, that installs what a Linux worker needs: bun, git, the shared browser and this build of splates, for the user `splates`. It is what a provider's snapshot is built from, and what the Windows installer runs in WSL",
129
- )
130
- .action((_opts, ctx) => workerRecipe({ log: ctx.log }));
131
-
132
- splatesWorker
133
- .command("list")
134
- .description(
135
- "list the workers the running `splates server` has: kind (local over its socket, registered otherwise), platform, build, protocol, slots used and total, health with when it was last heard, tasks and $/hour, then any it refused for speaking another protocol",
136
- )
137
- .option("dev", z.string(), {
138
- default: "",
139
- description:
140
- "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV",
141
- })
142
- .option("json", z.boolean(), { default: false, description: "emit JSON" })
143
- .action((opts, ctx) =>
144
- listWorkers({
145
- dev: opts.dev,
146
- json: opts.json,
147
- log: ctx.log,
148
- fs: ctx.fs,
149
- ps: ctx.ps,
150
- send: ctx.fetch,
151
- }),
152
- );
153
-
154
- const dev = {
155
- default: "",
156
- description:
157
- "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV",
158
- };
159
-
160
- splatesWorker
161
- .command("add")
162
- .description(
163
- "add a worker to the running `splates server`: prints a join token, single use and good for 15 minutes, for `splates worker run --join <token>` on the machine to join, which gets a key of its own that `splates worker remove` revokes; with --local N, makes this machine's own worker run N agents at once and take new ones again, adding it back if it was removed; with --provider, has the `workerProvider` in splates.config.ts make one, booted from the snapshot splates builds for it, which joins on its own and whose machine `splates worker remove` destroys",
164
- )
165
- .option("local", z.coerce.number().int().min(0), {
166
- default: 0,
167
- description: "this machine's own worker, with this many slots",
168
- })
169
- .option("provider", z.boolean(), {
170
- default: false,
171
- description:
172
- "have the server's worker provider make the worker, of its cheapest type unless --type says",
173
- })
174
- .option("type", z.string(), {
175
- default: "",
176
- description:
177
- "the worker provider's type to make, as it names it; implies --provider",
178
- })
179
- .option("dev", z.string(), dev)
180
- .action((opts, ctx) =>
181
- addWorker({
182
- local: opts.local,
183
- provider: opts.provider,
184
- type: opts.type,
185
- dev: opts.dev,
186
- log: ctx.log,
187
- fs: ctx.fs,
188
- ps: ctx.ps,
189
- send: ctx.fetch,
190
- }),
191
- );
192
-
193
- splatesWorker
194
- .command("drain")
195
- .description(
196
- "take no new agents on a worker, letting those on it run to their end, but for one asking you something in a task, which saves its task and lets its slot go, its question kept for you to answer and carry it on elsewhere; --undo takes new ones again",
197
- )
198
- .arg("id", z.string(), {
199
- description: "the worker, as `splates worker list` names it",
200
- })
201
- .option("undo", z.boolean(), {
202
- default: false,
203
- description: "take new agents again",
204
- })
205
- .option("dev", z.string(), dev)
206
- .action((opts, ctx) =>
207
- drainWorker({
208
- id: opts.id,
209
- undo: opts.undo,
210
- dev: opts.dev,
211
- log: ctx.log,
212
- fs: ctx.fs,
213
- ps: ctx.ps,
214
- send: ctx.fetch,
215
- }),
216
- );
217
-
218
- splatesWorker
219
- .command("wake")
220
- .description(
221
- "wake a worker asleep at home, a Windows PC that joined in WSL2: a worker awake on its network sends the Wake-on-LAN packet. Placement wakes one by itself when agents queue, before making a worker",
222
- )
223
- .arg("id", z.string(), {
224
- description: "the worker, as `splates worker list` names it",
225
- })
226
- .option("dev", z.string(), dev)
227
- .action((opts, ctx) =>
228
- wakeWorker({
229
- id: opts.id,
230
- dev: opts.dev,
231
- log: ctx.log,
232
- fs: ctx.fs,
233
- ps: ctx.ps,
234
- send: ctx.fetch,
235
- }),
236
- );
237
-
238
- splatesWorker
239
- .command("slots")
240
- .description(
241
- "set how many agents a worker runs at once, from now on, over what it started with",
242
- )
243
- .arg("id", z.string(), {
244
- description: "the worker, as `splates worker list` names it",
245
- })
246
- .arg("slots", z.coerce.number().int().min(1), {
247
- description: "how many at once",
248
- })
249
- .option("dev", z.string(), dev)
250
- .action((opts, ctx) =>
251
- setWorkerSlots({
252
- id: opts.id,
253
- slots: opts.slots,
254
- dev: opts.dev,
255
- log: ctx.log,
256
- fs: ctx.fs,
257
- ps: ctx.ps,
258
- send: ctx.fetch,
259
- }),
260
- );
261
-
262
- splatesWorker
263
- .command("remove")
264
- .description(
265
- "remove a worker, revoking its key: one with agents on it drains and is removed once they end; --force ends them now, frees their slots, and places each again on another worker, to carry on its task from the checkpoint it last pushed. This machine's own worker, `local`, stops taking agents, its agents running to their end, and leaves this machine only coordinating until `splates worker add --local <slots>`; it takes no --force",
266
- )
267
- .arg("id", z.string(), {
268
- description: "the worker, as `splates worker list` names it",
269
- })
270
- .option("force", z.boolean(), {
271
- default: false,
272
- description: "end the agents on it now rather than wait for them",
273
- })
274
- .option("dev", z.string(), dev)
275
- .action((opts, ctx) =>
276
- removeWorker({
277
- id: opts.id,
278
- force: opts.force,
279
- dev: opts.dev,
280
- log: ctx.log,
281
- fs: ctx.fs,
282
- ps: ctx.ps,
283
- send: ctx.fetch,
284
- }),
285
- );
286
-
287
- splatesWorker.mount("queue", splatesWaiting);
288
-
289
- /**
290
- * What a quiet worker says: what went wrong on the terminal, and everything
291
- * in `worker.log` in the splates home, which is all a worker `splates
292
- * server` started leaves of how it joined and lost its server.
293
- */
294
- function quietLog(ctx: SplatesDeps) {
295
- const file = new FileLogger(ctx.fs, `${splatesHome(ctx.ps)}/worker.log`);
296
- return new CompositeLogger([
297
- { info: () => {}, error: (...said) => ctx.log.error(...said) },
298
- file,
299
- ]);
300
- }