@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.
- package/dist/cli.js +99 -6
- 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
|
|
11113
|
-
o.
|
|
11114
|
-
|
|
11115
|
-
)
|
|
11116
|
-
|
|
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