@tablation/crew 0.67.1 → 0.68.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 +99 -6
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2662,6 +2662,14 @@ function resolveApiKey(r) {
2662
2662
  `route "${r.route}": no API key (looked for, in order: apiKey; ${r.apiKeyVar ?? "VAR"} in ${r.apiKeyFile ?? "<unset>"}; CREW_API_KEY in the environment)`
2663
2663
  );
2664
2664
  }
2665
+ function credentialSource(r) {
2666
+ if (r.apiKey) return KEYCHAIN_ELIGIBLE_ROUTES.has(r) ? "keychain" : "inline";
2667
+ if (r.apiKeyFile && r.apiKeyVar && existsSync7(r.apiKeyFile)) {
2668
+ const re = new RegExp(`^${r.apiKeyVar}=`);
2669
+ if (readFileSync6(r.apiKeyFile, "utf8").split("\n").some((l) => re.test(l))) return "apiKeyFile";
2670
+ }
2671
+ return process.env.CREW_API_KEY?.trim() ? "CREW_API_KEY" : "none";
2672
+ }
2665
2673
  var KEYCHAIN_ELIGIBLE_ROUTES = /* @__PURE__ */ new WeakSet();
2666
2674
  async function hydrateApiKeys(routes, getStore = getSessionStore) {
2667
2675
  let store;
@@ -10364,6 +10372,11 @@ ${section}`;
10364
10372
  return [...lines.slice(0, anchor), section, ...lines.slice(anchor)].join("\n");
10365
10373
  }
10366
10374
  var RELEASE_COMMIT_SUBJECT = /^Release \S+$/;
10375
+ function subjectsSince(cwd, sha) {
10376
+ if (!isAncestor(cwd, sha, "HEAD")) return null;
10377
+ return (gitOk(cwd, ["log", "--format=%h %s", `${sha}..HEAD`]) ?? "").split("\n").filter(Boolean);
10378
+ }
10379
+ var isReleaseCommitLine = (line) => RELEASE_COMMIT_SUBJECT.test(line.replace(/^\S+ /, ""));
10367
10380
  function decideRelease(cwd, tickets, contract, opts = {}) {
10368
10381
  const pattern = opts.tagPattern ?? "v*";
10369
10382
  const base = opts.base ?? "main";
@@ -11109,11 +11122,18 @@ async function runReleaseCycle(o, movedPast2) {
11109
11122
  }
11110
11123
  o.state?.clearBlocked();
11111
11124
  const failedAt = o.state?.deployFailedSha();
11112
- if (!o.force && failedAt && failedAt === decision.head) {
11113
- o.emit.warn(
11114
- `a deploy already failed on ${failedAt.slice(0, 8)} \u2014 not retrying until a new commit lands (force it with \`crew deploy\`)`
11115
- );
11116
- return { merged: [], deployed: false, stopped: "previous deploy failed on this commit", decision };
11125
+ if (!o.force && failedAt) {
11126
+ const since2 = failedAt === decision.head ? [] : subjectsSince(o.cwd, failedAt);
11127
+ const real = since2?.filter((l) => !isReleaseCommitLine(l)) ?? null;
11128
+ if (real && real.length === 0) {
11129
+ o.emit.warn(
11130
+ `a deploy already failed on ${failedAt.slice(0, 8)} \u2014 not retrying until a new commit lands ` + (since2.length > 0 ? `(${since2.length} release commit(s) from elsewhere since do not count: ${since2.join("; ")}) ` : "") + "(force it with `crew deploy`)"
11131
+ );
11132
+ return { merged: [], deployed: false, stopped: "previous deploy failed on this commit", decision };
11133
+ }
11134
+ if (real) {
11135
+ o.emit.emit(`deploy guard lifted by ${real.length} new commit(s) since ${failedAt.slice(0, 8)}: ${real.join("; ")}`);
11136
+ }
11117
11137
  }
11118
11138
  if (o.repo.release.mode === "external") {
11119
11139
  o.emit.emit("release.mode is external \u2014 the crew hands work off and does not release");
@@ -11662,6 +11682,61 @@ function formatProjectsReport(report) {
11662
11682
  `;
11663
11683
  }
11664
11684
 
11685
+ // src/whoami.ts
11686
+ async function buildWhoami(routes, deps = {}) {
11687
+ const doFetch = deps.fetch ?? fetch;
11688
+ return Promise.all(routes.map(async (route) => {
11689
+ const credential = credentialSource(route);
11690
+ try {
11691
+ const key = resolveApiKey(route);
11692
+ const q = route.resolved ? `?workspaceId=${encodeURIComponent(route.resolved.workspaceId)}` : "";
11693
+ const res = await doFetch(`${route.baseUrl.replace(/\/+$/, "")}/api/auth/me${q}`, {
11694
+ headers: { Authorization: `Bearer ${key}`, "User-Agent": deps.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
11695
+ });
11696
+ if (res.status === 401 || res.status === 403) {
11697
+ return { route: route.route, ok: false, credential, error: `key rejected by ${route.baseUrl} (HTTP ${res.status}): expired or revoked, re-run crew connect` };
11698
+ }
11699
+ if (!res.ok) return { route: route.route, ok: false, credential, error: `/auth/me answered HTTP ${res.status}` };
11700
+ const me = await res.json();
11701
+ const operatorId = route.resolved?.operator ?? null;
11702
+ let operatorName = null;
11703
+ if (operatorId && deps.operatorName) {
11704
+ try {
11705
+ operatorName = await deps.operatorName(route, operatorId) ?? null;
11706
+ } catch {
11707
+ }
11708
+ }
11709
+ return {
11710
+ route: route.route,
11711
+ ok: true,
11712
+ credential,
11713
+ identity: { id: me.id ?? null, email: me.email ?? null, name: me.name ?? null },
11714
+ role: typeof me.role === "string" ? me.role : null,
11715
+ operator: { id: operatorId, name: operatorName }
11716
+ };
11717
+ } catch (e) {
11718
+ return { route: route.route, ok: false, credential, error: e.message };
11719
+ }
11720
+ }));
11721
+ }
11722
+ function formatWhoami(report) {
11723
+ const lines = [];
11724
+ for (const r of report) {
11725
+ lines.push(`route ${r.route}`);
11726
+ if (!r.ok) {
11727
+ lines.push(` error: ${r.error}`, ` credential: ${r.credential}`);
11728
+ continue;
11729
+ }
11730
+ const who2 = [r.identity.name, r.identity.email ? `<${r.identity.email}>` : null].filter(Boolean).join(" ");
11731
+ lines.push(` signed in as: ${who2 || r.identity.id || "(unknown)"}${r.identity.id ? ` (${r.identity.id})` : ""}`);
11732
+ lines.push(` role: ${r.role ?? "(unknown)"}`);
11733
+ lines.push(` operator: ${r.operator.id ? `${r.operator.name ?? "(unnamed)"} (${r.operator.id})` : "(not linked)"}`);
11734
+ lines.push(` credential: ${r.credential}`);
11735
+ }
11736
+ return `${lines.join("\n")}
11737
+ `;
11738
+ }
11739
+
11665
11740
  // src/cli.ts
11666
11741
  import { StaleWriteError as StaleWriteError9 } from "@tablation/client";
11667
11742
 
@@ -15465,6 +15540,7 @@ function usage() {
15465
15540
  crew watch [route] live view of what the crew is doing
15466
15541
  crew status [route] [--json] paused/running state; --json for a machine reader
15467
15542
  crew projects [--route R] [--json] the Projects rows each route can read (id, name, prefix, work mode)
15543
+ crew whoami [route] [--json] the signed-in identity, key role, operator Crew row and credential source per route
15468
15544
  crew reload make the running daemon re-read crew.yaml (--now: restart it immediately)
15469
15545
  crew doctor [route] [--fix] [--offline] [--json] preflight; --fix enables clean routes and offers crew install;
15470
15546
  --offline skips push probes; --json prints push-auth and tool checks only
@@ -15686,7 +15762,7 @@ if (command === "passenger-sync-daemon") {
15686
15762
  const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
15687
15763
  const worktreeArgs = command === "worktree" ? splitWorktreeArgs(positional, cfg.routes.map((r) => r.route)) : void 0;
15688
15764
  const named = worktreeArgs ? worktreeArgs.route : command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
15689
- const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
15765
+ const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || command === "whoami" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
15690
15766
  let route;
15691
15767
  try {
15692
15768
  route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
@@ -18890,6 +18966,23 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18890
18966
  if (results.some((r) => !r.ok)) process.exitCode = 1;
18891
18967
  break;
18892
18968
  }
18969
+ case "whoami": {
18970
+ const only = value("route") ?? positional[1];
18971
+ const wanted = only ? cfg.routes.filter((r) => r.route === only) : cfg.routes;
18972
+ if (only && wanted.length === 0) {
18973
+ process.stderr.write(`crew whoami: no route "${only}" (have: ${cfg.routes.map((r) => r.route).join(", ")})
18974
+ `);
18975
+ process.exit(2);
18976
+ }
18977
+ const report = await buildWhoami(wanted, {
18978
+ userAgent: cfg.ship.userAgent,
18979
+ operatorName: async (r, id) => (await new Tracker(r, cfg.ship).crewRows()).find((c) => c.id === id)?.name
18980
+ });
18981
+ process.stdout.write(flag("json") ? `${JSON.stringify(report.length === 1 && only ? report[0] : report, null, 2)}
18982
+ ` : formatWhoami(report));
18983
+ if (report.some((r) => !r.ok)) process.exitCode = 1;
18984
+ break;
18985
+ }
18893
18986
  case "projects": {
18894
18987
  const only = value("route") ?? positional[1];
18895
18988
  const wanted = only ? cfg.routes.filter((r) => r.route === only) : cfg.routes;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.67.1",
3
+ "version": "0.68.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",