@tablation/crew 0.50.3 → 0.51.0

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/dist/cli.js +85 -17
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -11281,6 +11281,7 @@ async function discover(o) {
11281
11281
  out.role = me.role;
11282
11282
  out.meEmail = me.email;
11283
11283
  out.meId = me.id;
11284
+ out.meName = me.name;
11284
11285
  } catch {
11285
11286
  }
11286
11287
  if (o.project) {
@@ -11504,18 +11505,72 @@ async function discover(o) {
11504
11505
  }
11505
11506
  }
11506
11507
  const seatIds = /* @__PURE__ */ new Set([...Object.values(out.seats), ...out.releaseSeat ? [out.releaseSeat] : []]);
11507
- out.holds = rows.filter((r) => !seatIds.has(r.id)).map((r) => ({ id: r.id, name: r.name ?? "" }));
11508
- if (out.holds.length === 0) {
11509
- problems.push("no non-seat Crew rows \u2014 which row is the operator?");
11510
- } else if (out.meEmail) {
11511
- const holdRows = rows.filter((r) => !seatIds.has(r.id));
11512
- const matches2 = holdRows.filter((r) => eq(r.email, out.meEmail));
11513
- if (matches2.length === 1) out.operator = matches2[0].id;
11508
+ const holdRows = rows.filter((r) => !seatIds.has(r.id));
11509
+ out.holds = holdRows.map((r) => ({ id: r.id, name: r.name ?? "", email: r.email, user_id: r.user_id }));
11510
+ out.crewModelId = out.models.crew;
11511
+ if (!out.meId) {
11512
+ if (out.holds.length === 0) problems.push("no non-seat Crew rows \u2014 which row is the operator?");
11513
+ else {
11514
+ const byEmail = out.meEmail ? holdRows.filter((r) => eq(r.email, out.meEmail)) : [];
11515
+ if (byEmail.length === 1) out.operator = byEmail[0].id;
11516
+ else if (out.holds.length === 1) out.operator = out.holds[0].id;
11517
+ }
11518
+ } else {
11519
+ const candidates = holdRows.filter((r) => !r.user_id || r.user_id === out.meId);
11520
+ const mine = holdRows.find((r) => r.user_id === out.meId);
11521
+ const byEmail = out.meEmail ? candidates.filter((r) => eq(r.email, out.meEmail)) : [];
11522
+ const only = holdRows.length === 1 && candidates.length === 1 ? candidates[0] : void 0;
11523
+ const pick = mine ?? (byEmail.length === 1 ? byEmail[0] : only);
11524
+ if (pick) {
11525
+ out.operator = pick.id;
11526
+ if (pick.user_id !== out.meId && o.ship) await linkOperatorRow(o, out, pick);
11527
+ } else if (o.ship && (!o.interactive || candidates.length === 0)) {
11528
+ await createOperatorRow(o, out);
11529
+ }
11514
11530
  }
11515
- if (!out.operator && out.holds.length === 1) out.operator = out.holds[0].id;
11516
11531
  }
11517
11532
  return out;
11518
11533
  }
11534
+ async function linkOperatorRow(o, found, row) {
11535
+ const name = row.name || row.id;
11536
+ if (!found.meId || !found.crewModelId) return;
11537
+ if (o.dryRun) {
11538
+ found.provisioning.push(`would link Crew row "${name}" to your account`);
11539
+ return;
11540
+ }
11541
+ const body = { user_id: found.meId };
11542
+ if (!row.email && found.meEmail) body.email = found.meEmail;
11543
+ try {
11544
+ await patch(o, `/data-models/${found.crewModelId}/records/${row.id}`, body);
11545
+ found.provisioning.push(`linked Crew row "${name}" to your account`);
11546
+ } catch (e) {
11547
+ if (!(e instanceof ConnectHttpError) || e.status !== 403) throw e;
11548
+ found.provisioning.push(
11549
+ `could not link your Crew row (no write access) \u2014 ask a workspace admin to set user_id on "${name}"`
11550
+ );
11551
+ }
11552
+ }
11553
+ async function createOperatorRow(o, found) {
11554
+ if (!found.meId || !found.crewModelId) return;
11555
+ const name = found.meName || found.meEmail || "Operator";
11556
+ if (o.dryRun) {
11557
+ found.provisioning.push(`would create Crew row "${name}" for you`);
11558
+ return;
11559
+ }
11560
+ try {
11561
+ const created = await post2(o, `/data-models/${found.crewModelId}/records`, {
11562
+ name,
11563
+ ...found.meEmail ? { email: found.meEmail } : {},
11564
+ user_id: found.meId
11565
+ });
11566
+ found.operator = created.id;
11567
+ found.holds.push({ id: created.id, name, email: found.meEmail, user_id: found.meId });
11568
+ found.provisioning.push(`created Crew row "${name}" for you`);
11569
+ } catch (e) {
11570
+ if (!(e instanceof ConnectHttpError) || e.status !== 403) throw e;
11571
+ found.provisioning.push("could not create your Crew row (no write access) \u2014 ask a workspace admin to add one with your user_id");
11572
+ }
11573
+ }
11519
11574
  function renderConnection(d, route, dir, opts = {}) {
11520
11575
  const apiKeyFileLine = opts.keychainBacked ? ` # apiKey: resolved automatically from the OS keychain (crew connect) \u2014 no apiKeyFile needed` : opts.apiKeyFile ? ` apiKeyFile: "${opts.apiKeyFile}"` : ` apiKeyFile: "REPLACE \u2014 a file holding this workspace's key"`;
11521
11576
  const apiKeyVarLine = opts.keychainBacked ? "" : opts.apiKeyVar ? `
@@ -13466,20 +13521,25 @@ function terminalPrompter() {
13466
13521
  }
13467
13522
  };
13468
13523
  }
13469
- async function pickOperator(holds2) {
13524
+ async function pickOperator(holds2, me) {
13525
+ const choices = holds2.filter((h) => !h.user_id || h.user_id === me.id);
13526
+ const canCreate = !!me.id;
13470
13527
  process.stderr.write("\nWhich of these Crew rows is you?\n");
13471
- holds2.forEach((h, i) => process.stderr.write(` ${i + 1}. ${h.name || h.id}
13528
+ choices.forEach((h, i) => process.stderr.write(` ${i + 1}. ${h.name || h.id}
13472
13529
  `));
13530
+ if (canCreate) process.stderr.write(` 0. Create a new Crew row for ${me.name ?? ""} ${me.email ?? ""}`.trimEnd() + "\n");
13473
13531
  const rl = createInterface({ input: process.stdin, output: process.stderr });
13474
13532
  try {
13475
- const answer = (await rl.question("Enter a number (blank to skip): ")).trim();
13533
+ const fallback = canCreate && choices.length === 0 ? "0" : "";
13534
+ const answer = (await rl.question(`Enter a number (blank to ${fallback ? "create one" : "skip"}): `)).trim() || fallback;
13476
13535
  const n = Number(answer);
13477
- if (!answer || !Number.isInteger(n) || n < 1 || n > holds2.length) return void 0;
13478
- return holds2[n - 1].id;
13536
+ if (!answer || !Number.isInteger(n) || n < 0 || n > choices.length) return void 0;
13537
+ return n === 0 ? canCreate ? NEW_OPERATOR_ROW : void 0 : choices[n - 1].id;
13479
13538
  } finally {
13480
13539
  rl.close();
13481
13540
  }
13482
13541
  }
13542
+ var NEW_OPERATOR_ROW = "new";
13483
13543
  async function pickResolvedStatuses(unrecognized) {
13484
13544
  process.stderr.write(
13485
13545
  "\nThis workspace has status values the default contract doesn't know the meaning of.\nFor each: does it mean the ticket is DONE \u2014 resolved/terminal (shipped, closed, won't-fix) \u2014 so a ticket blocked by it should stop being blocked?\n"
@@ -15542,11 +15602,11 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
15542
15602
  sshPublicKey
15543
15603
  };
15544
15604
  const runConnect = async (ws, proj) => {
15545
- let found = await discover({ ...authOpts, workspace: ws, project: proj, area: value("area"), ship, dryRun });
15605
+ let found = await discover({ ...authOpts, workspace: ws, project: proj, area: value("area"), ship, dryRun, interactive: !!(process.stdin.isTTY && process.stdout.isTTY) });
15546
15606
  if (!found.projectId && found.offerTemplateInstall && process.stdin.isTTY && process.stdout.isTTY) {
15547
15607
  const installedSlug = await offerTemplateInstall(authOpts, found.workspaceId);
15548
15608
  if (installedSlug) {
15549
- found = await discover({ ...authOpts, workspace: ws, project: installedSlug, area: value("area"), ship, dryRun });
15609
+ found = await discover({ ...authOpts, workspace: ws, project: installedSlug, area: value("area"), ship, dryRun, interactive: !!(process.stdin.isTTY && process.stdout.isTTY) });
15550
15610
  }
15551
15611
  }
15552
15612
  if (!found.projectId) {
@@ -15572,7 +15632,15 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
15572
15632
  }
15573
15633
  let operator = found.operator;
15574
15634
  if (!operator && found.holds.length > 0 && process.stdin.isTTY && process.stdout.isTTY) {
15575
- operator = await pickOperator(found.holds);
15635
+ const picked = await pickOperator(found.holds, { id: found.meId, name: found.meName, email: found.meEmail });
15636
+ if (picked === NEW_OPERATOR_ROW) {
15637
+ await createOperatorRow({ ...authOpts, dryRun }, found);
15638
+ operator = found.operator;
15639
+ } else if (picked) {
15640
+ operator = picked;
15641
+ const row = found.holds.find((h) => h.id === picked);
15642
+ if (row && found.meId && row.user_id !== found.meId) await linkOperatorRow({ ...authOpts, dryRun }, found, row);
15643
+ }
15576
15644
  }
15577
15645
  let previous;
15578
15646
  const hadPreviousFile = existsSync26(resolvedPath);
@@ -16614,7 +16682,7 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
16614
16682
  process.exitCode = 1;
16615
16683
  } else if (!row.user_id) {
16616
16684
  process.stdout.write(
16617
- `operator identity: ERROR Crew row "${row.name ?? op}" has no user_id \u2014 their UI comments will read "(no identity)"; link it to the account they sign in with
16685
+ `operator identity: ERROR Crew row "${row.name ?? op}" has no user_id \u2014 their UI comments will read "(no identity)"; re-run \`crew connect\` to link it
16618
16686
  `
16619
16687
  );
16620
16688
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.50.3",
3
+ "version": "0.51.0",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",