@tablation/crew 0.87.1 → 0.88.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 +308 -67
- 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 join40, 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,8 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
9829
9842
|
"workspace-id",
|
|
9830
9843
|
"write",
|
|
9831
9844
|
"topic",
|
|
9832
|
-
"assignee"
|
|
9845
|
+
"assignee",
|
|
9846
|
+
"resume"
|
|
9833
9847
|
]);
|
|
9834
9848
|
function positionalArgs(argv2) {
|
|
9835
9849
|
const out = [];
|
|
@@ -13169,7 +13183,7 @@ async function isolateAndRetry(o, name, output, merged, preMerge, decision, fres
|
|
|
13169
13183
|
}
|
|
13170
13184
|
if (culprits.length === 0 && truncated) {
|
|
13171
13185
|
let lo = 2, hi = applied.length;
|
|
13172
|
-
let
|
|
13186
|
+
let tail3 = lastLines(output, HOOK_FAILURE_TAIL_LINES);
|
|
13173
13187
|
while (lo < hi) {
|
|
13174
13188
|
const mid = Math.floor((lo + hi) / 2);
|
|
13175
13189
|
gitOk(o.cwd, ["reset", "--hard", preMerge]);
|
|
@@ -13184,12 +13198,12 @@ async function isolateAndRetry(o, name, output, merged, preMerge, decision, fres
|
|
|
13184
13198
|
const r = clean ? await hook(o, name) : null;
|
|
13185
13199
|
if (r && r.code !== 0) {
|
|
13186
13200
|
hi = mid;
|
|
13187
|
-
|
|
13201
|
+
tail3 = lastLines(r.output, HOOK_FAILURE_TAIL_LINES);
|
|
13188
13202
|
} else lo = mid + 1;
|
|
13189
13203
|
}
|
|
13190
13204
|
const owner = applied[hi - 1];
|
|
13191
13205
|
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:
|
|
13206
|
+
culprits.push({ c: owner, how: "pair", sibling: applied[hi - 2], tail: tail3 });
|
|
13193
13207
|
}
|
|
13194
13208
|
}
|
|
13195
13209
|
}
|
|
@@ -14301,8 +14315,8 @@ async function decideFleet(o) {
|
|
|
14301
14315
|
}
|
|
14302
14316
|
var LOUD_STREAK = 5;
|
|
14303
14317
|
function since(iso, now = Date.now()) {
|
|
14304
|
-
const
|
|
14305
|
-
const mins = Math.round(
|
|
14318
|
+
const ms3 = Math.max(0, now - Date.parse(iso));
|
|
14319
|
+
const mins = Math.round(ms3 / 6e4);
|
|
14306
14320
|
if (mins < 60) return `${mins}m`;
|
|
14307
14321
|
const hours = Math.round(mins / 60);
|
|
14308
14322
|
if (hours < 48) return `${hours}h`;
|
|
@@ -16197,6 +16211,7 @@ function planWorktreeSweep(target, tickets, contract) {
|
|
|
16197
16211
|
for (const w of worktrees(target.dir).slice(1)) {
|
|
16198
16212
|
const ticket = ticketForWorktree(w, byNumber, contract);
|
|
16199
16213
|
if (!ticket) continue;
|
|
16214
|
+
if (ticket.status === contract.statuses.draft) continue;
|
|
16200
16215
|
const isVerified = ticket.status === verified;
|
|
16201
16216
|
if (!isVerified && !terminal.has(ticket.status)) continue;
|
|
16202
16217
|
actions.push({
|
|
@@ -16209,6 +16224,15 @@ function planWorktreeSweep(target, tickets, contract) {
|
|
|
16209
16224
|
}
|
|
16210
16225
|
return actions;
|
|
16211
16226
|
}
|
|
16227
|
+
function draftWorktrees(target, tickets, contract) {
|
|
16228
|
+
const byNumber = new Map(tickets.map((t) => [t.issue_id.replace(/^\D+/, ""), t]));
|
|
16229
|
+
const out = [];
|
|
16230
|
+
for (const w of worktrees(target.dir).slice(1)) {
|
|
16231
|
+
const ticket = ticketForWorktree(w, byNumber, contract);
|
|
16232
|
+
if (ticket && ticket.status === contract.statuses.draft) out.push({ ticket, path: w.path });
|
|
16233
|
+
}
|
|
16234
|
+
return out;
|
|
16235
|
+
}
|
|
16212
16236
|
var DIR_NUMBER = /(?:^|-)(\d+)$/;
|
|
16213
16237
|
var DIR_NUMBER_SUFFIXED = /(?:^|-)(\d+)(?:-[a-z0-9]{1,8})?$/;
|
|
16214
16238
|
var BRANCH_STEM = /^(?:.*\/)?([^/]+?-\d+)(?:-.*)?$/;
|
|
@@ -16508,7 +16532,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
16508
16532
|
}
|
|
16509
16533
|
|
|
16510
16534
|
// src/cli.ts
|
|
16511
|
-
import { realpathSync as realpathSync3, readFileSync as readFileSync32, existsSync as existsSync36, mkdirSync as mkdirSync23, writeFileSync as writeFileSync24, rmSync as rmSync16, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as
|
|
16535
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync32, existsSync as existsSync36, mkdirSync as mkdirSync23, writeFileSync as writeFileSync24, rmSync as rmSync16, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync11, readdirSync as readdirSync17 } from "fs";
|
|
16512
16536
|
import { createInterface } from "readline/promises";
|
|
16513
16537
|
|
|
16514
16538
|
// src/periscope-cmd.ts
|
|
@@ -16773,17 +16797,149 @@ function formatPeriscopeList(entries) {
|
|
|
16773
16797
|
].join(" ")).join("\n");
|
|
16774
16798
|
}
|
|
16775
16799
|
|
|
16800
|
+
// src/periscope-resume.ts
|
|
16801
|
+
var tail2 = (s, n = 20) => s.trimEnd().split("\n").slice(-n).join("\n");
|
|
16802
|
+
async function runPeriscopeResume(d, number) {
|
|
16803
|
+
const found = await d.find(number);
|
|
16804
|
+
if (!found) throw new PeriscopeError(`no live periscope for ISSUE-${number} on this ship \u2014 open one with \`crew periscope up\``);
|
|
16805
|
+
const { row, tag } = found;
|
|
16806
|
+
if (!d.worktreeExists(row.worktree)) {
|
|
16807
|
+
throw new PeriscopeError(`the worktree of ${tag} is gone (${row.worktree}); there is nothing to resume`);
|
|
16808
|
+
}
|
|
16809
|
+
const env = { CREW_TICKET: tag };
|
|
16810
|
+
if (row.branch) env.CREW_BRANCH = row.branch;
|
|
16811
|
+
const ports = await d.runHook("ports", row.worktree, env);
|
|
16812
|
+
if (ports) Object.assign(env, parseEnvLines(ports.output));
|
|
16813
|
+
if (row.isolated) {
|
|
16814
|
+
const iso = await d.runHook("isolate", row.worktree, env);
|
|
16815
|
+
if (iso && iso.code !== 0) throw new PeriscopeError(`isolate failed (exit ${iso.code})
|
|
16816
|
+
${tail2(iso.output)}`);
|
|
16817
|
+
if (iso) Object.assign(env, parseEnvLines(iso.output));
|
|
16818
|
+
}
|
|
16819
|
+
const preview = await d.runHook("preview", row.worktree, { ...env, CREW_PREVIEW_HOST: row.host || "localhost" });
|
|
16820
|
+
if (!preview || preview.code !== 0) {
|
|
16821
|
+
throw new PeriscopeError(`preview exited ${preview?.code ?? "without running"}
|
|
16822
|
+
${tail2(preview?.output ?? "")}`);
|
|
16823
|
+
}
|
|
16824
|
+
const url = previewUrlFrom(preview.output);
|
|
16825
|
+
if (!url) throw new PeriscopeError(`preview printed no URL as its last line
|
|
16826
|
+
${tail2(preview.output)}`);
|
|
16827
|
+
const now = d.now();
|
|
16828
|
+
await d.update(row.id, {
|
|
16829
|
+
url,
|
|
16830
|
+
last_activity_at: now.toISOString(),
|
|
16831
|
+
expires_at: new Date(now.getTime() + d.caps.idleMinutes * 6e4).toISOString()
|
|
16832
|
+
});
|
|
16833
|
+
d.clearItem(tag);
|
|
16834
|
+
d.say("resumed", `preview at ${url}`, { url });
|
|
16835
|
+
return { ticket: tag, worktree: row.worktree, url, periscopeRecordId: row.id };
|
|
16836
|
+
}
|
|
16837
|
+
|
|
16838
|
+
// src/periscope-idle.ts
|
|
16839
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
16840
|
+
import { statSync as statSync9 } from "fs";
|
|
16841
|
+
import { join as join35 } from "path";
|
|
16842
|
+
var MAX_SCANNED_FILES = 2e4;
|
|
16843
|
+
function newestWorktreeMtime(root) {
|
|
16844
|
+
let listing;
|
|
16845
|
+
try {
|
|
16846
|
+
listing = execFileSync6("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
16847
|
+
cwd: root,
|
|
16848
|
+
encoding: "utf8",
|
|
16849
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
16850
|
+
maxBuffer: 64 * 1024 * 1024
|
|
16851
|
+
});
|
|
16852
|
+
} catch {
|
|
16853
|
+
return null;
|
|
16854
|
+
}
|
|
16855
|
+
let newest = null;
|
|
16856
|
+
let scanned = 0;
|
|
16857
|
+
for (const rel of listing.split("\0")) {
|
|
16858
|
+
if (!rel) continue;
|
|
16859
|
+
if (++scanned > MAX_SCANNED_FILES) break;
|
|
16860
|
+
try {
|
|
16861
|
+
const m = statSync9(join35(root, rel)).mtimeMs;
|
|
16862
|
+
if (newest === null || m > newest) newest = m;
|
|
16863
|
+
} catch {
|
|
16864
|
+
}
|
|
16865
|
+
}
|
|
16866
|
+
return newest;
|
|
16867
|
+
}
|
|
16868
|
+
var ms2 = (iso) => {
|
|
16869
|
+
const t = iso ? Date.parse(iso) : NaN;
|
|
16870
|
+
return Number.isFinite(t) ? t : 0;
|
|
16871
|
+
};
|
|
16872
|
+
function decideIdle(row, mtimeMs, nowMs, idleMinutes) {
|
|
16873
|
+
const idleMs = idleMinutes * 6e4;
|
|
16874
|
+
const recordedActivity = ms2(row.last_activity_at);
|
|
16875
|
+
const activity = Math.max(recordedActivity, mtimeMs ?? 0);
|
|
16876
|
+
const expiresAt = ms2(row.expires_at);
|
|
16877
|
+
if (activity > 0 && activity + idleMs > expiresAt) {
|
|
16878
|
+
return {
|
|
16879
|
+
action: "refresh",
|
|
16880
|
+
lastActivityAt: new Date(activity).toISOString(),
|
|
16881
|
+
expiresAt: new Date(activity + idleMs).toISOString()
|
|
16882
|
+
};
|
|
16883
|
+
}
|
|
16884
|
+
if (expiresAt > 0 && nowMs >= expiresAt) return { action: "expire", idleMs: Math.max(0, nowMs - activity) };
|
|
16885
|
+
return { action: "none" };
|
|
16886
|
+
}
|
|
16887
|
+
var periscopeIdleKey = (ticket) => `periscope_idle:${ticket}`;
|
|
16888
|
+
var fmtIdle = (idleMs) => {
|
|
16889
|
+
const minutes = Math.max(1, Math.round(idleMs / 6e4));
|
|
16890
|
+
const h = Math.floor(minutes / 60);
|
|
16891
|
+
return h > 0 ? `${h}h ${minutes % 60}m` : `${minutes}m`;
|
|
16892
|
+
};
|
|
16893
|
+
function periscopeIdleItem(ticket, repo, number, idleMs, nowIso) {
|
|
16894
|
+
return {
|
|
16895
|
+
kind: "periscope_idle",
|
|
16896
|
+
key: periscopeIdleKey(ticket),
|
|
16897
|
+
since: nowIso,
|
|
16898
|
+
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}\`.`
|
|
16899
|
+
};
|
|
16900
|
+
}
|
|
16901
|
+
async function runPeriscopeIdlePass(d) {
|
|
16902
|
+
const result = { refreshed: 0, expired: 0 };
|
|
16903
|
+
const now = d.now();
|
|
16904
|
+
const liveKeys = /* @__PURE__ */ new Set();
|
|
16905
|
+
let lookupFailed = false;
|
|
16906
|
+
for (const row of await d.listLive()) {
|
|
16907
|
+
try {
|
|
16908
|
+
const t = await d.ticketOf(row);
|
|
16909
|
+
liveKeys.add(periscopeIdleKey(t.tag));
|
|
16910
|
+
const decision = decideIdle(row, d.mtimeOf(row.worktree), now.getTime(), d.idleMinutes);
|
|
16911
|
+
if (decision.action === "none") continue;
|
|
16912
|
+
if (decision.action === "refresh") {
|
|
16913
|
+
await d.update(row.id, { last_activity_at: decision.lastActivityAt, expires_at: decision.expiresAt });
|
|
16914
|
+
d.clear(periscopeIdleKey(t.tag));
|
|
16915
|
+
result.refreshed++;
|
|
16916
|
+
continue;
|
|
16917
|
+
}
|
|
16918
|
+
await d.stopServer(row.worktree, t.number);
|
|
16919
|
+
await d.raise(periscopeIdleItem(t.tag, t.repo, t.number, decision.idleMs, now.toISOString()));
|
|
16920
|
+
result.expired++;
|
|
16921
|
+
} catch (e) {
|
|
16922
|
+
lookupFailed = true;
|
|
16923
|
+
d.warn(`periscope idle check failed for ${row.worktree}: ${e.message}`);
|
|
16924
|
+
}
|
|
16925
|
+
}
|
|
16926
|
+
if (!lookupFailed) {
|
|
16927
|
+
for (const key of d.openKeys()) if (!liveKeys.has(key)) d.clear(key);
|
|
16928
|
+
}
|
|
16929
|
+
return result;
|
|
16930
|
+
}
|
|
16931
|
+
|
|
16776
16932
|
// src/cli.ts
|
|
16777
16933
|
import { dirname as dirOf2, resolve as resolvePath } from "path";
|
|
16778
16934
|
|
|
16779
16935
|
// src/ssh-keys.ts
|
|
16780
|
-
import { execFileSync as
|
|
16936
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
16781
16937
|
import { existsSync as existsSync32, mkdirSync as mkdirSync19, readFileSync as readFileSync28, chmodSync } from "fs";
|
|
16782
|
-
import { dirname as dirname19, join as
|
|
16938
|
+
import { dirname as dirname19, join as join36 } from "path";
|
|
16783
16939
|
var SshKeyError = class extends Error {
|
|
16784
16940
|
};
|
|
16785
16941
|
function shipSshKeyPathFor(stateDir) {
|
|
16786
|
-
return
|
|
16942
|
+
return join36(stateDir, "ssh", "id_ed25519");
|
|
16787
16943
|
}
|
|
16788
16944
|
function ensureShipSshKeypair(stateDir) {
|
|
16789
16945
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
@@ -16791,7 +16947,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
16791
16947
|
if (!existsSync32(privateKeyPath)) {
|
|
16792
16948
|
mkdirSync19(dirname19(privateKeyPath), { recursive: true });
|
|
16793
16949
|
try {
|
|
16794
|
-
|
|
16950
|
+
execFileSync7("ssh-keygen", [
|
|
16795
16951
|
"-t",
|
|
16796
16952
|
"ed25519",
|
|
16797
16953
|
"-N",
|
|
@@ -16817,7 +16973,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
16817
16973
|
}
|
|
16818
16974
|
function sshKeygenAvailable() {
|
|
16819
16975
|
try {
|
|
16820
|
-
|
|
16976
|
+
execFileSync7("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
16821
16977
|
return true;
|
|
16822
16978
|
} catch (e) {
|
|
16823
16979
|
return e.code !== "ENOENT";
|
|
@@ -16825,11 +16981,11 @@ function sshKeygenAvailable() {
|
|
|
16825
16981
|
}
|
|
16826
16982
|
|
|
16827
16983
|
// src/passenger-containers.ts
|
|
16828
|
-
import { execFileSync as
|
|
16984
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
16829
16985
|
import { createHash as createHash6 } from "crypto";
|
|
16830
16986
|
var realExec = (cmd, args, opts) => {
|
|
16831
16987
|
try {
|
|
16832
|
-
return
|
|
16988
|
+
return execFileSync8(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: opts?.timeoutMs });
|
|
16833
16989
|
} catch (e) {
|
|
16834
16990
|
const err = e;
|
|
16835
16991
|
const why = [err.stderr, err.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
|
|
@@ -17031,7 +17187,7 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
17031
17187
|
import { spawn as spawn5 } from "child_process";
|
|
17032
17188
|
import { existsSync as existsSync33, mkdirSync as mkdirSync20, readFileSync as readFileSync29, writeFileSync as writeFileSync21, unlinkSync as unlinkSync3, readdirSync as readdirSync14 } from "fs";
|
|
17033
17189
|
import { randomInt } from "crypto";
|
|
17034
|
-
import { join as
|
|
17190
|
+
import { join as join37 } from "path";
|
|
17035
17191
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
17036
17192
|
var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
17037
17193
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -17098,7 +17254,7 @@ function startTunnel(opts) {
|
|
|
17098
17254
|
};
|
|
17099
17255
|
}
|
|
17100
17256
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
17101
|
-
return
|
|
17257
|
+
return join37(stateDir, "passengers", `${workspaceId}.json`);
|
|
17102
17258
|
}
|
|
17103
17259
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
17104
17260
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
@@ -17111,7 +17267,7 @@ function readPersistedTunnel(stateDir, workspaceId) {
|
|
|
17111
17267
|
}
|
|
17112
17268
|
function writePersistedTunnel(stateDir, state) {
|
|
17113
17269
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
17114
|
-
mkdirSync20(
|
|
17270
|
+
mkdirSync20(join37(stateDir, "passengers"), { recursive: true });
|
|
17115
17271
|
writeFileSync21(path, `${JSON.stringify(state, null, 2)}
|
|
17116
17272
|
`);
|
|
17117
17273
|
}
|
|
@@ -17209,7 +17365,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
17209
17365
|
}
|
|
17210
17366
|
}
|
|
17211
17367
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
17212
|
-
const dir =
|
|
17368
|
+
const dir = join37(stateDir, "passengers");
|
|
17213
17369
|
if (!existsSync33(dir)) return [];
|
|
17214
17370
|
return readdirSync14(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
17215
17371
|
}
|
|
@@ -17332,7 +17488,7 @@ import { createServer as createHttpServer } from "http";
|
|
|
17332
17488
|
import { spawn as spawn6 } from "child_process";
|
|
17333
17489
|
import { randomBytes } from "crypto";
|
|
17334
17490
|
import { existsSync as existsSync34, mkdirSync as mkdirSync21, readFileSync as readFileSync30, writeFileSync as writeFileSync22, unlinkSync as unlinkSync4, readdirSync as readdirSync15 } from "fs";
|
|
17335
|
-
import { join as
|
|
17491
|
+
import { join as join38 } from "path";
|
|
17336
17492
|
|
|
17337
17493
|
// src/secret-compare.ts
|
|
17338
17494
|
import { timingSafeEqual } from "crypto";
|
|
@@ -17421,7 +17577,7 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
17421
17577
|
return child;
|
|
17422
17578
|
};
|
|
17423
17579
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
17424
|
-
return
|
|
17580
|
+
return join38(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
17425
17581
|
}
|
|
17426
17582
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
17427
17583
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
@@ -17434,7 +17590,7 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
|
17434
17590
|
}
|
|
17435
17591
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
17436
17592
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
17437
|
-
mkdirSync21(
|
|
17593
|
+
mkdirSync21(join38(stateDir, "passengers"), { recursive: true });
|
|
17438
17594
|
writeFileSync22(path, `${JSON.stringify(state, null, 2)}
|
|
17439
17595
|
`);
|
|
17440
17596
|
}
|
|
@@ -17445,8 +17601,8 @@ function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
|
17445
17601
|
} catch {
|
|
17446
17602
|
}
|
|
17447
17603
|
}
|
|
17448
|
-
function sleepSync2(
|
|
17449
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0,
|
|
17604
|
+
function sleepSync2(ms3) {
|
|
17605
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms3);
|
|
17450
17606
|
}
|
|
17451
17607
|
function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
17452
17608
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -17455,7 +17611,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
17455
17611
|
}
|
|
17456
17612
|
}
|
|
17457
17613
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
17458
|
-
const dir =
|
|
17614
|
+
const dir = join38(stateDir, "passengers");
|
|
17459
17615
|
if (!existsSync34(dir)) return [];
|
|
17460
17616
|
const suffix = "-sync.json";
|
|
17461
17617
|
return readdirSync15(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
@@ -17544,12 +17700,12 @@ function describeCause(err) {
|
|
|
17544
17700
|
}
|
|
17545
17701
|
|
|
17546
17702
|
// src/daemon.ts
|
|
17547
|
-
import { existsSync as existsSync35, mkdirSync as mkdirSync22, readFileSync as readFileSync31, readdirSync as readdirSync16, rmSync as rmSync15, statSync as
|
|
17548
|
-
import { join as
|
|
17703
|
+
import { existsSync as existsSync35, mkdirSync as mkdirSync22, readFileSync as readFileSync31, readdirSync as readdirSync16, rmSync as rmSync15, statSync as statSync10, writeFileSync as writeFileSync23 } from "fs";
|
|
17704
|
+
import { join as join39 } from "path";
|
|
17549
17705
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
17550
17706
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
17551
17707
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
17552
|
-
var defaultSleep = (
|
|
17708
|
+
var defaultSleep = (ms3) => new Promise((resolve6) => setTimeout(resolve6, ms3));
|
|
17553
17709
|
async function runDaemonLoop(o) {
|
|
17554
17710
|
const floor = o.floorMs ?? DEFAULT_FLOOR_MS;
|
|
17555
17711
|
const ceiling = o.ceilingMs ?? DEFAULT_CEILING_MS;
|
|
@@ -17608,7 +17764,7 @@ async function runDaemonLoop(o) {
|
|
|
17608
17764
|
function latestMtime(path) {
|
|
17609
17765
|
let stat;
|
|
17610
17766
|
try {
|
|
17611
|
-
stat =
|
|
17767
|
+
stat = statSync10(path);
|
|
17612
17768
|
} catch {
|
|
17613
17769
|
return 0;
|
|
17614
17770
|
}
|
|
@@ -17616,7 +17772,7 @@ function latestMtime(path) {
|
|
|
17616
17772
|
if (!stat.isDirectory()) return 0;
|
|
17617
17773
|
let latest = stat.mtimeMs;
|
|
17618
17774
|
for (const entry of readdirSync16(path, { withFileTypes: true })) {
|
|
17619
|
-
latest = Math.max(latest, latestMtime(
|
|
17775
|
+
latest = Math.max(latest, latestMtime(join39(path, entry.name)));
|
|
17620
17776
|
}
|
|
17621
17777
|
return latest;
|
|
17622
17778
|
}
|
|
@@ -17628,10 +17784,10 @@ function makeStaleChecker(paths, opts = {}) {
|
|
|
17628
17784
|
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync35(opts.reloadSentinel);
|
|
17629
17785
|
}
|
|
17630
17786
|
function reloadSentinelFor(stateDir) {
|
|
17631
|
-
return
|
|
17787
|
+
return join39(stateDir, "reload-requested");
|
|
17632
17788
|
}
|
|
17633
17789
|
function daemonStampFor(stateDir) {
|
|
17634
|
-
return
|
|
17790
|
+
return join39(stateDir, "daemon.started");
|
|
17635
17791
|
}
|
|
17636
17792
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
17637
17793
|
mkdirSync22(stateDir, { recursive: true });
|
|
@@ -17650,7 +17806,7 @@ function readDaemonStarted(stateDir) {
|
|
|
17650
17806
|
function configChangedAfter(files, startedAt) {
|
|
17651
17807
|
for (const f of files) {
|
|
17652
17808
|
try {
|
|
17653
|
-
if (
|
|
17809
|
+
if (statSync10(f).mtimeMs > startedAt) return f;
|
|
17654
17810
|
} catch {
|
|
17655
17811
|
}
|
|
17656
17812
|
}
|
|
@@ -17749,6 +17905,35 @@ async function runOnePassUnguarded(o) {
|
|
|
17749
17905
|
emit.warn(`could not sweep stalled tickets: ${e.message}`, { step: "sweep" });
|
|
17750
17906
|
}
|
|
17751
17907
|
}
|
|
17908
|
+
if (o.route.resolved?.periscopesModelId) {
|
|
17909
|
+
const tracker = new Tracker(o.route, o.ship);
|
|
17910
|
+
try {
|
|
17911
|
+
await runPeriscopeIdlePass({
|
|
17912
|
+
idleMinutes: periscopeCaps(o.ship).idleMinutes,
|
|
17913
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
17914
|
+
listLive: () => tracker.liveShipPeriscopes(),
|
|
17915
|
+
mtimeOf: newestWorktreeMtime,
|
|
17916
|
+
update: (id, fields) => tracker.updatePeriscope(id, fields),
|
|
17917
|
+
ticketOf: async (row) => {
|
|
17918
|
+
const t = await tracker.ticket(row.issue_id);
|
|
17919
|
+
return {
|
|
17920
|
+
tag: t.issue_tag ?? t.issue_id,
|
|
17921
|
+
number: t.issue_id.replace(/^\D+/, ""),
|
|
17922
|
+
repo: t.repo_id && o.route.resolved?.repoNames?.[t.repo_id] || "unknown repository"
|
|
17923
|
+
};
|
|
17924
|
+
},
|
|
17925
|
+
stopServer: (worktree, number) => killGently(pidsInWorktree(worktree, Number(number))),
|
|
17926
|
+
raise: async (item) => {
|
|
17927
|
+
await raiseShipAttention({ state: o.state, emit, route: o.route, ship: o.ship }, item);
|
|
17928
|
+
},
|
|
17929
|
+
clear: (key) => clearShipAttention(o.state, key),
|
|
17930
|
+
openKeys: () => openShipAttention(o.state).filter((i) => i.kind === "periscope_idle").map((i) => i.key),
|
|
17931
|
+
warn: (message) => emit.warn(message, { step: "sweep" })
|
|
17932
|
+
});
|
|
17933
|
+
} catch (e) {
|
|
17934
|
+
emit.warn(`could not check idle periscopes: ${e.message}`, { step: "sweep" });
|
|
17935
|
+
}
|
|
17936
|
+
}
|
|
17752
17937
|
const order = [...decision.selection.pending].sort(
|
|
17753
17938
|
(a, b) => (decision.selection.ranks[a] ?? Number.POSITIVE_INFINITY) - (decision.selection.ranks[b] ?? Number.POSITIVE_INFINITY)
|
|
17754
17939
|
);
|
|
@@ -18235,6 +18420,7 @@ function usage(problem, requested = false) {
|
|
|
18235
18420
|
crew drop [route] NNN remove a merged ticket's worktree and branch
|
|
18236
18421
|
crew rotate-passenger-url [route] force a Host Passengers tunnel to reconnect with a fresh public URL
|
|
18237
18422
|
crew periscope up [route] REPO --topic TEXT [--isolate] [--host H] [--assignee ID] [--dry-run] [--json]
|
|
18423
|
+
crew periscope up --resume NNN restart the preview an idle periscope's server was stopped for
|
|
18238
18424
|
file a draft ticket, cut and prepare its worktree, start the repo's preview hook and record a Periscopes row (alias: start)
|
|
18239
18425
|
crew periscope down [route] NNN --keep|--discard [--allow-empty] [--json]
|
|
18240
18426
|
end a periscope: commit and push its branch (ticket back to new), or throw it away (alias: end)
|
|
@@ -18751,7 +18937,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18751
18937
|
}
|
|
18752
18938
|
if (host === "linux" && hasSystemd) {
|
|
18753
18939
|
const label = labelFor(CREW_HOME, "run");
|
|
18754
|
-
const staleTimer =
|
|
18940
|
+
const staleTimer = join40(homedir10(), ".config", "systemd", "user", `${label}.timer`);
|
|
18755
18941
|
if (existsSync36(staleTimer)) {
|
|
18756
18942
|
process.stdout.write(
|
|
18757
18943
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
@@ -19918,7 +20104,7 @@ ${cu.tail.trimEnd()}
|
|
|
19918
20104
|
process.exit(1);
|
|
19919
20105
|
}
|
|
19920
20106
|
requireArmed("run the daemon loop");
|
|
19921
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
20107
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join40(CREW_HOME, "src"), join40(CREW_HOME, "dist"), join40(CREW_HOME, "bin")];
|
|
19922
20108
|
const configPaths = daemonConfigPaths(cfg);
|
|
19923
20109
|
watchPaths.push(...configPaths);
|
|
19924
20110
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -21449,6 +21635,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21449
21635
|
await tracker.ticketsByNumbers(remoteBranchTicketNumbers(r.dir, r.config.branch.remote, r.config.branch.base), truncated)
|
|
21450
21636
|
).byRepo.get(r.name) ?? [];
|
|
21451
21637
|
const actions = planWorktreeSweep(r, wtTickets, tracker.contract);
|
|
21638
|
+
for (const k of draftWorktrees(r, wtTickets, tracker.contract)) {
|
|
21639
|
+
anything = true;
|
|
21640
|
+
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" });
|
|
21641
|
+
}
|
|
21452
21642
|
const orphanDirs = orphanedWorktreeDirs(r, wtTickets, tracker.contract);
|
|
21453
21643
|
warnOrphanedWorktreeDirs(orphanDirs, emit, dryRun ? void 0 : { state, repo: r.name });
|
|
21454
21644
|
const stale = planRemoteBranchCleanup(r.dir, r.config.branch.remote, r.config.branch.base, brTickets, tracker.contract);
|
|
@@ -21514,6 +21704,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21514
21704
|
}
|
|
21515
21705
|
const { repo: target, found } = candidates[0];
|
|
21516
21706
|
const wt = found.path;
|
|
21707
|
+
try {
|
|
21708
|
+
const trD = new Tracker(route, cfg.ship);
|
|
21709
|
+
const tD = (await trD.ticketsByNumbers([n])).find((t) => t.issue_id.replace(/^\D+/, "") === String(num));
|
|
21710
|
+
if (tD && tD[trD.contract.columns.status] === trD.contract.statuses.draft) {
|
|
21711
|
+
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\`
|
|
21712
|
+
`);
|
|
21713
|
+
process.exit(2);
|
|
21714
|
+
}
|
|
21715
|
+
} catch {
|
|
21716
|
+
}
|
|
21517
21717
|
const branch = found.branch ?? branchForIssue(target.dir, `ISSUE-${n}`, {
|
|
21518
21718
|
name: target.config.branch.name,
|
|
21519
21719
|
push: target.config.branch.push
|
|
@@ -21802,6 +22002,47 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21802
22002
|
break;
|
|
21803
22003
|
}
|
|
21804
22004
|
try {
|
|
22005
|
+
const resumeRaw = value("resume");
|
|
22006
|
+
if (resumeRaw !== void 0) {
|
|
22007
|
+
const nR = parseTicketNumber(resumeRaw);
|
|
22008
|
+
if (!nR) throw new PeriscopeError("--resume needs a ticket number (326, ISSUE-326, or CREW-326)");
|
|
22009
|
+
let hit;
|
|
22010
|
+
for (const cand of cfg.routes.filter((r) => r.enabled && r.resolved?.periscopesModelId)) {
|
|
22011
|
+
const tr = new Tracker(cand, cfg.ship);
|
|
22012
|
+
for (const row of await tr.liveShipPeriscopes()) {
|
|
22013
|
+
const t = await tr.ticket(row.issue_id);
|
|
22014
|
+
if (t.issue_id.replace(/^\D+/, "") !== String(Number(nR))) continue;
|
|
22015
|
+
const repo = (await resolvedRepos(cand)).find((r) => r.id === t.repo_id);
|
|
22016
|
+
if (!repo) throw new PeriscopeError(`route ${cand.route} has no checkout for the repository of ${t.issue_tag ?? t.issue_id}`);
|
|
22017
|
+
hit = { route: cand, tracker: tr, row, tag: t.issue_tag ?? t.issue_id, repo };
|
|
22018
|
+
}
|
|
22019
|
+
}
|
|
22020
|
+
if (dryRun) {
|
|
22021
|
+
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`);
|
|
22022
|
+
break;
|
|
22023
|
+
}
|
|
22024
|
+
const shellR = hit && resolveShell(hit.repo.config.shell ?? cfg.ship.shell);
|
|
22025
|
+
const resumed = await runPeriscopeResume({
|
|
22026
|
+
find: async () => hit && { row: hit.row, tag: hit.tag },
|
|
22027
|
+
worktreeExists: (p) => existsSync36(p),
|
|
22028
|
+
caps: periscopeCaps(cfg.ship),
|
|
22029
|
+
runHook: async (name, cwd, env) => {
|
|
22030
|
+
const script = hit?.repo.config.hooks[name];
|
|
22031
|
+
return script ? runScript(script, { cwd, env, shell: shellR, timeoutMs: name === "preview" ? 18e4 : void 0 }) : null;
|
|
22032
|
+
},
|
|
22033
|
+
update: (id, fields) => hit.tracker.updatePeriscope(id, fields),
|
|
22034
|
+
clearItem: (tag) => clearShipAttention(state, periscopeIdleKey(tag)),
|
|
22035
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
22036
|
+
say
|
|
22037
|
+
}, String(Number(nR)));
|
|
22038
|
+
if (jsonP) process.stdout.write(`${JSON.stringify({ event: "done", ...resumed })}
|
|
22039
|
+
`);
|
|
22040
|
+
else process.stdout.write(`${resumed.ticket}
|
|
22041
|
+
${resumed.worktree}
|
|
22042
|
+
${resumed.url}
|
|
22043
|
+
`);
|
|
22044
|
+
break;
|
|
22045
|
+
}
|
|
21805
22046
|
const a = parsePeriscopeUpArgs(positional, { topic: value("topic"), host: value("host"), assignee: value("assignee"), isolate: flag("isolate"), json: jsonP });
|
|
21806
22047
|
const candidatesP = a.route ? [findRoute(cfg, a.route)] : cfg.routes.filter((r) => r.enabled);
|
|
21807
22048
|
const servedP = [];
|
|
@@ -21864,10 +22105,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21864
22105
|
},
|
|
21865
22106
|
prepare: async (path) => {
|
|
21866
22107
|
for (const f of repoP.config.worktrees.copy) {
|
|
21867
|
-
const src =
|
|
22108
|
+
const src = join40(repoP.dir, f);
|
|
21868
22109
|
if (!existsSync36(src)) continue;
|
|
21869
|
-
mkdirSync23(dirOf2(
|
|
21870
|
-
copyFileSync2(src,
|
|
22110
|
+
mkdirSync23(dirOf2(join40(path, f)), { recursive: true });
|
|
22111
|
+
copyFileSync2(src, join40(path, f));
|
|
21871
22112
|
}
|
|
21872
22113
|
const setup = repoP.config.hooks.setup;
|
|
21873
22114
|
return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
|
|
@@ -22253,12 +22494,12 @@ ${res.url}
|
|
|
22253
22494
|
`);
|
|
22254
22495
|
}
|
|
22255
22496
|
}
|
|
22256
|
-
if (existsSync36(
|
|
22497
|
+
if (existsSync36(join40(dir, ".git")) && basename12(dir) === SAMPLE_REPO) rmSync16(dir, { recursive: true, force: true });
|
|
22257
22498
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
22258
22499
|
break;
|
|
22259
22500
|
}
|
|
22260
22501
|
const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
22261
|
-
const fresh = !existsSync36(
|
|
22502
|
+
const fresh = !existsSync36(join40(dir, ".git"));
|
|
22262
22503
|
if (existsSync36(dir) && fresh && readdirSync17(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
22263
22504
|
if (dryRun) {
|
|
22264
22505
|
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`);
|
|
@@ -22266,8 +22507,8 @@ ${res.url}
|
|
|
22266
22507
|
}
|
|
22267
22508
|
if (fresh) {
|
|
22268
22509
|
mkdirSync23(dir, { recursive: true });
|
|
22269
|
-
writeFileSync24(
|
|
22270
|
-
writeFileSync24(
|
|
22510
|
+
writeFileSync24(join40(dir, "README.md"), SAMPLE_README);
|
|
22511
|
+
writeFileSync24(join40(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
22271
22512
|
const g = (...a) => {
|
|
22272
22513
|
const r = spawnSync7("git", a, { cwd: dir, encoding: "utf8" });
|
|
22273
22514
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -22707,7 +22948,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
22707
22948
|
}
|
|
22708
22949
|
try {
|
|
22709
22950
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
22710
|
-
const evSize = existsSync36(evFile) ?
|
|
22951
|
+
const evSize = existsSync36(evFile) ? statSync11(evFile).size : 0;
|
|
22711
22952
|
process.stdout.write(
|
|
22712
22953
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
22713
22954
|
` : "events log: ok\n"
|
|
@@ -22734,7 +22975,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
22734
22975
|
const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
|
|
22735
22976
|
if (changed2) {
|
|
22736
22977
|
process.stdout.write(
|
|
22737
|
-
`daemon config: WARN ${changed2} changed at ${new Date(
|
|
22978
|
+
`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
22979
|
`
|
|
22739
22980
|
);
|
|
22740
22981
|
}
|
package/package.json
CHANGED