@stringsync/splates 0.0.54 → 0.0.56
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 +9 -73
- package/handoff.d.ts +13 -0
- package/index.js +405 -605
- package/package.json +2 -2
- package/replica.d.ts +3 -0
- package/server.d.ts +1 -2
- package/budget.d.ts +0 -3
- package/infra.d.ts +0 -3
package/index.js
CHANGED
|
@@ -11,12 +11,46 @@ import {
|
|
|
11
11
|
import { assets } from "@stringsync/splates-server/server";
|
|
12
12
|
import { NodeFs } from "webappwiz/system";
|
|
13
13
|
|
|
14
|
+
// handoff.ts
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import { dirname, join, resolve } from "node:path";
|
|
17
|
+
var devMounted = { value: false };
|
|
18
|
+
var CHECKOUT_BIN = join("bin", "splates");
|
|
19
|
+
var CHECKOUT_ENTRY = join("packages", "splates", "checkout.ts");
|
|
20
|
+
function checkoutAbove(cwd) {
|
|
21
|
+
for (let dir = resolve(cwd);; dir = dirname(dir)) {
|
|
22
|
+
if (existsSync(join(dir, CHECKOUT_BIN)) && existsSync(join(dir, CHECKOUT_ENTRY))) {
|
|
23
|
+
return dir;
|
|
24
|
+
}
|
|
25
|
+
if (dirname(dir) === dir) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function handOffDev(argv, cwd) {
|
|
31
|
+
if (devMounted.value || argv[0] !== "dev") {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const checkout = checkoutAbove(cwd);
|
|
35
|
+
if (checkout === null) {
|
|
36
|
+
console.error("splates dev is only in a splates checkout, and this directory is in none: cd into one and run its bin/splates.");
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
const proc = Bun.spawn([join(checkout, CHECKOUT_BIN), ...argv], {
|
|
40
|
+
cwd,
|
|
41
|
+
stdin: "inherit",
|
|
42
|
+
stdout: "inherit",
|
|
43
|
+
stderr: "inherit"
|
|
44
|
+
});
|
|
45
|
+
return await proc.exited;
|
|
46
|
+
}
|
|
47
|
+
|
|
14
48
|
// splates.ts
|
|
15
49
|
import { agents } from "@stringsync/splates-server/agents";
|
|
16
50
|
import { attach } from "@stringsync/splates-server/artifacts";
|
|
17
|
-
import { commaList as
|
|
51
|
+
import { commaList as commaList5 } from "@stringsync/splates-server/comma-list";
|
|
18
52
|
import { refuseGroomer as refuseGroomer4 } from "@stringsync/splates-server/conversations";
|
|
19
|
-
import { exits, fail as
|
|
53
|
+
import { exits, fail as fail8 } from "@stringsync/splates-server/exit";
|
|
20
54
|
import { DEFAULT_COUNT, log as showLog } from "@stringsync/splates-server/log";
|
|
21
55
|
import { merge } from "@stringsync/splates-server/merge";
|
|
22
56
|
import { portsLs } from "@stringsync/splates-server/ports";
|
|
@@ -31,7 +65,6 @@ import {
|
|
|
31
65
|
newest,
|
|
32
66
|
recordsFirst,
|
|
33
67
|
rosterOf as rosterOf2,
|
|
34
|
-
runHeavy,
|
|
35
68
|
seeded as seeded2,
|
|
36
69
|
update
|
|
37
70
|
} from "@stringsync/splates-server/server";
|
|
@@ -52,12 +85,12 @@ import {
|
|
|
52
85
|
untilAfter as untilAfter3,
|
|
53
86
|
wait
|
|
54
87
|
} from "@stringsync/splates-server/tasks";
|
|
55
|
-
import { cli as
|
|
88
|
+
import { cli as cli14 } from "webappwiz/cmd";
|
|
56
89
|
import { Duration as Duration3 } from "webappwiz/time";
|
|
57
|
-
import { z as
|
|
90
|
+
import { z as z15 } from "zod";
|
|
58
91
|
|
|
59
92
|
// artifact.ts
|
|
60
|
-
import { basename, resolve } from "node:path";
|
|
93
|
+
import { basename, resolve as resolve2 } from "node:path";
|
|
61
94
|
import {
|
|
62
95
|
addArtifact,
|
|
63
96
|
listArtifacts,
|
|
@@ -186,7 +219,7 @@ splatesArtifact.command("add").use(repository({ needs: "own" })).description("ke
|
|
|
186
219
|
}
|
|
187
220
|
const id = await conversationOf(ctx.service, opts.conversation);
|
|
188
221
|
const files = await Promise.all(paths.map(async (given) => {
|
|
189
|
-
const path =
|
|
222
|
+
const path = resolve2(ctx.ps.cwd(), given);
|
|
190
223
|
const bytes = await ctx.fs.readBytes(path).catch(() => null);
|
|
191
224
|
if (bytes === null) {
|
|
192
225
|
fail2("usage", `cannot read ${path}: nothing was changed`, {
|
|
@@ -211,93 +244,30 @@ splatesArtifact.command("remove").use(repository({ needs: "own" })).description(
|
|
|
211
244
|
splatesArtifact.command("list").use(repository({ needs: "own" })).description("the artifacts this conversation lists, and their current version").option(...conversation).action(async (opts, ctx) => listArtifacts(ctx, await conversationOf(ctx.service, opts.conversation)));
|
|
212
245
|
splatesArtifact.command("show").use(repository({ needs: "own" })).description("every version of an artifact, and which is current").arg("name", z2.string(), { description: "the artifact's name" }).option(...conversation).action(async (opts, ctx) => listArtifacts(ctx, await conversationOf(ctx.service, opts.conversation), opts.name));
|
|
213
246
|
|
|
214
|
-
// budget.ts
|
|
215
|
-
import {
|
|
216
|
-
endBudgetRaise,
|
|
217
|
-
raiseBudget,
|
|
218
|
-
setBudget,
|
|
219
|
-
showBudget
|
|
220
|
-
} from "@stringsync/splates-server/workers";
|
|
221
|
-
import { cli as cli2 } from "webappwiz/cmd";
|
|
222
|
-
import { z as z3 } from "zod";
|
|
223
|
-
var DEV = {
|
|
224
|
-
default: "",
|
|
225
|
-
description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV"
|
|
226
|
-
};
|
|
227
|
-
var JSON_OPTION = { default: false, description: "emit JSON" };
|
|
228
|
-
var LIMIT = z3.enum(["hour", "day", "month"]);
|
|
229
|
-
var LIMIT_ARG = {
|
|
230
|
-
description: "hour, the rate cap on what made workers running at once cost an hour; day; or month"
|
|
231
|
-
};
|
|
232
|
-
var splatesBudget = cli2("budget").description("what the workers a `workerProvider` makes may cost, in dollars: a rate cap per hour on those running at once ($0.25/h by default), and limits on a day ($5) and a month ($75). Past the rate cap, or once a day's or month's limit is spent, no worker is made and the queue says why; reaching the day's or month's stops made workers, whose agents stop at their checkpoint. Each warns at 80%, 90% and 100% by Web Push and on the page's header. Spend is an estimate at each type's list price. Agents' own API spend is not counted here");
|
|
233
|
-
splatesBudget.command("show").description("show each limit: what made workers spent in its window, of how much, and what is left").option("dev", z3.string(), DEV).option("json", z3.boolean(), JSON_OPTION).action((opts, ctx) => showBudget({
|
|
234
|
-
dev: opts.dev,
|
|
235
|
-
json: opts.json,
|
|
236
|
-
log: ctx.log,
|
|
237
|
-
fs: ctx.fs,
|
|
238
|
-
ps: ctx.ps,
|
|
239
|
-
send: ctx.fetch
|
|
240
|
-
}));
|
|
241
|
-
splatesBudget.command("set").description("set a limit, in dollars (dollars an hour for hour), then show the budget").arg("limit", LIMIT, LIMIT_ARG).arg("dollars", z3.string(), {
|
|
242
|
-
description: "like 5 or 0.25, or `default` to give it back to its default"
|
|
243
|
-
}).option("dev", z3.string(), DEV).option("json", z3.boolean(), JSON_OPTION).action((opts, ctx) => setBudget({
|
|
244
|
-
limit: opts.limit,
|
|
245
|
-
dollars: opts.dollars,
|
|
246
|
-
dev: opts.dev,
|
|
247
|
-
json: opts.json,
|
|
248
|
-
log: ctx.log,
|
|
249
|
-
fs: ctx.fs,
|
|
250
|
-
ps: ctx.ps,
|
|
251
|
-
send: ctx.fetch
|
|
252
|
-
}));
|
|
253
|
-
splatesBudget.command("raise").description("add dollars to a limit for a while, on top of what it is set to, then show the budget").arg("limit", LIMIT, LIMIT_ARG).arg("dollars", z3.string(), { description: "how much to add, like 2 or 0.1" }).option("for", z3.string(), {
|
|
254
|
-
default: "1d",
|
|
255
|
-
description: "how long it lasts, in hours, days or weeks, like 24h, 1d or 2w"
|
|
256
|
-
}).option("dev", z3.string(), DEV).option("json", z3.boolean(), JSON_OPTION).action((opts, ctx) => raiseBudget({
|
|
257
|
-
limit: opts.limit,
|
|
258
|
-
dollars: opts.dollars,
|
|
259
|
-
for: opts.for,
|
|
260
|
-
dev: opts.dev,
|
|
261
|
-
json: opts.json,
|
|
262
|
-
log: ctx.log,
|
|
263
|
-
fs: ctx.fs,
|
|
264
|
-
ps: ctx.ps,
|
|
265
|
-
send: ctx.fetch
|
|
266
|
-
}));
|
|
267
|
-
splatesBudget.command("end").description("end a raise now, by the number `show` gives it").arg("id", z3.coerce.number().int(), { description: "the raise's number" }).option("dev", z3.string(), DEV).option("json", z3.boolean(), JSON_OPTION).action((opts, ctx) => endBudgetRaise({
|
|
268
|
-
id: opts.id,
|
|
269
|
-
dev: opts.dev,
|
|
270
|
-
json: opts.json,
|
|
271
|
-
log: ctx.log,
|
|
272
|
-
fs: ctx.fs,
|
|
273
|
-
ps: ctx.ps,
|
|
274
|
-
send: ctx.fetch
|
|
275
|
-
}));
|
|
276
|
-
|
|
277
247
|
// creds.ts
|
|
278
248
|
import { DEFAULT_TIMEOUT, PROVIDERS } from "@stringsync/splates-server/creds";
|
|
279
249
|
import { fail as fail3 } from "@stringsync/splates-server/exit";
|
|
280
250
|
import { repository as repository2 } from "@stringsync/splates-server/records";
|
|
281
251
|
import { untilAfter } from "@stringsync/splates-server/tasks";
|
|
282
|
-
import { cli as
|
|
252
|
+
import { cli as cli2 } from "webappwiz/cmd";
|
|
283
253
|
import { color } from "webappwiz/log";
|
|
284
254
|
import { Duration } from "webappwiz/time";
|
|
285
|
-
import { z as
|
|
286
|
-
var splatesCreds =
|
|
287
|
-
splatesCreds.command("request").use(repository2({ needs: "own" })).description("ask a person for a cred, saying why it is needed and what it will be used for, with markdown instructions for getting it; returns at once, and does nothing when the store already keeps it unless --replace; run from a worktree, it records the task asking").arg("name",
|
|
255
|
+
import { z as z3 } from "zod";
|
|
256
|
+
var splatesCreds = cli2("creds").description("creds an agent needs a person for, such as an API token: request one saying what it is for, with instructions for getting it, which the Creds page of `splates server` shows for the person to paste, then wait for it; the value goes into the system's store (the macOS Keychain, Windows Credential Manager, a Linux secret service), never to the agent, and code reads it through webappwiz/creds");
|
|
257
|
+
splatesCreds.command("request").use(repository2({ needs: "own" })).description("ask a person for a cred, saying why it is needed and what it will be used for, with markdown instructions for getting it; returns at once, and does nothing when the store already keeps it unless --replace; run from a worktree, it records the task asking").arg("name", z3.string(), {
|
|
288
258
|
description: "what it is kept as, an environment variable's name"
|
|
289
|
-
}).option("purpose",
|
|
259
|
+
}).option("purpose", z3.string(), {
|
|
290
260
|
default: "",
|
|
291
261
|
description: "why it is needed and what it will be used for, in a sentence the person reads before getting it"
|
|
292
|
-
}).option("instructions",
|
|
262
|
+
}).option("instructions", z3.string(), {
|
|
293
263
|
default: "",
|
|
294
264
|
description: "how to get it, in markdown: where to go, what to pick, what to paste"
|
|
295
|
-
}).option("device",
|
|
265
|
+
}).option("device", z3.boolean(), {
|
|
296
266
|
default: false,
|
|
297
267
|
description: "keep it for every project on this device, as for a person's own account, rather than this project alone"
|
|
298
|
-
}).option("provider",
|
|
299
|
-
description: "who keeps it: `system`, the machine's own store
|
|
300
|
-
}).option("replace",
|
|
268
|
+
}).option("provider", z3.optional(z3.enum(PROVIDERS)), {
|
|
269
|
+
description: "who keeps it: `system`, the machine's own store"
|
|
270
|
+
}).option("replace", z3.boolean(), {
|
|
301
271
|
default: false,
|
|
302
272
|
description: "ask again although one is kept, since the kept one does not work; --purpose and --instructions left off are those of the last request"
|
|
303
273
|
}).action(async (opts, ctx) => {
|
|
@@ -318,7 +288,7 @@ splatesCreds.command("request").use(repository2({ needs: "own" })).description("
|
|
|
318
288
|
ctx.log.info(`${color.yellow("requested")} ${opts.name} ${color.dim(request.purpose)}`);
|
|
319
289
|
ctx.log.error(`It shows on the Creds page of \`splates server\`. \`splates creds wait ${opts.name}\` until it is saved.`);
|
|
320
290
|
});
|
|
321
|
-
splatesCreds.command("wait").use(repository2({ needs: "own" })).description("block until a person saves the cred requested, or dismisses the request (`dismissed`); gives up with `timeout` rather than waiting forever").arg("name",
|
|
291
|
+
splatesCreds.command("wait").use(repository2({ needs: "own" })).description("block until a person saves the cred requested, or dismisses the request (`dismissed`); gives up with `timeout` rather than waiting forever").arg("name", z3.string(), { description: "the cred requested" }).option("timeout-secs", z3.coerce.number(), {
|
|
322
292
|
default: DEFAULT_TIMEOUT.secs,
|
|
323
293
|
description: "how long to wait before giving up"
|
|
324
294
|
}).action(async (opts, ctx) => {
|
|
@@ -331,7 +301,7 @@ splatesCreds.command("wait").use(repository2({ needs: "own" })).description("blo
|
|
|
331
301
|
});
|
|
332
302
|
ctx.log.info(`${color.green("saved")} ${opts.name}`);
|
|
333
303
|
});
|
|
334
|
-
splatesCreds.command("list").use(repository2({ needs: "read" })).description("the creds requested and still open, with what each is for, and every cred the project's store and the device's keep, by name; never a value").option("json",
|
|
304
|
+
splatesCreds.command("list").use(repository2({ needs: "read" })).description("the creds requested and still open, with what each is for, and every cred the project's store and the device's keep, by name; never a value").option("json", z3.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
|
|
335
305
|
const open = (await ctx.creds.requests()).filter((each) => each.status === "open");
|
|
336
306
|
const kept = await ctx.creds.kept();
|
|
337
307
|
if (opts.json) {
|
|
@@ -364,42 +334,43 @@ import {
|
|
|
364
334
|
hookList,
|
|
365
335
|
hookPause,
|
|
366
336
|
hookRemove,
|
|
337
|
+
hookRun,
|
|
367
338
|
lanesNamed,
|
|
368
339
|
stagesNamed,
|
|
369
340
|
TASK_EVENTS
|
|
370
341
|
} from "@stringsync/splates-server/hooks";
|
|
371
342
|
import { repository as repository3 } from "@stringsync/splates-server/records";
|
|
372
|
-
import { cli as
|
|
373
|
-
import { z as
|
|
374
|
-
var splatesHook =
|
|
375
|
-
splatesHook.command("list").use(repository3({ needs: "read" })).description("every automation set here, by number, with its trigger").option("json",
|
|
376
|
-
splatesHook.command("add").use(repository3({ needs: "tasks" })).description("run something on a task event, or on a schedule with `--every` or `--cron` in place of the event: `--agent <a> --prompt <words>` starts that agent in the main tree, told the event as `[SPLATES EVENT <event> #N]`, or a schedule as `[SPLATES SCHEDULE <number>]`, then your words; `--run <command>` runs it via `sh -c` in the main tree, with $SPLATES_EVENT (`schedule` for one), $SPLATES_TASKS and $SPLATES_TASK set. A schedule that came due while `splates server` was down runs once as it starts").arg("event",
|
|
343
|
+
import { cli as cli3 } from "webappwiz/cmd";
|
|
344
|
+
import { z as z4 } from "zod";
|
|
345
|
+
var splatesHook = cli3("hook").description(`automations: what runs on a task event (${TASK_EVENTS.join(", ")}), as \`splates server\` sees the journal take it, or on a schedule it keeps: an agent told of it; or a shell command. Kept for you in this project's .git/splates, never checked in, since an agent hook spends your tokens; splates.config.ts's \`on\` holds the shell commands a project checks in. None set, nothing runs. The page's Automations tab lists them with when each last ran and for what task`);
|
|
346
|
+
splatesHook.command("list").use(repository3({ needs: "read" })).description("every automation set here, by number, with its trigger").option("json", z4.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => hookList({ hooks: storeOf(ctx), log: ctx.log }, { json: opts.json }));
|
|
347
|
+
splatesHook.command("add").use(repository3({ needs: "tasks" })).description("run something on a task event, or on a schedule with `--every` or `--cron` in place of the event: `--agent <a> --prompt <words>` starts that agent in the main tree, told the event as `[SPLATES EVENT <event> #N]`, or a schedule as `[SPLATES SCHEDULE <number>]`, then your words; `--run <command>` runs it via `sh -c` in the main tree, with $SPLATES_EVENT (`schedule` for one), $SPLATES_TASKS and $SPLATES_TASK set. A schedule that came due while `splates server` was down runs once as it starts").arg("event", z4.string().optional(), {
|
|
377
348
|
description: "the task event, as task.created; none for a schedule"
|
|
378
|
-
}).option("stage",
|
|
349
|
+
}).option("stage", z4.string(), {
|
|
379
350
|
default: "",
|
|
380
351
|
description: "only for tasks in these stages, comma separated: backlog, ready, queued, running"
|
|
381
|
-
}).option("lane",
|
|
352
|
+
}).option("lane", z4.string(), {
|
|
382
353
|
default: "",
|
|
383
354
|
description: "only for tasks in these lanes, by number, comma separated"
|
|
384
|
-
}).option("every",
|
|
355
|
+
}).option("every", z4.string(), {
|
|
385
356
|
default: "",
|
|
386
357
|
description: "run every so often, as 30m, 2h or 1d"
|
|
387
|
-
}).option("cron",
|
|
358
|
+
}).option("cron", z4.string(), {
|
|
388
359
|
default: "",
|
|
389
360
|
description: `run on a five-field cron line, in this machine's time, as "0 9 * * 1-5"`
|
|
390
|
-
}).option("agent",
|
|
361
|
+
}).option("agent", z4.string(), {
|
|
391
362
|
default: "",
|
|
392
363
|
description: "the agent to run, as `splates agents` names it"
|
|
393
|
-
}).option("model",
|
|
364
|
+
}).option("model", z4.string(), {
|
|
394
365
|
default: "",
|
|
395
366
|
description: "its model (default: the agent's own)"
|
|
396
|
-
}).option("effort",
|
|
367
|
+
}).option("effort", z4.string(), {
|
|
397
368
|
default: "",
|
|
398
369
|
description: "its thinking level (default: the agent's own)"
|
|
399
|
-
}).option("prompt",
|
|
370
|
+
}).option("prompt", z4.string(), {
|
|
400
371
|
default: "",
|
|
401
372
|
description: "what the agent is told after the event"
|
|
402
|
-
}).option("run",
|
|
373
|
+
}).option("run", z4.string(), {
|
|
403
374
|
default: "",
|
|
404
375
|
description: "a shell command to run"
|
|
405
376
|
}).action(async (opts, ctx) => {
|
|
@@ -419,34 +390,34 @@ splatesHook.command("add").use(repository3({ needs: "tasks" })).description("run
|
|
|
419
390
|
}
|
|
420
391
|
await hookAdd({ hooks: storeOf(ctx), log: ctx.log }, triggerOf(opts), hook);
|
|
421
392
|
});
|
|
422
|
-
splatesHook.command("edit").use(repository3({ needs: "tasks" })).description("change one of your automations, by its number in `splates hook list`, keeping its number, its pause and its last runs. Each flag changes only what it names, as `splates hook add` takes it: `--event`, `--every` or `--cron` sets what sets it off anew, `--stage` or `--lane` alone narrows its event again (`any` for no narrowing), `--prompt` or `--agent` changes what an agent is told or which agent, and `--run` makes it a shell command. The groomer changes through `splates hook groomer`").arg("id",
|
|
393
|
+
splatesHook.command("edit").use(repository3({ needs: "tasks" })).description("change one of your automations, by its number in `splates hook list`, keeping its number, its pause and its last runs. Each flag changes only what it names, as `splates hook add` takes it: `--event`, `--every` or `--cron` sets what sets it off anew, `--stage` or `--lane` alone narrows its event again (`any` for no narrowing), `--prompt` or `--agent` changes what an agent is told or which agent, and `--run` makes it a shell command. The groomer changes through `splates hook groomer`").arg("id", z4.string(), { description: "its number in `splates hook list`" }).option("event", z4.string(), {
|
|
423
394
|
default: "",
|
|
424
395
|
description: "a task event to run on, as task.created, in place of what sets it off now"
|
|
425
|
-
}).option("stage",
|
|
396
|
+
}).option("stage", z4.string(), {
|
|
426
397
|
default: "",
|
|
427
398
|
description: "only for tasks in these stages, comma separated, or `any`: backlog, ready, queued, running"
|
|
428
|
-
}).option("lane",
|
|
399
|
+
}).option("lane", z4.string(), {
|
|
429
400
|
default: "",
|
|
430
401
|
description: "only for tasks in these lanes, by number, comma separated, or `any`"
|
|
431
|
-
}).option("every",
|
|
402
|
+
}).option("every", z4.string(), {
|
|
432
403
|
default: "",
|
|
433
404
|
description: "run every so often, as 30m, 2h or 1d"
|
|
434
|
-
}).option("cron",
|
|
405
|
+
}).option("cron", z4.string(), {
|
|
435
406
|
default: "",
|
|
436
407
|
description: `run on a five-field cron line, in this machine's time, as "0 9 * * 1-5"`
|
|
437
|
-
}).option("agent",
|
|
408
|
+
}).option("agent", z4.string(), {
|
|
438
409
|
default: "",
|
|
439
410
|
description: "the agent to run, as `splates agents` names it"
|
|
440
|
-
}).option("model",
|
|
411
|
+
}).option("model", z4.string(), {
|
|
441
412
|
default: "",
|
|
442
413
|
description: "its model, with --agent (default: the agent's own)"
|
|
443
|
-
}).option("effort",
|
|
414
|
+
}).option("effort", z4.string(), {
|
|
444
415
|
default: "",
|
|
445
416
|
description: "its thinking level, with --agent (default: the agent's own)"
|
|
446
|
-
}).option("prompt",
|
|
417
|
+
}).option("prompt", z4.string(), {
|
|
447
418
|
default: "",
|
|
448
419
|
description: "what the agent is told after the event"
|
|
449
|
-
}).option("run",
|
|
420
|
+
}).option("run", z4.string(), {
|
|
450
421
|
default: "",
|
|
451
422
|
description: "a shell command to run in place of an agent"
|
|
452
423
|
}).action(async (opts, ctx) => {
|
|
@@ -460,21 +431,24 @@ splatesHook.command("edit").use(repository3({ needs: "tasks" })).description("ch
|
|
|
460
431
|
...pick === undefined ? {} : { pick }
|
|
461
432
|
});
|
|
462
433
|
});
|
|
463
|
-
splatesHook.command("remove").use(repository3({ needs: "tasks" })).description("take away an automation, by its number in `splates hook list`, or every one on an event").arg("id",
|
|
434
|
+
splatesHook.command("remove").use(repository3({ needs: "tasks" })).description("take away an automation, by its number in `splates hook list`, or every one on an event").arg("id", z4.string(), {
|
|
464
435
|
description: "its number, or a task event, as task.created"
|
|
465
436
|
}).action((opts, ctx) => hookRemove({ hooks: storeOf(ctx), log: ctx.log }, opts.id));
|
|
466
|
-
splatesHook.command("pause").use(repository3({ needs: "tasks" })).description("stop an automation running, keeping it to resume").arg("id",
|
|
467
|
-
splatesHook.command("resume").use(repository3({ needs: "tasks" })).description("let a paused automation run again").arg("id",
|
|
468
|
-
splatesHook.command("
|
|
437
|
+
splatesHook.command("pause").use(repository3({ needs: "tasks" })).description("stop an automation running, keeping it to resume").arg("id", z4.string(), { description: "its number in `splates hook list`" }).action((opts, ctx) => hookPause({ hooks: storeOf(ctx), log: ctx.log }, opts.id, true));
|
|
438
|
+
splatesHook.command("resume").use(repository3({ needs: "tasks" })).description("let a paused automation run again").arg("id", z4.string(), { description: "its number in `splates hook list`" }).action((opts, ctx) => hookPause({ hooks: storeOf(ctx), log: ctx.log }, opts.id, false));
|
|
439
|
+
splatesHook.command("run").use(repository3({ needs: "tasks" })).description("run an automation now, as the Automations page's Run now does, its schedule left as it was: one on a schedule, the groomer or the notes gardener, never one on another task event, which needs that event's tasks. `splates server` starts it at once, or as it starts when it is not running, and lists the run as `by hand`; it runs paused too").arg("id", z4.string(), {
|
|
440
|
+
description: "its number in `splates hook list`, or `groomer` or `notes-gardener`"
|
|
441
|
+
}).action((opts, ctx) => hookRun({ hooks: storeOf(ctx), log: ctx.log }, opts.id));
|
|
442
|
+
splatesHook.command("groomer").use(repository3({ needs: "tasks" })).description("turn on the project's groomer, built-in automation `groomer`, or set the agent it runs as, as the Automations page does: on each task deferred to the backlog, and the backlog as `splates server` starts, a conversation of its own per run, archived as the run ends, organizes each task in order and marks it ready, never doing it. `--off` turns it off, retiring its conversations").option("agent", z4.string(), {
|
|
469
443
|
default: "",
|
|
470
444
|
description: "the agent it runs as, as `splates agents` names it"
|
|
471
|
-
}).option("model",
|
|
445
|
+
}).option("model", z4.string(), {
|
|
472
446
|
default: "",
|
|
473
447
|
description: "its model (default: the agent's own)"
|
|
474
|
-
}).option("effort",
|
|
448
|
+
}).option("effort", z4.string(), {
|
|
475
449
|
default: "",
|
|
476
450
|
description: "its thinking level (default: the agent's own)"
|
|
477
|
-
}).option("off",
|
|
451
|
+
}).option("off", z4.boolean(), {
|
|
478
452
|
default: false,
|
|
479
453
|
description: "turn it off"
|
|
480
454
|
}).action(async (opts, ctx) => {
|
|
@@ -484,22 +458,22 @@ splatesHook.command("groomer").use(repository3({ needs: "tasks" })).description(
|
|
|
484
458
|
}
|
|
485
459
|
await hookGroomer({ hooks: storeOf(ctx), log: ctx.log }, pick ?? null);
|
|
486
460
|
});
|
|
487
|
-
splatesHook.command("gardener").use(repository3({ needs: "tasks" })).description("turn on the notes gardener, built-in automation `notes-gardener`, or set the agent it runs as and its schedule, as the Automations page does: on each run, an agent reads each project note no open task names by its path, checks it against the code and the other notes, and drafts the fixes in one task, `notes-garden`: rewriting what went stale, merging notes that say one thing, splitting one that says several, settling a contradiction the code settles and sharpening descriptions, then asks you to review the change rather than merge it, asking about any contradiction the code does not settle. No run starts while `notes-garden` is open. Off by default, since each run reads code for every note on your tokens; it runs every 7d until you set `--every` or `--cron`. `--off` turns it off").option("agent",
|
|
461
|
+
splatesHook.command("gardener").use(repository3({ needs: "tasks" })).description("turn on the notes gardener, built-in automation `notes-gardener`, or set the agent it runs as and its schedule, as the Automations page does: on each run, an agent reads each project note no open task names by its path, checks it against the code and the other notes, and drafts the fixes in one task, `notes-garden`: rewriting what went stale, merging notes that say one thing, splitting one that says several, settling a contradiction the code settles and sharpening descriptions, then asks you to review the change rather than merge it, asking about any contradiction the code does not settle. No run starts while `notes-garden` is open. Off by default, since each run reads code for every note on your tokens; it runs every 7d until you set `--every` or `--cron`. `--off` turns it off").option("agent", z4.string(), {
|
|
488
462
|
default: "",
|
|
489
463
|
description: "the agent it runs as, as `splates agents` names it"
|
|
490
|
-
}).option("model",
|
|
464
|
+
}).option("model", z4.string(), {
|
|
491
465
|
default: "",
|
|
492
466
|
description: "its model (default: the agent's own)"
|
|
493
|
-
}).option("effort",
|
|
467
|
+
}).option("effort", z4.string(), {
|
|
494
468
|
default: "",
|
|
495
469
|
description: "its thinking level (default: the agent's own)"
|
|
496
|
-
}).option("every",
|
|
470
|
+
}).option("every", z4.string(), {
|
|
497
471
|
default: "",
|
|
498
472
|
description: "run every so often, as 1d (default: as it was, or 7d)"
|
|
499
|
-
}).option("cron",
|
|
473
|
+
}).option("cron", z4.string(), {
|
|
500
474
|
default: "",
|
|
501
475
|
description: `run on a five-field cron line, in this machine's time, as "0 9 * * 1"`
|
|
502
|
-
}).option("off",
|
|
476
|
+
}).option("off", z4.boolean(), {
|
|
503
477
|
default: false,
|
|
504
478
|
description: "turn it off"
|
|
505
479
|
}).action(async (opts, ctx) => {
|
|
@@ -553,25 +527,25 @@ import { refuseGroomer } from "@stringsync/splates-server/conversations";
|
|
|
553
527
|
import { UNTRIAGED } from "@stringsync/splates-server/plans";
|
|
554
528
|
import { refuseAgentQueue } from "@stringsync/splates-server/queue";
|
|
555
529
|
import { repository as repository4 } from "@stringsync/splates-server/records";
|
|
556
|
-
import { cli as
|
|
557
|
-
import { z as
|
|
558
|
-
var laneNumber =
|
|
559
|
-
|
|
560
|
-
|
|
530
|
+
import { cli as cli4 } from "webappwiz/cmd";
|
|
531
|
+
import { z as z5 } from "zod";
|
|
532
|
+
var laneNumber = z5.union([
|
|
533
|
+
z5.literal("backlog").transform(() => BACKLOG_LANE2),
|
|
534
|
+
z5.coerce.number().int().nonnegative()
|
|
561
535
|
]);
|
|
562
|
-
var splatesLane =
|
|
563
|
-
splatesLane.command("list").use(repository4({ needs: "read" })).description("every lane: its name, its queued tasks in order, the started tasks on it, its next task, and what blocks that in other lanes").option("json",
|
|
564
|
-
splatesLane.command("show").use(repository4({ needs: "read" })).description("one lane step by step, and what its agent does next: start the next task, or wait for the one blocking it in another lane to land first; `untriaged` lists the tasks in no lane, to place in lanes").arg("lane",
|
|
536
|
+
var splatesLane = cli4("lane").description("lanes: named groups of tasks, for you to see related work together on the board; queue one whole with `splates lane queue`. Backlog (lane 0) is permanent and holds unmatched deferred work. Untriaged is groomed into lanes and cannot be queued whole");
|
|
537
|
+
splatesLane.command("list").use(repository4({ needs: "read" })).description("every lane: its name, its queued tasks in order, the started tasks on it, its next task, and what blocks that in other lanes").option("json", z5.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => laneList(ctx, { json: opts.json }));
|
|
538
|
+
splatesLane.command("show").use(repository4({ needs: "read" })).description("one lane step by step, and what its agent does next: start the next task, or wait for the one blocking it in another lane to land first; `untriaged` lists the tasks in no lane, to place in lanes").arg("lane", z5.union([z5.literal(UNTRIAGED).transform(() => null), laneNumber], {
|
|
565
539
|
error: "pass a lane's number, backlog, or untriaged"
|
|
566
|
-
}), { description: "lane number, `backlog`, or `untriaged`" }).option("json",
|
|
567
|
-
splatesLane.command("add").use(repository4({ needs: "tasks" })).description("start an empty lane, after the others").arg("name",
|
|
540
|
+
}), { description: "lane number, `backlog`, or `untriaged`" }).option("json", z5.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => laneShow(ctx, opts.lane, { json: opts.json }));
|
|
541
|
+
splatesLane.command("add").use(repository4({ needs: "tasks" })).description("start an empty lane, after the others").arg("name", z5.string(), {
|
|
568
542
|
description: "what its tasks are for, like `Licensing`"
|
|
569
543
|
}).action(async (opts, ctx) => {
|
|
570
544
|
await ctx.journal.record("lane add", null, () => laneAdd(ctx, opts.name), (lane) => ({ lane: lane.id, name: lane.name }));
|
|
571
545
|
});
|
|
572
546
|
splatesLane.command("update").use(repository4({ needs: "tasks" })).description("rename a lane, or move it among the others").arg("lane", laneNumber, {
|
|
573
547
|
description: "lane number"
|
|
574
|
-
}).option("name",
|
|
548
|
+
}).option("name", z5.string().optional(), {
|
|
575
549
|
description: "what it is called from now on"
|
|
576
550
|
}).option("position", position, {
|
|
577
551
|
description: "where to move it among the lanes, 1 the first column on the board; the others close up around it"
|
|
@@ -601,11 +575,11 @@ splatesLane.command("unqueue").use(repository4({ needs: "tasks" })).description(
|
|
|
601
575
|
const change = { queued: false };
|
|
602
576
|
await ctx.journal.record("lane update", null, () => laneUpdate(ctx, opts.lane, change), (record) => laneUpdated(opts.lane, record, change));
|
|
603
577
|
});
|
|
604
|
-
splatesLane.command("remove").use(repository4({ needs: "tasks" })).description("remove a lane and its tasks, kept to restore; a lane whose last task merges goes on its own;
|
|
578
|
+
splatesLane.command("remove").use(repository4({ needs: "tasks" })).description("remove a lane and its tasks, kept to restore; a lane whose last task merges goes on its own; agents may remove an empty lane; for one containing tasks, ask with a permission card").arg("lane", laneNumber, {
|
|
605
579
|
description: "lane number"
|
|
606
580
|
}).action((opts, ctx) => {
|
|
607
|
-
|
|
608
|
-
return recordLaneRemove(ctx.journal, ctx.tasks, opts.lane, () => laneRemove(ctx, opts.lane));
|
|
581
|
+
const emptyOnly = ctx.service.ps.env("SPLATES_CONVERSATION") !== undefined || ctx.service.ps.env("SPLATES_STARTED_BY") !== undefined;
|
|
582
|
+
return recordLaneRemove(ctx.journal, ctx.tasks, opts.lane, () => laneRemove(ctx, opts.lane, { emptyOnly }));
|
|
609
583
|
});
|
|
610
584
|
|
|
611
585
|
// notes.ts
|
|
@@ -627,11 +601,11 @@ import {
|
|
|
627
601
|
REACHES
|
|
628
602
|
} from "@stringsync/splates-server/projects";
|
|
629
603
|
import { repository as repository5 } from "@stringsync/splates-server/records";
|
|
630
|
-
import { cli as
|
|
631
|
-
import { z as
|
|
632
|
-
var splatesProjects =
|
|
633
|
-
splatesProjects.command("list").use(repository5({ needs: "read" })).description("print each other project: its slug, its root, what it may do here, and what this one may do there").option("json",
|
|
634
|
-
splatesProjects.command("grant").use(repository5({ needs: "own" })).description("let agents of another project do as much as `read`, `queue` or `tasks` here, each including those before it; the user's call, so an agent runs it only when the user asks it to").arg("project",
|
|
604
|
+
import { cli as cli5 } from "webappwiz/cmd";
|
|
605
|
+
import { z as z6 } from "zod";
|
|
606
|
+
var splatesProjects = cli5("projects").description("the other splates projects on this machine, and what agents of each may do in the others through splates: `read` their tasks and lanes; add tasks to their untriaged lane with `queue`, as every project may until granted otherwise; or run their queue, lanes and hooks with `tasks`. Any repo command runs on another project as `splates --project <slug> <command>`, refused where it needs more than that project granted").fallback("list");
|
|
607
|
+
splatesProjects.command("list").use(repository5({ needs: "read" })).description("print each other project: its slug, its root, what it may do here, and what this one may do there").option("json", z6.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => listReaches(ctx, { json: opts.json }));
|
|
608
|
+
splatesProjects.command("grant").use(repository5({ needs: "own" })).description("let agents of another project do as much as `read`, `queue` or `tasks` here, each including those before it; the user's call, so an agent runs it only when the user asks it to").arg("project", z6.string(), { description: "the other project's slug" }).arg("reach", z6.enum(REACHES), {
|
|
635
609
|
description: "read, queue or tasks"
|
|
636
610
|
}).action((opts, ctx) => grantReach(ctx, opts.project, opts.reach));
|
|
637
611
|
|
|
@@ -646,308 +620,211 @@ import {
|
|
|
646
620
|
refuseAgentQueue as refuseAgentQueue2
|
|
647
621
|
} from "@stringsync/splates-server/queue";
|
|
648
622
|
import { repository as repository6 } from "@stringsync/splates-server/records";
|
|
649
|
-
import {
|
|
650
|
-
import {
|
|
651
|
-
|
|
652
|
-
splatesQueue
|
|
653
|
-
splatesQueue.command("
|
|
623
|
+
import { setQueuePaused } from "@stringsync/splates-server/workers";
|
|
624
|
+
import { cli as cli6 } from "webappwiz/cmd";
|
|
625
|
+
import { z as z7 } from "zod";
|
|
626
|
+
var splatesQueue = cli6("queue").description("the queue: the tasks a worker of `splates server` starts as soon as it has a slot free, each after every task it is blocked by, otherwise in the order you set. Only you queue a task, from the backlog or groomed, and an agent asks you to with a queue card; one that names no model runs on the roster's first agent");
|
|
627
|
+
splatesQueue.command("list").use(repository6({ needs: "read" })).description("the queue in the order workers start it, with each task's model and what it waits on: the open tasks it is blocked by, or those removed without merging").option("json", z7.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => queueList(ctx, { json: opts.json }));
|
|
628
|
+
splatesQueue.command("add").use(repository6({ needs: "tasks" })).description("queue tasks for a worker to start once nothing they are blocked by is open; refused for one already under way").rest("ids", z7.coerce.number().int().positive(), {
|
|
654
629
|
description: "task numbers"
|
|
655
630
|
}).action(async (opts, ctx) => {
|
|
656
631
|
refuseAgentQueue2(ctx.service, "queue add");
|
|
657
632
|
await refuseGroomer2(ctx.service, "queue add");
|
|
658
633
|
await ctx.journal.record("queue add", await here(ctx), () => queueAdd(ctx, opts.ids), (queued) => ({ tasks: tasksNamed(queued) }));
|
|
659
634
|
});
|
|
660
|
-
splatesQueue.command("remove").use(repository6({ needs: "tasks" })).description("take tasks out of the queue, back to ready when they name a model, else the backlog; the tasks stay").rest("ids",
|
|
635
|
+
splatesQueue.command("remove").use(repository6({ needs: "tasks" })).description("take tasks out of the queue, back to ready when they name a model, else the backlog; the tasks stay").rest("ids", z7.coerce.number().int().positive(), {
|
|
661
636
|
description: "task numbers"
|
|
662
637
|
}).action(async (opts, ctx) => {
|
|
663
638
|
refuseAgentQueue2(ctx.service, "queue remove");
|
|
664
639
|
await ctx.journal.record("queue remove", await here(ctx), () => queueRemove(ctx, opts.ids), (removed) => ({ tasks: tasksNamed(removed) }));
|
|
665
640
|
});
|
|
666
|
-
splatesQueue.command("move").use(repository6({ needs: "tasks" })).description("move a queued task to another place in the queue, from 1 for the next to start; what it is blocked by still goes first. Then lists the queue").arg("id",
|
|
641
|
+
splatesQueue.command("move").use(repository6({ needs: "tasks" })).description("move a queued task to another place in the queue, from 1 for the next to start; what it is blocked by still goes first. Then lists the queue").arg("id", z7.coerce.number().int().positive(), { description: "task number" }).arg("position", z7.coerce.number().int().positive(), {
|
|
667
642
|
description: "where in the queue to put it, from 1"
|
|
668
643
|
}).action(async (opts, ctx) => {
|
|
669
644
|
refuseAgentQueue2(ctx.service, "queue move");
|
|
670
645
|
return queueMove(ctx, opts.id, opts.position);
|
|
671
646
|
});
|
|
647
|
+
for (const command of ["pause", "resume"]) {
|
|
648
|
+
splatesQueue.command(command).use(repository6({ needs: "tasks" })).description(command === "pause" ? "pause automatic starts across the server, leaving running agents to finish and the person's own agents able to start; only the person may pause" : "resume automatic starts across the server; only the person may resume").option("dev", z7.string(), {
|
|
649
|
+
default: "",
|
|
650
|
+
description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one from SPLATES_DEV"
|
|
651
|
+
}).action(async (opts, ctx) => {
|
|
652
|
+
refuseAgentQueue2(ctx.service, `queue ${command}`);
|
|
653
|
+
await setQueuePaused({
|
|
654
|
+
dev: opts.dev,
|
|
655
|
+
paused: command === "pause",
|
|
656
|
+
log: ctx.log,
|
|
657
|
+
fs: ctx.fs,
|
|
658
|
+
ps: ctx.ps,
|
|
659
|
+
send: ctx.fetch
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
}
|
|
672
663
|
|
|
673
|
-
//
|
|
674
|
-
import {
|
|
675
|
-
|
|
676
|
-
CoordinatorBox as CoordinatorBox2,
|
|
677
|
-
coordinatorMoved,
|
|
678
|
-
deployKey,
|
|
679
|
-
machineMove,
|
|
680
|
-
neededCreds
|
|
681
|
-
} from "@stringsync/splates-server/coordinator";
|
|
664
|
+
// replica.ts
|
|
665
|
+
import { dirname as dirname2 } from "node:path";
|
|
666
|
+
import { loadConfig as loadConfig2 } from "@stringsync/splates-server/config";
|
|
682
667
|
import { SYSTEM_STORES as SYSTEM_STORES2 } from "@stringsync/splates-server/creds";
|
|
683
|
-
import { fail as fail6 } from "@stringsync/splates-server/exit";
|
|
684
|
-
import {
|
|
685
|
-
DEFAULT_PORT,
|
|
686
|
-
RUNNING,
|
|
687
|
-
serveDev
|
|
688
|
-
} from "@stringsync/splates-server/server";
|
|
689
|
-
import { LOCAL_SLOTS } from "@stringsync/splates-server/workers";
|
|
690
|
-
import { cli as cli9 } from "webappwiz/cmd";
|
|
691
|
-
import { z as z10 } from "zod";
|
|
692
|
-
|
|
693
|
-
// infra.ts
|
|
694
|
-
import { resolve as resolve2 } from "node:path";
|
|
695
|
-
import { commaList as commaList4 } from "@stringsync/splates-server/comma-list";
|
|
696
|
-
import {
|
|
697
|
-
ACCESS_ENVIRONMENT,
|
|
698
|
-
accessEnvironmentExists,
|
|
699
|
-
CoordinatorBox,
|
|
700
|
-
pulumiOrg,
|
|
701
|
-
writeInfraConfig
|
|
702
|
-
} from "@stringsync/splates-server/coordinator";
|
|
703
668
|
import { fail as fail5 } from "@stringsync/splates-server/exit";
|
|
704
|
-
import { infraConfigure } from "@stringsync/splates-server/infra/configure";
|
|
705
|
-
import { secretsEnvironmentUrl } from "@stringsync/splates-server/infra/secrets";
|
|
706
669
|
import { Projects } from "@stringsync/splates-server/projects";
|
|
707
|
-
import {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
|
|
670
|
+
import {
|
|
671
|
+
FORMAT,
|
|
672
|
+
ProjectHome,
|
|
673
|
+
repository as repository7
|
|
674
|
+
} from "@stringsync/splates-server/records";
|
|
675
|
+
import {
|
|
676
|
+
openReplica,
|
|
677
|
+
REPLICA_ACCESS_KEY,
|
|
678
|
+
REPLICA_SECRET_KEY,
|
|
679
|
+
Replicator,
|
|
680
|
+
restoreFromReplica,
|
|
681
|
+
statusLines
|
|
682
|
+
} from "@stringsync/splates-server/replica";
|
|
683
|
+
import { cli as cli7 } from "webappwiz/cmd";
|
|
684
|
+
import { Credentials } from "webappwiz/creds";
|
|
685
|
+
import { z as z8 } from "zod";
|
|
686
|
+
var splatesReplica = cli7("replica").description("the copy of this project's records `splates server` keeps a few seconds behind every change, in the bucket or folder `replica` names in splates.config.ts");
|
|
687
|
+
splatesReplica.command("status").use(repository7({ needs: "read" })).description("print where the replica is, when it last held every change, how many changes wait to reach it and since when, and why it is failing if it is; as of the server's last batch, so it reads the same with the server stopped").option("json", z8.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
|
|
688
|
+
const status = await Replicator.recorded(ctx.storage);
|
|
689
|
+
if (opts.json) {
|
|
690
|
+
ctx.log.info(JSON.stringify(status));
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
ctx.log.info(statusLines(status, ctx.config.replica !== null, Date.now()).join(`
|
|
694
|
+
`));
|
|
695
|
+
});
|
|
696
|
+
splatesReplica.command("restore").description("with `splates server` stopped, rebuild this project's records from the replica `replica` names in splates.config.ts, adopting its origin so the server started on them replicates on into it: how a server moves to another machine or comes back after its box is lost. Stops when the project already has records here, unless --force leaves them aside as state.db.moved. The roster and Auto pick rules are not in the replica; set them up again. A bucket's REPLICA_ACCESS_KEY and REPLICA_SECRET_KEY must be filled on this machine first").option("force", z8.boolean(), {
|
|
697
|
+
default: false,
|
|
698
|
+
description: "restore over the records here, leaving them aside"
|
|
725
699
|
}).action(async (opts, ctx) => {
|
|
726
|
-
const
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
});
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
host: opts.host,
|
|
741
|
-
email: opts.email,
|
|
742
|
-
region: opts.region,
|
|
743
|
-
projects: listed.length === 0 ? null : listed,
|
|
744
|
-
registered: async () => (await Projects.in(new FsStorage2(home, ctx.fs), ctx.fs).list()).filter((project) => !project.gone),
|
|
745
|
-
creds: ctx.creds,
|
|
746
|
-
saved: savedTunnelOf(ctx),
|
|
747
|
-
...org !== null && await accessEnvironmentExists(ctx.ps, org) ? { environment: ACCESS_ENVIRONMENT } : {},
|
|
700
|
+
const root = await mainTree(ctx);
|
|
701
|
+
const projects = Projects.ofMachine(ctx);
|
|
702
|
+
const home = new ProjectHome(root, { ...ctx, projects });
|
|
703
|
+
const stores = ctx.secrets ?? SYSTEM_STORES2;
|
|
704
|
+
const credentials = new Credentials([
|
|
705
|
+
await stores.project(root),
|
|
706
|
+
stores.device
|
|
707
|
+
]);
|
|
708
|
+
const config = await loadConfig2(root, { fs: ctx.fs });
|
|
709
|
+
if (config.replica === null) {
|
|
710
|
+
fail5("usage", "no replica: name one as `replica` in splates.config.ts first");
|
|
711
|
+
}
|
|
712
|
+
const replica = openReplica(config.replica, {
|
|
713
|
+
root,
|
|
748
714
|
fs: ctx.fs,
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
stack: opts.stack
|
|
715
|
+
creds: {
|
|
716
|
+
credentials,
|
|
717
|
+
request: async () => {
|
|
718
|
+
throw new Error(`fill ${REPLICA_ACCESS_KEY} and ${REPLICA_SECRET_KEY} with \`wiz creds\` first`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
756
721
|
});
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
722
|
+
try {
|
|
723
|
+
const done = await restoreFromReplica(replica, {
|
|
724
|
+
target: home,
|
|
725
|
+
fs: ctx.fs,
|
|
726
|
+
format: FORMAT,
|
|
727
|
+
force: opts.force
|
|
728
|
+
});
|
|
729
|
+
ctx.log.info([
|
|
730
|
+
`restored ${done.values} record(s) and ${done.logs} log(s) from ${replica.name}, last caught up on ${done.host}`,
|
|
731
|
+
...done.replaced === null ? [] : [`the records here went aside as ${done.replaced}`],
|
|
732
|
+
`project: ${root} (register its checkout with \`splates init\` if it is new on this machine)`,
|
|
733
|
+
"start `splates server` to serve it and replicate on"
|
|
734
|
+
].join(`
|
|
735
|
+
`));
|
|
736
|
+
} catch (error) {
|
|
737
|
+
fail5("usage", error instanceof Error ? error.message : String(error));
|
|
764
738
|
}
|
|
765
739
|
});
|
|
740
|
+
splatesReplica.command("retire").use(repository7({ needs: "own" })).description("after the server moved elsewhere (stop it and check `splates replica status` says caught up first), leave this project's records aside as state.db.moved, so a splates run here finds no stale records and says where the server went").arg("to", z8.string(), {
|
|
741
|
+
description: "where the server went, as `droplet.example.com`"
|
|
742
|
+
}).action(async (opts, ctx) => {
|
|
743
|
+
const home = new ProjectHome(ctx.git.root, ctx);
|
|
744
|
+
ctx.dispose();
|
|
745
|
+
const aside = await home.moveAside(opts.to);
|
|
746
|
+
ctx.log.info(`records left aside as ${aside}; the server is at ${opts.to}`);
|
|
747
|
+
});
|
|
748
|
+
async function mainTree(ctx) {
|
|
749
|
+
const { exitCode, stdout } = await ctx.ps.spawnCapture([
|
|
750
|
+
"git",
|
|
751
|
+
"-C",
|
|
752
|
+
ctx.ps.cwd(),
|
|
753
|
+
"rev-parse",
|
|
754
|
+
"--path-format=absolute",
|
|
755
|
+
"--git-common-dir"
|
|
756
|
+
]);
|
|
757
|
+
if (exitCode !== 0) {
|
|
758
|
+
fail5("usage", `not a git repository: ${ctx.ps.cwd()}`);
|
|
759
|
+
}
|
|
760
|
+
return dirname2(stdout.trim());
|
|
761
|
+
}
|
|
766
762
|
|
|
767
763
|
// server.ts
|
|
768
|
-
|
|
769
|
-
|
|
764
|
+
import {
|
|
765
|
+
DEFAULT_PORT,
|
|
766
|
+
serveDev
|
|
767
|
+
} from "@stringsync/splates-server/server";
|
|
768
|
+
import { cli as cli8 } from "webappwiz/cmd";
|
|
769
|
+
import { z as z9 } from "zod";
|
|
770
|
+
var splatesServer = cli8("server").description("the splates server: run it on this machine").fallback("run");
|
|
771
|
+
splatesServer.command("run").description("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").option("port", z9.coerce.number(), {
|
|
770
772
|
default: DEFAULT_PORT,
|
|
771
773
|
description: "port to listen on, or the next open one above it"
|
|
772
|
-
}).option("allow-hosts",
|
|
774
|
+
}).option("allow-hosts", z9.string(), {
|
|
773
775
|
default: "",
|
|
774
776
|
description: "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"
|
|
775
|
-
}).option("access-team",
|
|
777
|
+
}).option("access-team", z9.string(), {
|
|
776
778
|
default: "",
|
|
777
779
|
description: "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"
|
|
778
|
-
}).option("access-aud",
|
|
780
|
+
}).option("access-aud", z9.string(), {
|
|
779
781
|
default: "",
|
|
780
782
|
description: "the AUD tag of that Access application"
|
|
781
|
-
}).option("tunnel",
|
|
783
|
+
}).option("tunnel", z9.string(), {
|
|
782
784
|
default: "",
|
|
783
785
|
description: "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"
|
|
784
|
-
}).option("tunnel-name",
|
|
786
|
+
}).option("tunnel-name", z9.string(), {
|
|
785
787
|
default: "",
|
|
786
788
|
description: "the named tunnel cloudflared runs for --tunnel, by default the one its own config names; not with a tunnel provider of splates.config.ts"
|
|
787
|
-
}).option("ports-host",
|
|
789
|
+
}).option("ports-host", z9.string(), {
|
|
788
790
|
default: "",
|
|
789
791
|
description: "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"
|
|
790
|
-
}).option("
|
|
791
|
-
default: LOCAL_SLOTS,
|
|
792
|
-
description: "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"
|
|
793
|
-
}).option("keepalive", z10.boolean(), {
|
|
792
|
+
}).option("keepalive", z9.boolean(), {
|
|
794
793
|
default: false,
|
|
795
794
|
description: "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"
|
|
796
|
-
}).option("slot",
|
|
795
|
+
}).option("slot", z9.boolean(), {
|
|
797
796
|
default: false,
|
|
798
797
|
description: "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"
|
|
799
798
|
}).action((opts, ctx) => serveDev(ctx, opts));
|
|
800
|
-
splatesServer.mount("infra", splatesInfra);
|
|
801
|
-
splatesServer.command("deploy").description("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").option("dry-run", z10.boolean(), {
|
|
802
|
-
default: false,
|
|
803
|
-
description: "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"
|
|
804
|
-
}).option("dev", z10.string(), {
|
|
805
|
-
default: `http://localhost:${DEFAULT_PORT}`,
|
|
806
|
-
description: "this machine's `splates server`, which the deploy stops"
|
|
807
|
-
}).action(async (opts, ctx) => {
|
|
808
|
-
const moved = await machineMove({
|
|
809
|
-
ps: ctx.ps,
|
|
810
|
-
fs: ctx.fs,
|
|
811
|
-
log: ctx.log,
|
|
812
|
-
version: RUNNING,
|
|
813
|
-
dev: opts.dev,
|
|
814
|
-
stores: ctx.secrets ?? SYSTEM_STORES2,
|
|
815
|
-
...ctx.fetch === undefined ? {} : { fetch: ctx.fetch }
|
|
816
|
-
}).run({ dryRun: opts["dry-run"] });
|
|
817
|
-
if (moved.status === "blocked") {
|
|
818
|
-
fail6("blocked", [
|
|
819
|
-
"The box's Parameter Store lacks these. Set each (the Pulumi stack makes them: `pulumi up`), then deploy again:",
|
|
820
|
-
...moved.needs.map((name) => ` ${name}`)
|
|
821
|
-
].join(`
|
|
822
|
-
`), { needs: moved.needs });
|
|
823
|
-
}
|
|
824
|
-
if (moved.status === "deployed") {
|
|
825
|
-
ctx.log.info(`the server runs on the box at ${moved.ipv4}: deploy again without --dry-run to bring it to ${moved.version}`);
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
if (moved.status === "updated") {
|
|
829
|
-
ctx.log.info(`the server on the box at ${moved.ipv4} runs ${moved.version} now`);
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
if (moved.status === "ready") {
|
|
833
|
-
ctx.log.info(`the box is ready to serve ${moved.projects.join(", ")} through ${moved.host}: deploy again without --dry-run`);
|
|
834
|
-
return;
|
|
835
|
-
}
|
|
836
|
-
ctx.log.info([
|
|
837
|
-
`the server runs on the box: https://${moved.host}`,
|
|
838
|
-
`this machine is its worker ${ctx.ps.hostname}, kept up by ${moved.worker}`,
|
|
839
|
-
`its own records are kept aside in ${moved.aside} and each project's .git/splates.moved`
|
|
840
|
-
].join(`
|
|
841
|
-
`));
|
|
842
|
-
});
|
|
843
|
-
splatesServer.command("status").description("print where the server runs, here or deployed to its box, and the box as its cloud has it").option("json", z10.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
|
|
844
|
-
const deployed = await coordinatorMoved({
|
|
845
|
-
fs: ctx.fs,
|
|
846
|
-
ps: ctx.ps,
|
|
847
|
-
home: box(ctx).home
|
|
848
|
-
});
|
|
849
|
-
const standing = await box(ctx).status();
|
|
850
|
-
if (opts.json) {
|
|
851
|
-
ctx.log.info(JSON.stringify({ deployed, box: standing }));
|
|
852
|
-
return;
|
|
853
|
-
}
|
|
854
|
-
ctx.log.info([
|
|
855
|
-
`runs: ${deployed ? "on the box" : "on this machine"}`,
|
|
856
|
-
...standing === null ? [
|
|
857
|
-
"box: none: `splates server infra init` writes the Pulumi project, `pulumi up` there makes it"
|
|
858
|
-
] : [
|
|
859
|
-
`address: ${standing.ipv4}`,
|
|
860
|
-
`instance: ${standing.instanceId} (role ${standing.roleName})`,
|
|
861
|
-
`host: ${standing.host}`,
|
|
862
|
-
"ssh: splates server ssh"
|
|
863
|
-
]
|
|
864
|
-
].join(`
|
|
865
|
-
`));
|
|
866
|
-
});
|
|
867
|
-
splatesServer.command("ssh").description("open a root shell on the box over SSH, with the key its Pulumi stack made, or run the command given there").rest("command", z10.string(), {
|
|
868
|
-
description: "what to run on the box; by default a shell"
|
|
869
|
-
}).passThroughOptions().action(async (opts, ctx) => {
|
|
870
|
-
const standing = await box(ctx).status();
|
|
871
|
-
if (standing === null) {
|
|
872
|
-
fail6("not_found", "no box: `splates server infra init` writes the Pulumi project, `pulumi up` there makes it");
|
|
873
|
-
}
|
|
874
|
-
const { exitCode } = await ctx.ps.spawn(shell(ctx, standing).login(opts.command));
|
|
875
|
-
ctx.ps.exit(exitCode);
|
|
876
|
-
});
|
|
877
|
-
splatesServer.command("destroy").description("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").option("yes", z10.boolean(), {
|
|
878
|
-
default: false,
|
|
879
|
-
description: "destroy without asking"
|
|
880
|
-
}).action(async (opts, ctx) => {
|
|
881
|
-
const infra = await box(ctx).infra();
|
|
882
|
-
if (infra === null) {
|
|
883
|
-
fail6("not_found", "no infra project here: nothing to destroy, since `splates server infra init` has not written one");
|
|
884
|
-
}
|
|
885
|
-
const standing = await box(ctx).status();
|
|
886
|
-
const what = standing === null ? `what the stack in ${infra.dir} made` : `the box at ${standing.ipv4} (${standing.instanceId}) and everything the stack in ${infra.dir} made`;
|
|
887
|
-
if (!opts.yes) {
|
|
888
|
-
fail6("usage", `this destroys ${what}, and anything only on the box is lost: run again with --yes`, { dir: infra.dir, box: standing });
|
|
889
|
-
}
|
|
890
|
-
ctx.log.info(`Destroying ${what}, a few minutes…`);
|
|
891
|
-
await box(ctx).destroy();
|
|
892
|
-
ctx.log.info("Destroyed. The stack and its config stay for making it again.");
|
|
893
|
-
});
|
|
894
|
-
splatesServer.command("needs").description("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").rest("roots", z10.string(), {
|
|
895
|
-
description: "each project's clone on this machine"
|
|
896
|
-
}).action(async (opts, ctx) => {
|
|
897
|
-
const missing = await neededCreds(ctx.secrets ?? SYSTEM_STORES2, opts.roots);
|
|
898
|
-
for (const name of missing) {
|
|
899
|
-
ctx.log.info(name);
|
|
900
|
-
}
|
|
901
|
-
});
|
|
902
|
-
splatesServer.command("deploy-key").description("kept for `splates server deploy`, which runs it on the box: print the deploy key of the project whose clone is <root>").arg("root", z10.string(), {
|
|
903
|
-
description: "the project's clone on this machine"
|
|
904
|
-
}).action(async (opts, ctx) => {
|
|
905
|
-
const key = await deployKey(ctx.secrets ?? SYSTEM_STORES2, opts.root);
|
|
906
|
-
if (key === undefined) {
|
|
907
|
-
fail6("not_found", `no deploy key kept for ${opts.root}`);
|
|
908
|
-
}
|
|
909
|
-
ctx.log.info(key);
|
|
910
|
-
});
|
|
911
|
-
function box(ctx) {
|
|
912
|
-
return new CoordinatorBox2({ ps: ctx.ps, fs: ctx.fs });
|
|
913
|
-
}
|
|
914
|
-
function shell(ctx, standing) {
|
|
915
|
-
return new BoxShell({
|
|
916
|
-
ps: ctx.ps,
|
|
917
|
-
ipv4: standing.ipv4,
|
|
918
|
-
key: standing.key,
|
|
919
|
-
knownHosts: `${box(ctx).home}/coordinator/known_hosts`
|
|
920
|
-
});
|
|
921
|
-
}
|
|
922
799
|
|
|
923
800
|
// setup.ts
|
|
924
|
-
import { dirname } from "node:path";
|
|
801
|
+
import { dirname as dirname3 } from "node:path";
|
|
925
802
|
import { SYSTEM_STORES as SYSTEM_STORES3 } from "@stringsync/splates-server/creds";
|
|
926
|
-
import { fail as
|
|
803
|
+
import { fail as fail6 } from "@stringsync/splates-server/exit";
|
|
927
804
|
import { splatesHome as splatesHome2 } from "@stringsync/splates-server/models";
|
|
928
805
|
import { Projects as Projects2 } from "@stringsync/splates-server/projects";
|
|
806
|
+
import {
|
|
807
|
+
openRepository
|
|
808
|
+
} from "@stringsync/splates-server/records";
|
|
809
|
+
import { Replicator as Replicator2 } from "@stringsync/splates-server/replica";
|
|
929
810
|
import {
|
|
930
811
|
SETUP_AREAS,
|
|
931
812
|
Setup,
|
|
932
813
|
setupProjectAt
|
|
933
814
|
} from "@stringsync/splates-server/setup";
|
|
934
|
-
import {
|
|
935
|
-
fetchWorkerBudget,
|
|
936
|
-
fetchWorkers,
|
|
937
|
-
putWorkerLimit
|
|
938
|
-
} from "@stringsync/splates-server/workers";
|
|
815
|
+
import { fetchWorkers } from "@stringsync/splates-server/workers";
|
|
939
816
|
import { color as color2 } from "webappwiz/log";
|
|
940
817
|
async function setup(deps, opts) {
|
|
941
818
|
if (opts.step === "" && (opts.skip || opts.unskip || opts.undo)) {
|
|
942
|
-
|
|
819
|
+
fail6("usage", "--skip and --unskip name a step, --undo an area", {});
|
|
943
820
|
}
|
|
944
821
|
if (opts.undo && (opts.skip || opts.unskip)) {
|
|
945
|
-
|
|
822
|
+
fail6("usage", "--undo names an area, not a step to skip", {
|
|
946
823
|
area: opts.step
|
|
947
824
|
});
|
|
948
825
|
}
|
|
949
826
|
if (opts.skip && opts.unskip) {
|
|
950
|
-
|
|
827
|
+
fail6("usage", "--skip or --unskip, not both", { step: opts.step });
|
|
951
828
|
}
|
|
952
829
|
const machine = await setupOf(deps);
|
|
953
830
|
const { log } = deps;
|
|
@@ -994,17 +871,20 @@ async function setupOf(deps) {
|
|
|
994
871
|
device: secrets.device,
|
|
995
872
|
fetch,
|
|
996
873
|
workers: deps.ps.env("SPLATES_DEV") ? () => fetchWorkers({ dev: "", log: deps.log, ps: deps.ps }) : undefined,
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
log: deps.log,
|
|
1002
|
-
ps: deps.ps
|
|
1003
|
-
})
|
|
1004
|
-
} : undefined,
|
|
1005
|
-
project: root === null ? undefined : setupProjectAt(root, { fs, ps })
|
|
874
|
+
project: root === null ? undefined : setupProjectAt(root, { fs, ps }, {
|
|
875
|
+
replica: () => withRecords(deps, root, ({ storage }) => Replicator2.recorded(storage)),
|
|
876
|
+
kept: (name) => withRecords(deps, root, async ({ creds }) => await creds.credentials.get(name) !== undefined)
|
|
877
|
+
})
|
|
1006
878
|
}, SETUP_AREAS);
|
|
1007
879
|
}
|
|
880
|
+
async function withRecords(deps, root, read) {
|
|
881
|
+
const records = await openRepository(deps, { at: root });
|
|
882
|
+
try {
|
|
883
|
+
return await read(records);
|
|
884
|
+
} finally {
|
|
885
|
+
records.dispose();
|
|
886
|
+
}
|
|
887
|
+
}
|
|
1008
888
|
async function repoRoot(ps) {
|
|
1009
889
|
const { exitCode, stdout } = await ps.spawnCapture([
|
|
1010
890
|
"git",
|
|
@@ -1014,7 +894,7 @@ async function repoRoot(ps) {
|
|
|
1014
894
|
"--path-format=absolute",
|
|
1015
895
|
"--git-common-dir"
|
|
1016
896
|
]);
|
|
1017
|
-
return exitCode === 0 ?
|
|
897
|
+
return exitCode === 0 ? dirname3(stdout.trim()) : null;
|
|
1018
898
|
}
|
|
1019
899
|
var MARKS = {
|
|
1020
900
|
done: color2.green("✓"),
|
|
@@ -1091,10 +971,10 @@ function printUndo(log, plan) {
|
|
|
1091
971
|
|
|
1092
972
|
// slots.ts
|
|
1093
973
|
import { Slots } from "@stringsync/splates-server/ports";
|
|
1094
|
-
import { cli as
|
|
1095
|
-
import { z as
|
|
1096
|
-
var splatesSlots =
|
|
1097
|
-
splatesSlots.command("list").description("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").option("json",
|
|
974
|
+
import { cli as cli9 } from "webappwiz/cmd";
|
|
975
|
+
import { z as z10 } from "zod";
|
|
976
|
+
var splatesSlots = cli9("slots").description("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");
|
|
977
|
+
splatesSlots.command("list").description("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").option("json", z10.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
|
|
1098
978
|
const held = await Slots.ofMachine(ctx).status();
|
|
1099
979
|
if (opts.json) {
|
|
1100
980
|
ctx.log.info(JSON.stringify(held.map(({ slot, tree, pid, orphaned }) => ({
|
|
@@ -1137,19 +1017,19 @@ import {
|
|
|
1137
1017
|
tasksNamed as tasksNamed2,
|
|
1138
1018
|
taskUpdate
|
|
1139
1019
|
} from "@stringsync/splates-server/board";
|
|
1140
|
-
import { commaList as
|
|
1020
|
+
import { commaList as commaList4 } from "@stringsync/splates-server/comma-list";
|
|
1141
1021
|
import { refuseGroomer as refuseGroomer3 } from "@stringsync/splates-server/conversations";
|
|
1142
|
-
import { fail as
|
|
1022
|
+
import { fail as fail7 } from "@stringsync/splates-server/exit";
|
|
1143
1023
|
import { repository as repository8 } from "@stringsync/splates-server/records";
|
|
1144
1024
|
import {
|
|
1145
1025
|
DEFAULT_TIMEOUT as DEFAULT_TIMEOUT2,
|
|
1146
1026
|
taskWait,
|
|
1147
1027
|
untilAfter as untilAfter2
|
|
1148
1028
|
} from "@stringsync/splates-server/tasks";
|
|
1149
|
-
import { cli as
|
|
1029
|
+
import { cli as cli10 } from "webappwiz/cmd";
|
|
1150
1030
|
import { Duration as Duration2 } from "webappwiz/time";
|
|
1151
|
-
import { z as
|
|
1152
|
-
var splatesTask =
|
|
1031
|
+
import { z as z11 } from "zod";
|
|
1032
|
+
var splatesTask = cli10("task").description("the numbered tasks: defer work for later instead of growing the task you are in, and `splates add <slug> --task <n>` starts one; `merge` recommends the next one when a task lands. The person puts tasks in `splates queue`, for workers to start");
|
|
1153
1033
|
async function projectFrom(ctx) {
|
|
1154
1034
|
if (ctx.elsewhere === null) {
|
|
1155
1035
|
return;
|
|
@@ -1157,36 +1037,34 @@ async function projectFrom(ctx) {
|
|
|
1157
1037
|
return (await ctx.projects.list()).find((each) => each.root === ctx.git.root)?.slug;
|
|
1158
1038
|
}
|
|
1159
1039
|
function adding(name, description, ready) {
|
|
1160
|
-
splatesTask.command(name).use(repository8({ needs: "queue" })).description(description).arg("subject",
|
|
1040
|
+
splatesTask.command(name).use(repository8({ needs: "queue" })).description(description).arg("subject", z11.string(), {
|
|
1161
1041
|
description: "what is left to do, in a line"
|
|
1162
|
-
}).arg("text",
|
|
1042
|
+
}).arg("text", z11.string(), {
|
|
1163
1043
|
default: "",
|
|
1164
1044
|
description: "whatever more there is to say about it, in markdown"
|
|
1165
|
-
}).option("slug",
|
|
1045
|
+
}).option("slug", z11.string(), {
|
|
1166
1046
|
description: "a short name for the task that takes it (lowercase-with-dashes), naming its branch, worktree and conversation: a few words for what it changes, not the subject cut short"
|
|
1167
1047
|
}).option("position", position, {
|
|
1168
1048
|
description: "where it goes in the list, 1 at the top, pushing those from there down (default: the bottom)"
|
|
1169
|
-
}).
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
description: "numbers of the tasks blocking it, comma separated: each has to merge before this one starts"
|
|
1175
|
-
}).option("lane", z12.string(), {
|
|
1049
|
+
}).repeated("file", z11.string(), {
|
|
1050
|
+
description: "files to attach, comma separated or the flag repeated: copied under .git/splates/tasks/<n>/"
|
|
1051
|
+
}).repeated("blocked-by", z11.string(), {
|
|
1052
|
+
description: "numbers of the tasks blocking it, comma separated or the flag repeated: each has to merge before this one starts"
|
|
1053
|
+
}).option("lane", z11.string(), {
|
|
1176
1054
|
default: "",
|
|
1177
1055
|
description: "the lane it goes in, at the bottom: a lane's number, `backlog` for the permanent catch-all, or `new` for one of its own (default: none, untriaged)"
|
|
1178
|
-
}).option("agent",
|
|
1056
|
+
}).option("agent", z11.string(), {
|
|
1179
1057
|
default: "",
|
|
1180
1058
|
description: "the agent on the roster (`splates agents`) that should work it, which Start on the `splates server` board offers first"
|
|
1181
|
-
}).option("model",
|
|
1059
|
+
}).option("model", z11.string(), {
|
|
1182
1060
|
default: "",
|
|
1183
1061
|
description: "with --agent: the model, by its id or name on the roster"
|
|
1184
|
-
}).option("effort",
|
|
1062
|
+
}).option("effort", z11.string(), {
|
|
1185
1063
|
default: "",
|
|
1186
1064
|
description: "with --agent: the thinking level, one the agent offers (default: the agent's own)"
|
|
1187
1065
|
}).action(async (opts, ctx) => {
|
|
1188
1066
|
if (ctx.elsewhere !== null && opts.lane !== "") {
|
|
1189
|
-
|
|
1067
|
+
fail7("not_granted", "a task from another project goes in its untriaged lane, for whoever orchestrates it there to place: leave --lane off", { subject: opts.subject });
|
|
1190
1068
|
}
|
|
1191
1069
|
const from = await here(ctx);
|
|
1192
1070
|
const assignee = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
|
|
@@ -1199,8 +1077,8 @@ function adding(name, description, ready) {
|
|
|
1199
1077
|
slug: opts.slug,
|
|
1200
1078
|
text: opts.text,
|
|
1201
1079
|
position: opts.position,
|
|
1202
|
-
files:
|
|
1203
|
-
blockedBy: taskIds(opts["blocked-by"]),
|
|
1080
|
+
files: commaList4(opts.file.join(",")),
|
|
1081
|
+
blockedBy: taskIds(opts["blocked-by"].join(",")),
|
|
1204
1082
|
lane: laneIn(opts.lane) ?? null,
|
|
1205
1083
|
assignee,
|
|
1206
1084
|
origin,
|
|
@@ -1211,10 +1089,10 @@ function adding(name, description, ready) {
|
|
|
1211
1089
|
}
|
|
1212
1090
|
adding("add", "queue a task to do later and move on, groomed, so it goes in ready: you checked no task already asks it, and linked what blocks it; run from a worktree, it records the task it came up in", true);
|
|
1213
1091
|
adding("defer", "queue a task as it is, rough, in the backlog for whoever grooms it: to check for one that already asks it, link what blocks it, and mark it ready; run from a worktree, it records the task it came up in", false);
|
|
1214
|
-
splatesTask.command("list").use(repository8({ needs: "read" })).description("every queued task, top of the list first, with the task it came from and the started one that took it up").option("json",
|
|
1092
|
+
splatesTask.command("list").use(repository8({ needs: "read" })).description("every queued task, top of the list first, with the task it came from and the started one that took it up").option("json", z11.boolean(), { default: false, description: "emit JSON" }).option("open", z11.boolean(), {
|
|
1215
1093
|
default: false,
|
|
1216
1094
|
description: "only tasks no started one has taken up: the ones free to pick up"
|
|
1217
|
-
}).option("ready",
|
|
1095
|
+
}).option("ready", z11.boolean(), {
|
|
1218
1096
|
default: false,
|
|
1219
1097
|
description: "only tasks free to start now: none has taken them, and nothing blocking them is still open"
|
|
1220
1098
|
}).action(async (opts, ctx) => taskList(ctx, {
|
|
@@ -1223,58 +1101,54 @@ splatesTask.command("list").use(repository8({ needs: "read" })).description("eve
|
|
|
1223
1101
|
ready: opts.ready,
|
|
1224
1102
|
project: await projectFrom(ctx)
|
|
1225
1103
|
}));
|
|
1226
|
-
splatesTask.command("show").use(repository8({ needs: "read" })).description("one task in full: where it stands, its files, and its detail").arg("id",
|
|
1104
|
+
splatesTask.command("show").use(repository8({ needs: "read" })).description("one task in full: where it stands, its files, and its detail").arg("id", z11.coerce.number().int().positive(), { description: "task number" }).option("json", z11.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => taskShow(ctx, opts.id, {
|
|
1227
1105
|
json: opts.json,
|
|
1228
1106
|
project: await projectFrom(ctx),
|
|
1229
1107
|
others: (await ctx.projects.list()).filter((project) => project.root !== ctx.git.root).map((project) => project.slug)
|
|
1230
1108
|
}));
|
|
1231
|
-
splatesTask.command("update").use(repository8({ needs: "tasks" })).description("say what a queued task is in other words, move it up or down the list, or attach and drop files; it keeps its number").arg("id",
|
|
1109
|
+
splatesTask.command("update").use(repository8({ needs: "tasks" })).description("say what a queued task is in other words, move it up or down the list, or attach and drop files; it keeps its number").arg("id", z11.coerce.number().int().positive(), { description: "task number" }).arg("text", z11.string(), {
|
|
1232
1110
|
default: "",
|
|
1233
1111
|
description: "whatever more there is to say about it, in markdown; leave out to keep the detail"
|
|
1234
|
-
}).option("subject",
|
|
1112
|
+
}).option("subject", z11.string(), {
|
|
1235
1113
|
default: "",
|
|
1236
1114
|
description: "what is left to do, in a line; leave out to keep it"
|
|
1237
|
-
}).option("slug",
|
|
1115
|
+
}).option("slug", z11.string(), {
|
|
1238
1116
|
default: "",
|
|
1239
1117
|
description: "a new name for the task that takes it (lowercase-with-dashes); leave out to keep it"
|
|
1240
1118
|
}).option("position", position, {
|
|
1241
1119
|
description: "where to move it in the list, 1 at the top; the others close up around it"
|
|
1242
|
-
}).
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
description: "
|
|
1248
|
-
}).
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
}).option("remove-blocked-by", z12.string(), {
|
|
1252
|
-
default: "",
|
|
1253
|
-
description: "numbers of tasks no longer blocking it, comma separated"
|
|
1254
|
-
}).option("lane", z12.string(), {
|
|
1120
|
+
}).repeated("file", z11.string(), {
|
|
1121
|
+
description: "files to attach, comma separated or the flag repeated"
|
|
1122
|
+
}).repeated("remove-file", z11.string(), {
|
|
1123
|
+
description: "attached files to drop, comma separated or the flag repeated, by path or stored name"
|
|
1124
|
+
}).repeated("blocked-by", z11.string(), {
|
|
1125
|
+
description: "numbers of more tasks blocking it, comma separated or the flag repeated: each has to merge before this one starts; refused when that makes a loop"
|
|
1126
|
+
}).repeated("remove-blocked-by", z11.string(), {
|
|
1127
|
+
description: "numbers of tasks no longer blocking it, comma separated or the flag repeated"
|
|
1128
|
+
}).option("lane", z11.string(), {
|
|
1255
1129
|
default: "",
|
|
1256
1130
|
description: "move it to a lane, at the bottom: a lane's number, `backlog` for the permanent catch-all, `new` for one of its own, or `none` for untriaged"
|
|
1257
|
-
}).option("agent",
|
|
1131
|
+
}).option("agent", z11.string(), {
|
|
1258
1132
|
default: "",
|
|
1259
1133
|
description: "the agent on the roster (`splates agents`) that should work it, which Start on the `splates server` board offers first, or `none`"
|
|
1260
|
-
}).option("model",
|
|
1134
|
+
}).option("model", z11.string(), {
|
|
1261
1135
|
default: "",
|
|
1262
1136
|
description: "with --agent: the model, by its id or name on the roster"
|
|
1263
|
-
}).option("effort",
|
|
1137
|
+
}).option("effort", z11.string(), {
|
|
1264
1138
|
default: "",
|
|
1265
1139
|
description: "with --agent: the thinking level, one the agent offers (default: the agent's own)"
|
|
1266
|
-
}).option("ready",
|
|
1140
|
+
}).option("ready", z11.boolean(), {
|
|
1267
1141
|
default: false,
|
|
1268
1142
|
description: "mark it groomed, moving it from the backlog to ready: no task already asks it, and what blocks it is linked"
|
|
1269
|
-
}).option("manual",
|
|
1143
|
+
}).option("manual", z11.boolean(), {
|
|
1270
1144
|
default: false,
|
|
1271
1145
|
description: "queue it by hand only: a queued lane passes over it, and it starts once you `splates queue add` it"
|
|
1272
|
-
}).option("auto",
|
|
1146
|
+
}).option("auto", z11.boolean(), {
|
|
1273
1147
|
default: false,
|
|
1274
1148
|
description: "let a queued lane queue it again, undoing --manual"
|
|
1275
1149
|
}).action(async (opts, ctx) => {
|
|
1276
1150
|
if (opts.manual && opts.auto) {
|
|
1277
|
-
|
|
1151
|
+
fail7("usage", "pass --manual or --auto, not both", { number: opts.id });
|
|
1278
1152
|
}
|
|
1279
1153
|
await refuseGroomer3(ctx.service, "task update", await ctx.tasks.find(opts.id));
|
|
1280
1154
|
const assignee = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
|
|
@@ -1283,21 +1157,21 @@ splatesTask.command("update").use(repository8({ needs: "tasks" })).description("
|
|
|
1283
1157
|
slug: opts.slug,
|
|
1284
1158
|
text: opts.text,
|
|
1285
1159
|
position: opts.position,
|
|
1286
|
-
files:
|
|
1287
|
-
removeFiles:
|
|
1288
|
-
blockedBy: taskIds(opts["blocked-by"]),
|
|
1289
|
-
removeBlockedBy: taskIds(opts["remove-blocked-by"]),
|
|
1160
|
+
files: commaList4(opts.file.join(",")),
|
|
1161
|
+
removeFiles: commaList4(opts["remove-file"].join(",")),
|
|
1162
|
+
blockedBy: taskIds(opts["blocked-by"].join(",")),
|
|
1163
|
+
removeBlockedBy: taskIds(opts["remove-blocked-by"].join(",")),
|
|
1290
1164
|
lane: laneIn(opts.lane),
|
|
1291
1165
|
assignee,
|
|
1292
1166
|
ready: opts.ready,
|
|
1293
1167
|
manual: opts.manual ? true : opts.auto ? false : undefined
|
|
1294
1168
|
}));
|
|
1295
1169
|
});
|
|
1296
|
-
splatesTask.command("remove").use(repository8({ needs: "tasks" })).description("drop a queued task that was done some other way or no longer applies").arg("id",
|
|
1170
|
+
splatesTask.command("remove").use(repository8({ needs: "tasks" })).description("drop a queued task that was done some other way or no longer applies").arg("id", z11.coerce.number().int().positive(), { description: "task number" }).action(async (opts, ctx) => {
|
|
1297
1171
|
await refuseGroomer3(ctx.service, "task remove", await ctx.tasks.find(opts.id));
|
|
1298
1172
|
await ctx.journal.record("task remove", null, () => taskRemove(ctx, opts.id), (removed) => ({ tasks: tasksNamed2([removed]) }));
|
|
1299
1173
|
});
|
|
1300
|
-
splatesTask.command("wait").use(repository8({ needs: "read" })).description("block until a task leaves the queue, which it does when the task that took it merges: for a task blocked by one another lane is on; ends early when that task waits on a person to answer its questions, and gives up with `timeout`").arg("id",
|
|
1174
|
+
splatesTask.command("wait").use(repository8({ needs: "read" })).description("block until a task leaves the queue, which it does when the task that took it merges: for a task blocked by one another lane is on; ends early when that task waits on a person to answer its questions, and gives up with `timeout`").arg("id", z11.coerce.number().int().positive(), { description: "task number" }).option("timeout-secs", z11.coerce.number(), {
|
|
1301
1175
|
default: DEFAULT_TIMEOUT2.secs,
|
|
1302
1176
|
description: "how long to wait before giving up"
|
|
1303
1177
|
}).action(async (opts, ctx) => {
|
|
@@ -1312,36 +1186,36 @@ splatesTask.command("wait").use(repository8({ needs: "read" })).description("blo
|
|
|
1312
1186
|
|
|
1313
1187
|
// tunnel.ts
|
|
1314
1188
|
import { repository as repository9 } from "@stringsync/splates-server/records";
|
|
1315
|
-
import { savedTunnelOf
|
|
1189
|
+
import { savedTunnelOf } from "@stringsync/splates-server/server";
|
|
1316
1190
|
import {
|
|
1317
1191
|
API_TOKEN,
|
|
1318
1192
|
tunnelRemove,
|
|
1319
1193
|
tunnelSetup
|
|
1320
1194
|
} from "@stringsync/splates-server/tunnel";
|
|
1321
|
-
import { cli as
|
|
1322
|
-
import { z as
|
|
1323
|
-
var splatesTunnel =
|
|
1324
|
-
splatesTunnel.command("setup").use(repository9({ needs: "own" })).description(`make a Cloudflare tunnel to \`splates server\`, the DNS record routing <host> to it, and an Access application letting in only --email, through the API with the token kept as the cred ${API_TOKEN}, then save them for this machine, so a bare \`splates server\` comes up tunneled; run again, it changes only what is not so already. Without that token it requests it on the Creds page of \`splates server\`, with how to make it, and exits \`requested\`: \`splates creds wait ${API_TOKEN}\`, then run it again`).arg("host",
|
|
1195
|
+
import { cli as cli11 } from "webappwiz/cmd";
|
|
1196
|
+
import { z as z12 } from "zod";
|
|
1197
|
+
var splatesTunnel = cli11("tunnel").description("reach `splates server` from elsewhere: a Cloudflare tunnel with Cloudflare Access in front, set up once per machine");
|
|
1198
|
+
splatesTunnel.command("setup").use(repository9({ needs: "own" })).description(`make a Cloudflare tunnel to \`splates server\`, the DNS record routing <host> to it, and an Access application letting in only --email, through the API with the token kept as the cred ${API_TOKEN}, then save them for this machine, so a bare \`splates server\` comes up tunneled; run again, it changes only what is not so already. Without that token it requests it on the Creds page of \`splates server\`, with how to make it, and exits \`requested\`: \`splates creds wait ${API_TOKEN}\`, then run it again`).arg("host", z12.string(), {
|
|
1325
1199
|
description: "the public host name, in a zone of the account, as in splates.example.com"
|
|
1326
|
-
}).option("email",
|
|
1200
|
+
}).option("email", z12.string(), {
|
|
1327
1201
|
default: "",
|
|
1328
1202
|
description: "comma-separated email addresses Access lets in"
|
|
1329
|
-
}).option("name",
|
|
1203
|
+
}).option("name", z12.string(), {
|
|
1330
1204
|
default: "splates",
|
|
1331
1205
|
description: "what the tunnel, the Access application and its policy are called"
|
|
1332
|
-
}).option("team",
|
|
1206
|
+
}).option("team", z12.string(), {
|
|
1333
1207
|
default: "",
|
|
1334
1208
|
description: "the Zero Trust team to make, as in <team>.cloudflareaccess.com, when the account has none yet"
|
|
1335
1209
|
}).action(async (opts, ctx) => ctx.journal.record("tunnel setup", null, async () => tunnelSetup({
|
|
1336
1210
|
...opts,
|
|
1337
1211
|
creds: ctx.creds,
|
|
1338
1212
|
task: await here(ctx),
|
|
1339
|
-
saved:
|
|
1213
|
+
saved: savedTunnelOf(ctx),
|
|
1340
1214
|
log: ctx.log,
|
|
1341
1215
|
fetch: ctx.fetch
|
|
1342
1216
|
})));
|
|
1343
|
-
splatesTunnel.command("status").description("print the tunnel `splates tunnel setup` saved for this machine, which a bare `splates server` runs, and whether its token is in the system's secret store").option("json",
|
|
1344
|
-
const saved =
|
|
1217
|
+
splatesTunnel.command("status").description("print the tunnel `splates tunnel setup` saved for this machine, which a bare `splates server` runs, and whether its token is in the system's secret store").option("json", z12.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
|
|
1218
|
+
const saved = savedTunnelOf(ctx);
|
|
1345
1219
|
const record = await saved.read();
|
|
1346
1220
|
const token = record === null ? false : await saved.token() !== undefined;
|
|
1347
1221
|
if (opts.json) {
|
|
@@ -1362,7 +1236,7 @@ splatesTunnel.command("status").description("print the tunnel `splates tunnel se
|
|
|
1362
1236
|
`));
|
|
1363
1237
|
});
|
|
1364
1238
|
splatesTunnel.command("remove").use(repository9({ needs: "own" })).description(`delete what \`splates tunnel setup\` and \`splates server --slot\` made in Cloudflare by what they saved: each tunnel, the DNS record routing to it, the Access applications and their policy, then forget the saved files and the tunnels' tokens; the API token kept as ${API_TOKEN} stays. Run again after a failure, it finishes what is left`).action(async (_opts, ctx) => ctx.journal.record("tunnel remove", null, async () => tunnelRemove({
|
|
1365
|
-
saved:
|
|
1239
|
+
saved: savedTunnelOf(ctx),
|
|
1366
1240
|
slots: slotTunnelsOf(ctx),
|
|
1367
1241
|
creds: ctx.creds,
|
|
1368
1242
|
task: await here(ctx),
|
|
@@ -1374,32 +1248,27 @@ splatesTunnel.command("remove").use(repository9({ needs: "own" })).description(`
|
|
|
1374
1248
|
// worker.ts
|
|
1375
1249
|
import { splatesHome as splatesHome3 } from "@stringsync/splates-server/models";
|
|
1376
1250
|
import {
|
|
1377
|
-
addWorker,
|
|
1378
1251
|
drainWorker,
|
|
1379
1252
|
FileLogger,
|
|
1380
1253
|
listWorkers,
|
|
1381
1254
|
removeWorker,
|
|
1382
1255
|
setWorkerSlots,
|
|
1383
|
-
WORKER_JOIN,
|
|
1384
|
-
wakeWorker,
|
|
1385
|
-
workerJoin,
|
|
1386
|
-
workerRecipe,
|
|
1387
1256
|
workerRun
|
|
1388
1257
|
} from "@stringsync/splates-server/workers";
|
|
1389
|
-
import { cli as
|
|
1258
|
+
import { cli as cli13 } from "webappwiz/cmd";
|
|
1390
1259
|
import { CompositeLogger } from "webappwiz/log";
|
|
1391
|
-
import { z as
|
|
1260
|
+
import { z as z14 } from "zod";
|
|
1392
1261
|
|
|
1393
1262
|
// waiting.ts
|
|
1394
1263
|
import { listQueue, moveQueued } from "@stringsync/splates-server/workers";
|
|
1395
|
-
import { cli as
|
|
1396
|
-
import { z as
|
|
1397
|
-
var
|
|
1264
|
+
import { cli as cli12 } from "webappwiz/cmd";
|
|
1265
|
+
import { z as z13 } from "zod";
|
|
1266
|
+
var DEV = {
|
|
1398
1267
|
default: "",
|
|
1399
1268
|
description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV"
|
|
1400
1269
|
};
|
|
1401
|
-
var splatesWaiting =
|
|
1402
|
-
splatesWaiting.command("list").description("list the agents waiting for a slot, first in line first: position, project, task, conversation, how long each has waited and why (no free slot, no worker up, or a platform no worker runs, as `needs macOS`)").option("dev",
|
|
1270
|
+
var splatesWaiting = cli12("queue").description("the agents waiting for a slot on a worker of the running `splates server`: each one another agent started (with `splates add --agent`, or a lane's orchestrator handing out tasks), started or resumed while every worker it can run on is full, waits here, first come, first served, and starts once a slot frees, as when an agent left idle 30 minutes stops; none is ever failed for waiting. An agent you start waits only while no worker it can run on is up, as with this machine's own removed or none on the platform its project requires");
|
|
1271
|
+
splatesWaiting.command("list").description("list the agents waiting for a slot, first in line first: position, project, task, conversation, how long each has waited and why (no free slot, no worker up, or a platform no worker runs, as `needs macOS`)").option("dev", z13.string(), DEV).option("json", z13.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => listQueue({
|
|
1403
1272
|
dev: opts.dev,
|
|
1404
1273
|
json: opts.json,
|
|
1405
1274
|
log: ctx.log,
|
|
@@ -1407,11 +1276,11 @@ splatesWaiting.command("list").description("list the agents waiting for a slot,
|
|
|
1407
1276
|
ps: ctx.ps,
|
|
1408
1277
|
send: ctx.fetch
|
|
1409
1278
|
}));
|
|
1410
|
-
splatesWaiting.command("move").description("move an agent waiting for a slot to another place in line, from 1 for the next to start, then list the queue").arg("which",
|
|
1279
|
+
splatesWaiting.command("move").description("move an agent waiting for a slot to another place in line, from 1 for the next to start, then list the queue").arg("which", z13.string(), {
|
|
1411
1280
|
description: "the task it works, or its conversation's id"
|
|
1412
|
-
}).arg("position",
|
|
1281
|
+
}).arg("position", z13.coerce.number().int(), {
|
|
1413
1282
|
description: "where in line to put it, from 1"
|
|
1414
|
-
}).option("dev",
|
|
1283
|
+
}).option("dev", z13.string(), DEV).option("json", z13.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => moveQueued({
|
|
1415
1284
|
which: opts.which,
|
|
1416
1285
|
position: opts.position,
|
|
1417
1286
|
dev: opts.dev,
|
|
@@ -1423,71 +1292,34 @@ splatesWaiting.command("move").description("move an agent waiting for a slot to
|
|
|
1423
1292
|
}));
|
|
1424
1293
|
|
|
1425
1294
|
// worker.ts
|
|
1426
|
-
var splatesWorker =
|
|
1427
|
-
splatesWorker.command("run").description("run
|
|
1295
|
+
var splatesWorker = cli13("worker").description("this machine’s worker, which runs agents, terminals and process reads for `splates server` over its Unix socket");
|
|
1296
|
+
splatesWorker.command("run").description("run this machine’s worker until stopped, connecting over its private Unix socket and reconnecting when the server restarts").option("server", z14.string(), {
|
|
1428
1297
|
default: "",
|
|
1429
|
-
description: "the `splates server` to dial:
|
|
1430
|
-
}).option("coordinator",
|
|
1298
|
+
description: "the `splates server` to dial: unix:<path>; by default this machine's"
|
|
1299
|
+
}).option("coordinator", z14.string(), {
|
|
1431
1300
|
default: "",
|
|
1432
1301
|
description: "the old name for --server, still read for workers installed before it, and passed to a build older than it"
|
|
1433
|
-
}).option("slots",
|
|
1302
|
+
}).option("slots", z14.coerce.number().int().min(0), {
|
|
1434
1303
|
default: 0,
|
|
1435
1304
|
description: "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"
|
|
1436
|
-
}).option("id",
|
|
1437
|
-
default: "",
|
|
1438
|
-
description: "what the server knows this worker by, kept across restarts so it picks up where it was; by default
|
|
1439
|
-
}).option("
|
|
1440
|
-
default: "",
|
|
1441
|
-
description: "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"
|
|
1442
|
-
}).option("ephemeral", z15.boolean(), {
|
|
1443
|
-
default: false,
|
|
1444
|
-
description: "keep the key --join answers in memory alone, for a machine destroyed rather than restarted, as one a worker provider makes"
|
|
1445
|
-
}).option("key-file", z15.boolean(), {
|
|
1446
|
-
default: false,
|
|
1447
|
-
description: "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"
|
|
1448
|
-
}).option("quiet", z15.boolean(), {
|
|
1305
|
+
}).option("id", z14.literal("local"), {
|
|
1306
|
+
default: "local",
|
|
1307
|
+
description: "what the server knows this worker by, kept across restarts so it picks up where it was; by default local"
|
|
1308
|
+
}).option("quiet", z14.boolean(), {
|
|
1449
1309
|
default: false,
|
|
1450
1310
|
description: "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"
|
|
1451
1311
|
}).action((opts, ctx) => workerRun({
|
|
1452
1312
|
coordinator: opts.server || opts.coordinator,
|
|
1453
1313
|
slots: opts.slots,
|
|
1454
1314
|
id: opts.id,
|
|
1455
|
-
join: opts.join || ctx.ps.env(WORKER_JOIN) || "",
|
|
1456
|
-
ephemeral: opts.ephemeral,
|
|
1457
|
-
keyFile: opts["key-file"],
|
|
1458
1315
|
log: opts.quiet ? quietLog(ctx) : ctx.log,
|
|
1459
1316
|
ps: ctx.ps,
|
|
1460
1317
|
fs: ctx.fs
|
|
1461
1318
|
}));
|
|
1462
|
-
splatesWorker.command("
|
|
1463
|
-
default: "",
|
|
1464
|
-
description: "the `splates server` to join: its URL"
|
|
1465
|
-
}).option("coordinator", z15.string(), {
|
|
1466
|
-
default: "",
|
|
1467
|
-
description: "the old name for --server"
|
|
1468
|
-
}).option("join", z15.string(), {
|
|
1469
|
-
default: "",
|
|
1470
|
-
description: "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"
|
|
1471
|
-
}).option("id", z15.string(), {
|
|
1472
|
-
default: "",
|
|
1473
|
-
description: "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"
|
|
1474
|
-
}).option("key-file", z15.boolean(), {
|
|
1475
|
-
default: false,
|
|
1476
|
-
description: "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"
|
|
1477
|
-
}).action((opts, ctx) => workerJoin({
|
|
1478
|
-
coordinator: opts.server || opts.coordinator,
|
|
1479
|
-
join: opts.join || ctx.ps.env(WORKER_JOIN) || "",
|
|
1480
|
-
id: opts.id,
|
|
1481
|
-
keyFile: opts["key-file"],
|
|
1482
|
-
log: ctx.log,
|
|
1483
|
-
ps: ctx.ps,
|
|
1484
|
-
fs: ctx.fs
|
|
1485
|
-
}));
|
|
1486
|
-
splatesWorker.command("recipe").description("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").action((_opts, ctx) => workerRecipe({ log: ctx.log }));
|
|
1487
|
-
splatesWorker.command("list").description("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").option("dev", z15.string(), {
|
|
1319
|
+
splatesWorker.command("list").description("list the workers the running `splates server` has: platform, build, protocol, slots used and total, health with when it was last heard, and tasks, then any it refused for speaking another protocol").option("dev", z14.string(), {
|
|
1488
1320
|
default: "",
|
|
1489
1321
|
description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV"
|
|
1490
|
-
}).option("json",
|
|
1322
|
+
}).option("json", z14.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => listWorkers({
|
|
1491
1323
|
dev: opts.dev,
|
|
1492
1324
|
json: opts.json,
|
|
1493
1325
|
log: ctx.log,
|
|
@@ -1499,31 +1331,12 @@ var dev = {
|
|
|
1499
1331
|
default: "",
|
|
1500
1332
|
description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV"
|
|
1501
1333
|
};
|
|
1502
|
-
splatesWorker.command("
|
|
1503
|
-
default: 0,
|
|
1504
|
-
description: "this machine's own worker, with this many slots"
|
|
1505
|
-
}).option("provider", z15.boolean(), {
|
|
1506
|
-
default: false,
|
|
1507
|
-
description: "have the server's worker provider make the worker, of its cheapest type unless --type says"
|
|
1508
|
-
}).option("type", z15.string(), {
|
|
1509
|
-
default: "",
|
|
1510
|
-
description: "the worker provider's type to make, as it names it; implies --provider"
|
|
1511
|
-
}).option("dev", z15.string(), dev).action((opts, ctx) => addWorker({
|
|
1512
|
-
local: opts.local,
|
|
1513
|
-
provider: opts.provider,
|
|
1514
|
-
type: opts.type,
|
|
1515
|
-
dev: opts.dev,
|
|
1516
|
-
log: ctx.log,
|
|
1517
|
-
fs: ctx.fs,
|
|
1518
|
-
ps: ctx.ps,
|
|
1519
|
-
send: ctx.fetch
|
|
1520
|
-
}));
|
|
1521
|
-
splatesWorker.command("drain").description("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").arg("id", z15.string(), {
|
|
1334
|
+
splatesWorker.command("drain").description("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").arg("id", z14.string(), {
|
|
1522
1335
|
description: "the worker, as `splates worker list` names it"
|
|
1523
|
-
}).option("undo",
|
|
1336
|
+
}).option("undo", z14.boolean(), {
|
|
1524
1337
|
default: false,
|
|
1525
1338
|
description: "take new agents again"
|
|
1526
|
-
}).option("dev",
|
|
1339
|
+
}).option("dev", z14.string(), dev).action((opts, ctx) => drainWorker({
|
|
1527
1340
|
id: opts.id,
|
|
1528
1341
|
undo: opts.undo,
|
|
1529
1342
|
dev: opts.dev,
|
|
@@ -1532,21 +1345,11 @@ splatesWorker.command("drain").description("take no new agents on a worker, lett
|
|
|
1532
1345
|
ps: ctx.ps,
|
|
1533
1346
|
send: ctx.fetch
|
|
1534
1347
|
}));
|
|
1535
|
-
splatesWorker.command("
|
|
1348
|
+
splatesWorker.command("slots").description("set how many agents a worker runs at once, from now on, over what it started with").arg("id", z14.string(), {
|
|
1536
1349
|
description: "the worker, as `splates worker list` names it"
|
|
1537
|
-
}).
|
|
1538
|
-
id: opts.id,
|
|
1539
|
-
dev: opts.dev,
|
|
1540
|
-
log: ctx.log,
|
|
1541
|
-
fs: ctx.fs,
|
|
1542
|
-
ps: ctx.ps,
|
|
1543
|
-
send: ctx.fetch
|
|
1544
|
-
}));
|
|
1545
|
-
splatesWorker.command("slots").description("set how many agents a worker runs at once, from now on, over what it started with").arg("id", z15.string(), {
|
|
1546
|
-
description: "the worker, as `splates worker list` names it"
|
|
1547
|
-
}).arg("slots", z15.coerce.number().int().min(1), {
|
|
1350
|
+
}).arg("slots", z14.coerce.number().int().min(1), {
|
|
1548
1351
|
description: "how many at once"
|
|
1549
|
-
}).option("dev",
|
|
1352
|
+
}).option("dev", z14.string(), dev).action((opts, ctx) => setWorkerSlots({
|
|
1550
1353
|
id: opts.id,
|
|
1551
1354
|
slots: opts.slots,
|
|
1552
1355
|
dev: opts.dev,
|
|
@@ -1555,12 +1358,12 @@ splatesWorker.command("slots").description("set how many agents a worker runs at
|
|
|
1555
1358
|
ps: ctx.ps,
|
|
1556
1359
|
send: ctx.fetch
|
|
1557
1360
|
}));
|
|
1558
|
-
splatesWorker.command("remove").description("
|
|
1361
|
+
splatesWorker.command("remove").description("stop this machine’s worker taking agents, letting its agents run to their end; resume it with `splates worker drain local --undo`").arg("id", z14.string(), {
|
|
1559
1362
|
description: "the worker, as `splates worker list` names it"
|
|
1560
|
-
}).option("force",
|
|
1363
|
+
}).option("force", z14.boolean(), {
|
|
1561
1364
|
default: false,
|
|
1562
1365
|
description: "end the agents on it now rather than wait for them"
|
|
1563
|
-
}).option("dev",
|
|
1366
|
+
}).option("dev", z14.string(), dev).action((opts, ctx) => removeWorker({
|
|
1564
1367
|
id: opts.id,
|
|
1565
1368
|
force: opts.force,
|
|
1566
1369
|
dev: opts.dev,
|
|
@@ -1592,7 +1395,7 @@ function projectArgs(argv) {
|
|
|
1592
1395
|
}
|
|
1593
1396
|
return { argv };
|
|
1594
1397
|
}
|
|
1595
|
-
var splates =
|
|
1398
|
+
var splates = cli14("splates").use(exits());
|
|
1596
1399
|
splates.command("notes").use(repository10({ needs: "read" })).description("print each project note's frontmatter description, one per line; notesDir in splates.config.ts defaults to notes; an absent directory prints nothing").action((_opts, ctx) => notes(ctx));
|
|
1597
1400
|
splates.command("init").use(repository10({ creates: true, needs: "own" })).description("make this repository a splates project: set up its records under .git/splates when it has none, install the splates skill of this release with `bunx skills add`, and register it in ~/.splates/projects.json, so `splates server` serves it beside every other; run again, it changes only what is not so already").action((_opts, ctx) => init(ctx));
|
|
1598
1401
|
splates.command("migrate").description(`move this repository's records under .git/splates to this splates's format, as \`splates server\` does as it starts: backed up first, the steps run with ${MIGRATION_LIMIT.human()} to finish, and the backup put back if one fails or runs over, with why kept in .git/splates-migration.json; it tries again past a failure kept there, which the server and every other command leave alone. Run it only while no older \`splates server\` serves the repository, which would refuse the records once moved; \`splates update\` restarts one first`).action(async (_opts, ctx) => {
|
|
@@ -1613,50 +1416,42 @@ splates.command("update").use(recordsFirst()).use(repository10({ needs: "own" })
|
|
|
1613
1416
|
ctx.ps.exit(code);
|
|
1614
1417
|
}
|
|
1615
1418
|
});
|
|
1616
|
-
splates.command("heavy").description("run a heavy command (a full test run, an install) in one of this machine's heavy-job slots, waiting for one to come free first, so agents testing at once queue rather than run out of memory; `SPLATES_HEAVY_JOBS` sets how many run at once, 2 by default").rest("command", z16.string(), {
|
|
1617
|
-
description: "the command and its arguments, after `--`"
|
|
1618
|
-
}).action(async (opts, ctx) => {
|
|
1619
|
-
const code = await runHeavy(ctx, opts.command);
|
|
1620
|
-
if (code !== 0) {
|
|
1621
|
-
ctx.ps.exit(code);
|
|
1622
|
-
}
|
|
1623
|
-
});
|
|
1624
1419
|
splates.command("forget").use(repository10({ needs: "own" })).description("take this repository off ~/.splates/projects.json, so `splates server` no longer offers it; its records stay, and `splates init` brings it back").action((_opts, ctx) => forget(ctx));
|
|
1625
|
-
splates.command("add").use(repository10({ needs: "tasks" })).description("start a task: a new one, or with --task one from the queue; create branch task/<slug>, a worktree at ../<repo>-splates.noindex/<slug> and a state record").arg("slug",
|
|
1420
|
+
splates.command("add").use(repository10({ needs: "tasks" })).description("start a task: a new one, or with --task one from the queue; create branch task/<slug>, a worktree at ../<repo>-splates.noindex/<slug> and a state record").arg("slug", z15.string(), {
|
|
1626
1421
|
description: "the task's name, for its branch and worktree (lowercase-with-dashes)"
|
|
1627
|
-
}).option("base",
|
|
1422
|
+
}).option("base", z15.string(), {
|
|
1628
1423
|
default: "",
|
|
1629
1424
|
description: "branch this task starts from and merges onto (default: trunk); `task/<other>` stacks this task on that one and lands the work in its worktree"
|
|
1630
|
-
}).option("task",
|
|
1425
|
+
}).option("task", z15.string(), {
|
|
1631
1426
|
default: "",
|
|
1632
1427
|
description: "start this queued task by number: its words become the plan's Goal, merging removes it, and removing this task puts it back where it was"
|
|
1633
|
-
}).option("goal",
|
|
1428
|
+
}).option("goal", z15.string(), {
|
|
1634
1429
|
default: "",
|
|
1635
1430
|
description: "one or two lines on what done means, written under `## Goal` in the task's SPLATES.md; required unless --task seeds the Goal"
|
|
1636
|
-
}).option("agent",
|
|
1431
|
+
}).option("agent", z15.string(), {
|
|
1637
1432
|
default: "",
|
|
1638
1433
|
description: "how `splates server`'s queue hands the task to an agent on the roster (`splates agents`), which it starts in the worktree; refused from an agent, which starts no agents and queues work with `splates task add` instead"
|
|
1639
|
-
}).option("model",
|
|
1434
|
+
}).option("model", z15.string(), {
|
|
1640
1435
|
default: "",
|
|
1641
1436
|
description: "with --agent: the model it works on, by its id or name on the roster (default: the agent's own)"
|
|
1642
|
-
}).option("effort",
|
|
1437
|
+
}).option("effort", z15.string(), {
|
|
1643
1438
|
default: "",
|
|
1644
1439
|
description: "with --agent: the thinking level it works at, one the agent offers (default: the agent's own)"
|
|
1645
|
-
}).option("prompt",
|
|
1440
|
+
}).option("prompt", z15.string(), {
|
|
1646
1441
|
default: "",
|
|
1647
1442
|
description: "with --agent: what to tell the agent beyond claiming the task"
|
|
1648
|
-
}).option("held",
|
|
1443
|
+
}).option("held", z15.string(), {
|
|
1649
1444
|
default: "",
|
|
1650
1445
|
description: "with --agent: the conversation `splates server`'s queue held for the queued task, which takes the task rather than a new one"
|
|
1651
1446
|
}).action(async (opts, ctx) => {
|
|
1652
1447
|
await refuseGroomer4(ctx.service, "add");
|
|
1653
1448
|
if (opts.agent === "" && (opts.model !== "" || opts.effort !== "" || opts.prompt !== "" || opts.held !== "")) {
|
|
1654
|
-
|
|
1449
|
+
fail8("usage", "--model, --effort, --prompt and --held go with --agent", {
|
|
1655
1450
|
task: opts.slug
|
|
1656
1451
|
});
|
|
1657
1452
|
}
|
|
1658
1453
|
if (opts.goal.trim() === "" && opts.task === "") {
|
|
1659
|
-
|
|
1454
|
+
fail8("usage", `say what done means: \`splates add ${opts.slug} --goal "<one or two lines>"\`, or start queued tasks with --task`, { task: opts.slug });
|
|
1660
1455
|
}
|
|
1661
1456
|
const dispatch = opts.agent === "" ? undefined : {
|
|
1662
1457
|
agent: opts.agent,
|
|
@@ -1690,108 +1485,109 @@ splates.command("add").use(repository10({ needs: "tasks" })).description("start
|
|
|
1690
1485
|
await serveProject(ctx);
|
|
1691
1486
|
}
|
|
1692
1487
|
});
|
|
1693
|
-
splates.command("claim").use(repository10({ needs: "own" })).description("resume an existing task: take ownership of its worktree and print its path, status and any half-finished rebase; refuses while another agent holds the lease, but takes a stale one silently, so `splates show` first if the tree may not be abandoned").arg("task",
|
|
1488
|
+
splates.command("claim").use(repository10({ needs: "own" })).description("resume an existing task: take ownership of its worktree and print its path, status and any half-finished rebase; refuses while another agent holds the lease, but takes a stale one silently, so `splates show` first if the tree may not be abandoned").arg("task", z15.string(), { description: "task name" }).action(async (opts, ctx) => {
|
|
1694
1489
|
await ctx.journal.record("claim", opts.task, () => claim(ctx, opts.task), (claimed) => claimed);
|
|
1695
1490
|
await follow(ctx, opts.task);
|
|
1696
1491
|
});
|
|
1697
|
-
splates.command("merge").use(repository10({ needs: "own" })).description("land this worktree's branch on its base (trunk unless created with --base): rebase onto it, run tests on the rebased code, fast-forward it in whichever worktree has it checked out, then discard the worktree, branch and record (linear history, never a merge commit, no flag to skip tests); requires committed work, refusing a dirty worktree").option("message",
|
|
1492
|
+
splates.command("merge").use(repository10({ needs: "own" })).description("land this worktree's branch on its base (trunk unless created with --base): rebase onto it, run tests on the rebased code, fast-forward it in whichever worktree has it checked out, then discard the worktree, branch and record (linear history, never a merge commit, no flag to skip tests); requires committed work, refusing a dirty worktree").option("message", z15.string(), {
|
|
1698
1493
|
default: "",
|
|
1699
1494
|
description: "squash the task's commits into one with this message before landing, saying what the base gains in at least 5 characters"
|
|
1700
1495
|
}).action(async (opts, ctx) => {
|
|
1701
1496
|
await refuseGroomer4(ctx.service, "merge");
|
|
1702
1497
|
const task = await here(ctx);
|
|
1703
1498
|
const facts = {};
|
|
1704
|
-
await ctx.journal.record("merge", task, () => merge(ctx, ctx.ps.cwd(), facts, {
|
|
1499
|
+
await ctx.journal.record("merge", task, () => merge(ctx, ctx.ps.cwd(), facts, {
|
|
1500
|
+
message: opts.message
|
|
1501
|
+
}), facts);
|
|
1705
1502
|
if (task !== null) {
|
|
1706
1503
|
await addFactTasks(ctx, task).catch((error) => ctx.log.error(`could not queue tasks for what the merge found: ${error}`));
|
|
1707
1504
|
await follow(ctx, null, { from: task });
|
|
1708
1505
|
}
|
|
1709
1506
|
});
|
|
1710
|
-
splates.command("remove").use(repository10({ needs: "tasks" })).description("discard a task: worktree, branch and
|
|
1507
|
+
splates.command("remove").use(repository10({ needs: "tasks" })).description("discard a task: worktree, branch and board tasks; cheap and encouraged, since redoing a task against current trunk often beats a hard rebase").arg("task", z15.string(), { description: "task name" }).option("force", z15.boolean(), {
|
|
1711
1508
|
default: false,
|
|
1712
1509
|
description: "discard even when another agent holds the lease"
|
|
1713
|
-
}).option("reason",
|
|
1510
|
+
}).option("reason", z15.optional(z15.enum(REMOVE_REASONS)), {
|
|
1714
1511
|
description: "why, for the Stats page: nothing (nothing to change), moot (no longer needed), superseded (done some other way), redo (better redone against current trunk), blocked (set aside until what it waits on lands, so back on the list)"
|
|
1715
1512
|
}).action(async (opts, ctx) => {
|
|
1716
1513
|
await ctx.journal.record("remove", opts.task, () => remove(ctx, opts.task, { force: opts.force, reason: opts.reason }), ({ released }) => ({ why: opts.reason ?? null, released }));
|
|
1717
1514
|
await follow(ctx, null, { from: opts.task });
|
|
1718
1515
|
});
|
|
1719
|
-
splates.command("list").use(repository10({ needs: "read" })).description("list every started task: its number, name, status, lease (held: an agent is on it now; stale: gone quiet, normal for a task mid-edit; none), commits ahead of trunk, age; `splates task list` lists the queue").option("json",
|
|
1516
|
+
splates.command("list").use(repository10({ needs: "read" })).description("list every started task: its number, name, status, lease (held: an agent is on it now; stale: gone quiet, normal for a task mid-edit; none), commits ahead of trunk, age; `splates task list` lists the queue").option("json", z15.boolean(), { default: false, description: "emit JSON" }).option("files", z15.boolean(), {
|
|
1720
1517
|
default: false,
|
|
1721
1518
|
description: "add each task's Goal and changed files (committed or not), to check for overlap before starting"
|
|
1722
1519
|
}).action((opts, ctx) => list(ctx, { json: opts.json, files: opts.files }));
|
|
1723
|
-
splates.command("show").use(repository10({ needs: "read" })).description("read one task without touching it: everything `list` shows for it, plus the SPLATES.md its agent left at the worktree root; takes no lease, so it cannot knock that agent off its own tree; for a task no longer live, the newest finished one of that name: how it ended, its base, the commit it landed as, its conversation and the SPLATES.md it ended on").arg("task",
|
|
1724
|
-
splates.command("wait").use(repository10({ needs: "read" })).description("block until a task stops moving: waiting on a person to answer its questions, or gone (merged or removed), or broken; takes no lease, and gives up with `timeout` rather than waiting forever").arg("task",
|
|
1520
|
+
splates.command("show").use(repository10({ needs: "read" })).description("read one task without touching it: everything `list` shows for it, plus the SPLATES.md its agent left at the worktree root; takes no lease, so it cannot knock that agent off its own tree; for a task no longer live, the newest finished one of that name: how it ended, its base, the commit it landed as, its conversation and the SPLATES.md it ended on").arg("task", z15.string(), { description: "task name" }).option("json", z15.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => show(ctx, opts.task, { json: opts.json }));
|
|
1521
|
+
splates.command("wait").use(repository10({ needs: "read" })).description("block until a task stops moving: waiting on a person to answer its questions, or gone (merged or removed), or broken; takes no lease, and gives up with `timeout` rather than waiting forever").arg("task", z15.string(), { description: "task name" }).option("timeout-secs", z15.coerce.number(), {
|
|
1725
1522
|
default: DEFAULT_TIMEOUT3.secs,
|
|
1726
1523
|
description: "how long to wait before giving up"
|
|
1727
1524
|
}).action((opts, ctx) => {
|
|
1728
1525
|
const timeout = Duration3.secs(opts["timeout-secs"]);
|
|
1729
1526
|
return ctx.journal.waiting("wait", opts.task, { until: untilAfter3(timeout) }, () => wait(ctx, opts.task, { timeout }));
|
|
1730
1527
|
});
|
|
1731
|
-
splates.command("log").use(repository10({ needs: "read" })).description("show what has been done here recently: one line per add, claim, merge, remove, retry, queued task and more, with how it ended; outlives the tasks themselves").option("count",
|
|
1528
|
+
splates.command("log").use(repository10({ needs: "read" })).description("show what has been done here recently: one line per add, claim, merge, remove, retry, queued task and more, with how it ended; outlives the tasks themselves").option("count", z15.coerce.number(), {
|
|
1732
1529
|
default: DEFAULT_COUNT,
|
|
1733
1530
|
description: "how many entries to show"
|
|
1734
|
-
}).option("json",
|
|
1531
|
+
}).option("json", z15.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => showLog(ctx, { count: opts.count, json: opts.json }));
|
|
1735
1532
|
splates.mount("server", splatesServer);
|
|
1736
|
-
splates.command("ports").description("list the ports tasks serve, found by the running `splates server` as an editor finds ports to forward: each port a process in a task's worktree, or under its agent, listens on, with its link through the ports host `splates server --ports-host` saved, behind Cloudflare Access, and its link on this machine
|
|
1533
|
+
splates.command("ports").description("list the ports tasks serve, found by the running `splates server` as an editor finds ports to forward: each port a process in a task's worktree, or under its agent, listens on, with its link through the ports host `splates server --ports-host` saved, behind Cloudflare Access, and its link on this machine; give the user both").arg("task", z15.string(), {
|
|
1737
1534
|
default: "",
|
|
1738
1535
|
description: "only this task's ports; every task's by default"
|
|
1739
|
-
}).option("dev",
|
|
1536
|
+
}).option("dev", z15.string(), {
|
|
1740
1537
|
default: "",
|
|
1741
|
-
description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV, else
|
|
1742
|
-
}).option("json",
|
|
1538
|
+
description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV, else this machine's on its default port"
|
|
1539
|
+
}).option("json", z15.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => portsLs({
|
|
1743
1540
|
task: opts.task,
|
|
1744
1541
|
dev: opts.dev,
|
|
1745
1542
|
json: opts.json,
|
|
1746
1543
|
log: ctx.log,
|
|
1747
|
-
fs: ctx.fs,
|
|
1748
1544
|
ps: ctx.ps,
|
|
1749
1545
|
send: ctx.fetch
|
|
1750
1546
|
}));
|
|
1751
|
-
splates.command("agents").description("print the roster of agents splates can start, kept for every project in ~/.splates/agents.json: each agent's launch command and models, with what each model is good at and what it costs, and the thinking levels `--effort` takes, as the agent offered them when last started, for choosing whom to delegate to; writes the defaults (Claude, Codex) there first when there is none, and `splates server` starts conversations with those it lists").option("json",
|
|
1547
|
+
splates.command("agents").description("print the roster of agents splates can start, kept for every project in ~/.splates/agents.json: each agent's launch command and models, with what each model is good at and what it costs, and the thinking levels `--effort` takes, as the agent offered them when last started, for choosing whom to delegate to; writes the defaults (Claude, Codex) there first when there is none, and `splates server` starts conversations with those it lists").option("json", z15.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => agents({
|
|
1752
1548
|
roster: rosterOf2(ctx),
|
|
1753
1549
|
offered: await offeredOf(ctx).all(),
|
|
1754
1550
|
log: ctx.log
|
|
1755
1551
|
}, { json: opts.json }));
|
|
1756
|
-
splates.command("setup").description("set splates up on this machine, one step at a time: with no step, each area's steps, checked from real state, and the one to do next; with a step, why it matters, the one thing the user does and what the agent does. Skips are kept in ~/.splates/setup.json, so a skipped step stops asking").arg("step",
|
|
1552
|
+
splates.command("setup").description("set splates up on this machine, one step at a time: with no step, each area's steps, checked from real state, and the one to do next; with a step, why it matters, the one thing the user does and what the agent does. Skips are kept in ~/.splates/setup.json, so a skipped step stops asking").arg("step", z15.string(), {
|
|
1757
1553
|
default: "",
|
|
1758
1554
|
description: "a step's id; omit for every area's progress"
|
|
1759
|
-
}).option("json",
|
|
1555
|
+
}).option("json", z15.boolean(), { default: false, description: "emit JSON" }).option("skip", z15.boolean(), {
|
|
1760
1556
|
default: false,
|
|
1761
1557
|
description: "with a step: stop asking for it"
|
|
1762
|
-
}).option("unskip",
|
|
1558
|
+
}).option("unskip", z15.boolean(), {
|
|
1763
1559
|
default: false,
|
|
1764
1560
|
description: "with a step: ask for it again"
|
|
1765
|
-
}).option("undo",
|
|
1561
|
+
}).option("undo", z15.boolean(), {
|
|
1766
1562
|
default: false,
|
|
1767
1563
|
description: "with an area's id in place of a step: print what undoing it would remove, the creds it forgets and keeps, and what only the user can remove; the Setup page's Undo does it, after the user confirms"
|
|
1768
1564
|
}).action((opts, ctx) => setup(ctx, opts));
|
|
1769
|
-
splates.command("path").use(repository10({ needs: "read" })).description("print a task's worktree path, or the main tree with no task; names another agent's tree without taking its lease").arg("task",
|
|
1565
|
+
splates.command("path").use(repository10({ needs: "read" })).description("print a task's worktree path, or the main tree with no task; names another agent's tree without taking its lease").arg("task", z15.string(), {
|
|
1770
1566
|
default: "",
|
|
1771
1567
|
description: "task name; omit for the main tree"
|
|
1772
1568
|
}).action((opts, ctx) => path(ctx, opts.task || undefined));
|
|
1773
1569
|
splates.command("checkpoint").use(repository10({ needs: "own" })).description("save this task to the central remote (`remote` in splates.config.ts) before a stop: commit what is uncommitted as a `WIP: checkpoint` commit, to squash before merging, and push the branch as `splates/<task>`, so a worker that dies loses nothing; with no remote configured, leaves the tree as it is").action(async (_opts, ctx) => {
|
|
1774
1570
|
await checkpoint(ctx, ctx.ps.cwd());
|
|
1775
1571
|
});
|
|
1776
|
-
splates.command("attach").use(repository10({ needs: "own" })).description("store screenshots and recordings for this task's questions to show: prints the path each was stored at, under .git/splates, which the `splates server` page shows inline; from a conversation (its $SPLATES_CONVERSATION, or --conversation) they stay with it past the task, otherwise they go when the task merges or is removed; those the task's final SPLATES.md shows are copied to keep with its finished record. A task holds at most attachmentCapacity bytes; with no files, lists what it holds. A file the user will come back for on its own is an artifact instead (`splates artifact add`)").arg("files",
|
|
1572
|
+
splates.command("attach").use(repository10({ needs: "own" })).description("store screenshots and recordings for this task's questions to show: prints the path each was stored at, under .git/splates, which the `splates server` page shows inline; from a conversation (its $SPLATES_CONVERSATION, or --conversation) they stay with it past the task, otherwise they go when the task merges or is removed; those the task's final SPLATES.md shows are copied to keep with its finished record. A task holds at most attachmentCapacity bytes; with no files, lists what it holds. A file the user will come back for on its own is an artifact instead (`splates artifact add`)").arg("files", z15.string(), {
|
|
1777
1573
|
default: "",
|
|
1778
1574
|
description: "files to store, comma separated"
|
|
1779
|
-
}).option("task",
|
|
1575
|
+
}).option("task", z15.string(), {
|
|
1780
1576
|
default: "",
|
|
1781
1577
|
description: "task name, when run outside its worktree"
|
|
1782
|
-
}).option("remove",
|
|
1578
|
+
}).option("remove", z15.string(), {
|
|
1783
1579
|
default: "",
|
|
1784
1580
|
description: "files it holds to drop first, comma separated, by path or stored name"
|
|
1785
|
-
}).option("conversation",
|
|
1581
|
+
}).option("conversation", z15.string(), {
|
|
1786
1582
|
default: "",
|
|
1787
1583
|
description: "conversation to keep the files with, when $SPLATES_CONVERSATION is not set"
|
|
1788
|
-
}).action(async (opts, ctx) => ctx.journal.record("attach", opts.task || await here(ctx), () => attach(ctx,
|
|
1584
|
+
}).action(async (opts, ctx) => ctx.journal.record("attach", opts.task || await here(ctx), () => attach(ctx, commaList5(opts.files), ctx.ps.cwd(), {
|
|
1789
1585
|
task: opts.task || undefined,
|
|
1790
|
-
remove:
|
|
1586
|
+
remove: commaList5(opts.remove),
|
|
1791
1587
|
conversation: opts.conversation || undefined
|
|
1792
|
-
}), (stored) => ({ stored, removed:
|
|
1793
|
-
splates.command("retry").use(repository10({ needs: "own" })).description("give a task that spent its merge attempts another mergeRetryCount of them; the way out of `budget_exhausted` that is not remove and redo, for a person to run once they have seen the tree").arg("task",
|
|
1794
|
-
splates.mount("artifact", splatesArtifact).mount("task", splatesTask).mount("lane", splatesLane).mount("projects", splatesProjects).mount("creds", splatesCreds).mount("tunnel", splatesTunnel).mount("slots", splatesSlots).mount("worker", splatesWorker).mount("queue", splatesQueue).mount("hook", splatesHook).mount("
|
|
1588
|
+
}), (stored) => ({ stored, removed: commaList5(opts.remove) })));
|
|
1589
|
+
splates.command("retry").use(repository10({ needs: "own" })).description("give a task that spent its merge attempts another mergeRetryCount of them; the way out of `budget_exhausted` that is not remove and redo, for a person to run once they have seen the tree").arg("task", z15.string(), { description: "task name" }).action((opts, ctx) => recordRetry(ctx, opts.task));
|
|
1590
|
+
splates.mount("artifact", splatesArtifact).mount("task", splatesTask).mount("lane", splatesLane).mount("projects", splatesProjects).mount("creds", splatesCreds).mount("tunnel", splatesTunnel).mount("slots", splatesSlots).mount("worker", splatesWorker).mount("queue", splatesQueue).mount("hook", splatesHook).mount("replica", splatesReplica);
|
|
1795
1591
|
|
|
1796
1592
|
// index.ts
|
|
1797
1593
|
if (basename2(process.argv[1] ?? "") === LEGACY) {
|
|
@@ -1799,4 +1595,8 @@ if (basename2(process.argv[1] ?? "") === LEGACY) {
|
|
|
1799
1595
|
}
|
|
1800
1596
|
var ps = systemPs();
|
|
1801
1597
|
var { project, argv } = projectArgs(ps.args);
|
|
1598
|
+
var handedOff = await handOffDev(argv, ps.cwd());
|
|
1599
|
+
if (handedOff !== null) {
|
|
1600
|
+
process.exit(handedOff);
|
|
1601
|
+
}
|
|
1802
1602
|
await splates.run({ fs: new NodeFs, ps, assets, project }, argv);
|