@tablation/crew 0.95.0 → 0.97.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 +1390 -949
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2602,6 +2602,22 @@ function parseCloneScheme(v) {
|
|
|
2602
2602
|
if (v === "ssh" || v === "https") return v;
|
|
2603
2603
|
throw new ConfigError(`ship.cloneScheme must be "ssh" or "https", got ${JSON.stringify(v)}`);
|
|
2604
2604
|
}
|
|
2605
|
+
function parseShipEnv(v) {
|
|
2606
|
+
if (v === void 0 || v === null) return void 0;
|
|
2607
|
+
if (typeof v !== "object" || Array.isArray(v)) throw new ConfigError("ship.env must be a map of variable name to value");
|
|
2608
|
+
const out = {};
|
|
2609
|
+
for (const [k, val] of Object.entries(v)) {
|
|
2610
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) throw new ConfigError(`ship.env: "${k}" is not a valid variable name`);
|
|
2611
|
+
if (k === "PATH") throw new ConfigError("ship.env: PATH is set from ship.extraPath, not here");
|
|
2612
|
+
if (typeof val !== "string" && typeof val !== "number" && typeof val !== "boolean") {
|
|
2613
|
+
throw new ConfigError(`ship.env.${k} must be a string, number or boolean`);
|
|
2614
|
+
}
|
|
2615
|
+
const text = String(val);
|
|
2616
|
+
if (/[\r\n\0]/.test(text)) throw new ConfigError(`ship.env.${k} must be a single line`);
|
|
2617
|
+
out[k] = text;
|
|
2618
|
+
}
|
|
2619
|
+
return Object.keys(out).length ? out : void 0;
|
|
2620
|
+
}
|
|
2605
2621
|
function parseShipForges(v) {
|
|
2606
2622
|
try {
|
|
2607
2623
|
return parseForges(v);
|
|
@@ -2641,7 +2657,8 @@ var SHIP_KEYS = /* @__PURE__ */ new Set([
|
|
|
2641
2657
|
"previewHost",
|
|
2642
2658
|
"maxPeriscopes",
|
|
2643
2659
|
"periscopeIdleMinutes",
|
|
2644
|
-
"secretStore"
|
|
2660
|
+
"secretStore",
|
|
2661
|
+
"env"
|
|
2645
2662
|
]);
|
|
2646
2663
|
var ROUTE_KEYS = /* @__PURE__ */ new Set([
|
|
2647
2664
|
"route",
|
|
@@ -3065,7 +3082,8 @@ Create the first of those:
|
|
|
3065
3082
|
previewHost: shipRaw.previewHost ? String(shipRaw.previewHost).trim() || void 0 : void 0,
|
|
3066
3083
|
maxPeriscopes,
|
|
3067
3084
|
periscopeIdleMinutes,
|
|
3068
|
-
secretStore
|
|
3085
|
+
secretStore,
|
|
3086
|
+
env: parseShipEnv(shipRaw.env)
|
|
3069
3087
|
},
|
|
3070
3088
|
routes,
|
|
3071
3089
|
crewHome,
|
|
@@ -3717,7 +3735,7 @@ function recoveredFailureHeadlines(o, route2) {
|
|
|
3717
3735
|
|
|
3718
3736
|
// src/install.ts
|
|
3719
3737
|
import { dirname as dirname5, join as join13, extname } from "path";
|
|
3720
|
-
import { homedir as homedir7 } from "os";
|
|
3738
|
+
import { homedir as homedir7, userInfo } from "os";
|
|
3721
3739
|
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync11, mkdirSync as mkdirSync7, readdirSync as readdirSync3, readFileSync as readFileSync12, statSync as statSync4, writeFileSync as writeFileSync8, unlinkSync, realpathSync } from "fs";
|
|
3722
3740
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
3723
3741
|
import { createHash as createHash3 } from "crypto";
|
|
@@ -3785,6 +3803,30 @@ function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
|
|
|
3785
3803
|
const inShell = findOnPath("docker", shellPath);
|
|
3786
3804
|
return inShell ? `\`docker\` is not on the scheduler PATH (${schedulerPath}) but is at ${inShell} in your shell; add ${dirname5(inShell)} to ship.extraPath in crew.yaml and run \`crew install\`` : `\`docker\` is not on the scheduler PATH (${schedulerPath}) or your shell PATH; install Docker, then add its directory to ship.extraPath in crew.yaml and run \`crew install\``;
|
|
3787
3805
|
}
|
|
3806
|
+
var USER_INSTALL = { root: false, user: "" };
|
|
3807
|
+
function installContext(env = process.env) {
|
|
3808
|
+
const root = typeof process.getuid === "function" && process.getuid() === 0;
|
|
3809
|
+
const user = root && env.SUDO_USER || userInfo().username;
|
|
3810
|
+
return { root, user };
|
|
3811
|
+
}
|
|
3812
|
+
var SYSTEM_UNIT_DIR = "/etc/systemd/system";
|
|
3813
|
+
function systemUnitPathFor(label, suffix = "service") {
|
|
3814
|
+
return join13(SYSTEM_UNIT_DIR, `${label}.${suffix}`);
|
|
3815
|
+
}
|
|
3816
|
+
function unitEnvironment(ship) {
|
|
3817
|
+
return [["PATH", pathFor(ship)], ...Object.entries(ship.env ?? {})];
|
|
3818
|
+
}
|
|
3819
|
+
function systemdEnvLines(ship) {
|
|
3820
|
+
return unitEnvironment(ship).map(([k, v]) => {
|
|
3821
|
+
if (k === "PATH") return `Environment=PATH=${v}`;
|
|
3822
|
+
const escaped = `${k}=${v}`.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%");
|
|
3823
|
+
return `Environment="${escaped}"`;
|
|
3824
|
+
}).join("\n");
|
|
3825
|
+
}
|
|
3826
|
+
function shQuote(v) {
|
|
3827
|
+
if (/^[A-Za-z0-9_/.:,@%+=-]+$/.test(v)) return v;
|
|
3828
|
+
return `'${v.replace(/'/g, `'\\''`)}'`;
|
|
3829
|
+
}
|
|
3788
3830
|
function xmlEscape(s) {
|
|
3789
3831
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3790
3832
|
}
|
|
@@ -3817,8 +3859,8 @@ function planLaunchd(ship, crewHome, job) {
|
|
|
3817
3859
|
</array>
|
|
3818
3860
|
<key>EnvironmentVariables</key>
|
|
3819
3861
|
<dict>
|
|
3820
|
-
<key
|
|
3821
|
-
<string>${xmlEscape(
|
|
3862
|
+
${unitEnvironment(ship).map(([k, v]) => ` <key>${xmlEscape(k)}</key>
|
|
3863
|
+
<string>${xmlEscape(v)}</string>`).join("\n")}
|
|
3822
3864
|
</dict>
|
|
3823
3865
|
<key>WorkingDirectory</key>
|
|
3824
3866
|
<string>${xmlEscape(crewHome)}</string>
|
|
@@ -3842,9 +3884,17 @@ ${scheduleKeys}
|
|
|
3842
3884
|
schedulerLog
|
|
3843
3885
|
};
|
|
3844
3886
|
}
|
|
3845
|
-
function planSystemd(ship, crewHome, job) {
|
|
3887
|
+
function planSystemd(ship, crewHome, job, ctx) {
|
|
3846
3888
|
const label = labelFor(crewHome, job);
|
|
3847
|
-
const unitDir = join13(homedir7(), ".config", "systemd", "user");
|
|
3889
|
+
const unitDir = ctx.root ? SYSTEM_UNIT_DIR : join13(homedir7(), ".config", "systemd", "user");
|
|
3890
|
+
const ctl = ctx.root ? ["systemctl"] : ["systemctl", "--user"];
|
|
3891
|
+
const userLine = ctx.root && ctx.user && ctx.user !== "root" ? `User=${ctx.user}
|
|
3892
|
+
` : "";
|
|
3893
|
+
const wantedBy = ctx.root ? "multi-user.target" : "default.target";
|
|
3894
|
+
const linger = ctx.root || !ctx.user ? {} : {
|
|
3895
|
+
lingerCommand: ["loginctl", "enable-linger", ctx.user],
|
|
3896
|
+
lingerHint: `the daemon will stop when ${ctx.user} logs out; run \`sudo loginctl enable-linger ${ctx.user}\` once, or run \`crew install\` as root for a system unit`
|
|
3897
|
+
};
|
|
3848
3898
|
const servicePath = join13(unitDir, `${label}.service`);
|
|
3849
3899
|
const crewLog = ship.logFile;
|
|
3850
3900
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
@@ -3856,9 +3906,9 @@ Description=${description}
|
|
|
3856
3906
|
|
|
3857
3907
|
[Service]
|
|
3858
3908
|
Type=simple
|
|
3859
|
-
ExecStart=${[...crewInvocation(crewHome), ...subcommandFor(job, "systemd")].join(" ")}
|
|
3909
|
+
${userLine}ExecStart=${[...crewInvocation(crewHome), ...subcommandFor(job, "systemd")].join(" ")}
|
|
3860
3910
|
WorkingDirectory=${crewHome}
|
|
3861
|
-
|
|
3911
|
+
${systemdEnvLines(ship)}
|
|
3862
3912
|
StandardOutput=append:${schedulerLog}
|
|
3863
3913
|
StandardError=append:${schedulerLog}
|
|
3864
3914
|
Restart=on-failure
|
|
@@ -3868,7 +3918,7 @@ RestartSec=5
|
|
|
3868
3918
|
KillMode=process
|
|
3869
3919
|
|
|
3870
3920
|
[Install]
|
|
3871
|
-
WantedBy
|
|
3921
|
+
WantedBy=${wantedBy}
|
|
3872
3922
|
`;
|
|
3873
3923
|
return {
|
|
3874
3924
|
mechanism: "systemd",
|
|
@@ -3878,11 +3928,12 @@ WantedBy=default.target
|
|
|
3878
3928
|
cronMarker: null,
|
|
3879
3929
|
preLoadCommands: [],
|
|
3880
3930
|
loadCommands: [
|
|
3881
|
-
[
|
|
3882
|
-
[
|
|
3931
|
+
[...ctl, "daemon-reload"],
|
|
3932
|
+
[...ctl, "enable", "--now", `${label}.service`]
|
|
3883
3933
|
],
|
|
3884
3934
|
crewLog,
|
|
3885
|
-
schedulerLog
|
|
3935
|
+
schedulerLog,
|
|
3936
|
+
...linger
|
|
3886
3937
|
};
|
|
3887
3938
|
}
|
|
3888
3939
|
const timerPath = join13(unitDir, `${label}.timer`);
|
|
@@ -3892,9 +3943,9 @@ Description=${description}
|
|
|
3892
3943
|
|
|
3893
3944
|
[Service]
|
|
3894
3945
|
Type=oneshot
|
|
3895
|
-
ExecStart=${[...crewInvocation(crewHome), ...subcommandFor(job, "systemd")].join(" ")}
|
|
3946
|
+
${userLine}ExecStart=${[...crewInvocation(crewHome), ...subcommandFor(job, "systemd")].join(" ")}
|
|
3896
3947
|
WorkingDirectory=${crewHome}
|
|
3897
|
-
|
|
3948
|
+
${systemdEnvLines(ship)}
|
|
3898
3949
|
StandardOutput=append:${schedulerLog}
|
|
3899
3950
|
StandardError=append:${schedulerLog}
|
|
3900
3951
|
`;
|
|
@@ -3918,11 +3969,12 @@ WantedBy=timers.target
|
|
|
3918
3969
|
cronMarker: null,
|
|
3919
3970
|
preLoadCommands: [],
|
|
3920
3971
|
loadCommands: [
|
|
3921
|
-
[
|
|
3922
|
-
[
|
|
3972
|
+
[...ctl, "daemon-reload"],
|
|
3973
|
+
[...ctl, "enable", "--now", `${label}.timer`]
|
|
3923
3974
|
],
|
|
3924
3975
|
crewLog,
|
|
3925
|
-
schedulerLog
|
|
3976
|
+
schedulerLog,
|
|
3977
|
+
...linger
|
|
3926
3978
|
};
|
|
3927
3979
|
}
|
|
3928
3980
|
function planCron(ship, crewHome, job) {
|
|
@@ -3930,7 +3982,7 @@ function planCron(ship, crewHome, job) {
|
|
|
3930
3982
|
const crewLog = ship.logFile;
|
|
3931
3983
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
3932
3984
|
const minutes = Math.max(1, Math.round(INTERVAL_SECONDS / 60));
|
|
3933
|
-
const line = `*/${minutes} * * * * cd ${crewHome} &&
|
|
3985
|
+
const line = `*/${minutes} * * * * cd ${crewHome} && ${unitEnvironment(ship).map(([k, v]) => `${k}=${shQuote(v)}`).join(" ")} ${[...crewInvocation(crewHome), ...subcommandFor(job, "cron")].join(" ")} >> ${schedulerLog} 2>&1`;
|
|
3934
3986
|
return {
|
|
3935
3987
|
mechanism: "cron",
|
|
3936
3988
|
unitPaths: [],
|
|
@@ -3950,9 +4002,9 @@ function unitAbandonsAgents(plan, read) {
|
|
|
3950
4002
|
if (text === null) return null;
|
|
3951
4003
|
return plan.mechanism === "launchd" ? /<key>AbandonProcessGroup<\/key>\s*<true\/>/.test(text) : /^KillMode=process\s*$/m.test(text);
|
|
3952
4004
|
}
|
|
3953
|
-
function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
4005
|
+
function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run", ctx = USER_INSTALL) {
|
|
3954
4006
|
if (hostShip === "macos") return planLaunchd(ship, crewHome, job);
|
|
3955
|
-
if (hostShip === "linux" && hasSystemd) return planSystemd(ship, crewHome, job);
|
|
4007
|
+
if (hostShip === "linux" && hasSystemd) return planSystemd(ship, crewHome, job, ctx);
|
|
3956
4008
|
if (hostShip === "linux") return planCron(ship, crewHome, job);
|
|
3957
4009
|
throw new Error(`crew install: no scheduler support yet for ${hostShip} (Windows is planned, not built)`);
|
|
3958
4010
|
}
|
|
@@ -3969,20 +4021,22 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
3969
4021
|
};
|
|
3970
4022
|
}
|
|
3971
4023
|
if (hostShip === "linux" && hasSystemd) {
|
|
3972
|
-
const
|
|
4024
|
+
const system = existsSync11(systemUnitPathFor(label));
|
|
4025
|
+
const unitDir = system ? SYSTEM_UNIT_DIR : join13(homedir7(), ".config", "systemd", "user");
|
|
4026
|
+
const ctl = system ? ["systemctl"] : ["systemctl", "--user"];
|
|
3973
4027
|
if (job === "run") {
|
|
3974
4028
|
return {
|
|
3975
4029
|
mechanism: "systemd",
|
|
3976
4030
|
unitPaths: [join13(unitDir, `${label}.service`)],
|
|
3977
4031
|
crontabLine: null,
|
|
3978
|
-
unloadCommands: [[
|
|
4032
|
+
unloadCommands: [[...ctl, "disable", "--now", `${label}.service`]]
|
|
3979
4033
|
};
|
|
3980
4034
|
}
|
|
3981
4035
|
return {
|
|
3982
4036
|
mechanism: "systemd",
|
|
3983
4037
|
unitPaths: [join13(unitDir, `${label}.service`), join13(unitDir, `${label}.timer`)],
|
|
3984
4038
|
crontabLine: null,
|
|
3985
|
-
unloadCommands: [[
|
|
4039
|
+
unloadCommands: [[...ctl, "disable", "--now", `${label}.timer`]]
|
|
3986
4040
|
};
|
|
3987
4041
|
}
|
|
3988
4042
|
if (hostShip === "linux") {
|
|
@@ -4008,11 +4062,12 @@ function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
|
4008
4062
|
}
|
|
4009
4063
|
if (hostShip === "linux" && hasSystemd) {
|
|
4010
4064
|
const unit = `${label}.service`;
|
|
4065
|
+
const ctl = existsSync11(systemUnitPathFor(label)) ? ["systemctl"] : ["systemctl", "--user"];
|
|
4011
4066
|
return {
|
|
4012
4067
|
mechanism: "systemd",
|
|
4013
|
-
startCommand: [
|
|
4014
|
-
stopCommand: [
|
|
4015
|
-
statusCommand: [
|
|
4068
|
+
startCommand: [...ctl, "start", unit],
|
|
4069
|
+
stopCommand: [...ctl, "stop", unit],
|
|
4070
|
+
statusCommand: [...ctl, "status", unit]
|
|
4016
4071
|
};
|
|
4017
4072
|
}
|
|
4018
4073
|
if (hostShip === "linux") {
|
|
@@ -4033,11 +4088,14 @@ function runAll(commands, cwd, log) {
|
|
|
4033
4088
|
return true;
|
|
4034
4089
|
}
|
|
4035
4090
|
async function applyInstall(plan, crewHome, dryRun, log) {
|
|
4091
|
+
let changed2 = plan.crontabLine !== null;
|
|
4036
4092
|
for (const path of plan.unitPaths) {
|
|
4037
4093
|
if (dryRun) {
|
|
4038
4094
|
log.emit(`would write ${path}`);
|
|
4039
4095
|
continue;
|
|
4040
4096
|
}
|
|
4097
|
+
const before = existsSync11(path) ? readFileSync12(path, "utf8") : null;
|
|
4098
|
+
if (before !== plan.unitContent[path]) changed2 = true;
|
|
4041
4099
|
mkdirSync7(dirname5(path), { recursive: true });
|
|
4042
4100
|
writeFileSync8(path, plan.unitContent[path]);
|
|
4043
4101
|
log.emit(`wrote ${path}`);
|
|
@@ -4067,8 +4125,18 @@ async function applyInstall(plan, crewHome, dryRun, log) {
|
|
|
4067
4125
|
log.emit(`loaded (${plan.mechanism}), firing every ${INTERVAL_SECONDS}s starting ${INTERVAL_SECONDS}s from now`);
|
|
4068
4126
|
}
|
|
4069
4127
|
}
|
|
4128
|
+
if (plan.lingerCommand) {
|
|
4129
|
+
if (dryRun) {
|
|
4130
|
+
log.emit(`would run: ${plan.lingerCommand.join(" ")}`);
|
|
4131
|
+
} else if (runAll([plan.lingerCommand], crewHome, { emit: log.emit.bind(log), warn: () => void 0 })) {
|
|
4132
|
+
log.emit(`lingering enabled for ${plan.lingerCommand[2]}: the unit survives logout`);
|
|
4133
|
+
} else {
|
|
4134
|
+
log.warn(`could not enable lingering \u2014 ${plan.lingerHint ?? "run loginctl enable-linger by hand"}`);
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4070
4137
|
log.emit(`crew's own log: ${plan.crewLog}`);
|
|
4071
4138
|
log.emit(`scheduler log (only what escapes the crew, e.g. a crash before it starts): ${plan.schedulerLog}`);
|
|
4139
|
+
return { changed: changed2 };
|
|
4072
4140
|
}
|
|
4073
4141
|
async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
4074
4142
|
if (plan.mechanism === "none") {
|
|
@@ -7210,6 +7278,18 @@ var Tracker = class {
|
|
|
7210
7278
|
if (!row) return;
|
|
7211
7279
|
await this.client.records.update(this.route.resolved.shipsModelId, row.id, patch2);
|
|
7212
7280
|
}
|
|
7281
|
+
/**
|
|
7282
|
+
* Writes (or, with `null`, clears) this ship's `relay_url` (CREW-2803). A
|
|
7283
|
+
* no-op when there is no row for this ship or the Ships table has not taken
|
|
7284
|
+
* the column (probed by the key's absence from the row), so the tunnel
|
|
7285
|
+
* still runs on an install that predates it.
|
|
7286
|
+
*/
|
|
7287
|
+
async updateRelayUrl(shipName2, url) {
|
|
7288
|
+
const row = await this.myShipRow(shipName2);
|
|
7289
|
+
if (!row || !("relay_url" in row)) return;
|
|
7290
|
+
if ((row.relay_url ?? null) === url) return;
|
|
7291
|
+
await this.client.records.update(this.route.resolved.shipsModelId, row.id, { relay_url: url });
|
|
7292
|
+
}
|
|
7213
7293
|
/** The other half of `beatEngaged` — clears engagement back to idle. */
|
|
7214
7294
|
async beatIdle(shipName2) {
|
|
7215
7295
|
const row = await this.myShipRow(shipName2);
|
|
@@ -10465,8 +10545,8 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
10465
10545
|
// src/cli.ts
|
|
10466
10546
|
import { resolve as resolve5, dirname as dirname22, join as join44, basename as basename13 } from "path";
|
|
10467
10547
|
import { fileURLToPath } from "url";
|
|
10468
|
-
import { hostname as
|
|
10469
|
-
import { spawnSync as
|
|
10548
|
+
import { hostname as hostname3, homedir as homedir10 } from "os";
|
|
10549
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
10470
10550
|
import { parse as parseYaml2 } from "yaml";
|
|
10471
10551
|
|
|
10472
10552
|
// src/argv.ts
|
|
@@ -10478,6 +10558,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
10478
10558
|
"clear",
|
|
10479
10559
|
"dir",
|
|
10480
10560
|
"extra-path",
|
|
10561
|
+
"forge-host",
|
|
10481
10562
|
"harness",
|
|
10482
10563
|
"hook",
|
|
10483
10564
|
"host",
|
|
@@ -12718,191 +12799,776 @@ function flagWizardAnswers(f, env) {
|
|
|
12718
12799
|
};
|
|
12719
12800
|
}
|
|
12720
12801
|
|
|
12721
|
-
// src/
|
|
12722
|
-
import {
|
|
12723
|
-
import {
|
|
12724
|
-
import {
|
|
12802
|
+
// src/provision.ts
|
|
12803
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
12804
|
+
import { existsSync as existsSync25, readFileSync as readFileSync23, writeFileSync as writeFileSync19 } from "fs";
|
|
12805
|
+
import { hostname as hostname2 } from "os";
|
|
12806
|
+
import { parseDocument as parseDocument3 } from "yaml";
|
|
12725
12807
|
|
|
12726
|
-
// src/
|
|
12727
|
-
|
|
12728
|
-
|
|
12729
|
-
|
|
12730
|
-
|
|
12731
|
-
|
|
12732
|
-
|
|
12733
|
-
|
|
12734
|
-
|
|
12735
|
-
|
|
12736
|
-
|
|
12737
|
-
|
|
12738
|
-
|
|
12739
|
-
const
|
|
12740
|
-
if (
|
|
12741
|
-
|
|
12742
|
-
|
|
12743
|
-
|
|
12744
|
-
|
|
12745
|
-
|
|
12746
|
-
const elsewhere = stranded.filter((u) => u.reason === "not-served-here");
|
|
12747
|
-
if (elsewhere.length) {
|
|
12748
|
-
const byRepoName = /* @__PURE__ */ new Map();
|
|
12749
|
-
for (const u of elsewhere) {
|
|
12750
|
-
const name = u.repo ?? "?";
|
|
12751
|
-
byRepoName.set(name, [...byRepoName.get(name) ?? [], u.ticket.issue_id]);
|
|
12752
|
-
}
|
|
12753
|
-
notes.push({
|
|
12754
|
-
level: "info",
|
|
12755
|
-
message: `${elsewhere.length} verified ticket(s) belong to a repository this ship has no checkout for \u2014 another ship releases them: ` + [...byRepoName].map(([n, ids]) => `${n} (${ids.join(", ")})`).join("; ")
|
|
12756
|
-
});
|
|
12757
|
-
}
|
|
12758
|
-
return notes;
|
|
12808
|
+
// src/doctor-fix.ts
|
|
12809
|
+
import { parseDocument as parseDocument2 } from "yaml";
|
|
12810
|
+
|
|
12811
|
+
// src/repos-cmd.ts
|
|
12812
|
+
import { homedir as homedir9 } from "os";
|
|
12813
|
+
import { basename as basename8, join as join29, resolve as resolve3 } from "path";
|
|
12814
|
+
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
12815
|
+
var ReposError = class extends Error {
|
|
12816
|
+
};
|
|
12817
|
+
function normalizeRemote(url) {
|
|
12818
|
+
if (!url) return void 0;
|
|
12819
|
+
let s = url.trim();
|
|
12820
|
+
if (!s) return void 0;
|
|
12821
|
+
const scp = /^[^\s@/]+@[^\s:/]+:(.+)$/.exec(s);
|
|
12822
|
+
if (scp) s = scp[1];
|
|
12823
|
+
else s = s.replace(/^[a-z][a-z0-9+.-]*:\/\/(?:[^/@]+@)?[^/]+\//i, "");
|
|
12824
|
+
s = s.replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
12825
|
+
const parts = s.split("/").filter(Boolean);
|
|
12826
|
+
if (parts.length < 2) return void 0;
|
|
12827
|
+
return parts.slice(-2).join("/").toLowerCase();
|
|
12759
12828
|
}
|
|
12760
|
-
|
|
12761
|
-
|
|
12762
|
-
if (
|
|
12763
|
-
|
|
12764
|
-
|
|
12765
|
-
if (note.level === "warn") emit.warn(note.message, { step: "release" });
|
|
12766
|
-
else emit.emit(note.message, { step: "release" });
|
|
12829
|
+
function matchRepoName(origin, resolved) {
|
|
12830
|
+
const want = normalizeRemote(origin);
|
|
12831
|
+
if (!want || !resolved?.repoRemotes) return void 0;
|
|
12832
|
+
for (const [id, remote] of Object.entries(resolved.repoRemotes)) {
|
|
12833
|
+
if (normalizeRemote(remote) === want) return resolved.repoNames?.[id];
|
|
12767
12834
|
}
|
|
12835
|
+
return void 0;
|
|
12768
12836
|
}
|
|
12769
|
-
function
|
|
12770
|
-
const
|
|
12771
|
-
if (
|
|
12772
|
-
|
|
12837
|
+
function routeNamesOf(text) {
|
|
12838
|
+
const routes = parseDocument(text).get("routes", true);
|
|
12839
|
+
if (!isSeq(routes)) return [];
|
|
12840
|
+
return routes.items.flatMap((i) => isMap(i) && typeof i.get("route") === "string" ? [i.get("route")] : []);
|
|
12841
|
+
}
|
|
12842
|
+
function parseAddArgs(args, routes, isDir) {
|
|
12843
|
+
const rest = [...args];
|
|
12844
|
+
let route2;
|
|
12845
|
+
if (rest.length > 0 && routes.includes(rest[0])) route2 = rest.shift();
|
|
12846
|
+
else if (rest.length > 0 && /^[^/\s]+\/[^/\s]+$/.test(rest[0]) && !isDir(rest[0])) {
|
|
12847
|
+
throw new ReposError(`no route "${rest[0]}" in the config (have: ${routes.join(", ") || "none"})`);
|
|
12773
12848
|
}
|
|
12774
|
-
|
|
12775
|
-
|
|
12776
|
-
|
|
12777
|
-
|
|
12778
|
-
|
|
12779
|
-
|
|
12780
|
-
return {
|
|
12781
|
-
|
|
12782
|
-
detail: `working tree has ${changes.length} change(s)${untrackedNote}: ${names}${more}`
|
|
12783
|
-
};
|
|
12849
|
+
if (!route2) {
|
|
12850
|
+
if (routes.length === 1) route2 = routes[0];
|
|
12851
|
+
else throw new ReposError(routes.length === 0 ? "the config has no routes \u2014 run `crew connect` first" : `several routes are configured; name one: ${routes.join(", ")}`);
|
|
12852
|
+
}
|
|
12853
|
+
if (rest.length > 2) throw new ReposError("too many arguments: expected [route] [name] [path]");
|
|
12854
|
+
if (rest.length === 2) return { route: route2, name: rest[0], path: rest[1] };
|
|
12855
|
+
if (rest.length === 1) return isDir(rest[0]) ? { route: route2, path: rest[0] } : { route: route2, name: rest[0], path: "." };
|
|
12856
|
+
return { route: route2, path: "." };
|
|
12784
12857
|
}
|
|
12785
|
-
function
|
|
12786
|
-
|
|
12787
|
-
return
|
|
12858
|
+
function expandDir(p, base) {
|
|
12859
|
+
const home = p === "~" ? homedir9() : p.startsWith("~/") ? `${homedir9()}/${p.slice(2)}` : p;
|
|
12860
|
+
return resolve3(base, home);
|
|
12788
12861
|
}
|
|
12789
|
-
|
|
12790
|
-
|
|
12791
|
-
|
|
12792
|
-
|
|
12793
|
-
|
|
12794
|
-
const seen = /* @__PURE__ */ new Set();
|
|
12795
|
-
const out = [];
|
|
12796
|
-
for (const raw of entries) {
|
|
12797
|
-
let line = raw.trim();
|
|
12798
|
-
const ref = line.match(TICKET_REF_AT_START);
|
|
12799
|
-
let key = line;
|
|
12800
|
-
if (ref) {
|
|
12801
|
-
const prefix = ref[1];
|
|
12802
|
-
const num = ref[2];
|
|
12803
|
-
const canonical = own && own[2] === num ? own[1] : prefix;
|
|
12804
|
-
line = `${canonical}-${num}${line.slice(ref[0].length)}`;
|
|
12805
|
-
key = `${canonical}-${num}`;
|
|
12862
|
+
function routeMapOf(doc, route2) {
|
|
12863
|
+
const routes = doc.get("routes", true);
|
|
12864
|
+
if (isSeq(routes)) {
|
|
12865
|
+
for (const item of routes.items) {
|
|
12866
|
+
if (isMap(item) && item.get("route") === route2) return item;
|
|
12806
12867
|
}
|
|
12807
|
-
if (seen.has(key)) continue;
|
|
12808
|
-
seen.add(key);
|
|
12809
|
-
out.push(line);
|
|
12810
12868
|
}
|
|
12811
|
-
|
|
12812
|
-
}
|
|
12813
|
-
var BUMP_LINE = /^Bump:[ \t]*(major|minor|patch)\b/gim;
|
|
12814
|
-
function planMerge(cwd, tickets, contract, lastReleased, branchFor = (t) => branchForIssue(cwd, t.issue_id), base = "main", blockerInfo, ref = "HEAD") {
|
|
12815
|
-
const held = new Set(heldByBlockers(tickets, contract.statuses.verified, blockerInfo).map((h) => h.ticket.id));
|
|
12816
|
-
const verified = tickets.filter((t) => t.status === contract.statuses.verified && !held.has(t.id)).sort((a, b) => Number.parseInt(a.issue_id.replace(/^\D+/, ""), 10) - Number.parseInt(b.issue_id.replace(/^\D+/, ""), 10));
|
|
12817
|
-
return verified.map((ticket) => {
|
|
12818
|
-
const branch = branchFor(ticket);
|
|
12819
|
-
if (!branch) {
|
|
12820
|
-
const keys = referenceKeys(ticket);
|
|
12821
|
-
const unreleased = lastReleased ? findKeyInRange(cwd, keys, `${lastReleased}..${ref}`) : null;
|
|
12822
|
-
if (unreleased) {
|
|
12823
|
-
return {
|
|
12824
|
-
ticket,
|
|
12825
|
-
branch: null,
|
|
12826
|
-
entries: [],
|
|
12827
|
-
usedFallback: false,
|
|
12828
|
-
bump: "patch",
|
|
12829
|
-
majorRequested: false,
|
|
12830
|
-
skipReason: "already-merged"
|
|
12831
|
-
};
|
|
12832
|
-
}
|
|
12833
|
-
const anywhere = findKeyInRange(cwd, keys, ref);
|
|
12834
|
-
if (anywhere) {
|
|
12835
|
-
return {
|
|
12836
|
-
ticket,
|
|
12837
|
-
branch: null,
|
|
12838
|
-
entries: [],
|
|
12839
|
-
usedFallback: false,
|
|
12840
|
-
bump: "patch",
|
|
12841
|
-
majorRequested: false,
|
|
12842
|
-
skipReason: lastReleased ? "already-released" : "already-merged",
|
|
12843
|
-
mergedSha: anywhere
|
|
12844
|
-
};
|
|
12845
|
-
}
|
|
12846
|
-
return {
|
|
12847
|
-
ticket,
|
|
12848
|
-
branch: null,
|
|
12849
|
-
entries: [],
|
|
12850
|
-
usedFallback: false,
|
|
12851
|
-
bump: "patch",
|
|
12852
|
-
majorRequested: false,
|
|
12853
|
-
skipReason: "never-built"
|
|
12854
|
-
};
|
|
12855
|
-
}
|
|
12856
|
-
const squash = findSquashOfBranch(cwd, referenceKeys(ticket), branch, base, base);
|
|
12857
|
-
if (squash) {
|
|
12858
|
-
const released = !!lastReleased && isAncestor(cwd, squash, lastReleased);
|
|
12859
|
-
return {
|
|
12860
|
-
ticket,
|
|
12861
|
-
branch,
|
|
12862
|
-
entries: [],
|
|
12863
|
-
usedFallback: false,
|
|
12864
|
-
bump: "patch",
|
|
12865
|
-
majorRequested: false,
|
|
12866
|
-
skipReason: released ? "already-released" : "already-merged",
|
|
12867
|
-
mergedSha: squash
|
|
12868
|
-
};
|
|
12869
|
-
}
|
|
12870
|
-
const bodies = commitBodies(cwd, `${base}..${branch}`);
|
|
12871
|
-
const entries = dedupeChangelogEntries(
|
|
12872
|
-
[...bodies.matchAll(CHANGELOG_LINE)].map((m) => m[1].trim()).filter(Boolean),
|
|
12873
|
-
ticket
|
|
12874
|
-
);
|
|
12875
|
-
const usedFallback = entries.length === 0;
|
|
12876
|
-
const { size, majorRequested } = requestedBump(bodies);
|
|
12877
|
-
return {
|
|
12878
|
-
ticket,
|
|
12879
|
-
branch,
|
|
12880
|
-
entries: usedFallback ? [`${ticket.title || ticket.issue_id} (${ticket.issue_id})`] : entries,
|
|
12881
|
-
usedFallback,
|
|
12882
|
-
bump: size,
|
|
12883
|
-
majorRequested
|
|
12884
|
-
};
|
|
12885
|
-
});
|
|
12869
|
+
const have = isSeq(routes) ? routes.items.map((i) => isMap(i) ? String(i.get("route")) : "?").join(", ") : "none";
|
|
12870
|
+
throw new ReposError(`no route "${route2}" in the config (have: ${have})`);
|
|
12886
12871
|
}
|
|
12887
|
-
function
|
|
12888
|
-
const
|
|
12889
|
-
const
|
|
12890
|
-
|
|
12891
|
-
|
|
12892
|
-
|
|
12893
|
-
|
|
12894
|
-
|
|
12895
|
-
|
|
12896
|
-
|
|
12897
|
-
|
|
12872
|
+
function configuredRepos(text, route2, base) {
|
|
12873
|
+
const doc = parseDocument(text);
|
|
12874
|
+
const m = routeMapOf(doc, route2);
|
|
12875
|
+
const repos = m.get("repos");
|
|
12876
|
+
if (isMap(repos)) {
|
|
12877
|
+
return repos.items.map((p) => {
|
|
12878
|
+
const name = String(p.key.value ?? p.key);
|
|
12879
|
+
const v = p.value;
|
|
12880
|
+
const dir2 = isMap(v) ? String(v.get("dir") ?? "") : String(v?.value ?? "");
|
|
12881
|
+
return { name, dir: expandDir(dir2, base) };
|
|
12882
|
+
});
|
|
12898
12883
|
}
|
|
12899
|
-
|
|
12884
|
+
const dir = m.get("dir");
|
|
12885
|
+
return dir ? [{ name: basename8(String(dir)), dir: expandDir(String(dir), base) }] : [];
|
|
12900
12886
|
}
|
|
12901
|
-
function
|
|
12902
|
-
const
|
|
12903
|
-
const
|
|
12904
|
-
|
|
12905
|
-
|
|
12887
|
+
function cloneBaseOf(text, route2, base, isCheckout) {
|
|
12888
|
+
const m = routeMapOf(parseDocument(text), route2);
|
|
12889
|
+
const repos = m.get("repos", true);
|
|
12890
|
+
if (isMap(repos) && repos.items.length > 0) return null;
|
|
12891
|
+
const dir = m.get("dir");
|
|
12892
|
+
if (!dir) return null;
|
|
12893
|
+
const abs = expandDir(String(dir), base);
|
|
12894
|
+
return isCheckout(abs) ? null : abs;
|
|
12895
|
+
}
|
|
12896
|
+
function routeSetsHooks(text, route2, name) {
|
|
12897
|
+
const m = routeMapOf(parseDocument(text), route2);
|
|
12898
|
+
const repos = m.get("repos", true);
|
|
12899
|
+
let target = m;
|
|
12900
|
+
if (isMap(repos)) target = repos.get(name, true);
|
|
12901
|
+
if (!isMap(target)) return false;
|
|
12902
|
+
const hooks = target.get("hooks", true);
|
|
12903
|
+
return isMap(hooks) && hooks.items.length > 0;
|
|
12904
|
+
}
|
|
12905
|
+
function listRepoRows(i) {
|
|
12906
|
+
const cloneBase = i.isCheckout ? cloneBaseOf(i.text, i.route, i.base, i.isCheckout) : null;
|
|
12907
|
+
const checkouts = configuredRepos(i.text, i.route, i.base).filter((c) => c.dir !== cloneBase);
|
|
12908
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
12909
|
+
const hooksSourceOf = (c) => i.exists(c.dir) && i.repoHasHooks(c.dir) ? "repo" : routeSetsHooks(i.text, i.route, c.name) ? "route" : "none";
|
|
12910
|
+
const originFor = (c, remote) => {
|
|
12911
|
+
const o = i.originOf(c.dir) ?? null;
|
|
12912
|
+
if (!o) return null;
|
|
12913
|
+
return remote !== null && normalizeRemote(o) === normalizeRemote(remote) ? null : o;
|
|
12914
|
+
};
|
|
12915
|
+
const rows = [];
|
|
12916
|
+
const names = i.resolved?.repoNames ?? {};
|
|
12917
|
+
const remotes = i.resolved?.repoRemotes ?? {};
|
|
12918
|
+
for (const [id, name] of Object.entries(names)) {
|
|
12919
|
+
const remote = remotes[id] ?? null;
|
|
12920
|
+
const want = normalizeRemote(remote);
|
|
12921
|
+
let idx = checkouts.findIndex((c2, n) => !claimed.has(n) && want !== void 0 && normalizeRemote(i.originOf(c2.dir)) === want);
|
|
12922
|
+
if (idx < 0) idx = checkouts.findIndex((c2, n) => !claimed.has(n) && c2.name === name);
|
|
12923
|
+
const c = idx >= 0 ? checkouts[idx] : void 0;
|
|
12924
|
+
if (idx >= 0) claimed.add(idx);
|
|
12925
|
+
rows.push({
|
|
12926
|
+
name,
|
|
12927
|
+
remote,
|
|
12928
|
+
repoRowId: id,
|
|
12929
|
+
attached: c !== void 0,
|
|
12930
|
+
dir: c?.dir ?? null,
|
|
12931
|
+
exists: c ? i.exists(c.dir) : false,
|
|
12932
|
+
origin: c ? originFor(c, remote) : null,
|
|
12933
|
+
matchedRow: c ? name : null,
|
|
12934
|
+
hooksSource: c ? hooksSourceOf(c) : "none"
|
|
12935
|
+
});
|
|
12936
|
+
}
|
|
12937
|
+
checkouts.forEach((c, n) => {
|
|
12938
|
+
if (claimed.has(n)) return;
|
|
12939
|
+
rows.push({
|
|
12940
|
+
name: c.name,
|
|
12941
|
+
remote: null,
|
|
12942
|
+
repoRowId: null,
|
|
12943
|
+
attached: true,
|
|
12944
|
+
dir: c.dir,
|
|
12945
|
+
exists: i.exists(c.dir),
|
|
12946
|
+
origin: originFor(c, null),
|
|
12947
|
+
matchedRow: null,
|
|
12948
|
+
hooksSource: hooksSourceOf(c)
|
|
12949
|
+
});
|
|
12950
|
+
});
|
|
12951
|
+
return rows;
|
|
12952
|
+
}
|
|
12953
|
+
function planRepoAdd(i) {
|
|
12954
|
+
const doc = parseDocument(i.text);
|
|
12955
|
+
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
12956
|
+
const m = routeMapOf(doc, i.route);
|
|
12957
|
+
let existing = configuredRepos(i.text, i.route, i.base);
|
|
12958
|
+
const target = expandDir(i.path, i.base);
|
|
12959
|
+
let droppedDir;
|
|
12960
|
+
const placeholder = m.get("dir");
|
|
12961
|
+
const reposNow = m.get("repos", true);
|
|
12962
|
+
if (i.isCheckout && placeholder && !(isMap(reposNow) && reposNow.items.length > 0)) {
|
|
12963
|
+
const placeholderDir = expandDir(String(placeholder), i.base);
|
|
12964
|
+
if (placeholderDir !== target && !i.isCheckout(placeholderDir)) {
|
|
12965
|
+
droppedDir = placeholderDir;
|
|
12966
|
+
m.delete("dir");
|
|
12967
|
+
existing = [];
|
|
12968
|
+
}
|
|
12969
|
+
}
|
|
12970
|
+
if (existing.some((e) => e.dir === target)) {
|
|
12971
|
+
throw new ReposError(`${target} is already configured on route ${i.route}`);
|
|
12972
|
+
}
|
|
12973
|
+
const matched = matchRepoName(i.origin, i.resolved);
|
|
12974
|
+
const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename8(target);
|
|
12975
|
+
let warning;
|
|
12976
|
+
if (!matched) {
|
|
12977
|
+
const known = Object.values(i.resolved?.repoNames ?? {});
|
|
12978
|
+
warning = i.origin ? `the tracker has no Repos row for remote ${normalizeRemote(i.origin) ?? i.origin}${known.length ? ` (known: ${known.join(", ")})` : ""} \u2014 added anyway; create the row so ticket routing can find it` : "this checkout has no origin remote, so it cannot be matched to a Repos row \u2014 added anyway";
|
|
12979
|
+
}
|
|
12980
|
+
const repos = m.get("repos", true);
|
|
12981
|
+
let mode;
|
|
12982
|
+
if (isMap(repos)) {
|
|
12983
|
+
if (repos.has(name)) throw new ReposError(`route ${i.route} already has a repo named "${name}" (${String(repos.get(name))})`);
|
|
12984
|
+
repos.set(name, target);
|
|
12985
|
+
mode = "repos";
|
|
12986
|
+
} else if (m.has("dir") && existing.length > 0) {
|
|
12987
|
+
const old = existing[0];
|
|
12988
|
+
const oldName = i.nameForExisting?.(old.dir) ?? old.name;
|
|
12989
|
+
if (oldName === name) throw new ReposError(`route ${i.route} already has a repo named "${name}" (${old.dir})`);
|
|
12990
|
+
const oldDir = String(m.get("dir"));
|
|
12991
|
+
const pair = m.items.find((p) => p.key.value === "dir");
|
|
12992
|
+
pair.key = doc.createNode("repos");
|
|
12993
|
+
pair.value = doc.createNode({ [oldName]: oldDir, [name]: target });
|
|
12994
|
+
mode = "repos";
|
|
12995
|
+
} else if (droppedDir) {
|
|
12996
|
+
m.set("repos", doc.createNode({ [name]: target }));
|
|
12997
|
+
mode = "repos";
|
|
12998
|
+
} else {
|
|
12999
|
+
m.set("dir", target);
|
|
13000
|
+
mode = "dir";
|
|
13001
|
+
}
|
|
13002
|
+
return { text: String(doc), mode, name, block: renderBlock(m), warning, unmatched: !matched, droppedDir };
|
|
13003
|
+
}
|
|
13004
|
+
function planRepoRemove(text, base, route2, path) {
|
|
13005
|
+
const doc = parseDocument(text);
|
|
13006
|
+
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
13007
|
+
const m = routeMapOf(doc, route2);
|
|
13008
|
+
const target = expandDir(path, base);
|
|
13009
|
+
const existing = configuredRepos(text, route2, base);
|
|
13010
|
+
const hit = existing.find((e) => e.dir === target);
|
|
13011
|
+
if (!hit) throw new ReposError(`${target} is not configured on route ${route2}`);
|
|
13012
|
+
const repos = m.get("repos", true);
|
|
13013
|
+
if (isMap(repos)) {
|
|
13014
|
+
repos.delete(hit.name);
|
|
13015
|
+
const left = existing.filter((e) => e !== hit);
|
|
13016
|
+
if (left.length === 1) {
|
|
13017
|
+
const pair = m.items.find((p) => p.key.value === "repos");
|
|
13018
|
+
pair.key = doc.createNode("dir");
|
|
13019
|
+
pair.value = doc.createNode(String(repos.get(left[0].name)));
|
|
13020
|
+
} else if (left.length === 0) m.delete("repos");
|
|
13021
|
+
} else {
|
|
13022
|
+
m.delete("dir");
|
|
13023
|
+
}
|
|
13024
|
+
return { text: String(doc), name: hit.name };
|
|
13025
|
+
}
|
|
13026
|
+
function renderBlock(m) {
|
|
13027
|
+
return stringify2([m.toJSON()]).trimEnd();
|
|
13028
|
+
}
|
|
13029
|
+
var LOCAL_RELEASE_MODES = /* @__PURE__ */ new Set(["integrate", "external"]);
|
|
13030
|
+
function planRepoRow(i) {
|
|
13031
|
+
const remote = normalizeRemote(i.origin);
|
|
13032
|
+
if (!remote) throw new ReposError(`cannot derive an owner/repo from origin "${i.origin}"`);
|
|
13033
|
+
const mode = i.spec?.releaseMode;
|
|
13034
|
+
return {
|
|
13035
|
+
name: i.name,
|
|
13036
|
+
remote,
|
|
13037
|
+
platform: i.spec?.platform ?? "unix",
|
|
13038
|
+
release_mode: mode ? LOCAL_RELEASE_MODES.has(mode) ? "local" : mode : "local",
|
|
13039
|
+
ci_provider: i.spec?.ciProvider ?? "none",
|
|
13040
|
+
enabled: true,
|
|
13041
|
+
project_id: i.projectId
|
|
13042
|
+
};
|
|
13043
|
+
}
|
|
13044
|
+
function resolveRepoProject(projects, opt, routeAreaId) {
|
|
13045
|
+
if (opt) {
|
|
13046
|
+
const want = opt.toLowerCase();
|
|
13047
|
+
const hit = projects.find((p) => p.id === opt || p.name?.toLowerCase() === want);
|
|
13048
|
+
if (!hit) throw new ReposError(`no Projects row "${opt}" (have: ${projects.map((p) => p.name ?? p.id).join(", ") || "none"})`);
|
|
13049
|
+
return hit.id;
|
|
13050
|
+
}
|
|
13051
|
+
if (routeAreaId) return routeAreaId;
|
|
13052
|
+
if (projects.length === 1) return projects[0].id;
|
|
13053
|
+
throw new ReposError(
|
|
13054
|
+
`cannot tell which project the new Repos row belongs to \u2014 pass --project <name> (have: ${projects.map((p) => p.name ?? p.id).join(", ") || "none"}), or --no-create to skip creating it`
|
|
13055
|
+
);
|
|
13056
|
+
}
|
|
13057
|
+
function parseCloneArgs(args, routes) {
|
|
13058
|
+
const rest = [...args];
|
|
13059
|
+
let route2;
|
|
13060
|
+
if (rest.length > 1 && routes.includes(rest[0])) route2 = rest.shift();
|
|
13061
|
+
else if (rest.length > 1) throw new ReposError(`no route "${rest[0]}" in the config (have: ${routes.join(", ") || "none"})`);
|
|
13062
|
+
if (!route2) {
|
|
13063
|
+
if (routes.length === 1) route2 = routes[0];
|
|
13064
|
+
else throw new ReposError(routes.length === 0 ? "the config has no routes \u2014 run `crew connect` first" : `several routes are configured; name one: ${routes.join(", ")}`);
|
|
13065
|
+
}
|
|
13066
|
+
if (rest.length !== 1) throw new ReposError("expected [route] <name> \u2014 the Repos row to clone (see `crew repos list`)");
|
|
13067
|
+
return { route: route2, name: rest[0] };
|
|
13068
|
+
}
|
|
13069
|
+
function planRepoClone(i) {
|
|
13070
|
+
const names = i.resolved?.repoNames ?? {};
|
|
13071
|
+
const id = Object.keys(names).find((k) => names[k].toLowerCase() === i.name.toLowerCase());
|
|
13072
|
+
if (!id) {
|
|
13073
|
+
const known = Object.values(names);
|
|
13074
|
+
throw new ReposError(`the tracker has no Repos row named "${i.name}" for route ${i.route}${known.length ? ` (known: ${known.join(", ")})` : " \u2014 run `crew connect` to refresh them"}`);
|
|
13075
|
+
}
|
|
13076
|
+
const name = names[id];
|
|
13077
|
+
const remote = normalizeRemote(i.resolved?.repoRemotes?.[id]);
|
|
13078
|
+
if (!remote) {
|
|
13079
|
+
throw new ReposError(
|
|
13080
|
+
`the Repos row "${name}" has no remote to clone from \u2014 attach an existing checkout with \`crew repos add ${i.route} ${name} <path>\`, or set the row's remote in the tracker and re-run \`crew connect\``
|
|
13081
|
+
);
|
|
13082
|
+
}
|
|
13083
|
+
const doc = parseDocument(i.text);
|
|
13084
|
+
const m = routeMapOf(doc, i.route);
|
|
13085
|
+
const existing = configuredRepos(i.text, i.route, i.base);
|
|
13086
|
+
if (existing.some((e) => e.name.toLowerCase() === name.toLowerCase())) {
|
|
13087
|
+
throw new ReposError(`route ${i.route} already has a checkout named "${name}" \u2014 nothing to clone`);
|
|
13088
|
+
}
|
|
13089
|
+
const ship = doc.get("ship", true);
|
|
13090
|
+
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
13091
|
+
const target = i.dir ? expandDir(i.dir, i.base) : join29(expandDir(basePath, i.base), i.route.split("/")[0], name);
|
|
13092
|
+
const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name, isCheckout: i.isCheckout });
|
|
13093
|
+
return { ...add, remote, target };
|
|
13094
|
+
}
|
|
13095
|
+
|
|
13096
|
+
// src/doctor-fix.ts
|
|
13097
|
+
var sound = (r) => r.hasRepos && r.problems.length === 0;
|
|
13098
|
+
function planFix(f) {
|
|
13099
|
+
const actions = [];
|
|
13100
|
+
const notes = [];
|
|
13101
|
+
for (const r of f.routes) {
|
|
13102
|
+
if (!r.enabled && sound(r)) {
|
|
13103
|
+
actions.push({ kind: "enable", route: r.route });
|
|
13104
|
+
const gaps = r.readiness ?? [];
|
|
13105
|
+
if (gaps.length) {
|
|
13106
|
+
notes.push(`${r.route}: ${gaps.length} release-readiness gap${gaps.length === 1 ? "" : "s"} (${gaps[0]}${gaps.length > 1 ? ", ..." : ""}) \u2014 the route will run, but releases stop at the first missing hook until it is added`);
|
|
13107
|
+
}
|
|
13108
|
+
} else if (!sound(r) && !r.enabled) {
|
|
13109
|
+
notes.push(`${r.route}: not enabled \u2014 ${r.hasRepos ? r.problems[0] : "no checkout is configured"}`);
|
|
13110
|
+
}
|
|
13111
|
+
}
|
|
13112
|
+
if (f.schedulerAbsent && f.routes.some(sound)) {
|
|
13113
|
+
if (f.host === "windows") notes.push("scheduler support is not built for Windows yet \u2014 nothing to install; run `crew run` by hand");
|
|
13114
|
+
else actions.push({ kind: "install" });
|
|
13115
|
+
}
|
|
13116
|
+
return { actions, notes };
|
|
13117
|
+
}
|
|
13118
|
+
async function gatherHealth(c, check) {
|
|
13119
|
+
let problems;
|
|
13120
|
+
let readiness = [];
|
|
13121
|
+
try {
|
|
13122
|
+
const r = await check();
|
|
13123
|
+
if (Array.isArray(r)) problems = r;
|
|
13124
|
+
else ({ problems, readiness } = r);
|
|
13125
|
+
} catch (e) {
|
|
13126
|
+
problems = [`checks could not run: ${e instanceof Error ? e.message : String(e)}`];
|
|
13127
|
+
}
|
|
13128
|
+
return { route: c.route, enabled: c.enabled, hasRepos: c.hasRepos, problems, readiness };
|
|
13129
|
+
}
|
|
13130
|
+
function routesInScope(routes, named) {
|
|
13131
|
+
return named ? routes.filter((r) => r.route === named) : routes;
|
|
13132
|
+
}
|
|
13133
|
+
function planEnable(text, route2) {
|
|
13134
|
+
const doc = parseDocument2(text);
|
|
13135
|
+
routeMapOf(doc, route2).set("enabled", true);
|
|
13136
|
+
return String(doc);
|
|
13137
|
+
}
|
|
13138
|
+
function schedulerInstalled(plan, exists, crontab) {
|
|
13139
|
+
if (plan.mechanism === "cron") return plan.cronMarker !== null && crontab.includes(plan.cronMarker);
|
|
13140
|
+
return plan.unitPaths.length > 0 && plan.unitPaths.every(exists);
|
|
13141
|
+
}
|
|
13142
|
+
|
|
13143
|
+
// src/provision.ts
|
|
13144
|
+
var PROVISION_EXIT = { ok: 0, failed: 1, problem: 2, choice: 3 };
|
|
13145
|
+
function parseEvents(stdout) {
|
|
13146
|
+
const out = [];
|
|
13147
|
+
for (const line of stdout.split("\n")) {
|
|
13148
|
+
const t = line.trim();
|
|
13149
|
+
if (!t.startsWith("{")) continue;
|
|
13150
|
+
try {
|
|
13151
|
+
out.push(JSON.parse(t));
|
|
13152
|
+
} catch {
|
|
13153
|
+
}
|
|
13154
|
+
}
|
|
13155
|
+
return out;
|
|
13156
|
+
}
|
|
13157
|
+
async function runProvision(o, deps, write, human) {
|
|
13158
|
+
const summary = { ok: false, state: "failed", changed: false, routes: [], forges: [] };
|
|
13159
|
+
const emit = (event, message, data = {}) => {
|
|
13160
|
+
if (o.json) write(`${JSON.stringify({ ...data, event, message })}
|
|
13161
|
+
`);
|
|
13162
|
+
else human(`${message}
|
|
13163
|
+
`);
|
|
13164
|
+
};
|
|
13165
|
+
const fail = (code, message, data = {}) => {
|
|
13166
|
+
emit("problem", message, data);
|
|
13167
|
+
return { code, summary: { ...summary, ok: false, state: "failed" } };
|
|
13168
|
+
};
|
|
13169
|
+
if (!o.key) return fail(PROVISION_EXIT.problem, "no connect key: pass --key or set CREW_CONNECT_KEY", { stage: "input" });
|
|
13170
|
+
if (o.routes.length === 0) {
|
|
13171
|
+
return fail(PROVISION_EXIT.problem, "no routes to serve: pass --route WORKSPACE[/PROJECT] (repeatable) or set CREW_PROVISION_ROUTES", { stage: "input" });
|
|
13172
|
+
}
|
|
13173
|
+
for (const host of o.forgeHosts) {
|
|
13174
|
+
if (!o.tokens[host]) return fail(PROVISION_EXIT.problem, `no token for forge ${host}: set CREW_FORGE_TOKEN or CREW_FORGE_TOKEN_${envKey(host)}`, { stage: "input" });
|
|
13175
|
+
}
|
|
13176
|
+
const shipName2 = o.shipName ?? deps.defaultShipName();
|
|
13177
|
+
summary.ship = shipName2;
|
|
13178
|
+
const routeNames = [];
|
|
13179
|
+
for (const route2 of o.routes) {
|
|
13180
|
+
const args = [
|
|
13181
|
+
"connect",
|
|
13182
|
+
route2,
|
|
13183
|
+
"--json",
|
|
13184
|
+
"--no-wizard",
|
|
13185
|
+
"--ship-name",
|
|
13186
|
+
shipName2,
|
|
13187
|
+
...o.agentBin ? ["--agent-bin", o.agentBin] : [],
|
|
13188
|
+
...o.extraPath ? ["--extra-path", o.extraPath] : [],
|
|
13189
|
+
...o.baseUrl ? ["--base-url", o.baseUrl] : [],
|
|
13190
|
+
...o.dryRun ? ["--dry-run"] : [],
|
|
13191
|
+
...o.choices.flatMap((c) => ["--choose", c])
|
|
13192
|
+
];
|
|
13193
|
+
const res = deps.crew(args, { env: { CREW_CONNECT_KEY: o.key } });
|
|
13194
|
+
const events = parseEvents(res.stdout);
|
|
13195
|
+
for (const e of events) if (o.json) write(`${JSON.stringify({ ...e, phase: "connect" })}
|
|
13196
|
+
`);
|
|
13197
|
+
if (res.status !== 0) {
|
|
13198
|
+
const problem = events.find((e) => e.event === "problem");
|
|
13199
|
+
const msg = String(problem?.message ?? (res.stderr.trim().split("\n").pop() || `crew connect ${route2} exited ${res.status}`));
|
|
13200
|
+
if (!o.json) human(`crew provision: connect ${route2}: ${msg}
|
|
13201
|
+
`);
|
|
13202
|
+
return { code: res.status === PROVISION_EXIT.choice ? PROVISION_EXIT.choice : res.status, summary: { ...summary, ok: false, state: "failed" } };
|
|
13203
|
+
}
|
|
13204
|
+
const done = events.find((e) => e.event === "done");
|
|
13205
|
+
const name = String(done?.route ?? route2);
|
|
13206
|
+
routeNames.push(name);
|
|
13207
|
+
const shipRow = events.find((e) => e.event === "ship_row");
|
|
13208
|
+
if (shipRow?.action === "created") summary.changed = true;
|
|
13209
|
+
if (done?.wroteConfig === true) summary.changed = true;
|
|
13210
|
+
emit("connect", `connected ${name}${shipRow ? ` (ship row ${String(shipRow.action)})` : ""}`, { route: name });
|
|
13211
|
+
}
|
|
13212
|
+
summary.routes = routeNames;
|
|
13213
|
+
if (o.dryRun) {
|
|
13214
|
+
summary.ok = true;
|
|
13215
|
+
summary.state = "dry-run";
|
|
13216
|
+
emit("done", "dry run: connected nothing past discovery; no forge, route, unit or daemon was touched", { summary });
|
|
13217
|
+
return { code: PROVISION_EXIT.ok, summary };
|
|
13218
|
+
}
|
|
13219
|
+
const configPath = deps.configFile();
|
|
13220
|
+
if (!configPath) return fail(PROVISION_EXIT.failed, "connect finished but no crew.yaml exists", { stage: "connect" });
|
|
13221
|
+
summary.configPath = configPath;
|
|
13222
|
+
for (const host of o.forgeHosts) {
|
|
13223
|
+
const token = o.tokens[host];
|
|
13224
|
+
const stored = deps.storedToken(host);
|
|
13225
|
+
if (stored === token) {
|
|
13226
|
+
summary.forges.push({ host, action: "unchanged" });
|
|
13227
|
+
emit("forge", `forge ${host}: token already stored`, { host, action: "unchanged" });
|
|
13228
|
+
continue;
|
|
13229
|
+
}
|
|
13230
|
+
const sub = stored === null ? "set" : "rotate";
|
|
13231
|
+
const res = deps.crew(["forge", sub, host, "--token-stdin"], { input: `${token}
|
|
13232
|
+
` });
|
|
13233
|
+
if (res.status !== 0) return fail(PROVISION_EXIT.failed, `forge ${host}: ${res.stderr.trim() || `crew forge ${sub} exited ${res.status}`}`, { stage: "forge", host });
|
|
13234
|
+
const action = sub === "set" ? "stored" : "rotated";
|
|
13235
|
+
summary.forges.push({ host, action });
|
|
13236
|
+
summary.changed = true;
|
|
13237
|
+
emit("forge", `forge ${host}: token ${action}`, { host, action });
|
|
13238
|
+
}
|
|
13239
|
+
for (const route2 of routeNames) {
|
|
13240
|
+
if (deps.enableRoute(route2)) {
|
|
13241
|
+
summary.changed = true;
|
|
13242
|
+
emit("route_enabled", `enabled route ${route2}`, { route: route2 });
|
|
13243
|
+
}
|
|
13244
|
+
}
|
|
13245
|
+
const before = deps.installState();
|
|
13246
|
+
if (before.unitsStale || !before.running || !before.lingering) {
|
|
13247
|
+
const res = deps.crew(["install"]);
|
|
13248
|
+
if (res.status !== 0) {
|
|
13249
|
+
return fail(PROVISION_EXIT.failed, `crew install exited ${res.status}: ${(res.stderr || res.stdout).trim().split("\n").pop() ?? ""}`, { stage: "install" });
|
|
13250
|
+
}
|
|
13251
|
+
summary.changed = true;
|
|
13252
|
+
emit("install", `installed ${before.mechanism} units`, { mechanism: before.mechanism });
|
|
13253
|
+
}
|
|
13254
|
+
const after = deps.installState();
|
|
13255
|
+
summary.daemon = { mechanism: after.mechanism, running: after.running, lingering: after.lingering };
|
|
13256
|
+
if (!after.running) {
|
|
13257
|
+
return fail(PROVISION_EXIT.failed, `the ${after.mechanism} daemon unit is not running after install`, { stage: "daemon" });
|
|
13258
|
+
}
|
|
13259
|
+
emit("daemon", `daemon running (${after.mechanism}${after.lingering ? ", survives logout" : ", will stop at logout: lingering is off"})`, { ...summary.daemon });
|
|
13260
|
+
summary.ok = true;
|
|
13261
|
+
summary.state = summary.changed ? "provisioned" : "already provisioned";
|
|
13262
|
+
emit("done", summary.state, { summary });
|
|
13263
|
+
return { code: PROVISION_EXIT.ok, summary };
|
|
13264
|
+
}
|
|
13265
|
+
function envKey(host) {
|
|
13266
|
+
return host.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
13267
|
+
}
|
|
13268
|
+
function tokensFromEnv(hosts, env) {
|
|
13269
|
+
const out = {};
|
|
13270
|
+
for (const h of hosts) {
|
|
13271
|
+
const t = env[`CREW_FORGE_TOKEN_${envKey(h)}`] ?? env.CREW_FORGE_TOKEN;
|
|
13272
|
+
if (t?.trim()) out[h] = t.trim();
|
|
13273
|
+
}
|
|
13274
|
+
return out;
|
|
13275
|
+
}
|
|
13276
|
+
function repeated(argv2, name) {
|
|
13277
|
+
const out = [];
|
|
13278
|
+
for (let i = 0; i < argv2.length; i++) {
|
|
13279
|
+
const a = argv2[i];
|
|
13280
|
+
if (a === `--${name}`) {
|
|
13281
|
+
const v = argv2[++i];
|
|
13282
|
+
if (v !== void 0) out.push(v);
|
|
13283
|
+
} else if (a.startsWith(`--${name}=`)) out.push(a.slice(name.length + 3));
|
|
13284
|
+
}
|
|
13285
|
+
return out;
|
|
13286
|
+
}
|
|
13287
|
+
function splitList(v) {
|
|
13288
|
+
return (v ?? "").split(/[\s,]+/).filter(Boolean);
|
|
13289
|
+
}
|
|
13290
|
+
function provisionOptionsFrom(argv2, env) {
|
|
13291
|
+
const one = (name) => repeated(argv2, name).at(-1);
|
|
13292
|
+
const routes = repeated(argv2, "route");
|
|
13293
|
+
const forgeFlags = repeated(argv2, "forge-host");
|
|
13294
|
+
const tokenAvailable = !!env.CREW_FORGE_TOKEN?.trim() || Object.keys(env).some((k) => k.startsWith("CREW_FORGE_TOKEN_") && env[k]?.trim());
|
|
13295
|
+
const forgeHosts = (forgeFlags.length ? forgeFlags : tokenAvailable ? splitList(env.CREW_FORGE_HOSTS ?? "github.com") : []).map((h) => h.toLowerCase());
|
|
13296
|
+
return {
|
|
13297
|
+
key: one("key") ?? env.CREW_CONNECT_KEY,
|
|
13298
|
+
routes: routes.length ? routes : splitList(env.CREW_PROVISION_ROUTES),
|
|
13299
|
+
shipName: one("ship-name") ?? env.CREW_SHIP_NAME,
|
|
13300
|
+
agentBin: one("agent-bin"),
|
|
13301
|
+
extraPath: one("extra-path"),
|
|
13302
|
+
baseUrl: one("base-url"),
|
|
13303
|
+
forgeHosts,
|
|
13304
|
+
tokens: tokensFromEnv(forgeHosts, env),
|
|
13305
|
+
json: argv2.includes("--json"),
|
|
13306
|
+
dryRun: argv2.includes("--dry-run"),
|
|
13307
|
+
choices: repeated(argv2, "choose")
|
|
13308
|
+
};
|
|
13309
|
+
}
|
|
13310
|
+
function realProvisionDeps(crewHome) {
|
|
13311
|
+
const spawnCrew = (args, o = {}) => {
|
|
13312
|
+
const [bin, ...pre] = crewInvocation(crewHome);
|
|
13313
|
+
const env = { ...process.env, ...o.env };
|
|
13314
|
+
delete env.CREW_FORGE_TOKEN;
|
|
13315
|
+
const res = spawnSync6(bin, [...pre, ...args], { cwd: crewHome, encoding: "utf8", env, input: o.input, maxBuffer: 64 * 1024 * 1024 });
|
|
13316
|
+
return { status: res.status ?? 1, stdout: res.stdout ?? "", stderr: `${res.stderr ?? ""}${res.error ? String(res.error) : ""}` };
|
|
13317
|
+
};
|
|
13318
|
+
const configFile = () => {
|
|
13319
|
+
const f = findConfigFile(crewHome);
|
|
13320
|
+
return existsSync25(f) ? f : null;
|
|
13321
|
+
};
|
|
13322
|
+
return {
|
|
13323
|
+
crew: spawnCrew,
|
|
13324
|
+
configFile,
|
|
13325
|
+
enableRoute(route2) {
|
|
13326
|
+
const file = configFile();
|
|
13327
|
+
if (!file) return false;
|
|
13328
|
+
const text = readFileSync23(file, "utf8");
|
|
13329
|
+
const doc = parseDocument3(text);
|
|
13330
|
+
const routes = doc.get("routes");
|
|
13331
|
+
const entry = routes?.items?.find((r) => r.get("route") === route2);
|
|
13332
|
+
if (!entry) throw new Error(`route ${route2} is not in ${file}`);
|
|
13333
|
+
if (entry.get("enabled") === true) return false;
|
|
13334
|
+
writeFileSync19(file, planEnable(text, route2));
|
|
13335
|
+
return true;
|
|
13336
|
+
},
|
|
13337
|
+
installState() {
|
|
13338
|
+
const file = configFile();
|
|
13339
|
+
const cfg = loadConfig(crewHome, file ?? void 0, { allowNoRoutes: true });
|
|
13340
|
+
const host = hostPlatform();
|
|
13341
|
+
const ctx = installContext();
|
|
13342
|
+
const systemd = detectSystemd();
|
|
13343
|
+
const jobs = ["run", "release", "passengers"];
|
|
13344
|
+
let unitsStale = false;
|
|
13345
|
+
let mechanism = "none";
|
|
13346
|
+
for (const job of jobs) {
|
|
13347
|
+
const plan = planInstall(cfg.ship, crewHome, host, systemd, job, ctx);
|
|
13348
|
+
mechanism = plan.mechanism;
|
|
13349
|
+
if (plan.mechanism === "cron") {
|
|
13350
|
+
unitsStale = true;
|
|
13351
|
+
continue;
|
|
13352
|
+
}
|
|
13353
|
+
for (const p of plan.unitPaths) {
|
|
13354
|
+
if (!existsSync25(p) || readFileSync23(p, "utf8") !== plan.unitContent[p]) unitsStale = true;
|
|
13355
|
+
}
|
|
13356
|
+
}
|
|
13357
|
+
const label = labelFor(crewHome, "run");
|
|
13358
|
+
let running = false;
|
|
13359
|
+
let lingering = true;
|
|
13360
|
+
if (mechanism === "systemd") {
|
|
13361
|
+
const system = ctx.root || existsSync25(systemUnitPathFor(label));
|
|
13362
|
+
const ctl = system ? ["systemctl"] : ["systemctl", "--user"];
|
|
13363
|
+
running = spawnSync6(ctl[0], [...ctl.slice(1), "is-active", "--quiet", `${label}.service`]).status === 0;
|
|
13364
|
+
if (!system) {
|
|
13365
|
+
const out = spawnSync6("loginctl", ["show-user", ctx.user, "-p", "Linger"], { encoding: "utf8" });
|
|
13366
|
+
lingering = /Linger=yes/.test(out.stdout ?? "");
|
|
13367
|
+
}
|
|
13368
|
+
} else if (mechanism === "cron") {
|
|
13369
|
+
running = true;
|
|
13370
|
+
} else if (mechanism === "launchd") {
|
|
13371
|
+
running = spawnSync6("launchctl", ["list", label]).status === 0;
|
|
13372
|
+
}
|
|
13373
|
+
return { mechanism, unitsStale, running, lingering };
|
|
13374
|
+
},
|
|
13375
|
+
storedToken(host) {
|
|
13376
|
+
const res = spawnCrew(["credential", "get"], { input: `protocol=https
|
|
13377
|
+
host=${host}
|
|
13378
|
+
|
|
13379
|
+
` });
|
|
13380
|
+
const m = /^password=(.*)$/m.exec(res.stdout);
|
|
13381
|
+
return m ? m[1] : null;
|
|
13382
|
+
},
|
|
13383
|
+
defaultShipName: () => hostname2()
|
|
13384
|
+
};
|
|
13385
|
+
}
|
|
13386
|
+
|
|
13387
|
+
// src/release-run.ts
|
|
13388
|
+
import { readFileSync as readFileSync24, writeFileSync as writeFileSync20, existsSync as existsSync26, copyFileSync, mkdirSync as mkdirSync17, mkdtempSync as mkdtempSync2, rmSync as rmSync14 } from "fs";
|
|
13389
|
+
import { dirname as dirname14, join as join30 } from "path";
|
|
13390
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
13391
|
+
|
|
13392
|
+
// src/release.ts
|
|
13393
|
+
function describeUnplaceable(unplaceable, verifiedStatus) {
|
|
13394
|
+
const stranded = unplaceable.filter((u) => u.ticket.status === verifiedStatus);
|
|
13395
|
+
if (stranded.length === 0) return [];
|
|
13396
|
+
const notes = [];
|
|
13397
|
+
const keysFor = (r) => stranded.filter((u) => u.reason === r).map((u) => u.ticket.issue_id);
|
|
13398
|
+
const noRepo = keysFor("no-repo");
|
|
13399
|
+
if (noRepo.length) {
|
|
13400
|
+
notes.push({
|
|
13401
|
+
level: "warn",
|
|
13402
|
+
message: `${noRepo.length} verified ticket(s) name no repository and cannot be released: ` + noRepo.join(", ")
|
|
13403
|
+
});
|
|
13404
|
+
}
|
|
13405
|
+
const unknown = keysFor("unknown-repo");
|
|
13406
|
+
if (unknown.length) {
|
|
13407
|
+
notes.push({
|
|
13408
|
+
level: "warn",
|
|
13409
|
+
message: `${unknown.length} verified ticket(s) name a repository this ship does not recognise \u2014 run \`crew connect\` to refresh its repo list: ${unknown.join(", ")}`
|
|
13410
|
+
});
|
|
13411
|
+
}
|
|
13412
|
+
const elsewhere = stranded.filter((u) => u.reason === "not-served-here");
|
|
13413
|
+
if (elsewhere.length) {
|
|
13414
|
+
const byRepoName = /* @__PURE__ */ new Map();
|
|
13415
|
+
for (const u of elsewhere) {
|
|
13416
|
+
const name = u.repo ?? "?";
|
|
13417
|
+
byRepoName.set(name, [...byRepoName.get(name) ?? [], u.ticket.issue_id]);
|
|
13418
|
+
}
|
|
13419
|
+
notes.push({
|
|
13420
|
+
level: "info",
|
|
13421
|
+
message: `${elsewhere.length} verified ticket(s) belong to a repository this ship has no checkout for \u2014 another ship releases them: ` + [...byRepoName].map(([n, ids]) => `${n} (${ids.join(", ")})`).join("; ")
|
|
13422
|
+
});
|
|
13423
|
+
}
|
|
13424
|
+
return notes;
|
|
13425
|
+
}
|
|
13426
|
+
var unplaceableReported = /* @__PURE__ */ new Set();
|
|
13427
|
+
function reportUnplaceable(routeName, unplaceable, verified, emit) {
|
|
13428
|
+
if (unplaceableReported.has(routeName)) return;
|
|
13429
|
+
unplaceableReported.add(routeName);
|
|
13430
|
+
for (const note of describeUnplaceable(unplaceable, verified)) {
|
|
13431
|
+
if (note.level === "warn") emit.warn(note.message, { step: "release" });
|
|
13432
|
+
else emit.emit(note.message, { step: "release" });
|
|
13433
|
+
}
|
|
13434
|
+
}
|
|
13435
|
+
function checkGuards(cwd, base = "main") {
|
|
13436
|
+
const branch = currentBranch(cwd);
|
|
13437
|
+
if (branch !== base) {
|
|
13438
|
+
return { kind: "branch", detail: `primary checkout is on '${branch}', not ${base}` };
|
|
13439
|
+
}
|
|
13440
|
+
const changes = status(cwd);
|
|
13441
|
+
if (changes.length === 0) return null;
|
|
13442
|
+
const untracked = changes.filter((c) => c.untracked).length;
|
|
13443
|
+
const names = changes.slice(0, 3).map((c) => c.path).join(", ");
|
|
13444
|
+
const more = changes.length > 3 ? `, +${changes.length - 3} more` : "";
|
|
13445
|
+
const untrackedNote = untracked > 0 ? ` (${untracked} untracked)` : "";
|
|
13446
|
+
return {
|
|
13447
|
+
kind: "dirty",
|
|
13448
|
+
detail: `working tree has ${changes.length} change(s)${untrackedNote}: ${names}${more}`
|
|
13449
|
+
};
|
|
13450
|
+
}
|
|
13451
|
+
function heldByBlockers(tickets, verifiedStatus, info) {
|
|
13452
|
+
if (!info) return [];
|
|
13453
|
+
return tickets.filter((t) => t.status === verifiedStatus && unresolvedBlockers(t, info, { mergeGated: false }).length > 0).map((ticket) => ({ ticket, blockers: describeUnresolved(ticket, info, { mergeGated: false }) }));
|
|
13454
|
+
}
|
|
13455
|
+
var describeHeld = (held) => held.map((h) => `${h.ticket.issue_tag ?? h.ticket.issue_id} held: blocked by ${h.blockers}`);
|
|
13456
|
+
var CHANGELOG_LINE = /^Changelog:[ \t]*(.*)$/gm;
|
|
13457
|
+
var TICKET_REF_AT_START = /^([A-Z][A-Z0-9]*)-(\d+)\b/;
|
|
13458
|
+
function dedupeChangelogEntries(entries, ticket) {
|
|
13459
|
+
const own = (ticket.issue_tag ?? ticket.issue_id).match(/^([A-Z][A-Z0-9]*)-(\d+)$/);
|
|
13460
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13461
|
+
const out = [];
|
|
13462
|
+
for (const raw of entries) {
|
|
13463
|
+
let line = raw.trim();
|
|
13464
|
+
const ref = line.match(TICKET_REF_AT_START);
|
|
13465
|
+
let key = line;
|
|
13466
|
+
if (ref) {
|
|
13467
|
+
const prefix = ref[1];
|
|
13468
|
+
const num = ref[2];
|
|
13469
|
+
const canonical = own && own[2] === num ? own[1] : prefix;
|
|
13470
|
+
line = `${canonical}-${num}${line.slice(ref[0].length)}`;
|
|
13471
|
+
key = `${canonical}-${num}`;
|
|
13472
|
+
}
|
|
13473
|
+
if (seen.has(key)) continue;
|
|
13474
|
+
seen.add(key);
|
|
13475
|
+
out.push(line);
|
|
13476
|
+
}
|
|
13477
|
+
return out;
|
|
13478
|
+
}
|
|
13479
|
+
var BUMP_LINE = /^Bump:[ \t]*(major|minor|patch)\b/gim;
|
|
13480
|
+
function planMerge(cwd, tickets, contract, lastReleased, branchFor = (t) => branchForIssue(cwd, t.issue_id), base = "main", blockerInfo, ref = "HEAD") {
|
|
13481
|
+
const held = new Set(heldByBlockers(tickets, contract.statuses.verified, blockerInfo).map((h) => h.ticket.id));
|
|
13482
|
+
const verified = tickets.filter((t) => t.status === contract.statuses.verified && !held.has(t.id)).sort((a, b) => Number.parseInt(a.issue_id.replace(/^\D+/, ""), 10) - Number.parseInt(b.issue_id.replace(/^\D+/, ""), 10));
|
|
13483
|
+
return verified.map((ticket) => {
|
|
13484
|
+
const branch = branchFor(ticket);
|
|
13485
|
+
if (!branch) {
|
|
13486
|
+
const keys = referenceKeys(ticket);
|
|
13487
|
+
const unreleased = lastReleased ? findKeyInRange(cwd, keys, `${lastReleased}..${ref}`) : null;
|
|
13488
|
+
if (unreleased) {
|
|
13489
|
+
return {
|
|
13490
|
+
ticket,
|
|
13491
|
+
branch: null,
|
|
13492
|
+
entries: [],
|
|
13493
|
+
usedFallback: false,
|
|
13494
|
+
bump: "patch",
|
|
13495
|
+
majorRequested: false,
|
|
13496
|
+
skipReason: "already-merged"
|
|
13497
|
+
};
|
|
13498
|
+
}
|
|
13499
|
+
const anywhere = findKeyInRange(cwd, keys, ref);
|
|
13500
|
+
if (anywhere) {
|
|
13501
|
+
return {
|
|
13502
|
+
ticket,
|
|
13503
|
+
branch: null,
|
|
13504
|
+
entries: [],
|
|
13505
|
+
usedFallback: false,
|
|
13506
|
+
bump: "patch",
|
|
13507
|
+
majorRequested: false,
|
|
13508
|
+
skipReason: lastReleased ? "already-released" : "already-merged",
|
|
13509
|
+
mergedSha: anywhere
|
|
13510
|
+
};
|
|
13511
|
+
}
|
|
13512
|
+
return {
|
|
13513
|
+
ticket,
|
|
13514
|
+
branch: null,
|
|
13515
|
+
entries: [],
|
|
13516
|
+
usedFallback: false,
|
|
13517
|
+
bump: "patch",
|
|
13518
|
+
majorRequested: false,
|
|
13519
|
+
skipReason: "never-built"
|
|
13520
|
+
};
|
|
13521
|
+
}
|
|
13522
|
+
const squash = findSquashOfBranch(cwd, referenceKeys(ticket), branch, base, base);
|
|
13523
|
+
if (squash) {
|
|
13524
|
+
const released = !!lastReleased && isAncestor(cwd, squash, lastReleased);
|
|
13525
|
+
return {
|
|
13526
|
+
ticket,
|
|
13527
|
+
branch,
|
|
13528
|
+
entries: [],
|
|
13529
|
+
usedFallback: false,
|
|
13530
|
+
bump: "patch",
|
|
13531
|
+
majorRequested: false,
|
|
13532
|
+
skipReason: released ? "already-released" : "already-merged",
|
|
13533
|
+
mergedSha: squash
|
|
13534
|
+
};
|
|
13535
|
+
}
|
|
13536
|
+
const bodies = commitBodies(cwd, `${base}..${branch}`);
|
|
13537
|
+
const entries = dedupeChangelogEntries(
|
|
13538
|
+
[...bodies.matchAll(CHANGELOG_LINE)].map((m) => m[1].trim()).filter(Boolean),
|
|
13539
|
+
ticket
|
|
13540
|
+
);
|
|
13541
|
+
const usedFallback = entries.length === 0;
|
|
13542
|
+
const { size, majorRequested } = requestedBump(bodies);
|
|
13543
|
+
return {
|
|
13544
|
+
ticket,
|
|
13545
|
+
branch,
|
|
13546
|
+
entries: usedFallback ? [`${ticket.title || ticket.issue_id} (${ticket.issue_id})`] : entries,
|
|
13547
|
+
usedFallback,
|
|
13548
|
+
bump: size,
|
|
13549
|
+
majorRequested
|
|
13550
|
+
};
|
|
13551
|
+
});
|
|
13552
|
+
}
|
|
13553
|
+
function planReleasedBranches(cwd, tickets, contract, branchFor, base = "main", tagPattern = "v*") {
|
|
13554
|
+
const eligible = /* @__PURE__ */ new Set([contract.statuses.building, contract.statuses.needsHuman]);
|
|
13555
|
+
const out = [];
|
|
13556
|
+
for (const ticket of tickets) {
|
|
13557
|
+
if (!eligible.has(ticket.status)) continue;
|
|
13558
|
+
const branch = branchFor(ticket);
|
|
13559
|
+
if (!branch) continue;
|
|
13560
|
+
const sha = findSquashByBranchTip(cwd, referenceKeys(ticket), branch, base);
|
|
13561
|
+
if (!sha) continue;
|
|
13562
|
+
const tag = firstReleaseTagContaining(cwd, sha, tagPattern);
|
|
13563
|
+
if (tag) out.push({ ticket, sha, tag });
|
|
13564
|
+
}
|
|
13565
|
+
return out;
|
|
13566
|
+
}
|
|
13567
|
+
function requestedBump(bodies) {
|
|
13568
|
+
const asks = [...bodies.matchAll(BUMP_LINE)].map((m) => m[1].toLowerCase());
|
|
13569
|
+
const majorRequested = asks.includes("major");
|
|
13570
|
+
const size = asks.includes("minor") || majorRequested ? "minor" : "patch";
|
|
13571
|
+
return { size, majorRequested };
|
|
12906
13572
|
}
|
|
12907
13573
|
function renderChangelogSection(version, date, entries) {
|
|
12908
13574
|
const body = entries.map((e) => `- ${e}`).join("\n");
|
|
@@ -13461,13 +14127,13 @@ async function bump(o, size) {
|
|
|
13461
14127
|
}
|
|
13462
14128
|
return printed;
|
|
13463
14129
|
}
|
|
13464
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
14130
|
+
const files = o.repo.release.versionFiles.map((f) => join30(o.cwd, f));
|
|
13465
14131
|
const first = files[0];
|
|
13466
|
-
if (!
|
|
14132
|
+
if (!existsSync26(first)) {
|
|
13467
14133
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
13468
14134
|
return null;
|
|
13469
14135
|
}
|
|
13470
|
-
const current = JSON.parse(
|
|
14136
|
+
const current = JSON.parse(readFileSync24(first, "utf8")).version;
|
|
13471
14137
|
if (!current) {
|
|
13472
14138
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
13473
14139
|
return null;
|
|
@@ -13480,27 +14146,27 @@ async function bump(o, size) {
|
|
|
13480
14146
|
return version;
|
|
13481
14147
|
}
|
|
13482
14148
|
for (const f of files) {
|
|
13483
|
-
const src =
|
|
14149
|
+
const src = readFileSync24(f, "utf8");
|
|
13484
14150
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
13485
14151
|
if (out === src) {
|
|
13486
14152
|
o.emit.error(`no version field in ${f}`);
|
|
13487
14153
|
return null;
|
|
13488
14154
|
}
|
|
13489
|
-
|
|
14155
|
+
writeFileSync20(f, out);
|
|
13490
14156
|
}
|
|
13491
14157
|
return version;
|
|
13492
14158
|
}
|
|
13493
14159
|
function writeChangelog(o, version, entries) {
|
|
13494
14160
|
const file = o.repo.release.changelog;
|
|
13495
14161
|
if (!file || entries.length === 0) return;
|
|
13496
|
-
const path =
|
|
14162
|
+
const path = join30(o.cwd, file);
|
|
13497
14163
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
13498
14164
|
if (o.dryRun) {
|
|
13499
14165
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
13500
14166
|
return;
|
|
13501
14167
|
}
|
|
13502
|
-
const existing =
|
|
13503
|
-
|
|
14168
|
+
const existing = existsSync26(path) ? readFileSync24(path, "utf8") : "# Changelog\n";
|
|
14169
|
+
writeFileSync20(path, insertChangelogSection(existing, section));
|
|
13504
14170
|
}
|
|
13505
14171
|
async function confirm(o, expected) {
|
|
13506
14172
|
if (!o.repo.hooks.released) return void 0;
|
|
@@ -13593,8 +14259,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
13593
14259
|
function openReleaseCheckout(o) {
|
|
13594
14260
|
const { remote, base } = o.repo.branch;
|
|
13595
14261
|
const branch = `crew/release-${Date.now()}`;
|
|
13596
|
-
const root = mkdtempSync2(
|
|
13597
|
-
const dir =
|
|
14262
|
+
const root = mkdtempSync2(join30(tmpdir3(), "crew-release-"));
|
|
14263
|
+
const dir = join30(root, "checkout");
|
|
13598
14264
|
o.emit.emit(`release checkout: ${dir}`, { step: "release" });
|
|
13599
14265
|
const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
|
|
13600
14266
|
try {
|
|
@@ -13604,10 +14270,10 @@ function openReleaseCheckout(o) {
|
|
|
13604
14270
|
throw e;
|
|
13605
14271
|
}
|
|
13606
14272
|
for (const f of o.repo.worktrees.copy) {
|
|
13607
|
-
const src =
|
|
13608
|
-
if (!
|
|
13609
|
-
mkdirSync17(
|
|
13610
|
-
copyFileSync(src,
|
|
14273
|
+
const src = join30(o.cwd, f);
|
|
14274
|
+
if (!existsSync26(src)) continue;
|
|
14275
|
+
mkdirSync17(dirname14(join30(dir, f)), { recursive: true });
|
|
14276
|
+
copyFileSync(src, join30(dir, f));
|
|
13611
14277
|
}
|
|
13612
14278
|
return { dir, branch, close };
|
|
13613
14279
|
}
|
|
@@ -14308,12 +14974,12 @@ Passed: ${passed}.${willClose ? " Closing this alert." : ""}`,
|
|
|
14308
14974
|
}
|
|
14309
14975
|
|
|
14310
14976
|
// src/live-runs.ts
|
|
14311
|
-
import { existsSync as
|
|
14312
|
-
import { join as
|
|
14977
|
+
import { existsSync as existsSync28, readdirSync as readdirSync11 } from "fs";
|
|
14978
|
+
import { join as join32 } from "path";
|
|
14313
14979
|
|
|
14314
14980
|
// src/interrupted.ts
|
|
14315
|
-
import { existsSync as
|
|
14316
|
-
import { join as
|
|
14981
|
+
import { existsSync as existsSync27, readdirSync as readdirSync10, readFileSync as readFileSync25 } from "fs";
|
|
14982
|
+
import { join as join31 } from "path";
|
|
14317
14983
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
14318
14984
|
function interruptedComment(b) {
|
|
14319
14985
|
const tree = b.dirty === null ? "The worktree could not be checked." : b.dirty ? "The worktree has uncommitted changes \u2014 they are this seat's own, from that run." : "The worktree is clean.";
|
|
@@ -14332,21 +14998,21 @@ var pidAlive = (pid) => {
|
|
|
14332
14998
|
};
|
|
14333
14999
|
function runFinished(streamPath) {
|
|
14334
15000
|
const path = `${streamPath}.meta.json`;
|
|
14335
|
-
if (!
|
|
15001
|
+
if (!existsSync27(path)) return false;
|
|
14336
15002
|
try {
|
|
14337
|
-
return JSON.parse(
|
|
15003
|
+
return JSON.parse(readFileSync25(path, "utf8")).running !== true;
|
|
14338
15004
|
} catch {
|
|
14339
15005
|
return true;
|
|
14340
15006
|
}
|
|
14341
15007
|
}
|
|
14342
15008
|
function orphanedStreams(o) {
|
|
14343
|
-
const dir =
|
|
14344
|
-
if (!
|
|
15009
|
+
const dir = join31(o.stateDir, "streams");
|
|
15010
|
+
if (!existsSync27(dir)) return [];
|
|
14345
15011
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
14346
15012
|
const out = [];
|
|
14347
15013
|
for (const name of readdirSync10(dir)) {
|
|
14348
15014
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
14349
|
-
const streamPath =
|
|
15015
|
+
const streamPath = join31(dir, name);
|
|
14350
15016
|
if (runFinished(streamPath)) continue;
|
|
14351
15017
|
const sidecar = readRunSidecar(streamPath);
|
|
14352
15018
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
@@ -14400,12 +15066,12 @@ async function reconcileRuns(o) {
|
|
|
14400
15066
|
|
|
14401
15067
|
// src/live-runs.ts
|
|
14402
15068
|
function liveRuns(stateDir, isAlive = runIsAlive) {
|
|
14403
|
-
const dir =
|
|
14404
|
-
if (!
|
|
15069
|
+
const dir = join32(stateDir, "streams");
|
|
15070
|
+
if (!existsSync28(dir)) return [];
|
|
14405
15071
|
const out = [];
|
|
14406
15072
|
for (const name of readdirSync11(dir)) {
|
|
14407
15073
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
14408
|
-
const streamPath =
|
|
15074
|
+
const streamPath = join32(dir, name);
|
|
14409
15075
|
if (runFinished(streamPath)) continue;
|
|
14410
15076
|
const sidecar = readRunSidecar(streamPath);
|
|
14411
15077
|
if (!sidecar || !isAlive(sidecar)) continue;
|
|
@@ -14506,7 +15172,7 @@ function formatProjectsReport(report) {
|
|
|
14506
15172
|
}
|
|
14507
15173
|
|
|
14508
15174
|
// src/forge-cmd.ts
|
|
14509
|
-
import { parseDocument } from "yaml";
|
|
15175
|
+
import { parseDocument as parseDocument4 } from "yaml";
|
|
14510
15176
|
function runCredential(action, stdin, forges, stateDir, env = process.env) {
|
|
14511
15177
|
try {
|
|
14512
15178
|
return { code: 0, out: answerCredential(action ?? "", parseCredentialInput(stdin), forges, stateDir, env), err: "" };
|
|
@@ -14547,7 +15213,7 @@ function runForge(a, io) {
|
|
|
14547
15213
|
if (!existing) {
|
|
14548
15214
|
const tok = sourceFromArgs(a);
|
|
14549
15215
|
if (typeof tok === "string") return fail(tok);
|
|
14550
|
-
const doc =
|
|
15216
|
+
const doc = parseDocument4(io.readFile(io.configFile));
|
|
14551
15217
|
doc.setIn(["ship", "forges", host, "token"], tok);
|
|
14552
15218
|
io.writeFile(io.configFile, doc.toString());
|
|
14553
15219
|
io.forges = { ...io.forges ?? {}, [host]: { username: "x-access-token", token: tok } };
|
|
@@ -14740,7 +15406,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
14740
15406
|
}
|
|
14741
15407
|
|
|
14742
15408
|
// src/watch.ts
|
|
14743
|
-
import { watch as watchFile, existsSync as
|
|
15409
|
+
import { watch as watchFile, existsSync as existsSync29, statSync as statSync9, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
|
|
14744
15410
|
var E = "\x1B[";
|
|
14745
15411
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
14746
15412
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -14841,7 +15507,7 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
14841
15507
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
14842
15508
|
}
|
|
14843
15509
|
function readFrom(file, from) {
|
|
14844
|
-
if (!
|
|
15510
|
+
if (!existsSync29(file)) return { events: [], offset: from };
|
|
14845
15511
|
const size = statSync9(file).size;
|
|
14846
15512
|
if (size < from) return readFrom(file, 0);
|
|
14847
15513
|
if (size === from) return { events: [], offset: from };
|
|
@@ -15084,17 +15750,17 @@ function nextRoles(decision, ran) {
|
|
|
15084
15750
|
}
|
|
15085
15751
|
|
|
15086
15752
|
// src/ship-name.ts
|
|
15087
|
-
import { existsSync as
|
|
15088
|
-
import { dirname as
|
|
15089
|
-
import { parseDocument as
|
|
15753
|
+
import { existsSync as existsSync31, mkdirSync as mkdirSync19, readFileSync as readFileSync27, renameSync as renameSync3, writeFileSync as writeFileSync22 } from "fs";
|
|
15754
|
+
import { dirname as dirname16 } from "path";
|
|
15755
|
+
import { parseDocument as parseDocument5 } from "yaml";
|
|
15090
15756
|
|
|
15091
15757
|
// src/machine-id.ts
|
|
15092
15758
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
15093
15759
|
import { createHash as createHash5 } from "crypto";
|
|
15094
|
-
import { existsSync as
|
|
15095
|
-
import { dirname as
|
|
15760
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync18, readFileSync as readFileSync26, writeFileSync as writeFileSync21 } from "fs";
|
|
15761
|
+
import { dirname as dirname15, join as join33 } from "path";
|
|
15096
15762
|
function machineIdPathFor(stateDir) {
|
|
15097
|
-
return
|
|
15763
|
+
return join33(stateDir, "machine-id");
|
|
15098
15764
|
}
|
|
15099
15765
|
var sha256 = (s) => createHash5("sha256").update(s).digest("hex");
|
|
15100
15766
|
function readRawMachineId(platform2 = process.platform) {
|
|
@@ -15107,8 +15773,8 @@ function readRawMachineId(platform2 = process.platform) {
|
|
|
15107
15773
|
return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
|
|
15108
15774
|
}
|
|
15109
15775
|
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
15110
|
-
if (
|
|
15111
|
-
const id =
|
|
15776
|
+
if (existsSync30(p)) {
|
|
15777
|
+
const id = readFileSync26(p, "utf8").trim();
|
|
15112
15778
|
if (id) return id;
|
|
15113
15779
|
}
|
|
15114
15780
|
}
|
|
@@ -15119,15 +15785,15 @@ function readRawMachineId(platform2 = process.platform) {
|
|
|
15119
15785
|
function ensureMachineId(stateDir, opts = {}) {
|
|
15120
15786
|
if (opts.override?.trim()) return sha256(opts.override.trim());
|
|
15121
15787
|
const path = machineIdPathFor(stateDir);
|
|
15122
|
-
if (
|
|
15123
|
-
const cached =
|
|
15788
|
+
if (existsSync30(path)) {
|
|
15789
|
+
const cached = readFileSync26(path, "utf8").trim();
|
|
15124
15790
|
if (/^[0-9a-f]{64}$/.test(cached)) return cached;
|
|
15125
15791
|
}
|
|
15126
15792
|
const raw = (opts.read ?? readRawMachineId)();
|
|
15127
15793
|
if (!raw) return void 0;
|
|
15128
15794
|
const hashed = sha256(raw);
|
|
15129
|
-
mkdirSync18(
|
|
15130
|
-
|
|
15795
|
+
mkdirSync18(dirname15(path), { recursive: true });
|
|
15796
|
+
writeFileSync21(path, `${hashed}
|
|
15131
15797
|
`, { mode: 384 });
|
|
15132
15798
|
return hashed;
|
|
15133
15799
|
}
|
|
@@ -15156,11 +15822,11 @@ function planNameSync(local, row, lastSynced) {
|
|
|
15156
15822
|
return { drift: "local", action: "push" };
|
|
15157
15823
|
}
|
|
15158
15824
|
function shipNameOfYaml(text) {
|
|
15159
|
-
const v =
|
|
15825
|
+
const v = parseDocument5(text).getIn(["ship", "name"]);
|
|
15160
15826
|
return typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
15161
15827
|
}
|
|
15162
15828
|
function setShipNameInYaml(text, name) {
|
|
15163
|
-
const doc =
|
|
15829
|
+
const doc = parseDocument5(text);
|
|
15164
15830
|
doc.setIn(["ship", "name"], name);
|
|
15165
15831
|
return doc.toString();
|
|
15166
15832
|
}
|
|
@@ -15198,34 +15864,34 @@ async function syncRouteName(i) {
|
|
|
15198
15864
|
return { status: "ok", ...base, action, name: i.local };
|
|
15199
15865
|
}
|
|
15200
15866
|
function readSyncedName(resolvedFile) {
|
|
15201
|
-
if (!
|
|
15867
|
+
if (!existsSync31(resolvedFile)) return void 0;
|
|
15202
15868
|
try {
|
|
15203
|
-
const v = JSON.parse(
|
|
15869
|
+
const v = JSON.parse(readFileSync27(resolvedFile, "utf8")).shipName;
|
|
15204
15870
|
return typeof v === "string" ? v : void 0;
|
|
15205
15871
|
} catch {
|
|
15206
15872
|
return void 0;
|
|
15207
15873
|
}
|
|
15208
15874
|
}
|
|
15209
15875
|
function writeSyncedName(resolvedFile, name) {
|
|
15210
|
-
if (!
|
|
15876
|
+
if (!existsSync31(resolvedFile)) return;
|
|
15211
15877
|
let raw;
|
|
15212
15878
|
try {
|
|
15213
|
-
raw = JSON.parse(
|
|
15879
|
+
raw = JSON.parse(readFileSync27(resolvedFile, "utf8"));
|
|
15214
15880
|
} catch {
|
|
15215
15881
|
return;
|
|
15216
15882
|
}
|
|
15217
15883
|
if (raw.shipName === name) return;
|
|
15218
15884
|
raw.shipName = name;
|
|
15219
|
-
mkdirSync19(
|
|
15885
|
+
mkdirSync19(dirname16(resolvedFile), { recursive: true });
|
|
15220
15886
|
const tmp = `${resolvedFile}.tmp${process.pid}`;
|
|
15221
|
-
|
|
15887
|
+
writeFileSync22(tmp, `${JSON.stringify(raw, null, 2)}
|
|
15222
15888
|
`);
|
|
15223
15889
|
renameSync3(tmp, resolvedFile);
|
|
15224
15890
|
}
|
|
15225
15891
|
async function reconcileShipName(i) {
|
|
15226
15892
|
let local = i.ship.name;
|
|
15227
15893
|
try {
|
|
15228
|
-
local = shipNameOfYaml(
|
|
15894
|
+
local = shipNameOfYaml(readFileSync27(i.configFile, "utf8")) ?? local;
|
|
15229
15895
|
} catch {
|
|
15230
15896
|
}
|
|
15231
15897
|
const resolvedFile = resolvedPathFor(i.stateDir, i.route);
|
|
@@ -15249,8 +15915,8 @@ async function reconcileShipName(i) {
|
|
|
15249
15915
|
}
|
|
15250
15916
|
if (result.status !== "ok") return result;
|
|
15251
15917
|
if (result.action === "adopt" || result.action === "row-wins") {
|
|
15252
|
-
const text =
|
|
15253
|
-
|
|
15918
|
+
const text = readFileSync27(i.configFile, "utf8");
|
|
15919
|
+
writeFileSync22(i.configFile, setShipNameInYaml(text, result.name));
|
|
15254
15920
|
i.ship.name = result.name;
|
|
15255
15921
|
if (result.action === "row-wins") {
|
|
15256
15922
|
i.emit?.warn(
|
|
@@ -15902,8 +16568,8 @@ function describeCapability(i) {
|
|
|
15902
16568
|
}
|
|
15903
16569
|
|
|
15904
16570
|
// src/agents-sync-run.ts
|
|
15905
|
-
import { existsSync as
|
|
15906
|
-
import { dirname as
|
|
16571
|
+
import { existsSync as existsSync32, mkdirSync as mkdirSync20, readFileSync as readFileSync28, writeFileSync as writeFileSync23 } from "fs";
|
|
16572
|
+
import { dirname as dirname17 } from "path";
|
|
15907
16573
|
async function runAgentsSync(target, o) {
|
|
15908
16574
|
const dryRun = o.dryRun ?? false;
|
|
15909
16575
|
const force = o.force ?? false;
|
|
@@ -15914,519 +16580,232 @@ async function runAgentsSync(target, o) {
|
|
|
15914
16580
|
try {
|
|
15915
16581
|
personaResult = await runPersonas(readOnly);
|
|
15916
16582
|
} catch (e) {
|
|
15917
|
-
if (readOnly || !isForbidden(e)) throw e;
|
|
15918
|
-
readOnly = true;
|
|
15919
|
-
personaResult = await runPersonas(true);
|
|
15920
|
-
}
|
|
15921
|
-
o.say(`route: ${target.route}`);
|
|
15922
|
-
for (const p of personaResult.outcomes) o.say(describeSyncOutcome(p));
|
|
15923
|
-
for (const l of personaResult.crewLinks) o.say(describeCrewLink(l));
|
|
15924
|
-
if (personaResult.releaseNote) o.say(personaResult.releaseNote);
|
|
15925
|
-
const briefs = Object.fromEntries(ROLE_NAMES.map((r) => [r, personaDefaultPrompt(target.promptsDir, r)]));
|
|
15926
|
-
for (const w of skillMarkerWarnings(target.promptsDir, briefs)) o.warn(`warning: ${w}`);
|
|
15927
|
-
let skillResult;
|
|
15928
|
-
if (!readOnly) {
|
|
15929
|
-
try {
|
|
15930
|
-
skillResult = await syncSkills(target, { userAgent: o.userAgent, dryRun, force });
|
|
15931
|
-
} catch (e) {
|
|
15932
|
-
if (isForbidden(e)) readOnly = true;
|
|
15933
|
-
else throw e;
|
|
15934
|
-
}
|
|
15935
|
-
}
|
|
15936
|
-
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
15937
|
-
if (!dryRun) {
|
|
15938
|
-
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
15939
|
-
const raw = existsSync31(resolvedPath) ? JSON.parse(readFileSync27(resolvedPath, "utf8")) : {};
|
|
15940
|
-
raw.agentPersonas = personaResult.agentPersonas;
|
|
15941
|
-
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
15942
|
-
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
15943
|
-
mkdirSync20(dirname16(resolvedPath), { recursive: true });
|
|
15944
|
-
writeFileSync22(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
15945
|
-
`);
|
|
15946
|
-
if (target.resolved) {
|
|
15947
|
-
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
15948
|
-
if (personaResult.releaseAgentId) target.resolved.releaseAgentId = personaResult.releaseAgentId;
|
|
15949
|
-
}
|
|
15950
|
-
}
|
|
15951
|
-
if (readOnly) {
|
|
15952
|
-
o.say("personas and skills: not synced (this key is not a workspace admin; the workspace's rows are used as they are)");
|
|
15953
|
-
}
|
|
15954
|
-
const diverged = personaResult.outcomes.some((p) => p.action === "diverged") || (skillResult?.outcomes.some((s) => s.action === "diverged") ?? false);
|
|
15955
|
-
return { personaResult, skillResult, readOnly, diverged };
|
|
15956
|
-
}
|
|
15957
|
-
|
|
15958
|
-
// src/logbook.ts
|
|
15959
|
-
import { createHash as createHash6 } from "crypto";
|
|
15960
|
-
import { TablationClient as TablationClient4 } from "@tablation/client";
|
|
15961
|
-
var LogbookError = class extends Error {
|
|
15962
|
-
};
|
|
15963
|
-
function client(route2, userAgent) {
|
|
15964
|
-
return new TablationClient4({
|
|
15965
|
-
baseUrl: `${route2.baseUrl}/api`,
|
|
15966
|
-
apiKey: resolveApiKey(route2),
|
|
15967
|
-
headers: { "User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
15968
|
-
});
|
|
15969
|
-
}
|
|
15970
|
-
function requireWorkspace(route2) {
|
|
15971
|
-
if (!route2.resolved) {
|
|
15972
|
-
throw new LogbookError(`route "${route2.route}" has no resolved ids \u2014 run \`crew connect\` first`);
|
|
15973
|
-
}
|
|
15974
|
-
}
|
|
15975
|
-
async function listLogEntries(route2, opts = {}) {
|
|
15976
|
-
requireWorkspace(route2);
|
|
15977
|
-
const c = client(route2, opts.userAgent);
|
|
15978
|
-
const agentLogModel = await c.dataModels.get("agent_log", route2.resolved.workspaceId);
|
|
15979
|
-
const filters = [];
|
|
15980
|
-
if (opts.ticket) filters.push({ columnName: "ticket_reference", operator: "EQ", value: opts.ticket });
|
|
15981
|
-
if (opts.role) {
|
|
15982
|
-
const agentsModel = await c.dataModels.get("agents", route2.resolved.workspaceId);
|
|
15983
|
-
const rows = await c.records.list(agentsModel.id, { limit: 200 });
|
|
15984
|
-
const match = rows.find((r) => r.name.toLowerCase() === opts.role.toLowerCase());
|
|
15985
|
-
if (!match) return [];
|
|
15986
|
-
filters.push({ columnName: "agent_id", operator: "EQ", value: match.id });
|
|
15987
|
-
}
|
|
15988
|
-
const entries = await c.records.list(agentLogModel.id, {
|
|
15989
|
-
limit: opts.limit ?? 20,
|
|
15990
|
-
...filters.length > 0 ? { filters: JSON.stringify(filters) } : {}
|
|
15991
|
-
});
|
|
15992
|
-
return [...entries].sort((a, b) => String(b.started_at ?? "").localeCompare(String(a.started_at ?? "")));
|
|
15993
|
-
}
|
|
15994
|
-
async function showLogEntry(route2, entryId, opts = {}) {
|
|
15995
|
-
requireWorkspace(route2);
|
|
15996
|
-
const c = client(route2, opts.userAgent);
|
|
15997
|
-
const agentLogModel = await c.dataModels.get("agent_log", route2.resolved.workspaceId);
|
|
15998
|
-
let entry;
|
|
15999
|
-
try {
|
|
16000
|
-
entry = await c.records.get(agentLogModel.id, entryId);
|
|
16001
|
-
} catch (e) {
|
|
16002
|
-
throw new LogbookError(`no Agent Log entry ${entryId} in this workspace (${e.message})`);
|
|
16003
|
-
}
|
|
16004
|
-
if (!opts.withPrompt) return { entry, reconstruction: { status: "not_recorded", reason: "not requested" } };
|
|
16005
|
-
if (!entry.agent_id || !entry.prompt_version) {
|
|
16006
|
-
return {
|
|
16007
|
-
entry,
|
|
16008
|
-
reconstruction: {
|
|
16009
|
-
status: "not_recorded",
|
|
16010
|
-
reason: !entry.agent_id ? "this entry has no linked Agents row (predates crew agents sync being run for its role)" : "this entry predates ISSUE-377/529 \u2014 no prompt_version was recorded"
|
|
16011
|
-
}
|
|
16012
|
-
};
|
|
16013
|
-
}
|
|
16014
|
-
const agentsModel = await c.dataModels.get("agents", route2.resolved.workspaceId);
|
|
16015
|
-
let historyEntries;
|
|
16016
|
-
try {
|
|
16017
|
-
historyEntries = await fetchHistoryEntries(route2, agentsModel.id, entry.agent_id, opts.userAgent, 200);
|
|
16018
|
-
} catch (e) {
|
|
16019
|
-
return { entry, reconstruction: { status: "approximate", reason: `history lookup failed: ${e.message}` } };
|
|
16020
|
-
}
|
|
16021
|
-
const match = historyEntries.find((h) => h.history_id === entry.prompt_version);
|
|
16022
|
-
if (!match) {
|
|
16023
|
-
return {
|
|
16024
|
-
entry,
|
|
16025
|
-
reconstruction: {
|
|
16026
|
-
status: "approximate",
|
|
16027
|
-
reason: `history entry ${entry.prompt_version} no longer exists on the Agents row (purged, or the row itself was recreated)`
|
|
16028
|
-
}
|
|
16029
|
-
};
|
|
16030
|
-
}
|
|
16031
|
-
const prompt = appendAdditions(match.prompt ?? "", match.prompt_additions);
|
|
16032
|
-
const actualSha = createHash6("sha256").update(prompt).digest("hex");
|
|
16033
|
-
const exact = !!entry.prompt_sha && actualSha === entry.prompt_sha;
|
|
16034
|
-
return {
|
|
16035
|
-
entry,
|
|
16036
|
-
reconstruction: {
|
|
16037
|
-
status: exact ? "exact" : "approximate",
|
|
16038
|
-
historyId: match.history_id,
|
|
16039
|
-
changedAt: match.changed_at,
|
|
16040
|
-
prompt,
|
|
16041
|
-
...exact ? {} : { reason: entry.prompt_sha ? "stored prompt_sha does not match this history entry's content" : "this entry recorded no prompt_sha to verify against" }
|
|
16042
|
-
}
|
|
16043
|
-
};
|
|
16044
|
-
}
|
|
16045
|
-
|
|
16046
|
-
// src/repos-check.ts
|
|
16047
|
-
import { spawnSync as spawnSync6 } from "child_process";
|
|
16048
|
-
import { readFileSync as readFileSync28 } from "fs";
|
|
16049
|
-
import { basename as basename8 } from "path";
|
|
16050
|
-
var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
|
|
16051
|
-
function checkHookSyntax(script, shell) {
|
|
16052
|
-
const spec = resolveShell(shell);
|
|
16053
|
-
const name = basename8(spec.bin).replace(/\.exe$/i, "").toLowerCase();
|
|
16054
|
-
let run2;
|
|
16055
|
-
if (POSIX_SHELL.test(name)) {
|
|
16056
|
-
run2 = spawnSync6(spec.bin, ["-n"], { input: script, encoding: "utf8" });
|
|
16057
|
-
} else if (/^(pwsh|powershell)$/.test(name)) {
|
|
16058
|
-
const parse3 = "$e=$null;[void][System.Management.Automation.Language.Parser]::ParseInput($env:CREW_CHECK_SCRIPT,[ref]$null,[ref]$e);if($e){$e|ForEach-Object{[Console]::Error.WriteLine($_.Message)};exit 1}";
|
|
16059
|
-
run2 = spawnSync6(spec.bin, ["-NoProfile", "-NonInteractive", "-Command", parse3], {
|
|
16060
|
-
encoding: "utf8",
|
|
16061
|
-
env: { ...process.env, CREW_CHECK_SCRIPT: script }
|
|
16062
|
-
});
|
|
16063
|
-
} else {
|
|
16064
|
-
return { status: "unchecked", message: `no syntax check for ${spec.bin}` };
|
|
16065
|
-
}
|
|
16066
|
-
if (run2.error) return { status: "unchecked", message: `cannot run ${spec.bin}: ${run2.error.message}` };
|
|
16067
|
-
if (run2.status === 0) return { status: "ok" };
|
|
16068
|
-
const message = `${run2.stderr ?? ""}${run2.stdout ?? ""}`.trim() || `${spec.bin} exited ${run2.status}`;
|
|
16069
|
-
return { status: "error", message };
|
|
16070
|
-
}
|
|
16071
|
-
function renderSamples(cfg, dir) {
|
|
16072
|
-
const checkout = basename8(dir.replace(/[/\\]+$/, ""));
|
|
16073
|
-
const tagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", tag: "TABL-123", prefix: "tabl", lowercase: cfg.branch.lowercase };
|
|
16074
|
-
const untagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", lowercase: cfg.branch.lowercase };
|
|
16075
|
-
return [["with a project tag", tagged], ["without a project tag", untagged]].map(([label, ctx]) => {
|
|
16076
|
-
const branch = renderBranchName(effectiveBranchTemplate(cfg, ctx.prefix), ctx);
|
|
16077
|
-
const worktree = effectiveWorktreeDirName(cfg, dir, branch, "123", (t) => renderWorktreeName(t, { ...ctx, dir: checkout }));
|
|
16078
|
-
return { case: label, branch, worktree };
|
|
16079
|
-
});
|
|
16080
|
-
}
|
|
16081
|
-
function checkRepoSpec(dir, opts = {}) {
|
|
16082
|
-
const empty = { parsed: null, effective: null, readiness: [], hooks: [], shadowed: [], samples: [] };
|
|
16083
|
-
const file = findRepoConfig(dir);
|
|
16084
|
-
if (!file) {
|
|
16085
|
-
return { ...empty, file: null, error: `no .crew.yaml, .crew.yml or .crew.json in ${dir}`, exitCode: 1 };
|
|
16086
|
-
}
|
|
16087
|
-
let parsed;
|
|
16088
|
-
try {
|
|
16089
|
-
parsed = parseRepoConfig(readFileSync28(file, "utf8"), file);
|
|
16090
|
-
} catch (e) {
|
|
16091
|
-
if (!(e instanceof RepoConfigError)) throw e;
|
|
16092
|
-
return { ...empty, file, error: e.message, exitCode: 1 };
|
|
16093
|
-
}
|
|
16094
|
-
const effective = resolveRepoConfig(parsed, opts.ship, dir);
|
|
16095
|
-
const readiness = validateEffective(effective);
|
|
16096
|
-
const hooks = Object.keys(effective.hooks).map((name) => ({
|
|
16097
|
-
name,
|
|
16098
|
-
label: hookLabel(effective, name),
|
|
16099
|
-
syntax: checkHookSyntax(effective.hooks[name], effective.shell)
|
|
16100
|
-
}));
|
|
16101
|
-
const failed = hooks.some((h) => h.syntax.status === "error");
|
|
16102
|
-
return {
|
|
16103
|
-
file,
|
|
16104
|
-
parsed,
|
|
16105
|
-
effective,
|
|
16106
|
-
readiness,
|
|
16107
|
-
hooks,
|
|
16108
|
-
shadowed: effective.shadowed,
|
|
16109
|
-
samples: renderSamples(effective, dir),
|
|
16110
|
-
error: null,
|
|
16111
|
-
exitCode: failed || opts.strict && readiness.length > 0 ? 1 : 0
|
|
16112
|
-
};
|
|
16113
|
-
}
|
|
16114
|
-
function renderRepoCheck(r) {
|
|
16115
|
-
if (r.error) return `${r.file ?? "crew repos check"}: ${r.error}
|
|
16116
|
-
`;
|
|
16117
|
-
const e = r.effective;
|
|
16118
|
-
const lines = [
|
|
16119
|
-
`${r.file}`,
|
|
16120
|
-
` platform ${e.platform}`,
|
|
16121
|
-
` release.mode ${e.release.mode}`,
|
|
16122
|
-
` release.versioning ${e.release.versioning}`,
|
|
16123
|
-
` branch.base ${e.branch.base}`,
|
|
16124
|
-
` branch.name ${e.branch.name}`,
|
|
16125
|
-
` branch.push ${e.branch.push}`
|
|
16126
|
-
];
|
|
16127
|
-
for (const s of r.samples) lines.push(` ${s.case}: branch ${s.branch}, worktree ../${s.worktree}`);
|
|
16128
|
-
lines.push(r.hooks.length ? " hooks" : " hooks none");
|
|
16129
|
-
for (const h of r.hooks) {
|
|
16130
|
-
const named = h.label !== e.hooks[h.name] ? ` (${h.label})` : "";
|
|
16131
|
-
const syntax = h.syntax.status === "ok" ? "syntax ok" : h.syntax.status === "unchecked" ? `syntax not checked: ${h.syntax.message}` : "syntax ERROR";
|
|
16132
|
-
lines.push(` ${h.name}${named}: ${syntax}`);
|
|
16133
|
-
if (h.syntax.status === "error") lines.push(...(h.syntax.message ?? "").split("\n").map((l) => ` ${l}`));
|
|
16134
|
-
}
|
|
16135
|
-
if (r.shadowed.length) lines.push(` this ship declares ${r.shadowed.join(", ")}, and the file overrides it \u2014 the file wins`);
|
|
16136
|
-
for (const p of r.readiness) lines.push(` - release readiness: ${p}`);
|
|
16137
|
-
return `${lines.join("\n")}
|
|
16138
|
-
`;
|
|
16139
|
-
}
|
|
16140
|
-
|
|
16141
|
-
// src/repos-hooks.ts
|
|
16142
|
-
import { isMap as isMap2, parseDocument as parseDocument4, YAMLMap } from "yaml";
|
|
16143
|
-
|
|
16144
|
-
// src/repos-cmd.ts
|
|
16145
|
-
import { homedir as homedir9 } from "os";
|
|
16146
|
-
import { basename as basename9, join as join33, resolve as resolve3 } from "path";
|
|
16147
|
-
import { isMap, isSeq, parseDocument as parseDocument3, stringify as stringify2 } from "yaml";
|
|
16148
|
-
var ReposError = class extends Error {
|
|
16149
|
-
};
|
|
16150
|
-
function normalizeRemote(url) {
|
|
16151
|
-
if (!url) return void 0;
|
|
16152
|
-
let s = url.trim();
|
|
16153
|
-
if (!s) return void 0;
|
|
16154
|
-
const scp = /^[^\s@/]+@[^\s:/]+:(.+)$/.exec(s);
|
|
16155
|
-
if (scp) s = scp[1];
|
|
16156
|
-
else s = s.replace(/^[a-z][a-z0-9+.-]*:\/\/(?:[^/@]+@)?[^/]+\//i, "");
|
|
16157
|
-
s = s.replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
16158
|
-
const parts = s.split("/").filter(Boolean);
|
|
16159
|
-
if (parts.length < 2) return void 0;
|
|
16160
|
-
return parts.slice(-2).join("/").toLowerCase();
|
|
16161
|
-
}
|
|
16162
|
-
function matchRepoName(origin, resolved) {
|
|
16163
|
-
const want = normalizeRemote(origin);
|
|
16164
|
-
if (!want || !resolved?.repoRemotes) return void 0;
|
|
16165
|
-
for (const [id, remote] of Object.entries(resolved.repoRemotes)) {
|
|
16166
|
-
if (normalizeRemote(remote) === want) return resolved.repoNames?.[id];
|
|
16167
|
-
}
|
|
16168
|
-
return void 0;
|
|
16169
|
-
}
|
|
16170
|
-
function routeNamesOf(text) {
|
|
16171
|
-
const routes = parseDocument3(text).get("routes", true);
|
|
16172
|
-
if (!isSeq(routes)) return [];
|
|
16173
|
-
return routes.items.flatMap((i) => isMap(i) && typeof i.get("route") === "string" ? [i.get("route")] : []);
|
|
16174
|
-
}
|
|
16175
|
-
function parseAddArgs(args, routes, isDir) {
|
|
16176
|
-
const rest = [...args];
|
|
16177
|
-
let route2;
|
|
16178
|
-
if (rest.length > 0 && routes.includes(rest[0])) route2 = rest.shift();
|
|
16179
|
-
else if (rest.length > 0 && /^[^/\s]+\/[^/\s]+$/.test(rest[0]) && !isDir(rest[0])) {
|
|
16180
|
-
throw new ReposError(`no route "${rest[0]}" in the config (have: ${routes.join(", ") || "none"})`);
|
|
16583
|
+
if (readOnly || !isForbidden(e)) throw e;
|
|
16584
|
+
readOnly = true;
|
|
16585
|
+
personaResult = await runPersonas(true);
|
|
16181
16586
|
}
|
|
16182
|
-
|
|
16183
|
-
|
|
16184
|
-
|
|
16587
|
+
o.say(`route: ${target.route}`);
|
|
16588
|
+
for (const p of personaResult.outcomes) o.say(describeSyncOutcome(p));
|
|
16589
|
+
for (const l of personaResult.crewLinks) o.say(describeCrewLink(l));
|
|
16590
|
+
if (personaResult.releaseNote) o.say(personaResult.releaseNote);
|
|
16591
|
+
const briefs = Object.fromEntries(ROLE_NAMES.map((r) => [r, personaDefaultPrompt(target.promptsDir, r)]));
|
|
16592
|
+
for (const w of skillMarkerWarnings(target.promptsDir, briefs)) o.warn(`warning: ${w}`);
|
|
16593
|
+
let skillResult;
|
|
16594
|
+
if (!readOnly) {
|
|
16595
|
+
try {
|
|
16596
|
+
skillResult = await syncSkills(target, { userAgent: o.userAgent, dryRun, force });
|
|
16597
|
+
} catch (e) {
|
|
16598
|
+
if (isForbidden(e)) readOnly = true;
|
|
16599
|
+
else throw e;
|
|
16600
|
+
}
|
|
16185
16601
|
}
|
|
16186
|
-
|
|
16187
|
-
if (
|
|
16188
|
-
|
|
16189
|
-
|
|
16190
|
-
|
|
16191
|
-
|
|
16192
|
-
|
|
16193
|
-
|
|
16194
|
-
}
|
|
16195
|
-
|
|
16196
|
-
|
|
16197
|
-
|
|
16198
|
-
|
|
16199
|
-
if (isMap(item) && item.get("route") === route2) return item;
|
|
16602
|
+
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
16603
|
+
if (!dryRun) {
|
|
16604
|
+
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
16605
|
+
const raw = existsSync32(resolvedPath) ? JSON.parse(readFileSync28(resolvedPath, "utf8")) : {};
|
|
16606
|
+
raw.agentPersonas = personaResult.agentPersonas;
|
|
16607
|
+
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
16608
|
+
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
16609
|
+
mkdirSync20(dirname17(resolvedPath), { recursive: true });
|
|
16610
|
+
writeFileSync23(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
16611
|
+
`);
|
|
16612
|
+
if (target.resolved) {
|
|
16613
|
+
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
16614
|
+
if (personaResult.releaseAgentId) target.resolved.releaseAgentId = personaResult.releaseAgentId;
|
|
16200
16615
|
}
|
|
16201
16616
|
}
|
|
16202
|
-
|
|
16203
|
-
|
|
16204
|
-
}
|
|
16205
|
-
function configuredRepos(text, route2, base) {
|
|
16206
|
-
const doc = parseDocument3(text);
|
|
16207
|
-
const m = routeMapOf(doc, route2);
|
|
16208
|
-
const repos = m.get("repos");
|
|
16209
|
-
if (isMap(repos)) {
|
|
16210
|
-
return repos.items.map((p) => {
|
|
16211
|
-
const name = String(p.key.value ?? p.key);
|
|
16212
|
-
const v = p.value;
|
|
16213
|
-
const dir2 = isMap(v) ? String(v.get("dir") ?? "") : String(v?.value ?? "");
|
|
16214
|
-
return { name, dir: expandDir(dir2, base) };
|
|
16215
|
-
});
|
|
16617
|
+
if (readOnly) {
|
|
16618
|
+
o.say("personas and skills: not synced (this key is not a workspace admin; the workspace's rows are used as they are)");
|
|
16216
16619
|
}
|
|
16217
|
-
const
|
|
16218
|
-
return
|
|
16620
|
+
const diverged = personaResult.outcomes.some((p) => p.action === "diverged") || (skillResult?.outcomes.some((s) => s.action === "diverged") ?? false);
|
|
16621
|
+
return { personaResult, skillResult, readOnly, diverged };
|
|
16219
16622
|
}
|
|
16220
|
-
|
|
16221
|
-
|
|
16222
|
-
|
|
16223
|
-
|
|
16224
|
-
|
|
16225
|
-
|
|
16226
|
-
|
|
16227
|
-
return
|
|
16623
|
+
|
|
16624
|
+
// src/logbook.ts
|
|
16625
|
+
import { createHash as createHash6 } from "crypto";
|
|
16626
|
+
import { TablationClient as TablationClient4 } from "@tablation/client";
|
|
16627
|
+
var LogbookError = class extends Error {
|
|
16628
|
+
};
|
|
16629
|
+
function client(route2, userAgent) {
|
|
16630
|
+
return new TablationClient4({
|
|
16631
|
+
baseUrl: `${route2.baseUrl}/api`,
|
|
16632
|
+
apiKey: resolveApiKey(route2),
|
|
16633
|
+
headers: { "User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
16634
|
+
});
|
|
16228
16635
|
}
|
|
16229
|
-
function
|
|
16230
|
-
|
|
16231
|
-
|
|
16232
|
-
|
|
16233
|
-
if (isMap(repos)) target = repos.get(name, true);
|
|
16234
|
-
if (!isMap(target)) return false;
|
|
16235
|
-
const hooks = target.get("hooks", true);
|
|
16236
|
-
return isMap(hooks) && hooks.items.length > 0;
|
|
16636
|
+
function requireWorkspace(route2) {
|
|
16637
|
+
if (!route2.resolved) {
|
|
16638
|
+
throw new LogbookError(`route "${route2.route}" has no resolved ids \u2014 run \`crew connect\` first`);
|
|
16639
|
+
}
|
|
16237
16640
|
}
|
|
16238
|
-
function
|
|
16239
|
-
|
|
16240
|
-
const
|
|
16241
|
-
const
|
|
16242
|
-
const
|
|
16243
|
-
|
|
16244
|
-
|
|
16245
|
-
|
|
16246
|
-
|
|
16247
|
-
|
|
16248
|
-
|
|
16249
|
-
|
|
16250
|
-
const remotes = i.resolved?.repoRemotes ?? {};
|
|
16251
|
-
for (const [id, name] of Object.entries(names)) {
|
|
16252
|
-
const remote = remotes[id] ?? null;
|
|
16253
|
-
const want = normalizeRemote(remote);
|
|
16254
|
-
let idx = checkouts.findIndex((c2, n) => !claimed.has(n) && want !== void 0 && normalizeRemote(i.originOf(c2.dir)) === want);
|
|
16255
|
-
if (idx < 0) idx = checkouts.findIndex((c2, n) => !claimed.has(n) && c2.name === name);
|
|
16256
|
-
const c = idx >= 0 ? checkouts[idx] : void 0;
|
|
16257
|
-
if (idx >= 0) claimed.add(idx);
|
|
16258
|
-
rows.push({
|
|
16259
|
-
name,
|
|
16260
|
-
remote,
|
|
16261
|
-
repoRowId: id,
|
|
16262
|
-
attached: c !== void 0,
|
|
16263
|
-
dir: c?.dir ?? null,
|
|
16264
|
-
exists: c ? i.exists(c.dir) : false,
|
|
16265
|
-
origin: c ? originFor(c, remote) : null,
|
|
16266
|
-
matchedRow: c ? name : null,
|
|
16267
|
-
hooksSource: c ? hooksSourceOf(c) : "none"
|
|
16268
|
-
});
|
|
16641
|
+
async function listLogEntries(route2, opts = {}) {
|
|
16642
|
+
requireWorkspace(route2);
|
|
16643
|
+
const c = client(route2, opts.userAgent);
|
|
16644
|
+
const agentLogModel = await c.dataModels.get("agent_log", route2.resolved.workspaceId);
|
|
16645
|
+
const filters = [];
|
|
16646
|
+
if (opts.ticket) filters.push({ columnName: "ticket_reference", operator: "EQ", value: opts.ticket });
|
|
16647
|
+
if (opts.role) {
|
|
16648
|
+
const agentsModel = await c.dataModels.get("agents", route2.resolved.workspaceId);
|
|
16649
|
+
const rows = await c.records.list(agentsModel.id, { limit: 200 });
|
|
16650
|
+
const match = rows.find((r) => r.name.toLowerCase() === opts.role.toLowerCase());
|
|
16651
|
+
if (!match) return [];
|
|
16652
|
+
filters.push({ columnName: "agent_id", operator: "EQ", value: match.id });
|
|
16269
16653
|
}
|
|
16270
|
-
|
|
16271
|
-
|
|
16272
|
-
|
|
16273
|
-
name: c.name,
|
|
16274
|
-
remote: null,
|
|
16275
|
-
repoRowId: null,
|
|
16276
|
-
attached: true,
|
|
16277
|
-
dir: c.dir,
|
|
16278
|
-
exists: i.exists(c.dir),
|
|
16279
|
-
origin: originFor(c, null),
|
|
16280
|
-
matchedRow: null,
|
|
16281
|
-
hooksSource: hooksSourceOf(c)
|
|
16282
|
-
});
|
|
16654
|
+
const entries = await c.records.list(agentLogModel.id, {
|
|
16655
|
+
limit: opts.limit ?? 20,
|
|
16656
|
+
...filters.length > 0 ? { filters: JSON.stringify(filters) } : {}
|
|
16283
16657
|
});
|
|
16284
|
-
return
|
|
16658
|
+
return [...entries].sort((a, b) => String(b.started_at ?? "").localeCompare(String(a.started_at ?? "")));
|
|
16285
16659
|
}
|
|
16286
|
-
function
|
|
16287
|
-
|
|
16288
|
-
|
|
16289
|
-
const
|
|
16290
|
-
let
|
|
16291
|
-
|
|
16292
|
-
|
|
16293
|
-
|
|
16294
|
-
|
|
16295
|
-
if (i.isCheckout && placeholder && !(isMap(reposNow) && reposNow.items.length > 0)) {
|
|
16296
|
-
const placeholderDir = expandDir(String(placeholder), i.base);
|
|
16297
|
-
if (placeholderDir !== target && !i.isCheckout(placeholderDir)) {
|
|
16298
|
-
droppedDir = placeholderDir;
|
|
16299
|
-
m.delete("dir");
|
|
16300
|
-
existing = [];
|
|
16301
|
-
}
|
|
16302
|
-
}
|
|
16303
|
-
if (existing.some((e) => e.dir === target)) {
|
|
16304
|
-
throw new ReposError(`${target} is already configured on route ${i.route}`);
|
|
16305
|
-
}
|
|
16306
|
-
const matched = matchRepoName(i.origin, i.resolved);
|
|
16307
|
-
const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename9(target);
|
|
16308
|
-
let warning;
|
|
16309
|
-
if (!matched) {
|
|
16310
|
-
const known = Object.values(i.resolved?.repoNames ?? {});
|
|
16311
|
-
warning = i.origin ? `the tracker has no Repos row for remote ${normalizeRemote(i.origin) ?? i.origin}${known.length ? ` (known: ${known.join(", ")})` : ""} \u2014 added anyway; create the row so ticket routing can find it` : "this checkout has no origin remote, so it cannot be matched to a Repos row \u2014 added anyway";
|
|
16660
|
+
async function showLogEntry(route2, entryId, opts = {}) {
|
|
16661
|
+
requireWorkspace(route2);
|
|
16662
|
+
const c = client(route2, opts.userAgent);
|
|
16663
|
+
const agentLogModel = await c.dataModels.get("agent_log", route2.resolved.workspaceId);
|
|
16664
|
+
let entry;
|
|
16665
|
+
try {
|
|
16666
|
+
entry = await c.records.get(agentLogModel.id, entryId);
|
|
16667
|
+
} catch (e) {
|
|
16668
|
+
throw new LogbookError(`no Agent Log entry ${entryId} in this workspace (${e.message})`);
|
|
16312
16669
|
}
|
|
16313
|
-
|
|
16314
|
-
|
|
16315
|
-
|
|
16316
|
-
|
|
16317
|
-
|
|
16318
|
-
|
|
16319
|
-
|
|
16320
|
-
|
|
16321
|
-
|
|
16322
|
-
if (oldName === name) throw new ReposError(`route ${i.route} already has a repo named "${name}" (${old.dir})`);
|
|
16323
|
-
const oldDir = String(m.get("dir"));
|
|
16324
|
-
const pair = m.items.find((p) => p.key.value === "dir");
|
|
16325
|
-
pair.key = doc.createNode("repos");
|
|
16326
|
-
pair.value = doc.createNode({ [oldName]: oldDir, [name]: target });
|
|
16327
|
-
mode = "repos";
|
|
16328
|
-
} else if (droppedDir) {
|
|
16329
|
-
m.set("repos", doc.createNode({ [name]: target }));
|
|
16330
|
-
mode = "repos";
|
|
16331
|
-
} else {
|
|
16332
|
-
m.set("dir", target);
|
|
16333
|
-
mode = "dir";
|
|
16670
|
+
if (!opts.withPrompt) return { entry, reconstruction: { status: "not_recorded", reason: "not requested" } };
|
|
16671
|
+
if (!entry.agent_id || !entry.prompt_version) {
|
|
16672
|
+
return {
|
|
16673
|
+
entry,
|
|
16674
|
+
reconstruction: {
|
|
16675
|
+
status: "not_recorded",
|
|
16676
|
+
reason: !entry.agent_id ? "this entry has no linked Agents row (predates crew agents sync being run for its role)" : "this entry predates ISSUE-377/529 \u2014 no prompt_version was recorded"
|
|
16677
|
+
}
|
|
16678
|
+
};
|
|
16334
16679
|
}
|
|
16335
|
-
|
|
16336
|
-
|
|
16337
|
-
|
|
16338
|
-
|
|
16339
|
-
|
|
16340
|
-
|
|
16341
|
-
const target = expandDir(path, base);
|
|
16342
|
-
const existing = configuredRepos(text, route2, base);
|
|
16343
|
-
const hit = existing.find((e) => e.dir === target);
|
|
16344
|
-
if (!hit) throw new ReposError(`${target} is not configured on route ${route2}`);
|
|
16345
|
-
const repos = m.get("repos", true);
|
|
16346
|
-
if (isMap(repos)) {
|
|
16347
|
-
repos.delete(hit.name);
|
|
16348
|
-
const left = existing.filter((e) => e !== hit);
|
|
16349
|
-
if (left.length === 1) {
|
|
16350
|
-
const pair = m.items.find((p) => p.key.value === "repos");
|
|
16351
|
-
pair.key = doc.createNode("dir");
|
|
16352
|
-
pair.value = doc.createNode(String(repos.get(left[0].name)));
|
|
16353
|
-
} else if (left.length === 0) m.delete("repos");
|
|
16354
|
-
} else {
|
|
16355
|
-
m.delete("dir");
|
|
16680
|
+
const agentsModel = await c.dataModels.get("agents", route2.resolved.workspaceId);
|
|
16681
|
+
let historyEntries;
|
|
16682
|
+
try {
|
|
16683
|
+
historyEntries = await fetchHistoryEntries(route2, agentsModel.id, entry.agent_id, opts.userAgent, 200);
|
|
16684
|
+
} catch (e) {
|
|
16685
|
+
return { entry, reconstruction: { status: "approximate", reason: `history lookup failed: ${e.message}` } };
|
|
16356
16686
|
}
|
|
16357
|
-
|
|
16358
|
-
|
|
16359
|
-
|
|
16360
|
-
|
|
16361
|
-
|
|
16362
|
-
|
|
16363
|
-
|
|
16364
|
-
|
|
16365
|
-
|
|
16366
|
-
|
|
16687
|
+
const match = historyEntries.find((h) => h.history_id === entry.prompt_version);
|
|
16688
|
+
if (!match) {
|
|
16689
|
+
return {
|
|
16690
|
+
entry,
|
|
16691
|
+
reconstruction: {
|
|
16692
|
+
status: "approximate",
|
|
16693
|
+
reason: `history entry ${entry.prompt_version} no longer exists on the Agents row (purged, or the row itself was recreated)`
|
|
16694
|
+
}
|
|
16695
|
+
};
|
|
16696
|
+
}
|
|
16697
|
+
const prompt = appendAdditions(match.prompt ?? "", match.prompt_additions);
|
|
16698
|
+
const actualSha = createHash6("sha256").update(prompt).digest("hex");
|
|
16699
|
+
const exact = !!entry.prompt_sha && actualSha === entry.prompt_sha;
|
|
16367
16700
|
return {
|
|
16368
|
-
|
|
16369
|
-
|
|
16370
|
-
|
|
16371
|
-
|
|
16372
|
-
|
|
16373
|
-
|
|
16374
|
-
|
|
16701
|
+
entry,
|
|
16702
|
+
reconstruction: {
|
|
16703
|
+
status: exact ? "exact" : "approximate",
|
|
16704
|
+
historyId: match.history_id,
|
|
16705
|
+
changedAt: match.changed_at,
|
|
16706
|
+
prompt,
|
|
16707
|
+
...exact ? {} : { reason: entry.prompt_sha ? "stored prompt_sha does not match this history entry's content" : "this entry recorded no prompt_sha to verify against" }
|
|
16708
|
+
}
|
|
16375
16709
|
};
|
|
16376
16710
|
}
|
|
16377
|
-
|
|
16378
|
-
|
|
16379
|
-
|
|
16380
|
-
|
|
16381
|
-
|
|
16382
|
-
|
|
16711
|
+
|
|
16712
|
+
// src/repos-check.ts
|
|
16713
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
16714
|
+
import { readFileSync as readFileSync29 } from "fs";
|
|
16715
|
+
import { basename as basename9 } from "path";
|
|
16716
|
+
var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
|
|
16717
|
+
function checkHookSyntax(script, shell) {
|
|
16718
|
+
const spec = resolveShell(shell);
|
|
16719
|
+
const name = basename9(spec.bin).replace(/\.exe$/i, "").toLowerCase();
|
|
16720
|
+
let run2;
|
|
16721
|
+
if (POSIX_SHELL.test(name)) {
|
|
16722
|
+
run2 = spawnSync7(spec.bin, ["-n"], { input: script, encoding: "utf8" });
|
|
16723
|
+
} else if (/^(pwsh|powershell)$/.test(name)) {
|
|
16724
|
+
const parse3 = "$e=$null;[void][System.Management.Automation.Language.Parser]::ParseInput($env:CREW_CHECK_SCRIPT,[ref]$null,[ref]$e);if($e){$e|ForEach-Object{[Console]::Error.WriteLine($_.Message)};exit 1}";
|
|
16725
|
+
run2 = spawnSync7(spec.bin, ["-NoProfile", "-NonInteractive", "-Command", parse3], {
|
|
16726
|
+
encoding: "utf8",
|
|
16727
|
+
env: { ...process.env, CREW_CHECK_SCRIPT: script }
|
|
16728
|
+
});
|
|
16729
|
+
} else {
|
|
16730
|
+
return { status: "unchecked", message: `no syntax check for ${spec.bin}` };
|
|
16383
16731
|
}
|
|
16384
|
-
if (
|
|
16385
|
-
if (
|
|
16386
|
-
|
|
16387
|
-
|
|
16388
|
-
);
|
|
16732
|
+
if (run2.error) return { status: "unchecked", message: `cannot run ${spec.bin}: ${run2.error.message}` };
|
|
16733
|
+
if (run2.status === 0) return { status: "ok" };
|
|
16734
|
+
const message = `${run2.stderr ?? ""}${run2.stdout ?? ""}`.trim() || `${spec.bin} exited ${run2.status}`;
|
|
16735
|
+
return { status: "error", message };
|
|
16389
16736
|
}
|
|
16390
|
-
function
|
|
16391
|
-
const
|
|
16392
|
-
|
|
16393
|
-
|
|
16394
|
-
|
|
16395
|
-
|
|
16396
|
-
|
|
16397
|
-
|
|
16398
|
-
}
|
|
16399
|
-
if (rest.length !== 1) throw new ReposError("expected [route] <name> \u2014 the Repos row to clone (see `crew repos list`)");
|
|
16400
|
-
return { route: route2, name: rest[0] };
|
|
16737
|
+
function renderSamples(cfg, dir) {
|
|
16738
|
+
const checkout = basename9(dir.replace(/[/\\]+$/, ""));
|
|
16739
|
+
const tagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", tag: "TABL-123", prefix: "tabl", lowercase: cfg.branch.lowercase };
|
|
16740
|
+
const untagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", lowercase: cfg.branch.lowercase };
|
|
16741
|
+
return [["with a project tag", tagged], ["without a project tag", untagged]].map(([label, ctx]) => {
|
|
16742
|
+
const branch = renderBranchName(effectiveBranchTemplate(cfg, ctx.prefix), ctx);
|
|
16743
|
+
const worktree = effectiveWorktreeDirName(cfg, dir, branch, "123", (t) => renderWorktreeName(t, { ...ctx, dir: checkout }));
|
|
16744
|
+
return { case: label, branch, worktree };
|
|
16745
|
+
});
|
|
16401
16746
|
}
|
|
16402
|
-
function
|
|
16403
|
-
const
|
|
16404
|
-
const
|
|
16405
|
-
if (!
|
|
16406
|
-
|
|
16407
|
-
throw new ReposError(`the tracker has no Repos row named "${i.name}" for route ${i.route}${known.length ? ` (known: ${known.join(", ")})` : " \u2014 run `crew connect` to refresh them"}`);
|
|
16747
|
+
function checkRepoSpec(dir, opts = {}) {
|
|
16748
|
+
const empty = { parsed: null, effective: null, readiness: [], hooks: [], shadowed: [], samples: [] };
|
|
16749
|
+
const file = findRepoConfig(dir);
|
|
16750
|
+
if (!file) {
|
|
16751
|
+
return { ...empty, file: null, error: `no .crew.yaml, .crew.yml or .crew.json in ${dir}`, exitCode: 1 };
|
|
16408
16752
|
}
|
|
16409
|
-
|
|
16410
|
-
|
|
16411
|
-
|
|
16412
|
-
|
|
16413
|
-
|
|
16414
|
-
|
|
16753
|
+
let parsed;
|
|
16754
|
+
try {
|
|
16755
|
+
parsed = parseRepoConfig(readFileSync29(file, "utf8"), file);
|
|
16756
|
+
} catch (e) {
|
|
16757
|
+
if (!(e instanceof RepoConfigError)) throw e;
|
|
16758
|
+
return { ...empty, file, error: e.message, exitCode: 1 };
|
|
16415
16759
|
}
|
|
16416
|
-
const
|
|
16417
|
-
const
|
|
16418
|
-
const
|
|
16419
|
-
|
|
16420
|
-
|
|
16760
|
+
const effective = resolveRepoConfig(parsed, opts.ship, dir);
|
|
16761
|
+
const readiness = validateEffective(effective);
|
|
16762
|
+
const hooks = Object.keys(effective.hooks).map((name) => ({
|
|
16763
|
+
name,
|
|
16764
|
+
label: hookLabel(effective, name),
|
|
16765
|
+
syntax: checkHookSyntax(effective.hooks[name], effective.shell)
|
|
16766
|
+
}));
|
|
16767
|
+
const failed = hooks.some((h) => h.syntax.status === "error");
|
|
16768
|
+
return {
|
|
16769
|
+
file,
|
|
16770
|
+
parsed,
|
|
16771
|
+
effective,
|
|
16772
|
+
readiness,
|
|
16773
|
+
hooks,
|
|
16774
|
+
shadowed: effective.shadowed,
|
|
16775
|
+
samples: renderSamples(effective, dir),
|
|
16776
|
+
error: null,
|
|
16777
|
+
exitCode: failed || opts.strict && readiness.length > 0 ? 1 : 0
|
|
16778
|
+
};
|
|
16779
|
+
}
|
|
16780
|
+
function renderRepoCheck(r) {
|
|
16781
|
+
if (r.error) return `${r.file ?? "crew repos check"}: ${r.error}
|
|
16782
|
+
`;
|
|
16783
|
+
const e = r.effective;
|
|
16784
|
+
const lines = [
|
|
16785
|
+
`${r.file}`,
|
|
16786
|
+
` platform ${e.platform}`,
|
|
16787
|
+
` release.mode ${e.release.mode}`,
|
|
16788
|
+
` release.versioning ${e.release.versioning}`,
|
|
16789
|
+
` branch.base ${e.branch.base}`,
|
|
16790
|
+
` branch.name ${e.branch.name}`,
|
|
16791
|
+
` branch.push ${e.branch.push}`
|
|
16792
|
+
];
|
|
16793
|
+
for (const s of r.samples) lines.push(` ${s.case}: branch ${s.branch}, worktree ../${s.worktree}`);
|
|
16794
|
+
lines.push(r.hooks.length ? " hooks" : " hooks none");
|
|
16795
|
+
for (const h of r.hooks) {
|
|
16796
|
+
const named = h.label !== e.hooks[h.name] ? ` (${h.label})` : "";
|
|
16797
|
+
const syntax = h.syntax.status === "ok" ? "syntax ok" : h.syntax.status === "unchecked" ? `syntax not checked: ${h.syntax.message}` : "syntax ERROR";
|
|
16798
|
+
lines.push(` ${h.name}${named}: ${syntax}`);
|
|
16799
|
+
if (h.syntax.status === "error") lines.push(...(h.syntax.message ?? "").split("\n").map((l) => ` ${l}`));
|
|
16421
16800
|
}
|
|
16422
|
-
|
|
16423
|
-
const
|
|
16424
|
-
|
|
16425
|
-
|
|
16426
|
-
return { ...add, remote, target };
|
|
16801
|
+
if (r.shadowed.length) lines.push(` this ship declares ${r.shadowed.join(", ")}, and the file overrides it \u2014 the file wins`);
|
|
16802
|
+
for (const p of r.readiness) lines.push(` - release readiness: ${p}`);
|
|
16803
|
+
return `${lines.join("\n")}
|
|
16804
|
+
`;
|
|
16427
16805
|
}
|
|
16428
16806
|
|
|
16429
16807
|
// src/repos-hooks.ts
|
|
16808
|
+
import { isMap as isMap2, parseDocument as parseDocument6, YAMLMap } from "yaml";
|
|
16430
16809
|
function packageManager(f) {
|
|
16431
16810
|
if (f.exists("pnpm-lock.yaml")) return "pnpm";
|
|
16432
16811
|
if (f.exists("yarn.lock")) return "yarn";
|
|
@@ -16567,7 +16946,7 @@ function repoDirOf(text, base, route2, name) {
|
|
|
16567
16946
|
return hit.dir;
|
|
16568
16947
|
}
|
|
16569
16948
|
function planRouteHooks(text, route2, name, hooks, opts = {}) {
|
|
16570
|
-
const doc =
|
|
16949
|
+
const doc = parseDocument6(text);
|
|
16571
16950
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
16572
16951
|
const m = routeMapOf(doc, route2);
|
|
16573
16952
|
const repos = m.get("repos", true);
|
|
@@ -16604,7 +16983,7 @@ function planRouteHooks(text, route2, name, hooks, opts = {}) {
|
|
|
16604
16983
|
return { text: String(doc), written, kept, replaced };
|
|
16605
16984
|
}
|
|
16606
16985
|
function clearRouteHooks(text, route2, name) {
|
|
16607
|
-
const doc =
|
|
16986
|
+
const doc = parseDocument6(text);
|
|
16608
16987
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
16609
16988
|
const m = routeMapOf(doc, route2);
|
|
16610
16989
|
const repos = m.get("repos", true);
|
|
@@ -16887,56 +17266,8 @@ function planSampleTicket(i) {
|
|
|
16887
17266
|
};
|
|
16888
17267
|
}
|
|
16889
17268
|
|
|
16890
|
-
// src/doctor-fix.ts
|
|
16891
|
-
import { parseDocument as parseDocument5 } from "yaml";
|
|
16892
|
-
var sound = (r) => r.hasRepos && r.problems.length === 0;
|
|
16893
|
-
function planFix(f) {
|
|
16894
|
-
const actions = [];
|
|
16895
|
-
const notes = [];
|
|
16896
|
-
for (const r of f.routes) {
|
|
16897
|
-
if (!r.enabled && sound(r)) {
|
|
16898
|
-
actions.push({ kind: "enable", route: r.route });
|
|
16899
|
-
const gaps = r.readiness ?? [];
|
|
16900
|
-
if (gaps.length) {
|
|
16901
|
-
notes.push(`${r.route}: ${gaps.length} release-readiness gap${gaps.length === 1 ? "" : "s"} (${gaps[0]}${gaps.length > 1 ? ", ..." : ""}) \u2014 the route will run, but releases stop at the first missing hook until it is added`);
|
|
16902
|
-
}
|
|
16903
|
-
} else if (!sound(r) && !r.enabled) {
|
|
16904
|
-
notes.push(`${r.route}: not enabled \u2014 ${r.hasRepos ? r.problems[0] : "no checkout is configured"}`);
|
|
16905
|
-
}
|
|
16906
|
-
}
|
|
16907
|
-
if (f.schedulerAbsent && f.routes.some(sound)) {
|
|
16908
|
-
if (f.host === "windows") notes.push("scheduler support is not built for Windows yet \u2014 nothing to install; run `crew run` by hand");
|
|
16909
|
-
else actions.push({ kind: "install" });
|
|
16910
|
-
}
|
|
16911
|
-
return { actions, notes };
|
|
16912
|
-
}
|
|
16913
|
-
async function gatherHealth(c, check) {
|
|
16914
|
-
let problems;
|
|
16915
|
-
let readiness = [];
|
|
16916
|
-
try {
|
|
16917
|
-
const r = await check();
|
|
16918
|
-
if (Array.isArray(r)) problems = r;
|
|
16919
|
-
else ({ problems, readiness } = r);
|
|
16920
|
-
} catch (e) {
|
|
16921
|
-
problems = [`checks could not run: ${e instanceof Error ? e.message : String(e)}`];
|
|
16922
|
-
}
|
|
16923
|
-
return { route: c.route, enabled: c.enabled, hasRepos: c.hasRepos, problems, readiness };
|
|
16924
|
-
}
|
|
16925
|
-
function routesInScope(routes, named) {
|
|
16926
|
-
return named ? routes.filter((r) => r.route === named) : routes;
|
|
16927
|
-
}
|
|
16928
|
-
function planEnable(text, route2) {
|
|
16929
|
-
const doc = parseDocument5(text);
|
|
16930
|
-
routeMapOf(doc, route2).set("enabled", true);
|
|
16931
|
-
return String(doc);
|
|
16932
|
-
}
|
|
16933
|
-
function schedulerInstalled(plan, exists, crontab) {
|
|
16934
|
-
if (plan.mechanism === "cron") return plan.cronMarker !== null && crontab.includes(plan.cronMarker);
|
|
16935
|
-
return plan.unitPaths.length > 0 && plan.unitPaths.every(exists);
|
|
16936
|
-
}
|
|
16937
|
-
|
|
16938
17269
|
// src/worktree-sweep.ts
|
|
16939
|
-
import { existsSync as
|
|
17270
|
+
import { existsSync as existsSync33, readFileSync as readFileSync30, readdirSync as readdirSync12 } from "fs";
|
|
16940
17271
|
import { basename as basename11, dirname as dirname19, join as join35, resolve as resolve4 } from "path";
|
|
16941
17272
|
function planWorktreeSweep(target, tickets, contract) {
|
|
16942
17273
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
@@ -17049,7 +17380,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
17049
17380
|
let removed = 0;
|
|
17050
17381
|
let keptBranches = 0;
|
|
17051
17382
|
for (const a of actions) {
|
|
17052
|
-
if (!dryRun && base && !a.keepBranch &&
|
|
17383
|
+
if (!dryRun && base && !a.keepBranch && existsSync33(a.path) && isHandBackMerge(a.path, base)) {
|
|
17053
17384
|
if (gitOk(a.path, ["merge", "--abort"]) !== null) {
|
|
17054
17385
|
log.emit(
|
|
17055
17386
|
`abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
|
|
@@ -17057,7 +17388,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
17057
17388
|
);
|
|
17058
17389
|
}
|
|
17059
17390
|
}
|
|
17060
|
-
if (!dryRun &&
|
|
17391
|
+
if (!dryRun && existsSync33(a.path)) {
|
|
17061
17392
|
let dirty = false;
|
|
17062
17393
|
try {
|
|
17063
17394
|
dirty = status(a.path).length > 0;
|
|
@@ -17068,7 +17399,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
17068
17399
|
continue;
|
|
17069
17400
|
}
|
|
17070
17401
|
}
|
|
17071
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
17402
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync33(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
17072
17403
|
refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
|
|
17073
17404
|
continue;
|
|
17074
17405
|
}
|
|
@@ -17117,7 +17448,7 @@ function isHandBackMerge(path, base) {
|
|
|
17117
17448
|
if (!msgPath) return false;
|
|
17118
17449
|
let first;
|
|
17119
17450
|
try {
|
|
17120
|
-
first =
|
|
17451
|
+
first = readFileSync30(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
17121
17452
|
} catch {
|
|
17122
17453
|
return false;
|
|
17123
17454
|
}
|
|
@@ -17222,12 +17553,12 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
17222
17553
|
}
|
|
17223
17554
|
|
|
17224
17555
|
// src/stream-sweep.ts
|
|
17225
|
-
import { existsSync as
|
|
17556
|
+
import { existsSync as existsSync34, readdirSync as readdirSync13, rmSync as rmSync15, statSync as statSync10 } from "fs";
|
|
17226
17557
|
import { join as join36 } from "path";
|
|
17227
17558
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
17228
17559
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
17229
17560
|
const dir = join36(stateDir, "streams");
|
|
17230
|
-
if (!
|
|
17561
|
+
if (!existsSync34(dir)) return [];
|
|
17231
17562
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
17232
17563
|
return readdirSync13(dir).map((name) => join36(dir, name)).filter((path) => {
|
|
17233
17564
|
try {
|
|
@@ -17255,7 +17586,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
17255
17586
|
return removed;
|
|
17256
17587
|
}
|
|
17257
17588
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
17258
|
-
if (!
|
|
17589
|
+
if (!existsSync34(stateDir)) return [];
|
|
17259
17590
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
17260
17591
|
return readdirSync13(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join36(stateDir, name)).filter((path) => {
|
|
17261
17592
|
try {
|
|
@@ -17267,11 +17598,11 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
17267
17598
|
}
|
|
17268
17599
|
|
|
17269
17600
|
// src/cli.ts
|
|
17270
|
-
import { realpathSync as realpathSync3, readFileSync as
|
|
17601
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync36, existsSync as existsSync41, mkdirSync as mkdirSync26, writeFileSync as writeFileSync28, rmSync as rmSync19, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync4, statSync as statSync14, readdirSync as readdirSync18 } from "fs";
|
|
17271
17602
|
import { createInterface } from "readline/promises";
|
|
17272
17603
|
|
|
17273
17604
|
// src/periscope-cmd.ts
|
|
17274
|
-
import { existsSync as
|
|
17605
|
+
import { existsSync as existsSync35, mkdirSync as mkdirSync21, readFileSync as readFileSync31, rmSync as rmSync16, writeFileSync as writeFileSync24 } from "fs";
|
|
17275
17606
|
import { join as join37 } from "path";
|
|
17276
17607
|
var PeriscopeError = class extends Error {
|
|
17277
17608
|
};
|
|
@@ -17283,14 +17614,14 @@ function removePeriscopePrior(stateDir, ticketRecordId) {
|
|
|
17283
17614
|
function writePeriscopePrior(stateDir, ticketRecordId, prior) {
|
|
17284
17615
|
try {
|
|
17285
17616
|
mkdirSync21(priorDir(stateDir), { recursive: true });
|
|
17286
|
-
|
|
17617
|
+
writeFileSync24(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
|
|
17287
17618
|
} catch {
|
|
17288
17619
|
}
|
|
17289
17620
|
}
|
|
17290
17621
|
function readPeriscopePrior(stateDir, ticketRecordId) {
|
|
17291
17622
|
try {
|
|
17292
17623
|
const p = priorPath(stateDir, ticketRecordId);
|
|
17293
|
-
return
|
|
17624
|
+
return existsSync35(p) ? JSON.parse(readFileSync31(p, "utf8")) : void 0;
|
|
17294
17625
|
} catch {
|
|
17295
17626
|
return void 0;
|
|
17296
17627
|
}
|
|
@@ -17780,7 +18111,7 @@ import { dirname as dirOf2, resolve as resolvePath } from "path";
|
|
|
17780
18111
|
|
|
17781
18112
|
// src/ssh-keys.ts
|
|
17782
18113
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
17783
|
-
import { existsSync as
|
|
18114
|
+
import { existsSync as existsSync36, mkdirSync as mkdirSync22, readFileSync as readFileSync32, chmodSync as chmodSync3 } from "fs";
|
|
17784
18115
|
import { dirname as dirname20, join as join39 } from "path";
|
|
17785
18116
|
var SshKeyError = class extends Error {
|
|
17786
18117
|
};
|
|
@@ -17790,7 +18121,7 @@ function shipSshKeyPathFor(stateDir) {
|
|
|
17790
18121
|
function ensureShipSshKeypair(stateDir) {
|
|
17791
18122
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
17792
18123
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
17793
|
-
if (!
|
|
18124
|
+
if (!existsSync36(privateKeyPath)) {
|
|
17794
18125
|
mkdirSync22(dirname20(privateKeyPath), { recursive: true });
|
|
17795
18126
|
try {
|
|
17796
18127
|
execFileSync8("ssh-keygen", [
|
|
@@ -17811,10 +18142,10 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
17811
18142
|
}
|
|
17812
18143
|
chmodSync3(privateKeyPath, 384);
|
|
17813
18144
|
}
|
|
17814
|
-
if (!
|
|
18145
|
+
if (!existsSync36(publicKeyPath)) {
|
|
17815
18146
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
17816
18147
|
}
|
|
17817
|
-
const publicKey =
|
|
18148
|
+
const publicKey = readFileSync32(publicKeyPath, "utf8").trim();
|
|
17818
18149
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
17819
18150
|
}
|
|
17820
18151
|
function sshKeygenAvailable() {
|
|
@@ -17826,6 +18157,26 @@ function sshKeygenAvailable() {
|
|
|
17826
18157
|
}
|
|
17827
18158
|
}
|
|
17828
18159
|
|
|
18160
|
+
// src/relay-probe.ts
|
|
18161
|
+
async function probeRelayControl(relayUrl, apiKey, userAgent, fetchFn = fetch, timeoutMs = 1e4) {
|
|
18162
|
+
const started = Date.now();
|
|
18163
|
+
try {
|
|
18164
|
+
const res = await fetchFn(`${relayUrl.replace(/\/+$/, "")}/control/status`, {
|
|
18165
|
+
headers: { Authorization: `Bearer ${apiKey}`, "User-Agent": userAgent },
|
|
18166
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
18167
|
+
});
|
|
18168
|
+
const latencyMs = Date.now() - started;
|
|
18169
|
+
await res.arrayBuffer().catch(() => void 0);
|
|
18170
|
+
if (res.ok) return { ok: true, status: res.status, latencyMs };
|
|
18171
|
+
return { ok: false, status: res.status, error: `HTTP ${res.status}` };
|
|
18172
|
+
} catch (e) {
|
|
18173
|
+
return { ok: false, error: e.message };
|
|
18174
|
+
}
|
|
18175
|
+
}
|
|
18176
|
+
function describeRelayProbe(url, p) {
|
|
18177
|
+
return p.ok ? `relay control: OK ${url}/control/status (${p.status}, ${p.latencyMs} ms)` : `relay control: FAILED ${url}/control/status \u2014 ${p.error}`;
|
|
18178
|
+
}
|
|
18179
|
+
|
|
17829
18180
|
// src/passenger-containers.ts
|
|
17830
18181
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
17831
18182
|
import { createHash as createHash7 } from "crypto";
|
|
@@ -18031,7 +18382,7 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
18031
18382
|
|
|
18032
18383
|
// src/tunnel.ts
|
|
18033
18384
|
import { spawn as spawn5 } from "child_process";
|
|
18034
|
-
import { existsSync as
|
|
18385
|
+
import { existsSync as existsSync37, mkdirSync as mkdirSync23, readFileSync as readFileSync33, writeFileSync as writeFileSync25, unlinkSync as unlinkSync3, readdirSync as readdirSync14 } from "fs";
|
|
18035
18386
|
import { randomInt } from "crypto";
|
|
18036
18387
|
import { join as join40 } from "path";
|
|
18037
18388
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
@@ -18043,15 +18394,21 @@ function generateTunnelSlug() {
|
|
|
18043
18394
|
for (let i = 0; i < SLUG_LENGTH; i++) out += SLUG_ALPHABET[randomInt(SLUG_ALPHABET.length)];
|
|
18044
18395
|
return out;
|
|
18045
18396
|
}
|
|
18046
|
-
function bindAddrFor(o) {
|
|
18047
|
-
const base = `workspace-${o.workspaceId}-slug-${o.slug}-key-${o.apiKey}`;
|
|
18048
|
-
return o.mcpSecret ? `${base}-secret-${o.mcpSecret}` : base;
|
|
18397
|
+
function bindAddrFor(o, path) {
|
|
18398
|
+
const base = `workspace-${o.workspaceId}-slug-${o.slug}${path ? `-path-${path}` : ""}-key-${o.apiKey}`;
|
|
18399
|
+
return o.mcpSecret && !path ? `${base}-secret-${o.mcpSecret}` : base;
|
|
18049
18400
|
}
|
|
18050
|
-
function
|
|
18401
|
+
function relayBaseUrlFor(slug, relayHost, relayHttpPort, relayPublicDomain) {
|
|
18051
18402
|
const domain = relayPublicDomain ?? relayHost.replace(/^.*@/, "");
|
|
18052
|
-
return `https://${slug}.${domain}${relayHttpPort === 443 ? "" : `:${relayHttpPort}`}
|
|
18403
|
+
return `https://${slug}.${domain}${relayHttpPort === 443 ? "" : `:${relayHttpPort}`}`;
|
|
18404
|
+
}
|
|
18405
|
+
function publicUrlFor(slug, relayHost, relayHttpPort, relayPublicDomain) {
|
|
18406
|
+
return `${relayBaseUrlFor(slug, relayHost, relayHttpPort, relayPublicDomain)}/mcp`;
|
|
18053
18407
|
}
|
|
18054
18408
|
function sshArgsFor(o) {
|
|
18409
|
+
const forwards = [];
|
|
18410
|
+
if (o.localPort !== void 0) forwards.push("-R", `${bindAddrFor(o)}:0:127.0.0.1:${o.localPort}`);
|
|
18411
|
+
if (o.controlSocket) forwards.push("-R", `${bindAddrFor(o, "control")}:0:${o.controlSocket}`);
|
|
18055
18412
|
return [
|
|
18056
18413
|
"-N",
|
|
18057
18414
|
"-o",
|
|
@@ -18066,8 +18423,7 @@ function sshArgsFor(o) {
|
|
|
18066
18423
|
o.privateKeyPath,
|
|
18067
18424
|
"-p",
|
|
18068
18425
|
String(o.relayPort),
|
|
18069
|
-
|
|
18070
|
-
`${bindAddrFor(o)}:0:127.0.0.1:${o.localPort}`,
|
|
18426
|
+
...forwards,
|
|
18071
18427
|
o.relayHost
|
|
18072
18428
|
];
|
|
18073
18429
|
}
|
|
@@ -18104,9 +18460,9 @@ function tunnelStatePath(stateDir, workspaceId) {
|
|
|
18104
18460
|
}
|
|
18105
18461
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
18106
18462
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
18107
|
-
if (!
|
|
18463
|
+
if (!existsSync37(path)) return void 0;
|
|
18108
18464
|
try {
|
|
18109
|
-
return JSON.parse(
|
|
18465
|
+
return JSON.parse(readFileSync33(path, "utf8"));
|
|
18110
18466
|
} catch {
|
|
18111
18467
|
return void 0;
|
|
18112
18468
|
}
|
|
@@ -18114,7 +18470,7 @@ function readPersistedTunnel(stateDir, workspaceId) {
|
|
|
18114
18470
|
function writePersistedTunnel(stateDir, state) {
|
|
18115
18471
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
18116
18472
|
mkdirSync23(join40(stateDir, "passengers"), { recursive: true });
|
|
18117
|
-
|
|
18473
|
+
writeFileSync25(path, `${JSON.stringify(state, null, 2)}
|
|
18118
18474
|
`);
|
|
18119
18475
|
}
|
|
18120
18476
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -18138,16 +18494,19 @@ function isPidAlive(pid) {
|
|
|
18138
18494
|
return false;
|
|
18139
18495
|
}
|
|
18140
18496
|
}
|
|
18497
|
+
var CONTROL_ONLY_HASH = "control-only";
|
|
18141
18498
|
function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, stateDir, mcpSecrets = /* @__PURE__ */ new Map(), deps = {}) {
|
|
18142
18499
|
const alive = deps.isPidAlive ?? isPidAlive;
|
|
18143
18500
|
const plannedIds = new Set(plans.map((p) => p.workspaceId));
|
|
18144
18501
|
for (const plan of plans) {
|
|
18145
18502
|
const existing = readPersistedTunnel(stateDir, plan.workspaceId);
|
|
18503
|
+
const mountsHash = plan.mountsHash ?? CONTROL_ONLY_HASH;
|
|
18504
|
+
const controlUnchanged = existing?.controlSocket === deps.controlSocket;
|
|
18146
18505
|
const currentSecret = mcpSecrets.get(plan.workspaceId);
|
|
18147
18506
|
const secretUnchanged = (existing?.mcpSecret ?? void 0) === (currentSecret ?? void 0);
|
|
18148
18507
|
const slug = existing?.slug ?? generateTunnelSlug();
|
|
18149
18508
|
const slugUnchanged = existing?.liveSlug === slug;
|
|
18150
|
-
if (existing && alive(existing.pid) && existing.mountsHash ===
|
|
18509
|
+
if (existing && alive(existing.pid) && existing.mountsHash === mountsHash && secretUnchanged && slugUnchanged && controlUnchanged) {
|
|
18151
18510
|
if (existing.status === "connecting") {
|
|
18152
18511
|
const settled = { ...existing, status: "connected" };
|
|
18153
18512
|
writePersistedTunnel(stateDir, settled);
|
|
@@ -18174,6 +18533,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
18174
18533
|
slug,
|
|
18175
18534
|
apiKey,
|
|
18176
18535
|
localPort: plan.port,
|
|
18536
|
+
controlSocket: deps.controlSocket,
|
|
18177
18537
|
privateKeyPath,
|
|
18178
18538
|
mcpSecret: currentSecret,
|
|
18179
18539
|
spawnFn: deps.spawnFn,
|
|
@@ -18183,9 +18543,10 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
18183
18543
|
});
|
|
18184
18544
|
const started = {
|
|
18185
18545
|
pid: tunnel.pid ?? -1,
|
|
18186
|
-
containerName: plan.containerName,
|
|
18546
|
+
containerName: plan.containerName ?? "",
|
|
18187
18547
|
workspaceId: plan.workspaceId,
|
|
18188
|
-
mountsHash
|
|
18548
|
+
mountsHash,
|
|
18549
|
+
controlSocket: deps.controlSocket,
|
|
18189
18550
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18190
18551
|
status: settledStatus,
|
|
18191
18552
|
mcpSecret: currentSecret,
|
|
@@ -18212,7 +18573,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
18212
18573
|
}
|
|
18213
18574
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
18214
18575
|
const dir = join40(stateDir, "passengers");
|
|
18215
|
-
if (!
|
|
18576
|
+
if (!existsSync37(dir)) return [];
|
|
18216
18577
|
return readdirSync14(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
18217
18578
|
}
|
|
18218
18579
|
|
|
@@ -18333,7 +18694,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
18333
18694
|
import { createServer as createHttpServer } from "http";
|
|
18334
18695
|
import { spawn as spawn6 } from "child_process";
|
|
18335
18696
|
import { randomBytes } from "crypto";
|
|
18336
|
-
import { existsSync as
|
|
18697
|
+
import { existsSync as existsSync38, mkdirSync as mkdirSync24, readFileSync as readFileSync34, writeFileSync as writeFileSync26, unlinkSync as unlinkSync4, readdirSync as readdirSync15 } from "fs";
|
|
18337
18698
|
import { join as join41 } from "path";
|
|
18338
18699
|
|
|
18339
18700
|
// src/secret-compare.ts
|
|
@@ -18427,9 +18788,9 @@ function syncDaemonStatePath(stateDir, workspaceId) {
|
|
|
18427
18788
|
}
|
|
18428
18789
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
18429
18790
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
18430
|
-
if (!
|
|
18791
|
+
if (!existsSync38(path)) return void 0;
|
|
18431
18792
|
try {
|
|
18432
|
-
return JSON.parse(
|
|
18793
|
+
return JSON.parse(readFileSync34(path, "utf8"));
|
|
18433
18794
|
} catch {
|
|
18434
18795
|
return void 0;
|
|
18435
18796
|
}
|
|
@@ -18437,7 +18798,7 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
|
18437
18798
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
18438
18799
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
18439
18800
|
mkdirSync24(join41(stateDir, "passengers"), { recursive: true });
|
|
18440
|
-
|
|
18801
|
+
writeFileSync26(path, `${JSON.stringify(state, null, 2)}
|
|
18441
18802
|
`);
|
|
18442
18803
|
}
|
|
18443
18804
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -18458,7 +18819,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
18458
18819
|
}
|
|
18459
18820
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
18460
18821
|
const dir = join41(stateDir, "passengers");
|
|
18461
|
-
if (!
|
|
18822
|
+
if (!existsSync38(dir)) return [];
|
|
18462
18823
|
const suffix = "-sync.json";
|
|
18463
18824
|
return readdirSync15(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
18464
18825
|
}
|
|
@@ -18546,7 +18907,7 @@ function describeCause(err3) {
|
|
|
18546
18907
|
}
|
|
18547
18908
|
|
|
18548
18909
|
// src/daemon.ts
|
|
18549
|
-
import { existsSync as
|
|
18910
|
+
import { existsSync as existsSync39, mkdirSync as mkdirSync25, readFileSync as readFileSync35, readdirSync as readdirSync16, rmSync as rmSync17, statSync as statSync12, writeFileSync as writeFileSync27 } from "fs";
|
|
18550
18911
|
import { join as join42 } from "path";
|
|
18551
18912
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
18552
18913
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
@@ -18627,7 +18988,7 @@ function computeTreeSignature(paths) {
|
|
|
18627
18988
|
}
|
|
18628
18989
|
function makeStaleChecker(paths, opts = {}) {
|
|
18629
18990
|
const initial = computeTreeSignature(paths);
|
|
18630
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
18991
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync39(opts.reloadSentinel);
|
|
18631
18992
|
}
|
|
18632
18993
|
function reloadSentinelFor(stateDir) {
|
|
18633
18994
|
return join42(stateDir, "reload-requested");
|
|
@@ -18638,12 +18999,12 @@ function daemonStampFor(stateDir) {
|
|
|
18638
18999
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
18639
19000
|
mkdirSync25(stateDir, { recursive: true });
|
|
18640
19001
|
rmSync17(reloadSentinelFor(stateDir), { force: true });
|
|
18641
|
-
|
|
19002
|
+
writeFileSync27(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
18642
19003
|
`);
|
|
18643
19004
|
}
|
|
18644
19005
|
function readDaemonStarted(stateDir) {
|
|
18645
19006
|
try {
|
|
18646
|
-
const v = JSON.parse(
|
|
19007
|
+
const v = JSON.parse(readFileSync35(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
18647
19008
|
return typeof v === "number" ? v : null;
|
|
18648
19009
|
} catch {
|
|
18649
19010
|
return null;
|
|
@@ -19060,7 +19421,7 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
|
|
|
19060
19421
|
const target = repoTargetFor(route2, result.ticket.repo_id);
|
|
19061
19422
|
ticketRepo = target?.name;
|
|
19062
19423
|
try {
|
|
19063
|
-
if (target && ensureRepoCheckout(target.dir, target.remote)) {
|
|
19424
|
+
if (target && ensureRepoCheckout(target.dir, target.remote, { scheme: ship.cloneScheme })) {
|
|
19064
19425
|
emit.emit(`cloned ${target.name} into ${target.dir}`, { step: "select", role });
|
|
19065
19426
|
}
|
|
19066
19427
|
} catch (e) {
|
|
@@ -19165,7 +19526,7 @@ import { createHash as createHash9 } from "crypto";
|
|
|
19165
19526
|
import {
|
|
19166
19527
|
chmodSync as chmodSync4,
|
|
19167
19528
|
closeSync as closeSync5,
|
|
19168
|
-
existsSync as
|
|
19529
|
+
existsSync as existsSync40,
|
|
19169
19530
|
fstatSync,
|
|
19170
19531
|
openSync as openSync5,
|
|
19171
19532
|
readSync as readSync3,
|
|
@@ -19516,7 +19877,7 @@ function lastLines2(path, n) {
|
|
|
19516
19877
|
}
|
|
19517
19878
|
function readTail(live, opts) {
|
|
19518
19879
|
const max = opts.maxBytes ?? MAX_TAIL_BYTES;
|
|
19519
|
-
if (!
|
|
19880
|
+
if (!existsSync40(live)) {
|
|
19520
19881
|
return { lines: [], cursor: opts.after ?? encodeCursor({ dev: 0, ino: 0, off: 0 }), reset: false };
|
|
19521
19882
|
}
|
|
19522
19883
|
const cur = opts.after ? decodeCursor(opts.after) : null;
|
|
@@ -19673,7 +20034,7 @@ async function route(deps, method, url, req, abort) {
|
|
|
19673
20034
|
const run2 = decodeURIComponent(p.slice("/streams/".length));
|
|
19674
20035
|
if (!RUN_NAME_RE.test(run2) || run2.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run2)) return err2(400, "not a run name");
|
|
19675
20036
|
const file = join43(deps.stateDir, "streams", run2.endsWith(".jsonl") ? run2 : `${run2}.jsonl`);
|
|
19676
|
-
if (!
|
|
20037
|
+
if (!existsSync40(file)) return err2(404, "no such run");
|
|
19677
20038
|
return tailReply(file, url.searchParams, false, abort);
|
|
19678
20039
|
}
|
|
19679
20040
|
if ((p === "/pause" || p === "/resume") && post3) {
|
|
@@ -19717,7 +20078,7 @@ function send(res, r) {
|
|
|
19717
20078
|
res.end(body);
|
|
19718
20079
|
}
|
|
19719
20080
|
async function socketIsStale(path) {
|
|
19720
|
-
if (!
|
|
20081
|
+
if (!existsSync40(path)) return false;
|
|
19721
20082
|
return new Promise((resolve6) => {
|
|
19722
20083
|
const c = createConnection(path);
|
|
19723
20084
|
c.once("connect", () => {
|
|
@@ -19736,7 +20097,7 @@ async function startControlServer(deps) {
|
|
|
19736
20097
|
return null;
|
|
19737
20098
|
}
|
|
19738
20099
|
if (await socketIsStale(path)) rmSync18(path, { force: true });
|
|
19739
|
-
else if (
|
|
20100
|
+
else if (existsSync40(path)) {
|
|
19740
20101
|
log(`control API not started: ${path} is in use by another process`);
|
|
19741
20102
|
return null;
|
|
19742
20103
|
}
|
|
@@ -19945,6 +20306,9 @@ function usage(problem, requested = false) {
|
|
|
19945
20306
|
crew --version [--json] print the crew version on one line (also: crew version); --json adds home and compiled
|
|
19946
20307
|
crew status [route] [--json] paused/running state; --json for a machine reader
|
|
19947
20308
|
crew projects [--route R] [--json] the Projects rows each route can read (id, name, prefix, work mode)
|
|
20309
|
+
crew provision --key K --route WS[/PROJECT] [--forge-host H] [--ship-name N] [--json] one non-interactive run: connect, forge tokens,
|
|
20310
|
+
enable routes, install units with lingering, start the daemon; safe to re-run (CREW_CONNECT_KEY,
|
|
20311
|
+
CREW_PROVISION_ROUTES, CREW_SHIP_NAME, CREW_FORGE_TOKEN[_<HOST>] also read)
|
|
19948
20312
|
crew forge [set|rotate <host>] [--token T | --token-stdin] [--source file|keychain|env] this ship's git-forge tokens (ship.forges)
|
|
19949
20313
|
crew credential get|store|erase git credential helper answering from ship.forges (git runs it; not for hand use)
|
|
19950
20314
|
crew whoami [route] [--json] the signed-in identity, key role, operator Crew row and credential source per route
|
|
@@ -20045,6 +20409,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
20045
20409
|
"agents",
|
|
20046
20410
|
"connect",
|
|
20047
20411
|
"credential",
|
|
20412
|
+
"provision",
|
|
20048
20413
|
"forge",
|
|
20049
20414
|
"daemon",
|
|
20050
20415
|
"deploy",
|
|
@@ -20120,7 +20485,7 @@ function daemonConfigPaths(c) {
|
|
|
20120
20485
|
for (const r of c.routes) if (r.apiKeyFile && !files.includes(r.apiKeyFile)) files.push(r.apiKeyFile);
|
|
20121
20486
|
return files;
|
|
20122
20487
|
}
|
|
20123
|
-
var STATE_WRITING_COMMANDS = /* @__PURE__ */ new Set(["connect", "disconnect", "install", "uninstall", "reload", "release", "merge", "deploy"]);
|
|
20488
|
+
var STATE_WRITING_COMMANDS = /* @__PURE__ */ new Set(["connect", "provision", "disconnect", "install", "uninstall", "reload", "release", "merge", "deploy"]);
|
|
20124
20489
|
var STATE_WRITING_SUBCOMMANDS = {
|
|
20125
20490
|
agents: /* @__PURE__ */ new Set(["sync"]),
|
|
20126
20491
|
skills: /* @__PURE__ */ new Set(["sync"])
|
|
@@ -20134,6 +20499,19 @@ if (sessionRole && (STATE_WRITING_COMMANDS.has(command) || STATE_WRITING_SUBCOMM
|
|
|
20134
20499
|
);
|
|
20135
20500
|
process.exit(2);
|
|
20136
20501
|
}
|
|
20502
|
+
if (command === "provision") {
|
|
20503
|
+
const outcome = await runProvision(
|
|
20504
|
+
provisionOptionsFrom(argv, process.env),
|
|
20505
|
+
realProvisionDeps(CREW_HOME),
|
|
20506
|
+
(l) => {
|
|
20507
|
+
process.stdout.write(l);
|
|
20508
|
+
},
|
|
20509
|
+
(l) => {
|
|
20510
|
+
process.stderr.write(l);
|
|
20511
|
+
}
|
|
20512
|
+
);
|
|
20513
|
+
process.exit(outcome.code);
|
|
20514
|
+
}
|
|
20137
20515
|
if (command === "passenger-sync-daemon") {
|
|
20138
20516
|
runSyncDaemonFromEnv();
|
|
20139
20517
|
} else {
|
|
@@ -20148,6 +20526,11 @@ if (command === "passenger-sync-daemon") {
|
|
|
20148
20526
|
}, parseTicketNumber = function(input) {
|
|
20149
20527
|
const m = /^(?:[A-Za-z]+-)?(\d+)$/.exec(input.trim());
|
|
20150
20528
|
return m ? m[1] : null;
|
|
20529
|
+
}, routeRelayUrl = function(route3) {
|
|
20530
|
+
const ws = route3.resolved?.workspaceId;
|
|
20531
|
+
const slug = ws ? readPersistedTunnel(cfg.ship.stateDir, ws)?.slug : void 0;
|
|
20532
|
+
if (!cfg.ship.relayHost || !slug) return null;
|
|
20533
|
+
return relayBaseUrlFor(slug, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain);
|
|
20151
20534
|
}, passengerApiKeys = function(routes) {
|
|
20152
20535
|
const keys = /* @__PURE__ */ new Map();
|
|
20153
20536
|
for (const r of routes) {
|
|
@@ -20195,7 +20578,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20195
20578
|
const unsafe = baseBranchUnsafe(out);
|
|
20196
20579
|
return { action: out.action, unsafe, detail: unsafe ? describeUnsafeBase(out, remote, base) : out.detail };
|
|
20197
20580
|
};
|
|
20198
|
-
requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth;
|
|
20581
|
+
requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, routeRelayUrl2 = routeRelayUrl, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth;
|
|
20199
20582
|
let cfg;
|
|
20200
20583
|
let firstRun;
|
|
20201
20584
|
let firstRunFromFlags = false;
|
|
@@ -20211,7 +20594,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20211
20594
|
};
|
|
20212
20595
|
if (command === "connect" && !flag("sign-out") && usesFlagAnswers(wizardFlags, connectIsTTY) && shouldRunWizard({ isTTY: true, crewHome: CREW_HOME })) {
|
|
20213
20596
|
const flagged = flagWizardAnswers(wizardFlags, {
|
|
20214
|
-
hostname:
|
|
20597
|
+
hostname: hostname3(),
|
|
20215
20598
|
platform: hostPlatform(),
|
|
20216
20599
|
pathEnv: process.env.PATH ?? "",
|
|
20217
20600
|
nodePath: process.execPath,
|
|
@@ -20236,7 +20619,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20236
20619
|
let answers;
|
|
20237
20620
|
try {
|
|
20238
20621
|
answers = await runWizard(prompter, {
|
|
20239
|
-
hostname:
|
|
20622
|
+
hostname: hostname3(),
|
|
20240
20623
|
platform: hostPlatform(),
|
|
20241
20624
|
pathEnv: process.env.PATH ?? "",
|
|
20242
20625
|
nodePath: process.execPath,
|
|
@@ -20251,7 +20634,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20251
20634
|
} else {
|
|
20252
20635
|
const toleratesNoRoutes = command === "credential" || command === "forge" || command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
|
|
20253
20636
|
const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
|
|
20254
|
-
if (configless && !
|
|
20637
|
+
if (configless && !existsSync41(findConfigFile(CREW_HOME))) {
|
|
20255
20638
|
cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
|
|
20256
20639
|
} else {
|
|
20257
20640
|
cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
|
|
@@ -20266,7 +20649,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20266
20649
|
throw e;
|
|
20267
20650
|
}
|
|
20268
20651
|
if (command === "credential") {
|
|
20269
|
-
const r = runCredential(positional[1],
|
|
20652
|
+
const r = runCredential(positional[1], readFileSync36(0, "utf8"), cfg.ship.forges, cfg.ship.stateDir);
|
|
20270
20653
|
process.stdout.write(r.out);
|
|
20271
20654
|
process.stderr.write(r.err);
|
|
20272
20655
|
process.exit(r.code);
|
|
@@ -20278,9 +20661,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
20278
20661
|
forges: cfg.ship.forges,
|
|
20279
20662
|
stateDir: cfg.ship.stateDir,
|
|
20280
20663
|
configFile: cfg.configFile,
|
|
20281
|
-
readFile: (p) =>
|
|
20282
|
-
writeFile: (p, t) =>
|
|
20283
|
-
stdin: () =>
|
|
20664
|
+
readFile: (p) => readFileSync36(p, "utf8"),
|
|
20665
|
+
writeFile: (p, t) => writeFileSync28(p, t),
|
|
20666
|
+
stdin: () => readFileSync36(0, "utf8"),
|
|
20284
20667
|
env: process.env
|
|
20285
20668
|
}
|
|
20286
20669
|
);
|
|
@@ -20328,7 +20711,10 @@ if (command === "passenger-sync-daemon") {
|
|
|
20328
20711
|
async function syncHostPassengers() {
|
|
20329
20712
|
if (dryRun) return;
|
|
20330
20713
|
const passengerRoutes = cfg.routes.filter((r) => r.enabled && r.hostPassengers);
|
|
20331
|
-
if (passengerRoutes.length === 0)
|
|
20714
|
+
if (passengerRoutes.length === 0) {
|
|
20715
|
+
await syncShipTunnels([], /* @__PURE__ */ new Map(), []);
|
|
20716
|
+
return;
|
|
20717
|
+
}
|
|
20332
20718
|
if (!dockerAvailable()) {
|
|
20333
20719
|
const pathProblem = dockerPathProblem(cfg.ship, COMMON_DOCKER_DIRS);
|
|
20334
20720
|
emit.warn(
|
|
@@ -20370,37 +20756,64 @@ if (command === "passenger-sync-daemon") {
|
|
|
20370
20756
|
{ step: "passengers", data: { ...result } }
|
|
20371
20757
|
);
|
|
20372
20758
|
}
|
|
20373
|
-
|
|
20374
|
-
try {
|
|
20375
|
-
const { privateKeyPath } = ensureShipSshKeypair(cfg.ship.stateDir);
|
|
20376
|
-
const plans = planContainers(cfg);
|
|
20377
|
-
const mcpSecrets = new Map([...syncEndpoints].map(([workspaceId, e]) => [workspaceId, e.secret]));
|
|
20378
|
-
const relayHost = cfg.ship.relayHost;
|
|
20379
|
-
const reconcile = endpointReconcileDeps(passengerRoutes, relayHost);
|
|
20380
|
-
const pending = [];
|
|
20381
|
-
syncPassengerTunnels(plans, relayHost, cfg.ship.relayPort, privateKeyPath, cfg.ship.stateDir, mcpSecrets, {
|
|
20382
|
-
apiKeys: passengerApiKeys(passengerRoutes),
|
|
20383
|
-
onStatus: (workspaceId, status2, info) => {
|
|
20384
|
-
const owningRoute = passengerRoutes.find((r) => r.resolved?.workspaceId === workspaceId);
|
|
20385
|
-
if (!owningRoute) return;
|
|
20386
|
-
pending.push(
|
|
20387
|
-
new Tracker(owningRoute, cfg.ship).updateTunnelState(cfg.ship.name, { tunnel_status: status2 }).catch((e) => {
|
|
20388
|
-
emit.warn(`could not write tunnel_status: ${e.message}`, { step: "passengers" });
|
|
20389
|
-
})
|
|
20390
|
-
);
|
|
20391
|
-
if (status2 === "disconnected") pending.push(unregisterEndpoint(workspaceId, info, reconcile));
|
|
20392
|
-
}
|
|
20393
|
-
});
|
|
20394
|
-
for (const plan of plans) pending.push(reconcileEndpoint(plan.workspaceId, reconcile));
|
|
20395
|
-
await Promise.all(pending);
|
|
20396
|
-
} catch (e) {
|
|
20397
|
-
emit.warn(`passenger tunnel sync failed: ${e.message}`, { step: "passengers" });
|
|
20398
|
-
}
|
|
20399
|
-
}
|
|
20759
|
+
await syncShipTunnels(planContainers(cfg), new Map([...syncEndpoints].map(([workspaceId, e]) => [workspaceId, e.secret])), passengerRoutes);
|
|
20400
20760
|
} catch (e) {
|
|
20401
20761
|
emit.warn(`passenger container sync failed: ${e.message}`, { step: "passengers" });
|
|
20402
20762
|
}
|
|
20403
20763
|
}
|
|
20764
|
+
async function syncShipTunnels(containerPlans, mcpSecrets, passengerRoutes) {
|
|
20765
|
+
if (!cfg.ship.relayHost) return;
|
|
20766
|
+
const relayHost = cfg.ship.relayHost;
|
|
20767
|
+
try {
|
|
20768
|
+
const { privateKeyPath } = ensureShipSshKeypair(cfg.ship.stateDir);
|
|
20769
|
+
const controlRoutes = cfg.routes.filter((r) => r.enabled && r.resolved?.workspaceId);
|
|
20770
|
+
const planned = new Set(containerPlans.map((p) => p.workspaceId));
|
|
20771
|
+
const plans = [...containerPlans];
|
|
20772
|
+
for (const r of controlRoutes) {
|
|
20773
|
+
const ws = r.resolved.workspaceId;
|
|
20774
|
+
if (planned.has(ws)) continue;
|
|
20775
|
+
planned.add(ws);
|
|
20776
|
+
plans.push({ workspaceId: ws });
|
|
20777
|
+
}
|
|
20778
|
+
const apiRoutes = controlRoutes;
|
|
20779
|
+
const reconcile = endpointReconcileDeps(passengerRoutes, relayHost);
|
|
20780
|
+
const pending = [];
|
|
20781
|
+
syncPassengerTunnels(plans, relayHost, cfg.ship.relayPort, privateKeyPath, cfg.ship.stateDir, mcpSecrets, {
|
|
20782
|
+
apiKeys: passengerApiKeys(apiRoutes),
|
|
20783
|
+
controlSocket: controlSocketFor(cfg.ship.stateDir),
|
|
20784
|
+
onStatus: (workspaceId, status2, info) => {
|
|
20785
|
+
const owningRoute = apiRoutes.find((r) => r.resolved?.workspaceId === workspaceId);
|
|
20786
|
+
if (!owningRoute) return;
|
|
20787
|
+
pending.push(
|
|
20788
|
+
new Tracker(owningRoute, cfg.ship).updateTunnelState(cfg.ship.name, { tunnel_status: status2 }).catch((e) => {
|
|
20789
|
+
emit.warn(`could not write tunnel_status: ${e.message}`, { step: "passengers" });
|
|
20790
|
+
})
|
|
20791
|
+
);
|
|
20792
|
+
if (status2 === "disconnected") pending.push(unregisterEndpoint(workspaceId, info, reconcile));
|
|
20793
|
+
}
|
|
20794
|
+
});
|
|
20795
|
+
for (const plan of containerPlans) pending.push(reconcileEndpoint(plan.workspaceId, reconcile));
|
|
20796
|
+
for (const plan of plans) pending.push(reconcileRelayUrl(plan.workspaceId, apiRoutes));
|
|
20797
|
+
await Promise.all(pending);
|
|
20798
|
+
} catch (e) {
|
|
20799
|
+
emit.warn(`ship tunnel sync failed: ${e.message}`, { step: "passengers" });
|
|
20800
|
+
}
|
|
20801
|
+
}
|
|
20802
|
+
async function reconcileRelayUrl(workspaceId, routes) {
|
|
20803
|
+
if (!cfg.ship.relayHost) return;
|
|
20804
|
+
const persisted = readPersistedTunnel(cfg.ship.stateDir, workspaceId);
|
|
20805
|
+
if (!persisted?.slug || persisted.status !== "connected") return;
|
|
20806
|
+
const url = relayBaseUrlFor(persisted.slug, cfg.ship.relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain);
|
|
20807
|
+
if (persisted.relayUrl === url) return;
|
|
20808
|
+
const route3 = routes.find((r) => r.resolved?.workspaceId === workspaceId);
|
|
20809
|
+
if (!route3) return;
|
|
20810
|
+
try {
|
|
20811
|
+
await new Tracker(route3, cfg.ship).updateRelayUrl(cfg.ship.name, url);
|
|
20812
|
+
updatePersistedTunnel(cfg.ship.stateDir, workspaceId, { relayUrl: url });
|
|
20813
|
+
} catch (e) {
|
|
20814
|
+
emit.warn(`could not write relay_url: ${e.message}`, { step: "passengers" });
|
|
20815
|
+
}
|
|
20816
|
+
}
|
|
20404
20817
|
async function releaseFleet(opts = {}) {
|
|
20405
20818
|
if (releaseRefused()) return;
|
|
20406
20819
|
precheckTickets.clear();
|
|
@@ -20504,7 +20917,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20504
20917
|
}
|
|
20505
20918
|
const hasSystemd = detectSystemd();
|
|
20506
20919
|
for (const job of ["run", "release", "passengers"]) {
|
|
20507
|
-
const plan = planInstall(cfg.ship, CREW_HOME, host, hasSystemd, job);
|
|
20920
|
+
const plan = planInstall(cfg.ship, CREW_HOME, host, hasSystemd, job, installContext());
|
|
20508
20921
|
process.stdout.write(`installing ${job} via ${plan.mechanism} for ${host}${dryRun ? " (dry run)" : ""}
|
|
20509
20922
|
`);
|
|
20510
20923
|
await applyInstall(plan, CREW_HOME, dryRun, {
|
|
@@ -20517,13 +20930,13 @@ if (command === "passenger-sync-daemon") {
|
|
|
20517
20930
|
if (host === "linux" && hasSystemd) {
|
|
20518
20931
|
const label = labelFor(CREW_HOME, "run");
|
|
20519
20932
|
const staleTimer = join44(homedir10(), ".config", "systemd", "user", `${label}.timer`);
|
|
20520
|
-
if (
|
|
20933
|
+
if (existsSync41(staleTimer)) {
|
|
20521
20934
|
process.stdout.write(
|
|
20522
20935
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
20523
20936
|
`
|
|
20524
20937
|
);
|
|
20525
20938
|
if (!dryRun) {
|
|
20526
|
-
|
|
20939
|
+
spawnSync8("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
|
|
20527
20940
|
try {
|
|
20528
20941
|
unlinkSync5(staleTimer);
|
|
20529
20942
|
} catch {
|
|
@@ -20544,7 +20957,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20544
20957
|
const tracker = new Tracker(c, cfg.ship);
|
|
20545
20958
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
20546
20959
|
for (const r of await resolvedRepos(c)) {
|
|
20547
|
-
if (!
|
|
20960
|
+
if (!existsSync41(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
20548
20961
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
20549
20962
|
readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
20550
20963
|
}
|
|
@@ -20562,9 +20975,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
20562
20975
|
}
|
|
20563
20976
|
async function doctorFix(o) {
|
|
20564
20977
|
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))));
|
|
20565
|
-
const unitExists = (p) =>
|
|
20566
|
-
const crontab = o.host === "linux" ?
|
|
20567
|
-
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
20978
|
+
const unitExists = (p) => existsSync41(p);
|
|
20979
|
+
const crontab = o.host === "linux" ? spawnSync8("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
20980
|
+
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run", installContext()), unitExists, crontab) : true;
|
|
20568
20981
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
20569
20982
|
for (const n of plan.notes) process.stdout.write(`fix: ${n}
|
|
20570
20983
|
`);
|
|
@@ -20579,7 +20992,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20579
20992
|
for (const a of plan.actions) {
|
|
20580
20993
|
if (a.kind === "enable") {
|
|
20581
20994
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
20582
|
-
|
|
20995
|
+
writeFileSync28(cfg.configFile, planEnable(readFileSync36(cfg.configFile, "utf8"), a.route));
|
|
20583
20996
|
enabledSound.add(a.route);
|
|
20584
20997
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
20585
20998
|
} else {
|
|
@@ -21321,7 +21734,7 @@ ${cu.tail.trimEnd()}
|
|
|
21321
21734
|
const target = repoTargetFor(w.route, result.ticket.repo_id);
|
|
21322
21735
|
fleetTicketRepo = target?.name;
|
|
21323
21736
|
try {
|
|
21324
|
-
if (target && ensureRepoCheckout(target.dir, target.remote)) {
|
|
21737
|
+
if (target && ensureRepoCheckout(target.dir, target.remote, { scheme: cfg.ship.cloneScheme })) {
|
|
21325
21738
|
wemit.emit(`cloned ${target.name} into ${target.dir}`, { step: "select", role: w.role });
|
|
21326
21739
|
}
|
|
21327
21740
|
} catch (e) {
|
|
@@ -21538,7 +21951,7 @@ ${cu.tail.trimEnd()}
|
|
|
21538
21951
|
const target = repoTargetFor(route2, result.ticket.repo_id);
|
|
21539
21952
|
ticketRepo = target?.name;
|
|
21540
21953
|
try {
|
|
21541
|
-
if (target && ensureRepoCheckout(target.dir, target.remote)) {
|
|
21954
|
+
if (target && ensureRepoCheckout(target.dir, target.remote, { scheme: cfg.ship.cloneScheme })) {
|
|
21542
21955
|
emit.emit(`cloned ${target.name} into ${target.dir}`, { step: "select", role: current });
|
|
21543
21956
|
}
|
|
21544
21957
|
} catch (e) {
|
|
@@ -21659,7 +22072,7 @@ ${cu.tail.trimEnd()}
|
|
|
21659
22072
|
}
|
|
21660
22073
|
let failed = false;
|
|
21661
22074
|
for (const cmd2 of commands) {
|
|
21662
|
-
const res2 =
|
|
22075
|
+
const res2 = spawnSync8(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
21663
22076
|
if (res2.stdout) process.stdout.write(res2.stdout);
|
|
21664
22077
|
if (res2.stderr) process.stderr.write(res2.stderr);
|
|
21665
22078
|
if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
|
|
@@ -21677,7 +22090,7 @@ ${cu.tail.trimEnd()}
|
|
|
21677
22090
|
`);
|
|
21678
22091
|
break;
|
|
21679
22092
|
}
|
|
21680
|
-
const res =
|
|
22093
|
+
const res = spawnSync8(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
21681
22094
|
if (res.stdout) process.stdout.write(res.stdout);
|
|
21682
22095
|
if (res.stderr) process.stderr.write(res.stderr);
|
|
21683
22096
|
if (daemonSub !== "status" && (res.error || res.status !== 0)) {
|
|
@@ -21961,10 +22374,10 @@ ${cu.tail.trimEnd()}
|
|
|
21961
22374
|
`);
|
|
21962
22375
|
if (!dryRun) {
|
|
21963
22376
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
21964
|
-
const raw =
|
|
22377
|
+
const raw = existsSync41(resolvedPath) ? JSON.parse(readFileSync36(resolvedPath, "utf8")) : {};
|
|
21965
22378
|
raw.agentSkills = result.agentSkills;
|
|
21966
22379
|
mkdirSync26(dirOf2(resolvedPath), { recursive: true });
|
|
21967
|
-
|
|
22380
|
+
writeFileSync28(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
21968
22381
|
`);
|
|
21969
22382
|
}
|
|
21970
22383
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -21987,7 +22400,7 @@ ${cu.tail.trimEnd()}
|
|
|
21987
22400
|
throw e;
|
|
21988
22401
|
}
|
|
21989
22402
|
const oldName = cfg.ship.name;
|
|
21990
|
-
|
|
22403
|
+
writeFileSync28(cfg.configFile, setShipNameInYaml(readFileSync36(cfg.configFile, "utf8"), newName));
|
|
21991
22404
|
cfg.ship.name = newName;
|
|
21992
22405
|
let renameMachineId;
|
|
21993
22406
|
try {
|
|
@@ -22034,7 +22447,7 @@ ${cu.tail.trimEnd()}
|
|
|
22034
22447
|
process.exit(2);
|
|
22035
22448
|
};
|
|
22036
22449
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
|
|
22037
|
-
const configText =
|
|
22450
|
+
const configText = readFileSync36(cfg.configFile, "utf8");
|
|
22038
22451
|
if (sub === "check") {
|
|
22039
22452
|
try {
|
|
22040
22453
|
const routeArg = value("route") ?? positional[2];
|
|
@@ -22065,8 +22478,8 @@ ${cu.tail.trimEnd()}
|
|
|
22065
22478
|
try {
|
|
22066
22479
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
22067
22480
|
const dir = repoDirOf(configText, dirname22(cfg.configFile), a.route, a.name);
|
|
22068
|
-
if (!
|
|
22069
|
-
const files = { exists: (r) =>
|
|
22481
|
+
if (!existsSync41(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
22482
|
+
const files = { exists: (r) => existsSync41(resolvePath(dir, r)), list: (r) => {
|
|
22070
22483
|
try {
|
|
22071
22484
|
return readdirSync18(resolvePath(dir, r));
|
|
22072
22485
|
} catch {
|
|
@@ -22074,7 +22487,7 @@ ${cu.tail.trimEnd()}
|
|
|
22074
22487
|
}
|
|
22075
22488
|
}, read: (r) => {
|
|
22076
22489
|
try {
|
|
22077
|
-
return
|
|
22490
|
+
return readFileSync36(resolvePath(dir, r), "utf8");
|
|
22078
22491
|
} catch {
|
|
22079
22492
|
return void 0;
|
|
22080
22493
|
}
|
|
@@ -22116,19 +22529,19 @@ ${cu.tail.trimEnd()}
|
|
|
22116
22529
|
let cleared;
|
|
22117
22530
|
if (clear2) {
|
|
22118
22531
|
const plan = clearRouteHooks(configText, a.route, a.name);
|
|
22119
|
-
if (plan.removed.length > 0)
|
|
22532
|
+
if (plan.removed.length > 0) writeFileSync28(cfg.configFile, plan.text);
|
|
22120
22533
|
cleared = { file: cfg.configFile, removed: plan.removed };
|
|
22121
22534
|
}
|
|
22122
22535
|
if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
22123
22536
|
if (write === "route") {
|
|
22124
22537
|
const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
|
|
22125
|
-
|
|
22538
|
+
writeFileSync28(cfg.configFile, plan.text);
|
|
22126
22539
|
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
|
|
22127
22540
|
} else if (write === "repo") {
|
|
22128
22541
|
const have = existingRepoConfig(files);
|
|
22129
22542
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
22130
22543
|
const file = resolvePath(dir, ".crew.yaml");
|
|
22131
|
-
|
|
22544
|
+
writeFileSync28(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
|
|
22132
22545
|
written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
|
|
22133
22546
|
}
|
|
22134
22547
|
if (flag("json")) {
|
|
@@ -22184,7 +22597,7 @@ ${cu.tail.trimEnd()}
|
|
|
22184
22597
|
if (sub === "list" && !routeName) usage2();
|
|
22185
22598
|
if (sub === "add") {
|
|
22186
22599
|
try {
|
|
22187
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
22600
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync41(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
|
|
22188
22601
|
routeName = a.route;
|
|
22189
22602
|
addPath = a.path;
|
|
22190
22603
|
addName = a.name;
|
|
@@ -22213,16 +22626,16 @@ ${cu.tail.trimEnd()}
|
|
|
22213
22626
|
}
|
|
22214
22627
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
22215
22628
|
let resolvedRemotes;
|
|
22216
|
-
if (
|
|
22629
|
+
if (existsSync41(resolvedFile)) {
|
|
22217
22630
|
try {
|
|
22218
|
-
resolvedRemotes = JSON.parse(
|
|
22631
|
+
resolvedRemotes = JSON.parse(readFileSync36(resolvedFile, "utf8"));
|
|
22219
22632
|
} catch {
|
|
22220
22633
|
}
|
|
22221
22634
|
}
|
|
22222
22635
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
22223
22636
|
const text = configText;
|
|
22224
22637
|
const base = dirname22(cfg.configFile);
|
|
22225
|
-
const isCheckout = (d) =>
|
|
22638
|
+
const isCheckout = (d) => existsSync41(`${d}/.git`);
|
|
22226
22639
|
try {
|
|
22227
22640
|
if (sub === "list") {
|
|
22228
22641
|
if (flag("json")) {
|
|
@@ -22232,11 +22645,11 @@ ${cu.tail.trimEnd()}
|
|
|
22232
22645
|
base,
|
|
22233
22646
|
resolved: resolvedRemotes,
|
|
22234
22647
|
originOf,
|
|
22235
|
-
exists: (d) =>
|
|
22648
|
+
exists: (d) => existsSync41(d),
|
|
22236
22649
|
isCheckout,
|
|
22237
22650
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
22238
22651
|
try {
|
|
22239
|
-
return Object.keys(parseRepoConfig(
|
|
22652
|
+
return Object.keys(parseRepoConfig(readFileSync36(resolvePath(d, f), "utf8"), f).hooks).length > 0;
|
|
22240
22653
|
} catch {
|
|
22241
22654
|
return false;
|
|
22242
22655
|
}
|
|
@@ -22253,7 +22666,7 @@ ${cu.tail.trimEnd()}
|
|
|
22253
22666
|
for (const r of repos) {
|
|
22254
22667
|
const origin2 = originOf(r.dir);
|
|
22255
22668
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
22256
|
-
const note = !
|
|
22669
|
+
const note = !existsSync41(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
22257
22670
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
22258
22671
|
`);
|
|
22259
22672
|
}
|
|
@@ -22265,9 +22678,9 @@ ${cu.tail.trimEnd()}
|
|
|
22265
22678
|
` : `${human}
|
|
22266
22679
|
`);
|
|
22267
22680
|
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
|
|
22268
|
-
if (
|
|
22681
|
+
if (existsSync41(plan2.target) && (existsSync41(`${plan2.target}/.git`) || readdirSync18(plan2.target).length > 0)) {
|
|
22269
22682
|
throw new ReposError(
|
|
22270
|
-
|
|
22683
|
+
existsSync41(`${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`
|
|
22271
22684
|
);
|
|
22272
22685
|
}
|
|
22273
22686
|
if (dryRun) {
|
|
@@ -22285,14 +22698,14 @@ ${cu.tail.trimEnd()}
|
|
|
22285
22698
|
throw new ReposError(`${e.message}${authHint}
|
|
22286
22699
|
crew.yaml was not changed`);
|
|
22287
22700
|
}
|
|
22288
|
-
|
|
22701
|
+
writeFileSync28(cfg.configFile, plan2.text);
|
|
22289
22702
|
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:
|
|
22290
22703
|
`);
|
|
22291
22704
|
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}`);
|
|
22292
22705
|
break;
|
|
22293
22706
|
}
|
|
22294
22707
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
|
|
22295
|
-
const top =
|
|
22708
|
+
const top = existsSync41(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
22296
22709
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
22297
22710
|
const origin = originOf(top);
|
|
22298
22711
|
const plan = planRepoAdd({
|
|
@@ -22344,15 +22757,15 @@ ${cu.tail.trimEnd()}
|
|
|
22344
22757
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
22345
22758
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
22346
22759
|
`);
|
|
22347
|
-
if (
|
|
22348
|
-
const raw = JSON.parse(
|
|
22760
|
+
if (existsSync41(resolvedFile)) {
|
|
22761
|
+
const raw = JSON.parse(readFileSync36(resolvedFile, "utf8"));
|
|
22349
22762
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
22350
22763
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
22351
|
-
|
|
22764
|
+
writeFileSync28(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
22352
22765
|
`);
|
|
22353
22766
|
}
|
|
22354
22767
|
}
|
|
22355
|
-
|
|
22768
|
+
writeFileSync28(cfg.configFile, plan.text);
|
|
22356
22769
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
22357
22770
|
`);
|
|
22358
22771
|
} catch (e) {
|
|
@@ -22563,7 +22976,7 @@ ${reconstruction.prompt}
|
|
|
22563
22976
|
let result;
|
|
22564
22977
|
try {
|
|
22565
22978
|
result = await loginWithDeviceCode(`${baseUrl}/api`, {
|
|
22566
|
-
deviceName: `crew on ${
|
|
22979
|
+
deviceName: `crew on ${hostname3()}`,
|
|
22567
22980
|
// Lets the approval page preselect the workspace this connect is for.
|
|
22568
22981
|
// A bare `crew connect` has none and passes nothing.
|
|
22569
22982
|
...workspace ? { workspaceSlug: workspace } : {},
|
|
@@ -22719,7 +23132,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22719
23132
|
process.exit(2);
|
|
22720
23133
|
}
|
|
22721
23134
|
if (found.shipNameAdopted && !dryRun) {
|
|
22722
|
-
|
|
23135
|
+
writeFileSync28(cfg.configFile, setShipNameInYaml(readFileSync36(cfg.configFile, "utf8"), found.shipNameAdopted));
|
|
22723
23136
|
cfg.ship.name = found.shipNameAdopted;
|
|
22724
23137
|
}
|
|
22725
23138
|
if (found.shipRow) {
|
|
@@ -22762,10 +23175,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22762
23175
|
}
|
|
22763
23176
|
}
|
|
22764
23177
|
let previous;
|
|
22765
|
-
const hadPreviousFile =
|
|
23178
|
+
const hadPreviousFile = existsSync41(resolvedPath);
|
|
22766
23179
|
if (hadPreviousFile) {
|
|
22767
23180
|
try {
|
|
22768
|
-
previous = JSON.parse(
|
|
23181
|
+
previous = JSON.parse(readFileSync36(resolvedPath, "utf8"));
|
|
22769
23182
|
} catch {
|
|
22770
23183
|
}
|
|
22771
23184
|
}
|
|
@@ -22897,7 +23310,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22897
23310
|
mkdirSync26(dirname22(resolvedPath), { recursive: true });
|
|
22898
23311
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
22899
23312
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
22900
|
-
|
|
23313
|
+
writeFileSync28(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
22901
23314
|
`);
|
|
22902
23315
|
renameSync4(tmpPath, resolvedPath);
|
|
22903
23316
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -22944,7 +23357,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22944
23357
|
`);
|
|
22945
23358
|
} else {
|
|
22946
23359
|
mkdirSync26(dirname22(apiKeyFile), { recursive: true });
|
|
22947
|
-
|
|
23360
|
+
writeFileSync28(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
22948
23361
|
`, { mode: 384 });
|
|
22949
23362
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
22950
23363
|
`);
|
|
@@ -23417,6 +23830,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
23417
23830
|
const rotatePending = [];
|
|
23418
23831
|
syncPassengerTunnels(plans, cfg.ship.relayHost, cfg.ship.relayPort, privateKeyPath, cfg.ship.stateDir, /* @__PURE__ */ new Map(), {
|
|
23419
23832
|
apiKeys: passengerApiKeys([route2]),
|
|
23833
|
+
controlSocket: controlSocketFor(cfg.ship.stateDir),
|
|
23420
23834
|
onStatus: (wsId, status2, info) => {
|
|
23421
23835
|
rotatePending.push(
|
|
23422
23836
|
new Tracker(route2, cfg.ship).updateTunnelState(cfg.ship.name, { tunnel_status: status2 }).catch((e) => {
|
|
@@ -23678,7 +24092,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
23678
24092
|
const shellR = hit && resolveShell(hit.repo.config.shell ?? cfg.ship.shell);
|
|
23679
24093
|
const resumed = await runPeriscopeResume({
|
|
23680
24094
|
find: async () => hit && { row: hit.row, tag: hit.tag },
|
|
23681
|
-
worktreeExists: (p) =>
|
|
24095
|
+
worktreeExists: (p) => existsSync41(p),
|
|
23682
24096
|
caps: periscopeCaps(cfg.ship),
|
|
23683
24097
|
runHook: async (name, cwd, env) => {
|
|
23684
24098
|
const script = hit?.repo.config.hooks[name];
|
|
@@ -23773,7 +24187,7 @@ ${where.join("\n")}`);
|
|
|
23773
24187
|
prepare: async (path) => {
|
|
23774
24188
|
for (const f of repoP.config.worktrees.copy) {
|
|
23775
24189
|
const src = join44(repoP.dir, f);
|
|
23776
|
-
if (!
|
|
24190
|
+
if (!existsSync41(src)) continue;
|
|
23777
24191
|
mkdirSync26(dirOf2(join44(path, f)), { recursive: true });
|
|
23778
24192
|
copyFileSync2(src, join44(path, f));
|
|
23779
24193
|
}
|
|
@@ -23861,7 +24275,7 @@ ${where.join("\n")}`);
|
|
|
23861
24275
|
process.stdout.write(upOutput(res, terminal));
|
|
23862
24276
|
const opener = openerFor(process.platform);
|
|
23863
24277
|
if (opener && shouldOpen({ open: flag("open"), noOpen: flag("no-open"), json: jsonP, terminal })) {
|
|
23864
|
-
const r =
|
|
24278
|
+
const r = spawnSync8(opener, [res.url], { stdio: "ignore" });
|
|
23865
24279
|
if (r.error || r.status !== 0) process.stderr.write(`crew periscope: could not open ${res.url} with ${opener}
|
|
23866
24280
|
`);
|
|
23867
24281
|
}
|
|
@@ -23993,6 +24407,14 @@ ${where.join("\n")}`);
|
|
|
23993
24407
|
const url = mcpUrlFor(value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL);
|
|
23994
24408
|
const disconnectHarnesses = /* @__PURE__ */ new Set([...shipPair(cfg.ship), ...shipToolchains(cfg.ship).map((t) => t.harness).filter(isPairHarness)]);
|
|
23995
24409
|
const removePairEntries = (mcpUrl, dry) => [...disconnectHarnesses].map((h) => pairInstallerFor(h).removePair({ url: mcpUrl, bin: cfg.ship.agent.bin, dryRun: dry }));
|
|
24410
|
+
if (!dryRun) {
|
|
24411
|
+
for (const r of cfg.routes.filter((x) => x.resolved?.shipsModelId)) {
|
|
24412
|
+
await new Tracker(r, cfg.ship).updateRelayUrl(cfg.ship.name, null).catch((e) => {
|
|
24413
|
+
process.stderr.write(`could not clear relay_url for ${r.route}: ${e.message}
|
|
24414
|
+
`);
|
|
24415
|
+
});
|
|
24416
|
+
}
|
|
24417
|
+
}
|
|
23996
24418
|
if (!flag("reset")) {
|
|
23997
24419
|
for (const line of removePairEntries(url, dryRun)) process.stderr.write(`${line}
|
|
23998
24420
|
`);
|
|
@@ -24050,7 +24472,7 @@ ${where.join("\n")}`);
|
|
|
24050
24472
|
};
|
|
24051
24473
|
const droppedRoutes = [];
|
|
24052
24474
|
try {
|
|
24053
|
-
const rawCfg = parseYaml2(
|
|
24475
|
+
const rawCfg = parseYaml2(readFileSync36(cfg.configFile, "utf8"));
|
|
24054
24476
|
for (const r of rawCfg?.routes ?? []) {
|
|
24055
24477
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
24056
24478
|
try {
|
|
@@ -24086,19 +24508,19 @@ ${where.join("\n")}`);
|
|
|
24086
24508
|
}
|
|
24087
24509
|
const host = hostPlatform();
|
|
24088
24510
|
const control = host === "windows" ? null : planDaemonControl(CREW_HOME, host, detectSystemd());
|
|
24089
|
-
const unitStatus = control?.statusCommand ?
|
|
24511
|
+
const unitStatus = control?.statusCommand ? spawnSync8(control.statusCommand[0], control.statusCommand.slice(1), { cwd: CREW_HOME, encoding: "utf8" }) : null;
|
|
24090
24512
|
const unitRunning = unitStatus !== null && !unitStatus.error && unitStatus.status === 0;
|
|
24091
24513
|
if (!unitRunning) {
|
|
24092
24514
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
24093
24515
|
break;
|
|
24094
24516
|
}
|
|
24095
24517
|
mkdirSync26(cfg.ship.stateDir, { recursive: true });
|
|
24096
|
-
|
|
24518
|
+
writeFileSync28(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
24097
24519
|
`);
|
|
24098
24520
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
24099
24521
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
24100
24522
|
for (const cmd of [control.stopCommand, control.startCommand]) {
|
|
24101
|
-
const res =
|
|
24523
|
+
const res = spawnSync8(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
24102
24524
|
if (cmd === control.startCommand && (res.error || res.status !== 0)) {
|
|
24103
24525
|
process.stderr.write(`crew reload: ${cmd.join(" ")} exited ${res.status ?? "error"}
|
|
24104
24526
|
`);
|
|
@@ -24144,7 +24566,7 @@ ${where.join("\n")}`);
|
|
|
24144
24566
|
}
|
|
24145
24567
|
case "log": {
|
|
24146
24568
|
try {
|
|
24147
|
-
const text =
|
|
24569
|
+
const text = readFileSync36(cfg.ship.logFile, "utf8").split("\n");
|
|
24148
24570
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
24149
24571
|
`);
|
|
24150
24572
|
} catch {
|
|
@@ -24167,7 +24589,7 @@ ${where.join("\n")}`);
|
|
|
24167
24589
|
};
|
|
24168
24590
|
if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
|
|
24169
24591
|
const tracker = new Tracker(route2, cfg.ship);
|
|
24170
|
-
const configText =
|
|
24592
|
+
const configText = readFileSync36(cfg.configFile, "utf8");
|
|
24171
24593
|
const cfgBase = dirname22(cfg.configFile);
|
|
24172
24594
|
const configured2 = configuredRepos(configText, route2.route, cfgBase);
|
|
24173
24595
|
const dir = configured2.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured2.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
|
|
@@ -24196,7 +24618,7 @@ ${where.join("\n")}`);
|
|
|
24196
24618
|
}
|
|
24197
24619
|
if (configured2.some((r) => r.dir === dir)) {
|
|
24198
24620
|
try {
|
|
24199
|
-
|
|
24621
|
+
writeFileSync28(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
|
|
24200
24622
|
} catch (e) {
|
|
24201
24623
|
if (e instanceof ReposError) fail(e.message);
|
|
24202
24624
|
throw e;
|
|
@@ -24204,31 +24626,31 @@ ${where.join("\n")}`);
|
|
|
24204
24626
|
}
|
|
24205
24627
|
if (row) {
|
|
24206
24628
|
await tracker.deleteRepoRow(row.id);
|
|
24207
|
-
if (
|
|
24208
|
-
const raw = JSON.parse(
|
|
24629
|
+
if (existsSync41(resolvedFile)) {
|
|
24630
|
+
const raw = JSON.parse(readFileSync36(resolvedFile, "utf8"));
|
|
24209
24631
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
24210
24632
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
24211
|
-
|
|
24633
|
+
writeFileSync28(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
24212
24634
|
`);
|
|
24213
24635
|
}
|
|
24214
24636
|
}
|
|
24215
|
-
if (
|
|
24637
|
+
if (existsSync41(join44(dir, ".git")) && basename13(dir) === SAMPLE_REPO) rmSync19(dir, { recursive: true, force: true });
|
|
24216
24638
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
24217
24639
|
break;
|
|
24218
24640
|
}
|
|
24219
24641
|
const projectId = route2.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
24220
|
-
const fresh = !
|
|
24221
|
-
if (
|
|
24642
|
+
const fresh = !existsSync41(join44(dir, ".git"));
|
|
24643
|
+
if (existsSync41(dir) && fresh && readdirSync18(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
24222
24644
|
if (dryRun) {
|
|
24223
24645
|
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`);
|
|
24224
24646
|
break;
|
|
24225
24647
|
}
|
|
24226
24648
|
if (fresh) {
|
|
24227
24649
|
mkdirSync26(dir, { recursive: true });
|
|
24228
|
-
|
|
24229
|
-
|
|
24650
|
+
writeFileSync28(join44(dir, "README.md"), SAMPLE_README);
|
|
24651
|
+
writeFileSync28(join44(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
24230
24652
|
const g = (...a) => {
|
|
24231
|
-
const r =
|
|
24653
|
+
const r = spawnSync8("git", a, { cwd: dir, encoding: "utf8" });
|
|
24232
24654
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
24233
24655
|
};
|
|
24234
24656
|
g("init", "-b", "main");
|
|
@@ -24244,17 +24666,17 @@ ${where.join("\n")}`);
|
|
|
24244
24666
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
24245
24667
|
});
|
|
24246
24668
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
24247
|
-
if (
|
|
24248
|
-
const raw = JSON.parse(
|
|
24669
|
+
if (existsSync41(resolvedFile)) {
|
|
24670
|
+
const raw = JSON.parse(readFileSync36(resolvedFile, "utf8"));
|
|
24249
24671
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
24250
24672
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
24251
|
-
|
|
24673
|
+
writeFileSync28(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
24252
24674
|
`);
|
|
24253
24675
|
}
|
|
24254
24676
|
}
|
|
24255
24677
|
if (!configured2.some((r) => r.dir === dir)) {
|
|
24256
24678
|
try {
|
|
24257
|
-
|
|
24679
|
+
writeFileSync28(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route2.route, path: dir, name: SAMPLE_REPO }).text);
|
|
24258
24680
|
} catch (e) {
|
|
24259
24681
|
if (e instanceof ReposError) fail(e.message);
|
|
24260
24682
|
throw e;
|
|
@@ -24427,7 +24849,9 @@ ${where.join("\n")}`);
|
|
|
24427
24849
|
attention: openShipAttention(state),
|
|
24428
24850
|
// Additive (CREW-2495): this ship's Ships-row name and whether it has drifted from `name`.
|
|
24429
24851
|
rowName: nameSync.rowName ?? null,
|
|
24430
|
-
nameDrift: nameSync.drift
|
|
24852
|
+
nameDrift: nameSync.drift,
|
|
24853
|
+
// Additive (CREW-2803): the relay URL this route's tunnel is reachable at, or null.
|
|
24854
|
+
relayUrl: routeRelayUrl(route2)
|
|
24431
24855
|
},
|
|
24432
24856
|
// Additive (CREW-1402): what the operator owes. `version` stays 1.
|
|
24433
24857
|
todo: todo.map((i) => ({ ticket: displayKey(i.ticket), title: i.ticket.title ?? null, reasons: i.reasons })),
|
|
@@ -24517,7 +24941,8 @@ ${owed.map((l) => ` ${l}
|
|
|
24517
24941
|
route: ${route2.route} -> ${route2.dir}
|
|
24518
24942
|
enabled: ${route2.enabled}
|
|
24519
24943
|
periscopes: ${periscopeSummary(cfg)}
|
|
24520
|
-
` +
|
|
24944
|
+
` + (routeRelayUrl(route2) ? `relay: ${routeRelayUrl(route2)}
|
|
24945
|
+
` : "") + periscopes.map((p) => `periscope: ${p.ticket} ${p.repo} ${p.url} ${p.state}${p.serving ? "" : " (not serving)"} expires ${p.expiresAt}
|
|
24521
24946
|
`).join("") + `crew: ${state.isPaused() ? "paused" : "active"}
|
|
24522
24947
|
release: ${state.releaseSkipReason() ? "paused" : "active"}
|
|
24523
24948
|
watermark: ${state.watermark()}
|
|
@@ -24589,7 +25014,7 @@ watermark: ${state.watermark()}
|
|
|
24589
25014
|
}
|
|
24590
25015
|
for (const r of repos) {
|
|
24591
25016
|
const remote = r.config.branch.remote;
|
|
24592
|
-
if (
|
|
25017
|
+
if (existsSync41(r.dir)) {
|
|
24593
25018
|
pushAuth.push({
|
|
24594
25019
|
route: c.route,
|
|
24595
25020
|
repo: r.name,
|
|
@@ -24674,7 +25099,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
24674
25099
|
}
|
|
24675
25100
|
try {
|
|
24676
25101
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
24677
|
-
const evSize =
|
|
25102
|
+
const evSize = existsSync41(evFile) ? statSync14(evFile).size : 0;
|
|
24678
25103
|
process.stdout.write(
|
|
24679
25104
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
24680
25105
|
` : "events log: ok\n"
|
|
@@ -24682,10 +25107,10 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
24682
25107
|
} catch {
|
|
24683
25108
|
}
|
|
24684
25109
|
if (host !== "windows") {
|
|
24685
|
-
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
25110
|
+
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run", installContext());
|
|
24686
25111
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
24687
25112
|
try {
|
|
24688
|
-
return
|
|
25113
|
+
return readFileSync36(p, "utf8");
|
|
24689
25114
|
} catch {
|
|
24690
25115
|
return null;
|
|
24691
25116
|
}
|
|
@@ -25109,6 +25534,21 @@ ${foreign.join("\n")}
|
|
|
25109
25534
|
);
|
|
25110
25535
|
}
|
|
25111
25536
|
}
|
|
25537
|
+
if (cfg.ship.relayHost && !flag("offline")) {
|
|
25538
|
+
const relayUrl = routeRelayUrl(route2);
|
|
25539
|
+
if (!relayUrl) {
|
|
25540
|
+
process.stdout.write("relay control: no tunnel URL yet (the first `crew passengers` cycle opens it)\n");
|
|
25541
|
+
} else {
|
|
25542
|
+
let key;
|
|
25543
|
+
try {
|
|
25544
|
+
key = resolveApiKey(route2);
|
|
25545
|
+
} catch {
|
|
25546
|
+
key = void 0;
|
|
25547
|
+
}
|
|
25548
|
+
process.stdout.write(`${key ? describeRelayProbe(relayUrl, await probeRelayControl(relayUrl, key, cfg.ship.userAgent)) : "relay control: could not check (no API key for this route)"}
|
|
25549
|
+
`);
|
|
25550
|
+
}
|
|
25551
|
+
}
|
|
25112
25552
|
for (const l of doctorSelftestLines(readSelftest(cfg.ship.stateDir), shipToolchains(cfg.ship).map((t) => t.name))) {
|
|
25113
25553
|
process.stdout.write(`${l}
|
|
25114
25554
|
`);
|
|
@@ -25130,6 +25570,7 @@ ${foreign.join("\n")}
|
|
|
25130
25570
|
}
|
|
25131
25571
|
var requireArmed2;
|
|
25132
25572
|
var parseTicketNumber2;
|
|
25573
|
+
var routeRelayUrl2;
|
|
25133
25574
|
var passengerApiKeys2;
|
|
25134
25575
|
var endpointApiFor2;
|
|
25135
25576
|
var endpointReconcileDeps2;
|