@tablation/crew 0.88.0 → 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 +275 -130
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -9803,7 +9803,7 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
9803
9803
|
}
|
|
9804
9804
|
|
|
9805
9805
|
// src/cli.ts
|
|
9806
|
-
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";
|
|
9807
9807
|
import { fileURLToPath } from "url";
|
|
9808
9808
|
import { hostname as hostname2, homedir as homedir10 } from "os";
|
|
9809
9809
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
@@ -9843,6 +9843,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
9843
9843
|
"write",
|
|
9844
9844
|
"topic",
|
|
9845
9845
|
"assignee",
|
|
9846
|
+
"issue",
|
|
9846
9847
|
"resume"
|
|
9847
9848
|
]);
|
|
9848
9849
|
function positionalArgs(argv2) {
|
|
@@ -16532,24 +16533,55 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
16532
16533
|
}
|
|
16533
16534
|
|
|
16534
16535
|
// src/cli.ts
|
|
16535
|
-
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";
|
|
16536
16537
|
import { createInterface } from "readline/promises";
|
|
16537
16538
|
|
|
16538
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";
|
|
16539
16542
|
var PeriscopeError = class extends Error {
|
|
16540
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
|
+
}
|
|
16541
16564
|
var DEFAULT_MAX_PERISCOPES2 = 3;
|
|
16542
16565
|
var DEFAULT_PERISCOPE_IDLE_MINUTES2 = 120;
|
|
16543
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]";
|
|
16544
16568
|
const rest = positional2.slice(2);
|
|
16545
|
-
if (rest.length === 0 || rest.length > 2)
|
|
16546
|
-
throw new PeriscopeError("usage: crew periscope up [route] REPO [--topic TEXT] [--isolate] [--host HOST] [--assignee CREW_ROW_ID] [--json]");
|
|
16547
|
-
}
|
|
16569
|
+
if (rest.length === 0 || rest.length > 2) throw new PeriscopeError(usage2);
|
|
16548
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
|
+
}
|
|
16549
16580
|
return {
|
|
16550
16581
|
...rest.length === 2 ? { route: rest[0] } : {},
|
|
16551
16582
|
repo: rest[rest.length - 1],
|
|
16552
16583
|
...topic ? { topic } : {},
|
|
16584
|
+
...issue ? { issue } : {},
|
|
16553
16585
|
isolate: flags.isolate,
|
|
16554
16586
|
...flags.host ? { host: flags.host } : {},
|
|
16555
16587
|
...flags.assignee ? { assignee: flags.assignee } : {},
|
|
@@ -16574,6 +16606,17 @@ function periscopeCaps(l) {
|
|
|
16574
16606
|
idleMinutes: l.periscopeIdleMinutes ?? DEFAULT_PERISCOPE_IDLE_MINUTES2
|
|
16575
16607
|
};
|
|
16576
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
|
+
}
|
|
16577
16620
|
var tail = (s, n = 20) => s.trimEnd().split("\n").slice(-n).join("\n");
|
|
16578
16621
|
function progressComment(url, handoff) {
|
|
16579
16622
|
return [
|
|
@@ -16595,17 +16638,36 @@ async function periscopePreflight(d, repo) {
|
|
|
16595
16638
|
}
|
|
16596
16639
|
async function runPeriscopeUp(d, args) {
|
|
16597
16640
|
await periscopePreflight(d, args.repo);
|
|
16598
|
-
const
|
|
16599
|
-
|
|
16600
|
-
|
|
16601
|
-
if (
|
|
16602
|
-
|
|
16603
|
-
|
|
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
|
+
}
|
|
16604
16664
|
let wt;
|
|
16605
16665
|
const abandon = async (why) => {
|
|
16606
16666
|
if (wt) await d.teardown(wt.path, wt.branch).catch(() => {
|
|
16607
16667
|
});
|
|
16608
|
-
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(() => {
|
|
16609
16671
|
});
|
|
16610
16672
|
throw new PeriscopeError(why);
|
|
16611
16673
|
};
|
|
@@ -16682,7 +16744,15 @@ ${tail(preview.output)}`);
|
|
|
16682
16744
|
await d.comment(draft.id, progressComment(url, handoff?.output)).catch((e) => {
|
|
16683
16745
|
d.say("warning", `progress comment not posted: ${e.message}`);
|
|
16684
16746
|
});
|
|
16685
|
-
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
|
+
};
|
|
16686
16756
|
}
|
|
16687
16757
|
function parsePeriscopeDownArgs(positional2, flags) {
|
|
16688
16758
|
const usage2 = "usage: crew periscope down [route] NNN --keep|--discard [--allow-empty] [--json]";
|
|
@@ -16731,6 +16801,21 @@ async function runPeriscopeDown(d, args) {
|
|
|
16731
16801
|
d.say("unchanged", `${done.ticket} is already ${done.state}; nothing to do`);
|
|
16732
16802
|
return { ticket: done.ticket, mode: args.mode, outcome: "unchanged", state: done.state };
|
|
16733
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
|
+
}
|
|
16734
16819
|
if (args.mode === "discard") {
|
|
16735
16820
|
const killed2 = await d.stopServer(row.worktree, row.number);
|
|
16736
16821
|
d.say("server", killed2 ? `stopped ${killed2} process(es)` : "no server was listening");
|
|
@@ -16762,12 +16847,17 @@ async function runPeriscopeDown(d, args) {
|
|
|
16762
16847
|
const description = t.description.includes(PROTOTYPE_HEADING) ? t.description : `${t.description.trimEnd()}
|
|
16763
16848
|
|
|
16764
16849
|
${section}`;
|
|
16765
|
-
|
|
16766
|
-
description,
|
|
16767
|
-
|
|
16768
|
-
|
|
16769
|
-
|
|
16770
|
-
|
|
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
|
+
}
|
|
16771
16861
|
await d.postPushed(row.ticketRecordId, pushed.sha, pushed.remote, row.branch);
|
|
16772
16862
|
const killed = await d.stopServer(row.worktree, row.number);
|
|
16773
16863
|
d.say("server", killed ? `stopped ${killed} process(es)` : "no server was listening");
|
|
@@ -16776,7 +16866,7 @@ ${section}`;
|
|
|
16776
16866
|
await d.setRowState(row.id, "kept");
|
|
16777
16867
|
d.say("row", "row is kept");
|
|
16778
16868
|
forgetLocal(d, row.ticketRecordId);
|
|
16779
|
-
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 } : {} } : {} };
|
|
16780
16870
|
}
|
|
16781
16871
|
function periscopeListEntries(rows, now, isListening) {
|
|
16782
16872
|
return rows.filter((r) => r.state === "live").map((r) => {
|
|
@@ -16838,7 +16928,7 @@ ${tail2(preview.output)}`);
|
|
|
16838
16928
|
// src/periscope-idle.ts
|
|
16839
16929
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
16840
16930
|
import { statSync as statSync9 } from "fs";
|
|
16841
|
-
import { join as
|
|
16931
|
+
import { join as join36 } from "path";
|
|
16842
16932
|
var MAX_SCANNED_FILES = 2e4;
|
|
16843
16933
|
function newestWorktreeMtime(root) {
|
|
16844
16934
|
let listing;
|
|
@@ -16858,7 +16948,7 @@ function newestWorktreeMtime(root) {
|
|
|
16858
16948
|
if (!rel) continue;
|
|
16859
16949
|
if (++scanned > MAX_SCANNED_FILES) break;
|
|
16860
16950
|
try {
|
|
16861
|
-
const m = statSync9(
|
|
16951
|
+
const m = statSync9(join36(root, rel)).mtimeMs;
|
|
16862
16952
|
if (newest === null || m > newest) newest = m;
|
|
16863
16953
|
} catch {
|
|
16864
16954
|
}
|
|
@@ -16934,18 +17024,18 @@ import { dirname as dirOf2, resolve as resolvePath } from "path";
|
|
|
16934
17024
|
|
|
16935
17025
|
// src/ssh-keys.ts
|
|
16936
17026
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
16937
|
-
import { existsSync as
|
|
16938
|
-
import { dirname as dirname19, join as
|
|
17027
|
+
import { existsSync as existsSync33, mkdirSync as mkdirSync20, readFileSync as readFileSync29, chmodSync } from "fs";
|
|
17028
|
+
import { dirname as dirname19, join as join37 } from "path";
|
|
16939
17029
|
var SshKeyError = class extends Error {
|
|
16940
17030
|
};
|
|
16941
17031
|
function shipSshKeyPathFor(stateDir) {
|
|
16942
|
-
return
|
|
17032
|
+
return join37(stateDir, "ssh", "id_ed25519");
|
|
16943
17033
|
}
|
|
16944
17034
|
function ensureShipSshKeypair(stateDir) {
|
|
16945
17035
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
16946
17036
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
16947
|
-
if (!
|
|
16948
|
-
|
|
17037
|
+
if (!existsSync33(privateKeyPath)) {
|
|
17038
|
+
mkdirSync20(dirname19(privateKeyPath), { recursive: true });
|
|
16949
17039
|
try {
|
|
16950
17040
|
execFileSync7("ssh-keygen", [
|
|
16951
17041
|
"-t",
|
|
@@ -16965,10 +17055,10 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
16965
17055
|
}
|
|
16966
17056
|
chmodSync(privateKeyPath, 384);
|
|
16967
17057
|
}
|
|
16968
|
-
if (!
|
|
17058
|
+
if (!existsSync33(publicKeyPath)) {
|
|
16969
17059
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
16970
17060
|
}
|
|
16971
|
-
const publicKey =
|
|
17061
|
+
const publicKey = readFileSync29(publicKeyPath, "utf8").trim();
|
|
16972
17062
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
16973
17063
|
}
|
|
16974
17064
|
function sshKeygenAvailable() {
|
|
@@ -17185,9 +17275,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
17185
17275
|
|
|
17186
17276
|
// src/tunnel.ts
|
|
17187
17277
|
import { spawn as spawn5 } from "child_process";
|
|
17188
|
-
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";
|
|
17189
17279
|
import { randomInt } from "crypto";
|
|
17190
|
-
import { join as
|
|
17280
|
+
import { join as join38 } from "path";
|
|
17191
17281
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
17192
17282
|
var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
17193
17283
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -17254,21 +17344,21 @@ function startTunnel(opts) {
|
|
|
17254
17344
|
};
|
|
17255
17345
|
}
|
|
17256
17346
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
17257
|
-
return
|
|
17347
|
+
return join38(stateDir, "passengers", `${workspaceId}.json`);
|
|
17258
17348
|
}
|
|
17259
17349
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
17260
17350
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
17261
|
-
if (!
|
|
17351
|
+
if (!existsSync34(path)) return void 0;
|
|
17262
17352
|
try {
|
|
17263
|
-
return JSON.parse(
|
|
17353
|
+
return JSON.parse(readFileSync30(path, "utf8"));
|
|
17264
17354
|
} catch {
|
|
17265
17355
|
return void 0;
|
|
17266
17356
|
}
|
|
17267
17357
|
}
|
|
17268
17358
|
function writePersistedTunnel(stateDir, state) {
|
|
17269
17359
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
17270
|
-
|
|
17271
|
-
|
|
17360
|
+
mkdirSync21(join38(stateDir, "passengers"), { recursive: true });
|
|
17361
|
+
writeFileSync22(path, `${JSON.stringify(state, null, 2)}
|
|
17272
17362
|
`);
|
|
17273
17363
|
}
|
|
17274
17364
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -17365,8 +17455,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
17365
17455
|
}
|
|
17366
17456
|
}
|
|
17367
17457
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
17368
|
-
const dir =
|
|
17369
|
-
if (!
|
|
17458
|
+
const dir = join38(stateDir, "passengers");
|
|
17459
|
+
if (!existsSync34(dir)) return [];
|
|
17370
17460
|
return readdirSync14(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
17371
17461
|
}
|
|
17372
17462
|
|
|
@@ -17487,8 +17577,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
17487
17577
|
import { createServer as createHttpServer } from "http";
|
|
17488
17578
|
import { spawn as spawn6 } from "child_process";
|
|
17489
17579
|
import { randomBytes } from "crypto";
|
|
17490
|
-
import { existsSync as
|
|
17491
|
-
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";
|
|
17492
17582
|
|
|
17493
17583
|
// src/secret-compare.ts
|
|
17494
17584
|
import { timingSafeEqual } from "crypto";
|
|
@@ -17577,21 +17667,21 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
17577
17667
|
return child;
|
|
17578
17668
|
};
|
|
17579
17669
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
17580
|
-
return
|
|
17670
|
+
return join39(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
17581
17671
|
}
|
|
17582
17672
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
17583
17673
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
17584
|
-
if (!
|
|
17674
|
+
if (!existsSync35(path)) return void 0;
|
|
17585
17675
|
try {
|
|
17586
|
-
return JSON.parse(
|
|
17676
|
+
return JSON.parse(readFileSync31(path, "utf8"));
|
|
17587
17677
|
} catch {
|
|
17588
17678
|
return void 0;
|
|
17589
17679
|
}
|
|
17590
17680
|
}
|
|
17591
17681
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
17592
17682
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
17593
|
-
|
|
17594
|
-
|
|
17683
|
+
mkdirSync22(join39(stateDir, "passengers"), { recursive: true });
|
|
17684
|
+
writeFileSync23(path, `${JSON.stringify(state, null, 2)}
|
|
17595
17685
|
`);
|
|
17596
17686
|
}
|
|
17597
17687
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -17611,8 +17701,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
17611
17701
|
}
|
|
17612
17702
|
}
|
|
17613
17703
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
17614
|
-
const dir =
|
|
17615
|
-
if (!
|
|
17704
|
+
const dir = join39(stateDir, "passengers");
|
|
17705
|
+
if (!existsSync35(dir)) return [];
|
|
17616
17706
|
const suffix = "-sync.json";
|
|
17617
17707
|
return readdirSync15(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
17618
17708
|
}
|
|
@@ -17700,8 +17790,8 @@ function describeCause(err) {
|
|
|
17700
17790
|
}
|
|
17701
17791
|
|
|
17702
17792
|
// src/daemon.ts
|
|
17703
|
-
import { existsSync as
|
|
17704
|
-
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";
|
|
17705
17795
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
17706
17796
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
17707
17797
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
@@ -17772,7 +17862,7 @@ function latestMtime(path) {
|
|
|
17772
17862
|
if (!stat.isDirectory()) return 0;
|
|
17773
17863
|
let latest = stat.mtimeMs;
|
|
17774
17864
|
for (const entry of readdirSync16(path, { withFileTypes: true })) {
|
|
17775
|
-
latest = Math.max(latest, latestMtime(
|
|
17865
|
+
latest = Math.max(latest, latestMtime(join40(path, entry.name)));
|
|
17776
17866
|
}
|
|
17777
17867
|
return latest;
|
|
17778
17868
|
}
|
|
@@ -17781,23 +17871,23 @@ function computeTreeSignature(paths) {
|
|
|
17781
17871
|
}
|
|
17782
17872
|
function makeStaleChecker(paths, opts = {}) {
|
|
17783
17873
|
const initial = computeTreeSignature(paths);
|
|
17784
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
17874
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync36(opts.reloadSentinel);
|
|
17785
17875
|
}
|
|
17786
17876
|
function reloadSentinelFor(stateDir) {
|
|
17787
|
-
return
|
|
17877
|
+
return join40(stateDir, "reload-requested");
|
|
17788
17878
|
}
|
|
17789
17879
|
function daemonStampFor(stateDir) {
|
|
17790
|
-
return
|
|
17880
|
+
return join40(stateDir, "daemon.started");
|
|
17791
17881
|
}
|
|
17792
17882
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
17793
|
-
|
|
17794
|
-
|
|
17795
|
-
|
|
17883
|
+
mkdirSync23(stateDir, { recursive: true });
|
|
17884
|
+
rmSync16(reloadSentinelFor(stateDir), { force: true });
|
|
17885
|
+
writeFileSync24(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
17796
17886
|
`);
|
|
17797
17887
|
}
|
|
17798
17888
|
function readDaemonStarted(stateDir) {
|
|
17799
17889
|
try {
|
|
17800
|
-
const v = JSON.parse(
|
|
17890
|
+
const v = JSON.parse(readFileSync32(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
17801
17891
|
return typeof v === "number" ? v : null;
|
|
17802
17892
|
} catch {
|
|
17803
17893
|
return null;
|
|
@@ -18419,7 +18509,7 @@ function usage(problem, requested = false) {
|
|
|
18419
18509
|
crew reap [route] kill orphaned servers, drop worktrees for closed tickets
|
|
18420
18510
|
crew drop [route] NNN remove a merged ticket's worktree and branch
|
|
18421
18511
|
crew rotate-passenger-url [route] force a Host Passengers tunnel to reconnect with a fresh public URL
|
|
18422
|
-
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]
|
|
18423
18513
|
crew periscope up --resume NNN restart the preview an idle periscope's server was stopped for
|
|
18424
18514
|
file a draft ticket, cut and prepare its worktree, start the repo's preview hook and record a Periscopes row (alias: start)
|
|
18425
18515
|
crew periscope down [route] NNN --keep|--discard [--allow-empty] [--json]
|
|
@@ -18699,7 +18789,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18699
18789
|
} else {
|
|
18700
18790
|
const toleratesNoRoutes = command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
|
|
18701
18791
|
const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
|
|
18702
|
-
if (configless && !
|
|
18792
|
+
if (configless && !existsSync37(findConfigFile(CREW_HOME))) {
|
|
18703
18793
|
cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
|
|
18704
18794
|
} else {
|
|
18705
18795
|
cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
|
|
@@ -18937,8 +19027,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
18937
19027
|
}
|
|
18938
19028
|
if (host === "linux" && hasSystemd) {
|
|
18939
19029
|
const label = labelFor(CREW_HOME, "run");
|
|
18940
|
-
const staleTimer =
|
|
18941
|
-
if (
|
|
19030
|
+
const staleTimer = join41(homedir10(), ".config", "systemd", "user", `${label}.timer`);
|
|
19031
|
+
if (existsSync37(staleTimer)) {
|
|
18942
19032
|
process.stdout.write(
|
|
18943
19033
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
18944
19034
|
`
|
|
@@ -18965,7 +19055,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18965
19055
|
const tracker = new Tracker(c, cfg.ship);
|
|
18966
19056
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
18967
19057
|
for (const r of await resolvedRepos(c)) {
|
|
18968
|
-
if (!
|
|
19058
|
+
if (!existsSync37(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
18969
19059
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
18970
19060
|
readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
18971
19061
|
}
|
|
@@ -18983,7 +19073,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
18983
19073
|
}
|
|
18984
19074
|
async function doctorFix(o) {
|
|
18985
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))));
|
|
18986
|
-
const unitExists = (p) =>
|
|
19076
|
+
const unitExists = (p) => existsSync37(p);
|
|
18987
19077
|
const crontab = o.host === "linux" ? spawnSync7("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
18988
19078
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
18989
19079
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -19000,7 +19090,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
19000
19090
|
for (const a of plan.actions) {
|
|
19001
19091
|
if (a.kind === "enable") {
|
|
19002
19092
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
19003
|
-
|
|
19093
|
+
writeFileSync25(cfg.configFile, planEnable(readFileSync33(cfg.configFile, "utf8"), a.route));
|
|
19004
19094
|
enabledSound.add(a.route);
|
|
19005
19095
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
19006
19096
|
} else {
|
|
@@ -20104,7 +20194,7 @@ ${cu.tail.trimEnd()}
|
|
|
20104
20194
|
process.exit(1);
|
|
20105
20195
|
}
|
|
20106
20196
|
requireArmed("run the daemon loop");
|
|
20107
|
-
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")];
|
|
20108
20198
|
const configPaths = daemonConfigPaths(cfg);
|
|
20109
20199
|
watchPaths.push(...configPaths);
|
|
20110
20200
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -20333,10 +20423,10 @@ ${cu.tail.trimEnd()}
|
|
|
20333
20423
|
`);
|
|
20334
20424
|
if (!dryRun) {
|
|
20335
20425
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
20336
|
-
const raw =
|
|
20426
|
+
const raw = existsSync37(resolvedPath) ? JSON.parse(readFileSync33(resolvedPath, "utf8")) : {};
|
|
20337
20427
|
raw.agentSkills = result.agentSkills;
|
|
20338
|
-
|
|
20339
|
-
|
|
20428
|
+
mkdirSync24(dirOf2(resolvedPath), { recursive: true });
|
|
20429
|
+
writeFileSync25(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
20340
20430
|
`);
|
|
20341
20431
|
}
|
|
20342
20432
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -20359,7 +20449,7 @@ ${cu.tail.trimEnd()}
|
|
|
20359
20449
|
throw e;
|
|
20360
20450
|
}
|
|
20361
20451
|
const oldName = cfg.ship.name;
|
|
20362
|
-
|
|
20452
|
+
writeFileSync25(cfg.configFile, setShipNameInYaml(readFileSync33(cfg.configFile, "utf8"), newName));
|
|
20363
20453
|
cfg.ship.name = newName;
|
|
20364
20454
|
let renameMachineId;
|
|
20365
20455
|
try {
|
|
@@ -20406,7 +20496,7 @@ ${cu.tail.trimEnd()}
|
|
|
20406
20496
|
process.exit(2);
|
|
20407
20497
|
};
|
|
20408
20498
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
|
|
20409
|
-
const configText =
|
|
20499
|
+
const configText = readFileSync33(cfg.configFile, "utf8");
|
|
20410
20500
|
if (sub === "check") {
|
|
20411
20501
|
try {
|
|
20412
20502
|
const routeArg = value("route") ?? positional[2];
|
|
@@ -20437,8 +20527,8 @@ ${cu.tail.trimEnd()}
|
|
|
20437
20527
|
try {
|
|
20438
20528
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
20439
20529
|
const dir = repoDirOf(configText, dirname20(cfg.configFile), a.route, a.name);
|
|
20440
|
-
if (!
|
|
20441
|
-
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) => {
|
|
20442
20532
|
try {
|
|
20443
20533
|
return readdirSync17(resolvePath(dir, r));
|
|
20444
20534
|
} catch {
|
|
@@ -20446,7 +20536,7 @@ ${cu.tail.trimEnd()}
|
|
|
20446
20536
|
}
|
|
20447
20537
|
}, read: (r) => {
|
|
20448
20538
|
try {
|
|
20449
|
-
return
|
|
20539
|
+
return readFileSync33(resolvePath(dir, r), "utf8");
|
|
20450
20540
|
} catch {
|
|
20451
20541
|
return void 0;
|
|
20452
20542
|
}
|
|
@@ -20488,19 +20578,19 @@ ${cu.tail.trimEnd()}
|
|
|
20488
20578
|
let cleared;
|
|
20489
20579
|
if (clear2) {
|
|
20490
20580
|
const plan = clearRouteHooks(configText, a.route, a.name);
|
|
20491
|
-
if (plan.removed.length > 0)
|
|
20581
|
+
if (plan.removed.length > 0) writeFileSync25(cfg.configFile, plan.text);
|
|
20492
20582
|
cleared = { file: cfg.configFile, removed: plan.removed };
|
|
20493
20583
|
}
|
|
20494
20584
|
if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
20495
20585
|
if (write === "route") {
|
|
20496
20586
|
const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
|
|
20497
|
-
|
|
20587
|
+
writeFileSync25(cfg.configFile, plan.text);
|
|
20498
20588
|
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
|
|
20499
20589
|
} else if (write === "repo") {
|
|
20500
20590
|
const have = existingRepoConfig(files);
|
|
20501
20591
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
20502
20592
|
const file = resolvePath(dir, ".crew.yaml");
|
|
20503
|
-
|
|
20593
|
+
writeFileSync25(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
|
|
20504
20594
|
written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
|
|
20505
20595
|
}
|
|
20506
20596
|
if (flag("json")) {
|
|
@@ -20556,7 +20646,7 @@ ${cu.tail.trimEnd()}
|
|
|
20556
20646
|
if (sub === "list" && !routeName) usage2();
|
|
20557
20647
|
if (sub === "add") {
|
|
20558
20648
|
try {
|
|
20559
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
20649
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync37(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
|
|
20560
20650
|
routeName = a.route;
|
|
20561
20651
|
addPath = a.path;
|
|
20562
20652
|
addName = a.name;
|
|
@@ -20585,16 +20675,16 @@ ${cu.tail.trimEnd()}
|
|
|
20585
20675
|
}
|
|
20586
20676
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
20587
20677
|
let resolvedRemotes;
|
|
20588
|
-
if (
|
|
20678
|
+
if (existsSync37(resolvedFile)) {
|
|
20589
20679
|
try {
|
|
20590
|
-
resolvedRemotes = JSON.parse(
|
|
20680
|
+
resolvedRemotes = JSON.parse(readFileSync33(resolvedFile, "utf8"));
|
|
20591
20681
|
} catch {
|
|
20592
20682
|
}
|
|
20593
20683
|
}
|
|
20594
20684
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
20595
20685
|
const text = configText;
|
|
20596
20686
|
const base = dirname20(cfg.configFile);
|
|
20597
|
-
const isCheckout = (d) =>
|
|
20687
|
+
const isCheckout = (d) => existsSync37(`${d}/.git`);
|
|
20598
20688
|
try {
|
|
20599
20689
|
if (sub === "list") {
|
|
20600
20690
|
if (flag("json")) {
|
|
@@ -20604,11 +20694,11 @@ ${cu.tail.trimEnd()}
|
|
|
20604
20694
|
base,
|
|
20605
20695
|
resolved: resolvedRemotes,
|
|
20606
20696
|
originOf,
|
|
20607
|
-
exists: (d) =>
|
|
20697
|
+
exists: (d) => existsSync37(d),
|
|
20608
20698
|
isCheckout,
|
|
20609
20699
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
20610
20700
|
try {
|
|
20611
|
-
return Object.keys(parseRepoConfig(
|
|
20701
|
+
return Object.keys(parseRepoConfig(readFileSync33(resolvePath(d, f), "utf8"), f).hooks).length > 0;
|
|
20612
20702
|
} catch {
|
|
20613
20703
|
return false;
|
|
20614
20704
|
}
|
|
@@ -20625,7 +20715,7 @@ ${cu.tail.trimEnd()}
|
|
|
20625
20715
|
for (const r of repos) {
|
|
20626
20716
|
const origin2 = originOf(r.dir);
|
|
20627
20717
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
20628
|
-
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)"}`;
|
|
20629
20719
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
20630
20720
|
`);
|
|
20631
20721
|
}
|
|
@@ -20637,9 +20727,9 @@ ${cu.tail.trimEnd()}
|
|
|
20637
20727
|
` : `${human}
|
|
20638
20728
|
`);
|
|
20639
20729
|
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
|
|
20640
|
-
if (
|
|
20730
|
+
if (existsSync37(plan2.target) && (existsSync37(`${plan2.target}/.git`) || readdirSync17(plan2.target).length > 0)) {
|
|
20641
20731
|
throw new ReposError(
|
|
20642
|
-
|
|
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`
|
|
20643
20733
|
);
|
|
20644
20734
|
}
|
|
20645
20735
|
if (dryRun) {
|
|
@@ -20657,14 +20747,14 @@ ${cu.tail.trimEnd()}
|
|
|
20657
20747
|
throw new ReposError(`${e.message}${authHint}
|
|
20658
20748
|
crew.yaml was not changed`);
|
|
20659
20749
|
}
|
|
20660
|
-
|
|
20750
|
+
writeFileSync25(cfg.configFile, plan2.text);
|
|
20661
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:
|
|
20662
20752
|
`);
|
|
20663
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}`);
|
|
20664
20754
|
break;
|
|
20665
20755
|
}
|
|
20666
20756
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
|
|
20667
|
-
const top =
|
|
20757
|
+
const top = existsSync37(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
20668
20758
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
20669
20759
|
const origin = originOf(top);
|
|
20670
20760
|
const plan = planRepoAdd({
|
|
@@ -20716,15 +20806,15 @@ ${cu.tail.trimEnd()}
|
|
|
20716
20806
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
20717
20807
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
20718
20808
|
`);
|
|
20719
|
-
if (
|
|
20720
|
-
const raw = JSON.parse(
|
|
20809
|
+
if (existsSync37(resolvedFile)) {
|
|
20810
|
+
const raw = JSON.parse(readFileSync33(resolvedFile, "utf8"));
|
|
20721
20811
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
20722
20812
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
20723
|
-
|
|
20813
|
+
writeFileSync25(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
20724
20814
|
`);
|
|
20725
20815
|
}
|
|
20726
20816
|
}
|
|
20727
|
-
|
|
20817
|
+
writeFileSync25(cfg.configFile, plan.text);
|
|
20728
20818
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
20729
20819
|
`);
|
|
20730
20820
|
} catch (e) {
|
|
@@ -21091,7 +21181,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21091
21181
|
process.exit(2);
|
|
21092
21182
|
}
|
|
21093
21183
|
if (found.shipNameAdopted && !dryRun) {
|
|
21094
|
-
|
|
21184
|
+
writeFileSync25(cfg.configFile, setShipNameInYaml(readFileSync33(cfg.configFile, "utf8"), found.shipNameAdopted));
|
|
21095
21185
|
cfg.ship.name = found.shipNameAdopted;
|
|
21096
21186
|
}
|
|
21097
21187
|
if (found.shipRow) {
|
|
@@ -21134,10 +21224,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21134
21224
|
}
|
|
21135
21225
|
}
|
|
21136
21226
|
let previous;
|
|
21137
|
-
const hadPreviousFile =
|
|
21227
|
+
const hadPreviousFile = existsSync37(resolvedPath);
|
|
21138
21228
|
if (hadPreviousFile) {
|
|
21139
21229
|
try {
|
|
21140
|
-
previous = JSON.parse(
|
|
21230
|
+
previous = JSON.parse(readFileSync33(resolvedPath, "utf8"));
|
|
21141
21231
|
} catch {
|
|
21142
21232
|
}
|
|
21143
21233
|
}
|
|
@@ -21266,10 +21356,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21266
21356
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
21267
21357
|
`);
|
|
21268
21358
|
} else {
|
|
21269
|
-
|
|
21359
|
+
mkdirSync24(dirname20(resolvedPath), { recursive: true });
|
|
21270
21360
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
21271
21361
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
21272
|
-
|
|
21362
|
+
writeFileSync25(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
21273
21363
|
`);
|
|
21274
21364
|
renameSync3(tmpPath, resolvedPath);
|
|
21275
21365
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -21315,8 +21405,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21315
21405
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
21316
21406
|
`);
|
|
21317
21407
|
} else {
|
|
21318
|
-
|
|
21319
|
-
|
|
21408
|
+
mkdirSync24(dirname20(apiKeyFile), { recursive: true });
|
|
21409
|
+
writeFileSync25(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
21320
21410
|
`, { mode: 384 });
|
|
21321
21411
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
21322
21412
|
`);
|
|
@@ -21866,7 +21956,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21866
21956
|
case "periscope": {
|
|
21867
21957
|
const subP = positional[1];
|
|
21868
21958
|
if (subP !== "up" && subP !== "start" && subP !== "down" && subP !== "end" && subP !== "list") {
|
|
21869
|
-
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");
|
|
21870
21960
|
process.stderr.write("crew periscope down [route] NNN --keep|--discard [--allow-empty] [--json]\n");
|
|
21871
21961
|
process.stderr.write("crew periscope list [route] [--json]\n");
|
|
21872
21962
|
process.exit(2);
|
|
@@ -21950,11 +22040,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21950
22040
|
},
|
|
21951
22041
|
readTicket: async (id) => {
|
|
21952
22042
|
const t = await trD.ticket(id);
|
|
21953
|
-
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 };
|
|
21954
22044
|
},
|
|
22045
|
+
priorHold: (id) => readPeriscopePrior(cfg.ship.stateDir, id),
|
|
22046
|
+
remoteHasBranch: (_wt, branch) => remoteBranchExists(repoD.dir, remote, branch),
|
|
21955
22047
|
updateTicket: async (id, p) => {
|
|
21956
22048
|
const patch2 = {};
|
|
21957
|
-
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") {
|
|
21958
22054
|
patch2[cD.columns.status] = "new";
|
|
21959
22055
|
patch2[cD.columns.assignee] = null;
|
|
21960
22056
|
if (p.description !== void 0) patch2.description = p.description;
|
|
@@ -21979,7 +22075,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21979
22075
|
if (deleteBranch) gitOk(repoD.dir, ["branch", "-D", branch]);
|
|
21980
22076
|
},
|
|
21981
22077
|
setRowState: (id, state2) => trD.updatePeriscope(id, { state: state2 }),
|
|
21982
|
-
forgetLocal: (id) =>
|
|
22078
|
+
forgetLocal: (id) => {
|
|
22079
|
+
removePeriscopeSidecar(cfg.ship.stateDir, id);
|
|
22080
|
+
removePeriscopePrior(cfg.ship.stateDir, id);
|
|
22081
|
+
},
|
|
21983
22082
|
say
|
|
21984
22083
|
};
|
|
21985
22084
|
if (dryRun) {
|
|
@@ -22024,7 +22123,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22024
22123
|
const shellR = hit && resolveShell(hit.repo.config.shell ?? cfg.ship.shell);
|
|
22025
22124
|
const resumed = await runPeriscopeResume({
|
|
22026
22125
|
find: async () => hit && { row: hit.row, tag: hit.tag },
|
|
22027
|
-
worktreeExists: (p) =>
|
|
22126
|
+
worktreeExists: (p) => existsSync37(p),
|
|
22028
22127
|
caps: periscopeCaps(cfg.ship),
|
|
22029
22128
|
runHook: async (name, cwd, env) => {
|
|
22030
22129
|
const script = hit?.repo.config.hooks[name];
|
|
@@ -22043,7 +22142,7 @@ ${resumed.url}
|
|
|
22043
22142
|
`);
|
|
22044
22143
|
break;
|
|
22045
22144
|
}
|
|
22046
|
-
const a = parsePeriscopeUpArgs(positional, { topic: value("topic"), host: value("host"), assignee: value("assignee"), isolate: flag("isolate"), json: jsonP });
|
|
22145
|
+
const a = parsePeriscopeUpArgs(positional, { topic: value("topic"), issue: value("issue"), host: value("host"), assignee: value("assignee"), isolate: flag("isolate"), json: jsonP });
|
|
22047
22146
|
const candidatesP = a.route ? [findRoute(cfg, a.route)] : cfg.routes.filter((r) => r.enabled);
|
|
22048
22147
|
const servedP = [];
|
|
22049
22148
|
for (const cand of candidatesP) {
|
|
@@ -22105,10 +22204,10 @@ ${resumed.url}
|
|
|
22105
22204
|
},
|
|
22106
22205
|
prepare: async (path) => {
|
|
22107
22206
|
for (const f of repoP.config.worktrees.copy) {
|
|
22108
|
-
const src =
|
|
22109
|
-
if (!
|
|
22110
|
-
|
|
22111
|
-
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));
|
|
22112
22211
|
}
|
|
22113
22212
|
const setup = repoP.config.hooks.setup;
|
|
22114
22213
|
return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
|
|
@@ -22132,12 +22231,58 @@ ${resumed.url}
|
|
|
22132
22231
|
const closed = cP.statuses.resolved.includes("closed_obsolete") ? "closed_obsolete" : cP.statuses.wontFix;
|
|
22133
22232
|
await trackerP.updateTicket(ticketId, { [cP.columns.status]: closed, [cP.columns.resolutionNote]: note });
|
|
22134
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
|
+
},
|
|
22135
22272
|
now: () => /* @__PURE__ */ new Date(),
|
|
22136
22273
|
host: a.host ?? "localhost",
|
|
22137
22274
|
say
|
|
22138
22275
|
};
|
|
22139
22276
|
if (dryRun) {
|
|
22140
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
|
+
}
|
|
22141
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)`);
|
|
22142
22287
|
break;
|
|
22143
22288
|
}
|
|
@@ -22332,7 +22477,7 @@ ${res.url}
|
|
|
22332
22477
|
};
|
|
22333
22478
|
const droppedRoutes = [];
|
|
22334
22479
|
try {
|
|
22335
|
-
const rawCfg = parseYaml2(
|
|
22480
|
+
const rawCfg = parseYaml2(readFileSync33(cfg.configFile, "utf8"));
|
|
22336
22481
|
for (const r of rawCfg?.routes ?? []) {
|
|
22337
22482
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
22338
22483
|
try {
|
|
@@ -22374,8 +22519,8 @@ ${res.url}
|
|
|
22374
22519
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
22375
22520
|
break;
|
|
22376
22521
|
}
|
|
22377
|
-
|
|
22378
|
-
|
|
22522
|
+
mkdirSync24(cfg.ship.stateDir, { recursive: true });
|
|
22523
|
+
writeFileSync25(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
22379
22524
|
`);
|
|
22380
22525
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
22381
22526
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
@@ -22426,7 +22571,7 @@ ${res.url}
|
|
|
22426
22571
|
}
|
|
22427
22572
|
case "log": {
|
|
22428
22573
|
try {
|
|
22429
|
-
const text =
|
|
22574
|
+
const text = readFileSync33(cfg.ship.logFile, "utf8").split("\n");
|
|
22430
22575
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
22431
22576
|
`);
|
|
22432
22577
|
} catch {
|
|
@@ -22449,7 +22594,7 @@ ${res.url}
|
|
|
22449
22594
|
};
|
|
22450
22595
|
if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
|
|
22451
22596
|
const tracker = new Tracker(route, cfg.ship);
|
|
22452
|
-
const configText =
|
|
22597
|
+
const configText = readFileSync33(cfg.configFile, "utf8");
|
|
22453
22598
|
const cfgBase = dirname20(cfg.configFile);
|
|
22454
22599
|
const configured = configuredRepos(configText, route.route, cfgBase);
|
|
22455
22600
|
const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
|
|
@@ -22478,7 +22623,7 @@ ${res.url}
|
|
|
22478
22623
|
}
|
|
22479
22624
|
if (configured.some((r) => r.dir === dir)) {
|
|
22480
22625
|
try {
|
|
22481
|
-
|
|
22626
|
+
writeFileSync25(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
|
|
22482
22627
|
} catch (e) {
|
|
22483
22628
|
if (e instanceof ReposError) fail(e.message);
|
|
22484
22629
|
throw e;
|
|
@@ -22486,29 +22631,29 @@ ${res.url}
|
|
|
22486
22631
|
}
|
|
22487
22632
|
if (row) {
|
|
22488
22633
|
await tracker.deleteRepoRow(row.id);
|
|
22489
|
-
if (
|
|
22490
|
-
const raw = JSON.parse(
|
|
22634
|
+
if (existsSync37(resolvedFile)) {
|
|
22635
|
+
const raw = JSON.parse(readFileSync33(resolvedFile, "utf8"));
|
|
22491
22636
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
22492
22637
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
22493
|
-
|
|
22638
|
+
writeFileSync25(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
22494
22639
|
`);
|
|
22495
22640
|
}
|
|
22496
22641
|
}
|
|
22497
|
-
if (
|
|
22642
|
+
if (existsSync37(join41(dir, ".git")) && basename12(dir) === SAMPLE_REPO) rmSync17(dir, { recursive: true, force: true });
|
|
22498
22643
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
22499
22644
|
break;
|
|
22500
22645
|
}
|
|
22501
22646
|
const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
22502
|
-
const fresh = !
|
|
22503
|
-
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`);
|
|
22504
22649
|
if (dryRun) {
|
|
22505
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`);
|
|
22506
22651
|
break;
|
|
22507
22652
|
}
|
|
22508
22653
|
if (fresh) {
|
|
22509
|
-
|
|
22510
|
-
|
|
22511
|
-
|
|
22654
|
+
mkdirSync24(dir, { recursive: true });
|
|
22655
|
+
writeFileSync25(join41(dir, "README.md"), SAMPLE_README);
|
|
22656
|
+
writeFileSync25(join41(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
22512
22657
|
const g = (...a) => {
|
|
22513
22658
|
const r = spawnSync7("git", a, { cwd: dir, encoding: "utf8" });
|
|
22514
22659
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -22526,17 +22671,17 @@ ${res.url}
|
|
|
22526
22671
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
22527
22672
|
});
|
|
22528
22673
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
22529
|
-
if (
|
|
22530
|
-
const raw = JSON.parse(
|
|
22674
|
+
if (existsSync37(resolvedFile)) {
|
|
22675
|
+
const raw = JSON.parse(readFileSync33(resolvedFile, "utf8"));
|
|
22531
22676
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
22532
22677
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
22533
|
-
|
|
22678
|
+
writeFileSync25(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
22534
22679
|
`);
|
|
22535
22680
|
}
|
|
22536
22681
|
}
|
|
22537
22682
|
if (!configured.some((r) => r.dir === dir)) {
|
|
22538
22683
|
try {
|
|
22539
|
-
|
|
22684
|
+
writeFileSync25(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
|
|
22540
22685
|
} catch (e) {
|
|
22541
22686
|
if (e instanceof ReposError) fail(e.message);
|
|
22542
22687
|
throw e;
|
|
@@ -22871,7 +23016,7 @@ watermark: ${state.watermark()}
|
|
|
22871
23016
|
}
|
|
22872
23017
|
for (const r of repos) {
|
|
22873
23018
|
const remote = r.config.branch.remote;
|
|
22874
|
-
if (
|
|
23019
|
+
if (existsSync37(r.dir)) {
|
|
22875
23020
|
pushAuth.push({
|
|
22876
23021
|
route: c.route,
|
|
22877
23022
|
repo: r.name,
|
|
@@ -22948,7 +23093,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
22948
23093
|
}
|
|
22949
23094
|
try {
|
|
22950
23095
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
22951
|
-
const evSize =
|
|
23096
|
+
const evSize = existsSync37(evFile) ? statSync11(evFile).size : 0;
|
|
22952
23097
|
process.stdout.write(
|
|
22953
23098
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
22954
23099
|
` : "events log: ok\n"
|
|
@@ -22959,7 +23104,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
22959
23104
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
22960
23105
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
22961
23106
|
try {
|
|
22962
|
-
return
|
|
23107
|
+
return readFileSync33(p, "utf8");
|
|
22963
23108
|
} catch {
|
|
22964
23109
|
return null;
|
|
22965
23110
|
}
|
package/package.json
CHANGED