@tablation/crew 0.99.0 → 0.99.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 +91 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -83,6 +83,25 @@ function routePeriscopeProblems(o) {
|
|
|
83
83
|
function previewHostFinding(message) {
|
|
84
84
|
return /does not resolve/.test(message) ? { severity: "attention", kind: "preview_host_unresolvable", message } : { severity: "blocker", kind: "preview_host", message };
|
|
85
85
|
}
|
|
86
|
+
var refId = (v) => typeof v === "string" ? v : v && typeof v === "object" && typeof v.id === "string" ? v.id : void 0;
|
|
87
|
+
function duplicateLivePeriscopes(rows, tagOf, shipName2, route2) {
|
|
88
|
+
const byTicket = /* @__PURE__ */ new Map();
|
|
89
|
+
for (const r of rows) {
|
|
90
|
+
const t = refId(r.issue_id);
|
|
91
|
+
if (t) byTicket.set(t, [...byTicket.get(t) ?? [], r]);
|
|
92
|
+
}
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const [t, rs] of byTicket) {
|
|
95
|
+
if (rs.length < 2) continue;
|
|
96
|
+
out.push({
|
|
97
|
+
severity: "attention",
|
|
98
|
+
kind: "periscope_duplicate_live",
|
|
99
|
+
...route2 ? { route: route2 } : {},
|
|
100
|
+
message: `${tagOf(t)} has ${rs.length} live Periscopes rows (${rs.map((r) => `${r.id} on ${shipName2(r.ship_id)}${r.url ? ` ${r.url}` : ""}`).join("; ")}) \u2014 the periscope_url rollup assumes one; end all but one with \`crew periscope down\``
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
86
105
|
|
|
87
106
|
// src/periscope-state.ts
|
|
88
107
|
import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
@@ -7672,6 +7691,30 @@ var Tracker = class {
|
|
|
7672
7691
|
limit: 200
|
|
7673
7692
|
});
|
|
7674
7693
|
}
|
|
7694
|
+
/**
|
|
7695
|
+
* Every `live` Periscopes row on any ship (CREW-2871), for the one-per-issue
|
|
7696
|
+
* guard and doctor. Empty without the table.
|
|
7697
|
+
*/
|
|
7698
|
+
async livePeriscopesAllShips() {
|
|
7699
|
+
const modelId = this.route.resolved?.periscopesModelId;
|
|
7700
|
+
if (!modelId) return [];
|
|
7701
|
+
return this.client.records.list(modelId, {
|
|
7702
|
+
filters: encodeFilters([{ columnName: "state", operator: "EQ", value: "live" }]),
|
|
7703
|
+
limit: 500
|
|
7704
|
+
});
|
|
7705
|
+
}
|
|
7706
|
+
/** The `live` Periscopes rows of one ticket, on any ship (CREW-2871). */
|
|
7707
|
+
async livePeriscopesForTicket(ticketRecordId) {
|
|
7708
|
+
const modelId = this.route.resolved?.periscopesModelId;
|
|
7709
|
+
if (!modelId) return [];
|
|
7710
|
+
return this.client.records.list(modelId, {
|
|
7711
|
+
filters: encodeFilters([
|
|
7712
|
+
{ columnName: "issue_id", operator: "EQ", value: ticketRecordId },
|
|
7713
|
+
{ columnName: "state", operator: "EQ", value: "live" }
|
|
7714
|
+
]),
|
|
7715
|
+
limit: 50
|
|
7716
|
+
});
|
|
7717
|
+
}
|
|
7675
7718
|
/** Patches one Periscopes row (EPIC-077). */
|
|
7676
7719
|
async updatePeriscope(rowId, fields) {
|
|
7677
7720
|
const modelId = this.route.resolved?.periscopesModelId;
|
|
@@ -7696,12 +7739,12 @@ var Tracker = class {
|
|
|
7696
7739
|
const conds = [{ columnName: "ship_id", operator: "EQ", value: shipId }];
|
|
7697
7740
|
if (states?.length) conds.push({ columnName: "state", operator: "IN", value: states });
|
|
7698
7741
|
const rows = await this.client.records.list(modelId, { filters: encodeFilters(conds), limit: 200 });
|
|
7699
|
-
const
|
|
7700
|
-
const ids = [...new Set(rows.map((r) =>
|
|
7742
|
+
const refId2 = (v) => typeof v === "string" ? v : v && typeof v === "object" && typeof v.id === "string" ? v.id : void 0;
|
|
7743
|
+
const ids = [...new Set(rows.map((r) => refId2(r.issue_id)).filter((x) => !!x))];
|
|
7701
7744
|
const tickets = new Map((await this.ticketsByIds(ids)).map((t) => [t.id, t]));
|
|
7702
7745
|
const out = [];
|
|
7703
7746
|
for (const row of rows) {
|
|
7704
|
-
const t = tickets.get(
|
|
7747
|
+
const t = tickets.get(refId2(row.issue_id) ?? "");
|
|
7705
7748
|
if (t) out.push({ row, ticket: t });
|
|
7706
7749
|
}
|
|
7707
7750
|
return out.sort((a, b) => String(b.row.started_at ?? "").localeCompare(String(a.row.started_at ?? "")));
|
|
@@ -17710,6 +17753,13 @@ import { existsSync as existsSync35, mkdirSync as mkdirSync21, readFileSync as r
|
|
|
17710
17753
|
import { join as join37 } from "path";
|
|
17711
17754
|
var PeriscopeError = class extends Error {
|
|
17712
17755
|
};
|
|
17756
|
+
var LivePeriscopeError = class extends PeriscopeError {
|
|
17757
|
+
data;
|
|
17758
|
+
constructor(tag, number, live) {
|
|
17759
|
+
super(`${tag} already has a live periscope on ${live.ship}: ${live.url} (worktree ${live.worktree}) \u2014 run \`crew periscope down ${number} --keep|--discard\` on ${live.ship}, or resume it there with \`crew periscope up --resume ${number}\``);
|
|
17760
|
+
this.data = { refused: "live_periscope_exists", ...live };
|
|
17761
|
+
}
|
|
17762
|
+
};
|
|
17713
17763
|
var priorDir = (stateDir) => join37(stateDir, "periscope-priors");
|
|
17714
17764
|
var priorPath = (stateDir, ticketRecordId) => join37(priorDir(stateDir), `${ticketRecordId}.json`);
|
|
17715
17765
|
function removePeriscopePrior(stateDir, ticketRecordId) {
|
|
@@ -17776,6 +17826,10 @@ function periscopeCaps(l) {
|
|
|
17776
17826
|
idleMinutes: l.periscopeIdleMinutes ?? DEFAULT_PERISCOPE_IDLE_MINUTES2
|
|
17777
17827
|
};
|
|
17778
17828
|
}
|
|
17829
|
+
async function assertNoLivePeriscope(e, t, number) {
|
|
17830
|
+
const live = (await e.liveRows(t.id))[0];
|
|
17831
|
+
if (live) throw new LivePeriscopeError(t.tag, number, live);
|
|
17832
|
+
}
|
|
17779
17833
|
async function existingRefusal(e, t) {
|
|
17780
17834
|
if (t.closed) return `${t.tag} is ${t.status}; a closed ticket cannot be periscoped`;
|
|
17781
17835
|
if (e.repoId && t.repoId !== e.repoId) return `${t.tag} belongs to a different repository than ${e.repoName}`;
|
|
@@ -17834,6 +17888,7 @@ async function runPeriscopeUp(d, args) {
|
|
|
17834
17888
|
if (!ex) throw new PeriscopeError("--issue is not supported here");
|
|
17835
17889
|
prior = await ex.load(args.issue);
|
|
17836
17890
|
if (!prior) throw new PeriscopeError(`no ticket numbered ${args.issue} on this route`);
|
|
17891
|
+
await assertNoLivePeriscope(ex, prior, args.issue);
|
|
17837
17892
|
const refused = await existingRefusal(ex, prior);
|
|
17838
17893
|
if (refused) throw new PeriscopeError(refused);
|
|
17839
17894
|
const unsafe = d.baseProblem();
|
|
@@ -20311,6 +20366,25 @@ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname22(process.execPath)
|
|
|
20311
20366
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
20312
20367
|
var CYCLE_SECONDS2 = 120;
|
|
20313
20368
|
var STALE_CYCLES = 3;
|
|
20369
|
+
async function duplicateLivePeriscopeProblems(route2, ship) {
|
|
20370
|
+
try {
|
|
20371
|
+
const tr = new Tracker(route2, ship);
|
|
20372
|
+
const rows = await tr.livePeriscopesAllShips();
|
|
20373
|
+
const refOf = (v) => typeof v === "string" ? v : v?.id;
|
|
20374
|
+
const counts = /* @__PURE__ */ new Map();
|
|
20375
|
+
for (const r of rows) {
|
|
20376
|
+
const t = refOf(r.issue_id);
|
|
20377
|
+
if (t) counts.set(t, (counts.get(t) ?? 0) + 1);
|
|
20378
|
+
}
|
|
20379
|
+
const dupIds = [...counts].filter(([, n]) => n > 1).map(([t]) => t);
|
|
20380
|
+
if (dupIds.length === 0) return [];
|
|
20381
|
+
const tags = new Map((await tr.ticketsByIds(dupIds)).map((t) => [t.id, t.issue_tag ?? t.issue_id]));
|
|
20382
|
+
const ships = new Map((await tr.shipRows()).map((x) => [x.id, x.name ?? x.id]));
|
|
20383
|
+
return duplicateLivePeriscopes(rows, (id) => tags.get(id) ?? id, (id) => id ? ships.get(id) ?? id : "an unknown ship", route2.route);
|
|
20384
|
+
} catch {
|
|
20385
|
+
return [];
|
|
20386
|
+
}
|
|
20387
|
+
}
|
|
20314
20388
|
function renderShipLine(s, issueLabelById) {
|
|
20315
20389
|
const name = s.name ?? "(unnamed)";
|
|
20316
20390
|
if (!s.last_seen) return `${name.padEnd(20)} never seen`;
|
|
@@ -24371,6 +24445,10 @@ ${where.join("\n")}`);
|
|
|
24371
24445
|
const row = crewP.find((r) => r.id === assigneeId);
|
|
24372
24446
|
return row?.agent_id ? row.name ?? assigneeId : void 0;
|
|
24373
24447
|
},
|
|
24448
|
+
liveRows: async (ticketRecordId) => {
|
|
24449
|
+
const [rows, ships] = [await trackerP.livePeriscopesForTicket(ticketRecordId), await trackerP.shipRows()];
|
|
24450
|
+
return rows.map((r) => ({ ship: ships.find((x) => x.id === r.ship_id)?.name ?? r.ship_id ?? "an unknown ship", url: r.url ?? "", worktree: r.worktree }));
|
|
24451
|
+
},
|
|
24374
24452
|
hasLiveRun: (ticketRecordId) => liveRuns(cfg.ship.stateDir).some((r) => r.ticketRecordId === ticketRecordId),
|
|
24375
24453
|
hold: async (id) => {
|
|
24376
24454
|
const auth = await trackerP.authorshipProbe();
|
|
@@ -24394,6 +24472,7 @@ ${where.join("\n")}`);
|
|
|
24394
24472
|
if (a.issue) {
|
|
24395
24473
|
const t = await deps.existing.load(a.issue);
|
|
24396
24474
|
if (!t) throw new PeriscopeError(`no ticket numbered ${a.issue} on this route`);
|
|
24475
|
+
await assertNoLivePeriscope(deps.existing, t, a.issue);
|
|
24397
24476
|
const refused = await existingRefusal(deps.existing, t);
|
|
24398
24477
|
if (refused) throw new PeriscopeError(refused);
|
|
24399
24478
|
say("dry-run", `would hold ${t.tag} (status ${t.status} stays) for ${pairId}, cut or reuse its worktree for ${a.repo}, run setup${a.isolate ? ", isolate" : ""}, ports, handoff and preview on ${deps.host}, and record a Periscopes row`);
|
|
@@ -24417,6 +24496,8 @@ ${where.join("\n")}`);
|
|
|
24417
24496
|
} catch (e) {
|
|
24418
24497
|
if (e instanceof PeriscopeError || e instanceof ConfigError) {
|
|
24419
24498
|
process.stderr.write(`crew periscope: ${e.message}
|
|
24499
|
+
`);
|
|
24500
|
+
if (e instanceof LivePeriscopeError && jsonP) process.stdout.write(`${JSON.stringify(e.data)}
|
|
24420
24501
|
`);
|
|
24421
24502
|
process.exit(1);
|
|
24422
24503
|
}
|
|
@@ -25206,6 +25287,7 @@ watermark: ${state.watermark()}
|
|
|
25206
25287
|
const { settled } = await settleRepos(reposOf(route2), (t) => t.name, (t) => resolveOneRepo(route2, t));
|
|
25207
25288
|
const tablePresent = flag("offline") ? void 0 : (await new Tracker(route2, cfg.ship).periscopesStatus().catch(() => ({ present: true }))).present;
|
|
25208
25289
|
problems.push(...routePeriscopeProblems({ route: route2.route, hosts: true, repos: settled.map((r) => ({ name: r.name, hasPreviewHook: !!r.config.hooks.preview })), tablePresent }));
|
|
25290
|
+
if (!flag("offline")) problems.push(...await duplicateLivePeriscopeProblems(route2, cfg.ship));
|
|
25209
25291
|
}
|
|
25210
25292
|
problems.push(...attentionProblems(openShipAttention(state), problems));
|
|
25211
25293
|
if (problems.some((p) => p.severity === "blocker")) process.exitCode = 1;
|
|
@@ -25351,6 +25433,12 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
25351
25433
|
process.stdout.write(`periscopes: ${f.severity.toUpperCase()} ${f.message}
|
|
25352
25434
|
`);
|
|
25353
25435
|
}
|
|
25436
|
+
if (!flag("offline")) {
|
|
25437
|
+
for (const f of await duplicateLivePeriscopeProblems(route2, cfg.ship)) {
|
|
25438
|
+
process.stdout.write(`periscopes: ${f.severity.toUpperCase()} ${f.message}
|
|
25439
|
+
`);
|
|
25440
|
+
}
|
|
25441
|
+
}
|
|
25354
25442
|
}
|
|
25355
25443
|
process.stdout.write(
|
|
25356
25444
|
!periscopes.present ? "periscopes: Periscopes table absent \u2014 this workspace cannot host periscopes\n" : periscopes.missing.length === 0 ? `periscopes: Periscopes table present (${periscopes.modelId})
|
package/package.json
CHANGED