@tablation/crew 0.50.3 → 0.51.1
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/dist/cli.js +138 -26
- 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
|
-
|
|
11508
|
-
|
|
11509
|
-
|
|
11510
|
-
|
|
11511
|
-
|
|
11512
|
-
|
|
11513
|
-
|
|
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 ? `
|
|
@@ -11589,6 +11644,13 @@ async function resolveSeatKey(route, role, getStore = getSessionStore2) {
|
|
|
11589
11644
|
return ship("OS keychain unavailable");
|
|
11590
11645
|
}
|
|
11591
11646
|
}
|
|
11647
|
+
function describeStoreError(e) {
|
|
11648
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
11649
|
+
if (/User interaction is not allowed/i.test(msg)) {
|
|
11650
|
+
return "keychain unavailable (User interaction is not allowed \u2014 run from a logged-in session, or `security unlock-keychain` first)";
|
|
11651
|
+
}
|
|
11652
|
+
return "keychain unavailable (could not write to the OS keychain)";
|
|
11653
|
+
}
|
|
11592
11654
|
async function mintMissingSeatKeys(route, o) {
|
|
11593
11655
|
const out = [];
|
|
11594
11656
|
if (!route.resolved) return out;
|
|
@@ -11605,11 +11667,27 @@ async function mintMissingSeatKeys(route, o) {
|
|
|
11605
11667
|
return targets.map(({ role }) => ({
|
|
11606
11668
|
role,
|
|
11607
11669
|
action: "failed",
|
|
11608
|
-
detail:
|
|
11670
|
+
detail: describeStoreError(e)
|
|
11609
11671
|
}));
|
|
11610
11672
|
}
|
|
11611
11673
|
const host = hostFromUrl2(route.baseUrl);
|
|
11612
11674
|
const base = route.baseUrl.replace(/\/+$/, "");
|
|
11675
|
+
if (!o.dryRun) {
|
|
11676
|
+
const probeSlug = seatKeySlug(splitRoute(route.route)[0], "_probe");
|
|
11677
|
+
try {
|
|
11678
|
+
await store.set(host, probeSlug, {
|
|
11679
|
+
apiKey: "probe",
|
|
11680
|
+
shipId: "",
|
|
11681
|
+
workspaceId: resolved.workspaceId,
|
|
11682
|
+
identityId: "_probe",
|
|
11683
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11684
|
+
});
|
|
11685
|
+
await store.clear(host, probeSlug);
|
|
11686
|
+
} catch (e) {
|
|
11687
|
+
const detail = `${describeStoreError(e)}; no keys minted`;
|
|
11688
|
+
return targets.map(({ role, agentId }) => agentId ? { role, action: "failed", detail } : { role, action: "no_agent" });
|
|
11689
|
+
}
|
|
11690
|
+
}
|
|
11613
11691
|
for (const { role, agentId } of targets) {
|
|
11614
11692
|
if (!agentId) {
|
|
11615
11693
|
out.push({ role, action: "no_agent" });
|
|
@@ -11643,16 +11721,37 @@ async function mintMissingSeatKeys(route, o) {
|
|
|
11643
11721
|
out.push({ role, action: "failed", detail: "response carried no key" });
|
|
11644
11722
|
continue;
|
|
11645
11723
|
}
|
|
11646
|
-
|
|
11647
|
-
|
|
11648
|
-
|
|
11649
|
-
|
|
11650
|
-
|
|
11651
|
-
|
|
11652
|
-
|
|
11724
|
+
try {
|
|
11725
|
+
await store.set(host, slug, {
|
|
11726
|
+
apiKey: body.key,
|
|
11727
|
+
shipId: o.shipName ?? "",
|
|
11728
|
+
workspaceId: resolved.workspaceId,
|
|
11729
|
+
identityId: agentId,
|
|
11730
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11731
|
+
});
|
|
11732
|
+
} catch (e) {
|
|
11733
|
+
let revoked = false;
|
|
11734
|
+
if (body.id) {
|
|
11735
|
+
try {
|
|
11736
|
+
const del2 = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys/${body.id}`, {
|
|
11737
|
+
method: "DELETE",
|
|
11738
|
+
headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
11739
|
+
});
|
|
11740
|
+
revoked = del2.ok;
|
|
11741
|
+
} catch {
|
|
11742
|
+
revoked = false;
|
|
11743
|
+
}
|
|
11744
|
+
}
|
|
11745
|
+
out.push({
|
|
11746
|
+
role,
|
|
11747
|
+
action: "failed",
|
|
11748
|
+
detail: revoked ? `minted but could not be stored (${describeStoreError(e)}); revoked` : `a key for ${role} is live on the server and not stored (${describeStoreError(e)}) \u2014 revoke it from that seat's Agents row`
|
|
11749
|
+
});
|
|
11750
|
+
continue;
|
|
11751
|
+
}
|
|
11653
11752
|
out.push({ role, action: "minted" });
|
|
11654
11753
|
} catch (e) {
|
|
11655
|
-
out.push({ role, action: "failed", detail: e.message });
|
|
11754
|
+
out.push({ role, action: "failed", detail: /Command failed|security|sk_|-w /i.test(e.message) ? describeStoreError(e) : e.message });
|
|
11656
11755
|
}
|
|
11657
11756
|
}
|
|
11658
11757
|
return out;
|
|
@@ -13466,20 +13565,25 @@ function terminalPrompter() {
|
|
|
13466
13565
|
}
|
|
13467
13566
|
};
|
|
13468
13567
|
}
|
|
13469
|
-
async function pickOperator(holds2) {
|
|
13568
|
+
async function pickOperator(holds2, me) {
|
|
13569
|
+
const choices = holds2.filter((h) => !h.user_id || h.user_id === me.id);
|
|
13570
|
+
const canCreate = !!me.id;
|
|
13470
13571
|
process.stderr.write("\nWhich of these Crew rows is you?\n");
|
|
13471
|
-
|
|
13572
|
+
choices.forEach((h, i) => process.stderr.write(` ${i + 1}. ${h.name || h.id}
|
|
13472
13573
|
`));
|
|
13574
|
+
if (canCreate) process.stderr.write(` 0. Create a new Crew row for ${me.name ?? ""} ${me.email ?? ""}`.trimEnd() + "\n");
|
|
13473
13575
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
13474
13576
|
try {
|
|
13475
|
-
const
|
|
13577
|
+
const fallback = canCreate && choices.length === 0 ? "0" : "";
|
|
13578
|
+
const answer = (await rl.question(`Enter a number (blank to ${fallback ? "create one" : "skip"}): `)).trim() || fallback;
|
|
13476
13579
|
const n = Number(answer);
|
|
13477
|
-
if (!answer || !Number.isInteger(n) || n <
|
|
13478
|
-
return
|
|
13580
|
+
if (!answer || !Number.isInteger(n) || n < 0 || n > choices.length) return void 0;
|
|
13581
|
+
return n === 0 ? canCreate ? NEW_OPERATOR_ROW : void 0 : choices[n - 1].id;
|
|
13479
13582
|
} finally {
|
|
13480
13583
|
rl.close();
|
|
13481
13584
|
}
|
|
13482
13585
|
}
|
|
13586
|
+
var NEW_OPERATOR_ROW = "new";
|
|
13483
13587
|
async function pickResolvedStatuses(unrecognized) {
|
|
13484
13588
|
process.stderr.write(
|
|
13485
13589
|
"\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 +15646,11 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15542
15646
|
sshPublicKey
|
|
15543
15647
|
};
|
|
15544
15648
|
const runConnect = async (ws, proj) => {
|
|
15545
|
-
let found = await discover({ ...authOpts, workspace: ws, project: proj, area: value("area"), ship, dryRun });
|
|
15649
|
+
let found = await discover({ ...authOpts, workspace: ws, project: proj, area: value("area"), ship, dryRun, interactive: !!(process.stdin.isTTY && process.stdout.isTTY) });
|
|
15546
15650
|
if (!found.projectId && found.offerTemplateInstall && process.stdin.isTTY && process.stdout.isTTY) {
|
|
15547
15651
|
const installedSlug = await offerTemplateInstall(authOpts, found.workspaceId);
|
|
15548
15652
|
if (installedSlug) {
|
|
15549
|
-
found = await discover({ ...authOpts, workspace: ws, project: installedSlug, area: value("area"), ship, dryRun });
|
|
15653
|
+
found = await discover({ ...authOpts, workspace: ws, project: installedSlug, area: value("area"), ship, dryRun, interactive: !!(process.stdin.isTTY && process.stdout.isTTY) });
|
|
15550
15654
|
}
|
|
15551
15655
|
}
|
|
15552
15656
|
if (!found.projectId) {
|
|
@@ -15572,7 +15676,15 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15572
15676
|
}
|
|
15573
15677
|
let operator = found.operator;
|
|
15574
15678
|
if (!operator && found.holds.length > 0 && process.stdin.isTTY && process.stdout.isTTY) {
|
|
15575
|
-
|
|
15679
|
+
const picked = await pickOperator(found.holds, { id: found.meId, name: found.meName, email: found.meEmail });
|
|
15680
|
+
if (picked === NEW_OPERATOR_ROW) {
|
|
15681
|
+
await createOperatorRow({ ...authOpts, dryRun }, found);
|
|
15682
|
+
operator = found.operator;
|
|
15683
|
+
} else if (picked) {
|
|
15684
|
+
operator = picked;
|
|
15685
|
+
const row = found.holds.find((h) => h.id === picked);
|
|
15686
|
+
if (row && found.meId && row.user_id !== found.meId) await linkOperatorRow({ ...authOpts, dryRun }, found, row);
|
|
15687
|
+
}
|
|
15576
15688
|
}
|
|
15577
15689
|
let previous;
|
|
15578
15690
|
const hadPreviousFile = existsSync26(resolvedPath);
|
|
@@ -16614,7 +16726,7 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
16614
16726
|
process.exitCode = 1;
|
|
16615
16727
|
} else if (!row.user_id) {
|
|
16616
16728
|
process.stdout.write(
|
|
16617
|
-
`operator identity: ERROR Crew row "${row.name ?? op}" has no user_id \u2014 their UI comments will read "(no identity)";
|
|
16729
|
+
`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
16730
|
`
|
|
16619
16731
|
);
|
|
16620
16732
|
process.exitCode = 1;
|
package/package.json
CHANGED