@stringsync/splates 0.0.46 → 0.0.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +71 -18
  2. package/package.json +2 -2
package/index.js CHANGED
@@ -12,11 +12,11 @@ import { assets } from "@stringsync/splates-server/server";
12
12
  import { NodeFs } from "webappwiz/system";
13
13
 
14
14
  // splates.ts
15
- import { DEFAULT_PORT } from "@stringsync/splates-server";
15
+ import { DEFAULT_PORT as DEFAULT_PORT2 } from "@stringsync/splates-server";
16
16
  import { agents } from "@stringsync/splates-server/agents";
17
17
  import { attach } from "@stringsync/splates-server/artifacts";
18
18
  import { commaList as commaList4 } from "@stringsync/splates-server/comma-list";
19
- import { exits, fail as fail5 } from "@stringsync/splates-server/exit";
19
+ import { exits, fail as fail6 } from "@stringsync/splates-server/exit";
20
20
  import { DEFAULT_COUNT, log as showLog } from "@stringsync/splates-server/log";
21
21
  import { merge } from "@stringsync/splates-server/merge";
22
22
  import { portsLs } from "@stringsync/splates-server/ports";
@@ -270,13 +270,20 @@ splatesBudget.command("end").description("end a raise now, by the number `show`
270
270
 
271
271
  // coordinator.ts
272
272
  import {
273
- CoordinatorBox
273
+ CoordinatorBox,
274
+ keepCarriedCreds,
275
+ machineMove
274
276
  } from "@stringsync/splates-server/coordinator";
275
- import { RUNNING } from "@stringsync/splates-server/server";
277
+ import { SYSTEM_STORES as SYSTEM_STORES2 } from "@stringsync/splates-server/creds";
278
+ import { fail as fail3 } from "@stringsync/splates-server/exit";
279
+ import {
280
+ DEFAULT_PORT,
281
+ RUNNING
282
+ } from "@stringsync/splates-server/server";
276
283
  import { cli as cli3 } from "webappwiz/cmd";
277
284
  import { z as z4 } from "zod";
278
285
  var splatesCoordinator = cli3("coordinator").description("the always-on box in your own Hetzner Cloud account that `splates server` moves onto, made through Pulumi with the token and state the Cloud workers setup readied: a CPX21 in Ashburn with Hetzner's daily backups, reached by SSH alone");
279
- splatesCoordinator.command("up").description("make the coordinator's box, or bring it to its program, with splates of this release installed for the user splates and an SSH key splates makes under ~/.splates/coordinator; run again, it changes only what is not so already. The box serves nothing until the coordinator moves onto it").option("json", z4.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
286
+ splatesCoordinator.command("up").description("make the coordinator's box, or bring it to its program, with splates of this release installed for the user splates and an SSH key splates makes under ~/.splates/coordinator; run again, it changes only what is not so already. The box serves nothing until `splates coordinator move`").option("json", z4.boolean(), { default: false, description: "emit JSON" }).action(async (opts, ctx) => {
280
287
  ctx.log.info("making the coordinator's box; its first boot takes minutes");
281
288
  print(ctx, await box(ctx).up(), opts.json);
282
289
  });
@@ -292,6 +299,51 @@ splatesCoordinator.command("remove").description("destroy the coordinator's box
292
299
  await box(ctx).remove();
293
300
  ctx.log.info("removed the coordinator's box");
294
301
  });
302
+ splatesCoordinator.command("move").description("move the coordinator onto its box, over SSH. First it checks what the move needs, brings the box's splates to this release, and gives each project a clone there with a deploy key of its own; until GitHub lets each key push trunk, it prints each key with where to add it, with write access, and exits `blocked`: add them and run it again. Then it asks every agent of this machine's `splates server` to checkpoint and stops it, carries the coordinator's records and creds to the box, starts `splates server` there for good under systemd, with the saved tunnel, 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").option("dry-run", z4.boolean(), {
303
+ default: false,
304
+ description: "stop once the box is ready, before this machine's server stops or anything here changes"
305
+ }).option("dev", z4.string(), {
306
+ default: `http://localhost:${DEFAULT_PORT}`,
307
+ description: "this machine's `splates server`, which the move stops"
308
+ }).action(async (opts, ctx) => {
309
+ const moved = await machineMove({
310
+ ps: ctx.ps,
311
+ fs: ctx.fs,
312
+ log: ctx.log,
313
+ version: RUNNING,
314
+ dev: opts.dev,
315
+ stores: ctx.secrets ?? SYSTEM_STORES2,
316
+ ...ctx.fetch === undefined ? {} : { fetch: ctx.fetch }
317
+ }).run({ dryRun: opts["dry-run"] });
318
+ if (moved.status === "keys") {
319
+ fail3("blocked", [
320
+ 'GitHub does not let these deploy keys push yet. Add each to its repository with "Allow write access" ticked, then move again:',
321
+ ...moved.keys.flatMap(({ slug, key, add }) => [
322
+ "",
323
+ ` ${slug}${add === null ? "" : `: ${add}`}`,
324
+ ` ${key}`
325
+ ])
326
+ ].join(`
327
+ `), { keys: moved.keys });
328
+ }
329
+ if (moved.status === "ready") {
330
+ ctx.log.info(`the box is ready to coordinate ${moved.projects.join(", ")} through ${moved.host}: move again without --dry-run`);
331
+ return;
332
+ }
333
+ ctx.log.info([
334
+ `the coordinator runs on the box: https://${moved.host}`,
335
+ `this machine is its worker ${ctx.ps.hostname}, kept up by ${moved.worker}`,
336
+ `its own records are kept aside in ${moved.aside} and each project's .git/splates.moved`
337
+ ].join(`
338
+ `));
339
+ });
340
+ splatesCoordinator.command("keep-creds").description("keep the creds `splates coordinator move` sends on stdin in this machine's stores, as the box does when the coordinator moves onto it").action(async (_opts, ctx) => {
341
+ const kept = await keepCarriedCreds({
342
+ stores: ctx.secrets ?? SYSTEM_STORES2,
343
+ sent: await Bun.stdin.text()
344
+ });
345
+ ctx.log.info(`kept ${kept} creds`);
346
+ });
295
347
  function box(ctx) {
296
348
  return new CoordinatorBox({ version: RUNNING, ps: ctx.ps, fs: ctx.fs });
297
349
  }
@@ -310,7 +362,7 @@ function print(ctx, standing, json) {
310
362
 
311
363
  // creds.ts
312
364
  import { DEFAULT_TIMEOUT } from "@stringsync/splates-server/creds";
313
- import { fail as fail3 } from "@stringsync/splates-server/exit";
365
+ import { fail as fail4 } from "@stringsync/splates-server/exit";
314
366
  import { repository as repository2 } from "@stringsync/splates-server/records";
315
367
  import { untilAfter } from "@stringsync/splates-server/tasks";
316
368
  import { cli as cli4 } from "webappwiz/cmd";
@@ -356,7 +408,7 @@ splatesCreds.command("wait").use(repository2({ needs: "own" })).description("blo
356
408
  await ctx.journal.waiting("creds wait", null, { name: opts.name, until: untilAfter(timeout) }, async () => {
357
409
  const ended = await ctx.creds.wait(opts.name, { timeout });
358
410
  if (ended === "dismissed") {
359
- fail3("dismissed", `${opts.name} was dismissed: carry on without it, or ask the person why`, { name: opts.name });
411
+ fail4("dismissed", `${opts.name} was dismissed: carry on without it, or ask the person why`, { name: opts.name });
360
412
  }
361
413
  });
362
414
  ctx.log.info(`${color.green("saved")} ${opts.name}`);
@@ -468,7 +520,7 @@ splatesQueue.command("move").description("move an agent waiting for a slot to an
468
520
 
469
521
  // setup.ts
470
522
  import { dirname } from "node:path";
471
- import { SYSTEM_STORES as SYSTEM_STORES2 } from "@stringsync/splates-server/creds";
523
+ import { SYSTEM_STORES as SYSTEM_STORES3 } from "@stringsync/splates-server/creds";
472
524
  import { splatesHome as splatesHome2 } from "@stringsync/splates-server/models";
473
525
  import { Projects } from "@stringsync/splates-server/projects";
474
526
  import {
@@ -510,7 +562,7 @@ async function setup(deps, opts) {
510
562
  printStep(log, step);
511
563
  }
512
564
  async function setupOf(deps) {
513
- const { fs, ps, secrets = SYSTEM_STORES2, fetch } = deps;
565
+ const { fs, ps, secrets = SYSTEM_STORES3, fetch } = deps;
514
566
  const root = deps.project === undefined ? await repoRoot(ps) : (await Projects.ofMachine(deps).named(deps.project)).root;
515
567
  return new Setup({
516
568
  fs,
@@ -633,7 +685,7 @@ import {
633
685
  todoUpdate
634
686
  } from "@stringsync/splates-server/board";
635
687
  import { commaList as commaList3 } from "@stringsync/splates-server/comma-list";
636
- import { fail as fail4 } from "@stringsync/splates-server/exit";
688
+ import { fail as fail5 } from "@stringsync/splates-server/exit";
637
689
  import { repository as repository5 } from "@stringsync/splates-server/records";
638
690
  import {
639
691
  DEFAULT_TIMEOUT as DEFAULT_TIMEOUT2,
@@ -669,7 +721,7 @@ splatesTodo.command("add").use(repository5({ needs: "todos" })).description("not
669
721
  description: "with --agent: the thinking level, one the agent offers (default: the agent's own)"
670
722
  }).action(async (opts, ctx) => {
671
723
  if (ctx.elsewhere !== null && opts.lane !== "") {
672
- fail4("not_granted", "a todo from another project goes in its untriaged lane, for whoever orchestrates it there to place: leave --lane off", { subject: opts.subject });
724
+ fail5("not_granted", "a todo from another project goes in its untriaged lane, for whoever orchestrates it there to place: leave --lane off", { subject: opts.subject });
673
725
  }
674
726
  const from = await here(ctx);
675
727
  const assignee = await assigneeIn(ctx, opts.agent, opts.model, opts.effort);
@@ -1042,10 +1094,10 @@ splates.command("add").use(repository7({ needs: "tasks" })).description("start a
1042
1094
  description: "with --agent: hand it out though you have as many tasks out to agents as agentsPerAgent (splates.config.ts) allows; only when the person explicitly asked for more at once"
1043
1095
  }).action(async (opts, ctx) => {
1044
1096
  if (opts.agent === "" && (opts.model !== "" || opts.effort !== "" || opts.prompt !== "" || opts["over-capacity"])) {
1045
- fail5("usage", "--model, --effort, --prompt and --over-capacity go with --agent", { task: opts.task });
1097
+ fail6("usage", "--model, --effort, --prompt and --over-capacity go with --agent", { task: opts.task });
1046
1098
  }
1047
1099
  if (opts.goal.trim() === "" && opts.todo === "") {
1048
- fail5("usage", `say what done means: \`splates add ${opts.task} --goal "<one or two lines>"\`, or take todos with --todo`, { task: opts.task });
1100
+ fail6("usage", `say what done means: \`splates add ${opts.task} --goal "<one or two lines>"\`, or take todos with --todo`, { task: opts.task });
1049
1101
  }
1050
1102
  const dispatch = opts.agent === "" ? undefined : {
1051
1103
  agent: opts.agent,
@@ -1124,7 +1176,7 @@ splates.command("log").use(repository7({ needs: "read" })).description("show wha
1124
1176
  description: "how many entries to show"
1125
1177
  }).option("json", z13.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => showLog(ctx, { count: opts.count, json: opts.json }));
1126
1178
  splates.command("server").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 todos and tasks, where todos can be added, updated, reordered or removed; it brings each project's records forward as it opens it").option("port", z13.coerce.number(), {
1127
- default: DEFAULT_PORT,
1179
+ default: DEFAULT_PORT2,
1128
1180
  description: "port to listen on, or the next open one above it"
1129
1181
  }).option("allow-hosts", z13.string(), {
1130
1182
  default: "",
@@ -1151,17 +1203,18 @@ splates.command("server").description("serve every splates project on this machi
1151
1203
  default: false,
1152
1204
  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"
1153
1205
  }).action((opts, ctx) => serveDev(ctx, opts));
1154
- 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", z13.string(), {
1206
+ 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, which on a worker whose coordinator runs elsewhere only that worker's own ports have; give the user both").arg("task", z13.string(), {
1155
1207
  default: "",
1156
1208
  description: "only this task's ports; every task's by default"
1157
1209
  }).option("dev", z13.string(), {
1158
1210
  default: "",
1159
- 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"
1211
+ 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 the coordinator this machine's worker relays to, else this machine's on its default port"
1160
1212
  }).option("json", z13.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => portsLs({
1161
1213
  task: opts.task,
1162
1214
  dev: opts.dev,
1163
1215
  json: opts.json,
1164
1216
  log: ctx.log,
1217
+ fs: ctx.fs,
1165
1218
  ps: ctx.ps,
1166
1219
  send: ctx.fetch
1167
1220
  }));
@@ -1181,10 +1234,10 @@ splates.command("setup").description("set splates up on this machine, one step a
1181
1234
  description: "with a step: ask for it again"
1182
1235
  }).action(async (opts, ctx) => {
1183
1236
  if (opts.step === "" && (opts.skip || opts.unskip)) {
1184
- fail5("usage", "--skip and --unskip name a step", {});
1237
+ fail6("usage", "--skip and --unskip name a step", {});
1185
1238
  }
1186
1239
  if (opts.skip && opts.unskip) {
1187
- fail5("usage", "--skip or --unskip, not both", { step: opts.step });
1240
+ fail6("usage", "--skip or --unskip, not both", { step: opts.step });
1188
1241
  }
1189
1242
  await setup(ctx, opts);
1190
1243
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stringsync/splates",
3
- "version": "0.0.46",
3
+ "version": "0.0.47",
4
4
  "description": "Runs several AI coding agents on one repository at once, each in its own git worktree",
5
5
  "license": "MIT",
6
6
  "author": "stringsync LLC",
@@ -17,7 +17,7 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
- "@stringsync/splates-server": "^0.0.46",
20
+ "@stringsync/splates-server": "^0.0.47",
21
21
  "webappwiz": "0.0.46",
22
22
  "zod": "^4.4.3"
23
23
  },