@tablation/crew 0.84.0 → 0.85.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 +303 -14
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5606,7 +5606,7 @@ function validateContract(c) {
|
|
|
5606
5606
|
}
|
|
5607
5607
|
var EVENT_LOG_MISSING = "the Issues table has no event log \u2014 crew writes its audit trail there (CREW-1574). In the app, open the Issues table's Data & lifecycle tab and turn on Event log, then rerun.";
|
|
5608
5608
|
var PERISCOPE_COLUMNS = [
|
|
5609
|
-
"
|
|
5609
|
+
"issue_id",
|
|
5610
5610
|
"ship_id",
|
|
5611
5611
|
"url",
|
|
5612
5612
|
"host",
|
|
@@ -6879,6 +6879,32 @@ var Tracker = class {
|
|
|
6879
6879
|
const have = await this.columnNames(modelId);
|
|
6880
6880
|
return { present: true, modelId, missing: PERISCOPE_COLUMNS.filter((c) => !have.has(c)) };
|
|
6881
6881
|
}
|
|
6882
|
+
/**
|
|
6883
|
+
* How many `live` Periscopes rows this ship holds (EPIC-077). Zero when the
|
|
6884
|
+
* workspace has no Periscopes table or this ship has no Ships row yet (the
|
|
6885
|
+
* row's `ship_id` could not name it), so the cap only ever counts rows it
|
|
6886
|
+
* can attribute.
|
|
6887
|
+
*/
|
|
6888
|
+
async livePeriscopeCount() {
|
|
6889
|
+
const modelId = this.route.resolved?.periscopesModelId;
|
|
6890
|
+
const { shipId } = await this.authorshipProbe();
|
|
6891
|
+
if (!modelId || !shipId) return 0;
|
|
6892
|
+
const rows = await this.client.records.list(modelId, {
|
|
6893
|
+
filters: encodeFilters([
|
|
6894
|
+
{ columnName: "ship_id", operator: "EQ", value: shipId },
|
|
6895
|
+
{ columnName: "state", operator: "EQ", value: "live" }
|
|
6896
|
+
]),
|
|
6897
|
+
limit: 200
|
|
6898
|
+
});
|
|
6899
|
+
return rows.length;
|
|
6900
|
+
}
|
|
6901
|
+
/** Records a Periscopes row for this ship (EPIC-077); `ship_id` is stamped here, never by the caller. */
|
|
6902
|
+
async createPeriscope(fields) {
|
|
6903
|
+
const modelId = this.route.resolved?.periscopesModelId;
|
|
6904
|
+
if (!modelId) throw new Error("this workspace has no Periscopes table");
|
|
6905
|
+
const { shipId } = await this.authorshipProbe();
|
|
6906
|
+
return this.client.records.create(modelId, { ...fields, ...shipId ? { ship_id: shipId } : {} });
|
|
6907
|
+
}
|
|
6882
6908
|
/**
|
|
6883
6909
|
* The authorship columns this workspace is missing, as `table.column` names
|
|
6884
6910
|
* for `crew doctor` to report (CREW-1371). Empty on a fully updated install.
|
|
@@ -9102,8 +9128,8 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
9102
9128
|
const meta = { pid: child.pid ?? null };
|
|
9103
9129
|
obs.onClaim = () => writeRunMeta(id, startedAt, obs.liveTicket, meta);
|
|
9104
9130
|
if (child.pid !== void 0) writeRunMeta(id, startedAt, obs.liveTicket, meta);
|
|
9105
|
-
const
|
|
9106
|
-
const timer = setInterval(() => obs.feed(
|
|
9131
|
+
const tail2 = new FileTail(plan.streamPath);
|
|
9132
|
+
const timer = setInterval(() => obs.feed(tail2.poll()), TAIL_POLL_MS);
|
|
9107
9133
|
let spawnFailed = false;
|
|
9108
9134
|
child.on("error", (err) => {
|
|
9109
9135
|
spawnFailed = true;
|
|
@@ -9112,7 +9138,7 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
9112
9138
|
});
|
|
9113
9139
|
child.on("close", async (code) => {
|
|
9114
9140
|
clearInterval(timer);
|
|
9115
|
-
obs.feed(
|
|
9141
|
+
obs.feed(tail2.poll());
|
|
9116
9142
|
obs.end();
|
|
9117
9143
|
await endSink(eventsSink);
|
|
9118
9144
|
if (spawnFailed) return;
|
|
@@ -9131,20 +9157,20 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
|
|
|
9131
9157
|
const id = { ...sc, streamPath };
|
|
9132
9158
|
const eventsSink = openSink(sc.eventsPath);
|
|
9133
9159
|
const obs = new RunObserver(id, adapter, eventsSink, emit.cycle);
|
|
9134
|
-
const
|
|
9160
|
+
const tail2 = new FileTail(streamPath);
|
|
9135
9161
|
obs.muted = true;
|
|
9136
|
-
obs.feed(
|
|
9162
|
+
obs.feed(tail2.poll());
|
|
9137
9163
|
obs.muted = false;
|
|
9138
9164
|
const adopted = isAlive(sc);
|
|
9139
9165
|
if (adopted) {
|
|
9140
9166
|
o.onAdopted?.();
|
|
9141
9167
|
const pollMs = o.pollMs ?? TAIL_POLL_MS;
|
|
9142
9168
|
while (isAlive(sc)) {
|
|
9143
|
-
obs.feed(
|
|
9169
|
+
obs.feed(tail2.poll());
|
|
9144
9170
|
await new Promise((r) => setTimeout(r, pollMs));
|
|
9145
9171
|
}
|
|
9146
9172
|
}
|
|
9147
|
-
obs.feed(
|
|
9173
|
+
obs.feed(tail2.poll());
|
|
9148
9174
|
obs.end();
|
|
9149
9175
|
await endSink(eventsSink);
|
|
9150
9176
|
const interrupted = obs.sawResult ? void 0 : adopted ? "no-result" : "daemon-exit";
|
|
@@ -9692,7 +9718,9 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
9692
9718
|
"toolchain",
|
|
9693
9719
|
"workspace",
|
|
9694
9720
|
"workspace-id",
|
|
9695
|
-
"write"
|
|
9721
|
+
"write",
|
|
9722
|
+
"topic",
|
|
9723
|
+
"assignee"
|
|
9696
9724
|
]);
|
|
9697
9725
|
function positionalArgs(argv2) {
|
|
9698
9726
|
const out = [];
|
|
@@ -13032,7 +13060,7 @@ async function isolateAndRetry(o, name, output, merged, preMerge, decision, fres
|
|
|
13032
13060
|
}
|
|
13033
13061
|
if (culprits.length === 0 && truncated) {
|
|
13034
13062
|
let lo = 2, hi = applied.length;
|
|
13035
|
-
let
|
|
13063
|
+
let tail2 = lastLines(output, HOOK_FAILURE_TAIL_LINES);
|
|
13036
13064
|
while (lo < hi) {
|
|
13037
13065
|
const mid = Math.floor((lo + hi) / 2);
|
|
13038
13066
|
gitOk(o.cwd, ["reset", "--hard", preMerge]);
|
|
@@ -13047,12 +13075,12 @@ async function isolateAndRetry(o, name, output, merged, preMerge, decision, fres
|
|
|
13047
13075
|
const r = clean ? await hook(o, name) : null;
|
|
13048
13076
|
if (r && r.code !== 0) {
|
|
13049
13077
|
hi = mid;
|
|
13050
|
-
|
|
13078
|
+
tail2 = lastLines(r.output, HOOK_FAILURE_TAIL_LINES);
|
|
13051
13079
|
} else lo = mid + 1;
|
|
13052
13080
|
}
|
|
13053
13081
|
const owner = applied[hi - 1];
|
|
13054
13082
|
o.emit.warn(`${owner.branch} is the first branch whose prefix fails the ${name} hook (bisected over ${applied.length})`, { ticket: owner.ticket.issue_id });
|
|
13055
|
-
culprits.push({ c: owner, how: "pair", sibling: applied[hi - 2], tail });
|
|
13083
|
+
culprits.push({ c: owner, how: "pair", sibling: applied[hi - 2], tail: tail2 });
|
|
13056
13084
|
}
|
|
13057
13085
|
}
|
|
13058
13086
|
}
|
|
@@ -16373,6 +16401,139 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
16373
16401
|
// src/cli.ts
|
|
16374
16402
|
import { realpathSync as realpathSync3, readFileSync as readFileSync31, existsSync as existsSync35, mkdirSync as mkdirSync22, writeFileSync as writeFileSync23, rmSync as rmSync15, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync10, readdirSync as readdirSync16 } from "fs";
|
|
16375
16403
|
import { createInterface } from "readline/promises";
|
|
16404
|
+
|
|
16405
|
+
// src/periscope-cmd.ts
|
|
16406
|
+
var PeriscopeError = class extends Error {
|
|
16407
|
+
};
|
|
16408
|
+
var DEFAULT_MAX_PERISCOPES2 = 3;
|
|
16409
|
+
var DEFAULT_PERISCOPE_IDLE_MINUTES2 = 120;
|
|
16410
|
+
function parsePeriscopeUpArgs(positional2, flags) {
|
|
16411
|
+
const rest = positional2.slice(2);
|
|
16412
|
+
if (rest.length === 0 || rest.length > 2) {
|
|
16413
|
+
throw new PeriscopeError("usage: crew periscope up [route] REPO [--topic TEXT] [--isolate] [--host HOST] [--assignee CREW_ROW_ID] [--json]");
|
|
16414
|
+
}
|
|
16415
|
+
const topic = flags.topic?.trim();
|
|
16416
|
+
return {
|
|
16417
|
+
...rest.length === 2 ? { route: rest[0] } : {},
|
|
16418
|
+
repo: rest[rest.length - 1],
|
|
16419
|
+
...topic ? { topic } : {},
|
|
16420
|
+
isolate: flags.isolate,
|
|
16421
|
+
...flags.host ? { host: flags.host } : {},
|
|
16422
|
+
...flags.assignee ? { assignee: flags.assignee } : {},
|
|
16423
|
+
json: flags.json
|
|
16424
|
+
};
|
|
16425
|
+
}
|
|
16426
|
+
function parseEnvLines(output) {
|
|
16427
|
+
const env = {};
|
|
16428
|
+
for (const line of output.split("\n")) {
|
|
16429
|
+
const m = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line.trim());
|
|
16430
|
+
if (m) env[m[1]] = m[2].replace(/^"(.*)"$/, "$1");
|
|
16431
|
+
}
|
|
16432
|
+
return env;
|
|
16433
|
+
}
|
|
16434
|
+
function previewUrlFrom(output) {
|
|
16435
|
+
const last = output.split("\n").map((l) => l.trim()).filter(Boolean).pop();
|
|
16436
|
+
return last && /^https?:\/\/\S+$/i.test(last) ? last : void 0;
|
|
16437
|
+
}
|
|
16438
|
+
function periscopeCaps(l) {
|
|
16439
|
+
return {
|
|
16440
|
+
max: l.maxPeriscopes ?? DEFAULT_MAX_PERISCOPES2,
|
|
16441
|
+
idleMinutes: l.periscopeIdleMinutes ?? DEFAULT_PERISCOPE_IDLE_MINUTES2
|
|
16442
|
+
};
|
|
16443
|
+
}
|
|
16444
|
+
var tail = (s, n = 20) => s.trimEnd().split("\n").slice(-n).join("\n");
|
|
16445
|
+
function progressComment(url, handoff) {
|
|
16446
|
+
return [
|
|
16447
|
+
`Periscope is up: ${url}`,
|
|
16448
|
+
...handoff?.trim() ? ["", handoff.trim()] : []
|
|
16449
|
+
].join("\n");
|
|
16450
|
+
}
|
|
16451
|
+
async function periscopePreflight(d, repo) {
|
|
16452
|
+
if (!d.previewHook) {
|
|
16453
|
+
throw new PeriscopeError(`${repo} declares no \`preview\` hook, so it cannot host a periscope \u2014 add one to its .crew.yaml (see the repository spec page, "hooks.preview")`);
|
|
16454
|
+
}
|
|
16455
|
+
if (!d.hasPeriscopesTable) {
|
|
16456
|
+
throw new PeriscopeError("this workspace has no Periscopes table \u2014 update the Issues template and run `crew connect`");
|
|
16457
|
+
}
|
|
16458
|
+
const live = await d.liveCount();
|
|
16459
|
+
if (live >= d.caps.max) {
|
|
16460
|
+
throw new PeriscopeError(`this ship already hosts ${live} live periscope(s) (ship.maxPeriscopes is ${d.caps.max}) \u2014 discard one first`);
|
|
16461
|
+
}
|
|
16462
|
+
}
|
|
16463
|
+
async function runPeriscopeUp(d, args) {
|
|
16464
|
+
await periscopePreflight(d, args.repo);
|
|
16465
|
+
const topic = args.topic ?? (await d.askTopic())?.trim();
|
|
16466
|
+
if (!topic) throw new PeriscopeError("a topic is required: pass --topic TEXT");
|
|
16467
|
+
const unsafe = d.baseProblem();
|
|
16468
|
+
if (unsafe) throw new PeriscopeError(`DO NOT CUT A WORKTREE FROM THIS BASE \u2014 ${unsafe}`);
|
|
16469
|
+
const draft = await d.fileDraft(`Periscope: ${topic}`);
|
|
16470
|
+
d.say("filed", `filed draft ${draft.tag}`, { ticket: draft.tag, ticketRecordId: draft.id });
|
|
16471
|
+
let wt;
|
|
16472
|
+
const abandon = async (why) => {
|
|
16473
|
+
if (wt) await d.teardown(wt.path, wt.branch).catch(() => {
|
|
16474
|
+
});
|
|
16475
|
+
await d.closeDraft(draft.id, `Periscope abandoned: ${why.split("\n")[0]}`).catch(() => {
|
|
16476
|
+
});
|
|
16477
|
+
throw new PeriscopeError(why);
|
|
16478
|
+
};
|
|
16479
|
+
try {
|
|
16480
|
+
wt = d.cutWorktree(draft);
|
|
16481
|
+
} catch (e) {
|
|
16482
|
+
return abandon(e.message);
|
|
16483
|
+
}
|
|
16484
|
+
d.say("worktree", `cut ${wt.path} on ${wt.branch}`, { worktree: wt.path, branch: wt.branch });
|
|
16485
|
+
const prepared = await d.prepare(wt.path);
|
|
16486
|
+
if (prepared && prepared.code !== 0) {
|
|
16487
|
+
return abandon(`setup failed (exit ${prepared.code})
|
|
16488
|
+
${tail(prepared.output)}`);
|
|
16489
|
+
}
|
|
16490
|
+
const env = { CREW_TICKET: draft.tag, CREW_BRANCH: wt.branch };
|
|
16491
|
+
const ports = await d.runHook("ports", wt.path, env);
|
|
16492
|
+
if (ports) Object.assign(env, parseEnvLines(ports.output));
|
|
16493
|
+
if (args.isolate) {
|
|
16494
|
+
const iso = await d.runHook("isolate", wt.path, env);
|
|
16495
|
+
if (iso && iso.code !== 0) return abandon(`isolate failed (exit ${iso.code})
|
|
16496
|
+
${tail(iso.output)}`);
|
|
16497
|
+
if (iso) Object.assign(env, parseEnvLines(iso.output));
|
|
16498
|
+
}
|
|
16499
|
+
const handoff = await d.runHook("handoff", wt.path, env);
|
|
16500
|
+
if (handoff && handoff.code !== 0) return abandon(`handoff failed (exit ${handoff.code})
|
|
16501
|
+
${tail(handoff.output)}`);
|
|
16502
|
+
const preview = await d.runHook("preview", wt.path, { ...env, CREW_PREVIEW_HOST: d.host });
|
|
16503
|
+
if (!preview || preview.code !== 0) {
|
|
16504
|
+
return abandon(`preview exited ${preview?.code ?? "without running"}
|
|
16505
|
+
${tail(preview?.output ?? "")}`);
|
|
16506
|
+
}
|
|
16507
|
+
const url = previewUrlFrom(preview.output);
|
|
16508
|
+
if (!url) return abandon(`preview printed no URL as its last line
|
|
16509
|
+
${tail(preview.output)}`);
|
|
16510
|
+
d.say("preview", `preview at ${url}`, { url });
|
|
16511
|
+
const now = d.now();
|
|
16512
|
+
let row;
|
|
16513
|
+
try {
|
|
16514
|
+
row = await d.createRow({
|
|
16515
|
+
issue_id: draft.id,
|
|
16516
|
+
url,
|
|
16517
|
+
host: d.host,
|
|
16518
|
+
worktree: wt.path,
|
|
16519
|
+
branch: wt.branch,
|
|
16520
|
+
state: "live",
|
|
16521
|
+
isolated: args.isolate,
|
|
16522
|
+
started_at: now.toISOString(),
|
|
16523
|
+
last_activity_at: now.toISOString(),
|
|
16524
|
+
expires_at: new Date(now.getTime() + d.caps.idleMinutes * 6e4).toISOString()
|
|
16525
|
+
});
|
|
16526
|
+
} catch (e) {
|
|
16527
|
+
return abandon(`recording the Periscopes row failed: ${e.message}`);
|
|
16528
|
+
}
|
|
16529
|
+
d.say("recorded", "recorded the Periscopes row", { periscopeRecordId: row.id });
|
|
16530
|
+
await d.comment(draft.id, progressComment(url, handoff?.output)).catch((e) => {
|
|
16531
|
+
d.say("warning", `progress comment not posted: ${e.message}`);
|
|
16532
|
+
});
|
|
16533
|
+
return { ticket: draft.tag, ticketRecordId: draft.id, worktree: wt.path, branch: wt.branch, url, periscopeRecordId: row.id };
|
|
16534
|
+
}
|
|
16535
|
+
|
|
16536
|
+
// src/cli.ts
|
|
16376
16537
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
16377
16538
|
|
|
16378
16539
|
// src/ssh-keys.ts
|
|
@@ -17833,6 +17994,8 @@ function usage(problem, requested = false) {
|
|
|
17833
17994
|
crew reap [route] kill orphaned servers, drop worktrees for closed tickets
|
|
17834
17995
|
crew drop [route] NNN remove a merged ticket's worktree and branch
|
|
17835
17996
|
crew rotate-passenger-url [route] force a Host Passengers tunnel to reconnect with a fresh public URL
|
|
17997
|
+
crew periscope up [route] REPO --topic TEXT [--isolate] [--host H] [--assignee ID] [--dry-run] [--json]
|
|
17998
|
+
file a draft ticket, cut and prepare its worktree, start the repo's preview hook and record a Periscopes row (alias: start)
|
|
17836
17999
|
crew worktree [route] NNN cut (or refresh) this run's worktree for a ticket from the remote; prints its path
|
|
17837
18000
|
crew unassign [route] NNN hand back a session's ticket \u2014 clears assignee, next cycle picks it up
|
|
17838
18001
|
crew sync [route] fast-forward the checkout and its worktrees from the remote
|
|
@@ -17925,6 +18088,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
17925
18088
|
"merge",
|
|
17926
18089
|
"passengers",
|
|
17927
18090
|
"pause",
|
|
18091
|
+
"periscope",
|
|
17928
18092
|
"poll",
|
|
17929
18093
|
"ports",
|
|
17930
18094
|
"projects",
|
|
@@ -18128,7 +18292,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18128
18292
|
const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
|
|
18129
18293
|
const worktreeArgs = command === "worktree" ? splitWorktreeArgs(positional, cfg.routes.map((r) => r.route)) : void 0;
|
|
18130
18294
|
const named = worktreeArgs ? worktreeArgs.route : command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
|
|
18131
|
-
const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "ship" || 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;
|
|
18295
|
+
const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "ship" || command === "projects" || command === "periscope" || 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;
|
|
18132
18296
|
let route;
|
|
18133
18297
|
try {
|
|
18134
18298
|
route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
|
|
@@ -21209,7 +21373,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21209
21373
|
const hitsWt = /* @__PURE__ */ new Map();
|
|
21210
21374
|
for (const cand of candidatesWt) {
|
|
21211
21375
|
const tr = new Tracker(cand, cfg.ship);
|
|
21212
|
-
const hit = (await tr.openTickets()).find((t) => t[tr.contract.columns.key] === `ISSUE-${nWt}`);
|
|
21376
|
+
const hit = (await tr.openTickets()).find((t) => t[tr.contract.columns.key] === `ISSUE-${nWt}`) ?? (await tr.ticketsByNumbers([nWt])).find((t) => t[tr.contract.columns.key] === `ISSUE-${nWt}` && t[tr.contract.columns.status] === tr.contract.statuses.draft);
|
|
21213
21377
|
if (hit) hitsWt.set(cand.route, hit);
|
|
21214
21378
|
lookedWt.push({
|
|
21215
21379
|
route: cand.route,
|
|
@@ -21256,6 +21420,131 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21256
21420
|
if (!planIsUsable(planWt)) process.exitCode = 1;
|
|
21257
21421
|
break;
|
|
21258
21422
|
}
|
|
21423
|
+
case "periscope": {
|
|
21424
|
+
const subP = positional[1];
|
|
21425
|
+
if (subP !== "up" && subP !== "start") {
|
|
21426
|
+
process.stderr.write("crew periscope up [route] REPO --topic TEXT [--isolate] [--host H] [--assignee ID] [--dry-run] [--json]\n");
|
|
21427
|
+
process.exit(2);
|
|
21428
|
+
}
|
|
21429
|
+
const jsonP = flag("json");
|
|
21430
|
+
const say = (event, message, data = {}) => process.stderr.write(jsonP ? `${JSON.stringify({ ...data, event, message })}
|
|
21431
|
+
` : `${message}
|
|
21432
|
+
`);
|
|
21433
|
+
try {
|
|
21434
|
+
const a = parsePeriscopeUpArgs(positional, { topic: value("topic"), host: value("host"), assignee: value("assignee"), isolate: flag("isolate"), json: jsonP });
|
|
21435
|
+
const candidatesP = a.route ? [findRoute(cfg, a.route)] : cfg.routes.filter((r) => r.enabled);
|
|
21436
|
+
const servedP = [];
|
|
21437
|
+
for (const cand of candidatesP) {
|
|
21438
|
+
const hit = (await resolvedRepos(cand)).find((r) => r.name === a.repo);
|
|
21439
|
+
if (hit) servedP.push({ route: cand, repo: hit });
|
|
21440
|
+
}
|
|
21441
|
+
if (servedP.length !== 1) {
|
|
21442
|
+
throw new PeriscopeError(servedP.length === 0 ? `no route serves a repository named "${a.repo}"` : `"${a.repo}" is served by more than one route (${servedP.map((x) => x.route.route).join(", ")}) \u2014 name the route`);
|
|
21443
|
+
}
|
|
21444
|
+
const { route: routeP, repo: repoP } = servedP[0];
|
|
21445
|
+
if (!routeP.resolved) throw new PeriscopeError(`route ${routeP.route} is not connected \u2014 run \`crew connect\` first`);
|
|
21446
|
+
const trackerP = new Tracker(routeP, cfg.ship);
|
|
21447
|
+
const cP = trackerP.contract;
|
|
21448
|
+
const periscopes = await trackerP.periscopesStatus();
|
|
21449
|
+
const caps = periscopeCaps(cfg.ship);
|
|
21450
|
+
const shellP = resolveShell(repoP.config.shell ?? cfg.ship.shell);
|
|
21451
|
+
const pairId = a.assignee ?? routeP.resolved.seats.pair;
|
|
21452
|
+
if (!pairId) throw new PeriscopeError(`route ${routeP.route} has no Pair seat \u2014 pass --assignee CREW_ROW_ID`);
|
|
21453
|
+
const crewP = await trackerP.crewRows();
|
|
21454
|
+
const operatorName = crewP.find((r) => r.id === routeP.resolved.operator)?.name ?? "Pair";
|
|
21455
|
+
const memberId = routeP.resolved.seats.pair ?? routeP.resolved.operator;
|
|
21456
|
+
const deps = {
|
|
21457
|
+
previewHook: repoP.config.hooks.preview,
|
|
21458
|
+
hasPeriscopesTable: periscopes.present,
|
|
21459
|
+
liveCount: () => trackerP.livePeriscopeCount(),
|
|
21460
|
+
caps,
|
|
21461
|
+
baseProblem: () => {
|
|
21462
|
+
const h = baseHealth(repoP);
|
|
21463
|
+
return h.unsafe ? h.detail : void 0;
|
|
21464
|
+
},
|
|
21465
|
+
askTopic: async () => {
|
|
21466
|
+
if (!process.stdin.isTTY || jsonP) return void 0;
|
|
21467
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
21468
|
+
try {
|
|
21469
|
+
return await rl.question("Periscope topic: ");
|
|
21470
|
+
} finally {
|
|
21471
|
+
rl.close();
|
|
21472
|
+
}
|
|
21473
|
+
},
|
|
21474
|
+
fileDraft: async (title) => {
|
|
21475
|
+
const t = await trackerP.fileTicket({
|
|
21476
|
+
title,
|
|
21477
|
+
description: `A periscope: a throwaway preview of ${a.repo} for a viewer, opened by \`crew periscope up\`.`,
|
|
21478
|
+
report_type: "feature",
|
|
21479
|
+
reporter_name: operatorName,
|
|
21480
|
+
[cP.columns.status]: cP.statuses.draft,
|
|
21481
|
+
[cP.columns.assignee]: pairId,
|
|
21482
|
+
...routeP.resolved.areaId ? { [cP.columns.slice]: routeP.resolved.areaId } : {},
|
|
21483
|
+
...repoP.id ? { [cP.columns.repo]: repoP.id } : {}
|
|
21484
|
+
}, memberId);
|
|
21485
|
+
return { id: t.id, tag: t.issue_tag ?? t.issue_id, issueId: t.issue_id, prefix: t.project_issue_prefix };
|
|
21486
|
+
},
|
|
21487
|
+
cutWorktree: (draft) => {
|
|
21488
|
+
const plan = planRunWorktree(repoP.dir, repoP.config, { issue_id: draft.issueId, issue_tag: draft.tag, project_issue_prefix: draft.prefix }, void 0);
|
|
21489
|
+
if (!planIsUsable(plan)) throw new PeriscopeError(`cannot use the worktree: ${describeRunWorktreePlan(plan)}`);
|
|
21490
|
+
const path = applyRunWorktree(repoP.dir, repoP.config, plan);
|
|
21491
|
+
if (!path) throw new PeriscopeError("the worktree could not be cut");
|
|
21492
|
+
return { path, branch: plan.branch ?? "" };
|
|
21493
|
+
},
|
|
21494
|
+
prepare: async (path) => {
|
|
21495
|
+
for (const f of repoP.config.worktrees.copy) {
|
|
21496
|
+
const src = join38(repoP.dir, f);
|
|
21497
|
+
if (!existsSync35(src)) continue;
|
|
21498
|
+
mkdirSync22(dirOf(join38(path, f)), { recursive: true });
|
|
21499
|
+
copyFileSync2(src, join38(path, f));
|
|
21500
|
+
}
|
|
21501
|
+
const setup = repoP.config.hooks.setup;
|
|
21502
|
+
return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
|
|
21503
|
+
if (l.trim()) say("setup", l.trim());
|
|
21504
|
+
} }) : null;
|
|
21505
|
+
},
|
|
21506
|
+
runHook: async (name, cwd, env) => {
|
|
21507
|
+
const script = repoP.config.hooks[name];
|
|
21508
|
+
return script ? runScript(script, { cwd, env, shell: shellP, timeoutMs: name === "preview" ? 18e4 : void 0 }) : null;
|
|
21509
|
+
},
|
|
21510
|
+
createRow: (fields) => trackerP.createPeriscope(fields),
|
|
21511
|
+
comment: (ticketId, body) => trackerP.postComment(ticketId, body, memberId),
|
|
21512
|
+
teardown: async (path, branch) => {
|
|
21513
|
+
const n = Number(path.match(/(\d+)\/?$/)?.[1]);
|
|
21514
|
+
if (Number.isFinite(n)) await killGently(pidsInWorktree(path, n));
|
|
21515
|
+
gitOk(repoP.dir, ["worktree", "remove", "--force", path]);
|
|
21516
|
+
gitOk(repoP.dir, ["branch", "-D", branch]);
|
|
21517
|
+
},
|
|
21518
|
+
closeDraft: async (ticketId, note) => {
|
|
21519
|
+
const closed = cP.statuses.resolved.includes("closed_obsolete") ? "closed_obsolete" : cP.statuses.wontFix;
|
|
21520
|
+
await trackerP.updateTicket(ticketId, { [cP.columns.status]: closed, [cP.columns.resolutionNote]: note });
|
|
21521
|
+
},
|
|
21522
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
21523
|
+
host: a.host ?? "localhost",
|
|
21524
|
+
say
|
|
21525
|
+
};
|
|
21526
|
+
if (dryRun) {
|
|
21527
|
+
await periscopePreflight(deps, a.repo);
|
|
21528
|
+
say("dry-run", `would file "Periscope: ${a.topic ?? "<topic>"}" as ${cP.statuses.draft} for ${pairId}, cut a worktree for ${a.repo}, run setup${a.isolate ? ", isolate" : ""}, ports, handoff and preview on ${deps.host}, and record a Periscopes row (expires after ${caps.idleMinutes} minutes idle)`);
|
|
21529
|
+
break;
|
|
21530
|
+
}
|
|
21531
|
+
const res = await runPeriscopeUp(deps, a);
|
|
21532
|
+
if (jsonP) process.stdout.write(`${JSON.stringify({ event: "done", ...res })}
|
|
21533
|
+
`);
|
|
21534
|
+
else process.stdout.write(`${res.ticket}
|
|
21535
|
+
${res.worktree}
|
|
21536
|
+
${res.url}
|
|
21537
|
+
`);
|
|
21538
|
+
} catch (e) {
|
|
21539
|
+
if (e instanceof PeriscopeError || e instanceof ConfigError) {
|
|
21540
|
+
process.stderr.write(`crew periscope: ${e.message}
|
|
21541
|
+
`);
|
|
21542
|
+
process.exit(1);
|
|
21543
|
+
}
|
|
21544
|
+
throw e;
|
|
21545
|
+
}
|
|
21546
|
+
break;
|
|
21547
|
+
}
|
|
21259
21548
|
case "unassign": {
|
|
21260
21549
|
emit.enter("select");
|
|
21261
21550
|
const rawUnassign = positional[2] ?? positional[1];
|
package/package.json
CHANGED