@stringsync/splates 0.0.54 → 0.0.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -24
- package/handoff.d.ts +13 -0
- package/index.js +432 -390
- package/package.json +2 -2
- package/replica.d.ts +3 -0
- package/runners.d.ts +3 -0
- package/server.d.ts +1 -2
- package/infra.d.ts +0 -3
package/index.js
CHANGED
|
@@ -11,10 +11,44 @@ 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
53
|
import { exits, fail as fail9 } from "@stringsync/splates-server/exit";
|
|
20
54
|
import { DEFAULT_COUNT, log as showLog } from "@stringsync/splates-server/log";
|
|
@@ -52,12 +86,12 @@ import {
|
|
|
52
86
|
untilAfter as untilAfter3,
|
|
53
87
|
wait
|
|
54
88
|
} from "@stringsync/splates-server/tasks";
|
|
55
|
-
import { cli as
|
|
89
|
+
import { cli as cli16 } from "webappwiz/cmd";
|
|
56
90
|
import { Duration as Duration3 } from "webappwiz/time";
|
|
57
|
-
import { z as
|
|
91
|
+
import { z as z17 } from "zod";
|
|
58
92
|
|
|
59
93
|
// artifact.ts
|
|
60
|
-
import { basename, resolve } from "node:path";
|
|
94
|
+
import { basename, resolve as resolve2 } from "node:path";
|
|
61
95
|
import {
|
|
62
96
|
addArtifact,
|
|
63
97
|
listArtifacts,
|
|
@@ -186,7 +220,7 @@ splatesArtifact.command("add").use(repository({ needs: "own" })).description("ke
|
|
|
186
220
|
}
|
|
187
221
|
const id = await conversationOf(ctx.service, opts.conversation);
|
|
188
222
|
const files = await Promise.all(paths.map(async (given) => {
|
|
189
|
-
const path =
|
|
223
|
+
const path = resolve2(ctx.ps.cwd(), given);
|
|
190
224
|
const bytes = await ctx.fs.readBytes(path).catch(() => null);
|
|
191
225
|
if (bytes === null) {
|
|
192
226
|
fail2("usage", `cannot read ${path}: nothing was changed`, {
|
|
@@ -222,14 +256,15 @@ import { cli as cli2 } from "webappwiz/cmd";
|
|
|
222
256
|
import { z as z3 } from "zod";
|
|
223
257
|
var DEV = {
|
|
224
258
|
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"
|
|
259
|
+
description: "the `splates server` to ask, a URL such as http://localhost:4310; by default the one an agent runs under, from SPLATES_DEV. With none answering, this machine's own budget file is read and changed"
|
|
226
260
|
};
|
|
227
261
|
var JSON_OPTION = { default: false, description: "emit JSON" };
|
|
228
262
|
var LIMIT = z3.enum(["hour", "day", "month"]);
|
|
263
|
+
var SET_LIMIT = z3.enum(["hour", "day", "month", "runners"]);
|
|
229
264
|
var LIMIT_ARG = {
|
|
230
265
|
description: "hour, the rate cap on what made workers running at once cost an hour; day; or month"
|
|
231
266
|
};
|
|
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 ($
|
|
267
|
+
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 ($1/h by default), and limits on a day ($15) and a month ($200). 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. No more than 16 runners are up at once by default, those the server made and `splates run` made together, counted from the cloud's own list; past that the queue waits for one to free. Spend is an estimate at each type's list price. Agents' own API spend is not counted here");
|
|
233
268
|
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
269
|
dev: opts.dev,
|
|
235
270
|
json: opts.json,
|
|
@@ -238,8 +273,10 @@ splatesBudget.command("show").description("show each limit: what made workers sp
|
|
|
238
273
|
ps: ctx.ps,
|
|
239
274
|
send: ctx.fetch
|
|
240
275
|
}));
|
|
241
|
-
splatesBudget.command("set").description("set a limit, in dollars (dollars an hour for hour), then show the budget").arg("limit",
|
|
242
|
-
description:
|
|
276
|
+
splatesBudget.command("set").description("set a limit, in dollars (dollars an hour for hour, a count for runners), then show the budget").arg("limit", SET_LIMIT, {
|
|
277
|
+
description: `${LIMIT_ARG.description}; or runners, the most runners up at once in the account, whoever made them (16 by default)`
|
|
278
|
+
}).arg("dollars", z3.string(), {
|
|
279
|
+
description: "like 5 or 0.25 (a whole number for runners, like 16), or `default` to give it back to its default"
|
|
243
280
|
}).option("dev", z3.string(), DEV).option("json", z3.boolean(), JSON_OPTION).action((opts, ctx) => setBudget({
|
|
244
281
|
limit: opts.limit,
|
|
245
282
|
dollars: opts.dollars,
|
|
@@ -296,7 +333,7 @@ splatesCreds.command("request").use(repository2({ needs: "own" })).description("
|
|
|
296
333
|
default: false,
|
|
297
334
|
description: "keep it for every project on this device, as for a person's own account, rather than this project alone"
|
|
298
335
|
}).option("provider", z4.optional(z4.enum(PROVIDERS)), {
|
|
299
|
-
description: "who keeps it: `system`, the machine's own store
|
|
336
|
+
description: "who keeps it: `system`, the machine's own store"
|
|
300
337
|
}).option("replace", z4.boolean(), {
|
|
301
338
|
default: false,
|
|
302
339
|
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"
|
|
@@ -670,262 +707,256 @@ splatesQueue.command("move").use(repository6({ needs: "tasks" })).description("m
|
|
|
670
707
|
return queueMove(ctx, opts.id, opts.position);
|
|
671
708
|
});
|
|
672
709
|
|
|
673
|
-
//
|
|
674
|
-
import {
|
|
675
|
-
|
|
676
|
-
CoordinatorBox as CoordinatorBox2,
|
|
677
|
-
coordinatorMoved,
|
|
678
|
-
deployKey,
|
|
679
|
-
machineMove,
|
|
680
|
-
neededCreds
|
|
681
|
-
} from "@stringsync/splates-server/coordinator";
|
|
710
|
+
// replica.ts
|
|
711
|
+
import { dirname as dirname2 } from "node:path";
|
|
712
|
+
import { loadConfig as loadConfig2 } from "@stringsync/splates-server/config";
|
|
682
713
|
import { SYSTEM_STORES as SYSTEM_STORES2 } from "@stringsync/splates-server/creds";
|
|
714
|
+
import { fail as fail5 } from "@stringsync/splates-server/exit";
|
|
715
|
+
import { Projects } from "@stringsync/splates-server/projects";
|
|
716
|
+
import {
|
|
717
|
+
FORMAT,
|
|
718
|
+
ProjectHome,
|
|
719
|
+
repository as repository7
|
|
720
|
+
} from "@stringsync/splates-server/records";
|
|
721
|
+
import {
|
|
722
|
+
openReplica,
|
|
723
|
+
REPLICA_ACCESS_KEY,
|
|
724
|
+
REPLICA_SECRET_KEY,
|
|
725
|
+
Replicator,
|
|
726
|
+
restoreFromReplica,
|
|
727
|
+
statusLines
|
|
728
|
+
} from "@stringsync/splates-server/replica";
|
|
729
|
+
import { cli as cli8 } from "webappwiz/cmd";
|
|
730
|
+
import { Credentials } from "webappwiz/creds";
|
|
731
|
+
import { z as z9 } from "zod";
|
|
732
|
+
var splatesReplica = cli8("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");
|
|
733
|
+
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", z9.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
|
|
734
|
+
const status = await Replicator.recorded(ctx.storage);
|
|
735
|
+
if (opts.json) {
|
|
736
|
+
ctx.log.info(JSON.stringify(status));
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
ctx.log.info(statusLines(status, ctx.config.replica !== null, Date.now()).join(`
|
|
740
|
+
`));
|
|
741
|
+
});
|
|
742
|
+
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", z9.boolean(), {
|
|
743
|
+
default: false,
|
|
744
|
+
description: "restore over the records here, leaving them aside"
|
|
745
|
+
}).action(async (opts, ctx) => {
|
|
746
|
+
const root = await mainTree(ctx);
|
|
747
|
+
const projects = Projects.ofMachine(ctx);
|
|
748
|
+
const home = new ProjectHome(root, { ...ctx, projects });
|
|
749
|
+
const stores = ctx.secrets ?? SYSTEM_STORES2;
|
|
750
|
+
const credentials = new Credentials([
|
|
751
|
+
await stores.project(root),
|
|
752
|
+
stores.device
|
|
753
|
+
]);
|
|
754
|
+
const config = await loadConfig2(root, { fs: ctx.fs });
|
|
755
|
+
if (config.replica === null) {
|
|
756
|
+
fail5("usage", "no replica: name one as `replica` in splates.config.ts first");
|
|
757
|
+
}
|
|
758
|
+
const replica = openReplica(config.replica, {
|
|
759
|
+
root,
|
|
760
|
+
fs: ctx.fs,
|
|
761
|
+
creds: {
|
|
762
|
+
credentials,
|
|
763
|
+
request: async () => {
|
|
764
|
+
throw new Error(`fill ${REPLICA_ACCESS_KEY} and ${REPLICA_SECRET_KEY} with \`wiz creds\` first`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
try {
|
|
769
|
+
const done = await restoreFromReplica(replica, {
|
|
770
|
+
target: home,
|
|
771
|
+
fs: ctx.fs,
|
|
772
|
+
format: FORMAT,
|
|
773
|
+
force: opts.force
|
|
774
|
+
});
|
|
775
|
+
ctx.log.info([
|
|
776
|
+
`restored ${done.values} record(s) and ${done.logs} log(s) from ${replica.name}, last caught up on ${done.host}`,
|
|
777
|
+
...done.replaced === null ? [] : [`the records here went aside as ${done.replaced}`],
|
|
778
|
+
`project: ${root} (register its checkout with \`splates init\` if it is new on this machine)`,
|
|
779
|
+
"start `splates server` to serve it and replicate on"
|
|
780
|
+
].join(`
|
|
781
|
+
`));
|
|
782
|
+
} catch (error) {
|
|
783
|
+
fail5("usage", error instanceof Error ? error.message : String(error));
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
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", z9.string(), {
|
|
787
|
+
description: "where the server went, as `droplet.example.com`"
|
|
788
|
+
}).action(async (opts, ctx) => {
|
|
789
|
+
const home = new ProjectHome(ctx.git.root, ctx);
|
|
790
|
+
ctx.dispose();
|
|
791
|
+
const aside = await home.moveAside(opts.to);
|
|
792
|
+
ctx.log.info(`records left aside as ${aside}; the server is at ${opts.to}`);
|
|
793
|
+
});
|
|
794
|
+
async function mainTree(ctx) {
|
|
795
|
+
const { exitCode, stdout } = await ctx.ps.spawnCapture([
|
|
796
|
+
"git",
|
|
797
|
+
"-C",
|
|
798
|
+
ctx.ps.cwd(),
|
|
799
|
+
"rev-parse",
|
|
800
|
+
"--path-format=absolute",
|
|
801
|
+
"--git-common-dir"
|
|
802
|
+
]);
|
|
803
|
+
if (exitCode !== 0) {
|
|
804
|
+
fail5("usage", `not a git repository: ${ctx.ps.cwd()}`);
|
|
805
|
+
}
|
|
806
|
+
return dirname2(stdout.trim());
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// runners.ts
|
|
810
|
+
import { join as join2 } from "node:path";
|
|
811
|
+
import { loadConfig as loadConfig3 } from "@stringsync/splates-server/config";
|
|
683
812
|
import { fail as fail6 } from "@stringsync/splates-server/exit";
|
|
813
|
+
import { splatesHome as splatesHome2 } from "@stringsync/splates-server/models";
|
|
684
814
|
import {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
serveDev
|
|
815
|
+
REAPER_FILE,
|
|
816
|
+
withRunners
|
|
688
817
|
} from "@stringsync/splates-server/server";
|
|
689
|
-
import {
|
|
818
|
+
import {
|
|
819
|
+
DIRECT_IDLE,
|
|
820
|
+
NoRunner,
|
|
821
|
+
parseTimeout,
|
|
822
|
+
runnerServe,
|
|
823
|
+
sizeLabel
|
|
824
|
+
} from "@stringsync/splates-server/workers";
|
|
690
825
|
import { cli as cli9 } from "webappwiz/cmd";
|
|
826
|
+
import { sleep } from "webappwiz/time";
|
|
691
827
|
import { z as z10 } from "zod";
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
}
|
|
703
|
-
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
|
-
import { Projects } from "@stringsync/splates-server/projects";
|
|
707
|
-
import { repository as repository7 } from "@stringsync/splates-server/records";
|
|
708
|
-
import { savedTunnelOf } from "@stringsync/splates-server/server";
|
|
709
|
-
import { FsStorage as FsStorage2 } from "@stringsync/splates-server/storage";
|
|
710
|
-
import { cli as cli8 } from "webappwiz/cmd";
|
|
711
|
-
import { z as z9 } from "zod";
|
|
712
|
-
var splatesInfra = cli8("infra").description("the Pulumi project that makes everything the server's box needs in AWS, Cloudflare and GitHub, which `splates server deploy` then installs splates on");
|
|
713
|
-
splatesInfra.command("init").use(repository7({ needs: "own" })).description("write a Pulumi project in <dir>, its program a dozen lines using the components of @stringsync/splates-server/infra, and the stack's config from the flags; run again, it writes only what is not so already and keeps values you changed. It fills the stack's config from what splates keeps (the projects, the saved tunnel, the Cloudflare ids). The secrets never pass through splates: it makes the Pulumi ESC environment splates/secrets with a placeholder for each (the Cloudflare token, a GitHub token per owner, the Anthropic key), which the stack imports and the user replaces on the Pulumi site; `splates setup` has a step for each saying how to get it. When the Pulumi ESC environment `coordinator-access` makes (splates/aws) is there, the stack imports it. It records the directory in ~/.splates/coordinator/infra.json, where `splates server deploy` finds the stack. Its runtime is bun, which runs the program's TypeScript itself. Commit the project but not Pulumi.<stack>.yaml, which holds your own accounts and ids and which the .gitignore it writes keeps out of git; init writes it again from what splates keeps. Then `bun add @stringsync/splates-server @pulumi/pulumi @pulumi/aws @pulumi/awsx @pulumi/cloudflare @pulumi/github @pulumi/tls @pulumi/random` there and `pulumi up`").arg("dir", z9.string(), { description: "where the Pulumi project goes" }).option("stack", z9.string(), { default: "prod", description: "the stack" }).option("host", z9.string(), {
|
|
714
|
-
default: "",
|
|
715
|
-
description: "the public host name, as in splates.example.com; the saved tunnel's by default"
|
|
716
|
-
}).option("email", z9.string(), {
|
|
828
|
+
var splatesRunners = cli9("runners").description(`the runners \`splates run\` makes for itself in the project's cloud (\`cloud\` in splates.config.ts) when \`splates server\` has none for it: each takes as many commands at once as fit in its CPUs and memory, another is made when none has room, within \`splates budget\`, and each is deleted once idle ${DIRECT_IDLE.mins} minutes`);
|
|
829
|
+
splatesRunners.command("list").description("list them: name, size and price, the address Fly's proxy reaches them at, and busy, idle, or the machine's state").option("json", z10.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
|
|
830
|
+
const runners = await opened(ctx, (each) => each.list());
|
|
831
|
+
if (opts.json) {
|
|
832
|
+
ctx.log.info(JSON.stringify(runners));
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
ctx.log.info(runners.length === 0 ? "no runner is up" : runners.map(runnerLine).join(`
|
|
836
|
+
`));
|
|
837
|
+
});
|
|
838
|
+
splatesRunners.command("reap").description(`delete each runner idle ${DIRECT_IDLE.mins} minutes or more, every idle one once the budget's day or month is spent, and any that never booted; with --watch, again every so often for as long as any is up, which \`splates run\` leaves running behind it, one on this machine at a time, so none is left running unused`).option("watch", z10.string(), {
|
|
717
839
|
default: "",
|
|
718
|
-
description: "
|
|
719
|
-
}).option("
|
|
720
|
-
default: "us-east-1",
|
|
721
|
-
description: "the AWS region"
|
|
722
|
-
}).option("projects", z9.string(), {
|
|
840
|
+
description: "how long to wait before each look, as 10m or 90s, looking again for as long as any runner is up"
|
|
841
|
+
}).option("idle", z10.string(), {
|
|
723
842
|
default: "",
|
|
724
|
-
description:
|
|
843
|
+
description: `how long a runner must have stood idle, as 10m or 0s; ${DIRECT_IDLE.mins}m by default`
|
|
725
844
|
}).action(async (opts, ctx) => {
|
|
726
|
-
const
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
const org = await pulumiOrg(ctx.ps);
|
|
736
|
-
const home = new CoordinatorBox({ ps: ctx.ps, fs: ctx.fs }).home;
|
|
737
|
-
const { written, todo } = await infraConfigure({
|
|
738
|
-
dir: opts.dir,
|
|
739
|
-
stack: opts.stack,
|
|
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 } : {},
|
|
748
|
-
fs: ctx.fs,
|
|
749
|
-
ps: ctx.ps,
|
|
750
|
-
log: ctx.log,
|
|
751
|
-
fetch: ctx.fetch
|
|
752
|
-
});
|
|
753
|
-
await writeInfraConfig(ctx.fs, home, {
|
|
754
|
-
dir: resolve2(opts.dir),
|
|
755
|
-
stack: opts.stack
|
|
756
|
-
});
|
|
757
|
-
ctx.log.info(written.length === 0 ? "already so: nothing written" : `wrote ${written.join(", ")}`);
|
|
758
|
-
ctx.log.info(`Pulumi.${opts.stack}.yaml stays out of git: it holds your own accounts and ids, and init writes it again from what splates keeps`);
|
|
759
|
-
for (const command of todo) {
|
|
760
|
-
ctx.log.info(`still to run in ${opts.dir}: ${command}`);
|
|
845
|
+
const idle = opts.idle === "" ? DIRECT_IDLE : lengthOf(opts.idle);
|
|
846
|
+
if (opts.watch === "") {
|
|
847
|
+
const reaped = await opened(ctx, (each) => each.reap(idle));
|
|
848
|
+
ctx.log.info(reaped.length === 0 ? "nothing to reap" : reaped.map((line) => `deleted ${line}`).join(`
|
|
849
|
+
`));
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (!await watching(ctx)) {
|
|
853
|
+
return;
|
|
761
854
|
}
|
|
762
|
-
|
|
763
|
-
|
|
855
|
+
const every = lengthOf(opts.watch);
|
|
856
|
+
for (;; ) {
|
|
857
|
+
await sleep(every);
|
|
858
|
+
const left = await opened(ctx, async (each) => {
|
|
859
|
+
await each.reap(idle);
|
|
860
|
+
return (await each.list()).length;
|
|
861
|
+
});
|
|
862
|
+
if (left === 0) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
764
865
|
}
|
|
765
866
|
});
|
|
867
|
+
async function watching(ctx) {
|
|
868
|
+
const file = reaperFile(ctx);
|
|
869
|
+
const held = Number((await ctx.fs.read(file).catch(() => "")).trim());
|
|
870
|
+
if (Number.isInteger(held) && held > 0 && held !== process.pid && ctx.ps.alive(held)) {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
await ctx.fs.write(file, String(process.pid));
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
async function opened(ctx, use) {
|
|
877
|
+
const { stdout, exitCode } = await ctx.ps.spawnCapture([
|
|
878
|
+
"git",
|
|
879
|
+
"rev-parse",
|
|
880
|
+
"--show-toplevel"
|
|
881
|
+
]);
|
|
882
|
+
if (exitCode !== 0) {
|
|
883
|
+
fail6("usage", "run it in a splates project", {});
|
|
884
|
+
}
|
|
885
|
+
const { cloud } = await loadConfig3(stdout.trim(), { fs: ctx.fs });
|
|
886
|
+
if (cloud === null) {
|
|
887
|
+
fail6("no_runner", "this project names no `cloud` in splates.config.ts, so `splates run` makes no runners", {});
|
|
888
|
+
}
|
|
889
|
+
try {
|
|
890
|
+
return await withRunners(ctx, cloud, use);
|
|
891
|
+
} catch (error) {
|
|
892
|
+
if (error instanceof NoRunner) {
|
|
893
|
+
fail6("no_runner", error.message, {});
|
|
894
|
+
}
|
|
895
|
+
throw error;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
function lengthOf(text) {
|
|
899
|
+
return parseTimeout(text) ?? fail6("usage", `${text} is no length of time: name one as 10m or 90s`, {});
|
|
900
|
+
}
|
|
901
|
+
function runnerLine(runner) {
|
|
902
|
+
const size = runner.size === null ? runner.type : sizeLabel(runner.size, ...runner.dollarsPerHour === null ? [] : [runner.dollarsPerHour]);
|
|
903
|
+
return `${runner.id} ${size} ${runner.address ?? "no address yet"} ${runner.state}`;
|
|
904
|
+
}
|
|
905
|
+
function reaperFile(ctx) {
|
|
906
|
+
return join2(splatesHome2(ctx.ps), REAPER_FILE);
|
|
907
|
+
}
|
|
908
|
+
splatesRunners.command("serve").description("serve only job channels at /api/jobs over authenticated WebSockets; for the runner image, with SPLATES_RUNNER_SECRET from its hosting environment").option("port", z10.coerce.number().int().min(0).max(65535), {
|
|
909
|
+
default: 8080,
|
|
910
|
+
description: "HTTP port behind the hosting proxy"
|
|
911
|
+
}).action(({ port }, { ps }) => runnerServe({ port, ps }));
|
|
766
912
|
|
|
767
913
|
// server.ts
|
|
768
|
-
|
|
769
|
-
|
|
914
|
+
import {
|
|
915
|
+
DEFAULT_PORT,
|
|
916
|
+
serveDev
|
|
917
|
+
} from "@stringsync/splates-server/server";
|
|
918
|
+
import { cli as cli10 } from "webappwiz/cmd";
|
|
919
|
+
import { z as z11 } from "zod";
|
|
920
|
+
var splatesServer = cli10("server").description("the splates server: run it on this machine").fallback("run");
|
|
921
|
+
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", z11.coerce.number(), {
|
|
770
922
|
default: DEFAULT_PORT,
|
|
771
923
|
description: "port to listen on, or the next open one above it"
|
|
772
|
-
}).option("allow-hosts",
|
|
924
|
+
}).option("allow-hosts", z11.string(), {
|
|
773
925
|
default: "",
|
|
774
926
|
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",
|
|
927
|
+
}).option("access-team", z11.string(), {
|
|
776
928
|
default: "",
|
|
777
929
|
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",
|
|
930
|
+
}).option("access-aud", z11.string(), {
|
|
779
931
|
default: "",
|
|
780
932
|
description: "the AUD tag of that Access application"
|
|
781
|
-
}).option("tunnel",
|
|
933
|
+
}).option("tunnel", z11.string(), {
|
|
782
934
|
default: "",
|
|
783
935
|
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",
|
|
936
|
+
}).option("tunnel-name", z11.string(), {
|
|
785
937
|
default: "",
|
|
786
938
|
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",
|
|
939
|
+
}).option("ports-host", z11.string(), {
|
|
788
940
|
default: "",
|
|
789
941
|
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(), {
|
|
942
|
+
}).option("keepalive", z11.boolean(), {
|
|
794
943
|
default: false,
|
|
795
944
|
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",
|
|
945
|
+
}).option("slot", z11.boolean(), {
|
|
797
946
|
default: false,
|
|
798
947
|
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
948
|
}).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
949
|
|
|
923
950
|
// setup.ts
|
|
924
|
-
import { dirname } from "node:path";
|
|
951
|
+
import { dirname as dirname3 } from "node:path";
|
|
925
952
|
import { SYSTEM_STORES as SYSTEM_STORES3 } from "@stringsync/splates-server/creds";
|
|
926
953
|
import { fail as fail7 } from "@stringsync/splates-server/exit";
|
|
927
|
-
import { splatesHome as
|
|
954
|
+
import { splatesHome as splatesHome3 } from "@stringsync/splates-server/models";
|
|
928
955
|
import { Projects as Projects2 } from "@stringsync/splates-server/projects";
|
|
956
|
+
import {
|
|
957
|
+
openRepository
|
|
958
|
+
} from "@stringsync/splates-server/records";
|
|
959
|
+
import { Replicator as Replicator2 } from "@stringsync/splates-server/replica";
|
|
929
960
|
import {
|
|
930
961
|
SETUP_AREAS,
|
|
931
962
|
Setup,
|
|
@@ -990,7 +1021,7 @@ async function setupOf(deps) {
|
|
|
990
1021
|
return new Setup({
|
|
991
1022
|
fs,
|
|
992
1023
|
ps,
|
|
993
|
-
home:
|
|
1024
|
+
home: splatesHome3(ps),
|
|
994
1025
|
device: secrets.device,
|
|
995
1026
|
fetch,
|
|
996
1027
|
workers: deps.ps.env("SPLATES_DEV") ? () => fetchWorkers({ dev: "", log: deps.log, ps: deps.ps }) : undefined,
|
|
@@ -1002,9 +1033,20 @@ async function setupOf(deps) {
|
|
|
1002
1033
|
ps: deps.ps
|
|
1003
1034
|
})
|
|
1004
1035
|
} : undefined,
|
|
1005
|
-
project: root === null ? undefined : setupProjectAt(root, { fs, ps }
|
|
1036
|
+
project: root === null ? undefined : setupProjectAt(root, { fs, ps }, {
|
|
1037
|
+
replica: () => withRecords(deps, root, ({ storage }) => Replicator2.recorded(storage)),
|
|
1038
|
+
kept: (name) => withRecords(deps, root, async ({ creds }) => await creds.credentials.get(name) !== undefined)
|
|
1039
|
+
})
|
|
1006
1040
|
}, SETUP_AREAS);
|
|
1007
1041
|
}
|
|
1042
|
+
async function withRecords(deps, root, read) {
|
|
1043
|
+
const records = await openRepository(deps, { at: root });
|
|
1044
|
+
try {
|
|
1045
|
+
return await read(records);
|
|
1046
|
+
} finally {
|
|
1047
|
+
records.dispose();
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1008
1050
|
async function repoRoot(ps) {
|
|
1009
1051
|
const { exitCode, stdout } = await ps.spawnCapture([
|
|
1010
1052
|
"git",
|
|
@@ -1014,7 +1056,7 @@ async function repoRoot(ps) {
|
|
|
1014
1056
|
"--path-format=absolute",
|
|
1015
1057
|
"--git-common-dir"
|
|
1016
1058
|
]);
|
|
1017
|
-
return exitCode === 0 ?
|
|
1059
|
+
return exitCode === 0 ? dirname3(stdout.trim()) : null;
|
|
1018
1060
|
}
|
|
1019
1061
|
var MARKS = {
|
|
1020
1062
|
done: color2.green("✓"),
|
|
@@ -1091,10 +1133,10 @@ function printUndo(log, plan) {
|
|
|
1091
1133
|
|
|
1092
1134
|
// slots.ts
|
|
1093
1135
|
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",
|
|
1136
|
+
import { cli as cli11 } from "webappwiz/cmd";
|
|
1137
|
+
import { z as z12 } from "zod";
|
|
1138
|
+
var splatesSlots = cli11("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");
|
|
1139
|
+
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", z12.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
|
|
1098
1140
|
const held = await Slots.ofMachine(ctx).status();
|
|
1099
1141
|
if (opts.json) {
|
|
1100
1142
|
ctx.log.info(JSON.stringify(held.map(({ slot, tree, pid, orphaned }) => ({
|
|
@@ -1137,7 +1179,7 @@ import {
|
|
|
1137
1179
|
tasksNamed as tasksNamed2,
|
|
1138
1180
|
taskUpdate
|
|
1139
1181
|
} from "@stringsync/splates-server/board";
|
|
1140
|
-
import { commaList as
|
|
1182
|
+
import { commaList as commaList4 } from "@stringsync/splates-server/comma-list";
|
|
1141
1183
|
import { refuseGroomer as refuseGroomer3 } from "@stringsync/splates-server/conversations";
|
|
1142
1184
|
import { fail as fail8 } from "@stringsync/splates-server/exit";
|
|
1143
1185
|
import { repository as repository8 } from "@stringsync/splates-server/records";
|
|
@@ -1146,10 +1188,10 @@ import {
|
|
|
1146
1188
|
taskWait,
|
|
1147
1189
|
untilAfter as untilAfter2
|
|
1148
1190
|
} from "@stringsync/splates-server/tasks";
|
|
1149
|
-
import { cli as
|
|
1191
|
+
import { cli as cli12 } from "webappwiz/cmd";
|
|
1150
1192
|
import { Duration as Duration2 } from "webappwiz/time";
|
|
1151
|
-
import { z as
|
|
1152
|
-
var splatesTask =
|
|
1193
|
+
import { z as z13 } from "zod";
|
|
1194
|
+
var splatesTask = cli12("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
1195
|
async function projectFrom(ctx) {
|
|
1154
1196
|
if (ctx.elsewhere === null) {
|
|
1155
1197
|
return;
|
|
@@ -1157,31 +1199,29 @@ async function projectFrom(ctx) {
|
|
|
1157
1199
|
return (await ctx.projects.list()).find((each) => each.root === ctx.git.root)?.slug;
|
|
1158
1200
|
}
|
|
1159
1201
|
function adding(name, description, ready) {
|
|
1160
|
-
splatesTask.command(name).use(repository8({ needs: "queue" })).description(description).arg("subject",
|
|
1202
|
+
splatesTask.command(name).use(repository8({ needs: "queue" })).description(description).arg("subject", z13.string(), {
|
|
1161
1203
|
description: "what is left to do, in a line"
|
|
1162
|
-
}).arg("text",
|
|
1204
|
+
}).arg("text", z13.string(), {
|
|
1163
1205
|
default: "",
|
|
1164
1206
|
description: "whatever more there is to say about it, in markdown"
|
|
1165
|
-
}).option("slug",
|
|
1207
|
+
}).option("slug", z13.string(), {
|
|
1166
1208
|
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
1209
|
}).option("position", position, {
|
|
1168
1210
|
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(), {
|
|
1211
|
+
}).repeated("file", z13.string(), {
|
|
1212
|
+
description: "files to attach, comma separated or the flag repeated: copied under .git/splates/tasks/<n>/"
|
|
1213
|
+
}).repeated("blocked-by", z13.string(), {
|
|
1214
|
+
description: "numbers of the tasks blocking it, comma separated or the flag repeated: each has to merge before this one starts"
|
|
1215
|
+
}).option("lane", z13.string(), {
|
|
1176
1216
|
default: "",
|
|
1177
1217
|
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",
|
|
1218
|
+
}).option("agent", z13.string(), {
|
|
1179
1219
|
default: "",
|
|
1180
1220
|
description: "the agent on the roster (`splates agents`) that should work it, which Start on the `splates server` board offers first"
|
|
1181
|
-
}).option("model",
|
|
1221
|
+
}).option("model", z13.string(), {
|
|
1182
1222
|
default: "",
|
|
1183
1223
|
description: "with --agent: the model, by its id or name on the roster"
|
|
1184
|
-
}).option("effort",
|
|
1224
|
+
}).option("effort", z13.string(), {
|
|
1185
1225
|
default: "",
|
|
1186
1226
|
description: "with --agent: the thinking level, one the agent offers (default: the agent's own)"
|
|
1187
1227
|
}).action(async (opts, ctx) => {
|
|
@@ -1199,8 +1239,8 @@ function adding(name, description, ready) {
|
|
|
1199
1239
|
slug: opts.slug,
|
|
1200
1240
|
text: opts.text,
|
|
1201
1241
|
position: opts.position,
|
|
1202
|
-
files:
|
|
1203
|
-
blockedBy: taskIds(opts["blocked-by"]),
|
|
1242
|
+
files: commaList4(opts.file.join(",")),
|
|
1243
|
+
blockedBy: taskIds(opts["blocked-by"].join(",")),
|
|
1204
1244
|
lane: laneIn(opts.lane) ?? null,
|
|
1205
1245
|
assignee,
|
|
1206
1246
|
origin,
|
|
@@ -1211,10 +1251,10 @@ function adding(name, description, ready) {
|
|
|
1211
1251
|
}
|
|
1212
1252
|
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
1253
|
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",
|
|
1254
|
+
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", z13.boolean(), { default: false, description: "emit JSON" }).option("open", z13.boolean(), {
|
|
1215
1255
|
default: false,
|
|
1216
1256
|
description: "only tasks no started one has taken up: the ones free to pick up"
|
|
1217
|
-
}).option("ready",
|
|
1257
|
+
}).option("ready", z13.boolean(), {
|
|
1218
1258
|
default: false,
|
|
1219
1259
|
description: "only tasks free to start now: none has taken them, and nothing blocking them is still open"
|
|
1220
1260
|
}).action(async (opts, ctx) => taskList(ctx, {
|
|
@@ -1223,53 +1263,49 @@ splatesTask.command("list").use(repository8({ needs: "read" })).description("eve
|
|
|
1223
1263
|
ready: opts.ready,
|
|
1224
1264
|
project: await projectFrom(ctx)
|
|
1225
1265
|
}));
|
|
1226
|
-
splatesTask.command("show").use(repository8({ needs: "read" })).description("one task in full: where it stands, its files, and its detail").arg("id",
|
|
1266
|
+
splatesTask.command("show").use(repository8({ needs: "read" })).description("one task in full: where it stands, its files, and its detail").arg("id", z13.coerce.number().int().positive(), { description: "task number" }).option("json", z13.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => taskShow(ctx, opts.id, {
|
|
1227
1267
|
json: opts.json,
|
|
1228
1268
|
project: await projectFrom(ctx),
|
|
1229
1269
|
others: (await ctx.projects.list()).filter((project) => project.root !== ctx.git.root).map((project) => project.slug)
|
|
1230
1270
|
}));
|
|
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",
|
|
1271
|
+
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", z13.coerce.number().int().positive(), { description: "task number" }).arg("text", z13.string(), {
|
|
1232
1272
|
default: "",
|
|
1233
1273
|
description: "whatever more there is to say about it, in markdown; leave out to keep the detail"
|
|
1234
|
-
}).option("subject",
|
|
1274
|
+
}).option("subject", z13.string(), {
|
|
1235
1275
|
default: "",
|
|
1236
1276
|
description: "what is left to do, in a line; leave out to keep it"
|
|
1237
|
-
}).option("slug",
|
|
1277
|
+
}).option("slug", z13.string(), {
|
|
1238
1278
|
default: "",
|
|
1239
1279
|
description: "a new name for the task that takes it (lowercase-with-dashes); leave out to keep it"
|
|
1240
1280
|
}).option("position", position, {
|
|
1241
1281
|
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(), {
|
|
1282
|
+
}).repeated("file", z13.string(), {
|
|
1283
|
+
description: "files to attach, comma separated or the flag repeated"
|
|
1284
|
+
}).repeated("remove-file", z13.string(), {
|
|
1285
|
+
description: "attached files to drop, comma separated or the flag repeated, by path or stored name"
|
|
1286
|
+
}).repeated("blocked-by", z13.string(), {
|
|
1287
|
+
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"
|
|
1288
|
+
}).repeated("remove-blocked-by", z13.string(), {
|
|
1289
|
+
description: "numbers of tasks no longer blocking it, comma separated or the flag repeated"
|
|
1290
|
+
}).option("lane", z13.string(), {
|
|
1255
1291
|
default: "",
|
|
1256
1292
|
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",
|
|
1293
|
+
}).option("agent", z13.string(), {
|
|
1258
1294
|
default: "",
|
|
1259
1295
|
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",
|
|
1296
|
+
}).option("model", z13.string(), {
|
|
1261
1297
|
default: "",
|
|
1262
1298
|
description: "with --agent: the model, by its id or name on the roster"
|
|
1263
|
-
}).option("effort",
|
|
1299
|
+
}).option("effort", z13.string(), {
|
|
1264
1300
|
default: "",
|
|
1265
1301
|
description: "with --agent: the thinking level, one the agent offers (default: the agent's own)"
|
|
1266
|
-
}).option("ready",
|
|
1302
|
+
}).option("ready", z13.boolean(), {
|
|
1267
1303
|
default: false,
|
|
1268
1304
|
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",
|
|
1305
|
+
}).option("manual", z13.boolean(), {
|
|
1270
1306
|
default: false,
|
|
1271
1307
|
description: "queue it by hand only: a queued lane passes over it, and it starts once you `splates queue add` it"
|
|
1272
|
-
}).option("auto",
|
|
1308
|
+
}).option("auto", z13.boolean(), {
|
|
1273
1309
|
default: false,
|
|
1274
1310
|
description: "let a queued lane queue it again, undoing --manual"
|
|
1275
1311
|
}).action(async (opts, ctx) => {
|
|
@@ -1283,21 +1319,21 @@ splatesTask.command("update").use(repository8({ needs: "tasks" })).description("
|
|
|
1283
1319
|
slug: opts.slug,
|
|
1284
1320
|
text: opts.text,
|
|
1285
1321
|
position: opts.position,
|
|
1286
|
-
files:
|
|
1287
|
-
removeFiles:
|
|
1288
|
-
blockedBy: taskIds(opts["blocked-by"]),
|
|
1289
|
-
removeBlockedBy: taskIds(opts["remove-blocked-by"]),
|
|
1322
|
+
files: commaList4(opts.file.join(",")),
|
|
1323
|
+
removeFiles: commaList4(opts["remove-file"].join(",")),
|
|
1324
|
+
blockedBy: taskIds(opts["blocked-by"].join(",")),
|
|
1325
|
+
removeBlockedBy: taskIds(opts["remove-blocked-by"].join(",")),
|
|
1290
1326
|
lane: laneIn(opts.lane),
|
|
1291
1327
|
assignee,
|
|
1292
1328
|
ready: opts.ready,
|
|
1293
1329
|
manual: opts.manual ? true : opts.auto ? false : undefined
|
|
1294
1330
|
}));
|
|
1295
1331
|
});
|
|
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",
|
|
1332
|
+
splatesTask.command("remove").use(repository8({ needs: "tasks" })).description("drop a queued task that was done some other way or no longer applies").arg("id", z13.coerce.number().int().positive(), { description: "task number" }).action(async (opts, ctx) => {
|
|
1297
1333
|
await refuseGroomer3(ctx.service, "task remove", await ctx.tasks.find(opts.id));
|
|
1298
1334
|
await ctx.journal.record("task remove", null, () => taskRemove(ctx, opts.id), (removed) => ({ tasks: tasksNamed2([removed]) }));
|
|
1299
1335
|
});
|
|
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",
|
|
1336
|
+
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", z13.coerce.number().int().positive(), { description: "task number" }).option("timeout-secs", z13.coerce.number(), {
|
|
1301
1337
|
default: DEFAULT_TIMEOUT2.secs,
|
|
1302
1338
|
description: "how long to wait before giving up"
|
|
1303
1339
|
}).action(async (opts, ctx) => {
|
|
@@ -1312,36 +1348,36 @@ splatesTask.command("wait").use(repository8({ needs: "read" })).description("blo
|
|
|
1312
1348
|
|
|
1313
1349
|
// tunnel.ts
|
|
1314
1350
|
import { repository as repository9 } from "@stringsync/splates-server/records";
|
|
1315
|
-
import { savedTunnelOf
|
|
1351
|
+
import { savedTunnelOf } from "@stringsync/splates-server/server";
|
|
1316
1352
|
import {
|
|
1317
1353
|
API_TOKEN,
|
|
1318
1354
|
tunnelRemove,
|
|
1319
1355
|
tunnelSetup
|
|
1320
1356
|
} 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",
|
|
1357
|
+
import { cli as cli13 } from "webappwiz/cmd";
|
|
1358
|
+
import { z as z14 } from "zod";
|
|
1359
|
+
var splatesTunnel = cli13("tunnel").description("reach `splates server` from elsewhere: a Cloudflare tunnel with Cloudflare Access in front, set up once per machine");
|
|
1360
|
+
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", z14.string(), {
|
|
1325
1361
|
description: "the public host name, in a zone of the account, as in splates.example.com"
|
|
1326
|
-
}).option("email",
|
|
1362
|
+
}).option("email", z14.string(), {
|
|
1327
1363
|
default: "",
|
|
1328
1364
|
description: "comma-separated email addresses Access lets in"
|
|
1329
|
-
}).option("name",
|
|
1365
|
+
}).option("name", z14.string(), {
|
|
1330
1366
|
default: "splates",
|
|
1331
1367
|
description: "what the tunnel, the Access application and its policy are called"
|
|
1332
|
-
}).option("team",
|
|
1368
|
+
}).option("team", z14.string(), {
|
|
1333
1369
|
default: "",
|
|
1334
1370
|
description: "the Zero Trust team to make, as in <team>.cloudflareaccess.com, when the account has none yet"
|
|
1335
1371
|
}).action(async (opts, ctx) => ctx.journal.record("tunnel setup", null, async () => tunnelSetup({
|
|
1336
1372
|
...opts,
|
|
1337
1373
|
creds: ctx.creds,
|
|
1338
1374
|
task: await here(ctx),
|
|
1339
|
-
saved:
|
|
1375
|
+
saved: savedTunnelOf(ctx),
|
|
1340
1376
|
log: ctx.log,
|
|
1341
1377
|
fetch: ctx.fetch
|
|
1342
1378
|
})));
|
|
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 =
|
|
1379
|
+
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", z14.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
|
|
1380
|
+
const saved = savedTunnelOf(ctx);
|
|
1345
1381
|
const record = await saved.read();
|
|
1346
1382
|
const token = record === null ? false : await saved.token() !== undefined;
|
|
1347
1383
|
if (opts.json) {
|
|
@@ -1362,7 +1398,7 @@ splatesTunnel.command("status").description("print the tunnel `splates tunnel se
|
|
|
1362
1398
|
`));
|
|
1363
1399
|
});
|
|
1364
1400
|
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:
|
|
1401
|
+
saved: savedTunnelOf(ctx),
|
|
1366
1402
|
slots: slotTunnelsOf(ctx),
|
|
1367
1403
|
creds: ctx.creds,
|
|
1368
1404
|
task: await here(ctx),
|
|
@@ -1372,7 +1408,7 @@ splatesTunnel.command("remove").use(repository9({ needs: "own" })).description(`
|
|
|
1372
1408
|
})));
|
|
1373
1409
|
|
|
1374
1410
|
// worker.ts
|
|
1375
|
-
import { splatesHome as
|
|
1411
|
+
import { splatesHome as splatesHome4 } from "@stringsync/splates-server/models";
|
|
1376
1412
|
import {
|
|
1377
1413
|
addWorker,
|
|
1378
1414
|
drainWorker,
|
|
@@ -1381,25 +1417,24 @@ import {
|
|
|
1381
1417
|
removeWorker,
|
|
1382
1418
|
setWorkerSlots,
|
|
1383
1419
|
WORKER_JOIN,
|
|
1384
|
-
wakeWorker,
|
|
1385
1420
|
workerJoin,
|
|
1386
1421
|
workerRecipe,
|
|
1387
1422
|
workerRun
|
|
1388
1423
|
} from "@stringsync/splates-server/workers";
|
|
1389
|
-
import { cli as
|
|
1424
|
+
import { cli as cli15 } from "webappwiz/cmd";
|
|
1390
1425
|
import { CompositeLogger } from "webappwiz/log";
|
|
1391
|
-
import { z as
|
|
1426
|
+
import { z as z16 } from "zod";
|
|
1392
1427
|
|
|
1393
1428
|
// waiting.ts
|
|
1394
1429
|
import { listQueue, moveQueued } from "@stringsync/splates-server/workers";
|
|
1395
|
-
import { cli as
|
|
1396
|
-
import { z as
|
|
1430
|
+
import { cli as cli14 } from "webappwiz/cmd";
|
|
1431
|
+
import { z as z15 } from "zod";
|
|
1397
1432
|
var DEV2 = {
|
|
1398
1433
|
default: "",
|
|
1399
1434
|
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
1435
|
};
|
|
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",
|
|
1436
|
+
var splatesWaiting = cli14("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");
|
|
1437
|
+
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", z15.string(), DEV2).option("json", z15.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => listQueue({
|
|
1403
1438
|
dev: opts.dev,
|
|
1404
1439
|
json: opts.json,
|
|
1405
1440
|
log: ctx.log,
|
|
@@ -1407,11 +1442,11 @@ splatesWaiting.command("list").description("list the agents waiting for a slot,
|
|
|
1407
1442
|
ps: ctx.ps,
|
|
1408
1443
|
send: ctx.fetch
|
|
1409
1444
|
}));
|
|
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",
|
|
1445
|
+
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", z15.string(), {
|
|
1411
1446
|
description: "the task it works, or its conversation's id"
|
|
1412
|
-
}).arg("position",
|
|
1447
|
+
}).arg("position", z15.coerce.number().int(), {
|
|
1413
1448
|
description: "where in line to put it, from 1"
|
|
1414
|
-
}).option("dev",
|
|
1449
|
+
}).option("dev", z15.string(), DEV2).option("json", z15.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => moveQueued({
|
|
1415
1450
|
which: opts.which,
|
|
1416
1451
|
position: opts.position,
|
|
1417
1452
|
dev: opts.dev,
|
|
@@ -1423,29 +1458,29 @@ splatesWaiting.command("move").description("move an agent waiting for a slot to
|
|
|
1423
1458
|
}));
|
|
1424
1459
|
|
|
1425
1460
|
// worker.ts
|
|
1426
|
-
var splatesWorker =
|
|
1427
|
-
splatesWorker.command("run").description("run a worker on this machine until stopped: it dials `splates server`, by default the one on this machine through a socket in ~/.splates only you may open, and dials again whenever the link drops, while what it runs carries on; it stops reading a process's output while the server is away too long, so nothing is lost. Once its agents have moved off it, a server on another build has it restart on that build, through `bunx @stringsync/splates@<build>`, as it does when it refuses this worker for another protocol; one that says no build has it exit `outdated`").option("server",
|
|
1461
|
+
var splatesWorker = cli15("worker").description("the machines that run tasks for `splates server`: each dials out to it and runs what it is given, agents, terminals and the git and process reads around them, on its own machine");
|
|
1462
|
+
splatesWorker.command("run").description("run a worker on this machine until stopped: it dials `splates server`, by default the one on this machine through a socket in ~/.splates only you may open, and dials again whenever the link drops, while what it runs carries on; it stops reading a process's output while the server is away too long, so nothing is lost. Once its agents have moved off it, a server on another build has it restart on that build, through `bunx @stringsync/splates@<build>`, as it does when it refuses this worker for another protocol; one that says no build has it exit `outdated`").option("server", z16.string(), {
|
|
1428
1463
|
default: "",
|
|
1429
1464
|
description: "the `splates server` to dial: its URL, whose /api/workers it opens, or unix:<path>; by default this machine's"
|
|
1430
|
-
}).option("coordinator",
|
|
1465
|
+
}).option("coordinator", z16.string(), {
|
|
1431
1466
|
default: "",
|
|
1432
1467
|
description: "the old name for --server, still read for workers installed before it, and passed to a build older than it"
|
|
1433
|
-
}).option("slots",
|
|
1468
|
+
}).option("slots", z16.coerce.number().int().min(0), {
|
|
1434
1469
|
default: 0,
|
|
1435
1470
|
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",
|
|
1471
|
+
}).option("id", z16.string(), {
|
|
1437
1472
|
default: "",
|
|
1438
1473
|
description: "what the server knows this worker by, kept across restarts so it picks up where it was; by default the id it joined as, kept beside its key, else the machine's host name"
|
|
1439
|
-
}).option("join",
|
|
1474
|
+
}).option("join", z16.string(), {
|
|
1440
1475
|
default: "",
|
|
1441
1476
|
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",
|
|
1477
|
+
}).option("ephemeral", z16.boolean(), {
|
|
1443
1478
|
default: false,
|
|
1444
1479
|
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",
|
|
1480
|
+
}).option("key-file", z16.boolean(), {
|
|
1446
1481
|
default: false,
|
|
1447
1482
|
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",
|
|
1483
|
+
}).option("quiet", z16.boolean(), {
|
|
1449
1484
|
default: false,
|
|
1450
1485
|
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
1486
|
}).action((opts, ctx) => workerRun({
|
|
@@ -1459,19 +1494,19 @@ splatesWorker.command("run").description("run a worker on this machine until sto
|
|
|
1459
1494
|
ps: ctx.ps,
|
|
1460
1495
|
fs: ctx.fs
|
|
1461
1496
|
}));
|
|
1462
|
-
splatesWorker.command("join").description("spend a join token from `splates worker add` and keep the key it answers, as `splates worker run --join` does, then exit, for an installer that leaves running the worker to a service").option("server",
|
|
1497
|
+
splatesWorker.command("join").description("spend a join token from `splates worker add` and keep the key it answers, as `splates worker run --join` does, then exit, for an installer that leaves running the worker to a service").option("server", z16.string(), {
|
|
1463
1498
|
default: "",
|
|
1464
1499
|
description: "the `splates server` to join: its URL"
|
|
1465
|
-
}).option("coordinator",
|
|
1500
|
+
}).option("coordinator", z16.string(), {
|
|
1466
1501
|
default: "",
|
|
1467
1502
|
description: "the old name for --server"
|
|
1468
|
-
}).option("join",
|
|
1503
|
+
}).option("join", z16.string(), {
|
|
1469
1504
|
default: "",
|
|
1470
1505
|
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",
|
|
1506
|
+
}).option("id", z16.string(), {
|
|
1472
1507
|
default: "",
|
|
1473
1508
|
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",
|
|
1509
|
+
}).option("key-file", z16.boolean(), {
|
|
1475
1510
|
default: false,
|
|
1476
1511
|
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
1512
|
}).action((opts, ctx) => workerJoin({
|
|
@@ -1484,10 +1519,10 @@ splatesWorker.command("join").description("spend a join token from `splates work
|
|
|
1484
1519
|
fs: ctx.fs
|
|
1485
1520
|
}));
|
|
1486
1521
|
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",
|
|
1522
|
+
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", z16.string(), {
|
|
1488
1523
|
default: "",
|
|
1489
1524
|
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",
|
|
1525
|
+
}).option("json", z16.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => listWorkers({
|
|
1491
1526
|
dev: opts.dev,
|
|
1492
1527
|
json: opts.json,
|
|
1493
1528
|
log: ctx.log,
|
|
@@ -1499,16 +1534,16 @@ var dev = {
|
|
|
1499
1534
|
default: "",
|
|
1500
1535
|
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
1536
|
};
|
|
1502
|
-
splatesWorker.command("add").description("add a worker to the running `splates server`: prints a join token, single use and good for 15 minutes, for `splates worker run --join <token>` on the machine to join, which gets a key of its own that `splates worker remove` revokes; with --local N, makes this machine's own worker run N agents at once and take new ones again, adding it back if it was removed; with --provider, has the `workerProvider` in splates.config.ts make one, booted from the snapshot splates builds for it, which joins on its own and whose machine `splates worker remove` destroys").option("local",
|
|
1537
|
+
splatesWorker.command("add").description("add a worker to the running `splates server`: prints a join token, single use and good for 15 minutes, for `splates worker run --join <token>` on the machine to join, which gets a key of its own that `splates worker remove` revokes; with --local N, makes this machine's own worker run N agents at once and take new ones again, adding it back if it was removed; with --provider, has the `workerProvider` in splates.config.ts make one, booted from the snapshot splates builds for it, which joins on its own and whose machine `splates worker remove` destroys").option("local", z16.coerce.number().int().min(0), {
|
|
1503
1538
|
default: 0,
|
|
1504
1539
|
description: "this machine's own worker, with this many slots"
|
|
1505
|
-
}).option("provider",
|
|
1540
|
+
}).option("provider", z16.boolean(), {
|
|
1506
1541
|
default: false,
|
|
1507
1542
|
description: "have the server's worker provider make the worker, of its cheapest type unless --type says"
|
|
1508
|
-
}).option("type",
|
|
1543
|
+
}).option("type", z16.string(), {
|
|
1509
1544
|
default: "",
|
|
1510
1545
|
description: "the worker provider's type to make, as it names it; implies --provider"
|
|
1511
|
-
}).option("dev",
|
|
1546
|
+
}).option("dev", z16.string(), dev).action((opts, ctx) => addWorker({
|
|
1512
1547
|
local: opts.local,
|
|
1513
1548
|
provider: opts.provider,
|
|
1514
1549
|
type: opts.type,
|
|
@@ -1518,12 +1553,12 @@ splatesWorker.command("add").description("add a worker to the running `splates s
|
|
|
1518
1553
|
ps: ctx.ps,
|
|
1519
1554
|
send: ctx.fetch
|
|
1520
1555
|
}));
|
|
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",
|
|
1556
|
+
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", z16.string(), {
|
|
1522
1557
|
description: "the worker, as `splates worker list` names it"
|
|
1523
|
-
}).option("undo",
|
|
1558
|
+
}).option("undo", z16.boolean(), {
|
|
1524
1559
|
default: false,
|
|
1525
1560
|
description: "take new agents again"
|
|
1526
|
-
}).option("dev",
|
|
1561
|
+
}).option("dev", z16.string(), dev).action((opts, ctx) => drainWorker({
|
|
1527
1562
|
id: opts.id,
|
|
1528
1563
|
undo: opts.undo,
|
|
1529
1564
|
dev: opts.dev,
|
|
@@ -1532,21 +1567,11 @@ splatesWorker.command("drain").description("take no new agents on a worker, lett
|
|
|
1532
1567
|
ps: ctx.ps,
|
|
1533
1568
|
send: ctx.fetch
|
|
1534
1569
|
}));
|
|
1535
|
-
splatesWorker.command("
|
|
1536
|
-
description: "the worker, as `splates worker list` names it"
|
|
1537
|
-
}).option("dev", z15.string(), dev).action((opts, ctx) => wakeWorker({
|
|
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(), {
|
|
1570
|
+
splatesWorker.command("slots").description("set how many agents a worker runs at once, from now on, over what it started with").arg("id", z16.string(), {
|
|
1546
1571
|
description: "the worker, as `splates worker list` names it"
|
|
1547
|
-
}).arg("slots",
|
|
1572
|
+
}).arg("slots", z16.coerce.number().int().min(1), {
|
|
1548
1573
|
description: "how many at once"
|
|
1549
|
-
}).option("dev",
|
|
1574
|
+
}).option("dev", z16.string(), dev).action((opts, ctx) => setWorkerSlots({
|
|
1550
1575
|
id: opts.id,
|
|
1551
1576
|
slots: opts.slots,
|
|
1552
1577
|
dev: opts.dev,
|
|
@@ -1555,12 +1580,12 @@ splatesWorker.command("slots").description("set how many agents a worker runs at
|
|
|
1555
1580
|
ps: ctx.ps,
|
|
1556
1581
|
send: ctx.fetch
|
|
1557
1582
|
}));
|
|
1558
|
-
splatesWorker.command("remove").description("remove a worker, revoking its key: one with agents on it drains and is removed once they end; --force ends them now, frees their slots, and places each again on another worker, to carry on its task from the checkpoint it last pushed. This machine's own worker, `local`, stops taking agents, its agents running to their end, and leaves this machine only coordinating until `splates worker add --local <slots>`; it takes no --force").arg("id",
|
|
1583
|
+
splatesWorker.command("remove").description("remove a worker, revoking its key: one with agents on it drains and is removed once they end; --force ends them now, frees their slots, and places each again on another worker, to carry on its task from the checkpoint it last pushed. This machine's own worker, `local`, stops taking agents, its agents running to their end, and leaves this machine only coordinating until `splates worker add --local <slots>`; it takes no --force").arg("id", z16.string(), {
|
|
1559
1584
|
description: "the worker, as `splates worker list` names it"
|
|
1560
|
-
}).option("force",
|
|
1585
|
+
}).option("force", z16.boolean(), {
|
|
1561
1586
|
default: false,
|
|
1562
1587
|
description: "end the agents on it now rather than wait for them"
|
|
1563
|
-
}).option("dev",
|
|
1588
|
+
}).option("dev", z16.string(), dev).action((opts, ctx) => removeWorker({
|
|
1564
1589
|
id: opts.id,
|
|
1565
1590
|
force: opts.force,
|
|
1566
1591
|
dev: opts.dev,
|
|
@@ -1571,7 +1596,7 @@ splatesWorker.command("remove").description("remove a worker, revoking its key:
|
|
|
1571
1596
|
}));
|
|
1572
1597
|
splatesWorker.mount("queue", splatesWaiting);
|
|
1573
1598
|
function quietLog(ctx) {
|
|
1574
|
-
const file = new FileLogger(ctx.fs, `${
|
|
1599
|
+
const file = new FileLogger(ctx.fs, `${splatesHome4(ctx.ps)}/worker.log`);
|
|
1575
1600
|
return new CompositeLogger([
|
|
1576
1601
|
{ info: () => {}, error: (...said) => ctx.log.error(...said) },
|
|
1577
1602
|
file
|
|
@@ -1592,7 +1617,7 @@ function projectArgs(argv) {
|
|
|
1592
1617
|
}
|
|
1593
1618
|
return { argv };
|
|
1594
1619
|
}
|
|
1595
|
-
var splates =
|
|
1620
|
+
var splates = cli16("splates").use(exits());
|
|
1596
1621
|
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
1622
|
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
1623
|
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,39 +1638,51 @@ splates.command("update").use(recordsFirst()).use(repository10({ needs: "own" })
|
|
|
1613
1638
|
ctx.ps.exit(code);
|
|
1614
1639
|
}
|
|
1615
1640
|
});
|
|
1616
|
-
splates.command("
|
|
1641
|
+
splates.command("run").description("run a command that needs a machine of its own (a full test run, an install) on a runner, in a checkout of this worktree as it stands, uncommitted edits included, its output streamed back and its exit code yours: one that has joined `splates server` when it has one, else, in a project whose splates.config.ts names a `cloud`, one `splates run` makes there itself, server or no server (`splates runners`), refusing with `no_runner` rather than run here when none can be had, as without the cloud's token or past `splates budget`. A project that names no cloud runs it here, 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, 1 by default. Run on a runner, it changes nothing here: files it writes stay there").option("cpus", z17.coerce.number().positive().optional(), {
|
|
1642
|
+
description: "the CPUs it asks of a runner, which takes as many commands at once as fit; the project's `job` in splates.config.ts, else 4, by default"
|
|
1643
|
+
}).option("memory", z17.string(), {
|
|
1644
|
+
default: "",
|
|
1645
|
+
description: "the memory it asks of a runner, as 8GB or 512MB; the project's `job` in splates.config.ts, else 8GB, by default"
|
|
1646
|
+
}).option("timeout", z17.string(), {
|
|
1647
|
+
default: "",
|
|
1648
|
+
description: "how long it may run, as 30m, 90s or 2h, before it is killed and exits 124, so a hung command holds no runner or slot; the project's `job.timeout` in splates.config.ts, else 30m, by default"
|
|
1649
|
+
}).rest("command", z17.string(), {
|
|
1617
1650
|
description: "the command and its arguments, after `--`"
|
|
1618
1651
|
}).action(async (opts, ctx) => {
|
|
1619
|
-
const code = await runHeavy(ctx, opts.command
|
|
1652
|
+
const code = await runHeavy(ctx, opts.command, undefined, {
|
|
1653
|
+
...opts.cpus === undefined ? {} : { cpus: opts.cpus },
|
|
1654
|
+
memory: opts.memory,
|
|
1655
|
+
timeout: opts.timeout
|
|
1656
|
+
});
|
|
1620
1657
|
if (code !== 0) {
|
|
1621
1658
|
ctx.ps.exit(code);
|
|
1622
1659
|
}
|
|
1623
1660
|
});
|
|
1624
1661
|
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",
|
|
1662
|
+
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", z17.string(), {
|
|
1626
1663
|
description: "the task's name, for its branch and worktree (lowercase-with-dashes)"
|
|
1627
|
-
}).option("base",
|
|
1664
|
+
}).option("base", z17.string(), {
|
|
1628
1665
|
default: "",
|
|
1629
1666
|
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",
|
|
1667
|
+
}).option("task", z17.string(), {
|
|
1631
1668
|
default: "",
|
|
1632
1669
|
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",
|
|
1670
|
+
}).option("goal", z17.string(), {
|
|
1634
1671
|
default: "",
|
|
1635
1672
|
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",
|
|
1673
|
+
}).option("agent", z17.string(), {
|
|
1637
1674
|
default: "",
|
|
1638
1675
|
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",
|
|
1676
|
+
}).option("model", z17.string(), {
|
|
1640
1677
|
default: "",
|
|
1641
1678
|
description: "with --agent: the model it works on, by its id or name on the roster (default: the agent's own)"
|
|
1642
|
-
}).option("effort",
|
|
1679
|
+
}).option("effort", z17.string(), {
|
|
1643
1680
|
default: "",
|
|
1644
1681
|
description: "with --agent: the thinking level it works at, one the agent offers (default: the agent's own)"
|
|
1645
|
-
}).option("prompt",
|
|
1682
|
+
}).option("prompt", z17.string(), {
|
|
1646
1683
|
default: "",
|
|
1647
1684
|
description: "with --agent: what to tell the agent beyond claiming the task"
|
|
1648
|
-
}).option("held",
|
|
1685
|
+
}).option("held", z17.string(), {
|
|
1649
1686
|
default: "",
|
|
1650
1687
|
description: "with --agent: the conversation `splates server`'s queue held for the queued task, which takes the task rather than a new one"
|
|
1651
1688
|
}).action(async (opts, ctx) => {
|
|
@@ -1690,108 +1727,109 @@ splates.command("add").use(repository10({ needs: "tasks" })).description("start
|
|
|
1690
1727
|
await serveProject(ctx);
|
|
1691
1728
|
}
|
|
1692
1729
|
});
|
|
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",
|
|
1730
|
+
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", z17.string(), { description: "task name" }).action(async (opts, ctx) => {
|
|
1694
1731
|
await ctx.journal.record("claim", opts.task, () => claim(ctx, opts.task), (claimed) => claimed);
|
|
1695
1732
|
await follow(ctx, opts.task);
|
|
1696
1733
|
});
|
|
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",
|
|
1734
|
+
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", z17.string(), {
|
|
1698
1735
|
default: "",
|
|
1699
1736
|
description: "squash the task's commits into one with this message before landing, saying what the base gains in at least 5 characters"
|
|
1700
1737
|
}).action(async (opts, ctx) => {
|
|
1701
1738
|
await refuseGroomer4(ctx.service, "merge");
|
|
1702
1739
|
const task = await here(ctx);
|
|
1703
1740
|
const facts = {};
|
|
1704
|
-
await ctx.journal.record("merge", task, () => merge(ctx, ctx.ps.cwd(), facts, {
|
|
1741
|
+
await ctx.journal.record("merge", task, () => merge({ ...ctx, send: ctx.fetch }, ctx.ps.cwd(), facts, {
|
|
1742
|
+
message: opts.message
|
|
1743
|
+
}), facts);
|
|
1705
1744
|
if (task !== null) {
|
|
1706
1745
|
await addFactTasks(ctx, task).catch((error) => ctx.log.error(`could not queue tasks for what the merge found: ${error}`));
|
|
1707
1746
|
await follow(ctx, null, { from: task });
|
|
1708
1747
|
}
|
|
1709
1748
|
});
|
|
1710
|
-
splates.command("remove").use(repository10({ needs: "tasks" })).description("discard a task: worktree, branch and state file; cheap and encouraged, since redoing a task against current trunk often beats a hard rebase").arg("task",
|
|
1749
|
+
splates.command("remove").use(repository10({ needs: "tasks" })).description("discard a task: worktree, branch and state file; cheap and encouraged, since redoing a task against current trunk often beats a hard rebase").arg("task", z17.string(), { description: "task name" }).option("force", z17.boolean(), {
|
|
1711
1750
|
default: false,
|
|
1712
1751
|
description: "discard even when another agent holds the lease"
|
|
1713
|
-
}).option("reason",
|
|
1752
|
+
}).option("reason", z17.optional(z17.enum(REMOVE_REASONS)), {
|
|
1714
1753
|
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
1754
|
}).action(async (opts, ctx) => {
|
|
1716
1755
|
await ctx.journal.record("remove", opts.task, () => remove(ctx, opts.task, { force: opts.force, reason: opts.reason }), ({ released }) => ({ why: opts.reason ?? null, released }));
|
|
1717
1756
|
await follow(ctx, null, { from: opts.task });
|
|
1718
1757
|
});
|
|
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",
|
|
1758
|
+
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", z17.boolean(), { default: false, description: "emit JSON" }).option("files", z17.boolean(), {
|
|
1720
1759
|
default: false,
|
|
1721
1760
|
description: "add each task's Goal and changed files (committed or not), to check for overlap before starting"
|
|
1722
1761
|
}).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",
|
|
1762
|
+
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", z17.string(), { description: "task name" }).option("json", z17.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => show(ctx, opts.task, { json: opts.json }));
|
|
1763
|
+
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", z17.string(), { description: "task name" }).option("timeout-secs", z17.coerce.number(), {
|
|
1725
1764
|
default: DEFAULT_TIMEOUT3.secs,
|
|
1726
1765
|
description: "how long to wait before giving up"
|
|
1727
1766
|
}).action((opts, ctx) => {
|
|
1728
1767
|
const timeout = Duration3.secs(opts["timeout-secs"]);
|
|
1729
1768
|
return ctx.journal.waiting("wait", opts.task, { until: untilAfter3(timeout) }, () => wait(ctx, opts.task, { timeout }));
|
|
1730
1769
|
});
|
|
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",
|
|
1770
|
+
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", z17.coerce.number(), {
|
|
1732
1771
|
default: DEFAULT_COUNT,
|
|
1733
1772
|
description: "how many entries to show"
|
|
1734
|
-
}).option("json",
|
|
1773
|
+
}).option("json", z17.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => showLog(ctx, { count: opts.count, json: opts.json }));
|
|
1735
1774
|
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
|
|
1775
|
+
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", z17.string(), {
|
|
1737
1776
|
default: "",
|
|
1738
1777
|
description: "only this task's ports; every task's by default"
|
|
1739
|
-
}).option("dev",
|
|
1778
|
+
}).option("dev", z17.string(), {
|
|
1740
1779
|
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",
|
|
1780
|
+
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"
|
|
1781
|
+
}).option("json", z17.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => portsLs({
|
|
1743
1782
|
task: opts.task,
|
|
1744
1783
|
dev: opts.dev,
|
|
1745
1784
|
json: opts.json,
|
|
1746
1785
|
log: ctx.log,
|
|
1747
|
-
fs: ctx.fs,
|
|
1748
1786
|
ps: ctx.ps,
|
|
1749
1787
|
send: ctx.fetch
|
|
1750
1788
|
}));
|
|
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",
|
|
1789
|
+
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", z17.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => agents({
|
|
1752
1790
|
roster: rosterOf2(ctx),
|
|
1753
1791
|
offered: await offeredOf(ctx).all(),
|
|
1754
1792
|
log: ctx.log
|
|
1755
1793
|
}, { 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",
|
|
1794
|
+
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", z17.string(), {
|
|
1757
1795
|
default: "",
|
|
1758
1796
|
description: "a step's id; omit for every area's progress"
|
|
1759
|
-
}).option("json",
|
|
1797
|
+
}).option("json", z17.boolean(), { default: false, description: "emit JSON" }).option("skip", z17.boolean(), {
|
|
1760
1798
|
default: false,
|
|
1761
1799
|
description: "with a step: stop asking for it"
|
|
1762
|
-
}).option("unskip",
|
|
1800
|
+
}).option("unskip", z17.boolean(), {
|
|
1763
1801
|
default: false,
|
|
1764
1802
|
description: "with a step: ask for it again"
|
|
1765
|
-
}).option("undo",
|
|
1803
|
+
}).option("undo", z17.boolean(), {
|
|
1766
1804
|
default: false,
|
|
1767
1805
|
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
1806
|
}).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",
|
|
1807
|
+
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", z17.string(), {
|
|
1770
1808
|
default: "",
|
|
1771
1809
|
description: "task name; omit for the main tree"
|
|
1772
1810
|
}).action((opts, ctx) => path(ctx, opts.task || undefined));
|
|
1773
1811
|
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
1812
|
await checkpoint(ctx, ctx.ps.cwd());
|
|
1775
1813
|
});
|
|
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",
|
|
1814
|
+
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", z17.string(), {
|
|
1777
1815
|
default: "",
|
|
1778
1816
|
description: "files to store, comma separated"
|
|
1779
|
-
}).option("task",
|
|
1817
|
+
}).option("task", z17.string(), {
|
|
1780
1818
|
default: "",
|
|
1781
1819
|
description: "task name, when run outside its worktree"
|
|
1782
|
-
}).option("remove",
|
|
1820
|
+
}).option("remove", z17.string(), {
|
|
1783
1821
|
default: "",
|
|
1784
1822
|
description: "files it holds to drop first, comma separated, by path or stored name"
|
|
1785
|
-
}).option("conversation",
|
|
1823
|
+
}).option("conversation", z17.string(), {
|
|
1786
1824
|
default: "",
|
|
1787
1825
|
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,
|
|
1826
|
+
}).action(async (opts, ctx) => ctx.journal.record("attach", opts.task || await here(ctx), () => attach(ctx, commaList5(opts.files), ctx.ps.cwd(), {
|
|
1789
1827
|
task: opts.task || undefined,
|
|
1790
|
-
remove:
|
|
1828
|
+
remove: commaList5(opts.remove),
|
|
1791
1829
|
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("budget", splatesBudget);
|
|
1830
|
+
}), (stored) => ({ stored, removed: commaList5(opts.remove) })));
|
|
1831
|
+
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", z17.string(), { description: "task name" }).action((opts, ctx) => recordRetry(ctx, opts.task));
|
|
1832
|
+
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("runners", splatesRunners).mount("queue", splatesQueue).mount("hook", splatesHook).mount("budget", splatesBudget).mount("replica", splatesReplica);
|
|
1795
1833
|
|
|
1796
1834
|
// index.ts
|
|
1797
1835
|
if (basename2(process.argv[1] ?? "") === LEGACY) {
|
|
@@ -1799,4 +1837,8 @@ if (basename2(process.argv[1] ?? "") === LEGACY) {
|
|
|
1799
1837
|
}
|
|
1800
1838
|
var ps = systemPs();
|
|
1801
1839
|
var { project, argv } = projectArgs(ps.args);
|
|
1840
|
+
var handedOff = await handOffDev(argv, ps.cwd());
|
|
1841
|
+
if (handedOff !== null) {
|
|
1842
|
+
process.exit(handedOff);
|
|
1843
|
+
}
|
|
1802
1844
|
await splates.run({ fs: new NodeFs, ps, assets, project }, argv);
|