@stringsync/splates 0.0.53 → 0.0.54
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/artifact.d.ts +3 -0
- package/budget.d.ts +3 -0
- package/creds.d.ts +3 -0
- package/flags.d.ts +37 -0
- package/hook.d.ts +3 -0
- package/index.d.ts +2 -0
- package/index.js +1802 -0
- package/infra.d.ts +3 -0
- package/lane.d.ts +3 -0
- package/notes.d.ts +4 -0
- package/package.json +12 -14
- package/projects.d.ts +3 -0
- package/queue.d.ts +3 -0
- package/server.d.ts +7 -0
- package/setup.d.ts +22 -0
- package/slots.d.ts +3 -0
- package/splates.d.ts +10 -0
- package/task.d.ts +3 -0
- package/tunnel.d.ts +3 -0
- package/waiting.d.ts +3 -0
- package/worker.d.ts +3 -0
- package/artifact.ts +0 -120
- package/budget.ts +0 -117
- package/checkout.ts +0 -8
- package/creds.test.ts +0 -336
- package/creds.ts +0 -144
- package/dev/dev.ts +0 -86
- package/dev/eval.ts +0 -21
- package/dev/fix.ts +0 -63
- package/dev/index.ts +0 -1
- package/dev/prove.ts +0 -38
- package/dev/root.ts +0 -7
- package/dev/ship.ts +0 -46
- package/dev/test.ts +0 -154
- package/e2e.test.ts +0 -472
- package/flags.ts +0 -157
- package/hook.ts +0 -328
- package/index.ts +0 -18
- package/infra.ts +0 -107
- package/lane.test.ts +0 -29
- package/lane.ts +0 -172
- package/notes.ts +0 -19
- package/projects.test.ts +0 -191
- package/projects.ts +0 -37
- package/queue.ts +0 -82
- package/screenshots/annotate.png +0 -0
- package/screenshots/demo.png +0 -0
- package/screenshots/parallel.png +0 -0
- package/screenshots/questions.png +0 -0
- package/server.ts +0 -287
- package/setup.ts +0 -233
- package/slots.ts +0 -65
- package/splates.test.ts +0 -338
- package/splates.ts +0 -567
- package/task.ts +0 -354
- package/tunnel.ts +0 -106
- package/wait-journal.test.ts +0 -96
- package/waiting.ts +0 -59
- package/worker-relay.test.ts +0 -233
- package/worker.ts +0 -300
package/server.ts
DELETED
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BoxShell,
|
|
3
|
-
type BoxStanding,
|
|
4
|
-
CoordinatorBox,
|
|
5
|
-
coordinatorMoved,
|
|
6
|
-
deployKey,
|
|
7
|
-
machineMove,
|
|
8
|
-
neededCreds,
|
|
9
|
-
} from "@stringsync/splates-server/coordinator";
|
|
10
|
-
import { SYSTEM_STORES } from "@stringsync/splates-server/creds";
|
|
11
|
-
import { fail } from "@stringsync/splates-server/exit";
|
|
12
|
-
import {
|
|
13
|
-
DEFAULT_PORT,
|
|
14
|
-
RUNNING,
|
|
15
|
-
type SplatesDeps,
|
|
16
|
-
serveDev,
|
|
17
|
-
} from "@stringsync/splates-server/server";
|
|
18
|
-
import { LOCAL_SLOTS } from "@stringsync/splates-server/workers";
|
|
19
|
-
import { cli } from "webappwiz/cmd";
|
|
20
|
-
import { z } from "zod";
|
|
21
|
-
import { splatesInfra } from "./infra";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* `splates server`, which the root cli mounts after `splates log`: bare, it
|
|
25
|
-
* runs the server here; its other commands deploy it onto a box of the user's
|
|
26
|
-
* own and look after that box.
|
|
27
|
-
*/
|
|
28
|
-
export const splatesServer = cli<SplatesDeps>("server")
|
|
29
|
-
.description(
|
|
30
|
-
"the splates server: run it on this machine, or deploy it to an always-on box in your own AWS account, which the Pulumi project `splates server infra init` writes makes, reached by SSH alone",
|
|
31
|
-
)
|
|
32
|
-
.fallback("run");
|
|
33
|
-
|
|
34
|
-
splatesServer
|
|
35
|
-
.command("run")
|
|
36
|
-
.description(
|
|
37
|
-
"serve every splates project on this machine as a web page, from wherever it is run: the page picks the project it shows, adds one by its path, running `splates init` there, and shows its queued and started tasks, where queued tasks can be added, updated, reordered or removed; it brings each project's records forward as it opens it. A server that crashes starts again after 2s, then 4s, 8s and 16s, and is left down after five crashes in a row that each came within a minute of its start",
|
|
38
|
-
)
|
|
39
|
-
.option("port", z.coerce.number(), {
|
|
40
|
-
default: DEFAULT_PORT,
|
|
41
|
-
description: "port to listen on, or the next open one above it",
|
|
42
|
-
})
|
|
43
|
-
.option("allow-hosts", z.string(), {
|
|
44
|
-
default: "",
|
|
45
|
-
description:
|
|
46
|
-
"comma-separated host names the page may also be reached by, like a tunnel's; put Cloudflare Access in front of them with --access-team and --access-aud",
|
|
47
|
-
})
|
|
48
|
-
.option("access-team", z.string(), {
|
|
49
|
-
default: "",
|
|
50
|
-
description:
|
|
51
|
-
"the Cloudflare Access team in front of --allow-hosts, as in <team>.cloudflareaccess.com; with --access-aud, those hosts answer only to a verified Access token, and may then use conversations and terminals",
|
|
52
|
-
})
|
|
53
|
-
.option("access-aud", z.string(), {
|
|
54
|
-
default: "",
|
|
55
|
-
description: "the AUD tag of that Access application",
|
|
56
|
-
})
|
|
57
|
-
.option("tunnel", z.string(), {
|
|
58
|
-
default: "",
|
|
59
|
-
description:
|
|
60
|
-
"the public host name of a tunnel to run, with cloudflared or the tunnel provider named by splates.config.ts in the repository it is run in, if any, which is allowed as a host too; refused without --access-team and --access-aud, so a tunnel never exposes the page unauthenticated. With none of the tunnel and Access flags, the tunnel `splates tunnel setup` saved runs, if any",
|
|
61
|
-
})
|
|
62
|
-
.option("tunnel-name", z.string(), {
|
|
63
|
-
default: "",
|
|
64
|
-
description:
|
|
65
|
-
"the named tunnel cloudflared runs for --tunnel, by default the one its own config names; not with a tunnel provider of splates.config.ts",
|
|
66
|
-
})
|
|
67
|
-
.option("ports-host", z.string(), {
|
|
68
|
-
default: "",
|
|
69
|
-
description:
|
|
70
|
-
"the wildcard base host to serve task ports under, saved for this machine so a bare `splates server` serves it too: each port a task's processes listen on opens at https://<task>-<port>.<base>, to whoever Cloudflare Access lets in, and is refused without Access; routing *.<base> here, its DNS and TLS, are yours. `none` forgets it, leaving each task port forwarded to a port on this machine's loopback alone",
|
|
71
|
-
})
|
|
72
|
-
.option("worker-slots", z.coerce.number().int().min(1), {
|
|
73
|
-
default: LOCAL_SLOTS,
|
|
74
|
-
description:
|
|
75
|
-
"how many agents that other agents started (with `splates add --agent`, or a lane's orchestrator handing out tasks) this machine's worker runs at once; past them, each new or resumed one waits in the queue, first come, first served, which `splates worker queue` shows and reorders. An agent you start runs at once on any worker up, taking no slot. Read as the server starts its worker, which a restart from the page leaves running",
|
|
76
|
-
})
|
|
77
|
-
.option("keepalive", z.boolean(), {
|
|
78
|
-
default: false,
|
|
79
|
-
description:
|
|
80
|
-
"keep a machine left to serve awake while the server runs: on macOS with `caffeinate`, on power or idle, though closing a laptop's lid still sleeps it; on Linux with a systemd-inhibit lock on sleep and idle, which the server serves on without, saying so, where systemd is missing or refuses it. Refused on other platforms",
|
|
81
|
-
})
|
|
82
|
-
.option("slot", z.boolean(), {
|
|
83
|
-
default: false,
|
|
84
|
-
description:
|
|
85
|
-
"serve this checkout's splates at https://<word>.<zone>, a slot of its own on the zone `splates tunnel setup` uses, behind the same Access policy, so several run side by side and a phone opens each by name: a task's worktree keeps its word, as fox, until the task merges or is removed, which stops its server; run again, it stops the tree's last server first. Refused in a splates repository's main tree, which every agent shares. A slot keeps its own projects in ~/.splates/slots/<word>, apart from yours, starting with a throwaway project there; not with the tunnel and Access flags",
|
|
86
|
-
})
|
|
87
|
-
.action((opts, ctx) => serveDev(ctx, opts));
|
|
88
|
-
|
|
89
|
-
splatesServer.mount("infra", splatesInfra);
|
|
90
|
-
|
|
91
|
-
splatesServer
|
|
92
|
-
.command("deploy")
|
|
93
|
-
.description(
|
|
94
|
-
"deploy the server to the box its Pulumi stack made (`splates server infra init`, then `pulumi up`), over SSH, with splates of this release installed for the user splates. The SSH key and the host key come from the stack and the instance's console. It checks what the deploy needs, brings the box's splates to this release, and exits `blocked`, naming each, until the box's Parameter Store holds the creds the stack should have put there; then gives each project a clone there with its deploy key from Parameter Store. Then it asks every agent of this machine's `splates server` to checkpoint and stops it, carries the server's records (no creds: the box reads its own through its instance role) to the box, starts `splates server` there for good under systemd, and joins this machine to it as a worker, kept up by a LaunchAgent, where each task carries on in its worktree here. This machine's records go aside, kept, and its `splates` reaches the box from then on. Run again once the server runs on the box, it brings the box's splates to this release and restarts it there",
|
|
95
|
-
)
|
|
96
|
-
.option("dry-run", z.boolean(), {
|
|
97
|
-
default: false,
|
|
98
|
-
description:
|
|
99
|
-
"stop once the box is ready, before this machine's server stops or anything here changes; once the server runs on the box, change nothing",
|
|
100
|
-
})
|
|
101
|
-
.option("dev", z.string(), {
|
|
102
|
-
default: `http://localhost:${DEFAULT_PORT}`,
|
|
103
|
-
description: "this machine's `splates server`, which the deploy stops",
|
|
104
|
-
})
|
|
105
|
-
.action(async (opts, ctx) => {
|
|
106
|
-
const moved = await machineMove({
|
|
107
|
-
ps: ctx.ps,
|
|
108
|
-
fs: ctx.fs,
|
|
109
|
-
log: ctx.log,
|
|
110
|
-
version: RUNNING,
|
|
111
|
-
dev: opts.dev,
|
|
112
|
-
stores: ctx.secrets ?? SYSTEM_STORES,
|
|
113
|
-
...(ctx.fetch === undefined ? {} : { fetch: ctx.fetch }),
|
|
114
|
-
}).run({ dryRun: opts["dry-run"] });
|
|
115
|
-
if (moved.status === "blocked") {
|
|
116
|
-
fail(
|
|
117
|
-
"blocked",
|
|
118
|
-
[
|
|
119
|
-
"The box's Parameter Store lacks these. Set each (the Pulumi stack makes them: `pulumi up`), then deploy again:",
|
|
120
|
-
...moved.needs.map((name) => ` ${name}`),
|
|
121
|
-
].join("\n"),
|
|
122
|
-
{ needs: moved.needs },
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
if (moved.status === "deployed") {
|
|
126
|
-
ctx.log.info(
|
|
127
|
-
`the server runs on the box at ${moved.ipv4}: deploy again without --dry-run to bring it to ${moved.version}`,
|
|
128
|
-
);
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
if (moved.status === "updated") {
|
|
132
|
-
ctx.log.info(
|
|
133
|
-
`the server on the box at ${moved.ipv4} runs ${moved.version} now`,
|
|
134
|
-
);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
if (moved.status === "ready") {
|
|
138
|
-
ctx.log.info(
|
|
139
|
-
`the box is ready to serve ${moved.projects.join(", ")} through ${moved.host}: deploy again without --dry-run`,
|
|
140
|
-
);
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
ctx.log.info(
|
|
144
|
-
[
|
|
145
|
-
`the server runs on the box: https://${moved.host}`,
|
|
146
|
-
`this machine is its worker ${ctx.ps.hostname}, kept up by ${moved.worker}`,
|
|
147
|
-
`its own records are kept aside in ${moved.aside} and each project's .git/splates.moved`,
|
|
148
|
-
].join("\n"),
|
|
149
|
-
);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
splatesServer
|
|
153
|
-
.command("status")
|
|
154
|
-
.description(
|
|
155
|
-
"print where the server runs, here or deployed to its box, and the box as its cloud has it",
|
|
156
|
-
)
|
|
157
|
-
.option("json", z.boolean(), { default: false, description: "emit JSON" })
|
|
158
|
-
.action(async (opts, ctx) => {
|
|
159
|
-
const deployed = await coordinatorMoved({
|
|
160
|
-
fs: ctx.fs,
|
|
161
|
-
ps: ctx.ps,
|
|
162
|
-
home: box(ctx).home,
|
|
163
|
-
});
|
|
164
|
-
const standing = await box(ctx).status();
|
|
165
|
-
if (opts.json) {
|
|
166
|
-
ctx.log.info(JSON.stringify({ deployed, box: standing }));
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
ctx.log.info(
|
|
170
|
-
[
|
|
171
|
-
`runs: ${deployed ? "on the box" : "on this machine"}`,
|
|
172
|
-
...(standing === null
|
|
173
|
-
? [
|
|
174
|
-
"box: none: `splates server infra init` writes the Pulumi project, `pulumi up` there makes it",
|
|
175
|
-
]
|
|
176
|
-
: [
|
|
177
|
-
`address: ${standing.ipv4}`,
|
|
178
|
-
`instance: ${standing.instanceId} (role ${standing.roleName})`,
|
|
179
|
-
`host: ${standing.host}`,
|
|
180
|
-
"ssh: splates server ssh",
|
|
181
|
-
]),
|
|
182
|
-
].join("\n"),
|
|
183
|
-
);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
splatesServer
|
|
187
|
-
.command("ssh")
|
|
188
|
-
.description(
|
|
189
|
-
"open a root shell on the box over SSH, with the key its Pulumi stack made, or run the command given there",
|
|
190
|
-
)
|
|
191
|
-
.rest("command", z.string(), {
|
|
192
|
-
description: "what to run on the box; by default a shell",
|
|
193
|
-
})
|
|
194
|
-
.passThroughOptions()
|
|
195
|
-
.action(async (opts, ctx) => {
|
|
196
|
-
const standing = await box(ctx).status();
|
|
197
|
-
if (standing === null) {
|
|
198
|
-
fail(
|
|
199
|
-
"not_found",
|
|
200
|
-
"no box: `splates server infra init` writes the Pulumi project, `pulumi up` there makes it",
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
const { exitCode } = await ctx.ps.spawn(
|
|
204
|
-
shell(ctx, standing).login(opts.command),
|
|
205
|
-
);
|
|
206
|
-
ctx.ps.exit(exitCode);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
splatesServer
|
|
210
|
-
.command("destroy")
|
|
211
|
-
.description(
|
|
212
|
-
"unmake the box and everything its Pulumi stack made (its role, snapshots, creds, tunnel and deploy keys), lifting the box's termination protection first: anything only on the box is lost. The stack and its config stay, so the setup's Make the box step, or `splates server deploy`, makes it again. Without --yes, says what it would destroy",
|
|
213
|
-
)
|
|
214
|
-
.option("yes", z.boolean(), {
|
|
215
|
-
default: false,
|
|
216
|
-
description: "destroy without asking",
|
|
217
|
-
})
|
|
218
|
-
.action(async (opts, ctx) => {
|
|
219
|
-
const infra = await box(ctx).infra();
|
|
220
|
-
if (infra === null) {
|
|
221
|
-
fail(
|
|
222
|
-
"not_found",
|
|
223
|
-
"no infra project here: nothing to destroy, since `splates server infra init` has not written one",
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
const standing = await box(ctx).status();
|
|
227
|
-
const what =
|
|
228
|
-
standing === null
|
|
229
|
-
? `what the stack in ${infra.dir} made`
|
|
230
|
-
: `the box at ${standing.ipv4} (${standing.instanceId}) and everything the stack in ${infra.dir} made`;
|
|
231
|
-
if (!opts.yes) {
|
|
232
|
-
fail(
|
|
233
|
-
"usage",
|
|
234
|
-
`this destroys ${what}, and anything only on the box is lost: run again with --yes`,
|
|
235
|
-
{ dir: infra.dir, box: standing },
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
ctx.log.info(`Destroying ${what}, a few minutes…`);
|
|
239
|
-
await box(ctx).destroy();
|
|
240
|
-
ctx.log.info(
|
|
241
|
-
"Destroyed. The stack and its config stay for making it again.",
|
|
242
|
-
);
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
splatesServer
|
|
246
|
-
.command("needs")
|
|
247
|
-
.description(
|
|
248
|
-
"kept for `splates server deploy`, which runs it on the box: print the name of each cred this box's store lacks of those the server needs, one per line",
|
|
249
|
-
)
|
|
250
|
-
.rest("roots", z.string(), {
|
|
251
|
-
description: "each project's clone on this machine",
|
|
252
|
-
})
|
|
253
|
-
.action(async (opts, ctx) => {
|
|
254
|
-
const missing = await neededCreds(ctx.secrets ?? SYSTEM_STORES, opts.roots);
|
|
255
|
-
for (const name of missing) {
|
|
256
|
-
ctx.log.info(name);
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
splatesServer
|
|
261
|
-
.command("deploy-key")
|
|
262
|
-
.description(
|
|
263
|
-
"kept for `splates server deploy`, which runs it on the box: print the deploy key of the project whose clone is <root>",
|
|
264
|
-
)
|
|
265
|
-
.arg("root", z.string(), {
|
|
266
|
-
description: "the project's clone on this machine",
|
|
267
|
-
})
|
|
268
|
-
.action(async (opts, ctx) => {
|
|
269
|
-
const key = await deployKey(ctx.secrets ?? SYSTEM_STORES, opts.root);
|
|
270
|
-
if (key === undefined) {
|
|
271
|
-
fail("not_found", `no deploy key kept for ${opts.root}`);
|
|
272
|
-
}
|
|
273
|
-
ctx.log.info(key);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
function box(ctx: SplatesDeps): CoordinatorBox {
|
|
277
|
-
return new CoordinatorBox({ ps: ctx.ps, fs: ctx.fs });
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function shell(ctx: SplatesDeps, standing: BoxStanding): BoxShell {
|
|
281
|
-
return new BoxShell({
|
|
282
|
-
ps: ctx.ps,
|
|
283
|
-
ipv4: standing.ipv4,
|
|
284
|
-
key: standing.key,
|
|
285
|
-
knownHosts: `${box(ctx).home}/coordinator/known_hosts`,
|
|
286
|
-
});
|
|
287
|
-
}
|
package/setup.ts
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
import { dirname } from "node:path";
|
|
2
|
-
import { SYSTEM_STORES } from "@stringsync/splates-server/creds";
|
|
3
|
-
import { fail } from "@stringsync/splates-server/exit";
|
|
4
|
-
import { splatesHome } from "@stringsync/splates-server/models";
|
|
5
|
-
import { Projects } from "@stringsync/splates-server/projects";
|
|
6
|
-
import type { SplatesDeps } from "@stringsync/splates-server/server";
|
|
7
|
-
import {
|
|
8
|
-
SETUP_AREAS,
|
|
9
|
-
Setup,
|
|
10
|
-
type SetupProgress,
|
|
11
|
-
type StepProgress,
|
|
12
|
-
type StepStatus,
|
|
13
|
-
setupProjectAt,
|
|
14
|
-
type UndoPlan,
|
|
15
|
-
} from "@stringsync/splates-server/setup";
|
|
16
|
-
import {
|
|
17
|
-
fetchWorkerBudget,
|
|
18
|
-
fetchWorkers,
|
|
19
|
-
putWorkerLimit,
|
|
20
|
-
} from "@stringsync/splates-server/workers";
|
|
21
|
-
import type { Logger } from "webappwiz/log";
|
|
22
|
-
import { color } from "webappwiz/log";
|
|
23
|
-
import type { Ps } from "webappwiz/system";
|
|
24
|
-
|
|
25
|
-
/** What `splates setup` was asked for. */
|
|
26
|
-
export interface SetupOptions {
|
|
27
|
-
/** A step's id, or empty for every area's progress. */
|
|
28
|
-
step: string;
|
|
29
|
-
json: boolean;
|
|
30
|
-
skip: boolean;
|
|
31
|
-
unskip: boolean;
|
|
32
|
-
/** Print what undoing the area `step` names would do. */
|
|
33
|
-
undo: boolean;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* `splates setup [step]`: progress by area and the one next step, or one step's
|
|
38
|
-
* why and instructions, after skipping or unskipping it when asked.
|
|
39
|
-
*/
|
|
40
|
-
export async function setup(
|
|
41
|
-
deps: SplatesDeps,
|
|
42
|
-
opts: SetupOptions,
|
|
43
|
-
): Promise<void> {
|
|
44
|
-
if (opts.step === "" && (opts.skip || opts.unskip || opts.undo)) {
|
|
45
|
-
fail("usage", "--skip and --unskip name a step, --undo an area", {});
|
|
46
|
-
}
|
|
47
|
-
if (opts.undo && (opts.skip || opts.unskip)) {
|
|
48
|
-
fail("usage", "--undo names an area, not a step to skip", {
|
|
49
|
-
area: opts.step,
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
if (opts.skip && opts.unskip) {
|
|
53
|
-
fail("usage", "--skip or --unskip, not both", { step: opts.step });
|
|
54
|
-
}
|
|
55
|
-
const machine = await setupOf(deps);
|
|
56
|
-
const { log } = deps;
|
|
57
|
-
if (opts.undo) {
|
|
58
|
-
const plan = await machine.undoPlan(opts.step);
|
|
59
|
-
if (opts.json) {
|
|
60
|
-
log.info(JSON.stringify(plan, null, 2));
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
printUndo(log, plan);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (opts.step !== "") {
|
|
67
|
-
machine.step(opts.step);
|
|
68
|
-
if (opts.skip) {
|
|
69
|
-
await machine.skip(opts.step);
|
|
70
|
-
} else if (opts.unskip) {
|
|
71
|
-
await machine.unskip(opts.step);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
const progress = await machine.read();
|
|
75
|
-
if (opts.step === "") {
|
|
76
|
-
if (opts.json) {
|
|
77
|
-
log.info(JSON.stringify(progress, null, 2));
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
printProgress(log, progress);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
const step = progress.areas
|
|
84
|
-
.flatMap((area) => area.steps)
|
|
85
|
-
.find((each) => each.id === opts.step) as StepProgress;
|
|
86
|
-
if (opts.json) {
|
|
87
|
-
log.info(JSON.stringify(step, null, 2));
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
printStep(log, step);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* This machine's setup, read through `deps`, for the project `--project`
|
|
95
|
-
* names or the repository the working directory is in, if either.
|
|
96
|
-
*/
|
|
97
|
-
export async function setupOf(deps: SplatesDeps): Promise<Setup> {
|
|
98
|
-
const { fs, ps, secrets = SYSTEM_STORES, fetch } = deps;
|
|
99
|
-
const root =
|
|
100
|
-
deps.project === undefined
|
|
101
|
-
? await repoRoot(ps)
|
|
102
|
-
: (await Projects.ofMachine(deps).named(deps.project)).root;
|
|
103
|
-
return new Setup(
|
|
104
|
-
{
|
|
105
|
-
fs,
|
|
106
|
-
ps,
|
|
107
|
-
home: splatesHome(ps),
|
|
108
|
-
device: secrets.device,
|
|
109
|
-
fetch,
|
|
110
|
-
// Only a server this agent runs under, or is pointed at, can be asked.
|
|
111
|
-
workers: deps.ps.env("SPLATES_DEV")
|
|
112
|
-
? () => fetchWorkers({ dev: "", log: deps.log, ps: deps.ps })
|
|
113
|
-
: undefined,
|
|
114
|
-
workerBudget: deps.ps.env("SPLATES_DEV")
|
|
115
|
-
? {
|
|
116
|
-
report: () =>
|
|
117
|
-
fetchWorkerBudget({ dev: "", log: deps.log, ps: deps.ps }),
|
|
118
|
-
set: (limit, dollars) =>
|
|
119
|
-
putWorkerLimit(limit, dollars, {
|
|
120
|
-
dev: "",
|
|
121
|
-
log: deps.log,
|
|
122
|
-
ps: deps.ps,
|
|
123
|
-
}),
|
|
124
|
-
}
|
|
125
|
-
: undefined,
|
|
126
|
-
project: root === null ? undefined : setupProjectAt(root, { fs, ps }),
|
|
127
|
-
},
|
|
128
|
-
SETUP_AREAS,
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** The main tree of the repository the working directory is in, or null. */
|
|
133
|
-
async function repoRoot(ps: Ps): Promise<string | null> {
|
|
134
|
-
const { exitCode, stdout } = await ps.spawnCapture([
|
|
135
|
-
"git",
|
|
136
|
-
"-C",
|
|
137
|
-
ps.cwd(),
|
|
138
|
-
"rev-parse",
|
|
139
|
-
"--path-format=absolute",
|
|
140
|
-
"--git-common-dir",
|
|
141
|
-
]);
|
|
142
|
-
return exitCode === 0 ? dirname(stdout.trim()) : null;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const MARKS: Record<StepStatus, string> = {
|
|
146
|
-
done: color.green("✓"),
|
|
147
|
-
skipped: color.dim("-"),
|
|
148
|
-
ready: color.yellow("○"),
|
|
149
|
-
waiting: color.dim("○"),
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
function printProgress(log: Logger, progress: SetupProgress): void {
|
|
153
|
-
for (const area of progress.areas) {
|
|
154
|
-
const counts = [
|
|
155
|
-
`${area.done}/${area.steps.length} done`,
|
|
156
|
-
area.skipped > 0 ? `${area.skipped} skipped` : "",
|
|
157
|
-
].filter(Boolean);
|
|
158
|
-
log.info(`${color.bold(area.title)} ${color.dim(counts.join(", "))}`);
|
|
159
|
-
for (const step of area.steps) {
|
|
160
|
-
const notes = [
|
|
161
|
-
step.optional ? "optional" : "",
|
|
162
|
-
step.status === "waiting" ? `after ${step.after.join(", ")}` : "",
|
|
163
|
-
step.problem ?? "",
|
|
164
|
-
step.warning === null ? "" : "to put right",
|
|
165
|
-
].filter(Boolean);
|
|
166
|
-
log.info(
|
|
167
|
-
` ${MARKS[step.status]} ${step.id} ${step.title}${notes.length > 0 ? color.dim(` ${notes.join("; ")}`) : ""}`,
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
const steps = progress.areas.flatMap((area) => area.steps);
|
|
172
|
-
const next = steps.find((step) => step.id === progress.next);
|
|
173
|
-
for (const step of steps.filter((each) => each.warning !== null)) {
|
|
174
|
-
log.info(
|
|
175
|
-
`${color.yellow("put right:")} ${step.title} ${color.dim(`splates setup ${step.id}`)}`,
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
log.info(
|
|
179
|
-
next === undefined
|
|
180
|
-
? color.green("nothing left to set up")
|
|
181
|
-
: `next: ${next.title} ${color.dim(`splates setup ${next.id}`)}`,
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function printStep(log: Logger, step: StepProgress): void {
|
|
186
|
-
log.info(
|
|
187
|
-
`${color.bold(step.title)} ${color.dim(`${step.status}${step.optional ? ", optional" : ""}`)}`,
|
|
188
|
-
);
|
|
189
|
-
log.info(step.why);
|
|
190
|
-
if (step.detail !== null) {
|
|
191
|
-
log.info(step.detail);
|
|
192
|
-
}
|
|
193
|
-
if (step.warning !== null) {
|
|
194
|
-
log.info(`${color.yellow("put right:")} ${step.warning}`);
|
|
195
|
-
}
|
|
196
|
-
log.info(`you: ${step.user}`);
|
|
197
|
-
log.info(`agent: ${step.agent}`);
|
|
198
|
-
if (step.after.length > 0) {
|
|
199
|
-
log.info(`after: ${step.after.join(", ")}`);
|
|
200
|
-
}
|
|
201
|
-
if (step.problem !== null) {
|
|
202
|
-
log.info(`${color.red("problem:")} ${step.problem}`);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function printUndo(log: Logger, plan: UndoPlan): void {
|
|
207
|
-
log.info(`${color.bold(`Undo ${plan.title}`)} ${color.dim(plan.area)}`);
|
|
208
|
-
if (plan.refused !== null) {
|
|
209
|
-
log.info(`${color.red("refused:")} ${plan.refused}`);
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
for (const step of plan.steps) {
|
|
213
|
-
const started = step.done ? "" : ` ${color.yellow("started")}`;
|
|
214
|
-
log.info(` ${step.title} ${color.dim(step.id)}${started}`);
|
|
215
|
-
for (const line of step.removes) {
|
|
216
|
-
log.info(` removes ${line}`);
|
|
217
|
-
}
|
|
218
|
-
for (const line of step.leaves) {
|
|
219
|
-
log.info(` leaves ${line}`);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
for (const cred of plan.creds) {
|
|
223
|
-
log.info(
|
|
224
|
-
cred.keptFor === null
|
|
225
|
-
? ` forgets ${cred.name}`
|
|
226
|
-
: ` keeps ${cred.name} ${color.dim(`${cred.keptFor} still uses it`)}`,
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
if (plan.confirm !== null) {
|
|
230
|
-
log.info(` the page asks for ${plan.confirm} to be typed`);
|
|
231
|
-
}
|
|
232
|
-
log.info(color.dim("the Setup page's Undo does it, after the user confirms"));
|
|
233
|
-
}
|
package/slots.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { type Held, Slots } from "@stringsync/splates-server/ports";
|
|
2
|
-
import type { SplatesDeps } from "@stringsync/splates-server/server";
|
|
3
|
-
import { cli } from "webappwiz/cmd";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
|
|
6
|
-
/** `splates slots`, which the root cli mounts after `splates tunnel`. */
|
|
7
|
-
export const splatesSlots = cli<SplatesDeps>("slots").description(
|
|
8
|
-
"the words `splates server --slot` hands out on this machine, each held by the tree that claimed it: a task's worktree holds its word until the task merges or is removed, which stops its server and gives the word back",
|
|
9
|
-
);
|
|
10
|
-
|
|
11
|
-
splatesSlots
|
|
12
|
-
.command("list")
|
|
13
|
-
.description(
|
|
14
|
-
"print each claimed word, the tree holding it, its server's pid while one runs, and whether it is orphaned: its tree gone, or a splates repository's main tree, which every agent shares",
|
|
15
|
-
)
|
|
16
|
-
.option("json", z.boolean(), { default: false, description: "emit JSON" })
|
|
17
|
-
.action(async (opts, ctx) => {
|
|
18
|
-
const held = await Slots.ofMachine(ctx).status();
|
|
19
|
-
if (opts.json) {
|
|
20
|
-
ctx.log.info(
|
|
21
|
-
JSON.stringify(
|
|
22
|
-
held.map(({ slot, tree, pid, orphaned }) => ({
|
|
23
|
-
word: slot.word,
|
|
24
|
-
tree,
|
|
25
|
-
pid,
|
|
26
|
-
orphaned,
|
|
27
|
-
})),
|
|
28
|
-
),
|
|
29
|
-
);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
if (held.length === 0) {
|
|
33
|
-
ctx.log.info("no slot is claimed");
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
ctx.log.info(held.map(heldLine).join("\n"));
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
splatesSlots
|
|
40
|
-
.command("prune")
|
|
41
|
-
.description(
|
|
42
|
-
"free every orphaned word, stopping its server, so another tree can claim it; the slots a task's worktree holds are that task's, and left alone. The way to free a slot: never kill another tree's server or edit ~/.splates/slots.json",
|
|
43
|
-
)
|
|
44
|
-
.action(async (_opts, ctx) => {
|
|
45
|
-
const freed = await Slots.ofMachine(ctx).prune();
|
|
46
|
-
if (freed.length === 0) {
|
|
47
|
-
ctx.log.info(
|
|
48
|
-
"nothing to prune: each claimed word is held by a task's worktree",
|
|
49
|
-
);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
ctx.log.info(
|
|
53
|
-
freed
|
|
54
|
-
.map(
|
|
55
|
-
({ slot, tree, pid }) =>
|
|
56
|
-
`freed ${slot.word} ${slot.emoji} from ${tree}${pid === null ? "" : `, its server (pid ${pid}) stopped`}`,
|
|
57
|
-
)
|
|
58
|
-
.join("\n"),
|
|
59
|
-
);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
function heldLine({ slot, tree, pid, orphaned }: Held): string {
|
|
63
|
-
const server = pid === null ? "no server" : `server pid ${pid}`;
|
|
64
|
-
return `${slot.word} ${slot.emoji} ${tree} ${server}${orphaned ? " orphaned: `splates slots prune` frees it" : ""}`;
|
|
65
|
-
}
|