@tablation/crew 0.87.1 → 0.89.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 +553 -167
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4225,8 +4225,8 @@ var CHECKOUT_LOCK_STALE_MS = 5 * 60 * 1e3;
|
|
|
4225
4225
|
function checkoutLockPath(dir) {
|
|
4226
4226
|
return `${dir}.sync.lock`;
|
|
4227
4227
|
}
|
|
4228
|
-
function sleepSync(
|
|
4229
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0,
|
|
4228
|
+
function sleepSync(ms3) {
|
|
4229
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms3);
|
|
4230
4230
|
}
|
|
4231
4231
|
function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
4232
4232
|
const lockPath = checkoutLockPath(dir);
|
|
@@ -6980,6 +6980,25 @@ var Tracker = class {
|
|
|
6980
6980
|
});
|
|
6981
6981
|
return rows.length;
|
|
6982
6982
|
}
|
|
6983
|
+
/** This ship's `live` Periscopes rows (EPIC-077); empty without the table or a Ships row. */
|
|
6984
|
+
async liveShipPeriscopes() {
|
|
6985
|
+
const modelId = this.route.resolved?.periscopesModelId;
|
|
6986
|
+
const { shipId } = await this.authorshipProbe();
|
|
6987
|
+
if (!modelId || !shipId) return [];
|
|
6988
|
+
return this.client.records.list(modelId, {
|
|
6989
|
+
filters: encodeFilters([
|
|
6990
|
+
{ columnName: "ship_id", operator: "EQ", value: shipId },
|
|
6991
|
+
{ columnName: "state", operator: "EQ", value: "live" }
|
|
6992
|
+
]),
|
|
6993
|
+
limit: 200
|
|
6994
|
+
});
|
|
6995
|
+
}
|
|
6996
|
+
/** Patches one Periscopes row (EPIC-077). */
|
|
6997
|
+
async updatePeriscope(rowId, fields) {
|
|
6998
|
+
const modelId = this.route.resolved?.periscopesModelId;
|
|
6999
|
+
if (!modelId) throw new Error("this workspace has no Periscopes table");
|
|
7000
|
+
await this.client.records.update(modelId, rowId, fields);
|
|
7001
|
+
}
|
|
6983
7002
|
/** Records a Periscopes row for this ship (EPIC-077); `ship_id` is stamped here, never by the caller. */
|
|
6984
7003
|
async createPeriscope(fields) {
|
|
6985
7004
|
const modelId = this.route.resolved?.periscopesModelId;
|
|
@@ -7008,12 +7027,6 @@ var Tracker = class {
|
|
|
7008
7027
|
}
|
|
7009
7028
|
return out.sort((a, b) => String(b.row.started_at ?? "").localeCompare(String(a.row.started_at ?? "")));
|
|
7010
7029
|
}
|
|
7011
|
-
/** Updates one Periscopes row (EPIC-077). */
|
|
7012
|
-
async updatePeriscope(id, patch2) {
|
|
7013
|
-
const modelId = this.route.resolved?.periscopesModelId;
|
|
7014
|
-
if (!modelId) throw new Error("this workspace has no Periscopes table");
|
|
7015
|
-
await this.client.records.update(modelId, id, patch2);
|
|
7016
|
-
}
|
|
7017
7030
|
/**
|
|
7018
7031
|
* The authorship columns this workspace is missing, as `table.column` names
|
|
7019
7032
|
* for `crew doctor` to report (CREW-1371). Empty on a fully updated install.
|
|
@@ -9096,10 +9109,10 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
|
|
|
9096
9109
|
});
|
|
9097
9110
|
} catch {
|
|
9098
9111
|
}
|
|
9099
|
-
const
|
|
9112
|
+
const ms3 = Date.now() - Date.parse(startedAt);
|
|
9100
9113
|
emit.emit(end.interrupted ? "agent run interrupted" : `agent run finished (${pending ? "hand-off pending" : `exit ${end.code ?? "unknown"}`})`, {
|
|
9101
9114
|
data: {
|
|
9102
|
-
ms:
|
|
9115
|
+
ms: ms3,
|
|
9103
9116
|
code: end.code,
|
|
9104
9117
|
...pending ? { handoff: "pending" } : {},
|
|
9105
9118
|
...end.interrupted ? { interrupted: end.interrupted } : {},
|
|
@@ -9166,7 +9179,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
|
|
|
9166
9179
|
nextAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9167
9180
|
});
|
|
9168
9181
|
emit.warn(`could not report agent log (${e.message}); queued to retry`, { step: "agent" });
|
|
9169
|
-
return { ms:
|
|
9182
|
+
return { ms: ms3, ticket: attributedTicket, handoffPending: !!pending };
|
|
9170
9183
|
} catch {
|
|
9171
9184
|
}
|
|
9172
9185
|
}
|
|
@@ -9182,7 +9195,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved)
|
|
|
9182
9195
|
emit.warn(`could not report agent log: ${e.message}`, { step: "agent" });
|
|
9183
9196
|
}
|
|
9184
9197
|
}
|
|
9185
|
-
return { ms:
|
|
9198
|
+
return { ms: ms3, ticket: attributedTicket, handoffPending: !!pending };
|
|
9186
9199
|
}
|
|
9187
9200
|
function spawnAgent(plan, emit, hintMoved) {
|
|
9188
9201
|
const adapter = adapterFor(plan.harness ?? LEGACY_TOOLCHAIN_HARNESS);
|
|
@@ -9237,8 +9250,8 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
9237
9250
|
const meta = { pid: child.pid ?? null };
|
|
9238
9251
|
obs.onClaim = () => writeRunMeta(id, startedAt, obs.liveTicket, meta);
|
|
9239
9252
|
if (child.pid !== void 0) writeRunMeta(id, startedAt, obs.liveTicket, meta);
|
|
9240
|
-
const
|
|
9241
|
-
const timer = setInterval(() => obs.feed(
|
|
9253
|
+
const tail3 = new FileTail(plan.streamPath);
|
|
9254
|
+
const timer = setInterval(() => obs.feed(tail3.poll()), TAIL_POLL_MS);
|
|
9242
9255
|
let spawnFailed = false;
|
|
9243
9256
|
child.on("error", (err) => {
|
|
9244
9257
|
spawnFailed = true;
|
|
@@ -9247,12 +9260,12 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
9247
9260
|
});
|
|
9248
9261
|
child.on("close", async (code) => {
|
|
9249
9262
|
clearInterval(timer);
|
|
9250
|
-
obs.feed(
|
|
9263
|
+
obs.feed(tail3.poll());
|
|
9251
9264
|
obs.end();
|
|
9252
9265
|
await endSink(eventsSink);
|
|
9253
9266
|
if (spawnFailed) return;
|
|
9254
|
-
const { ms:
|
|
9255
|
-
resolve6({ code: code ?? 1, ms:
|
|
9267
|
+
const { ms: ms3, handoffPending } = await finishRun(id, obs, emit, startedAt, { code: code ?? 1 }, plan.agentLog?.apiKey, hintMoved);
|
|
9268
|
+
resolve6({ code: code ?? 1, ms: ms3, ...handoffPending ? { handoffPending } : {} });
|
|
9256
9269
|
});
|
|
9257
9270
|
child.stdin?.on("error", (err) => {
|
|
9258
9271
|
emit.warn(`agent closed stdin before the prompt was delivered: ${err.message}`, { step: "agent" });
|
|
@@ -9266,24 +9279,24 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
|
|
|
9266
9279
|
const id = { ...sc, streamPath };
|
|
9267
9280
|
const eventsSink = openSink(sc.eventsPath);
|
|
9268
9281
|
const obs = new RunObserver(id, adapter, eventsSink, emit.cycle);
|
|
9269
|
-
const
|
|
9282
|
+
const tail3 = new FileTail(streamPath);
|
|
9270
9283
|
obs.muted = true;
|
|
9271
|
-
obs.feed(
|
|
9284
|
+
obs.feed(tail3.poll());
|
|
9272
9285
|
obs.muted = false;
|
|
9273
9286
|
const adopted = isAlive(sc);
|
|
9274
9287
|
if (adopted) {
|
|
9275
9288
|
o.onAdopted?.();
|
|
9276
9289
|
const pollMs = o.pollMs ?? TAIL_POLL_MS;
|
|
9277
9290
|
while (isAlive(sc)) {
|
|
9278
|
-
obs.feed(
|
|
9291
|
+
obs.feed(tail3.poll());
|
|
9279
9292
|
await new Promise((r) => setTimeout(r, pollMs));
|
|
9280
9293
|
}
|
|
9281
9294
|
}
|
|
9282
|
-
obs.feed(
|
|
9295
|
+
obs.feed(tail3.poll());
|
|
9283
9296
|
obs.end();
|
|
9284
9297
|
await endSink(eventsSink);
|
|
9285
9298
|
const interrupted = obs.sawResult ? void 0 : adopted ? "no-result" : "daemon-exit";
|
|
9286
|
-
const { ms:
|
|
9299
|
+
const { ms: ms3, ticket } = await finishRun(
|
|
9287
9300
|
id,
|
|
9288
9301
|
obs,
|
|
9289
9302
|
emit,
|
|
@@ -9291,7 +9304,7 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
|
|
|
9291
9304
|
{ code: null, ...interrupted ? { interrupted } : {} },
|
|
9292
9305
|
o.agentLogKey
|
|
9293
9306
|
);
|
|
9294
|
-
return { adopted, ...interrupted ? { interrupted } : {}, ms:
|
|
9307
|
+
return { adopted, ...interrupted ? { interrupted } : {}, ms: ms3, ticket };
|
|
9295
9308
|
}
|
|
9296
9309
|
|
|
9297
9310
|
// src/seat-keys.ts
|
|
@@ -9790,7 +9803,7 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
9790
9803
|
}
|
|
9791
9804
|
|
|
9792
9805
|
// src/cli.ts
|
|
9793
|
-
import { resolve as resolve5, dirname as dirname20, join as
|
|
9806
|
+
import { resolve as resolve5, dirname as dirname20, join as join41, basename as basename12 } from "path";
|
|
9794
9807
|
import { fileURLToPath } from "url";
|
|
9795
9808
|
import { hostname as hostname2, homedir as homedir10 } from "os";
|
|
9796
9809
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
@@ -9829,7 +9842,9 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
9829
9842
|
"workspace-id",
|
|
9830
9843
|
"write",
|
|
9831
9844
|
"topic",
|
|
9832
|
-
"assignee"
|
|
9845
|
+
"assignee",
|
|
9846
|
+
"issue",
|
|
9847
|
+
"resume"
|
|
9833
9848
|
]);
|
|
9834
9849
|
function positionalArgs(argv2) {
|
|
9835
9850
|
const out = [];
|
|
@@ -13169,7 +13184,7 @@ async function isolateAndRetry(o, name, output, merged, preMerge, decision, fres
|
|
|
13169
13184
|
}
|
|
13170
13185
|
if (culprits.length === 0 && truncated) {
|
|
13171
13186
|
let lo = 2, hi = applied.length;
|
|
13172
|
-
let
|
|
13187
|
+
let tail3 = lastLines(output, HOOK_FAILURE_TAIL_LINES);
|
|
13173
13188
|
while (lo < hi) {
|
|
13174
13189
|
const mid = Math.floor((lo + hi) / 2);
|
|
13175
13190
|
gitOk(o.cwd, ["reset", "--hard", preMerge]);
|
|
@@ -13184,12 +13199,12 @@ async function isolateAndRetry(o, name, output, merged, preMerge, decision, fres
|
|
|
13184
13199
|
const r = clean ? await hook(o, name) : null;
|
|
13185
13200
|
if (r && r.code !== 0) {
|
|
13186
13201
|
hi = mid;
|
|
13187
|
-
|
|
13202
|
+
tail3 = lastLines(r.output, HOOK_FAILURE_TAIL_LINES);
|
|
13188
13203
|
} else lo = mid + 1;
|
|
13189
13204
|
}
|
|
13190
13205
|
const owner = applied[hi - 1];
|
|
13191
13206
|
o.emit.warn(`${owner.branch} is the first branch whose prefix fails the ${name} hook (bisected over ${applied.length})`, { ticket: owner.ticket.issue_id });
|
|
13192
|
-
culprits.push({ c: owner, how: "pair", sibling: applied[hi - 2], tail:
|
|
13207
|
+
culprits.push({ c: owner, how: "pair", sibling: applied[hi - 2], tail: tail3 });
|
|
13193
13208
|
}
|
|
13194
13209
|
}
|
|
13195
13210
|
}
|
|
@@ -14301,8 +14316,8 @@ async function decideFleet(o) {
|
|
|
14301
14316
|
}
|
|
14302
14317
|
var LOUD_STREAK = 5;
|
|
14303
14318
|
function since(iso, now = Date.now()) {
|
|
14304
|
-
const
|
|
14305
|
-
const mins = Math.round(
|
|
14319
|
+
const ms3 = Math.max(0, now - Date.parse(iso));
|
|
14320
|
+
const mins = Math.round(ms3 / 6e4);
|
|
14306
14321
|
if (mins < 60) return `${mins}m`;
|
|
14307
14322
|
const hours = Math.round(mins / 60);
|
|
14308
14323
|
if (hours < 48) return `${hours}h`;
|
|
@@ -16197,6 +16212,7 @@ function planWorktreeSweep(target, tickets, contract) {
|
|
|
16197
16212
|
for (const w of worktrees(target.dir).slice(1)) {
|
|
16198
16213
|
const ticket = ticketForWorktree(w, byNumber, contract);
|
|
16199
16214
|
if (!ticket) continue;
|
|
16215
|
+
if (ticket.status === contract.statuses.draft) continue;
|
|
16200
16216
|
const isVerified = ticket.status === verified;
|
|
16201
16217
|
if (!isVerified && !terminal.has(ticket.status)) continue;
|
|
16202
16218
|
actions.push({
|
|
@@ -16209,6 +16225,15 @@ function planWorktreeSweep(target, tickets, contract) {
|
|
|
16209
16225
|
}
|
|
16210
16226
|
return actions;
|
|
16211
16227
|
}
|
|
16228
|
+
function draftWorktrees(target, tickets, contract) {
|
|
16229
|
+
const byNumber = new Map(tickets.map((t) => [t.issue_id.replace(/^\D+/, ""), t]));
|
|
16230
|
+
const out = [];
|
|
16231
|
+
for (const w of worktrees(target.dir).slice(1)) {
|
|
16232
|
+
const ticket = ticketForWorktree(w, byNumber, contract);
|
|
16233
|
+
if (ticket && ticket.status === contract.statuses.draft) out.push({ ticket, path: w.path });
|
|
16234
|
+
}
|
|
16235
|
+
return out;
|
|
16236
|
+
}
|
|
16212
16237
|
var DIR_NUMBER = /(?:^|-)(\d+)$/;
|
|
16213
16238
|
var DIR_NUMBER_SUFFIXED = /(?:^|-)(\d+)(?:-[a-z0-9]{1,8})?$/;
|
|
16214
16239
|
var BRANCH_STEM = /^(?:.*\/)?([^/]+?-\d+)(?:-.*)?$/;
|
|
@@ -16508,24 +16533,55 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
16508
16533
|
}
|
|
16509
16534
|
|
|
16510
16535
|
// src/cli.ts
|
|
16511
|
-
import { realpathSync as realpathSync3, readFileSync as
|
|
16536
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync33, existsSync as existsSync37, mkdirSync as mkdirSync24, writeFileSync as writeFileSync25, rmSync as rmSync17, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync11, readdirSync as readdirSync17 } from "fs";
|
|
16512
16537
|
import { createInterface } from "readline/promises";
|
|
16513
16538
|
|
|
16514
16539
|
// src/periscope-cmd.ts
|
|
16540
|
+
import { existsSync as existsSync32, mkdirSync as mkdirSync19, readFileSync as readFileSync28, rmSync as rmSync15, writeFileSync as writeFileSync21 } from "fs";
|
|
16541
|
+
import { join as join35 } from "path";
|
|
16515
16542
|
var PeriscopeError = class extends Error {
|
|
16516
16543
|
};
|
|
16544
|
+
var priorDir = (stateDir) => join35(stateDir, "periscope-priors");
|
|
16545
|
+
var priorPath = (stateDir, ticketRecordId) => join35(priorDir(stateDir), `${ticketRecordId}.json`);
|
|
16546
|
+
function removePeriscopePrior(stateDir, ticketRecordId) {
|
|
16547
|
+
rmSync15(priorPath(stateDir, ticketRecordId), { force: true });
|
|
16548
|
+
}
|
|
16549
|
+
function writePeriscopePrior(stateDir, ticketRecordId, prior) {
|
|
16550
|
+
try {
|
|
16551
|
+
mkdirSync19(priorDir(stateDir), { recursive: true });
|
|
16552
|
+
writeFileSync21(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
|
|
16553
|
+
} catch {
|
|
16554
|
+
}
|
|
16555
|
+
}
|
|
16556
|
+
function readPeriscopePrior(stateDir, ticketRecordId) {
|
|
16557
|
+
try {
|
|
16558
|
+
const p = priorPath(stateDir, ticketRecordId);
|
|
16559
|
+
return existsSync32(p) ? JSON.parse(readFileSync28(p, "utf8")) : void 0;
|
|
16560
|
+
} catch {
|
|
16561
|
+
return void 0;
|
|
16562
|
+
}
|
|
16563
|
+
}
|
|
16517
16564
|
var DEFAULT_MAX_PERISCOPES2 = 3;
|
|
16518
16565
|
var DEFAULT_PERISCOPE_IDLE_MINUTES2 = 120;
|
|
16519
16566
|
function parsePeriscopeUpArgs(positional2, flags) {
|
|
16567
|
+
const usage2 = "usage: crew periscope up [route] REPO [--topic TEXT | --issue NNN] [--isolate] [--host HOST] [--assignee CREW_ROW_ID] [--json]";
|
|
16520
16568
|
const rest = positional2.slice(2);
|
|
16521
|
-
if (rest.length === 0 || rest.length > 2)
|
|
16522
|
-
throw new PeriscopeError("usage: crew periscope up [route] REPO [--topic TEXT] [--isolate] [--host HOST] [--assignee CREW_ROW_ID] [--json]");
|
|
16523
|
-
}
|
|
16569
|
+
if (rest.length === 0 || rest.length > 2) throw new PeriscopeError(usage2);
|
|
16524
16570
|
const topic = flags.topic?.trim();
|
|
16571
|
+
let issue;
|
|
16572
|
+
if (flags.issue !== void 0) {
|
|
16573
|
+
if (topic) throw new PeriscopeError(`--issue and --topic cannot be combined: --issue periscopes a ticket that already exists
|
|
16574
|
+
${usage2}`);
|
|
16575
|
+
const m = /^(?:[A-Za-z]+-)?(\d+)$/.exec(flags.issue.trim());
|
|
16576
|
+
if (!m) throw new PeriscopeError(`"${flags.issue}" is not a ticket number
|
|
16577
|
+
${usage2}`);
|
|
16578
|
+
issue = String(Number.parseInt(m[1], 10));
|
|
16579
|
+
}
|
|
16525
16580
|
return {
|
|
16526
16581
|
...rest.length === 2 ? { route: rest[0] } : {},
|
|
16527
16582
|
repo: rest[rest.length - 1],
|
|
16528
16583
|
...topic ? { topic } : {},
|
|
16584
|
+
...issue ? { issue } : {},
|
|
16529
16585
|
isolate: flags.isolate,
|
|
16530
16586
|
...flags.host ? { host: flags.host } : {},
|
|
16531
16587
|
...flags.assignee ? { assignee: flags.assignee } : {},
|
|
@@ -16550,6 +16606,17 @@ function periscopeCaps(l) {
|
|
|
16550
16606
|
idleMinutes: l.periscopeIdleMinutes ?? DEFAULT_PERISCOPE_IDLE_MINUTES2
|
|
16551
16607
|
};
|
|
16552
16608
|
}
|
|
16609
|
+
async function existingRefusal(e, t) {
|
|
16610
|
+
if (t.closed) return `${t.tag} is ${t.status}; a closed ticket cannot be periscoped`;
|
|
16611
|
+
if (e.repoId && t.repoId !== e.repoId) return `${t.tag} belongs to a different repository than ${e.repoName}`;
|
|
16612
|
+
if (t.assignee && t.assignee !== e.pairId) {
|
|
16613
|
+
const agent = await e.agentHolder(t.assignee);
|
|
16614
|
+
if (agent) return `${t.tag} is held by ${agent}`;
|
|
16615
|
+
}
|
|
16616
|
+
if (t.heldByShipId && e.shipId && t.heldByShipId !== e.shipId) return `${t.tag} is held by another ship (${t.heldByShipId})`;
|
|
16617
|
+
if (e.hasLiveRun(t.id)) return `${t.tag} has a run in flight on this ship`;
|
|
16618
|
+
return void 0;
|
|
16619
|
+
}
|
|
16553
16620
|
var tail = (s, n = 20) => s.trimEnd().split("\n").slice(-n).join("\n");
|
|
16554
16621
|
function progressComment(url, handoff) {
|
|
16555
16622
|
return [
|
|
@@ -16571,17 +16638,36 @@ async function periscopePreflight(d, repo) {
|
|
|
16571
16638
|
}
|
|
16572
16639
|
async function runPeriscopeUp(d, args) {
|
|
16573
16640
|
await periscopePreflight(d, args.repo);
|
|
16574
|
-
const
|
|
16575
|
-
|
|
16576
|
-
|
|
16577
|
-
if (
|
|
16578
|
-
|
|
16579
|
-
|
|
16641
|
+
const ex = d.existing;
|
|
16642
|
+
let draft;
|
|
16643
|
+
let prior;
|
|
16644
|
+
if (args.issue) {
|
|
16645
|
+
if (!ex) throw new PeriscopeError("--issue is not supported here");
|
|
16646
|
+
prior = await ex.load(args.issue);
|
|
16647
|
+
if (!prior) throw new PeriscopeError(`no ticket numbered ${args.issue} on this route`);
|
|
16648
|
+
const refused = await existingRefusal(ex, prior);
|
|
16649
|
+
if (refused) throw new PeriscopeError(refused);
|
|
16650
|
+
const unsafe = d.baseProblem();
|
|
16651
|
+
if (unsafe) throw new PeriscopeError(`DO NOT CUT A WORKTREE FROM THIS BASE \u2014 ${unsafe}`);
|
|
16652
|
+
await ex.hold(prior.id);
|
|
16653
|
+
ex.remember(prior.id, { assignee: prior.assignee, status: prior.status });
|
|
16654
|
+
draft = prior;
|
|
16655
|
+
d.say("held", `holding ${prior.tag} (status ${prior.status} stays)`, { ticket: prior.tag, ticketRecordId: prior.id });
|
|
16656
|
+
} else {
|
|
16657
|
+
const topic = args.topic ?? (await d.askTopic())?.trim();
|
|
16658
|
+
if (!topic) throw new PeriscopeError("a topic is required: pass --topic TEXT");
|
|
16659
|
+
const unsafe = d.baseProblem();
|
|
16660
|
+
if (unsafe) throw new PeriscopeError(`DO NOT CUT A WORKTREE FROM THIS BASE \u2014 ${unsafe}`);
|
|
16661
|
+
draft = await d.fileDraft(`Periscope: ${topic}`);
|
|
16662
|
+
d.say("filed", `filed draft ${draft.tag}`, { ticket: draft.tag, ticketRecordId: draft.id });
|
|
16663
|
+
}
|
|
16580
16664
|
let wt;
|
|
16581
16665
|
const abandon = async (why) => {
|
|
16582
16666
|
if (wt) await d.teardown(wt.path, wt.branch).catch(() => {
|
|
16583
16667
|
});
|
|
16584
|
-
await d.
|
|
16668
|
+
if (prior) await d.existing.restore(prior.id, prior.assignee).catch(() => {
|
|
16669
|
+
});
|
|
16670
|
+
else await d.closeDraft(draft.id, `Periscope abandoned: ${why.split("\n")[0]}`).catch(() => {
|
|
16585
16671
|
});
|
|
16586
16672
|
throw new PeriscopeError(why);
|
|
16587
16673
|
};
|
|
@@ -16658,7 +16744,15 @@ ${tail(preview.output)}`);
|
|
|
16658
16744
|
await d.comment(draft.id, progressComment(url, handoff?.output)).catch((e) => {
|
|
16659
16745
|
d.say("warning", `progress comment not posted: ${e.message}`);
|
|
16660
16746
|
});
|
|
16661
|
-
return {
|
|
16747
|
+
return {
|
|
16748
|
+
ticket: draft.tag,
|
|
16749
|
+
ticketRecordId: draft.id,
|
|
16750
|
+
worktree: wt.path,
|
|
16751
|
+
branch: wt.branch,
|
|
16752
|
+
url,
|
|
16753
|
+
periscopeRecordId: row.id,
|
|
16754
|
+
...prior ? { existing: true, priorStatus: prior.status } : {}
|
|
16755
|
+
};
|
|
16662
16756
|
}
|
|
16663
16757
|
function parsePeriscopeDownArgs(positional2, flags) {
|
|
16664
16758
|
const usage2 = "usage: crew periscope down [route] NNN --keep|--discard [--allow-empty] [--json]";
|
|
@@ -16707,6 +16801,21 @@ async function runPeriscopeDown(d, args) {
|
|
|
16707
16801
|
d.say("unchanged", `${done.ticket} is already ${done.state}; nothing to do`);
|
|
16708
16802
|
return { ticket: done.ticket, mode: args.mode, outcome: "unchanged", state: done.state };
|
|
16709
16803
|
}
|
|
16804
|
+
const existing = (await d.readTicket(row.ticketRecordId)).isDraft === false;
|
|
16805
|
+
const prior = existing ? d.priorHold?.(row.ticketRecordId) : void 0;
|
|
16806
|
+
if (existing && args.mode === "discard") {
|
|
16807
|
+
const killed2 = await d.stopServer(row.worktree, row.number);
|
|
16808
|
+
d.say("server", killed2 ? `stopped ${killed2} process(es)` : "no server was listening");
|
|
16809
|
+
const onRemote = d.remoteHasBranch?.(row.worktree, row.branch) ?? true;
|
|
16810
|
+
d.removeWorktree(row.worktree, row.branch, !onRemote);
|
|
16811
|
+
d.say("worktree", onRemote ? `removed ${row.worktree}; ${row.branch} stays, it is on the remote` : `removed ${row.worktree} and branch ${row.branch}`);
|
|
16812
|
+
await d.updateTicket(row.ticketRecordId, { status: "released", fillProjectAndRepo: false });
|
|
16813
|
+
d.say("ticket", `${row.ticket} hold cleared; status unchanged${prior ? ` (assignee before the periscope: ${prior.assignee ?? "none"})` : ""}`);
|
|
16814
|
+
await d.setRowState(row.id, "discarded");
|
|
16815
|
+
d.say("row", "row is discarded");
|
|
16816
|
+
forgetLocal(d, row.ticketRecordId);
|
|
16817
|
+
return { ticket: row.ticket, mode: "discard", outcome: "done", state: "discarded", existing: true, ...prior ? { status: prior.status } : {} };
|
|
16818
|
+
}
|
|
16710
16819
|
if (args.mode === "discard") {
|
|
16711
16820
|
const killed2 = await d.stopServer(row.worktree, row.number);
|
|
16712
16821
|
d.say("server", killed2 ? `stopped ${killed2} process(es)` : "no server was listening");
|
|
@@ -16738,12 +16847,17 @@ async function runPeriscopeDown(d, args) {
|
|
|
16738
16847
|
const description = t.description.includes(PROTOTYPE_HEADING) ? t.description : `${t.description.trimEnd()}
|
|
16739
16848
|
|
|
16740
16849
|
${section}`;
|
|
16741
|
-
|
|
16742
|
-
description,
|
|
16743
|
-
|
|
16744
|
-
|
|
16745
|
-
|
|
16746
|
-
|
|
16850
|
+
if (existing) {
|
|
16851
|
+
await d.updateTicket(row.ticketRecordId, { description, fillProjectAndRepo: false, status: "released" });
|
|
16852
|
+
d.say("ticket", `${row.ticket} has a Prototype section and no assignee; status unchanged${prior ? ` (assignee before the periscope: ${prior.assignee ?? "none"})` : ""}`);
|
|
16853
|
+
} else {
|
|
16854
|
+
await d.updateTicket(row.ticketRecordId, {
|
|
16855
|
+
description,
|
|
16856
|
+
fillProjectAndRepo: !t.hasProject || !t.hasRepo,
|
|
16857
|
+
status: "new"
|
|
16858
|
+
});
|
|
16859
|
+
d.say("ticket", `${row.ticket} is new and unassigned with a Prototype section`);
|
|
16860
|
+
}
|
|
16747
16861
|
await d.postPushed(row.ticketRecordId, pushed.sha, pushed.remote, row.branch);
|
|
16748
16862
|
const killed = await d.stopServer(row.worktree, row.number);
|
|
16749
16863
|
d.say("server", killed ? `stopped ${killed} process(es)` : "no server was listening");
|
|
@@ -16752,7 +16866,7 @@ ${section}`;
|
|
|
16752
16866
|
await d.setRowState(row.id, "kept");
|
|
16753
16867
|
d.say("row", "row is kept");
|
|
16754
16868
|
forgetLocal(d, row.ticketRecordId);
|
|
16755
|
-
return { ticket: row.ticket, mode: "keep", outcome: "done", state: "kept", sha: pushed.sha };
|
|
16869
|
+
return { ticket: row.ticket, mode: "keep", outcome: "done", state: "kept", sha: pushed.sha, ...existing ? { existing: true, ...prior ? { status: prior.status } : {} } : {} };
|
|
16756
16870
|
}
|
|
16757
16871
|
function periscopeListEntries(rows, now, isListening) {
|
|
16758
16872
|
return rows.filter((r) => r.state === "live").map((r) => {
|
|
@@ -16773,25 +16887,157 @@ function formatPeriscopeList(entries) {
|
|
|
16773
16887
|
].join(" ")).join("\n");
|
|
16774
16888
|
}
|
|
16775
16889
|
|
|
16890
|
+
// src/periscope-resume.ts
|
|
16891
|
+
var tail2 = (s, n = 20) => s.trimEnd().split("\n").slice(-n).join("\n");
|
|
16892
|
+
async function runPeriscopeResume(d, number) {
|
|
16893
|
+
const found = await d.find(number);
|
|
16894
|
+
if (!found) throw new PeriscopeError(`no live periscope for ISSUE-${number} on this ship \u2014 open one with \`crew periscope up\``);
|
|
16895
|
+
const { row, tag } = found;
|
|
16896
|
+
if (!d.worktreeExists(row.worktree)) {
|
|
16897
|
+
throw new PeriscopeError(`the worktree of ${tag} is gone (${row.worktree}); there is nothing to resume`);
|
|
16898
|
+
}
|
|
16899
|
+
const env = { CREW_TICKET: tag };
|
|
16900
|
+
if (row.branch) env.CREW_BRANCH = row.branch;
|
|
16901
|
+
const ports = await d.runHook("ports", row.worktree, env);
|
|
16902
|
+
if (ports) Object.assign(env, parseEnvLines(ports.output));
|
|
16903
|
+
if (row.isolated) {
|
|
16904
|
+
const iso = await d.runHook("isolate", row.worktree, env);
|
|
16905
|
+
if (iso && iso.code !== 0) throw new PeriscopeError(`isolate failed (exit ${iso.code})
|
|
16906
|
+
${tail2(iso.output)}`);
|
|
16907
|
+
if (iso) Object.assign(env, parseEnvLines(iso.output));
|
|
16908
|
+
}
|
|
16909
|
+
const preview = await d.runHook("preview", row.worktree, { ...env, CREW_PREVIEW_HOST: row.host || "localhost" });
|
|
16910
|
+
if (!preview || preview.code !== 0) {
|
|
16911
|
+
throw new PeriscopeError(`preview exited ${preview?.code ?? "without running"}
|
|
16912
|
+
${tail2(preview?.output ?? "")}`);
|
|
16913
|
+
}
|
|
16914
|
+
const url = previewUrlFrom(preview.output);
|
|
16915
|
+
if (!url) throw new PeriscopeError(`preview printed no URL as its last line
|
|
16916
|
+
${tail2(preview.output)}`);
|
|
16917
|
+
const now = d.now();
|
|
16918
|
+
await d.update(row.id, {
|
|
16919
|
+
url,
|
|
16920
|
+
last_activity_at: now.toISOString(),
|
|
16921
|
+
expires_at: new Date(now.getTime() + d.caps.idleMinutes * 6e4).toISOString()
|
|
16922
|
+
});
|
|
16923
|
+
d.clearItem(tag);
|
|
16924
|
+
d.say("resumed", `preview at ${url}`, { url });
|
|
16925
|
+
return { ticket: tag, worktree: row.worktree, url, periscopeRecordId: row.id };
|
|
16926
|
+
}
|
|
16927
|
+
|
|
16928
|
+
// src/periscope-idle.ts
|
|
16929
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
16930
|
+
import { statSync as statSync9 } from "fs";
|
|
16931
|
+
import { join as join36 } from "path";
|
|
16932
|
+
var MAX_SCANNED_FILES = 2e4;
|
|
16933
|
+
function newestWorktreeMtime(root) {
|
|
16934
|
+
let listing;
|
|
16935
|
+
try {
|
|
16936
|
+
listing = execFileSync6("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
16937
|
+
cwd: root,
|
|
16938
|
+
encoding: "utf8",
|
|
16939
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
16940
|
+
maxBuffer: 64 * 1024 * 1024
|
|
16941
|
+
});
|
|
16942
|
+
} catch {
|
|
16943
|
+
return null;
|
|
16944
|
+
}
|
|
16945
|
+
let newest = null;
|
|
16946
|
+
let scanned = 0;
|
|
16947
|
+
for (const rel of listing.split("\0")) {
|
|
16948
|
+
if (!rel) continue;
|
|
16949
|
+
if (++scanned > MAX_SCANNED_FILES) break;
|
|
16950
|
+
try {
|
|
16951
|
+
const m = statSync9(join36(root, rel)).mtimeMs;
|
|
16952
|
+
if (newest === null || m > newest) newest = m;
|
|
16953
|
+
} catch {
|
|
16954
|
+
}
|
|
16955
|
+
}
|
|
16956
|
+
return newest;
|
|
16957
|
+
}
|
|
16958
|
+
var ms2 = (iso) => {
|
|
16959
|
+
const t = iso ? Date.parse(iso) : NaN;
|
|
16960
|
+
return Number.isFinite(t) ? t : 0;
|
|
16961
|
+
};
|
|
16962
|
+
function decideIdle(row, mtimeMs, nowMs, idleMinutes) {
|
|
16963
|
+
const idleMs = idleMinutes * 6e4;
|
|
16964
|
+
const recordedActivity = ms2(row.last_activity_at);
|
|
16965
|
+
const activity = Math.max(recordedActivity, mtimeMs ?? 0);
|
|
16966
|
+
const expiresAt = ms2(row.expires_at);
|
|
16967
|
+
if (activity > 0 && activity + idleMs > expiresAt) {
|
|
16968
|
+
return {
|
|
16969
|
+
action: "refresh",
|
|
16970
|
+
lastActivityAt: new Date(activity).toISOString(),
|
|
16971
|
+
expiresAt: new Date(activity + idleMs).toISOString()
|
|
16972
|
+
};
|
|
16973
|
+
}
|
|
16974
|
+
if (expiresAt > 0 && nowMs >= expiresAt) return { action: "expire", idleMs: Math.max(0, nowMs - activity) };
|
|
16975
|
+
return { action: "none" };
|
|
16976
|
+
}
|
|
16977
|
+
var periscopeIdleKey = (ticket) => `periscope_idle:${ticket}`;
|
|
16978
|
+
var fmtIdle = (idleMs) => {
|
|
16979
|
+
const minutes = Math.max(1, Math.round(idleMs / 6e4));
|
|
16980
|
+
const h = Math.floor(minutes / 60);
|
|
16981
|
+
return h > 0 ? `${h}h ${minutes % 60}m` : `${minutes}m`;
|
|
16982
|
+
};
|
|
16983
|
+
function periscopeIdleItem(ticket, repo, number, idleMs, nowIso) {
|
|
16984
|
+
return {
|
|
16985
|
+
kind: "periscope_idle",
|
|
16986
|
+
key: periscopeIdleKey(ticket),
|
|
16987
|
+
since: nowIso,
|
|
16988
|
+
message: `Periscope ${ticket} (${repo}) has been idle for ${fmtIdle(idleMs)}; its server is stopped and the worktree is untouched. End it with \`crew periscope down ${number} --keep\` (push the work) or \`crew periscope down ${number} --discard\`, or bring the server back with \`crew periscope up --resume ${number}\`.`
|
|
16989
|
+
};
|
|
16990
|
+
}
|
|
16991
|
+
async function runPeriscopeIdlePass(d) {
|
|
16992
|
+
const result = { refreshed: 0, expired: 0 };
|
|
16993
|
+
const now = d.now();
|
|
16994
|
+
const liveKeys = /* @__PURE__ */ new Set();
|
|
16995
|
+
let lookupFailed = false;
|
|
16996
|
+
for (const row of await d.listLive()) {
|
|
16997
|
+
try {
|
|
16998
|
+
const t = await d.ticketOf(row);
|
|
16999
|
+
liveKeys.add(periscopeIdleKey(t.tag));
|
|
17000
|
+
const decision = decideIdle(row, d.mtimeOf(row.worktree), now.getTime(), d.idleMinutes);
|
|
17001
|
+
if (decision.action === "none") continue;
|
|
17002
|
+
if (decision.action === "refresh") {
|
|
17003
|
+
await d.update(row.id, { last_activity_at: decision.lastActivityAt, expires_at: decision.expiresAt });
|
|
17004
|
+
d.clear(periscopeIdleKey(t.tag));
|
|
17005
|
+
result.refreshed++;
|
|
17006
|
+
continue;
|
|
17007
|
+
}
|
|
17008
|
+
await d.stopServer(row.worktree, t.number);
|
|
17009
|
+
await d.raise(periscopeIdleItem(t.tag, t.repo, t.number, decision.idleMs, now.toISOString()));
|
|
17010
|
+
result.expired++;
|
|
17011
|
+
} catch (e) {
|
|
17012
|
+
lookupFailed = true;
|
|
17013
|
+
d.warn(`periscope idle check failed for ${row.worktree}: ${e.message}`);
|
|
17014
|
+
}
|
|
17015
|
+
}
|
|
17016
|
+
if (!lookupFailed) {
|
|
17017
|
+
for (const key of d.openKeys()) if (!liveKeys.has(key)) d.clear(key);
|
|
17018
|
+
}
|
|
17019
|
+
return result;
|
|
17020
|
+
}
|
|
17021
|
+
|
|
16776
17022
|
// src/cli.ts
|
|
16777
17023
|
import { dirname as dirOf2, resolve as resolvePath } from "path";
|
|
16778
17024
|
|
|
16779
17025
|
// src/ssh-keys.ts
|
|
16780
|
-
import { execFileSync as
|
|
16781
|
-
import { existsSync as
|
|
16782
|
-
import { dirname as dirname19, join as
|
|
17026
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
17027
|
+
import { existsSync as existsSync33, mkdirSync as mkdirSync20, readFileSync as readFileSync29, chmodSync } from "fs";
|
|
17028
|
+
import { dirname as dirname19, join as join37 } from "path";
|
|
16783
17029
|
var SshKeyError = class extends Error {
|
|
16784
17030
|
};
|
|
16785
17031
|
function shipSshKeyPathFor(stateDir) {
|
|
16786
|
-
return
|
|
17032
|
+
return join37(stateDir, "ssh", "id_ed25519");
|
|
16787
17033
|
}
|
|
16788
17034
|
function ensureShipSshKeypair(stateDir) {
|
|
16789
17035
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
16790
17036
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
16791
|
-
if (!
|
|
16792
|
-
|
|
17037
|
+
if (!existsSync33(privateKeyPath)) {
|
|
17038
|
+
mkdirSync20(dirname19(privateKeyPath), { recursive: true });
|
|
16793
17039
|
try {
|
|
16794
|
-
|
|
17040
|
+
execFileSync7("ssh-keygen", [
|
|
16795
17041
|
"-t",
|
|
16796
17042
|
"ed25519",
|
|
16797
17043
|
"-N",
|
|
@@ -16809,15 +17055,15 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
16809
17055
|
}
|
|
16810
17056
|
chmodSync(privateKeyPath, 384);
|
|
16811
17057
|
}
|
|
16812
|
-
if (!
|
|
17058
|
+
if (!existsSync33(publicKeyPath)) {
|
|
16813
17059
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
16814
17060
|
}
|
|
16815
|
-
const publicKey =
|
|
17061
|
+
const publicKey = readFileSync29(publicKeyPath, "utf8").trim();
|
|
16816
17062
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
16817
17063
|
}
|
|
16818
17064
|
function sshKeygenAvailable() {
|
|
16819
17065
|
try {
|
|
16820
|
-
|
|
17066
|
+
execFileSync7("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
16821
17067
|
return true;
|
|
16822
17068
|
} catch (e) {
|
|
16823
17069
|
return e.code !== "ENOENT";
|
|
@@ -16825,11 +17071,11 @@ function sshKeygenAvailable() {
|
|
|
16825
17071
|
}
|
|
16826
17072
|
|
|
16827
17073
|
// src/passenger-containers.ts
|
|
16828
|
-
import { execFileSync as
|
|
17074
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
16829
17075
|
import { createHash as createHash6 } from "crypto";
|
|
16830
17076
|
var realExec = (cmd, args, opts) => {
|
|
16831
17077
|
try {
|
|
16832
|
-
return
|
|
17078
|
+
return execFileSync8(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: opts?.timeoutMs });
|
|
16833
17079
|
} catch (e) {
|
|
16834
17080
|
const err = e;
|
|
16835
17081
|
const why = [err.stderr, err.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
|
|
@@ -17029,9 +17275,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
17029
17275
|
|
|
17030
17276
|
// src/tunnel.ts
|
|
17031
17277
|
import { spawn as spawn5 } from "child_process";
|
|
17032
|
-
import { existsSync as
|
|
17278
|
+
import { existsSync as existsSync34, mkdirSync as mkdirSync21, readFileSync as readFileSync30, writeFileSync as writeFileSync22, unlinkSync as unlinkSync3, readdirSync as readdirSync14 } from "fs";
|
|
17033
17279
|
import { randomInt } from "crypto";
|
|
17034
|
-
import { join as
|
|
17280
|
+
import { join as join38 } from "path";
|
|
17035
17281
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
17036
17282
|
var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
17037
17283
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -17098,21 +17344,21 @@ function startTunnel(opts) {
|
|
|
17098
17344
|
};
|
|
17099
17345
|
}
|
|
17100
17346
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
17101
|
-
return
|
|
17347
|
+
return join38(stateDir, "passengers", `${workspaceId}.json`);
|
|
17102
17348
|
}
|
|
17103
17349
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
17104
17350
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
17105
|
-
if (!
|
|
17351
|
+
if (!existsSync34(path)) return void 0;
|
|
17106
17352
|
try {
|
|
17107
|
-
return JSON.parse(
|
|
17353
|
+
return JSON.parse(readFileSync30(path, "utf8"));
|
|
17108
17354
|
} catch {
|
|
17109
17355
|
return void 0;
|
|
17110
17356
|
}
|
|
17111
17357
|
}
|
|
17112
17358
|
function writePersistedTunnel(stateDir, state) {
|
|
17113
17359
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
17114
|
-
|
|
17115
|
-
|
|
17360
|
+
mkdirSync21(join38(stateDir, "passengers"), { recursive: true });
|
|
17361
|
+
writeFileSync22(path, `${JSON.stringify(state, null, 2)}
|
|
17116
17362
|
`);
|
|
17117
17363
|
}
|
|
17118
17364
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -17209,8 +17455,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
17209
17455
|
}
|
|
17210
17456
|
}
|
|
17211
17457
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
17212
|
-
const dir =
|
|
17213
|
-
if (!
|
|
17458
|
+
const dir = join38(stateDir, "passengers");
|
|
17459
|
+
if (!existsSync34(dir)) return [];
|
|
17214
17460
|
return readdirSync14(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
17215
17461
|
}
|
|
17216
17462
|
|
|
@@ -17331,8 +17577,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
17331
17577
|
import { createServer as createHttpServer } from "http";
|
|
17332
17578
|
import { spawn as spawn6 } from "child_process";
|
|
17333
17579
|
import { randomBytes } from "crypto";
|
|
17334
|
-
import { existsSync as
|
|
17335
|
-
import { join as
|
|
17580
|
+
import { existsSync as existsSync35, mkdirSync as mkdirSync22, readFileSync as readFileSync31, writeFileSync as writeFileSync23, unlinkSync as unlinkSync4, readdirSync as readdirSync15 } from "fs";
|
|
17581
|
+
import { join as join39 } from "path";
|
|
17336
17582
|
|
|
17337
17583
|
// src/secret-compare.ts
|
|
17338
17584
|
import { timingSafeEqual } from "crypto";
|
|
@@ -17421,21 +17667,21 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
17421
17667
|
return child;
|
|
17422
17668
|
};
|
|
17423
17669
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
17424
|
-
return
|
|
17670
|
+
return join39(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
17425
17671
|
}
|
|
17426
17672
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
17427
17673
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
17428
|
-
if (!
|
|
17674
|
+
if (!existsSync35(path)) return void 0;
|
|
17429
17675
|
try {
|
|
17430
|
-
return JSON.parse(
|
|
17676
|
+
return JSON.parse(readFileSync31(path, "utf8"));
|
|
17431
17677
|
} catch {
|
|
17432
17678
|
return void 0;
|
|
17433
17679
|
}
|
|
17434
17680
|
}
|
|
17435
17681
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
17436
17682
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
17437
|
-
|
|
17438
|
-
|
|
17683
|
+
mkdirSync22(join39(stateDir, "passengers"), { recursive: true });
|
|
17684
|
+
writeFileSync23(path, `${JSON.stringify(state, null, 2)}
|
|
17439
17685
|
`);
|
|
17440
17686
|
}
|
|
17441
17687
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -17445,8 +17691,8 @@ function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
|
17445
17691
|
} catch {
|
|
17446
17692
|
}
|
|
17447
17693
|
}
|
|
17448
|
-
function sleepSync2(
|
|
17449
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0,
|
|
17694
|
+
function sleepSync2(ms3) {
|
|
17695
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms3);
|
|
17450
17696
|
}
|
|
17451
17697
|
function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
17452
17698
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -17455,8 +17701,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
17455
17701
|
}
|
|
17456
17702
|
}
|
|
17457
17703
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
17458
|
-
const dir =
|
|
17459
|
-
if (!
|
|
17704
|
+
const dir = join39(stateDir, "passengers");
|
|
17705
|
+
if (!existsSync35(dir)) return [];
|
|
17460
17706
|
const suffix = "-sync.json";
|
|
17461
17707
|
return readdirSync15(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
17462
17708
|
}
|
|
@@ -17544,12 +17790,12 @@ function describeCause(err) {
|
|
|
17544
17790
|
}
|
|
17545
17791
|
|
|
17546
17792
|
// src/daemon.ts
|
|
17547
|
-
import { existsSync as
|
|
17548
|
-
import { join as
|
|
17793
|
+
import { existsSync as existsSync36, mkdirSync as mkdirSync23, readFileSync as readFileSync32, readdirSync as readdirSync16, rmSync as rmSync16, statSync as statSync10, writeFileSync as writeFileSync24 } from "fs";
|
|
17794
|
+
import { join as join40 } from "path";
|
|
17549
17795
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
17550
17796
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
17551
17797
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
17552
|
-
var defaultSleep = (
|
|
17798
|
+
var defaultSleep = (ms3) => new Promise((resolve6) => setTimeout(resolve6, ms3));
|
|
17553
17799
|
async function runDaemonLoop(o) {
|
|
17554
17800
|
const floor = o.floorMs ?? DEFAULT_FLOOR_MS;
|
|
17555
17801
|
const ceiling = o.ceilingMs ?? DEFAULT_CEILING_MS;
|
|
@@ -17608,7 +17854,7 @@ async function runDaemonLoop(o) {
|
|
|
17608
17854
|
function latestMtime(path) {
|
|
17609
17855
|
let stat;
|
|
17610
17856
|
try {
|
|
17611
|
-
stat =
|
|
17857
|
+
stat = statSync10(path);
|
|
17612
17858
|
} catch {
|
|
17613
17859
|
return 0;
|
|
17614
17860
|
}
|
|
@@ -17616,7 +17862,7 @@ function latestMtime(path) {
|
|
|
17616
17862
|
if (!stat.isDirectory()) return 0;
|
|
17617
17863
|
let latest = stat.mtimeMs;
|
|
17618
17864
|
for (const entry of readdirSync16(path, { withFileTypes: true })) {
|
|
17619
|
-
latest = Math.max(latest, latestMtime(
|
|
17865
|
+
latest = Math.max(latest, latestMtime(join40(path, entry.name)));
|
|
17620
17866
|
}
|
|
17621
17867
|
return latest;
|
|
17622
17868
|
}
|
|
@@ -17625,23 +17871,23 @@ function computeTreeSignature(paths) {
|
|
|
17625
17871
|
}
|
|
17626
17872
|
function makeStaleChecker(paths, opts = {}) {
|
|
17627
17873
|
const initial = computeTreeSignature(paths);
|
|
17628
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
17874
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync36(opts.reloadSentinel);
|
|
17629
17875
|
}
|
|
17630
17876
|
function reloadSentinelFor(stateDir) {
|
|
17631
|
-
return
|
|
17877
|
+
return join40(stateDir, "reload-requested");
|
|
17632
17878
|
}
|
|
17633
17879
|
function daemonStampFor(stateDir) {
|
|
17634
|
-
return
|
|
17880
|
+
return join40(stateDir, "daemon.started");
|
|
17635
17881
|
}
|
|
17636
17882
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
17637
|
-
|
|
17638
|
-
|
|
17639
|
-
|
|
17883
|
+
mkdirSync23(stateDir, { recursive: true });
|
|
17884
|
+
rmSync16(reloadSentinelFor(stateDir), { force: true });
|
|
17885
|
+
writeFileSync24(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
17640
17886
|
`);
|
|
17641
17887
|
}
|
|
17642
17888
|
function readDaemonStarted(stateDir) {
|
|
17643
17889
|
try {
|
|
17644
|
-
const v = JSON.parse(
|
|
17890
|
+
const v = JSON.parse(readFileSync32(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
17645
17891
|
return typeof v === "number" ? v : null;
|
|
17646
17892
|
} catch {
|
|
17647
17893
|
return null;
|
|
@@ -17650,7 +17896,7 @@ function readDaemonStarted(stateDir) {
|
|
|
17650
17896
|
function configChangedAfter(files, startedAt) {
|
|
17651
17897
|
for (const f of files) {
|
|
17652
17898
|
try {
|
|
17653
|
-
if (
|
|
17899
|
+
if (statSync10(f).mtimeMs > startedAt) return f;
|
|
17654
17900
|
} catch {
|
|
17655
17901
|
}
|
|
17656
17902
|
}
|
|
@@ -17749,6 +17995,35 @@ async function runOnePassUnguarded(o) {
|
|
|
17749
17995
|
emit.warn(`could not sweep stalled tickets: ${e.message}`, { step: "sweep" });
|
|
17750
17996
|
}
|
|
17751
17997
|
}
|
|
17998
|
+
if (o.route.resolved?.periscopesModelId) {
|
|
17999
|
+
const tracker = new Tracker(o.route, o.ship);
|
|
18000
|
+
try {
|
|
18001
|
+
await runPeriscopeIdlePass({
|
|
18002
|
+
idleMinutes: periscopeCaps(o.ship).idleMinutes,
|
|
18003
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
18004
|
+
listLive: () => tracker.liveShipPeriscopes(),
|
|
18005
|
+
mtimeOf: newestWorktreeMtime,
|
|
18006
|
+
update: (id, fields) => tracker.updatePeriscope(id, fields),
|
|
18007
|
+
ticketOf: async (row) => {
|
|
18008
|
+
const t = await tracker.ticket(row.issue_id);
|
|
18009
|
+
return {
|
|
18010
|
+
tag: t.issue_tag ?? t.issue_id,
|
|
18011
|
+
number: t.issue_id.replace(/^\D+/, ""),
|
|
18012
|
+
repo: t.repo_id && o.route.resolved?.repoNames?.[t.repo_id] || "unknown repository"
|
|
18013
|
+
};
|
|
18014
|
+
},
|
|
18015
|
+
stopServer: (worktree, number) => killGently(pidsInWorktree(worktree, Number(number))),
|
|
18016
|
+
raise: async (item) => {
|
|
18017
|
+
await raiseShipAttention({ state: o.state, emit, route: o.route, ship: o.ship }, item);
|
|
18018
|
+
},
|
|
18019
|
+
clear: (key) => clearShipAttention(o.state, key),
|
|
18020
|
+
openKeys: () => openShipAttention(o.state).filter((i) => i.kind === "periscope_idle").map((i) => i.key),
|
|
18021
|
+
warn: (message) => emit.warn(message, { step: "sweep" })
|
|
18022
|
+
});
|
|
18023
|
+
} catch (e) {
|
|
18024
|
+
emit.warn(`could not check idle periscopes: ${e.message}`, { step: "sweep" });
|
|
18025
|
+
}
|
|
18026
|
+
}
|
|
17752
18027
|
const order = [...decision.selection.pending].sort(
|
|
17753
18028
|
(a, b) => (decision.selection.ranks[a] ?? Number.POSITIVE_INFINITY) - (decision.selection.ranks[b] ?? Number.POSITIVE_INFINITY)
|
|
17754
18029
|
);
|
|
@@ -18234,7 +18509,8 @@ function usage(problem, requested = false) {
|
|
|
18234
18509
|
crew reap [route] kill orphaned servers, drop worktrees for closed tickets
|
|
18235
18510
|
crew drop [route] NNN remove a merged ticket's worktree and branch
|
|
18236
18511
|
crew rotate-passenger-url [route] force a Host Passengers tunnel to reconnect with a fresh public URL
|
|
18237
|
-
crew periscope up [route] REPO --topic TEXT [--isolate] [--host H] [--assignee ID] [--dry-run] [--json]
|
|
18512
|
+
crew periscope up [route] REPO --topic TEXT|--issue NNN [--isolate] [--host H] [--assignee ID] [--dry-run] [--json]
|
|
18513
|
+
crew periscope up --resume NNN restart the preview an idle periscope's server was stopped for
|
|
18238
18514
|
file a draft ticket, cut and prepare its worktree, start the repo's preview hook and record a Periscopes row (alias: start)
|
|
18239
18515
|
crew periscope down [route] NNN --keep|--discard [--allow-empty] [--json]
|
|
18240
18516
|
end a periscope: commit and push its branch (ticket back to new), or throw it away (alias: end)
|
|
@@ -18513,7 +18789,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18513
18789
|
} else {
|
|
18514
18790
|
const toleratesNoRoutes = command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
|
|
18515
18791
|
const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
|
|
18516
|
-
if (configless && !
|
|
18792
|
+
if (configless && !existsSync37(findConfigFile(CREW_HOME))) {
|
|
18517
18793
|
cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
|
|
18518
18794
|
} else {
|
|
18519
18795
|
cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
|
|
@@ -18751,8 +19027,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
18751
19027
|
}
|
|
18752
19028
|
if (host === "linux" && hasSystemd) {
|
|
18753
19029
|
const label = labelFor(CREW_HOME, "run");
|
|
18754
|
-
const staleTimer =
|
|
18755
|
-
if (
|
|
19030
|
+
const staleTimer = join41(homedir10(), ".config", "systemd", "user", `${label}.timer`);
|
|
19031
|
+
if (existsSync37(staleTimer)) {
|
|
18756
19032
|
process.stdout.write(
|
|
18757
19033
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
18758
19034
|
`
|
|
@@ -18779,7 +19055,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18779
19055
|
const tracker = new Tracker(c, cfg.ship);
|
|
18780
19056
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
18781
19057
|
for (const r of await resolvedRepos(c)) {
|
|
18782
|
-
if (!
|
|
19058
|
+
if (!existsSync37(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
18783
19059
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
18784
19060
|
readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
18785
19061
|
}
|
|
@@ -18797,7 +19073,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18797
19073
|
}
|
|
18798
19074
|
async function doctorFix(o) {
|
|
18799
19075
|
const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
|
|
18800
|
-
const unitExists = (p) =>
|
|
19076
|
+
const unitExists = (p) => existsSync37(p);
|
|
18801
19077
|
const crontab = o.host === "linux" ? spawnSync7("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
18802
19078
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
18803
19079
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -18814,7 +19090,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18814
19090
|
for (const a of plan.actions) {
|
|
18815
19091
|
if (a.kind === "enable") {
|
|
18816
19092
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
18817
|
-
|
|
19093
|
+
writeFileSync25(cfg.configFile, planEnable(readFileSync33(cfg.configFile, "utf8"), a.route));
|
|
18818
19094
|
enabledSound.add(a.route);
|
|
18819
19095
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
18820
19096
|
} else {
|
|
@@ -19918,7 +20194,7 @@ ${cu.tail.trimEnd()}
|
|
|
19918
20194
|
process.exit(1);
|
|
19919
20195
|
}
|
|
19920
20196
|
requireArmed("run the daemon loop");
|
|
19921
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
20197
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join41(CREW_HOME, "src"), join41(CREW_HOME, "dist"), join41(CREW_HOME, "bin")];
|
|
19922
20198
|
const configPaths = daemonConfigPaths(cfg);
|
|
19923
20199
|
watchPaths.push(...configPaths);
|
|
19924
20200
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -20147,10 +20423,10 @@ ${cu.tail.trimEnd()}
|
|
|
20147
20423
|
`);
|
|
20148
20424
|
if (!dryRun) {
|
|
20149
20425
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
20150
|
-
const raw =
|
|
20426
|
+
const raw = existsSync37(resolvedPath) ? JSON.parse(readFileSync33(resolvedPath, "utf8")) : {};
|
|
20151
20427
|
raw.agentSkills = result.agentSkills;
|
|
20152
|
-
|
|
20153
|
-
|
|
20428
|
+
mkdirSync24(dirOf2(resolvedPath), { recursive: true });
|
|
20429
|
+
writeFileSync25(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
20154
20430
|
`);
|
|
20155
20431
|
}
|
|
20156
20432
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -20173,7 +20449,7 @@ ${cu.tail.trimEnd()}
|
|
|
20173
20449
|
throw e;
|
|
20174
20450
|
}
|
|
20175
20451
|
const oldName = cfg.ship.name;
|
|
20176
|
-
|
|
20452
|
+
writeFileSync25(cfg.configFile, setShipNameInYaml(readFileSync33(cfg.configFile, "utf8"), newName));
|
|
20177
20453
|
cfg.ship.name = newName;
|
|
20178
20454
|
let renameMachineId;
|
|
20179
20455
|
try {
|
|
@@ -20220,7 +20496,7 @@ ${cu.tail.trimEnd()}
|
|
|
20220
20496
|
process.exit(2);
|
|
20221
20497
|
};
|
|
20222
20498
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
|
|
20223
|
-
const configText =
|
|
20499
|
+
const configText = readFileSync33(cfg.configFile, "utf8");
|
|
20224
20500
|
if (sub === "check") {
|
|
20225
20501
|
try {
|
|
20226
20502
|
const routeArg = value("route") ?? positional[2];
|
|
@@ -20251,8 +20527,8 @@ ${cu.tail.trimEnd()}
|
|
|
20251
20527
|
try {
|
|
20252
20528
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
20253
20529
|
const dir = repoDirOf(configText, dirname20(cfg.configFile), a.route, a.name);
|
|
20254
|
-
if (!
|
|
20255
|
-
const files = { exists: (r) =>
|
|
20530
|
+
if (!existsSync37(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
20531
|
+
const files = { exists: (r) => existsSync37(resolvePath(dir, r)), list: (r) => {
|
|
20256
20532
|
try {
|
|
20257
20533
|
return readdirSync17(resolvePath(dir, r));
|
|
20258
20534
|
} catch {
|
|
@@ -20260,7 +20536,7 @@ ${cu.tail.trimEnd()}
|
|
|
20260
20536
|
}
|
|
20261
20537
|
}, read: (r) => {
|
|
20262
20538
|
try {
|
|
20263
|
-
return
|
|
20539
|
+
return readFileSync33(resolvePath(dir, r), "utf8");
|
|
20264
20540
|
} catch {
|
|
20265
20541
|
return void 0;
|
|
20266
20542
|
}
|
|
@@ -20302,19 +20578,19 @@ ${cu.tail.trimEnd()}
|
|
|
20302
20578
|
let cleared;
|
|
20303
20579
|
if (clear2) {
|
|
20304
20580
|
const plan = clearRouteHooks(configText, a.route, a.name);
|
|
20305
|
-
if (plan.removed.length > 0)
|
|
20581
|
+
if (plan.removed.length > 0) writeFileSync25(cfg.configFile, plan.text);
|
|
20306
20582
|
cleared = { file: cfg.configFile, removed: plan.removed };
|
|
20307
20583
|
}
|
|
20308
20584
|
if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
20309
20585
|
if (write === "route") {
|
|
20310
20586
|
const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
|
|
20311
|
-
|
|
20587
|
+
writeFileSync25(cfg.configFile, plan.text);
|
|
20312
20588
|
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
|
|
20313
20589
|
} else if (write === "repo") {
|
|
20314
20590
|
const have = existingRepoConfig(files);
|
|
20315
20591
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
20316
20592
|
const file = resolvePath(dir, ".crew.yaml");
|
|
20317
|
-
|
|
20593
|
+
writeFileSync25(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
|
|
20318
20594
|
written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
|
|
20319
20595
|
}
|
|
20320
20596
|
if (flag("json")) {
|
|
@@ -20370,7 +20646,7 @@ ${cu.tail.trimEnd()}
|
|
|
20370
20646
|
if (sub === "list" && !routeName) usage2();
|
|
20371
20647
|
if (sub === "add") {
|
|
20372
20648
|
try {
|
|
20373
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
20649
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync37(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
|
|
20374
20650
|
routeName = a.route;
|
|
20375
20651
|
addPath = a.path;
|
|
20376
20652
|
addName = a.name;
|
|
@@ -20399,16 +20675,16 @@ ${cu.tail.trimEnd()}
|
|
|
20399
20675
|
}
|
|
20400
20676
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
20401
20677
|
let resolvedRemotes;
|
|
20402
|
-
if (
|
|
20678
|
+
if (existsSync37(resolvedFile)) {
|
|
20403
20679
|
try {
|
|
20404
|
-
resolvedRemotes = JSON.parse(
|
|
20680
|
+
resolvedRemotes = JSON.parse(readFileSync33(resolvedFile, "utf8"));
|
|
20405
20681
|
} catch {
|
|
20406
20682
|
}
|
|
20407
20683
|
}
|
|
20408
20684
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
20409
20685
|
const text = configText;
|
|
20410
20686
|
const base = dirname20(cfg.configFile);
|
|
20411
|
-
const isCheckout = (d) =>
|
|
20687
|
+
const isCheckout = (d) => existsSync37(`${d}/.git`);
|
|
20412
20688
|
try {
|
|
20413
20689
|
if (sub === "list") {
|
|
20414
20690
|
if (flag("json")) {
|
|
@@ -20418,11 +20694,11 @@ ${cu.tail.trimEnd()}
|
|
|
20418
20694
|
base,
|
|
20419
20695
|
resolved: resolvedRemotes,
|
|
20420
20696
|
originOf,
|
|
20421
|
-
exists: (d) =>
|
|
20697
|
+
exists: (d) => existsSync37(d),
|
|
20422
20698
|
isCheckout,
|
|
20423
20699
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
20424
20700
|
try {
|
|
20425
|
-
return Object.keys(parseRepoConfig(
|
|
20701
|
+
return Object.keys(parseRepoConfig(readFileSync33(resolvePath(d, f), "utf8"), f).hooks).length > 0;
|
|
20426
20702
|
} catch {
|
|
20427
20703
|
return false;
|
|
20428
20704
|
}
|
|
@@ -20439,7 +20715,7 @@ ${cu.tail.trimEnd()}
|
|
|
20439
20715
|
for (const r of repos) {
|
|
20440
20716
|
const origin2 = originOf(r.dir);
|
|
20441
20717
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
20442
|
-
const note = !
|
|
20718
|
+
const note = !existsSync37(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
20443
20719
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
20444
20720
|
`);
|
|
20445
20721
|
}
|
|
@@ -20451,9 +20727,9 @@ ${cu.tail.trimEnd()}
|
|
|
20451
20727
|
` : `${human}
|
|
20452
20728
|
`);
|
|
20453
20729
|
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
|
|
20454
|
-
if (
|
|
20730
|
+
if (existsSync37(plan2.target) && (existsSync37(`${plan2.target}/.git`) || readdirSync17(plan2.target).length > 0)) {
|
|
20455
20731
|
throw new ReposError(
|
|
20456
|
-
|
|
20732
|
+
existsSync37(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
|
|
20457
20733
|
);
|
|
20458
20734
|
}
|
|
20459
20735
|
if (dryRun) {
|
|
@@ -20471,14 +20747,14 @@ ${cu.tail.trimEnd()}
|
|
|
20471
20747
|
throw new ReposError(`${e.message}${authHint}
|
|
20472
20748
|
crew.yaml was not changed`);
|
|
20473
20749
|
}
|
|
20474
|
-
|
|
20750
|
+
writeFileSync25(cfg.configFile, plan2.text);
|
|
20475
20751
|
if (plan2.droppedDir) process.stderr.write(`crew repos: dropped dir: ${plan2.droppedDir} from route ${routeName} \u2014 it is not a checkout (the clone base); checkouts now live under repos:
|
|
20476
20752
|
`);
|
|
20477
20753
|
say("attached", { route: routeName, name: plan2.name, dir: plan2.target, mode: plan2.mode }, `cloned and attached ${plan2.target} to ${routeName} as ${plan2.mode === "dir" ? "dir" : `repos.${plan2.name}`} in ${cfg.configFile}`);
|
|
20478
20754
|
break;
|
|
20479
20755
|
}
|
|
20480
20756
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
|
|
20481
|
-
const top =
|
|
20757
|
+
const top = existsSync37(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
20482
20758
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
20483
20759
|
const origin = originOf(top);
|
|
20484
20760
|
const plan = planRepoAdd({
|
|
@@ -20530,15 +20806,15 @@ ${cu.tail.trimEnd()}
|
|
|
20530
20806
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
20531
20807
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
20532
20808
|
`);
|
|
20533
|
-
if (
|
|
20534
|
-
const raw = JSON.parse(
|
|
20809
|
+
if (existsSync37(resolvedFile)) {
|
|
20810
|
+
const raw = JSON.parse(readFileSync33(resolvedFile, "utf8"));
|
|
20535
20811
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
20536
20812
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
20537
|
-
|
|
20813
|
+
writeFileSync25(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
20538
20814
|
`);
|
|
20539
20815
|
}
|
|
20540
20816
|
}
|
|
20541
|
-
|
|
20817
|
+
writeFileSync25(cfg.configFile, plan.text);
|
|
20542
20818
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
20543
20819
|
`);
|
|
20544
20820
|
} catch (e) {
|
|
@@ -20905,7 +21181,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20905
21181
|
process.exit(2);
|
|
20906
21182
|
}
|
|
20907
21183
|
if (found.shipNameAdopted && !dryRun) {
|
|
20908
|
-
|
|
21184
|
+
writeFileSync25(cfg.configFile, setShipNameInYaml(readFileSync33(cfg.configFile, "utf8"), found.shipNameAdopted));
|
|
20909
21185
|
cfg.ship.name = found.shipNameAdopted;
|
|
20910
21186
|
}
|
|
20911
21187
|
if (found.shipRow) {
|
|
@@ -20948,10 +21224,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20948
21224
|
}
|
|
20949
21225
|
}
|
|
20950
21226
|
let previous;
|
|
20951
|
-
const hadPreviousFile =
|
|
21227
|
+
const hadPreviousFile = existsSync37(resolvedPath);
|
|
20952
21228
|
if (hadPreviousFile) {
|
|
20953
21229
|
try {
|
|
20954
|
-
previous = JSON.parse(
|
|
21230
|
+
previous = JSON.parse(readFileSync33(resolvedPath, "utf8"));
|
|
20955
21231
|
} catch {
|
|
20956
21232
|
}
|
|
20957
21233
|
}
|
|
@@ -21080,10 +21356,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21080
21356
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
21081
21357
|
`);
|
|
21082
21358
|
} else {
|
|
21083
|
-
|
|
21359
|
+
mkdirSync24(dirname20(resolvedPath), { recursive: true });
|
|
21084
21360
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
21085
21361
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
21086
|
-
|
|
21362
|
+
writeFileSync25(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
21087
21363
|
`);
|
|
21088
21364
|
renameSync3(tmpPath, resolvedPath);
|
|
21089
21365
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -21129,8 +21405,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21129
21405
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
21130
21406
|
`);
|
|
21131
21407
|
} else {
|
|
21132
|
-
|
|
21133
|
-
|
|
21408
|
+
mkdirSync24(dirname20(apiKeyFile), { recursive: true });
|
|
21409
|
+
writeFileSync25(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
21134
21410
|
`, { mode: 384 });
|
|
21135
21411
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
21136
21412
|
`);
|
|
@@ -21449,6 +21725,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21449
21725
|
await tracker.ticketsByNumbers(remoteBranchTicketNumbers(r.dir, r.config.branch.remote, r.config.branch.base), truncated)
|
|
21450
21726
|
).byRepo.get(r.name) ?? [];
|
|
21451
21727
|
const actions = planWorktreeSweep(r, wtTickets, tracker.contract);
|
|
21728
|
+
for (const k of draftWorktrees(r, wtTickets, tracker.contract)) {
|
|
21729
|
+
anything = true;
|
|
21730
|
+
emit.emit(`keeping ${k.path}: ${k.ticket.issue_tag ?? k.ticket.issue_id} is a draft (a periscope) \u2014 end it with \`crew periscope down\``, { step: "worktree" });
|
|
21731
|
+
}
|
|
21452
21732
|
const orphanDirs = orphanedWorktreeDirs(r, wtTickets, tracker.contract);
|
|
21453
21733
|
warnOrphanedWorktreeDirs(orphanDirs, emit, dryRun ? void 0 : { state, repo: r.name });
|
|
21454
21734
|
const stale = planRemoteBranchCleanup(r.dir, r.config.branch.remote, r.config.branch.base, brTickets, tracker.contract);
|
|
@@ -21514,6 +21794,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21514
21794
|
}
|
|
21515
21795
|
const { repo: target, found } = candidates[0];
|
|
21516
21796
|
const wt = found.path;
|
|
21797
|
+
try {
|
|
21798
|
+
const trD = new Tracker(route, cfg.ship);
|
|
21799
|
+
const tD = (await trD.ticketsByNumbers([n])).find((t) => t.issue_id.replace(/^\D+/, "") === String(num));
|
|
21800
|
+
if (tD && tD[trD.contract.columns.status] === trD.contract.statuses.draft) {
|
|
21801
|
+
process.stderr.write(`drop: ${tD.issue_tag ?? tD.issue_id} is a draft ticket \u2014 its worktree belongs to a periscope; end it with \`crew periscope down ${num} --keep\` or \`--discard\`
|
|
21802
|
+
`);
|
|
21803
|
+
process.exit(2);
|
|
21804
|
+
}
|
|
21805
|
+
} catch {
|
|
21806
|
+
}
|
|
21517
21807
|
const branch = found.branch ?? branchForIssue(target.dir, `ISSUE-${n}`, {
|
|
21518
21808
|
name: target.config.branch.name,
|
|
21519
21809
|
push: target.config.branch.push
|
|
@@ -21666,7 +21956,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21666
21956
|
case "periscope": {
|
|
21667
21957
|
const subP = positional[1];
|
|
21668
21958
|
if (subP !== "up" && subP !== "start" && subP !== "down" && subP !== "end" && subP !== "list") {
|
|
21669
|
-
process.stderr.write("crew periscope up [route] REPO --topic TEXT [--isolate] [--host H] [--assignee ID] [--dry-run] [--json]\n");
|
|
21959
|
+
process.stderr.write("crew periscope up [route] REPO --topic TEXT|--issue NNN [--isolate] [--host H] [--assignee ID] [--dry-run] [--json]\n");
|
|
21670
21960
|
process.stderr.write("crew periscope down [route] NNN --keep|--discard [--allow-empty] [--json]\n");
|
|
21671
21961
|
process.stderr.write("crew periscope list [route] [--json]\n");
|
|
21672
21962
|
process.exit(2);
|
|
@@ -21750,11 +22040,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21750
22040
|
},
|
|
21751
22041
|
readTicket: async (id) => {
|
|
21752
22042
|
const t = await trD.ticket(id);
|
|
21753
|
-
return { description: String(t.description ?? ""), hasProject: !!t[cD.columns.slice], hasRepo: !!t[cD.columns.repo] };
|
|
22043
|
+
return { description: String(t.description ?? ""), hasProject: !!t[cD.columns.slice], hasRepo: !!t[cD.columns.repo], isDraft: t.status === cD.statuses.draft };
|
|
21754
22044
|
},
|
|
22045
|
+
priorHold: (id) => readPeriscopePrior(cfg.ship.stateDir, id),
|
|
22046
|
+
remoteHasBranch: (_wt, branch) => remoteBranchExists(repoD.dir, remote, branch),
|
|
21755
22047
|
updateTicket: async (id, p) => {
|
|
21756
22048
|
const patch2 = {};
|
|
21757
|
-
if (p.status === "
|
|
22049
|
+
if (p.status === "released") {
|
|
22050
|
+
patch2[cD.columns.assignee] = null;
|
|
22051
|
+
if ((await trD.authorshipProbe()).issueHeld) patch2[cD.columns.heldBy] = null;
|
|
22052
|
+
if (p.description !== void 0) patch2.description = p.description;
|
|
22053
|
+
} else if (p.status === "new") {
|
|
21758
22054
|
patch2[cD.columns.status] = "new";
|
|
21759
22055
|
patch2[cD.columns.assignee] = null;
|
|
21760
22056
|
if (p.description !== void 0) patch2.description = p.description;
|
|
@@ -21779,7 +22075,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21779
22075
|
if (deleteBranch) gitOk(repoD.dir, ["branch", "-D", branch]);
|
|
21780
22076
|
},
|
|
21781
22077
|
setRowState: (id, state2) => trD.updatePeriscope(id, { state: state2 }),
|
|
21782
|
-
forgetLocal: (id) =>
|
|
22078
|
+
forgetLocal: (id) => {
|
|
22079
|
+
removePeriscopeSidecar(cfg.ship.stateDir, id);
|
|
22080
|
+
removePeriscopePrior(cfg.ship.stateDir, id);
|
|
22081
|
+
},
|
|
21783
22082
|
say
|
|
21784
22083
|
};
|
|
21785
22084
|
if (dryRun) {
|
|
@@ -21802,7 +22101,48 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21802
22101
|
break;
|
|
21803
22102
|
}
|
|
21804
22103
|
try {
|
|
21805
|
-
const
|
|
22104
|
+
const resumeRaw = value("resume");
|
|
22105
|
+
if (resumeRaw !== void 0) {
|
|
22106
|
+
const nR = parseTicketNumber(resumeRaw);
|
|
22107
|
+
if (!nR) throw new PeriscopeError("--resume needs a ticket number (326, ISSUE-326, or CREW-326)");
|
|
22108
|
+
let hit;
|
|
22109
|
+
for (const cand of cfg.routes.filter((r) => r.enabled && r.resolved?.periscopesModelId)) {
|
|
22110
|
+
const tr = new Tracker(cand, cfg.ship);
|
|
22111
|
+
for (const row of await tr.liveShipPeriscopes()) {
|
|
22112
|
+
const t = await tr.ticket(row.issue_id);
|
|
22113
|
+
if (t.issue_id.replace(/^\D+/, "") !== String(Number(nR))) continue;
|
|
22114
|
+
const repo = (await resolvedRepos(cand)).find((r) => r.id === t.repo_id);
|
|
22115
|
+
if (!repo) throw new PeriscopeError(`route ${cand.route} has no checkout for the repository of ${t.issue_tag ?? t.issue_id}`);
|
|
22116
|
+
hit = { route: cand, tracker: tr, row, tag: t.issue_tag ?? t.issue_id, repo };
|
|
22117
|
+
}
|
|
22118
|
+
}
|
|
22119
|
+
if (dryRun) {
|
|
22120
|
+
say("dry-run", hit ? `would rerun ports${hit.row.isolated ? ", isolate" : ""} and preview in ${hit.row.worktree} and refresh the row` : `no live periscope for ISSUE-${nR} on this ship`);
|
|
22121
|
+
break;
|
|
22122
|
+
}
|
|
22123
|
+
const shellR = hit && resolveShell(hit.repo.config.shell ?? cfg.ship.shell);
|
|
22124
|
+
const resumed = await runPeriscopeResume({
|
|
22125
|
+
find: async () => hit && { row: hit.row, tag: hit.tag },
|
|
22126
|
+
worktreeExists: (p) => existsSync37(p),
|
|
22127
|
+
caps: periscopeCaps(cfg.ship),
|
|
22128
|
+
runHook: async (name, cwd, env) => {
|
|
22129
|
+
const script = hit?.repo.config.hooks[name];
|
|
22130
|
+
return script ? runScript(script, { cwd, env, shell: shellR, timeoutMs: name === "preview" ? 18e4 : void 0 }) : null;
|
|
22131
|
+
},
|
|
22132
|
+
update: (id, fields) => hit.tracker.updatePeriscope(id, fields),
|
|
22133
|
+
clearItem: (tag) => clearShipAttention(state, periscopeIdleKey(tag)),
|
|
22134
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
22135
|
+
say
|
|
22136
|
+
}, String(Number(nR)));
|
|
22137
|
+
if (jsonP) process.stdout.write(`${JSON.stringify({ event: "done", ...resumed })}
|
|
22138
|
+
`);
|
|
22139
|
+
else process.stdout.write(`${resumed.ticket}
|
|
22140
|
+
${resumed.worktree}
|
|
22141
|
+
${resumed.url}
|
|
22142
|
+
`);
|
|
22143
|
+
break;
|
|
22144
|
+
}
|
|
22145
|
+
const a = parsePeriscopeUpArgs(positional, { topic: value("topic"), issue: value("issue"), host: value("host"), assignee: value("assignee"), isolate: flag("isolate"), json: jsonP });
|
|
21806
22146
|
const candidatesP = a.route ? [findRoute(cfg, a.route)] : cfg.routes.filter((r) => r.enabled);
|
|
21807
22147
|
const servedP = [];
|
|
21808
22148
|
for (const cand of candidatesP) {
|
|
@@ -21864,10 +22204,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21864
22204
|
},
|
|
21865
22205
|
prepare: async (path) => {
|
|
21866
22206
|
for (const f of repoP.config.worktrees.copy) {
|
|
21867
|
-
const src =
|
|
21868
|
-
if (!
|
|
21869
|
-
|
|
21870
|
-
copyFileSync2(src,
|
|
22207
|
+
const src = join41(repoP.dir, f);
|
|
22208
|
+
if (!existsSync37(src)) continue;
|
|
22209
|
+
mkdirSync24(dirOf2(join41(path, f)), { recursive: true });
|
|
22210
|
+
copyFileSync2(src, join41(path, f));
|
|
21871
22211
|
}
|
|
21872
22212
|
const setup = repoP.config.hooks.setup;
|
|
21873
22213
|
return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
|
|
@@ -21891,12 +22231,58 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21891
22231
|
const closed = cP.statuses.resolved.includes("closed_obsolete") ? "closed_obsolete" : cP.statuses.wontFix;
|
|
21892
22232
|
await trackerP.updateTicket(ticketId, { [cP.columns.status]: closed, [cP.columns.resolutionNote]: note });
|
|
21893
22233
|
},
|
|
22234
|
+
existing: {
|
|
22235
|
+
load: async (number) => {
|
|
22236
|
+
const t = (await trackerP.ticketsByNumbers([number])).find((x) => String(x[cP.columns.key]).replace(/^\D+/, "").replace(/^0+(?=\d)/, "") === number);
|
|
22237
|
+
if (!t) return void 0;
|
|
22238
|
+
return {
|
|
22239
|
+
id: t.id,
|
|
22240
|
+
tag: t.issue_tag ?? t.issue_id,
|
|
22241
|
+
issueId: t.issue_id,
|
|
22242
|
+
prefix: t.project_issue_prefix,
|
|
22243
|
+
status: t.status,
|
|
22244
|
+
closed: cP.statuses.resolved.includes(t.status) || t.status === cP.statuses.wontFix,
|
|
22245
|
+
repoId: t[cP.columns.repo] ?? null,
|
|
22246
|
+
assignee: t[cP.columns.assignee] ?? null,
|
|
22247
|
+
heldByShipId: t[cP.columns.heldBy] ?? null
|
|
22248
|
+
};
|
|
22249
|
+
},
|
|
22250
|
+
repoId: repoP.id ?? null,
|
|
22251
|
+
repoName: a.repo,
|
|
22252
|
+
pairId,
|
|
22253
|
+
shipId: (await trackerP.authorshipProbe()).shipId,
|
|
22254
|
+
agentHolder: async (assigneeId) => {
|
|
22255
|
+
const row = crewP.find((r) => r.id === assigneeId);
|
|
22256
|
+
return row?.agent_id ? row.name ?? assigneeId : void 0;
|
|
22257
|
+
},
|
|
22258
|
+
hasLiveRun: (ticketRecordId) => liveRuns(cfg.ship.stateDir).some((r) => r.ticketRecordId === ticketRecordId),
|
|
22259
|
+
hold: async (id) => {
|
|
22260
|
+
const auth = await trackerP.authorshipProbe();
|
|
22261
|
+
await trackerP.updateTicket(id, {
|
|
22262
|
+
[cP.columns.assignee]: pairId,
|
|
22263
|
+
...auth.issueHeld && auth.shipId ? { [cP.columns.heldBy]: auth.shipId } : {}
|
|
22264
|
+
});
|
|
22265
|
+
},
|
|
22266
|
+
restore: async (id, assignee) => {
|
|
22267
|
+
const auth = await trackerP.authorshipProbe();
|
|
22268
|
+
await trackerP.updateTicket(id, { [cP.columns.assignee]: assignee, ...auth.issueHeld ? { [cP.columns.heldBy]: null } : {} });
|
|
22269
|
+
},
|
|
22270
|
+
remember: (id, prior) => writePeriscopePrior(cfg.ship.stateDir, id, prior)
|
|
22271
|
+
},
|
|
21894
22272
|
now: () => /* @__PURE__ */ new Date(),
|
|
21895
22273
|
host: a.host ?? "localhost",
|
|
21896
22274
|
say
|
|
21897
22275
|
};
|
|
21898
22276
|
if (dryRun) {
|
|
21899
22277
|
await periscopePreflight(deps, a.repo);
|
|
22278
|
+
if (a.issue) {
|
|
22279
|
+
const t = await deps.existing.load(a.issue);
|
|
22280
|
+
if (!t) throw new PeriscopeError(`no ticket numbered ${a.issue} on this route`);
|
|
22281
|
+
const refused = await existingRefusal(deps.existing, t);
|
|
22282
|
+
if (refused) throw new PeriscopeError(refused);
|
|
22283
|
+
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`);
|
|
22284
|
+
break;
|
|
22285
|
+
}
|
|
21900
22286
|
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)`);
|
|
21901
22287
|
break;
|
|
21902
22288
|
}
|
|
@@ -22091,7 +22477,7 @@ ${res.url}
|
|
|
22091
22477
|
};
|
|
22092
22478
|
const droppedRoutes = [];
|
|
22093
22479
|
try {
|
|
22094
|
-
const rawCfg = parseYaml2(
|
|
22480
|
+
const rawCfg = parseYaml2(readFileSync33(cfg.configFile, "utf8"));
|
|
22095
22481
|
for (const r of rawCfg?.routes ?? []) {
|
|
22096
22482
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
22097
22483
|
try {
|
|
@@ -22133,8 +22519,8 @@ ${res.url}
|
|
|
22133
22519
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
22134
22520
|
break;
|
|
22135
22521
|
}
|
|
22136
|
-
|
|
22137
|
-
|
|
22522
|
+
mkdirSync24(cfg.ship.stateDir, { recursive: true });
|
|
22523
|
+
writeFileSync25(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
22138
22524
|
`);
|
|
22139
22525
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
22140
22526
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
@@ -22185,7 +22571,7 @@ ${res.url}
|
|
|
22185
22571
|
}
|
|
22186
22572
|
case "log": {
|
|
22187
22573
|
try {
|
|
22188
|
-
const text =
|
|
22574
|
+
const text = readFileSync33(cfg.ship.logFile, "utf8").split("\n");
|
|
22189
22575
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
22190
22576
|
`);
|
|
22191
22577
|
} catch {
|
|
@@ -22208,7 +22594,7 @@ ${res.url}
|
|
|
22208
22594
|
};
|
|
22209
22595
|
if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
|
|
22210
22596
|
const tracker = new Tracker(route, cfg.ship);
|
|
22211
|
-
const configText =
|
|
22597
|
+
const configText = readFileSync33(cfg.configFile, "utf8");
|
|
22212
22598
|
const cfgBase = dirname20(cfg.configFile);
|
|
22213
22599
|
const configured = configuredRepos(configText, route.route, cfgBase);
|
|
22214
22600
|
const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
|
|
@@ -22237,7 +22623,7 @@ ${res.url}
|
|
|
22237
22623
|
}
|
|
22238
22624
|
if (configured.some((r) => r.dir === dir)) {
|
|
22239
22625
|
try {
|
|
22240
|
-
|
|
22626
|
+
writeFileSync25(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
|
|
22241
22627
|
} catch (e) {
|
|
22242
22628
|
if (e instanceof ReposError) fail(e.message);
|
|
22243
22629
|
throw e;
|
|
@@ -22245,29 +22631,29 @@ ${res.url}
|
|
|
22245
22631
|
}
|
|
22246
22632
|
if (row) {
|
|
22247
22633
|
await tracker.deleteRepoRow(row.id);
|
|
22248
|
-
if (
|
|
22249
|
-
const raw = JSON.parse(
|
|
22634
|
+
if (existsSync37(resolvedFile)) {
|
|
22635
|
+
const raw = JSON.parse(readFileSync33(resolvedFile, "utf8"));
|
|
22250
22636
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
22251
22637
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
22252
|
-
|
|
22638
|
+
writeFileSync25(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
22253
22639
|
`);
|
|
22254
22640
|
}
|
|
22255
22641
|
}
|
|
22256
|
-
if (
|
|
22642
|
+
if (existsSync37(join41(dir, ".git")) && basename12(dir) === SAMPLE_REPO) rmSync17(dir, { recursive: true, force: true });
|
|
22257
22643
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
22258
22644
|
break;
|
|
22259
22645
|
}
|
|
22260
22646
|
const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
22261
|
-
const fresh = !
|
|
22262
|
-
if (
|
|
22647
|
+
const fresh = !existsSync37(join41(dir, ".git"));
|
|
22648
|
+
if (existsSync37(dir) && fresh && readdirSync17(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
22263
22649
|
if (dryRun) {
|
|
22264
22650
|
say("dry-run", { dir, newCheckout: fresh, repoRow: row?.id ?? null }, `would ${fresh ? "create" : "reuse"} ${dir}${row ? "" : ", create the Repos row"}, and file one ${tracker.contract.statuses.approved} ticket`);
|
|
22265
22651
|
break;
|
|
22266
22652
|
}
|
|
22267
22653
|
if (fresh) {
|
|
22268
|
-
|
|
22269
|
-
|
|
22270
|
-
|
|
22654
|
+
mkdirSync24(dir, { recursive: true });
|
|
22655
|
+
writeFileSync25(join41(dir, "README.md"), SAMPLE_README);
|
|
22656
|
+
writeFileSync25(join41(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
22271
22657
|
const g = (...a) => {
|
|
22272
22658
|
const r = spawnSync7("git", a, { cwd: dir, encoding: "utf8" });
|
|
22273
22659
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -22285,17 +22671,17 @@ ${res.url}
|
|
|
22285
22671
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
22286
22672
|
});
|
|
22287
22673
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
22288
|
-
if (
|
|
22289
|
-
const raw = JSON.parse(
|
|
22674
|
+
if (existsSync37(resolvedFile)) {
|
|
22675
|
+
const raw = JSON.parse(readFileSync33(resolvedFile, "utf8"));
|
|
22290
22676
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
22291
22677
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
22292
|
-
|
|
22678
|
+
writeFileSync25(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
22293
22679
|
`);
|
|
22294
22680
|
}
|
|
22295
22681
|
}
|
|
22296
22682
|
if (!configured.some((r) => r.dir === dir)) {
|
|
22297
22683
|
try {
|
|
22298
|
-
|
|
22684
|
+
writeFileSync25(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
|
|
22299
22685
|
} catch (e) {
|
|
22300
22686
|
if (e instanceof ReposError) fail(e.message);
|
|
22301
22687
|
throw e;
|
|
@@ -22630,7 +23016,7 @@ watermark: ${state.watermark()}
|
|
|
22630
23016
|
}
|
|
22631
23017
|
for (const r of repos) {
|
|
22632
23018
|
const remote = r.config.branch.remote;
|
|
22633
|
-
if (
|
|
23019
|
+
if (existsSync37(r.dir)) {
|
|
22634
23020
|
pushAuth.push({
|
|
22635
23021
|
route: c.route,
|
|
22636
23022
|
repo: r.name,
|
|
@@ -22707,7 +23093,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
22707
23093
|
}
|
|
22708
23094
|
try {
|
|
22709
23095
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
22710
|
-
const evSize =
|
|
23096
|
+
const evSize = existsSync37(evFile) ? statSync11(evFile).size : 0;
|
|
22711
23097
|
process.stdout.write(
|
|
22712
23098
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
22713
23099
|
` : "events log: ok\n"
|
|
@@ -22718,7 +23104,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
22718
23104
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
22719
23105
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
22720
23106
|
try {
|
|
22721
|
-
return
|
|
23107
|
+
return readFileSync33(p, "utf8");
|
|
22722
23108
|
} catch {
|
|
22723
23109
|
return null;
|
|
22724
23110
|
}
|
|
@@ -22734,7 +23120,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
22734
23120
|
const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
|
|
22735
23121
|
if (changed2) {
|
|
22736
23122
|
process.stdout.write(
|
|
22737
|
-
`daemon config: WARN ${changed2} changed at ${new Date(
|
|
23123
|
+
`daemon config: WARN ${changed2} changed at ${new Date(statSync11(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
|
|
22738
23124
|
`
|
|
22739
23125
|
);
|
|
22740
23126
|
}
|