@tablation/crew 0.94.0 → 0.96.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/crew.example.yaml +1 -0
- package/dist/cli.js +1480 -984
- 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",
|
|
@@ -2665,7 +2682,8 @@ var ROUTE_KEYS = /* @__PURE__ */ new Set([
|
|
|
2665
2682
|
"promptSet",
|
|
2666
2683
|
"hostPassengers",
|
|
2667
2684
|
"hostPeriscopes",
|
|
2668
|
-
"toolchain"
|
|
2685
|
+
"toolchain",
|
|
2686
|
+
"maxRunMinutes"
|
|
2669
2687
|
]);
|
|
2670
2688
|
function parseWorktreeName(v, where, file) {
|
|
2671
2689
|
const problem = typeof v === "string" ? worktreeNameProblem(v) : "must be a string";
|
|
@@ -3064,7 +3082,8 @@ Create the first of those:
|
|
|
3064
3082
|
previewHost: shipRaw.previewHost ? String(shipRaw.previewHost).trim() || void 0 : void 0,
|
|
3065
3083
|
maxPeriscopes,
|
|
3066
3084
|
periscopeIdleMinutes,
|
|
3067
|
-
secretStore
|
|
3085
|
+
secretStore,
|
|
3086
|
+
env: parseShipEnv(shipRaw.env)
|
|
3068
3087
|
},
|
|
3069
3088
|
routes,
|
|
3070
3089
|
crewHome,
|
|
@@ -3075,7 +3094,21 @@ Create the first of those:
|
|
|
3075
3094
|
function expandCommand(cmd, base) {
|
|
3076
3095
|
return cmd.includes("/") || cmd.startsWith("~") ? expand(cmd, base) : cmd;
|
|
3077
3096
|
}
|
|
3078
|
-
var TOOLCHAIN_KEYS = /* @__PURE__ */ new Set(["harness", "bin", "launcher", "launcherBin", "model", "tiers", "args", "env", "maxThinkingTokens", "auth"]);
|
|
3097
|
+
var TOOLCHAIN_KEYS = /* @__PURE__ */ new Set(["harness", "bin", "launcher", "launcherBin", "model", "tiers", "args", "env", "maxThinkingTokens", "auth", "maxRunMinutes"]);
|
|
3098
|
+
var MAX_RUN_MINUTES_CEILING = Math.floor((2 ** 31 - 1) / 6e4);
|
|
3099
|
+
function parseMaxRunMinutes(raw, where, missing) {
|
|
3100
|
+
if (raw === void 0) return void 0;
|
|
3101
|
+
const n = Number(raw);
|
|
3102
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
3103
|
+
missing.add(`${where} must be a non-negative integer number of minutes, 0 for no limit (got "${String(raw)}")`);
|
|
3104
|
+
return void 0;
|
|
3105
|
+
}
|
|
3106
|
+
if (n > MAX_RUN_MINUTES_CEILING) {
|
|
3107
|
+
missing.add(`${where} is too large (${n}); the most it can be is ${MAX_RUN_MINUTES_CEILING} minutes \u2014 use 0 for no limit`);
|
|
3108
|
+
return void 0;
|
|
3109
|
+
}
|
|
3110
|
+
return n;
|
|
3111
|
+
}
|
|
3079
3112
|
function parsePair(raw, missing) {
|
|
3080
3113
|
if (raw === void 0) return void 0;
|
|
3081
3114
|
if (!Array.isArray(raw)) {
|
|
@@ -3132,6 +3165,7 @@ function parseToolchains(raw, base, missing, file) {
|
|
|
3132
3165
|
if (!Number.isInteger(maxThinkingTokens) || maxThinkingTokens < 0) {
|
|
3133
3166
|
missing.add(`${where}.maxThinkingTokens must be a non-negative integer (got "${entryRaw.maxThinkingTokens}")`);
|
|
3134
3167
|
}
|
|
3168
|
+
const maxRunMinutes = parseMaxRunMinutes(entryRaw.maxRunMinutes, `${where}.maxRunMinutes`, missing);
|
|
3135
3169
|
let auth;
|
|
3136
3170
|
if (entryRaw.auth !== void 0) {
|
|
3137
3171
|
if (entryRaw.auth !== "subscription" && entryRaw.auth !== "inherit") {
|
|
@@ -3153,7 +3187,8 @@ function parseToolchains(raw, base, missing, file) {
|
|
|
3153
3187
|
tiers,
|
|
3154
3188
|
args: Array.isArray(args) ? args.map(String) : [],
|
|
3155
3189
|
env: Object.fromEntries(Object.entries(env).map(([k, v]) => [k, String(v)])),
|
|
3156
|
-
maxThinkingTokens
|
|
3190
|
+
maxThinkingTokens,
|
|
3191
|
+
...maxRunMinutes !== void 0 ? { maxRunMinutes } : {}
|
|
3157
3192
|
});
|
|
3158
3193
|
}
|
|
3159
3194
|
return out;
|
|
@@ -3161,6 +3196,7 @@ function parseToolchains(raw, base, missing, file) {
|
|
|
3161
3196
|
function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, toolchainNames) {
|
|
3162
3197
|
rejectUnknownKeys(c, ROUTE_KEYS, `${where}`, file);
|
|
3163
3198
|
const toolchain = c?.toolchain === void 0 ? void 0 : String(c.toolchain);
|
|
3199
|
+
const maxRunMinutes = parseMaxRunMinutes(c?.maxRunMinutes, `${where}.maxRunMinutes`, missing);
|
|
3164
3200
|
if (toolchain !== void 0 && !toolchainNames.has(toolchain)) {
|
|
3165
3201
|
missing.add(`${where}.toolchain "${toolchain}" is not defined in ship.toolchains (have: ${[...toolchainNames].join(", ")})`);
|
|
3166
3202
|
}
|
|
@@ -3224,6 +3260,7 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
|
|
|
3224
3260
|
repoOverrides,
|
|
3225
3261
|
worktreePrefix: c?.worktreePrefix ? String(c.worktreePrefix) : void 0,
|
|
3226
3262
|
...toolchain !== void 0 ? { toolchain } : {},
|
|
3263
|
+
...maxRunMinutes !== void 0 ? { maxRunMinutes } : {},
|
|
3227
3264
|
promptsDir: resolvePromptsDir(c?.promptSet ? String(c.promptSet) : void 0, crewHome, base),
|
|
3228
3265
|
weight,
|
|
3229
3266
|
baseUrl: String(c?.baseUrl ?? raw.ship?.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
|
|
@@ -3698,7 +3735,7 @@ function recoveredFailureHeadlines(o, route2) {
|
|
|
3698
3735
|
|
|
3699
3736
|
// src/install.ts
|
|
3700
3737
|
import { dirname as dirname5, join as join13, extname } from "path";
|
|
3701
|
-
import { homedir as homedir7 } from "os";
|
|
3738
|
+
import { homedir as homedir7, userInfo } from "os";
|
|
3702
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";
|
|
3703
3740
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
3704
3741
|
import { createHash as createHash3 } from "crypto";
|
|
@@ -3766,6 +3803,30 @@ function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
|
|
|
3766
3803
|
const inShell = findOnPath("docker", shellPath);
|
|
3767
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\``;
|
|
3768
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
|
+
}
|
|
3769
3830
|
function xmlEscape(s) {
|
|
3770
3831
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3771
3832
|
}
|
|
@@ -3798,8 +3859,8 @@ function planLaunchd(ship, crewHome, job) {
|
|
|
3798
3859
|
</array>
|
|
3799
3860
|
<key>EnvironmentVariables</key>
|
|
3800
3861
|
<dict>
|
|
3801
|
-
<key
|
|
3802
|
-
<string>${xmlEscape(
|
|
3862
|
+
${unitEnvironment(ship).map(([k, v]) => ` <key>${xmlEscape(k)}</key>
|
|
3863
|
+
<string>${xmlEscape(v)}</string>`).join("\n")}
|
|
3803
3864
|
</dict>
|
|
3804
3865
|
<key>WorkingDirectory</key>
|
|
3805
3866
|
<string>${xmlEscape(crewHome)}</string>
|
|
@@ -3823,9 +3884,17 @@ ${scheduleKeys}
|
|
|
3823
3884
|
schedulerLog
|
|
3824
3885
|
};
|
|
3825
3886
|
}
|
|
3826
|
-
function planSystemd(ship, crewHome, job) {
|
|
3887
|
+
function planSystemd(ship, crewHome, job, ctx) {
|
|
3827
3888
|
const label = labelFor(crewHome, job);
|
|
3828
|
-
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
|
+
};
|
|
3829
3898
|
const servicePath = join13(unitDir, `${label}.service`);
|
|
3830
3899
|
const crewLog = ship.logFile;
|
|
3831
3900
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
@@ -3837,9 +3906,9 @@ Description=${description}
|
|
|
3837
3906
|
|
|
3838
3907
|
[Service]
|
|
3839
3908
|
Type=simple
|
|
3840
|
-
ExecStart=${[...crewInvocation(crewHome), ...subcommandFor(job, "systemd")].join(" ")}
|
|
3909
|
+
${userLine}ExecStart=${[...crewInvocation(crewHome), ...subcommandFor(job, "systemd")].join(" ")}
|
|
3841
3910
|
WorkingDirectory=${crewHome}
|
|
3842
|
-
|
|
3911
|
+
${systemdEnvLines(ship)}
|
|
3843
3912
|
StandardOutput=append:${schedulerLog}
|
|
3844
3913
|
StandardError=append:${schedulerLog}
|
|
3845
3914
|
Restart=on-failure
|
|
@@ -3849,7 +3918,7 @@ RestartSec=5
|
|
|
3849
3918
|
KillMode=process
|
|
3850
3919
|
|
|
3851
3920
|
[Install]
|
|
3852
|
-
WantedBy
|
|
3921
|
+
WantedBy=${wantedBy}
|
|
3853
3922
|
`;
|
|
3854
3923
|
return {
|
|
3855
3924
|
mechanism: "systemd",
|
|
@@ -3859,11 +3928,12 @@ WantedBy=default.target
|
|
|
3859
3928
|
cronMarker: null,
|
|
3860
3929
|
preLoadCommands: [],
|
|
3861
3930
|
loadCommands: [
|
|
3862
|
-
[
|
|
3863
|
-
[
|
|
3931
|
+
[...ctl, "daemon-reload"],
|
|
3932
|
+
[...ctl, "enable", "--now", `${label}.service`]
|
|
3864
3933
|
],
|
|
3865
3934
|
crewLog,
|
|
3866
|
-
schedulerLog
|
|
3935
|
+
schedulerLog,
|
|
3936
|
+
...linger
|
|
3867
3937
|
};
|
|
3868
3938
|
}
|
|
3869
3939
|
const timerPath = join13(unitDir, `${label}.timer`);
|
|
@@ -3873,9 +3943,9 @@ Description=${description}
|
|
|
3873
3943
|
|
|
3874
3944
|
[Service]
|
|
3875
3945
|
Type=oneshot
|
|
3876
|
-
ExecStart=${[...crewInvocation(crewHome), ...subcommandFor(job, "systemd")].join(" ")}
|
|
3946
|
+
${userLine}ExecStart=${[...crewInvocation(crewHome), ...subcommandFor(job, "systemd")].join(" ")}
|
|
3877
3947
|
WorkingDirectory=${crewHome}
|
|
3878
|
-
|
|
3948
|
+
${systemdEnvLines(ship)}
|
|
3879
3949
|
StandardOutput=append:${schedulerLog}
|
|
3880
3950
|
StandardError=append:${schedulerLog}
|
|
3881
3951
|
`;
|
|
@@ -3899,11 +3969,12 @@ WantedBy=timers.target
|
|
|
3899
3969
|
cronMarker: null,
|
|
3900
3970
|
preLoadCommands: [],
|
|
3901
3971
|
loadCommands: [
|
|
3902
|
-
[
|
|
3903
|
-
[
|
|
3972
|
+
[...ctl, "daemon-reload"],
|
|
3973
|
+
[...ctl, "enable", "--now", `${label}.timer`]
|
|
3904
3974
|
],
|
|
3905
3975
|
crewLog,
|
|
3906
|
-
schedulerLog
|
|
3976
|
+
schedulerLog,
|
|
3977
|
+
...linger
|
|
3907
3978
|
};
|
|
3908
3979
|
}
|
|
3909
3980
|
function planCron(ship, crewHome, job) {
|
|
@@ -3911,7 +3982,7 @@ function planCron(ship, crewHome, job) {
|
|
|
3911
3982
|
const crewLog = ship.logFile;
|
|
3912
3983
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
3913
3984
|
const minutes = Math.max(1, Math.round(INTERVAL_SECONDS / 60));
|
|
3914
|
-
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`;
|
|
3915
3986
|
return {
|
|
3916
3987
|
mechanism: "cron",
|
|
3917
3988
|
unitPaths: [],
|
|
@@ -3931,9 +4002,9 @@ function unitAbandonsAgents(plan, read) {
|
|
|
3931
4002
|
if (text === null) return null;
|
|
3932
4003
|
return plan.mechanism === "launchd" ? /<key>AbandonProcessGroup<\/key>\s*<true\/>/.test(text) : /^KillMode=process\s*$/m.test(text);
|
|
3933
4004
|
}
|
|
3934
|
-
function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
4005
|
+
function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run", ctx = USER_INSTALL) {
|
|
3935
4006
|
if (hostShip === "macos") return planLaunchd(ship, crewHome, job);
|
|
3936
|
-
if (hostShip === "linux" && hasSystemd) return planSystemd(ship, crewHome, job);
|
|
4007
|
+
if (hostShip === "linux" && hasSystemd) return planSystemd(ship, crewHome, job, ctx);
|
|
3937
4008
|
if (hostShip === "linux") return planCron(ship, crewHome, job);
|
|
3938
4009
|
throw new Error(`crew install: no scheduler support yet for ${hostShip} (Windows is planned, not built)`);
|
|
3939
4010
|
}
|
|
@@ -3950,20 +4021,22 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
3950
4021
|
};
|
|
3951
4022
|
}
|
|
3952
4023
|
if (hostShip === "linux" && hasSystemd) {
|
|
3953
|
-
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"];
|
|
3954
4027
|
if (job === "run") {
|
|
3955
4028
|
return {
|
|
3956
4029
|
mechanism: "systemd",
|
|
3957
4030
|
unitPaths: [join13(unitDir, `${label}.service`)],
|
|
3958
4031
|
crontabLine: null,
|
|
3959
|
-
unloadCommands: [[
|
|
4032
|
+
unloadCommands: [[...ctl, "disable", "--now", `${label}.service`]]
|
|
3960
4033
|
};
|
|
3961
4034
|
}
|
|
3962
4035
|
return {
|
|
3963
4036
|
mechanism: "systemd",
|
|
3964
4037
|
unitPaths: [join13(unitDir, `${label}.service`), join13(unitDir, `${label}.timer`)],
|
|
3965
4038
|
crontabLine: null,
|
|
3966
|
-
unloadCommands: [[
|
|
4039
|
+
unloadCommands: [[...ctl, "disable", "--now", `${label}.timer`]]
|
|
3967
4040
|
};
|
|
3968
4041
|
}
|
|
3969
4042
|
if (hostShip === "linux") {
|
|
@@ -3989,11 +4062,12 @@ function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
|
3989
4062
|
}
|
|
3990
4063
|
if (hostShip === "linux" && hasSystemd) {
|
|
3991
4064
|
const unit = `${label}.service`;
|
|
4065
|
+
const ctl = existsSync11(systemUnitPathFor(label)) ? ["systemctl"] : ["systemctl", "--user"];
|
|
3992
4066
|
return {
|
|
3993
4067
|
mechanism: "systemd",
|
|
3994
|
-
startCommand: [
|
|
3995
|
-
stopCommand: [
|
|
3996
|
-
statusCommand: [
|
|
4068
|
+
startCommand: [...ctl, "start", unit],
|
|
4069
|
+
stopCommand: [...ctl, "stop", unit],
|
|
4070
|
+
statusCommand: [...ctl, "status", unit]
|
|
3997
4071
|
};
|
|
3998
4072
|
}
|
|
3999
4073
|
if (hostShip === "linux") {
|
|
@@ -4014,11 +4088,14 @@ function runAll(commands, cwd, log) {
|
|
|
4014
4088
|
return true;
|
|
4015
4089
|
}
|
|
4016
4090
|
async function applyInstall(plan, crewHome, dryRun, log) {
|
|
4091
|
+
let changed2 = plan.crontabLine !== null;
|
|
4017
4092
|
for (const path of plan.unitPaths) {
|
|
4018
4093
|
if (dryRun) {
|
|
4019
4094
|
log.emit(`would write ${path}`);
|
|
4020
4095
|
continue;
|
|
4021
4096
|
}
|
|
4097
|
+
const before = existsSync11(path) ? readFileSync12(path, "utf8") : null;
|
|
4098
|
+
if (before !== plan.unitContent[path]) changed2 = true;
|
|
4022
4099
|
mkdirSync7(dirname5(path), { recursive: true });
|
|
4023
4100
|
writeFileSync8(path, plan.unitContent[path]);
|
|
4024
4101
|
log.emit(`wrote ${path}`);
|
|
@@ -4048,8 +4125,18 @@ async function applyInstall(plan, crewHome, dryRun, log) {
|
|
|
4048
4125
|
log.emit(`loaded (${plan.mechanism}), firing every ${INTERVAL_SECONDS}s starting ${INTERVAL_SECONDS}s from now`);
|
|
4049
4126
|
}
|
|
4050
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
|
+
}
|
|
4051
4137
|
log.emit(`crew's own log: ${plan.crewLog}`);
|
|
4052
4138
|
log.emit(`scheduler log (only what escapes the crew, e.g. a crash before it starts): ${plan.schedulerLog}`);
|
|
4139
|
+
return { changed: changed2 };
|
|
4053
4140
|
}
|
|
4054
4141
|
async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
4055
4142
|
if (plan.mechanism === "none") {
|
|
@@ -7563,6 +7650,28 @@ var Tracker = class {
|
|
|
7563
7650
|
if (!auth.commentShip) missing.push(`Comments.${this.contract.comments.ship}`);
|
|
7564
7651
|
return missing;
|
|
7565
7652
|
}
|
|
7653
|
+
/**
|
|
7654
|
+
* The runtime-map columns (ISSUE-2791) this workspace's template has not
|
|
7655
|
+
* taken yet, as `table.column` names for `crew doctor`. Each is read as its
|
|
7656
|
+
* default when absent, so this is informational: `Repos.forge_host`
|
|
7657
|
+
* (default `github.com`), `Ships.kind` (`developer_machine`),
|
|
7658
|
+
* `Ships.instance_class` and `Ships.relay_url` (blank). A table the route
|
|
7659
|
+
* does not have is skipped, not reported.
|
|
7660
|
+
*/
|
|
7661
|
+
async missingRuntimeColumns() {
|
|
7662
|
+
const wanted = [
|
|
7663
|
+
[this.route.resolved?.reposModelId, "Repos", ["forge_host"]],
|
|
7664
|
+
[this.route.resolved?.shipsModelId, "Ships", ["kind", "instance_class", "relay_url"]]
|
|
7665
|
+
];
|
|
7666
|
+
const missing = [];
|
|
7667
|
+
for (const [modelId, label, columns] of wanted) {
|
|
7668
|
+
if (!modelId) continue;
|
|
7669
|
+
const have = await this.columnNames(modelId);
|
|
7670
|
+
if (have.size === 0) continue;
|
|
7671
|
+
for (const c of columns) if (!have.has(c)) missing.push(`${label}.${c}`);
|
|
7672
|
+
}
|
|
7673
|
+
return missing;
|
|
7674
|
+
}
|
|
7566
7675
|
/**
|
|
7567
7676
|
* The public `/v/<slug>/r/<token>` link for one ticket (ISSUE-928) —
|
|
7568
7677
|
* fetch-or-create via `POST /views/:viewId/record-links`, so a repeated
|
|
@@ -9190,6 +9299,55 @@ function skillMarkerWarnings(promptsDir, personaPrompts) {
|
|
|
9190
9299
|
return out;
|
|
9191
9300
|
}
|
|
9192
9301
|
|
|
9302
|
+
// src/run-limit.ts
|
|
9303
|
+
var DEFAULT_MAX_RUN_MINUTES = 240;
|
|
9304
|
+
var KILL_GRACE_MS = 5e3;
|
|
9305
|
+
var WALL_CLOCK_REASON = "wall-clock";
|
|
9306
|
+
function resolveMaxRunMinutes(route2, toolchain) {
|
|
9307
|
+
return route2 ?? toolchain ?? DEFAULT_MAX_RUN_MINUTES;
|
|
9308
|
+
}
|
|
9309
|
+
function deadlineFor(spawnedAt, maxRunMinutes) {
|
|
9310
|
+
if (!maxRunMinutes || maxRunMinutes <= 0) return void 0;
|
|
9311
|
+
const t = Date.parse(spawnedAt);
|
|
9312
|
+
return Number.isNaN(t) ? void 0 : new Date(t + maxRunMinutes * 6e4).toISOString();
|
|
9313
|
+
}
|
|
9314
|
+
function msUntilDeadline(deadline, now = Date.now()) {
|
|
9315
|
+
if (!deadline) return void 0;
|
|
9316
|
+
const t = Date.parse(deadline);
|
|
9317
|
+
return Number.isNaN(t) ? void 0 : t - now;
|
|
9318
|
+
}
|
|
9319
|
+
async function killRunGroup(pid, o = {}) {
|
|
9320
|
+
const alive = o.isAlive ?? ((p) => {
|
|
9321
|
+
try {
|
|
9322
|
+
process.kill(p, 0);
|
|
9323
|
+
return true;
|
|
9324
|
+
} catch (e) {
|
|
9325
|
+
return e.code === "EPERM";
|
|
9326
|
+
}
|
|
9327
|
+
});
|
|
9328
|
+
const signal = (sig) => {
|
|
9329
|
+
try {
|
|
9330
|
+
process.kill(-pid, sig);
|
|
9331
|
+
} catch {
|
|
9332
|
+
try {
|
|
9333
|
+
process.kill(pid, sig);
|
|
9334
|
+
} catch {
|
|
9335
|
+
}
|
|
9336
|
+
}
|
|
9337
|
+
};
|
|
9338
|
+
signal("SIGTERM");
|
|
9339
|
+
const end = Date.now() + (o.graceMs ?? KILL_GRACE_MS);
|
|
9340
|
+
while (alive(pid) && Date.now() < end) await new Promise((r) => setTimeout(r, 50));
|
|
9341
|
+
if (alive(pid)) signal("SIGKILL");
|
|
9342
|
+
}
|
|
9343
|
+
var RUN_KILLED_MARKER = "<!-- crew:run-killed -->";
|
|
9344
|
+
function runKilledComment(b) {
|
|
9345
|
+
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.";
|
|
9346
|
+
return `${RUN_KILLED_MARKER}
|
|
9347
|
+
|
|
9348
|
+
The agent run on this ticket was stopped at its wall-clock limit of ${b.limitMinutes} minutes before it handed on${b.branch ? `; its branch is \`${b.branch}\`` : ""}. ${tree} The worktree is left as it was. The next run on this seat resumes from it rather than starting over.`;
|
|
9349
|
+
}
|
|
9350
|
+
|
|
9193
9351
|
// src/agent.ts
|
|
9194
9352
|
var DIGEST_MAX_AGE_SECONDS = 600;
|
|
9195
9353
|
var LEGACY_TOOLCHAIN_HARNESS = "claude";
|
|
@@ -9299,6 +9457,7 @@ function planAgentRun(o) {
|
|
|
9299
9457
|
toolchain: toolchain.name,
|
|
9300
9458
|
harness: toolchain.harness,
|
|
9301
9459
|
provider: toolchainProvider(toolchain),
|
|
9460
|
+
maxRunMinutes: resolveMaxRunMinutes(o.route.maxRunMinutes, toolchain.maxRunMinutes),
|
|
9302
9461
|
resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
|
|
9303
9462
|
warnings: [...seat.warnings ?? [], ...expanded.warnings],
|
|
9304
9463
|
prompt,
|
|
@@ -9388,6 +9547,17 @@ function identityOf(plan, adapterHarness) {
|
|
|
9388
9547
|
promptSha: plan.promptSha
|
|
9389
9548
|
};
|
|
9390
9549
|
}
|
|
9550
|
+
async function killForDeadline(pid, emit, role, ticket, spawnedAt, limitMinutes) {
|
|
9551
|
+
const elapsedMs = Date.now() - Date.parse(spawnedAt);
|
|
9552
|
+
const limit = limitMinutes ?? Math.round(elapsedMs / 6e4);
|
|
9553
|
+
emit.emit(`run_killed: ${role} run exceeded its wall-clock limit of ${limit} min (ran ${Math.round(elapsedMs / 6e4)} min)`, {
|
|
9554
|
+
role,
|
|
9555
|
+
...ticket ? { ticket } : {},
|
|
9556
|
+
data: { event: "run_killed", reason: WALL_CLOCK_REASON, limitMinutes: limit, elapsedMs, pid }
|
|
9557
|
+
});
|
|
9558
|
+
await killRunGroup(pid);
|
|
9559
|
+
return { limitMinutes: limit, elapsedMs };
|
|
9560
|
+
}
|
|
9391
9561
|
var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
|
|
9392
9562
|
function openSink(path) {
|
|
9393
9563
|
try {
|
|
@@ -9747,13 +9917,16 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
9747
9917
|
detached: true
|
|
9748
9918
|
});
|
|
9749
9919
|
closeSync3(streamFd);
|
|
9920
|
+
const spawnedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9921
|
+
const deadline = deadlineFor(spawnedAt, plan.maxRunMinutes);
|
|
9750
9922
|
if (child.pid !== void 0) {
|
|
9751
9923
|
const sidecar = {
|
|
9752
9924
|
pid: child.pid,
|
|
9753
9925
|
ownerPid: process.pid,
|
|
9754
|
-
spawnedAt
|
|
9926
|
+
spawnedAt,
|
|
9755
9927
|
role: plan.role,
|
|
9756
9928
|
route: plan.route,
|
|
9929
|
+
...deadline ? { deadline, maxRunMinutes: plan.maxRunMinutes } : {},
|
|
9757
9930
|
ticket: plan.ticket ?? null,
|
|
9758
9931
|
ticketRecordId: plan.ticketRecordId ?? null,
|
|
9759
9932
|
repo: plan.repo ?? null,
|
|
@@ -9773,25 +9946,45 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
9773
9946
|
}
|
|
9774
9947
|
}
|
|
9775
9948
|
const obs = new RunObserver(id, adapter, eventsSink, emit.cycle);
|
|
9776
|
-
const meta = { pid: child.pid ?? null };
|
|
9949
|
+
const meta = { pid: child.pid ?? null, ...deadline ? { deadline } : {} };
|
|
9777
9950
|
obs.onClaim = () => writeRunMeta(id, startedAt, obs.liveTicket, meta);
|
|
9778
9951
|
if (child.pid !== void 0) writeRunMeta(id, startedAt, obs.liveTicket, meta);
|
|
9779
9952
|
const tail3 = new FileTail(plan.streamPath);
|
|
9780
9953
|
const timer = setInterval(() => obs.feed(tail3.poll()), TAIL_POLL_MS);
|
|
9954
|
+
let killed;
|
|
9955
|
+
const limitTimer = child.pid !== void 0 && deadline ? setTimeout(() => {
|
|
9956
|
+
const pid = child.pid;
|
|
9957
|
+
void killForDeadline(pid, emit, plan.role, obs.liveTicket ?? plan.ticket ?? null, spawnedAt, plan.maxRunMinutes).then((k) => {
|
|
9958
|
+
killed = k;
|
|
9959
|
+
});
|
|
9960
|
+
}, Math.max(0, msUntilDeadline(deadline) ?? 0)) : void 0;
|
|
9781
9961
|
let spawnFailed = false;
|
|
9782
9962
|
child.on("error", (err3) => {
|
|
9783
9963
|
spawnFailed = true;
|
|
9784
9964
|
clearInterval(timer);
|
|
9965
|
+
clearTimeout(limitTimer);
|
|
9785
9966
|
reject(new AgentSpawnError(`cannot run ${plan.bin}: ${err3.message}`));
|
|
9786
9967
|
});
|
|
9787
9968
|
child.on("close", async (code) => {
|
|
9788
9969
|
clearInterval(timer);
|
|
9970
|
+
clearTimeout(limitTimer);
|
|
9789
9971
|
obs.feed(tail3.poll());
|
|
9790
9972
|
obs.end();
|
|
9791
9973
|
await endSink(eventsSink);
|
|
9792
9974
|
if (spawnFailed) return;
|
|
9793
|
-
|
|
9794
|
-
|
|
9975
|
+
if (limitTimer && !killed && !obs.sawResult && msUntilDeadline(deadline) <= 0) {
|
|
9976
|
+
killed = { limitMinutes: plan.maxRunMinutes ?? 0, elapsedMs: Date.now() - Date.parse(spawnedAt) };
|
|
9977
|
+
}
|
|
9978
|
+
const { ms: ms3, handoffPending } = await finishRun(
|
|
9979
|
+
id,
|
|
9980
|
+
obs,
|
|
9981
|
+
emit,
|
|
9982
|
+
startedAt,
|
|
9983
|
+
{ code: code ?? 1, ...killed ? { interrupted: WALL_CLOCK_REASON } : {} },
|
|
9984
|
+
plan.agentLog?.apiKey,
|
|
9985
|
+
hintMoved
|
|
9986
|
+
);
|
|
9987
|
+
resolve6({ code: code ?? 1, ms: ms3, ...handoffPending ? { handoffPending } : {}, ...killed ? { killed } : {} });
|
|
9795
9988
|
});
|
|
9796
9989
|
child.stdin?.on("error", (err3) => {
|
|
9797
9990
|
emit.warn(`agent closed stdin before the prompt was delivered: ${err3.message}`, { step: "agent" });
|
|
@@ -9810,18 +10003,23 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
|
|
|
9810
10003
|
obs.feed(tail3.poll());
|
|
9811
10004
|
obs.muted = false;
|
|
9812
10005
|
const adopted = isAlive(sc);
|
|
10006
|
+
let killed;
|
|
9813
10007
|
if (adopted) {
|
|
9814
10008
|
o.onAdopted?.();
|
|
9815
10009
|
const pollMs = o.pollMs ?? TAIL_POLL_MS;
|
|
9816
10010
|
while (isAlive(sc)) {
|
|
9817
10011
|
obs.feed(tail3.poll());
|
|
10012
|
+
if (!killed && (msUntilDeadline(sc.deadline) ?? 1) <= 0) {
|
|
10013
|
+
killed = await killForDeadline(sc.pid, emit, sc.role, obs.liveTicket ?? sc.ticket, sc.spawnedAt, sc.maxRunMinutes);
|
|
10014
|
+
continue;
|
|
10015
|
+
}
|
|
9818
10016
|
await new Promise((r) => setTimeout(r, pollMs));
|
|
9819
10017
|
}
|
|
9820
10018
|
}
|
|
9821
10019
|
obs.feed(tail3.poll());
|
|
9822
10020
|
obs.end();
|
|
9823
10021
|
await endSink(eventsSink);
|
|
9824
|
-
const interrupted = obs.sawResult ? void 0 : adopted ? "no-result" : "daemon-exit";
|
|
10022
|
+
const interrupted = killed ? WALL_CLOCK_REASON : obs.sawResult ? void 0 : adopted ? "no-result" : "daemon-exit";
|
|
9825
10023
|
const { ms: ms3, ticket } = await finishRun(
|
|
9826
10024
|
id,
|
|
9827
10025
|
obs,
|
|
@@ -9830,7 +10028,7 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
|
|
|
9830
10028
|
{ code: null, ...interrupted ? { interrupted } : {} },
|
|
9831
10029
|
o.agentLogKey
|
|
9832
10030
|
);
|
|
9833
|
-
return { adopted, ...interrupted ? { interrupted } : {}, ms: ms3, ticket };
|
|
10031
|
+
return { adopted, ...interrupted ? { interrupted } : {}, ...killed ? { killed } : {}, ms: ms3, ticket };
|
|
9834
10032
|
}
|
|
9835
10033
|
|
|
9836
10034
|
// src/seat-keys.ts
|
|
@@ -10335,8 +10533,8 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
10335
10533
|
// src/cli.ts
|
|
10336
10534
|
import { resolve as resolve5, dirname as dirname22, join as join44, basename as basename13 } from "path";
|
|
10337
10535
|
import { fileURLToPath } from "url";
|
|
10338
|
-
import { hostname as
|
|
10339
|
-
import { spawnSync as
|
|
10536
|
+
import { hostname as hostname3, homedir as homedir10 } from "os";
|
|
10537
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
10340
10538
|
import { parse as parseYaml2 } from "yaml";
|
|
10341
10539
|
|
|
10342
10540
|
// src/argv.ts
|
|
@@ -10348,6 +10546,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
10348
10546
|
"clear",
|
|
10349
10547
|
"dir",
|
|
10350
10548
|
"extra-path",
|
|
10549
|
+
"forge-host",
|
|
10351
10550
|
"harness",
|
|
10352
10551
|
"hook",
|
|
10353
10552
|
"host",
|
|
@@ -12588,134 +12787,719 @@ function flagWizardAnswers(f, env) {
|
|
|
12588
12787
|
};
|
|
12589
12788
|
}
|
|
12590
12789
|
|
|
12591
|
-
// src/
|
|
12592
|
-
import {
|
|
12593
|
-
import {
|
|
12594
|
-
import {
|
|
12790
|
+
// src/provision.ts
|
|
12791
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
12792
|
+
import { existsSync as existsSync25, readFileSync as readFileSync23, writeFileSync as writeFileSync19 } from "fs";
|
|
12793
|
+
import { hostname as hostname2 } from "os";
|
|
12794
|
+
import { parseDocument as parseDocument3 } from "yaml";
|
|
12595
12795
|
|
|
12596
|
-
// src/
|
|
12597
|
-
|
|
12598
|
-
|
|
12599
|
-
|
|
12600
|
-
|
|
12601
|
-
|
|
12602
|
-
|
|
12603
|
-
|
|
12604
|
-
|
|
12605
|
-
|
|
12606
|
-
|
|
12607
|
-
|
|
12796
|
+
// src/doctor-fix.ts
|
|
12797
|
+
import { parseDocument as parseDocument2 } from "yaml";
|
|
12798
|
+
|
|
12799
|
+
// src/repos-cmd.ts
|
|
12800
|
+
import { homedir as homedir9 } from "os";
|
|
12801
|
+
import { basename as basename8, join as join29, resolve as resolve3 } from "path";
|
|
12802
|
+
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
12803
|
+
var ReposError = class extends Error {
|
|
12804
|
+
};
|
|
12805
|
+
function normalizeRemote(url) {
|
|
12806
|
+
if (!url) return void 0;
|
|
12807
|
+
let s = url.trim();
|
|
12808
|
+
if (!s) return void 0;
|
|
12809
|
+
const scp = /^[^\s@/]+@[^\s:/]+:(.+)$/.exec(s);
|
|
12810
|
+
if (scp) s = scp[1];
|
|
12811
|
+
else s = s.replace(/^[a-z][a-z0-9+.-]*:\/\/(?:[^/@]+@)?[^/]+\//i, "");
|
|
12812
|
+
s = s.replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
12813
|
+
const parts = s.split("/").filter(Boolean);
|
|
12814
|
+
if (parts.length < 2) return void 0;
|
|
12815
|
+
return parts.slice(-2).join("/").toLowerCase();
|
|
12816
|
+
}
|
|
12817
|
+
function matchRepoName(origin, resolved) {
|
|
12818
|
+
const want = normalizeRemote(origin);
|
|
12819
|
+
if (!want || !resolved?.repoRemotes) return void 0;
|
|
12820
|
+
for (const [id, remote] of Object.entries(resolved.repoRemotes)) {
|
|
12821
|
+
if (normalizeRemote(remote) === want) return resolved.repoNames?.[id];
|
|
12608
12822
|
}
|
|
12609
|
-
|
|
12610
|
-
|
|
12611
|
-
|
|
12612
|
-
|
|
12613
|
-
|
|
12614
|
-
|
|
12823
|
+
return void 0;
|
|
12824
|
+
}
|
|
12825
|
+
function routeNamesOf(text) {
|
|
12826
|
+
const routes = parseDocument(text).get("routes", true);
|
|
12827
|
+
if (!isSeq(routes)) return [];
|
|
12828
|
+
return routes.items.flatMap((i) => isMap(i) && typeof i.get("route") === "string" ? [i.get("route")] : []);
|
|
12829
|
+
}
|
|
12830
|
+
function parseAddArgs(args, routes, isDir) {
|
|
12831
|
+
const rest = [...args];
|
|
12832
|
+
let route2;
|
|
12833
|
+
if (rest.length > 0 && routes.includes(rest[0])) route2 = rest.shift();
|
|
12834
|
+
else if (rest.length > 0 && /^[^/\s]+\/[^/\s]+$/.test(rest[0]) && !isDir(rest[0])) {
|
|
12835
|
+
throw new ReposError(`no route "${rest[0]}" in the config (have: ${routes.join(", ") || "none"})`);
|
|
12615
12836
|
}
|
|
12616
|
-
|
|
12617
|
-
|
|
12618
|
-
|
|
12619
|
-
for (const u of elsewhere) {
|
|
12620
|
-
const name = u.repo ?? "?";
|
|
12621
|
-
byRepoName.set(name, [...byRepoName.get(name) ?? [], u.ticket.issue_id]);
|
|
12622
|
-
}
|
|
12623
|
-
notes.push({
|
|
12624
|
-
level: "info",
|
|
12625
|
-
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("; ")
|
|
12626
|
-
});
|
|
12837
|
+
if (!route2) {
|
|
12838
|
+
if (routes.length === 1) route2 = routes[0];
|
|
12839
|
+
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(", ")}`);
|
|
12627
12840
|
}
|
|
12628
|
-
|
|
12841
|
+
if (rest.length > 2) throw new ReposError("too many arguments: expected [route] [name] [path]");
|
|
12842
|
+
if (rest.length === 2) return { route: route2, name: rest[0], path: rest[1] };
|
|
12843
|
+
if (rest.length === 1) return isDir(rest[0]) ? { route: route2, path: rest[0] } : { route: route2, name: rest[0], path: "." };
|
|
12844
|
+
return { route: route2, path: "." };
|
|
12629
12845
|
}
|
|
12630
|
-
|
|
12631
|
-
|
|
12632
|
-
|
|
12633
|
-
|
|
12634
|
-
|
|
12635
|
-
|
|
12636
|
-
|
|
12846
|
+
function expandDir(p, base) {
|
|
12847
|
+
const home = p === "~" ? homedir9() : p.startsWith("~/") ? `${homedir9()}/${p.slice(2)}` : p;
|
|
12848
|
+
return resolve3(base, home);
|
|
12849
|
+
}
|
|
12850
|
+
function routeMapOf(doc, route2) {
|
|
12851
|
+
const routes = doc.get("routes", true);
|
|
12852
|
+
if (isSeq(routes)) {
|
|
12853
|
+
for (const item of routes.items) {
|
|
12854
|
+
if (isMap(item) && item.get("route") === route2) return item;
|
|
12855
|
+
}
|
|
12637
12856
|
}
|
|
12857
|
+
const have = isSeq(routes) ? routes.items.map((i) => isMap(i) ? String(i.get("route")) : "?").join(", ") : "none";
|
|
12858
|
+
throw new ReposError(`no route "${route2}" in the config (have: ${have})`);
|
|
12638
12859
|
}
|
|
12639
|
-
function
|
|
12640
|
-
const
|
|
12641
|
-
|
|
12642
|
-
|
|
12860
|
+
function configuredRepos(text, route2, base) {
|
|
12861
|
+
const doc = parseDocument(text);
|
|
12862
|
+
const m = routeMapOf(doc, route2);
|
|
12863
|
+
const repos = m.get("repos");
|
|
12864
|
+
if (isMap(repos)) {
|
|
12865
|
+
return repos.items.map((p) => {
|
|
12866
|
+
const name = String(p.key.value ?? p.key);
|
|
12867
|
+
const v = p.value;
|
|
12868
|
+
const dir2 = isMap(v) ? String(v.get("dir") ?? "") : String(v?.value ?? "");
|
|
12869
|
+
return { name, dir: expandDir(dir2, base) };
|
|
12870
|
+
});
|
|
12643
12871
|
}
|
|
12644
|
-
const
|
|
12645
|
-
|
|
12646
|
-
const untracked = changes.filter((c) => c.untracked).length;
|
|
12647
|
-
const names = changes.slice(0, 3).map((c) => c.path).join(", ");
|
|
12648
|
-
const more = changes.length > 3 ? `, +${changes.length - 3} more` : "";
|
|
12649
|
-
const untrackedNote = untracked > 0 ? ` (${untracked} untracked)` : "";
|
|
12650
|
-
return {
|
|
12651
|
-
kind: "dirty",
|
|
12652
|
-
detail: `working tree has ${changes.length} change(s)${untrackedNote}: ${names}${more}`
|
|
12653
|
-
};
|
|
12872
|
+
const dir = m.get("dir");
|
|
12873
|
+
return dir ? [{ name: basename8(String(dir)), dir: expandDir(String(dir), base) }] : [];
|
|
12654
12874
|
}
|
|
12655
|
-
function
|
|
12656
|
-
|
|
12657
|
-
|
|
12875
|
+
function cloneBaseOf(text, route2, base, isCheckout) {
|
|
12876
|
+
const m = routeMapOf(parseDocument(text), route2);
|
|
12877
|
+
const repos = m.get("repos", true);
|
|
12878
|
+
if (isMap(repos) && repos.items.length > 0) return null;
|
|
12879
|
+
const dir = m.get("dir");
|
|
12880
|
+
if (!dir) return null;
|
|
12881
|
+
const abs = expandDir(String(dir), base);
|
|
12882
|
+
return isCheckout(abs) ? null : abs;
|
|
12658
12883
|
}
|
|
12659
|
-
|
|
12660
|
-
|
|
12661
|
-
|
|
12662
|
-
|
|
12663
|
-
|
|
12664
|
-
|
|
12665
|
-
const
|
|
12666
|
-
|
|
12667
|
-
let line = raw.trim();
|
|
12668
|
-
const ref = line.match(TICKET_REF_AT_START);
|
|
12669
|
-
let key = line;
|
|
12670
|
-
if (ref) {
|
|
12671
|
-
const prefix = ref[1];
|
|
12672
|
-
const num = ref[2];
|
|
12673
|
-
const canonical = own && own[2] === num ? own[1] : prefix;
|
|
12674
|
-
line = `${canonical}-${num}${line.slice(ref[0].length)}`;
|
|
12675
|
-
key = `${canonical}-${num}`;
|
|
12676
|
-
}
|
|
12677
|
-
if (seen.has(key)) continue;
|
|
12678
|
-
seen.add(key);
|
|
12679
|
-
out.push(line);
|
|
12680
|
-
}
|
|
12681
|
-
return out;
|
|
12884
|
+
function routeSetsHooks(text, route2, name) {
|
|
12885
|
+
const m = routeMapOf(parseDocument(text), route2);
|
|
12886
|
+
const repos = m.get("repos", true);
|
|
12887
|
+
let target = m;
|
|
12888
|
+
if (isMap(repos)) target = repos.get(name, true);
|
|
12889
|
+
if (!isMap(target)) return false;
|
|
12890
|
+
const hooks = target.get("hooks", true);
|
|
12891
|
+
return isMap(hooks) && hooks.items.length > 0;
|
|
12682
12892
|
}
|
|
12683
|
-
|
|
12684
|
-
|
|
12685
|
-
const
|
|
12686
|
-
const
|
|
12687
|
-
|
|
12688
|
-
|
|
12689
|
-
|
|
12690
|
-
|
|
12691
|
-
|
|
12692
|
-
|
|
12693
|
-
|
|
12694
|
-
|
|
12695
|
-
|
|
12696
|
-
|
|
12697
|
-
|
|
12698
|
-
|
|
12699
|
-
|
|
12700
|
-
|
|
12701
|
-
|
|
12702
|
-
|
|
12703
|
-
|
|
12704
|
-
|
|
12705
|
-
|
|
12706
|
-
|
|
12707
|
-
|
|
12708
|
-
|
|
12709
|
-
|
|
12710
|
-
|
|
12711
|
-
|
|
12712
|
-
|
|
12713
|
-
|
|
12714
|
-
|
|
12715
|
-
|
|
12716
|
-
|
|
12717
|
-
|
|
12718
|
-
|
|
12893
|
+
function listRepoRows(i) {
|
|
12894
|
+
const cloneBase = i.isCheckout ? cloneBaseOf(i.text, i.route, i.base, i.isCheckout) : null;
|
|
12895
|
+
const checkouts = configuredRepos(i.text, i.route, i.base).filter((c) => c.dir !== cloneBase);
|
|
12896
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
12897
|
+
const hooksSourceOf = (c) => i.exists(c.dir) && i.repoHasHooks(c.dir) ? "repo" : routeSetsHooks(i.text, i.route, c.name) ? "route" : "none";
|
|
12898
|
+
const originFor = (c, remote) => {
|
|
12899
|
+
const o = i.originOf(c.dir) ?? null;
|
|
12900
|
+
if (!o) return null;
|
|
12901
|
+
return remote !== null && normalizeRemote(o) === normalizeRemote(remote) ? null : o;
|
|
12902
|
+
};
|
|
12903
|
+
const rows = [];
|
|
12904
|
+
const names = i.resolved?.repoNames ?? {};
|
|
12905
|
+
const remotes = i.resolved?.repoRemotes ?? {};
|
|
12906
|
+
for (const [id, name] of Object.entries(names)) {
|
|
12907
|
+
const remote = remotes[id] ?? null;
|
|
12908
|
+
const want = normalizeRemote(remote);
|
|
12909
|
+
let idx = checkouts.findIndex((c2, n) => !claimed.has(n) && want !== void 0 && normalizeRemote(i.originOf(c2.dir)) === want);
|
|
12910
|
+
if (idx < 0) idx = checkouts.findIndex((c2, n) => !claimed.has(n) && c2.name === name);
|
|
12911
|
+
const c = idx >= 0 ? checkouts[idx] : void 0;
|
|
12912
|
+
if (idx >= 0) claimed.add(idx);
|
|
12913
|
+
rows.push({
|
|
12914
|
+
name,
|
|
12915
|
+
remote,
|
|
12916
|
+
repoRowId: id,
|
|
12917
|
+
attached: c !== void 0,
|
|
12918
|
+
dir: c?.dir ?? null,
|
|
12919
|
+
exists: c ? i.exists(c.dir) : false,
|
|
12920
|
+
origin: c ? originFor(c, remote) : null,
|
|
12921
|
+
matchedRow: c ? name : null,
|
|
12922
|
+
hooksSource: c ? hooksSourceOf(c) : "none"
|
|
12923
|
+
});
|
|
12924
|
+
}
|
|
12925
|
+
checkouts.forEach((c, n) => {
|
|
12926
|
+
if (claimed.has(n)) return;
|
|
12927
|
+
rows.push({
|
|
12928
|
+
name: c.name,
|
|
12929
|
+
remote: null,
|
|
12930
|
+
repoRowId: null,
|
|
12931
|
+
attached: true,
|
|
12932
|
+
dir: c.dir,
|
|
12933
|
+
exists: i.exists(c.dir),
|
|
12934
|
+
origin: originFor(c, null),
|
|
12935
|
+
matchedRow: null,
|
|
12936
|
+
hooksSource: hooksSourceOf(c)
|
|
12937
|
+
});
|
|
12938
|
+
});
|
|
12939
|
+
return rows;
|
|
12940
|
+
}
|
|
12941
|
+
function planRepoAdd(i) {
|
|
12942
|
+
const doc = parseDocument(i.text);
|
|
12943
|
+
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
12944
|
+
const m = routeMapOf(doc, i.route);
|
|
12945
|
+
let existing = configuredRepos(i.text, i.route, i.base);
|
|
12946
|
+
const target = expandDir(i.path, i.base);
|
|
12947
|
+
let droppedDir;
|
|
12948
|
+
const placeholder = m.get("dir");
|
|
12949
|
+
const reposNow = m.get("repos", true);
|
|
12950
|
+
if (i.isCheckout && placeholder && !(isMap(reposNow) && reposNow.items.length > 0)) {
|
|
12951
|
+
const placeholderDir = expandDir(String(placeholder), i.base);
|
|
12952
|
+
if (placeholderDir !== target && !i.isCheckout(placeholderDir)) {
|
|
12953
|
+
droppedDir = placeholderDir;
|
|
12954
|
+
m.delete("dir");
|
|
12955
|
+
existing = [];
|
|
12956
|
+
}
|
|
12957
|
+
}
|
|
12958
|
+
if (existing.some((e) => e.dir === target)) {
|
|
12959
|
+
throw new ReposError(`${target} is already configured on route ${i.route}`);
|
|
12960
|
+
}
|
|
12961
|
+
const matched = matchRepoName(i.origin, i.resolved);
|
|
12962
|
+
const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename8(target);
|
|
12963
|
+
let warning;
|
|
12964
|
+
if (!matched) {
|
|
12965
|
+
const known = Object.values(i.resolved?.repoNames ?? {});
|
|
12966
|
+
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";
|
|
12967
|
+
}
|
|
12968
|
+
const repos = m.get("repos", true);
|
|
12969
|
+
let mode;
|
|
12970
|
+
if (isMap(repos)) {
|
|
12971
|
+
if (repos.has(name)) throw new ReposError(`route ${i.route} already has a repo named "${name}" (${String(repos.get(name))})`);
|
|
12972
|
+
repos.set(name, target);
|
|
12973
|
+
mode = "repos";
|
|
12974
|
+
} else if (m.has("dir") && existing.length > 0) {
|
|
12975
|
+
const old = existing[0];
|
|
12976
|
+
const oldName = i.nameForExisting?.(old.dir) ?? old.name;
|
|
12977
|
+
if (oldName === name) throw new ReposError(`route ${i.route} already has a repo named "${name}" (${old.dir})`);
|
|
12978
|
+
const oldDir = String(m.get("dir"));
|
|
12979
|
+
const pair = m.items.find((p) => p.key.value === "dir");
|
|
12980
|
+
pair.key = doc.createNode("repos");
|
|
12981
|
+
pair.value = doc.createNode({ [oldName]: oldDir, [name]: target });
|
|
12982
|
+
mode = "repos";
|
|
12983
|
+
} else if (droppedDir) {
|
|
12984
|
+
m.set("repos", doc.createNode({ [name]: target }));
|
|
12985
|
+
mode = "repos";
|
|
12986
|
+
} else {
|
|
12987
|
+
m.set("dir", target);
|
|
12988
|
+
mode = "dir";
|
|
12989
|
+
}
|
|
12990
|
+
return { text: String(doc), mode, name, block: renderBlock(m), warning, unmatched: !matched, droppedDir };
|
|
12991
|
+
}
|
|
12992
|
+
function planRepoRemove(text, base, route2, path) {
|
|
12993
|
+
const doc = parseDocument(text);
|
|
12994
|
+
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
12995
|
+
const m = routeMapOf(doc, route2);
|
|
12996
|
+
const target = expandDir(path, base);
|
|
12997
|
+
const existing = configuredRepos(text, route2, base);
|
|
12998
|
+
const hit = existing.find((e) => e.dir === target);
|
|
12999
|
+
if (!hit) throw new ReposError(`${target} is not configured on route ${route2}`);
|
|
13000
|
+
const repos = m.get("repos", true);
|
|
13001
|
+
if (isMap(repos)) {
|
|
13002
|
+
repos.delete(hit.name);
|
|
13003
|
+
const left = existing.filter((e) => e !== hit);
|
|
13004
|
+
if (left.length === 1) {
|
|
13005
|
+
const pair = m.items.find((p) => p.key.value === "repos");
|
|
13006
|
+
pair.key = doc.createNode("dir");
|
|
13007
|
+
pair.value = doc.createNode(String(repos.get(left[0].name)));
|
|
13008
|
+
} else if (left.length === 0) m.delete("repos");
|
|
13009
|
+
} else {
|
|
13010
|
+
m.delete("dir");
|
|
13011
|
+
}
|
|
13012
|
+
return { text: String(doc), name: hit.name };
|
|
13013
|
+
}
|
|
13014
|
+
function renderBlock(m) {
|
|
13015
|
+
return stringify2([m.toJSON()]).trimEnd();
|
|
13016
|
+
}
|
|
13017
|
+
var LOCAL_RELEASE_MODES = /* @__PURE__ */ new Set(["integrate", "external"]);
|
|
13018
|
+
function planRepoRow(i) {
|
|
13019
|
+
const remote = normalizeRemote(i.origin);
|
|
13020
|
+
if (!remote) throw new ReposError(`cannot derive an owner/repo from origin "${i.origin}"`);
|
|
13021
|
+
const mode = i.spec?.releaseMode;
|
|
13022
|
+
return {
|
|
13023
|
+
name: i.name,
|
|
13024
|
+
remote,
|
|
13025
|
+
platform: i.spec?.platform ?? "unix",
|
|
13026
|
+
release_mode: mode ? LOCAL_RELEASE_MODES.has(mode) ? "local" : mode : "local",
|
|
13027
|
+
ci_provider: i.spec?.ciProvider ?? "none",
|
|
13028
|
+
enabled: true,
|
|
13029
|
+
project_id: i.projectId
|
|
13030
|
+
};
|
|
13031
|
+
}
|
|
13032
|
+
function resolveRepoProject(projects, opt, routeAreaId) {
|
|
13033
|
+
if (opt) {
|
|
13034
|
+
const want = opt.toLowerCase();
|
|
13035
|
+
const hit = projects.find((p) => p.id === opt || p.name?.toLowerCase() === want);
|
|
13036
|
+
if (!hit) throw new ReposError(`no Projects row "${opt}" (have: ${projects.map((p) => p.name ?? p.id).join(", ") || "none"})`);
|
|
13037
|
+
return hit.id;
|
|
13038
|
+
}
|
|
13039
|
+
if (routeAreaId) return routeAreaId;
|
|
13040
|
+
if (projects.length === 1) return projects[0].id;
|
|
13041
|
+
throw new ReposError(
|
|
13042
|
+
`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`
|
|
13043
|
+
);
|
|
13044
|
+
}
|
|
13045
|
+
function parseCloneArgs(args, routes) {
|
|
13046
|
+
const rest = [...args];
|
|
13047
|
+
let route2;
|
|
13048
|
+
if (rest.length > 1 && routes.includes(rest[0])) route2 = rest.shift();
|
|
13049
|
+
else if (rest.length > 1) throw new ReposError(`no route "${rest[0]}" in the config (have: ${routes.join(", ") || "none"})`);
|
|
13050
|
+
if (!route2) {
|
|
13051
|
+
if (routes.length === 1) route2 = routes[0];
|
|
13052
|
+
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(", ")}`);
|
|
13053
|
+
}
|
|
13054
|
+
if (rest.length !== 1) throw new ReposError("expected [route] <name> \u2014 the Repos row to clone (see `crew repos list`)");
|
|
13055
|
+
return { route: route2, name: rest[0] };
|
|
13056
|
+
}
|
|
13057
|
+
function planRepoClone(i) {
|
|
13058
|
+
const names = i.resolved?.repoNames ?? {};
|
|
13059
|
+
const id = Object.keys(names).find((k) => names[k].toLowerCase() === i.name.toLowerCase());
|
|
13060
|
+
if (!id) {
|
|
13061
|
+
const known = Object.values(names);
|
|
13062
|
+
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"}`);
|
|
13063
|
+
}
|
|
13064
|
+
const name = names[id];
|
|
13065
|
+
const remote = normalizeRemote(i.resolved?.repoRemotes?.[id]);
|
|
13066
|
+
if (!remote) {
|
|
13067
|
+
throw new ReposError(
|
|
13068
|
+
`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\``
|
|
13069
|
+
);
|
|
13070
|
+
}
|
|
13071
|
+
const doc = parseDocument(i.text);
|
|
13072
|
+
const m = routeMapOf(doc, i.route);
|
|
13073
|
+
const existing = configuredRepos(i.text, i.route, i.base);
|
|
13074
|
+
if (existing.some((e) => e.name.toLowerCase() === name.toLowerCase())) {
|
|
13075
|
+
throw new ReposError(`route ${i.route} already has a checkout named "${name}" \u2014 nothing to clone`);
|
|
13076
|
+
}
|
|
13077
|
+
const ship = doc.get("ship", true);
|
|
13078
|
+
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
13079
|
+
const target = i.dir ? expandDir(i.dir, i.base) : join29(expandDir(basePath, i.base), i.route.split("/")[0], name);
|
|
13080
|
+
const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name, isCheckout: i.isCheckout });
|
|
13081
|
+
return { ...add, remote, target };
|
|
13082
|
+
}
|
|
13083
|
+
|
|
13084
|
+
// src/doctor-fix.ts
|
|
13085
|
+
var sound = (r) => r.hasRepos && r.problems.length === 0;
|
|
13086
|
+
function planFix(f) {
|
|
13087
|
+
const actions = [];
|
|
13088
|
+
const notes = [];
|
|
13089
|
+
for (const r of f.routes) {
|
|
13090
|
+
if (!r.enabled && sound(r)) {
|
|
13091
|
+
actions.push({ kind: "enable", route: r.route });
|
|
13092
|
+
const gaps = r.readiness ?? [];
|
|
13093
|
+
if (gaps.length) {
|
|
13094
|
+
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`);
|
|
13095
|
+
}
|
|
13096
|
+
} else if (!sound(r) && !r.enabled) {
|
|
13097
|
+
notes.push(`${r.route}: not enabled \u2014 ${r.hasRepos ? r.problems[0] : "no checkout is configured"}`);
|
|
13098
|
+
}
|
|
13099
|
+
}
|
|
13100
|
+
if (f.schedulerAbsent && f.routes.some(sound)) {
|
|
13101
|
+
if (f.host === "windows") notes.push("scheduler support is not built for Windows yet \u2014 nothing to install; run `crew run` by hand");
|
|
13102
|
+
else actions.push({ kind: "install" });
|
|
13103
|
+
}
|
|
13104
|
+
return { actions, notes };
|
|
13105
|
+
}
|
|
13106
|
+
async function gatherHealth(c, check) {
|
|
13107
|
+
let problems;
|
|
13108
|
+
let readiness = [];
|
|
13109
|
+
try {
|
|
13110
|
+
const r = await check();
|
|
13111
|
+
if (Array.isArray(r)) problems = r;
|
|
13112
|
+
else ({ problems, readiness } = r);
|
|
13113
|
+
} catch (e) {
|
|
13114
|
+
problems = [`checks could not run: ${e instanceof Error ? e.message : String(e)}`];
|
|
13115
|
+
}
|
|
13116
|
+
return { route: c.route, enabled: c.enabled, hasRepos: c.hasRepos, problems, readiness };
|
|
13117
|
+
}
|
|
13118
|
+
function routesInScope(routes, named) {
|
|
13119
|
+
return named ? routes.filter((r) => r.route === named) : routes;
|
|
13120
|
+
}
|
|
13121
|
+
function planEnable(text, route2) {
|
|
13122
|
+
const doc = parseDocument2(text);
|
|
13123
|
+
routeMapOf(doc, route2).set("enabled", true);
|
|
13124
|
+
return String(doc);
|
|
13125
|
+
}
|
|
13126
|
+
function schedulerInstalled(plan, exists, crontab) {
|
|
13127
|
+
if (plan.mechanism === "cron") return plan.cronMarker !== null && crontab.includes(plan.cronMarker);
|
|
13128
|
+
return plan.unitPaths.length > 0 && plan.unitPaths.every(exists);
|
|
13129
|
+
}
|
|
13130
|
+
|
|
13131
|
+
// src/provision.ts
|
|
13132
|
+
var PROVISION_EXIT = { ok: 0, failed: 1, problem: 2, choice: 3 };
|
|
13133
|
+
function parseEvents(stdout) {
|
|
13134
|
+
const out = [];
|
|
13135
|
+
for (const line of stdout.split("\n")) {
|
|
13136
|
+
const t = line.trim();
|
|
13137
|
+
if (!t.startsWith("{")) continue;
|
|
13138
|
+
try {
|
|
13139
|
+
out.push(JSON.parse(t));
|
|
13140
|
+
} catch {
|
|
13141
|
+
}
|
|
13142
|
+
}
|
|
13143
|
+
return out;
|
|
13144
|
+
}
|
|
13145
|
+
async function runProvision(o, deps, write, human) {
|
|
13146
|
+
const summary = { ok: false, state: "failed", changed: false, routes: [], forges: [] };
|
|
13147
|
+
const emit = (event, message, data = {}) => {
|
|
13148
|
+
if (o.json) write(`${JSON.stringify({ ...data, event, message })}
|
|
13149
|
+
`);
|
|
13150
|
+
else human(`${message}
|
|
13151
|
+
`);
|
|
13152
|
+
};
|
|
13153
|
+
const fail = (code, message, data = {}) => {
|
|
13154
|
+
emit("problem", message, data);
|
|
13155
|
+
return { code, summary: { ...summary, ok: false, state: "failed" } };
|
|
13156
|
+
};
|
|
13157
|
+
if (!o.key) return fail(PROVISION_EXIT.problem, "no connect key: pass --key or set CREW_CONNECT_KEY", { stage: "input" });
|
|
13158
|
+
if (o.routes.length === 0) {
|
|
13159
|
+
return fail(PROVISION_EXIT.problem, "no routes to serve: pass --route WORKSPACE[/PROJECT] (repeatable) or set CREW_PROVISION_ROUTES", { stage: "input" });
|
|
13160
|
+
}
|
|
13161
|
+
for (const host of o.forgeHosts) {
|
|
13162
|
+
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" });
|
|
13163
|
+
}
|
|
13164
|
+
const shipName2 = o.shipName ?? deps.defaultShipName();
|
|
13165
|
+
summary.ship = shipName2;
|
|
13166
|
+
const routeNames = [];
|
|
13167
|
+
for (const route2 of o.routes) {
|
|
13168
|
+
const args = [
|
|
13169
|
+
"connect",
|
|
13170
|
+
route2,
|
|
13171
|
+
"--json",
|
|
13172
|
+
"--no-wizard",
|
|
13173
|
+
"--ship-name",
|
|
13174
|
+
shipName2,
|
|
13175
|
+
...o.agentBin ? ["--agent-bin", o.agentBin] : [],
|
|
13176
|
+
...o.extraPath ? ["--extra-path", o.extraPath] : [],
|
|
13177
|
+
...o.baseUrl ? ["--base-url", o.baseUrl] : [],
|
|
13178
|
+
...o.dryRun ? ["--dry-run"] : [],
|
|
13179
|
+
...o.choices.flatMap((c) => ["--choose", c])
|
|
13180
|
+
];
|
|
13181
|
+
const res = deps.crew(args, { env: { CREW_CONNECT_KEY: o.key } });
|
|
13182
|
+
const events = parseEvents(res.stdout);
|
|
13183
|
+
for (const e of events) if (o.json) write(`${JSON.stringify({ ...e, phase: "connect" })}
|
|
13184
|
+
`);
|
|
13185
|
+
if (res.status !== 0) {
|
|
13186
|
+
const problem = events.find((e) => e.event === "problem");
|
|
13187
|
+
const msg = String(problem?.message ?? (res.stderr.trim().split("\n").pop() || `crew connect ${route2} exited ${res.status}`));
|
|
13188
|
+
if (!o.json) human(`crew provision: connect ${route2}: ${msg}
|
|
13189
|
+
`);
|
|
13190
|
+
return { code: res.status === PROVISION_EXIT.choice ? PROVISION_EXIT.choice : res.status, summary: { ...summary, ok: false, state: "failed" } };
|
|
13191
|
+
}
|
|
13192
|
+
const done = events.find((e) => e.event === "done");
|
|
13193
|
+
const name = String(done?.route ?? route2);
|
|
13194
|
+
routeNames.push(name);
|
|
13195
|
+
const shipRow = events.find((e) => e.event === "ship_row");
|
|
13196
|
+
if (shipRow?.action === "created") summary.changed = true;
|
|
13197
|
+
if (done?.wroteConfig === true) summary.changed = true;
|
|
13198
|
+
emit("connect", `connected ${name}${shipRow ? ` (ship row ${String(shipRow.action)})` : ""}`, { route: name });
|
|
13199
|
+
}
|
|
13200
|
+
summary.routes = routeNames;
|
|
13201
|
+
if (o.dryRun) {
|
|
13202
|
+
summary.ok = true;
|
|
13203
|
+
summary.state = "dry-run";
|
|
13204
|
+
emit("done", "dry run: connected nothing past discovery; no forge, route, unit or daemon was touched", { summary });
|
|
13205
|
+
return { code: PROVISION_EXIT.ok, summary };
|
|
13206
|
+
}
|
|
13207
|
+
const configPath = deps.configFile();
|
|
13208
|
+
if (!configPath) return fail(PROVISION_EXIT.failed, "connect finished but no crew.yaml exists", { stage: "connect" });
|
|
13209
|
+
summary.configPath = configPath;
|
|
13210
|
+
for (const host of o.forgeHosts) {
|
|
13211
|
+
const token = o.tokens[host];
|
|
13212
|
+
const stored = deps.storedToken(host);
|
|
13213
|
+
if (stored === token) {
|
|
13214
|
+
summary.forges.push({ host, action: "unchanged" });
|
|
13215
|
+
emit("forge", `forge ${host}: token already stored`, { host, action: "unchanged" });
|
|
13216
|
+
continue;
|
|
13217
|
+
}
|
|
13218
|
+
const sub = stored === null ? "set" : "rotate";
|
|
13219
|
+
const res = deps.crew(["forge", sub, host, "--token-stdin"], { input: `${token}
|
|
13220
|
+
` });
|
|
13221
|
+
if (res.status !== 0) return fail(PROVISION_EXIT.failed, `forge ${host}: ${res.stderr.trim() || `crew forge ${sub} exited ${res.status}`}`, { stage: "forge", host });
|
|
13222
|
+
const action = sub === "set" ? "stored" : "rotated";
|
|
13223
|
+
summary.forges.push({ host, action });
|
|
13224
|
+
summary.changed = true;
|
|
13225
|
+
emit("forge", `forge ${host}: token ${action}`, { host, action });
|
|
13226
|
+
}
|
|
13227
|
+
for (const route2 of routeNames) {
|
|
13228
|
+
if (deps.enableRoute(route2)) {
|
|
13229
|
+
summary.changed = true;
|
|
13230
|
+
emit("route_enabled", `enabled route ${route2}`, { route: route2 });
|
|
13231
|
+
}
|
|
13232
|
+
}
|
|
13233
|
+
const before = deps.installState();
|
|
13234
|
+
if (before.unitsStale || !before.running || !before.lingering) {
|
|
13235
|
+
const res = deps.crew(["install"]);
|
|
13236
|
+
if (res.status !== 0) {
|
|
13237
|
+
return fail(PROVISION_EXIT.failed, `crew install exited ${res.status}: ${(res.stderr || res.stdout).trim().split("\n").pop() ?? ""}`, { stage: "install" });
|
|
13238
|
+
}
|
|
13239
|
+
summary.changed = true;
|
|
13240
|
+
emit("install", `installed ${before.mechanism} units`, { mechanism: before.mechanism });
|
|
13241
|
+
}
|
|
13242
|
+
const after = deps.installState();
|
|
13243
|
+
summary.daemon = { mechanism: after.mechanism, running: after.running, lingering: after.lingering };
|
|
13244
|
+
if (!after.running) {
|
|
13245
|
+
return fail(PROVISION_EXIT.failed, `the ${after.mechanism} daemon unit is not running after install`, { stage: "daemon" });
|
|
13246
|
+
}
|
|
13247
|
+
emit("daemon", `daemon running (${after.mechanism}${after.lingering ? ", survives logout" : ", will stop at logout: lingering is off"})`, { ...summary.daemon });
|
|
13248
|
+
summary.ok = true;
|
|
13249
|
+
summary.state = summary.changed ? "provisioned" : "already provisioned";
|
|
13250
|
+
emit("done", summary.state, { summary });
|
|
13251
|
+
return { code: PROVISION_EXIT.ok, summary };
|
|
13252
|
+
}
|
|
13253
|
+
function envKey(host) {
|
|
13254
|
+
return host.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
13255
|
+
}
|
|
13256
|
+
function tokensFromEnv(hosts, env) {
|
|
13257
|
+
const out = {};
|
|
13258
|
+
for (const h of hosts) {
|
|
13259
|
+
const t = env[`CREW_FORGE_TOKEN_${envKey(h)}`] ?? env.CREW_FORGE_TOKEN;
|
|
13260
|
+
if (t?.trim()) out[h] = t.trim();
|
|
13261
|
+
}
|
|
13262
|
+
return out;
|
|
13263
|
+
}
|
|
13264
|
+
function repeated(argv2, name) {
|
|
13265
|
+
const out = [];
|
|
13266
|
+
for (let i = 0; i < argv2.length; i++) {
|
|
13267
|
+
const a = argv2[i];
|
|
13268
|
+
if (a === `--${name}`) {
|
|
13269
|
+
const v = argv2[++i];
|
|
13270
|
+
if (v !== void 0) out.push(v);
|
|
13271
|
+
} else if (a.startsWith(`--${name}=`)) out.push(a.slice(name.length + 3));
|
|
13272
|
+
}
|
|
13273
|
+
return out;
|
|
13274
|
+
}
|
|
13275
|
+
function splitList(v) {
|
|
13276
|
+
return (v ?? "").split(/[\s,]+/).filter(Boolean);
|
|
13277
|
+
}
|
|
13278
|
+
function provisionOptionsFrom(argv2, env) {
|
|
13279
|
+
const one = (name) => repeated(argv2, name).at(-1);
|
|
13280
|
+
const routes = repeated(argv2, "route");
|
|
13281
|
+
const forgeFlags = repeated(argv2, "forge-host");
|
|
13282
|
+
const tokenAvailable = !!env.CREW_FORGE_TOKEN?.trim() || Object.keys(env).some((k) => k.startsWith("CREW_FORGE_TOKEN_") && env[k]?.trim());
|
|
13283
|
+
const forgeHosts = (forgeFlags.length ? forgeFlags : tokenAvailable ? splitList(env.CREW_FORGE_HOSTS ?? "github.com") : []).map((h) => h.toLowerCase());
|
|
13284
|
+
return {
|
|
13285
|
+
key: one("key") ?? env.CREW_CONNECT_KEY,
|
|
13286
|
+
routes: routes.length ? routes : splitList(env.CREW_PROVISION_ROUTES),
|
|
13287
|
+
shipName: one("ship-name") ?? env.CREW_SHIP_NAME,
|
|
13288
|
+
agentBin: one("agent-bin"),
|
|
13289
|
+
extraPath: one("extra-path"),
|
|
13290
|
+
baseUrl: one("base-url"),
|
|
13291
|
+
forgeHosts,
|
|
13292
|
+
tokens: tokensFromEnv(forgeHosts, env),
|
|
13293
|
+
json: argv2.includes("--json"),
|
|
13294
|
+
dryRun: argv2.includes("--dry-run"),
|
|
13295
|
+
choices: repeated(argv2, "choose")
|
|
13296
|
+
};
|
|
13297
|
+
}
|
|
13298
|
+
function realProvisionDeps(crewHome) {
|
|
13299
|
+
const spawnCrew = (args, o = {}) => {
|
|
13300
|
+
const [bin, ...pre] = crewInvocation(crewHome);
|
|
13301
|
+
const env = { ...process.env, ...o.env };
|
|
13302
|
+
delete env.CREW_FORGE_TOKEN;
|
|
13303
|
+
const res = spawnSync6(bin, [...pre, ...args], { cwd: crewHome, encoding: "utf8", env, input: o.input, maxBuffer: 64 * 1024 * 1024 });
|
|
13304
|
+
return { status: res.status ?? 1, stdout: res.stdout ?? "", stderr: `${res.stderr ?? ""}${res.error ? String(res.error) : ""}` };
|
|
13305
|
+
};
|
|
13306
|
+
const configFile = () => {
|
|
13307
|
+
const f = findConfigFile(crewHome);
|
|
13308
|
+
return existsSync25(f) ? f : null;
|
|
13309
|
+
};
|
|
13310
|
+
return {
|
|
13311
|
+
crew: spawnCrew,
|
|
13312
|
+
configFile,
|
|
13313
|
+
enableRoute(route2) {
|
|
13314
|
+
const file = configFile();
|
|
13315
|
+
if (!file) return false;
|
|
13316
|
+
const text = readFileSync23(file, "utf8");
|
|
13317
|
+
const doc = parseDocument3(text);
|
|
13318
|
+
const routes = doc.get("routes");
|
|
13319
|
+
const entry = routes?.items?.find((r) => r.get("route") === route2);
|
|
13320
|
+
if (!entry) throw new Error(`route ${route2} is not in ${file}`);
|
|
13321
|
+
if (entry.get("enabled") === true) return false;
|
|
13322
|
+
writeFileSync19(file, planEnable(text, route2));
|
|
13323
|
+
return true;
|
|
13324
|
+
},
|
|
13325
|
+
installState() {
|
|
13326
|
+
const file = configFile();
|
|
13327
|
+
const cfg = loadConfig(crewHome, file ?? void 0, { allowNoRoutes: true });
|
|
13328
|
+
const host = hostPlatform();
|
|
13329
|
+
const ctx = installContext();
|
|
13330
|
+
const systemd = detectSystemd();
|
|
13331
|
+
const jobs = ["run", "release", "passengers"];
|
|
13332
|
+
let unitsStale = false;
|
|
13333
|
+
let mechanism = "none";
|
|
13334
|
+
for (const job of jobs) {
|
|
13335
|
+
const plan = planInstall(cfg.ship, crewHome, host, systemd, job, ctx);
|
|
13336
|
+
mechanism = plan.mechanism;
|
|
13337
|
+
if (plan.mechanism === "cron") {
|
|
13338
|
+
unitsStale = true;
|
|
13339
|
+
continue;
|
|
13340
|
+
}
|
|
13341
|
+
for (const p of plan.unitPaths) {
|
|
13342
|
+
if (!existsSync25(p) || readFileSync23(p, "utf8") !== plan.unitContent[p]) unitsStale = true;
|
|
13343
|
+
}
|
|
13344
|
+
}
|
|
13345
|
+
const label = labelFor(crewHome, "run");
|
|
13346
|
+
let running = false;
|
|
13347
|
+
let lingering = true;
|
|
13348
|
+
if (mechanism === "systemd") {
|
|
13349
|
+
const system = ctx.root || existsSync25(systemUnitPathFor(label));
|
|
13350
|
+
const ctl = system ? ["systemctl"] : ["systemctl", "--user"];
|
|
13351
|
+
running = spawnSync6(ctl[0], [...ctl.slice(1), "is-active", "--quiet", `${label}.service`]).status === 0;
|
|
13352
|
+
if (!system) {
|
|
13353
|
+
const out = spawnSync6("loginctl", ["show-user", ctx.user, "-p", "Linger"], { encoding: "utf8" });
|
|
13354
|
+
lingering = /Linger=yes/.test(out.stdout ?? "");
|
|
13355
|
+
}
|
|
13356
|
+
} else if (mechanism === "cron") {
|
|
13357
|
+
running = true;
|
|
13358
|
+
} else if (mechanism === "launchd") {
|
|
13359
|
+
running = spawnSync6("launchctl", ["list", label]).status === 0;
|
|
13360
|
+
}
|
|
13361
|
+
return { mechanism, unitsStale, running, lingering };
|
|
13362
|
+
},
|
|
13363
|
+
storedToken(host) {
|
|
13364
|
+
const res = spawnCrew(["credential", "get"], { input: `protocol=https
|
|
13365
|
+
host=${host}
|
|
13366
|
+
|
|
13367
|
+
` });
|
|
13368
|
+
const m = /^password=(.*)$/m.exec(res.stdout);
|
|
13369
|
+
return m ? m[1] : null;
|
|
13370
|
+
},
|
|
13371
|
+
defaultShipName: () => hostname2()
|
|
13372
|
+
};
|
|
13373
|
+
}
|
|
13374
|
+
|
|
13375
|
+
// src/release-run.ts
|
|
13376
|
+
import { readFileSync as readFileSync24, writeFileSync as writeFileSync20, existsSync as existsSync26, copyFileSync, mkdirSync as mkdirSync17, mkdtempSync as mkdtempSync2, rmSync as rmSync14 } from "fs";
|
|
13377
|
+
import { dirname as dirname14, join as join30 } from "path";
|
|
13378
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
13379
|
+
|
|
13380
|
+
// src/release.ts
|
|
13381
|
+
function describeUnplaceable(unplaceable, verifiedStatus) {
|
|
13382
|
+
const stranded = unplaceable.filter((u) => u.ticket.status === verifiedStatus);
|
|
13383
|
+
if (stranded.length === 0) return [];
|
|
13384
|
+
const notes = [];
|
|
13385
|
+
const keysFor = (r) => stranded.filter((u) => u.reason === r).map((u) => u.ticket.issue_id);
|
|
13386
|
+
const noRepo = keysFor("no-repo");
|
|
13387
|
+
if (noRepo.length) {
|
|
13388
|
+
notes.push({
|
|
13389
|
+
level: "warn",
|
|
13390
|
+
message: `${noRepo.length} verified ticket(s) name no repository and cannot be released: ` + noRepo.join(", ")
|
|
13391
|
+
});
|
|
13392
|
+
}
|
|
13393
|
+
const unknown = keysFor("unknown-repo");
|
|
13394
|
+
if (unknown.length) {
|
|
13395
|
+
notes.push({
|
|
13396
|
+
level: "warn",
|
|
13397
|
+
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(", ")}`
|
|
13398
|
+
});
|
|
13399
|
+
}
|
|
13400
|
+
const elsewhere = stranded.filter((u) => u.reason === "not-served-here");
|
|
13401
|
+
if (elsewhere.length) {
|
|
13402
|
+
const byRepoName = /* @__PURE__ */ new Map();
|
|
13403
|
+
for (const u of elsewhere) {
|
|
13404
|
+
const name = u.repo ?? "?";
|
|
13405
|
+
byRepoName.set(name, [...byRepoName.get(name) ?? [], u.ticket.issue_id]);
|
|
13406
|
+
}
|
|
13407
|
+
notes.push({
|
|
13408
|
+
level: "info",
|
|
13409
|
+
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("; ")
|
|
13410
|
+
});
|
|
13411
|
+
}
|
|
13412
|
+
return notes;
|
|
13413
|
+
}
|
|
13414
|
+
var unplaceableReported = /* @__PURE__ */ new Set();
|
|
13415
|
+
function reportUnplaceable(routeName, unplaceable, verified, emit) {
|
|
13416
|
+
if (unplaceableReported.has(routeName)) return;
|
|
13417
|
+
unplaceableReported.add(routeName);
|
|
13418
|
+
for (const note of describeUnplaceable(unplaceable, verified)) {
|
|
13419
|
+
if (note.level === "warn") emit.warn(note.message, { step: "release" });
|
|
13420
|
+
else emit.emit(note.message, { step: "release" });
|
|
13421
|
+
}
|
|
13422
|
+
}
|
|
13423
|
+
function checkGuards(cwd, base = "main") {
|
|
13424
|
+
const branch = currentBranch(cwd);
|
|
13425
|
+
if (branch !== base) {
|
|
13426
|
+
return { kind: "branch", detail: `primary checkout is on '${branch}', not ${base}` };
|
|
13427
|
+
}
|
|
13428
|
+
const changes = status(cwd);
|
|
13429
|
+
if (changes.length === 0) return null;
|
|
13430
|
+
const untracked = changes.filter((c) => c.untracked).length;
|
|
13431
|
+
const names = changes.slice(0, 3).map((c) => c.path).join(", ");
|
|
13432
|
+
const more = changes.length > 3 ? `, +${changes.length - 3} more` : "";
|
|
13433
|
+
const untrackedNote = untracked > 0 ? ` (${untracked} untracked)` : "";
|
|
13434
|
+
return {
|
|
13435
|
+
kind: "dirty",
|
|
13436
|
+
detail: `working tree has ${changes.length} change(s)${untrackedNote}: ${names}${more}`
|
|
13437
|
+
};
|
|
13438
|
+
}
|
|
13439
|
+
function heldByBlockers(tickets, verifiedStatus, info) {
|
|
13440
|
+
if (!info) return [];
|
|
13441
|
+
return tickets.filter((t) => t.status === verifiedStatus && unresolvedBlockers(t, info, { mergeGated: false }).length > 0).map((ticket) => ({ ticket, blockers: describeUnresolved(ticket, info, { mergeGated: false }) }));
|
|
13442
|
+
}
|
|
13443
|
+
var describeHeld = (held) => held.map((h) => `${h.ticket.issue_tag ?? h.ticket.issue_id} held: blocked by ${h.blockers}`);
|
|
13444
|
+
var CHANGELOG_LINE = /^Changelog:[ \t]*(.*)$/gm;
|
|
13445
|
+
var TICKET_REF_AT_START = /^([A-Z][A-Z0-9]*)-(\d+)\b/;
|
|
13446
|
+
function dedupeChangelogEntries(entries, ticket) {
|
|
13447
|
+
const own = (ticket.issue_tag ?? ticket.issue_id).match(/^([A-Z][A-Z0-9]*)-(\d+)$/);
|
|
13448
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13449
|
+
const out = [];
|
|
13450
|
+
for (const raw of entries) {
|
|
13451
|
+
let line = raw.trim();
|
|
13452
|
+
const ref = line.match(TICKET_REF_AT_START);
|
|
13453
|
+
let key = line;
|
|
13454
|
+
if (ref) {
|
|
13455
|
+
const prefix = ref[1];
|
|
13456
|
+
const num = ref[2];
|
|
13457
|
+
const canonical = own && own[2] === num ? own[1] : prefix;
|
|
13458
|
+
line = `${canonical}-${num}${line.slice(ref[0].length)}`;
|
|
13459
|
+
key = `${canonical}-${num}`;
|
|
13460
|
+
}
|
|
13461
|
+
if (seen.has(key)) continue;
|
|
13462
|
+
seen.add(key);
|
|
13463
|
+
out.push(line);
|
|
13464
|
+
}
|
|
13465
|
+
return out;
|
|
13466
|
+
}
|
|
13467
|
+
var BUMP_LINE = /^Bump:[ \t]*(major|minor|patch)\b/gim;
|
|
13468
|
+
function planMerge(cwd, tickets, contract, lastReleased, branchFor = (t) => branchForIssue(cwd, t.issue_id), base = "main", blockerInfo, ref = "HEAD") {
|
|
13469
|
+
const held = new Set(heldByBlockers(tickets, contract.statuses.verified, blockerInfo).map((h) => h.ticket.id));
|
|
13470
|
+
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));
|
|
13471
|
+
return verified.map((ticket) => {
|
|
13472
|
+
const branch = branchFor(ticket);
|
|
13473
|
+
if (!branch) {
|
|
13474
|
+
const keys = referenceKeys(ticket);
|
|
13475
|
+
const unreleased = lastReleased ? findKeyInRange(cwd, keys, `${lastReleased}..${ref}`) : null;
|
|
13476
|
+
if (unreleased) {
|
|
13477
|
+
return {
|
|
13478
|
+
ticket,
|
|
13479
|
+
branch: null,
|
|
13480
|
+
entries: [],
|
|
13481
|
+
usedFallback: false,
|
|
13482
|
+
bump: "patch",
|
|
13483
|
+
majorRequested: false,
|
|
13484
|
+
skipReason: "already-merged"
|
|
13485
|
+
};
|
|
13486
|
+
}
|
|
13487
|
+
const anywhere = findKeyInRange(cwd, keys, ref);
|
|
13488
|
+
if (anywhere) {
|
|
13489
|
+
return {
|
|
13490
|
+
ticket,
|
|
13491
|
+
branch: null,
|
|
13492
|
+
entries: [],
|
|
13493
|
+
usedFallback: false,
|
|
13494
|
+
bump: "patch",
|
|
13495
|
+
majorRequested: false,
|
|
13496
|
+
skipReason: lastReleased ? "already-released" : "already-merged",
|
|
13497
|
+
mergedSha: anywhere
|
|
13498
|
+
};
|
|
13499
|
+
}
|
|
13500
|
+
return {
|
|
13501
|
+
ticket,
|
|
13502
|
+
branch: null,
|
|
12719
13503
|
entries: [],
|
|
12720
13504
|
usedFallback: false,
|
|
12721
13505
|
bump: "patch",
|
|
@@ -13331,13 +14115,13 @@ async function bump(o, size) {
|
|
|
13331
14115
|
}
|
|
13332
14116
|
return printed;
|
|
13333
14117
|
}
|
|
13334
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
14118
|
+
const files = o.repo.release.versionFiles.map((f) => join30(o.cwd, f));
|
|
13335
14119
|
const first = files[0];
|
|
13336
|
-
if (!
|
|
14120
|
+
if (!existsSync26(first)) {
|
|
13337
14121
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
13338
14122
|
return null;
|
|
13339
14123
|
}
|
|
13340
|
-
const current = JSON.parse(
|
|
14124
|
+
const current = JSON.parse(readFileSync24(first, "utf8")).version;
|
|
13341
14125
|
if (!current) {
|
|
13342
14126
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
13343
14127
|
return null;
|
|
@@ -13350,27 +14134,27 @@ async function bump(o, size) {
|
|
|
13350
14134
|
return version;
|
|
13351
14135
|
}
|
|
13352
14136
|
for (const f of files) {
|
|
13353
|
-
const src =
|
|
14137
|
+
const src = readFileSync24(f, "utf8");
|
|
13354
14138
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
13355
14139
|
if (out === src) {
|
|
13356
14140
|
o.emit.error(`no version field in ${f}`);
|
|
13357
14141
|
return null;
|
|
13358
14142
|
}
|
|
13359
|
-
|
|
14143
|
+
writeFileSync20(f, out);
|
|
13360
14144
|
}
|
|
13361
14145
|
return version;
|
|
13362
14146
|
}
|
|
13363
14147
|
function writeChangelog(o, version, entries) {
|
|
13364
14148
|
const file = o.repo.release.changelog;
|
|
13365
14149
|
if (!file || entries.length === 0) return;
|
|
13366
|
-
const path =
|
|
14150
|
+
const path = join30(o.cwd, file);
|
|
13367
14151
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
13368
14152
|
if (o.dryRun) {
|
|
13369
14153
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
13370
14154
|
return;
|
|
13371
14155
|
}
|
|
13372
|
-
const existing =
|
|
13373
|
-
|
|
14156
|
+
const existing = existsSync26(path) ? readFileSync24(path, "utf8") : "# Changelog\n";
|
|
14157
|
+
writeFileSync20(path, insertChangelogSection(existing, section));
|
|
13374
14158
|
}
|
|
13375
14159
|
async function confirm(o, expected) {
|
|
13376
14160
|
if (!o.repo.hooks.released) return void 0;
|
|
@@ -13463,8 +14247,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
13463
14247
|
function openReleaseCheckout(o) {
|
|
13464
14248
|
const { remote, base } = o.repo.branch;
|
|
13465
14249
|
const branch = `crew/release-${Date.now()}`;
|
|
13466
|
-
const root = mkdtempSync2(
|
|
13467
|
-
const dir =
|
|
14250
|
+
const root = mkdtempSync2(join30(tmpdir3(), "crew-release-"));
|
|
14251
|
+
const dir = join30(root, "checkout");
|
|
13468
14252
|
o.emit.emit(`release checkout: ${dir}`, { step: "release" });
|
|
13469
14253
|
const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
|
|
13470
14254
|
try {
|
|
@@ -13474,10 +14258,10 @@ function openReleaseCheckout(o) {
|
|
|
13474
14258
|
throw e;
|
|
13475
14259
|
}
|
|
13476
14260
|
for (const f of o.repo.worktrees.copy) {
|
|
13477
|
-
const src =
|
|
13478
|
-
if (!
|
|
13479
|
-
mkdirSync17(
|
|
13480
|
-
copyFileSync(src,
|
|
14261
|
+
const src = join30(o.cwd, f);
|
|
14262
|
+
if (!existsSync26(src)) continue;
|
|
14263
|
+
mkdirSync17(dirname14(join30(dir, f)), { recursive: true });
|
|
14264
|
+
copyFileSync(src, join30(dir, f));
|
|
13481
14265
|
}
|
|
13482
14266
|
return { dir, branch, close };
|
|
13483
14267
|
}
|
|
@@ -14178,12 +14962,12 @@ Passed: ${passed}.${willClose ? " Closing this alert." : ""}`,
|
|
|
14178
14962
|
}
|
|
14179
14963
|
|
|
14180
14964
|
// src/live-runs.ts
|
|
14181
|
-
import { existsSync as
|
|
14182
|
-
import { join as
|
|
14965
|
+
import { existsSync as existsSync28, readdirSync as readdirSync11 } from "fs";
|
|
14966
|
+
import { join as join32 } from "path";
|
|
14183
14967
|
|
|
14184
14968
|
// src/interrupted.ts
|
|
14185
|
-
import { existsSync as
|
|
14186
|
-
import { join as
|
|
14969
|
+
import { existsSync as existsSync27, readdirSync as readdirSync10, readFileSync as readFileSync25 } from "fs";
|
|
14970
|
+
import { join as join31 } from "path";
|
|
14187
14971
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
14188
14972
|
function interruptedComment(b) {
|
|
14189
14973
|
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.";
|
|
@@ -14202,21 +14986,21 @@ var pidAlive = (pid) => {
|
|
|
14202
14986
|
};
|
|
14203
14987
|
function runFinished(streamPath) {
|
|
14204
14988
|
const path = `${streamPath}.meta.json`;
|
|
14205
|
-
if (!
|
|
14989
|
+
if (!existsSync27(path)) return false;
|
|
14206
14990
|
try {
|
|
14207
|
-
return JSON.parse(
|
|
14991
|
+
return JSON.parse(readFileSync25(path, "utf8")).running !== true;
|
|
14208
14992
|
} catch {
|
|
14209
14993
|
return true;
|
|
14210
14994
|
}
|
|
14211
14995
|
}
|
|
14212
14996
|
function orphanedStreams(o) {
|
|
14213
|
-
const dir =
|
|
14214
|
-
if (!
|
|
14997
|
+
const dir = join31(o.stateDir, "streams");
|
|
14998
|
+
if (!existsSync27(dir)) return [];
|
|
14215
14999
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
14216
15000
|
const out = [];
|
|
14217
15001
|
for (const name of readdirSync10(dir)) {
|
|
14218
15002
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
14219
|
-
const streamPath =
|
|
15003
|
+
const streamPath = join31(dir, name);
|
|
14220
15004
|
if (runFinished(streamPath)) continue;
|
|
14221
15005
|
const sidecar = readRunSidecar(streamPath);
|
|
14222
15006
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
@@ -14270,12 +15054,12 @@ async function reconcileRuns(o) {
|
|
|
14270
15054
|
|
|
14271
15055
|
// src/live-runs.ts
|
|
14272
15056
|
function liveRuns(stateDir, isAlive = runIsAlive) {
|
|
14273
|
-
const dir =
|
|
14274
|
-
if (!
|
|
15057
|
+
const dir = join32(stateDir, "streams");
|
|
15058
|
+
if (!existsSync28(dir)) return [];
|
|
14275
15059
|
const out = [];
|
|
14276
15060
|
for (const name of readdirSync11(dir)) {
|
|
14277
15061
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
14278
|
-
const streamPath =
|
|
15062
|
+
const streamPath = join32(dir, name);
|
|
14279
15063
|
if (runFinished(streamPath)) continue;
|
|
14280
15064
|
const sidecar = readRunSidecar(streamPath);
|
|
14281
15065
|
if (!sidecar || !isAlive(sidecar)) continue;
|
|
@@ -14331,7 +15115,8 @@ function describeRun(run2, route2, seatIds, worktreeOf = (d, n) => {
|
|
|
14331
15115
|
worktree: repoDir && num ? worktreeOf(repoDir, num) : null,
|
|
14332
15116
|
stream: run2.streamPath,
|
|
14333
15117
|
pid: run2.pid,
|
|
14334
|
-
startedAt: run2.spawnedAt
|
|
15118
|
+
startedAt: run2.spawnedAt,
|
|
15119
|
+
deadline: run2.deadline ?? null
|
|
14335
15120
|
};
|
|
14336
15121
|
}
|
|
14337
15122
|
|
|
@@ -14375,7 +15160,7 @@ function formatProjectsReport(report) {
|
|
|
14375
15160
|
}
|
|
14376
15161
|
|
|
14377
15162
|
// src/forge-cmd.ts
|
|
14378
|
-
import { parseDocument } from "yaml";
|
|
15163
|
+
import { parseDocument as parseDocument4 } from "yaml";
|
|
14379
15164
|
function runCredential(action, stdin, forges, stateDir, env = process.env) {
|
|
14380
15165
|
try {
|
|
14381
15166
|
return { code: 0, out: answerCredential(action ?? "", parseCredentialInput(stdin), forges, stateDir, env), err: "" };
|
|
@@ -14416,7 +15201,7 @@ function runForge(a, io) {
|
|
|
14416
15201
|
if (!existing) {
|
|
14417
15202
|
const tok = sourceFromArgs(a);
|
|
14418
15203
|
if (typeof tok === "string") return fail(tok);
|
|
14419
|
-
const doc =
|
|
15204
|
+
const doc = parseDocument4(io.readFile(io.configFile));
|
|
14420
15205
|
doc.setIn(["ship", "forges", host, "token"], tok);
|
|
14421
15206
|
io.writeFile(io.configFile, doc.toString());
|
|
14422
15207
|
io.forges = { ...io.forges ?? {}, [host]: { username: "x-access-token", token: tok } };
|
|
@@ -14609,7 +15394,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
14609
15394
|
}
|
|
14610
15395
|
|
|
14611
15396
|
// src/watch.ts
|
|
14612
|
-
import { watch as watchFile, existsSync as
|
|
15397
|
+
import { watch as watchFile, existsSync as existsSync29, statSync as statSync9, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
|
|
14613
15398
|
var E = "\x1B[";
|
|
14614
15399
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
14615
15400
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -14710,7 +15495,7 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
14710
15495
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
14711
15496
|
}
|
|
14712
15497
|
function readFrom(file, from) {
|
|
14713
|
-
if (!
|
|
15498
|
+
if (!existsSync29(file)) return { events: [], offset: from };
|
|
14714
15499
|
const size = statSync9(file).size;
|
|
14715
15500
|
if (size < from) return readFrom(file, 0);
|
|
14716
15501
|
if (size === from) return { events: [], offset: from };
|
|
@@ -14953,17 +15738,17 @@ function nextRoles(decision, ran) {
|
|
|
14953
15738
|
}
|
|
14954
15739
|
|
|
14955
15740
|
// src/ship-name.ts
|
|
14956
|
-
import { existsSync as
|
|
14957
|
-
import { dirname as
|
|
14958
|
-
import { parseDocument as
|
|
15741
|
+
import { existsSync as existsSync31, mkdirSync as mkdirSync19, readFileSync as readFileSync27, renameSync as renameSync3, writeFileSync as writeFileSync22 } from "fs";
|
|
15742
|
+
import { dirname as dirname16 } from "path";
|
|
15743
|
+
import { parseDocument as parseDocument5 } from "yaml";
|
|
14959
15744
|
|
|
14960
15745
|
// src/machine-id.ts
|
|
14961
15746
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
14962
15747
|
import { createHash as createHash5 } from "crypto";
|
|
14963
|
-
import { existsSync as
|
|
14964
|
-
import { dirname as
|
|
15748
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync18, readFileSync as readFileSync26, writeFileSync as writeFileSync21 } from "fs";
|
|
15749
|
+
import { dirname as dirname15, join as join33 } from "path";
|
|
14965
15750
|
function machineIdPathFor(stateDir) {
|
|
14966
|
-
return
|
|
15751
|
+
return join33(stateDir, "machine-id");
|
|
14967
15752
|
}
|
|
14968
15753
|
var sha256 = (s) => createHash5("sha256").update(s).digest("hex");
|
|
14969
15754
|
function readRawMachineId(platform2 = process.platform) {
|
|
@@ -14976,8 +15761,8 @@ function readRawMachineId(platform2 = process.platform) {
|
|
|
14976
15761
|
return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
|
|
14977
15762
|
}
|
|
14978
15763
|
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
14979
|
-
if (
|
|
14980
|
-
const id =
|
|
15764
|
+
if (existsSync30(p)) {
|
|
15765
|
+
const id = readFileSync26(p, "utf8").trim();
|
|
14981
15766
|
if (id) return id;
|
|
14982
15767
|
}
|
|
14983
15768
|
}
|
|
@@ -14988,15 +15773,15 @@ function readRawMachineId(platform2 = process.platform) {
|
|
|
14988
15773
|
function ensureMachineId(stateDir, opts = {}) {
|
|
14989
15774
|
if (opts.override?.trim()) return sha256(opts.override.trim());
|
|
14990
15775
|
const path = machineIdPathFor(stateDir);
|
|
14991
|
-
if (
|
|
14992
|
-
const cached =
|
|
15776
|
+
if (existsSync30(path)) {
|
|
15777
|
+
const cached = readFileSync26(path, "utf8").trim();
|
|
14993
15778
|
if (/^[0-9a-f]{64}$/.test(cached)) return cached;
|
|
14994
15779
|
}
|
|
14995
15780
|
const raw = (opts.read ?? readRawMachineId)();
|
|
14996
15781
|
if (!raw) return void 0;
|
|
14997
15782
|
const hashed = sha256(raw);
|
|
14998
|
-
mkdirSync18(
|
|
14999
|
-
|
|
15783
|
+
mkdirSync18(dirname15(path), { recursive: true });
|
|
15784
|
+
writeFileSync21(path, `${hashed}
|
|
15000
15785
|
`, { mode: 384 });
|
|
15001
15786
|
return hashed;
|
|
15002
15787
|
}
|
|
@@ -15025,11 +15810,11 @@ function planNameSync(local, row, lastSynced) {
|
|
|
15025
15810
|
return { drift: "local", action: "push" };
|
|
15026
15811
|
}
|
|
15027
15812
|
function shipNameOfYaml(text) {
|
|
15028
|
-
const v =
|
|
15813
|
+
const v = parseDocument5(text).getIn(["ship", "name"]);
|
|
15029
15814
|
return typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
15030
15815
|
}
|
|
15031
15816
|
function setShipNameInYaml(text, name) {
|
|
15032
|
-
const doc =
|
|
15817
|
+
const doc = parseDocument5(text);
|
|
15033
15818
|
doc.setIn(["ship", "name"], name);
|
|
15034
15819
|
return doc.toString();
|
|
15035
15820
|
}
|
|
@@ -15067,34 +15852,34 @@ async function syncRouteName(i) {
|
|
|
15067
15852
|
return { status: "ok", ...base, action, name: i.local };
|
|
15068
15853
|
}
|
|
15069
15854
|
function readSyncedName(resolvedFile) {
|
|
15070
|
-
if (!
|
|
15855
|
+
if (!existsSync31(resolvedFile)) return void 0;
|
|
15071
15856
|
try {
|
|
15072
|
-
const v = JSON.parse(
|
|
15857
|
+
const v = JSON.parse(readFileSync27(resolvedFile, "utf8")).shipName;
|
|
15073
15858
|
return typeof v === "string" ? v : void 0;
|
|
15074
15859
|
} catch {
|
|
15075
15860
|
return void 0;
|
|
15076
15861
|
}
|
|
15077
15862
|
}
|
|
15078
15863
|
function writeSyncedName(resolvedFile, name) {
|
|
15079
|
-
if (!
|
|
15864
|
+
if (!existsSync31(resolvedFile)) return;
|
|
15080
15865
|
let raw;
|
|
15081
15866
|
try {
|
|
15082
|
-
raw = JSON.parse(
|
|
15867
|
+
raw = JSON.parse(readFileSync27(resolvedFile, "utf8"));
|
|
15083
15868
|
} catch {
|
|
15084
15869
|
return;
|
|
15085
15870
|
}
|
|
15086
15871
|
if (raw.shipName === name) return;
|
|
15087
15872
|
raw.shipName = name;
|
|
15088
|
-
mkdirSync19(
|
|
15873
|
+
mkdirSync19(dirname16(resolvedFile), { recursive: true });
|
|
15089
15874
|
const tmp = `${resolvedFile}.tmp${process.pid}`;
|
|
15090
|
-
|
|
15875
|
+
writeFileSync22(tmp, `${JSON.stringify(raw, null, 2)}
|
|
15091
15876
|
`);
|
|
15092
15877
|
renameSync3(tmp, resolvedFile);
|
|
15093
15878
|
}
|
|
15094
15879
|
async function reconcileShipName(i) {
|
|
15095
15880
|
let local = i.ship.name;
|
|
15096
15881
|
try {
|
|
15097
|
-
local = shipNameOfYaml(
|
|
15882
|
+
local = shipNameOfYaml(readFileSync27(i.configFile, "utf8")) ?? local;
|
|
15098
15883
|
} catch {
|
|
15099
15884
|
}
|
|
15100
15885
|
const resolvedFile = resolvedPathFor(i.stateDir, i.route);
|
|
@@ -15118,8 +15903,8 @@ async function reconcileShipName(i) {
|
|
|
15118
15903
|
}
|
|
15119
15904
|
if (result.status !== "ok") return result;
|
|
15120
15905
|
if (result.action === "adopt" || result.action === "row-wins") {
|
|
15121
|
-
const text =
|
|
15122
|
-
|
|
15906
|
+
const text = readFileSync27(i.configFile, "utf8");
|
|
15907
|
+
writeFileSync22(i.configFile, setShipNameInYaml(text, result.name));
|
|
15123
15908
|
i.ship.name = result.name;
|
|
15124
15909
|
if (result.action === "row-wins") {
|
|
15125
15910
|
i.emit?.warn(
|
|
@@ -15505,7 +16290,10 @@ async function discover(o) {
|
|
|
15505
16290
|
...hostPeriscopes !== void 0 ? { host_periscopes: hostPeriscopes } : {},
|
|
15506
16291
|
...sshPublicKey !== void 0 ? { ssh_public_key: sshPublicKey } : {},
|
|
15507
16292
|
...ownerId !== void 0 ? { owner_id: ownerId } : {},
|
|
15508
|
-
...writeMachineId ? { machine_id: machineId } : {}
|
|
16293
|
+
...writeMachineId ? { machine_id: machineId } : {},
|
|
16294
|
+
// ISSUE-2791: a ship connecting by hand is a developer machine; the
|
|
16295
|
+
// column arrives with a template update, so only written when present.
|
|
16296
|
+
...shipColumns.has("kind") ? { kind: "developer_machine" } : {}
|
|
15509
16297
|
});
|
|
15510
16298
|
shipRowId = created.id;
|
|
15511
16299
|
out.shipRow = { id: shipRowId, name: o.ship.name, action: "created", matchedBy };
|
|
@@ -15640,659 +16428,372 @@ async function discover(o) {
|
|
|
15640
16428
|
const mine = people.find(isMine);
|
|
15641
16429
|
const byEmail = out.meEmail ? candidates2.filter((r) => emailOf(r).some((e) => eq(e, out.meEmail))) : [];
|
|
15642
16430
|
const unlinked = people.filter((r) => !r.user_id);
|
|
15643
|
-
const only = candidates2.length === 1 && unlinked.length === 1 ? candidates2[0] : void 0;
|
|
15644
|
-
const pick = mine ?? (byEmail.length === 1 ? byEmail[0] : only);
|
|
15645
|
-
if (pick) {
|
|
15646
|
-
out.operator = pick.id;
|
|
15647
|
-
out.operatorAction = isMine(pick) ? "matched" : "linked";
|
|
15648
|
-
if (!isMine(pick) && o.ship) await linkOperatorRow(o, out, pick);
|
|
15649
|
-
} else {
|
|
15650
|
-
out.operatorCompared = `signed in as ${out.meEmail ?? "unknown email"} (identity ${out.meIdentityId ?? out.meId}); ` + (people.length === 0 ? "no person rows in the Crew table" : people.map((r) => {
|
|
15651
|
-
const m = r.user_id ? members.get(r.user_id) : void 0;
|
|
15652
|
-
return `"${r.name || "unnamed row"}" <${r.email ?? "no email"}>` + (r.user_id ? ` linked to member ${m?.email ?? r.user_id}${m?.identityId ? ` (identity ${m.identityId})` : ""}` : " unlinked");
|
|
15653
|
-
}).join("; "));
|
|
15654
|
-
if (o.ship && (!o.interactive || candidates2.length === 0)) await createOperatorRow(o, out);
|
|
15655
|
-
}
|
|
15656
|
-
}
|
|
15657
|
-
}
|
|
15658
|
-
return out;
|
|
15659
|
-
}
|
|
15660
|
-
async function linkOperatorRow(o, found, row) {
|
|
15661
|
-
const name = row.name || row.id;
|
|
15662
|
-
if (!found.meId || !found.crewModelId) return;
|
|
15663
|
-
if (o.dryRun) {
|
|
15664
|
-
found.provisioning.push(`would link Crew row "${name}" to your account`);
|
|
15665
|
-
return;
|
|
15666
|
-
}
|
|
15667
|
-
const body = { user_id: found.meId };
|
|
15668
|
-
if (!row.email && found.meEmail) body.email = found.meEmail;
|
|
15669
|
-
try {
|
|
15670
|
-
await patch(o, `/data-models/${found.crewModelId}/records/${row.id}`, body);
|
|
15671
|
-
found.provisioning.push(`linked Crew row "${name}" to your account`);
|
|
15672
|
-
} catch (e) {
|
|
15673
|
-
if (!(e instanceof ConnectHttpError) || e.status !== 403) throw e;
|
|
15674
|
-
found.provisioning.push(
|
|
15675
|
-
`could not link your Crew row (no write access) \u2014 ask a workspace admin to set user_id on "${name}"`
|
|
15676
|
-
);
|
|
15677
|
-
}
|
|
15678
|
-
}
|
|
15679
|
-
async function createOperatorRow(o, found) {
|
|
15680
|
-
if (!found.meId || !found.crewModelId) return;
|
|
15681
|
-
const name = found.meName || found.meEmail || "Operator";
|
|
15682
|
-
if (o.dryRun) {
|
|
15683
|
-
found.provisioning.push(`would create Crew row "${name}" for you`);
|
|
15684
|
-
found.operatorAction = "would-create";
|
|
15685
|
-
return;
|
|
15686
|
-
}
|
|
15687
|
-
try {
|
|
15688
|
-
const created = await post2(o, `/data-models/${found.crewModelId}/records`, {
|
|
15689
|
-
name,
|
|
15690
|
-
...found.meEmail ? { email: found.meEmail } : {},
|
|
15691
|
-
user_id: found.meId
|
|
15692
|
-
});
|
|
15693
|
-
found.operator = created.id;
|
|
15694
|
-
found.operatorAction = "created";
|
|
15695
|
-
found.holds.push({ id: created.id, name, email: found.meEmail, user_id: found.meId });
|
|
15696
|
-
found.candidates.push({ id: created.id, name, email: found.meEmail, user_id: found.meId, detail: found.meEmail });
|
|
15697
|
-
found.provisioning.push(`created Crew row "${name}" for you`);
|
|
15698
|
-
} catch (e) {
|
|
15699
|
-
if (!(e instanceof ConnectHttpError) || e.status !== 403) throw e;
|
|
15700
|
-
found.provisioning.push("could not create your Crew row (no write access) \u2014 ask a workspace admin to add one with your user_id");
|
|
15701
|
-
}
|
|
15702
|
-
}
|
|
15703
|
-
function renderConnection(d, route2, dir, opts = {}) {
|
|
15704
|
-
const apiKeyFileLine = opts.keychainBacked ? "" : opts.apiKeyFile ? ` apiKeyFile: "${opts.apiKeyFile}"` : ` apiKeyFile: "REPLACE \u2014 a file holding this workspace's key"`;
|
|
15705
|
-
const apiKeyVarLine = opts.keychainBacked ? "" : opts.apiKeyVar ? `
|
|
15706
|
-
apiKeyVar: ${opts.apiKeyVar}` : `
|
|
15707
|
-
apiKeyVar: REPLACE_KEY_VAR`;
|
|
15708
|
-
const baseUrlLine = opts.baseUrl ? opts.baseUrl.replace(/\/+$/, "") === DEFAULT_BASE_URL ? "" : ` baseUrl: "${opts.baseUrl}"
|
|
15709
|
-
` : ' baseUrl: "REPLACE \u2014 the same baseUrl as your other routes"\n';
|
|
15710
|
-
const keyLines = [apiKeyFileLine, apiKeyVarLine.replace(/^\n/, "")].filter(Boolean).join("\n");
|
|
15711
|
-
return ` - route: ${route2}
|
|
15712
|
-
enabled: false # set to true once \`crew doctor\` is green
|
|
15713
|
-
${opts.area ? ` area: "${opts.area}"
|
|
15714
|
-
` : ""}${dir === null ? "" : ` dir: "${dir}"
|
|
15715
|
-
`}${baseUrlLine}${opts.hostPassengers ? " hostPassengers: true\n" : ""}${keyLines ? keyLines + "\n" : ""}`;
|
|
15716
|
-
}
|
|
15717
|
-
|
|
15718
|
-
// src/cli.ts
|
|
15719
|
-
import {
|
|
15720
|
-
hostFromUrl as hostFromUrl4,
|
|
15721
|
-
loginWithDeviceCode,
|
|
15722
|
-
DeviceLoginError
|
|
15723
|
-
} from "@tablation/client";
|
|
15724
|
-
|
|
15725
|
-
// src/capability-check.ts
|
|
15726
|
-
var WRITE_LEVELS = /* @__PURE__ */ new Set(["WRITE", "DELETE"]);
|
|
15727
|
-
async function canWriteTable(o, modelId) {
|
|
15728
|
-
try {
|
|
15729
|
-
const { access } = await get(o, `/data-models/${modelId}/access`);
|
|
15730
|
-
return access === void 0 ? void 0 : WRITE_LEVELS.has(access);
|
|
15731
|
-
} catch {
|
|
15732
|
-
return void 0;
|
|
15733
|
-
}
|
|
15734
|
-
}
|
|
15735
|
-
async function projectCapabilities(o, projectId) {
|
|
15736
|
-
if (!projectId) return void 0;
|
|
15737
|
-
try {
|
|
15738
|
-
return await get(o, `/projects/${projectId}/capabilities`);
|
|
15739
|
-
} catch {
|
|
15740
|
-
return void 0;
|
|
15741
|
-
}
|
|
15742
|
-
}
|
|
15743
|
-
async function checkCapabilities(o, found) {
|
|
15744
|
-
const admin = isAdminRole(found.role);
|
|
15745
|
-
const table2 = async (key, label, tableName2, modelId, askFor) => {
|
|
15746
|
-
if (!modelId) return { key, label, status: "skipped", note: `no ${tableName2} table in this project` };
|
|
15747
|
-
const ok = await canWriteTable(o, modelId);
|
|
15748
|
-
if (ok === void 0) return { key, label, status: "skipped", note: "could not read access" };
|
|
15749
|
-
return ok ? { key, label, status: "ok" } : { key, label, status: "missing", askFor };
|
|
15750
|
-
};
|
|
15751
|
-
const [crew, ships, repos, caps] = await Promise.all([
|
|
15752
|
-
table2("crew-table", "write the Crew table", "Crew", found.crewModelId ?? found.models.crew, "WRITE access on the Crew table"),
|
|
15753
|
-
table2("ships-table", "write the Ships table", "Ships", found.shipsModelId, "WRITE access on the Ships table"),
|
|
15754
|
-
table2("repos-table", "create Repos rows", "Repos", found.reposModelId, "WRITE access on the Repos table"),
|
|
15755
|
-
projectCapabilities(o, found.projectId)
|
|
15756
|
-
]);
|
|
15757
|
-
const mint = admin || caps?.MINT_AGENT_KEYS ? { key: "mint-agent-keys", label: "mint agent keys", status: "ok" } : caps ? { key: "mint-agent-keys", label: "mint agent keys", status: "missing", askFor: 'the "Mint agent keys" capability' } : { key: "mint-agent-keys", label: "mint agent keys", status: "skipped", note: "could not read capabilities" };
|
|
15758
|
-
const sync = admin ? { key: "sync-personas", label: "sync personas and skills", status: "ok" } : { key: "sync-personas", label: "sync personas and skills", status: "missing", askFor: "the workspace admin role (personas and skills are workspace-level tables)" };
|
|
15759
|
-
return [crew, ships, repos, mint, sync];
|
|
15760
|
-
}
|
|
15761
|
-
function missLine(i) {
|
|
15762
|
-
return `${i.label} \u2014 ask a workspace admin for ${i.askFor ?? "access"}`;
|
|
15763
|
-
}
|
|
15764
|
-
function describeCapability(i) {
|
|
15765
|
-
if (i.status === "ok") return `ok ${i.label}`;
|
|
15766
|
-
if (i.status === "skipped") return `skipped ${i.label} (${i.note ?? "not checked"})`;
|
|
15767
|
-
return `missing ${missLine(i)}`;
|
|
15768
|
-
}
|
|
15769
|
-
|
|
15770
|
-
// src/agents-sync-run.ts
|
|
15771
|
-
import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as readFileSync27, writeFileSync as writeFileSync22 } from "fs";
|
|
15772
|
-
import { dirname as dirname16 } from "path";
|
|
15773
|
-
async function runAgentsSync(target, o) {
|
|
15774
|
-
const dryRun = o.dryRun ?? false;
|
|
15775
|
-
const force = o.force ?? false;
|
|
15776
|
-
const keyRole = target.resolved?.role ?? await fetchKeyRole(target, o.userAgent);
|
|
15777
|
-
let readOnly = !isAdminRole(keyRole);
|
|
15778
|
-
const runPersonas = (ro) => syncPersonas(target, { userAgent: o.userAgent, dryRun, force, readOnly: ro });
|
|
15779
|
-
let personaResult;
|
|
15780
|
-
try {
|
|
15781
|
-
personaResult = await runPersonas(readOnly);
|
|
15782
|
-
} catch (e) {
|
|
15783
|
-
if (readOnly || !isForbidden(e)) throw e;
|
|
15784
|
-
readOnly = true;
|
|
15785
|
-
personaResult = await runPersonas(true);
|
|
15786
|
-
}
|
|
15787
|
-
o.say(`route: ${target.route}`);
|
|
15788
|
-
for (const p of personaResult.outcomes) o.say(describeSyncOutcome(p));
|
|
15789
|
-
for (const l of personaResult.crewLinks) o.say(describeCrewLink(l));
|
|
15790
|
-
if (personaResult.releaseNote) o.say(personaResult.releaseNote);
|
|
15791
|
-
const briefs = Object.fromEntries(ROLE_NAMES.map((r) => [r, personaDefaultPrompt(target.promptsDir, r)]));
|
|
15792
|
-
for (const w of skillMarkerWarnings(target.promptsDir, briefs)) o.warn(`warning: ${w}`);
|
|
15793
|
-
let skillResult;
|
|
15794
|
-
if (!readOnly) {
|
|
15795
|
-
try {
|
|
15796
|
-
skillResult = await syncSkills(target, { userAgent: o.userAgent, dryRun, force });
|
|
15797
|
-
} catch (e) {
|
|
15798
|
-
if (isForbidden(e)) readOnly = true;
|
|
15799
|
-
else throw e;
|
|
15800
|
-
}
|
|
15801
|
-
}
|
|
15802
|
-
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
15803
|
-
if (!dryRun) {
|
|
15804
|
-
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
15805
|
-
const raw = existsSync31(resolvedPath) ? JSON.parse(readFileSync27(resolvedPath, "utf8")) : {};
|
|
15806
|
-
raw.agentPersonas = personaResult.agentPersonas;
|
|
15807
|
-
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
15808
|
-
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
15809
|
-
mkdirSync20(dirname16(resolvedPath), { recursive: true });
|
|
15810
|
-
writeFileSync22(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
15811
|
-
`);
|
|
15812
|
-
if (target.resolved) {
|
|
15813
|
-
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
15814
|
-
if (personaResult.releaseAgentId) target.resolved.releaseAgentId = personaResult.releaseAgentId;
|
|
15815
|
-
}
|
|
15816
|
-
}
|
|
15817
|
-
if (readOnly) {
|
|
15818
|
-
o.say("personas and skills: not synced (this key is not a workspace admin; the workspace's rows are used as they are)");
|
|
15819
|
-
}
|
|
15820
|
-
const diverged = personaResult.outcomes.some((p) => p.action === "diverged") || (skillResult?.outcomes.some((s) => s.action === "diverged") ?? false);
|
|
15821
|
-
return { personaResult, skillResult, readOnly, diverged };
|
|
15822
|
-
}
|
|
15823
|
-
|
|
15824
|
-
// src/logbook.ts
|
|
15825
|
-
import { createHash as createHash6 } from "crypto";
|
|
15826
|
-
import { TablationClient as TablationClient4 } from "@tablation/client";
|
|
15827
|
-
var LogbookError = class extends Error {
|
|
15828
|
-
};
|
|
15829
|
-
function client(route2, userAgent) {
|
|
15830
|
-
return new TablationClient4({
|
|
15831
|
-
baseUrl: `${route2.baseUrl}/api`,
|
|
15832
|
-
apiKey: resolveApiKey(route2),
|
|
15833
|
-
headers: { "User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
15834
|
-
});
|
|
15835
|
-
}
|
|
15836
|
-
function requireWorkspace(route2) {
|
|
15837
|
-
if (!route2.resolved) {
|
|
15838
|
-
throw new LogbookError(`route "${route2.route}" has no resolved ids \u2014 run \`crew connect\` first`);
|
|
16431
|
+
const only = candidates2.length === 1 && unlinked.length === 1 ? candidates2[0] : void 0;
|
|
16432
|
+
const pick = mine ?? (byEmail.length === 1 ? byEmail[0] : only);
|
|
16433
|
+
if (pick) {
|
|
16434
|
+
out.operator = pick.id;
|
|
16435
|
+
out.operatorAction = isMine(pick) ? "matched" : "linked";
|
|
16436
|
+
if (!isMine(pick) && o.ship) await linkOperatorRow(o, out, pick);
|
|
16437
|
+
} else {
|
|
16438
|
+
out.operatorCompared = `signed in as ${out.meEmail ?? "unknown email"} (identity ${out.meIdentityId ?? out.meId}); ` + (people.length === 0 ? "no person rows in the Crew table" : people.map((r) => {
|
|
16439
|
+
const m = r.user_id ? members.get(r.user_id) : void 0;
|
|
16440
|
+
return `"${r.name || "unnamed row"}" <${r.email ?? "no email"}>` + (r.user_id ? ` linked to member ${m?.email ?? r.user_id}${m?.identityId ? ` (identity ${m.identityId})` : ""}` : " unlinked");
|
|
16441
|
+
}).join("; "));
|
|
16442
|
+
if (o.ship && (!o.interactive || candidates2.length === 0)) await createOperatorRow(o, out);
|
|
16443
|
+
}
|
|
16444
|
+
}
|
|
15839
16445
|
}
|
|
16446
|
+
return out;
|
|
15840
16447
|
}
|
|
15841
|
-
async function
|
|
15842
|
-
|
|
15843
|
-
|
|
15844
|
-
|
|
15845
|
-
|
|
15846
|
-
|
|
15847
|
-
if (opts.role) {
|
|
15848
|
-
const agentsModel = await c.dataModels.get("agents", route2.resolved.workspaceId);
|
|
15849
|
-
const rows = await c.records.list(agentsModel.id, { limit: 200 });
|
|
15850
|
-
const match = rows.find((r) => r.name.toLowerCase() === opts.role.toLowerCase());
|
|
15851
|
-
if (!match) return [];
|
|
15852
|
-
filters.push({ columnName: "agent_id", operator: "EQ", value: match.id });
|
|
16448
|
+
async function linkOperatorRow(o, found, row) {
|
|
16449
|
+
const name = row.name || row.id;
|
|
16450
|
+
if (!found.meId || !found.crewModelId) return;
|
|
16451
|
+
if (o.dryRun) {
|
|
16452
|
+
found.provisioning.push(`would link Crew row "${name}" to your account`);
|
|
16453
|
+
return;
|
|
15853
16454
|
}
|
|
15854
|
-
const
|
|
15855
|
-
|
|
15856
|
-
...filters.length > 0 ? { filters: JSON.stringify(filters) } : {}
|
|
15857
|
-
});
|
|
15858
|
-
return [...entries].sort((a, b) => String(b.started_at ?? "").localeCompare(String(a.started_at ?? "")));
|
|
15859
|
-
}
|
|
15860
|
-
async function showLogEntry(route2, entryId, opts = {}) {
|
|
15861
|
-
requireWorkspace(route2);
|
|
15862
|
-
const c = client(route2, opts.userAgent);
|
|
15863
|
-
const agentLogModel = await c.dataModels.get("agent_log", route2.resolved.workspaceId);
|
|
15864
|
-
let entry;
|
|
16455
|
+
const body = { user_id: found.meId };
|
|
16456
|
+
if (!row.email && found.meEmail) body.email = found.meEmail;
|
|
15865
16457
|
try {
|
|
15866
|
-
|
|
16458
|
+
await patch(o, `/data-models/${found.crewModelId}/records/${row.id}`, body);
|
|
16459
|
+
found.provisioning.push(`linked Crew row "${name}" to your account`);
|
|
15867
16460
|
} catch (e) {
|
|
15868
|
-
|
|
16461
|
+
if (!(e instanceof ConnectHttpError) || e.status !== 403) throw e;
|
|
16462
|
+
found.provisioning.push(
|
|
16463
|
+
`could not link your Crew row (no write access) \u2014 ask a workspace admin to set user_id on "${name}"`
|
|
16464
|
+
);
|
|
15869
16465
|
}
|
|
15870
|
-
|
|
15871
|
-
|
|
15872
|
-
|
|
15873
|
-
|
|
15874
|
-
|
|
15875
|
-
|
|
15876
|
-
|
|
15877
|
-
|
|
15878
|
-
};
|
|
16466
|
+
}
|
|
16467
|
+
async function createOperatorRow(o, found) {
|
|
16468
|
+
if (!found.meId || !found.crewModelId) return;
|
|
16469
|
+
const name = found.meName || found.meEmail || "Operator";
|
|
16470
|
+
if (o.dryRun) {
|
|
16471
|
+
found.provisioning.push(`would create Crew row "${name}" for you`);
|
|
16472
|
+
found.operatorAction = "would-create";
|
|
16473
|
+
return;
|
|
15879
16474
|
}
|
|
15880
|
-
const agentsModel = await c.dataModels.get("agents", route2.resolved.workspaceId);
|
|
15881
|
-
let historyEntries;
|
|
15882
16475
|
try {
|
|
15883
|
-
|
|
15884
|
-
|
|
15885
|
-
|
|
15886
|
-
|
|
15887
|
-
const match = historyEntries.find((h) => h.history_id === entry.prompt_version);
|
|
15888
|
-
if (!match) {
|
|
15889
|
-
return {
|
|
15890
|
-
entry,
|
|
15891
|
-
reconstruction: {
|
|
15892
|
-
status: "approximate",
|
|
15893
|
-
reason: `history entry ${entry.prompt_version} no longer exists on the Agents row (purged, or the row itself was recreated)`
|
|
15894
|
-
}
|
|
15895
|
-
};
|
|
15896
|
-
}
|
|
15897
|
-
const prompt = appendAdditions(match.prompt ?? "", match.prompt_additions);
|
|
15898
|
-
const actualSha = createHash6("sha256").update(prompt).digest("hex");
|
|
15899
|
-
const exact = !!entry.prompt_sha && actualSha === entry.prompt_sha;
|
|
15900
|
-
return {
|
|
15901
|
-
entry,
|
|
15902
|
-
reconstruction: {
|
|
15903
|
-
status: exact ? "exact" : "approximate",
|
|
15904
|
-
historyId: match.history_id,
|
|
15905
|
-
changedAt: match.changed_at,
|
|
15906
|
-
prompt,
|
|
15907
|
-
...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" }
|
|
15908
|
-
}
|
|
15909
|
-
};
|
|
15910
|
-
}
|
|
15911
|
-
|
|
15912
|
-
// src/repos-check.ts
|
|
15913
|
-
import { spawnSync as spawnSync6 } from "child_process";
|
|
15914
|
-
import { readFileSync as readFileSync28 } from "fs";
|
|
15915
|
-
import { basename as basename8 } from "path";
|
|
15916
|
-
var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
|
|
15917
|
-
function checkHookSyntax(script, shell) {
|
|
15918
|
-
const spec = resolveShell(shell);
|
|
15919
|
-
const name = basename8(spec.bin).replace(/\.exe$/i, "").toLowerCase();
|
|
15920
|
-
let run2;
|
|
15921
|
-
if (POSIX_SHELL.test(name)) {
|
|
15922
|
-
run2 = spawnSync6(spec.bin, ["-n"], { input: script, encoding: "utf8" });
|
|
15923
|
-
} else if (/^(pwsh|powershell)$/.test(name)) {
|
|
15924
|
-
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}";
|
|
15925
|
-
run2 = spawnSync6(spec.bin, ["-NoProfile", "-NonInteractive", "-Command", parse3], {
|
|
15926
|
-
encoding: "utf8",
|
|
15927
|
-
env: { ...process.env, CREW_CHECK_SCRIPT: script }
|
|
16476
|
+
const created = await post2(o, `/data-models/${found.crewModelId}/records`, {
|
|
16477
|
+
name,
|
|
16478
|
+
...found.meEmail ? { email: found.meEmail } : {},
|
|
16479
|
+
user_id: found.meId
|
|
15928
16480
|
});
|
|
15929
|
-
|
|
15930
|
-
|
|
16481
|
+
found.operator = created.id;
|
|
16482
|
+
found.operatorAction = "created";
|
|
16483
|
+
found.holds.push({ id: created.id, name, email: found.meEmail, user_id: found.meId });
|
|
16484
|
+
found.candidates.push({ id: created.id, name, email: found.meEmail, user_id: found.meId, detail: found.meEmail });
|
|
16485
|
+
found.provisioning.push(`created Crew row "${name}" for you`);
|
|
16486
|
+
} catch (e) {
|
|
16487
|
+
if (!(e instanceof ConnectHttpError) || e.status !== 403) throw e;
|
|
16488
|
+
found.provisioning.push("could not create your Crew row (no write access) \u2014 ask a workspace admin to add one with your user_id");
|
|
15931
16489
|
}
|
|
15932
|
-
if (run2.error) return { status: "unchecked", message: `cannot run ${spec.bin}: ${run2.error.message}` };
|
|
15933
|
-
if (run2.status === 0) return { status: "ok" };
|
|
15934
|
-
const message = `${run2.stderr ?? ""}${run2.stdout ?? ""}`.trim() || `${spec.bin} exited ${run2.status}`;
|
|
15935
|
-
return { status: "error", message };
|
|
15936
16490
|
}
|
|
15937
|
-
function
|
|
15938
|
-
const
|
|
15939
|
-
const
|
|
15940
|
-
|
|
15941
|
-
|
|
15942
|
-
|
|
15943
|
-
|
|
15944
|
-
|
|
15945
|
-
}
|
|
16491
|
+
function renderConnection(d, route2, dir, opts = {}) {
|
|
16492
|
+
const apiKeyFileLine = opts.keychainBacked ? "" : opts.apiKeyFile ? ` apiKeyFile: "${opts.apiKeyFile}"` : ` apiKeyFile: "REPLACE \u2014 a file holding this workspace's key"`;
|
|
16493
|
+
const apiKeyVarLine = opts.keychainBacked ? "" : opts.apiKeyVar ? `
|
|
16494
|
+
apiKeyVar: ${opts.apiKeyVar}` : `
|
|
16495
|
+
apiKeyVar: REPLACE_KEY_VAR`;
|
|
16496
|
+
const baseUrlLine = opts.baseUrl ? opts.baseUrl.replace(/\/+$/, "") === DEFAULT_BASE_URL ? "" : ` baseUrl: "${opts.baseUrl}"
|
|
16497
|
+
` : ' baseUrl: "REPLACE \u2014 the same baseUrl as your other routes"\n';
|
|
16498
|
+
const keyLines = [apiKeyFileLine, apiKeyVarLine.replace(/^\n/, "")].filter(Boolean).join("\n");
|
|
16499
|
+
return ` - route: ${route2}
|
|
16500
|
+
enabled: false # set to true once \`crew doctor\` is green
|
|
16501
|
+
${opts.area ? ` area: "${opts.area}"
|
|
16502
|
+
` : ""}${dir === null ? "" : ` dir: "${dir}"
|
|
16503
|
+
`}${baseUrlLine}${opts.hostPassengers ? " hostPassengers: true\n" : ""}${keyLines ? keyLines + "\n" : ""}`;
|
|
15946
16504
|
}
|
|
15947
|
-
|
|
15948
|
-
|
|
15949
|
-
|
|
15950
|
-
|
|
15951
|
-
|
|
15952
|
-
|
|
15953
|
-
|
|
16505
|
+
|
|
16506
|
+
// src/cli.ts
|
|
16507
|
+
import {
|
|
16508
|
+
hostFromUrl as hostFromUrl4,
|
|
16509
|
+
loginWithDeviceCode,
|
|
16510
|
+
DeviceLoginError
|
|
16511
|
+
} from "@tablation/client";
|
|
16512
|
+
|
|
16513
|
+
// src/capability-check.ts
|
|
16514
|
+
var WRITE_LEVELS = /* @__PURE__ */ new Set(["WRITE", "DELETE"]);
|
|
16515
|
+
async function canWriteTable(o, modelId) {
|
|
15954
16516
|
try {
|
|
15955
|
-
|
|
15956
|
-
|
|
15957
|
-
|
|
15958
|
-
return
|
|
16517
|
+
const { access } = await get(o, `/data-models/${modelId}/access`);
|
|
16518
|
+
return access === void 0 ? void 0 : WRITE_LEVELS.has(access);
|
|
16519
|
+
} catch {
|
|
16520
|
+
return void 0;
|
|
15959
16521
|
}
|
|
15960
|
-
const effective = resolveRepoConfig(parsed, opts.ship, dir);
|
|
15961
|
-
const readiness = validateEffective(effective);
|
|
15962
|
-
const hooks = Object.keys(effective.hooks).map((name) => ({
|
|
15963
|
-
name,
|
|
15964
|
-
label: hookLabel(effective, name),
|
|
15965
|
-
syntax: checkHookSyntax(effective.hooks[name], effective.shell)
|
|
15966
|
-
}));
|
|
15967
|
-
const failed = hooks.some((h) => h.syntax.status === "error");
|
|
15968
|
-
return {
|
|
15969
|
-
file,
|
|
15970
|
-
parsed,
|
|
15971
|
-
effective,
|
|
15972
|
-
readiness,
|
|
15973
|
-
hooks,
|
|
15974
|
-
shadowed: effective.shadowed,
|
|
15975
|
-
samples: renderSamples(effective, dir),
|
|
15976
|
-
error: null,
|
|
15977
|
-
exitCode: failed || opts.strict && readiness.length > 0 ? 1 : 0
|
|
15978
|
-
};
|
|
15979
16522
|
}
|
|
15980
|
-
function
|
|
15981
|
-
if (
|
|
15982
|
-
|
|
15983
|
-
|
|
15984
|
-
|
|
15985
|
-
|
|
15986
|
-
` platform ${e.platform}`,
|
|
15987
|
-
` release.mode ${e.release.mode}`,
|
|
15988
|
-
` release.versioning ${e.release.versioning}`,
|
|
15989
|
-
` branch.base ${e.branch.base}`,
|
|
15990
|
-
` branch.name ${e.branch.name}`,
|
|
15991
|
-
` branch.push ${e.branch.push}`
|
|
15992
|
-
];
|
|
15993
|
-
for (const s of r.samples) lines.push(` ${s.case}: branch ${s.branch}, worktree ../${s.worktree}`);
|
|
15994
|
-
lines.push(r.hooks.length ? " hooks" : " hooks none");
|
|
15995
|
-
for (const h of r.hooks) {
|
|
15996
|
-
const named = h.label !== e.hooks[h.name] ? ` (${h.label})` : "";
|
|
15997
|
-
const syntax = h.syntax.status === "ok" ? "syntax ok" : h.syntax.status === "unchecked" ? `syntax not checked: ${h.syntax.message}` : "syntax ERROR";
|
|
15998
|
-
lines.push(` ${h.name}${named}: ${syntax}`);
|
|
15999
|
-
if (h.syntax.status === "error") lines.push(...(h.syntax.message ?? "").split("\n").map((l) => ` ${l}`));
|
|
16523
|
+
async function projectCapabilities(o, projectId) {
|
|
16524
|
+
if (!projectId) return void 0;
|
|
16525
|
+
try {
|
|
16526
|
+
return await get(o, `/projects/${projectId}/capabilities`);
|
|
16527
|
+
} catch {
|
|
16528
|
+
return void 0;
|
|
16000
16529
|
}
|
|
16001
|
-
if (r.shadowed.length) lines.push(` this ship declares ${r.shadowed.join(", ")}, and the file overrides it \u2014 the file wins`);
|
|
16002
|
-
for (const p of r.readiness) lines.push(` - release readiness: ${p}`);
|
|
16003
|
-
return `${lines.join("\n")}
|
|
16004
|
-
`;
|
|
16005
16530
|
}
|
|
16006
|
-
|
|
16007
|
-
|
|
16008
|
-
|
|
16009
|
-
|
|
16010
|
-
|
|
16011
|
-
|
|
16012
|
-
|
|
16013
|
-
|
|
16014
|
-
|
|
16015
|
-
|
|
16016
|
-
|
|
16017
|
-
|
|
16018
|
-
|
|
16019
|
-
|
|
16020
|
-
const
|
|
16021
|
-
|
|
16022
|
-
|
|
16023
|
-
s = s.replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
16024
|
-
const parts = s.split("/").filter(Boolean);
|
|
16025
|
-
if (parts.length < 2) return void 0;
|
|
16026
|
-
return parts.slice(-2).join("/").toLowerCase();
|
|
16531
|
+
async function checkCapabilities(o, found) {
|
|
16532
|
+
const admin = isAdminRole(found.role);
|
|
16533
|
+
const table2 = async (key, label, tableName2, modelId, askFor) => {
|
|
16534
|
+
if (!modelId) return { key, label, status: "skipped", note: `no ${tableName2} table in this project` };
|
|
16535
|
+
const ok = await canWriteTable(o, modelId);
|
|
16536
|
+
if (ok === void 0) return { key, label, status: "skipped", note: "could not read access" };
|
|
16537
|
+
return ok ? { key, label, status: "ok" } : { key, label, status: "missing", askFor };
|
|
16538
|
+
};
|
|
16539
|
+
const [crew, ships, repos, caps] = await Promise.all([
|
|
16540
|
+
table2("crew-table", "write the Crew table", "Crew", found.crewModelId ?? found.models.crew, "WRITE access on the Crew table"),
|
|
16541
|
+
table2("ships-table", "write the Ships table", "Ships", found.shipsModelId, "WRITE access on the Ships table"),
|
|
16542
|
+
table2("repos-table", "create Repos rows", "Repos", found.reposModelId, "WRITE access on the Repos table"),
|
|
16543
|
+
projectCapabilities(o, found.projectId)
|
|
16544
|
+
]);
|
|
16545
|
+
const mint = admin || caps?.MINT_AGENT_KEYS ? { key: "mint-agent-keys", label: "mint agent keys", status: "ok" } : caps ? { key: "mint-agent-keys", label: "mint agent keys", status: "missing", askFor: 'the "Mint agent keys" capability' } : { key: "mint-agent-keys", label: "mint agent keys", status: "skipped", note: "could not read capabilities" };
|
|
16546
|
+
const sync = admin ? { key: "sync-personas", label: "sync personas and skills", status: "ok" } : { key: "sync-personas", label: "sync personas and skills", status: "missing", askFor: "the workspace admin role (personas and skills are workspace-level tables)" };
|
|
16547
|
+
return [crew, ships, repos, mint, sync];
|
|
16027
16548
|
}
|
|
16028
|
-
function
|
|
16029
|
-
|
|
16030
|
-
if (!want || !resolved?.repoRemotes) return void 0;
|
|
16031
|
-
for (const [id, remote] of Object.entries(resolved.repoRemotes)) {
|
|
16032
|
-
if (normalizeRemote(remote) === want) return resolved.repoNames?.[id];
|
|
16033
|
-
}
|
|
16034
|
-
return void 0;
|
|
16549
|
+
function missLine(i) {
|
|
16550
|
+
return `${i.label} \u2014 ask a workspace admin for ${i.askFor ?? "access"}`;
|
|
16035
16551
|
}
|
|
16036
|
-
function
|
|
16037
|
-
|
|
16038
|
-
if (
|
|
16039
|
-
return
|
|
16552
|
+
function describeCapability(i) {
|
|
16553
|
+
if (i.status === "ok") return `ok ${i.label}`;
|
|
16554
|
+
if (i.status === "skipped") return `skipped ${i.label} (${i.note ?? "not checked"})`;
|
|
16555
|
+
return `missing ${missLine(i)}`;
|
|
16040
16556
|
}
|
|
16041
|
-
|
|
16042
|
-
|
|
16043
|
-
|
|
16044
|
-
|
|
16045
|
-
|
|
16046
|
-
|
|
16557
|
+
|
|
16558
|
+
// src/agents-sync-run.ts
|
|
16559
|
+
import { existsSync as existsSync32, mkdirSync as mkdirSync20, readFileSync as readFileSync28, writeFileSync as writeFileSync23 } from "fs";
|
|
16560
|
+
import { dirname as dirname17 } from "path";
|
|
16561
|
+
async function runAgentsSync(target, o) {
|
|
16562
|
+
const dryRun = o.dryRun ?? false;
|
|
16563
|
+
const force = o.force ?? false;
|
|
16564
|
+
const keyRole = target.resolved?.role ?? await fetchKeyRole(target, o.userAgent);
|
|
16565
|
+
let readOnly = !isAdminRole(keyRole);
|
|
16566
|
+
const runPersonas = (ro) => syncPersonas(target, { userAgent: o.userAgent, dryRun, force, readOnly: ro });
|
|
16567
|
+
let personaResult;
|
|
16568
|
+
try {
|
|
16569
|
+
personaResult = await runPersonas(readOnly);
|
|
16570
|
+
} catch (e) {
|
|
16571
|
+
if (readOnly || !isForbidden(e)) throw e;
|
|
16572
|
+
readOnly = true;
|
|
16573
|
+
personaResult = await runPersonas(true);
|
|
16047
16574
|
}
|
|
16048
|
-
|
|
16049
|
-
|
|
16050
|
-
|
|
16575
|
+
o.say(`route: ${target.route}`);
|
|
16576
|
+
for (const p of personaResult.outcomes) o.say(describeSyncOutcome(p));
|
|
16577
|
+
for (const l of personaResult.crewLinks) o.say(describeCrewLink(l));
|
|
16578
|
+
if (personaResult.releaseNote) o.say(personaResult.releaseNote);
|
|
16579
|
+
const briefs = Object.fromEntries(ROLE_NAMES.map((r) => [r, personaDefaultPrompt(target.promptsDir, r)]));
|
|
16580
|
+
for (const w of skillMarkerWarnings(target.promptsDir, briefs)) o.warn(`warning: ${w}`);
|
|
16581
|
+
let skillResult;
|
|
16582
|
+
if (!readOnly) {
|
|
16583
|
+
try {
|
|
16584
|
+
skillResult = await syncSkills(target, { userAgent: o.userAgent, dryRun, force });
|
|
16585
|
+
} catch (e) {
|
|
16586
|
+
if (isForbidden(e)) readOnly = true;
|
|
16587
|
+
else throw e;
|
|
16588
|
+
}
|
|
16051
16589
|
}
|
|
16052
|
-
|
|
16053
|
-
if (
|
|
16054
|
-
|
|
16055
|
-
|
|
16056
|
-
|
|
16057
|
-
|
|
16058
|
-
|
|
16059
|
-
|
|
16060
|
-
}
|
|
16061
|
-
|
|
16062
|
-
|
|
16063
|
-
|
|
16064
|
-
|
|
16065
|
-
if (isMap(item) && item.get("route") === route2) return item;
|
|
16590
|
+
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
16591
|
+
if (!dryRun) {
|
|
16592
|
+
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
16593
|
+
const raw = existsSync32(resolvedPath) ? JSON.parse(readFileSync28(resolvedPath, "utf8")) : {};
|
|
16594
|
+
raw.agentPersonas = personaResult.agentPersonas;
|
|
16595
|
+
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
16596
|
+
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
16597
|
+
mkdirSync20(dirname17(resolvedPath), { recursive: true });
|
|
16598
|
+
writeFileSync23(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
16599
|
+
`);
|
|
16600
|
+
if (target.resolved) {
|
|
16601
|
+
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
16602
|
+
if (personaResult.releaseAgentId) target.resolved.releaseAgentId = personaResult.releaseAgentId;
|
|
16066
16603
|
}
|
|
16067
16604
|
}
|
|
16068
|
-
|
|
16069
|
-
|
|
16070
|
-
}
|
|
16071
|
-
function configuredRepos(text, route2, base) {
|
|
16072
|
-
const doc = parseDocument3(text);
|
|
16073
|
-
const m = routeMapOf(doc, route2);
|
|
16074
|
-
const repos = m.get("repos");
|
|
16075
|
-
if (isMap(repos)) {
|
|
16076
|
-
return repos.items.map((p) => {
|
|
16077
|
-
const name = String(p.key.value ?? p.key);
|
|
16078
|
-
const v = p.value;
|
|
16079
|
-
const dir2 = isMap(v) ? String(v.get("dir") ?? "") : String(v?.value ?? "");
|
|
16080
|
-
return { name, dir: expandDir(dir2, base) };
|
|
16081
|
-
});
|
|
16605
|
+
if (readOnly) {
|
|
16606
|
+
o.say("personas and skills: not synced (this key is not a workspace admin; the workspace's rows are used as they are)");
|
|
16082
16607
|
}
|
|
16083
|
-
const
|
|
16084
|
-
return
|
|
16608
|
+
const diverged = personaResult.outcomes.some((p) => p.action === "diverged") || (skillResult?.outcomes.some((s) => s.action === "diverged") ?? false);
|
|
16609
|
+
return { personaResult, skillResult, readOnly, diverged };
|
|
16085
16610
|
}
|
|
16086
|
-
|
|
16087
|
-
|
|
16088
|
-
|
|
16089
|
-
|
|
16090
|
-
|
|
16091
|
-
|
|
16092
|
-
|
|
16093
|
-
return
|
|
16611
|
+
|
|
16612
|
+
// src/logbook.ts
|
|
16613
|
+
import { createHash as createHash6 } from "crypto";
|
|
16614
|
+
import { TablationClient as TablationClient4 } from "@tablation/client";
|
|
16615
|
+
var LogbookError = class extends Error {
|
|
16616
|
+
};
|
|
16617
|
+
function client(route2, userAgent) {
|
|
16618
|
+
return new TablationClient4({
|
|
16619
|
+
baseUrl: `${route2.baseUrl}/api`,
|
|
16620
|
+
apiKey: resolveApiKey(route2),
|
|
16621
|
+
headers: { "User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
16622
|
+
});
|
|
16094
16623
|
}
|
|
16095
|
-
function
|
|
16096
|
-
|
|
16097
|
-
|
|
16098
|
-
|
|
16099
|
-
if (isMap(repos)) target = repos.get(name, true);
|
|
16100
|
-
if (!isMap(target)) return false;
|
|
16101
|
-
const hooks = target.get("hooks", true);
|
|
16102
|
-
return isMap(hooks) && hooks.items.length > 0;
|
|
16624
|
+
function requireWorkspace(route2) {
|
|
16625
|
+
if (!route2.resolved) {
|
|
16626
|
+
throw new LogbookError(`route "${route2.route}" has no resolved ids \u2014 run \`crew connect\` first`);
|
|
16627
|
+
}
|
|
16103
16628
|
}
|
|
16104
|
-
function
|
|
16105
|
-
|
|
16106
|
-
const
|
|
16107
|
-
const
|
|
16108
|
-
const
|
|
16109
|
-
|
|
16110
|
-
|
|
16111
|
-
|
|
16112
|
-
|
|
16113
|
-
|
|
16114
|
-
|
|
16115
|
-
|
|
16116
|
-
const remotes = i.resolved?.repoRemotes ?? {};
|
|
16117
|
-
for (const [id, name] of Object.entries(names)) {
|
|
16118
|
-
const remote = remotes[id] ?? null;
|
|
16119
|
-
const want = normalizeRemote(remote);
|
|
16120
|
-
let idx = checkouts.findIndex((c2, n) => !claimed.has(n) && want !== void 0 && normalizeRemote(i.originOf(c2.dir)) === want);
|
|
16121
|
-
if (idx < 0) idx = checkouts.findIndex((c2, n) => !claimed.has(n) && c2.name === name);
|
|
16122
|
-
const c = idx >= 0 ? checkouts[idx] : void 0;
|
|
16123
|
-
if (idx >= 0) claimed.add(idx);
|
|
16124
|
-
rows.push({
|
|
16125
|
-
name,
|
|
16126
|
-
remote,
|
|
16127
|
-
repoRowId: id,
|
|
16128
|
-
attached: c !== void 0,
|
|
16129
|
-
dir: c?.dir ?? null,
|
|
16130
|
-
exists: c ? i.exists(c.dir) : false,
|
|
16131
|
-
origin: c ? originFor(c, remote) : null,
|
|
16132
|
-
matchedRow: c ? name : null,
|
|
16133
|
-
hooksSource: c ? hooksSourceOf(c) : "none"
|
|
16134
|
-
});
|
|
16629
|
+
async function listLogEntries(route2, opts = {}) {
|
|
16630
|
+
requireWorkspace(route2);
|
|
16631
|
+
const c = client(route2, opts.userAgent);
|
|
16632
|
+
const agentLogModel = await c.dataModels.get("agent_log", route2.resolved.workspaceId);
|
|
16633
|
+
const filters = [];
|
|
16634
|
+
if (opts.ticket) filters.push({ columnName: "ticket_reference", operator: "EQ", value: opts.ticket });
|
|
16635
|
+
if (opts.role) {
|
|
16636
|
+
const agentsModel = await c.dataModels.get("agents", route2.resolved.workspaceId);
|
|
16637
|
+
const rows = await c.records.list(agentsModel.id, { limit: 200 });
|
|
16638
|
+
const match = rows.find((r) => r.name.toLowerCase() === opts.role.toLowerCase());
|
|
16639
|
+
if (!match) return [];
|
|
16640
|
+
filters.push({ columnName: "agent_id", operator: "EQ", value: match.id });
|
|
16135
16641
|
}
|
|
16136
|
-
|
|
16137
|
-
|
|
16138
|
-
|
|
16139
|
-
name: c.name,
|
|
16140
|
-
remote: null,
|
|
16141
|
-
repoRowId: null,
|
|
16142
|
-
attached: true,
|
|
16143
|
-
dir: c.dir,
|
|
16144
|
-
exists: i.exists(c.dir),
|
|
16145
|
-
origin: originFor(c, null),
|
|
16146
|
-
matchedRow: null,
|
|
16147
|
-
hooksSource: hooksSourceOf(c)
|
|
16148
|
-
});
|
|
16642
|
+
const entries = await c.records.list(agentLogModel.id, {
|
|
16643
|
+
limit: opts.limit ?? 20,
|
|
16644
|
+
...filters.length > 0 ? { filters: JSON.stringify(filters) } : {}
|
|
16149
16645
|
});
|
|
16150
|
-
return
|
|
16646
|
+
return [...entries].sort((a, b) => String(b.started_at ?? "").localeCompare(String(a.started_at ?? "")));
|
|
16151
16647
|
}
|
|
16152
|
-
function
|
|
16153
|
-
|
|
16154
|
-
|
|
16155
|
-
const
|
|
16156
|
-
let
|
|
16157
|
-
|
|
16158
|
-
|
|
16159
|
-
|
|
16160
|
-
|
|
16161
|
-
if (i.isCheckout && placeholder && !(isMap(reposNow) && reposNow.items.length > 0)) {
|
|
16162
|
-
const placeholderDir = expandDir(String(placeholder), i.base);
|
|
16163
|
-
if (placeholderDir !== target && !i.isCheckout(placeholderDir)) {
|
|
16164
|
-
droppedDir = placeholderDir;
|
|
16165
|
-
m.delete("dir");
|
|
16166
|
-
existing = [];
|
|
16167
|
-
}
|
|
16168
|
-
}
|
|
16169
|
-
if (existing.some((e) => e.dir === target)) {
|
|
16170
|
-
throw new ReposError(`${target} is already configured on route ${i.route}`);
|
|
16648
|
+
async function showLogEntry(route2, entryId, opts = {}) {
|
|
16649
|
+
requireWorkspace(route2);
|
|
16650
|
+
const c = client(route2, opts.userAgent);
|
|
16651
|
+
const agentLogModel = await c.dataModels.get("agent_log", route2.resolved.workspaceId);
|
|
16652
|
+
let entry;
|
|
16653
|
+
try {
|
|
16654
|
+
entry = await c.records.get(agentLogModel.id, entryId);
|
|
16655
|
+
} catch (e) {
|
|
16656
|
+
throw new LogbookError(`no Agent Log entry ${entryId} in this workspace (${e.message})`);
|
|
16171
16657
|
}
|
|
16172
|
-
|
|
16173
|
-
|
|
16174
|
-
|
|
16175
|
-
|
|
16176
|
-
|
|
16177
|
-
|
|
16658
|
+
if (!opts.withPrompt) return { entry, reconstruction: { status: "not_recorded", reason: "not requested" } };
|
|
16659
|
+
if (!entry.agent_id || !entry.prompt_version) {
|
|
16660
|
+
return {
|
|
16661
|
+
entry,
|
|
16662
|
+
reconstruction: {
|
|
16663
|
+
status: "not_recorded",
|
|
16664
|
+
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"
|
|
16665
|
+
}
|
|
16666
|
+
};
|
|
16178
16667
|
}
|
|
16179
|
-
const
|
|
16180
|
-
let
|
|
16181
|
-
|
|
16182
|
-
|
|
16183
|
-
|
|
16184
|
-
|
|
16185
|
-
} else if (m.has("dir") && existing.length > 0) {
|
|
16186
|
-
const old = existing[0];
|
|
16187
|
-
const oldName = i.nameForExisting?.(old.dir) ?? old.name;
|
|
16188
|
-
if (oldName === name) throw new ReposError(`route ${i.route} already has a repo named "${name}" (${old.dir})`);
|
|
16189
|
-
const oldDir = String(m.get("dir"));
|
|
16190
|
-
const pair = m.items.find((p) => p.key.value === "dir");
|
|
16191
|
-
pair.key = doc.createNode("repos");
|
|
16192
|
-
pair.value = doc.createNode({ [oldName]: oldDir, [name]: target });
|
|
16193
|
-
mode = "repos";
|
|
16194
|
-
} else if (droppedDir) {
|
|
16195
|
-
m.set("repos", doc.createNode({ [name]: target }));
|
|
16196
|
-
mode = "repos";
|
|
16197
|
-
} else {
|
|
16198
|
-
m.set("dir", target);
|
|
16199
|
-
mode = "dir";
|
|
16668
|
+
const agentsModel = await c.dataModels.get("agents", route2.resolved.workspaceId);
|
|
16669
|
+
let historyEntries;
|
|
16670
|
+
try {
|
|
16671
|
+
historyEntries = await fetchHistoryEntries(route2, agentsModel.id, entry.agent_id, opts.userAgent, 200);
|
|
16672
|
+
} catch (e) {
|
|
16673
|
+
return { entry, reconstruction: { status: "approximate", reason: `history lookup failed: ${e.message}` } };
|
|
16200
16674
|
}
|
|
16201
|
-
|
|
16202
|
-
|
|
16203
|
-
|
|
16204
|
-
|
|
16205
|
-
|
|
16206
|
-
|
|
16207
|
-
|
|
16208
|
-
|
|
16209
|
-
|
|
16210
|
-
if (!hit) throw new ReposError(`${target} is not configured on route ${route2}`);
|
|
16211
|
-
const repos = m.get("repos", true);
|
|
16212
|
-
if (isMap(repos)) {
|
|
16213
|
-
repos.delete(hit.name);
|
|
16214
|
-
const left = existing.filter((e) => e !== hit);
|
|
16215
|
-
if (left.length === 1) {
|
|
16216
|
-
const pair = m.items.find((p) => p.key.value === "repos");
|
|
16217
|
-
pair.key = doc.createNode("dir");
|
|
16218
|
-
pair.value = doc.createNode(String(repos.get(left[0].name)));
|
|
16219
|
-
} else if (left.length === 0) m.delete("repos");
|
|
16220
|
-
} else {
|
|
16221
|
-
m.delete("dir");
|
|
16675
|
+
const match = historyEntries.find((h) => h.history_id === entry.prompt_version);
|
|
16676
|
+
if (!match) {
|
|
16677
|
+
return {
|
|
16678
|
+
entry,
|
|
16679
|
+
reconstruction: {
|
|
16680
|
+
status: "approximate",
|
|
16681
|
+
reason: `history entry ${entry.prompt_version} no longer exists on the Agents row (purged, or the row itself was recreated)`
|
|
16682
|
+
}
|
|
16683
|
+
};
|
|
16222
16684
|
}
|
|
16223
|
-
|
|
16224
|
-
|
|
16225
|
-
|
|
16226
|
-
return stringify2([m.toJSON()]).trimEnd();
|
|
16227
|
-
}
|
|
16228
|
-
var LOCAL_RELEASE_MODES = /* @__PURE__ */ new Set(["integrate", "external"]);
|
|
16229
|
-
function planRepoRow(i) {
|
|
16230
|
-
const remote = normalizeRemote(i.origin);
|
|
16231
|
-
if (!remote) throw new ReposError(`cannot derive an owner/repo from origin "${i.origin}"`);
|
|
16232
|
-
const mode = i.spec?.releaseMode;
|
|
16685
|
+
const prompt = appendAdditions(match.prompt ?? "", match.prompt_additions);
|
|
16686
|
+
const actualSha = createHash6("sha256").update(prompt).digest("hex");
|
|
16687
|
+
const exact = !!entry.prompt_sha && actualSha === entry.prompt_sha;
|
|
16233
16688
|
return {
|
|
16234
|
-
|
|
16235
|
-
|
|
16236
|
-
|
|
16237
|
-
|
|
16238
|
-
|
|
16239
|
-
|
|
16240
|
-
|
|
16689
|
+
entry,
|
|
16690
|
+
reconstruction: {
|
|
16691
|
+
status: exact ? "exact" : "approximate",
|
|
16692
|
+
historyId: match.history_id,
|
|
16693
|
+
changedAt: match.changed_at,
|
|
16694
|
+
prompt,
|
|
16695
|
+
...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" }
|
|
16696
|
+
}
|
|
16241
16697
|
};
|
|
16242
16698
|
}
|
|
16243
|
-
|
|
16244
|
-
|
|
16245
|
-
|
|
16246
|
-
|
|
16247
|
-
|
|
16248
|
-
|
|
16699
|
+
|
|
16700
|
+
// src/repos-check.ts
|
|
16701
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
16702
|
+
import { readFileSync as readFileSync29 } from "fs";
|
|
16703
|
+
import { basename as basename9 } from "path";
|
|
16704
|
+
var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
|
|
16705
|
+
function checkHookSyntax(script, shell) {
|
|
16706
|
+
const spec = resolveShell(shell);
|
|
16707
|
+
const name = basename9(spec.bin).replace(/\.exe$/i, "").toLowerCase();
|
|
16708
|
+
let run2;
|
|
16709
|
+
if (POSIX_SHELL.test(name)) {
|
|
16710
|
+
run2 = spawnSync7(spec.bin, ["-n"], { input: script, encoding: "utf8" });
|
|
16711
|
+
} else if (/^(pwsh|powershell)$/.test(name)) {
|
|
16712
|
+
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}";
|
|
16713
|
+
run2 = spawnSync7(spec.bin, ["-NoProfile", "-NonInteractive", "-Command", parse3], {
|
|
16714
|
+
encoding: "utf8",
|
|
16715
|
+
env: { ...process.env, CREW_CHECK_SCRIPT: script }
|
|
16716
|
+
});
|
|
16717
|
+
} else {
|
|
16718
|
+
return { status: "unchecked", message: `no syntax check for ${spec.bin}` };
|
|
16249
16719
|
}
|
|
16250
|
-
if (
|
|
16251
|
-
if (
|
|
16252
|
-
|
|
16253
|
-
|
|
16254
|
-
);
|
|
16720
|
+
if (run2.error) return { status: "unchecked", message: `cannot run ${spec.bin}: ${run2.error.message}` };
|
|
16721
|
+
if (run2.status === 0) return { status: "ok" };
|
|
16722
|
+
const message = `${run2.stderr ?? ""}${run2.stdout ?? ""}`.trim() || `${spec.bin} exited ${run2.status}`;
|
|
16723
|
+
return { status: "error", message };
|
|
16255
16724
|
}
|
|
16256
|
-
function
|
|
16257
|
-
const
|
|
16258
|
-
|
|
16259
|
-
|
|
16260
|
-
|
|
16261
|
-
|
|
16262
|
-
|
|
16263
|
-
|
|
16264
|
-
}
|
|
16265
|
-
if (rest.length !== 1) throw new ReposError("expected [route] <name> \u2014 the Repos row to clone (see `crew repos list`)");
|
|
16266
|
-
return { route: route2, name: rest[0] };
|
|
16725
|
+
function renderSamples(cfg, dir) {
|
|
16726
|
+
const checkout = basename9(dir.replace(/[/\\]+$/, ""));
|
|
16727
|
+
const tagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", tag: "TABL-123", prefix: "tabl", lowercase: cfg.branch.lowercase };
|
|
16728
|
+
const untagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", lowercase: cfg.branch.lowercase };
|
|
16729
|
+
return [["with a project tag", tagged], ["without a project tag", untagged]].map(([label, ctx]) => {
|
|
16730
|
+
const branch = renderBranchName(effectiveBranchTemplate(cfg, ctx.prefix), ctx);
|
|
16731
|
+
const worktree = effectiveWorktreeDirName(cfg, dir, branch, "123", (t) => renderWorktreeName(t, { ...ctx, dir: checkout }));
|
|
16732
|
+
return { case: label, branch, worktree };
|
|
16733
|
+
});
|
|
16267
16734
|
}
|
|
16268
|
-
function
|
|
16269
|
-
const
|
|
16270
|
-
const
|
|
16271
|
-
if (!
|
|
16272
|
-
|
|
16273
|
-
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"}`);
|
|
16735
|
+
function checkRepoSpec(dir, opts = {}) {
|
|
16736
|
+
const empty = { parsed: null, effective: null, readiness: [], hooks: [], shadowed: [], samples: [] };
|
|
16737
|
+
const file = findRepoConfig(dir);
|
|
16738
|
+
if (!file) {
|
|
16739
|
+
return { ...empty, file: null, error: `no .crew.yaml, .crew.yml or .crew.json in ${dir}`, exitCode: 1 };
|
|
16274
16740
|
}
|
|
16275
|
-
|
|
16276
|
-
|
|
16277
|
-
|
|
16278
|
-
|
|
16279
|
-
|
|
16280
|
-
|
|
16741
|
+
let parsed;
|
|
16742
|
+
try {
|
|
16743
|
+
parsed = parseRepoConfig(readFileSync29(file, "utf8"), file);
|
|
16744
|
+
} catch (e) {
|
|
16745
|
+
if (!(e instanceof RepoConfigError)) throw e;
|
|
16746
|
+
return { ...empty, file, error: e.message, exitCode: 1 };
|
|
16281
16747
|
}
|
|
16282
|
-
const
|
|
16283
|
-
const
|
|
16284
|
-
const
|
|
16285
|
-
|
|
16286
|
-
|
|
16748
|
+
const effective = resolveRepoConfig(parsed, opts.ship, dir);
|
|
16749
|
+
const readiness = validateEffective(effective);
|
|
16750
|
+
const hooks = Object.keys(effective.hooks).map((name) => ({
|
|
16751
|
+
name,
|
|
16752
|
+
label: hookLabel(effective, name),
|
|
16753
|
+
syntax: checkHookSyntax(effective.hooks[name], effective.shell)
|
|
16754
|
+
}));
|
|
16755
|
+
const failed = hooks.some((h) => h.syntax.status === "error");
|
|
16756
|
+
return {
|
|
16757
|
+
file,
|
|
16758
|
+
parsed,
|
|
16759
|
+
effective,
|
|
16760
|
+
readiness,
|
|
16761
|
+
hooks,
|
|
16762
|
+
shadowed: effective.shadowed,
|
|
16763
|
+
samples: renderSamples(effective, dir),
|
|
16764
|
+
error: null,
|
|
16765
|
+
exitCode: failed || opts.strict && readiness.length > 0 ? 1 : 0
|
|
16766
|
+
};
|
|
16767
|
+
}
|
|
16768
|
+
function renderRepoCheck(r) {
|
|
16769
|
+
if (r.error) return `${r.file ?? "crew repos check"}: ${r.error}
|
|
16770
|
+
`;
|
|
16771
|
+
const e = r.effective;
|
|
16772
|
+
const lines = [
|
|
16773
|
+
`${r.file}`,
|
|
16774
|
+
` platform ${e.platform}`,
|
|
16775
|
+
` release.mode ${e.release.mode}`,
|
|
16776
|
+
` release.versioning ${e.release.versioning}`,
|
|
16777
|
+
` branch.base ${e.branch.base}`,
|
|
16778
|
+
` branch.name ${e.branch.name}`,
|
|
16779
|
+
` branch.push ${e.branch.push}`
|
|
16780
|
+
];
|
|
16781
|
+
for (const s of r.samples) lines.push(` ${s.case}: branch ${s.branch}, worktree ../${s.worktree}`);
|
|
16782
|
+
lines.push(r.hooks.length ? " hooks" : " hooks none");
|
|
16783
|
+
for (const h of r.hooks) {
|
|
16784
|
+
const named = h.label !== e.hooks[h.name] ? ` (${h.label})` : "";
|
|
16785
|
+
const syntax = h.syntax.status === "ok" ? "syntax ok" : h.syntax.status === "unchecked" ? `syntax not checked: ${h.syntax.message}` : "syntax ERROR";
|
|
16786
|
+
lines.push(` ${h.name}${named}: ${syntax}`);
|
|
16787
|
+
if (h.syntax.status === "error") lines.push(...(h.syntax.message ?? "").split("\n").map((l) => ` ${l}`));
|
|
16287
16788
|
}
|
|
16288
|
-
|
|
16289
|
-
const
|
|
16290
|
-
|
|
16291
|
-
|
|
16292
|
-
return { ...add, remote, target };
|
|
16789
|
+
if (r.shadowed.length) lines.push(` this ship declares ${r.shadowed.join(", ")}, and the file overrides it \u2014 the file wins`);
|
|
16790
|
+
for (const p of r.readiness) lines.push(` - release readiness: ${p}`);
|
|
16791
|
+
return `${lines.join("\n")}
|
|
16792
|
+
`;
|
|
16293
16793
|
}
|
|
16294
16794
|
|
|
16295
16795
|
// src/repos-hooks.ts
|
|
16796
|
+
import { isMap as isMap2, parseDocument as parseDocument6, YAMLMap } from "yaml";
|
|
16296
16797
|
function packageManager(f) {
|
|
16297
16798
|
if (f.exists("pnpm-lock.yaml")) return "pnpm";
|
|
16298
16799
|
if (f.exists("yarn.lock")) return "yarn";
|
|
@@ -16433,7 +16934,7 @@ function repoDirOf(text, base, route2, name) {
|
|
|
16433
16934
|
return hit.dir;
|
|
16434
16935
|
}
|
|
16435
16936
|
function planRouteHooks(text, route2, name, hooks, opts = {}) {
|
|
16436
|
-
const doc =
|
|
16937
|
+
const doc = parseDocument6(text);
|
|
16437
16938
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
16438
16939
|
const m = routeMapOf(doc, route2);
|
|
16439
16940
|
const repos = m.get("repos", true);
|
|
@@ -16470,7 +16971,7 @@ function planRouteHooks(text, route2, name, hooks, opts = {}) {
|
|
|
16470
16971
|
return { text: String(doc), written, kept, replaced };
|
|
16471
16972
|
}
|
|
16472
16973
|
function clearRouteHooks(text, route2, name) {
|
|
16473
|
-
const doc =
|
|
16974
|
+
const doc = parseDocument6(text);
|
|
16474
16975
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
16475
16976
|
const m = routeMapOf(doc, route2);
|
|
16476
16977
|
const repos = m.get("repos", true);
|
|
@@ -16753,56 +17254,8 @@ function planSampleTicket(i) {
|
|
|
16753
17254
|
};
|
|
16754
17255
|
}
|
|
16755
17256
|
|
|
16756
|
-
// src/doctor-fix.ts
|
|
16757
|
-
import { parseDocument as parseDocument5 } from "yaml";
|
|
16758
|
-
var sound = (r) => r.hasRepos && r.problems.length === 0;
|
|
16759
|
-
function planFix(f) {
|
|
16760
|
-
const actions = [];
|
|
16761
|
-
const notes = [];
|
|
16762
|
-
for (const r of f.routes) {
|
|
16763
|
-
if (!r.enabled && sound(r)) {
|
|
16764
|
-
actions.push({ kind: "enable", route: r.route });
|
|
16765
|
-
const gaps = r.readiness ?? [];
|
|
16766
|
-
if (gaps.length) {
|
|
16767
|
-
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`);
|
|
16768
|
-
}
|
|
16769
|
-
} else if (!sound(r) && !r.enabled) {
|
|
16770
|
-
notes.push(`${r.route}: not enabled \u2014 ${r.hasRepos ? r.problems[0] : "no checkout is configured"}`);
|
|
16771
|
-
}
|
|
16772
|
-
}
|
|
16773
|
-
if (f.schedulerAbsent && f.routes.some(sound)) {
|
|
16774
|
-
if (f.host === "windows") notes.push("scheduler support is not built for Windows yet \u2014 nothing to install; run `crew run` by hand");
|
|
16775
|
-
else actions.push({ kind: "install" });
|
|
16776
|
-
}
|
|
16777
|
-
return { actions, notes };
|
|
16778
|
-
}
|
|
16779
|
-
async function gatherHealth(c, check) {
|
|
16780
|
-
let problems;
|
|
16781
|
-
let readiness = [];
|
|
16782
|
-
try {
|
|
16783
|
-
const r = await check();
|
|
16784
|
-
if (Array.isArray(r)) problems = r;
|
|
16785
|
-
else ({ problems, readiness } = r);
|
|
16786
|
-
} catch (e) {
|
|
16787
|
-
problems = [`checks could not run: ${e instanceof Error ? e.message : String(e)}`];
|
|
16788
|
-
}
|
|
16789
|
-
return { route: c.route, enabled: c.enabled, hasRepos: c.hasRepos, problems, readiness };
|
|
16790
|
-
}
|
|
16791
|
-
function routesInScope(routes, named) {
|
|
16792
|
-
return named ? routes.filter((r) => r.route === named) : routes;
|
|
16793
|
-
}
|
|
16794
|
-
function planEnable(text, route2) {
|
|
16795
|
-
const doc = parseDocument5(text);
|
|
16796
|
-
routeMapOf(doc, route2).set("enabled", true);
|
|
16797
|
-
return String(doc);
|
|
16798
|
-
}
|
|
16799
|
-
function schedulerInstalled(plan, exists, crontab) {
|
|
16800
|
-
if (plan.mechanism === "cron") return plan.cronMarker !== null && crontab.includes(plan.cronMarker);
|
|
16801
|
-
return plan.unitPaths.length > 0 && plan.unitPaths.every(exists);
|
|
16802
|
-
}
|
|
16803
|
-
|
|
16804
17257
|
// src/worktree-sweep.ts
|
|
16805
|
-
import { existsSync as
|
|
17258
|
+
import { existsSync as existsSync33, readFileSync as readFileSync30, readdirSync as readdirSync12 } from "fs";
|
|
16806
17259
|
import { basename as basename11, dirname as dirname19, join as join35, resolve as resolve4 } from "path";
|
|
16807
17260
|
function planWorktreeSweep(target, tickets, contract) {
|
|
16808
17261
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
@@ -16915,7 +17368,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
16915
17368
|
let removed = 0;
|
|
16916
17369
|
let keptBranches = 0;
|
|
16917
17370
|
for (const a of actions) {
|
|
16918
|
-
if (!dryRun && base && !a.keepBranch &&
|
|
17371
|
+
if (!dryRun && base && !a.keepBranch && existsSync33(a.path) && isHandBackMerge(a.path, base)) {
|
|
16919
17372
|
if (gitOk(a.path, ["merge", "--abort"]) !== null) {
|
|
16920
17373
|
log.emit(
|
|
16921
17374
|
`abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
|
|
@@ -16923,7 +17376,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
16923
17376
|
);
|
|
16924
17377
|
}
|
|
16925
17378
|
}
|
|
16926
|
-
if (!dryRun &&
|
|
17379
|
+
if (!dryRun && existsSync33(a.path)) {
|
|
16927
17380
|
let dirty = false;
|
|
16928
17381
|
try {
|
|
16929
17382
|
dirty = status(a.path).length > 0;
|
|
@@ -16934,7 +17387,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
16934
17387
|
continue;
|
|
16935
17388
|
}
|
|
16936
17389
|
}
|
|
16937
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
17390
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync33(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
16938
17391
|
refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
|
|
16939
17392
|
continue;
|
|
16940
17393
|
}
|
|
@@ -16983,7 +17436,7 @@ function isHandBackMerge(path, base) {
|
|
|
16983
17436
|
if (!msgPath) return false;
|
|
16984
17437
|
let first;
|
|
16985
17438
|
try {
|
|
16986
|
-
first =
|
|
17439
|
+
first = readFileSync30(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
16987
17440
|
} catch {
|
|
16988
17441
|
return false;
|
|
16989
17442
|
}
|
|
@@ -17088,12 +17541,12 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
17088
17541
|
}
|
|
17089
17542
|
|
|
17090
17543
|
// src/stream-sweep.ts
|
|
17091
|
-
import { existsSync as
|
|
17544
|
+
import { existsSync as existsSync34, readdirSync as readdirSync13, rmSync as rmSync15, statSync as statSync10 } from "fs";
|
|
17092
17545
|
import { join as join36 } from "path";
|
|
17093
17546
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
17094
17547
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
17095
17548
|
const dir = join36(stateDir, "streams");
|
|
17096
|
-
if (!
|
|
17549
|
+
if (!existsSync34(dir)) return [];
|
|
17097
17550
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
17098
17551
|
return readdirSync13(dir).map((name) => join36(dir, name)).filter((path) => {
|
|
17099
17552
|
try {
|
|
@@ -17121,7 +17574,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
17121
17574
|
return removed;
|
|
17122
17575
|
}
|
|
17123
17576
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
17124
|
-
if (!
|
|
17577
|
+
if (!existsSync34(stateDir)) return [];
|
|
17125
17578
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
17126
17579
|
return readdirSync13(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join36(stateDir, name)).filter((path) => {
|
|
17127
17580
|
try {
|
|
@@ -17133,11 +17586,11 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
17133
17586
|
}
|
|
17134
17587
|
|
|
17135
17588
|
// src/cli.ts
|
|
17136
|
-
import { realpathSync as realpathSync3, readFileSync as
|
|
17589
|
+
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";
|
|
17137
17590
|
import { createInterface } from "readline/promises";
|
|
17138
17591
|
|
|
17139
17592
|
// src/periscope-cmd.ts
|
|
17140
|
-
import { existsSync as
|
|
17593
|
+
import { existsSync as existsSync35, mkdirSync as mkdirSync21, readFileSync as readFileSync31, rmSync as rmSync16, writeFileSync as writeFileSync24 } from "fs";
|
|
17141
17594
|
import { join as join37 } from "path";
|
|
17142
17595
|
var PeriscopeError = class extends Error {
|
|
17143
17596
|
};
|
|
@@ -17149,14 +17602,14 @@ function removePeriscopePrior(stateDir, ticketRecordId) {
|
|
|
17149
17602
|
function writePeriscopePrior(stateDir, ticketRecordId, prior) {
|
|
17150
17603
|
try {
|
|
17151
17604
|
mkdirSync21(priorDir(stateDir), { recursive: true });
|
|
17152
|
-
|
|
17605
|
+
writeFileSync24(priorPath(stateDir, ticketRecordId), JSON.stringify(prior));
|
|
17153
17606
|
} catch {
|
|
17154
17607
|
}
|
|
17155
17608
|
}
|
|
17156
17609
|
function readPeriscopePrior(stateDir, ticketRecordId) {
|
|
17157
17610
|
try {
|
|
17158
17611
|
const p = priorPath(stateDir, ticketRecordId);
|
|
17159
|
-
return
|
|
17612
|
+
return existsSync35(p) ? JSON.parse(readFileSync31(p, "utf8")) : void 0;
|
|
17160
17613
|
} catch {
|
|
17161
17614
|
return void 0;
|
|
17162
17615
|
}
|
|
@@ -17646,7 +18099,7 @@ import { dirname as dirOf2, resolve as resolvePath } from "path";
|
|
|
17646
18099
|
|
|
17647
18100
|
// src/ssh-keys.ts
|
|
17648
18101
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
17649
|
-
import { existsSync as
|
|
18102
|
+
import { existsSync as existsSync36, mkdirSync as mkdirSync22, readFileSync as readFileSync32, chmodSync as chmodSync3 } from "fs";
|
|
17650
18103
|
import { dirname as dirname20, join as join39 } from "path";
|
|
17651
18104
|
var SshKeyError = class extends Error {
|
|
17652
18105
|
};
|
|
@@ -17656,7 +18109,7 @@ function shipSshKeyPathFor(stateDir) {
|
|
|
17656
18109
|
function ensureShipSshKeypair(stateDir) {
|
|
17657
18110
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
17658
18111
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
17659
|
-
if (!
|
|
18112
|
+
if (!existsSync36(privateKeyPath)) {
|
|
17660
18113
|
mkdirSync22(dirname20(privateKeyPath), { recursive: true });
|
|
17661
18114
|
try {
|
|
17662
18115
|
execFileSync8("ssh-keygen", [
|
|
@@ -17677,10 +18130,10 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
17677
18130
|
}
|
|
17678
18131
|
chmodSync3(privateKeyPath, 384);
|
|
17679
18132
|
}
|
|
17680
|
-
if (!
|
|
18133
|
+
if (!existsSync36(publicKeyPath)) {
|
|
17681
18134
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
17682
18135
|
}
|
|
17683
|
-
const publicKey =
|
|
18136
|
+
const publicKey = readFileSync32(publicKeyPath, "utf8").trim();
|
|
17684
18137
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
17685
18138
|
}
|
|
17686
18139
|
function sshKeygenAvailable() {
|
|
@@ -17897,7 +18350,7 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
17897
18350
|
|
|
17898
18351
|
// src/tunnel.ts
|
|
17899
18352
|
import { spawn as spawn5 } from "child_process";
|
|
17900
|
-
import { existsSync as
|
|
18353
|
+
import { existsSync as existsSync37, mkdirSync as mkdirSync23, readFileSync as readFileSync33, writeFileSync as writeFileSync25, unlinkSync as unlinkSync3, readdirSync as readdirSync14 } from "fs";
|
|
17901
18354
|
import { randomInt } from "crypto";
|
|
17902
18355
|
import { join as join40 } from "path";
|
|
17903
18356
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
@@ -17970,9 +18423,9 @@ function tunnelStatePath(stateDir, workspaceId) {
|
|
|
17970
18423
|
}
|
|
17971
18424
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
17972
18425
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
17973
|
-
if (!
|
|
18426
|
+
if (!existsSync37(path)) return void 0;
|
|
17974
18427
|
try {
|
|
17975
|
-
return JSON.parse(
|
|
18428
|
+
return JSON.parse(readFileSync33(path, "utf8"));
|
|
17976
18429
|
} catch {
|
|
17977
18430
|
return void 0;
|
|
17978
18431
|
}
|
|
@@ -17980,7 +18433,7 @@ function readPersistedTunnel(stateDir, workspaceId) {
|
|
|
17980
18433
|
function writePersistedTunnel(stateDir, state) {
|
|
17981
18434
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
17982
18435
|
mkdirSync23(join40(stateDir, "passengers"), { recursive: true });
|
|
17983
|
-
|
|
18436
|
+
writeFileSync25(path, `${JSON.stringify(state, null, 2)}
|
|
17984
18437
|
`);
|
|
17985
18438
|
}
|
|
17986
18439
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -18078,7 +18531,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
18078
18531
|
}
|
|
18079
18532
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
18080
18533
|
const dir = join40(stateDir, "passengers");
|
|
18081
|
-
if (!
|
|
18534
|
+
if (!existsSync37(dir)) return [];
|
|
18082
18535
|
return readdirSync14(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
18083
18536
|
}
|
|
18084
18537
|
|
|
@@ -18199,7 +18652,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
18199
18652
|
import { createServer as createHttpServer } from "http";
|
|
18200
18653
|
import { spawn as spawn6 } from "child_process";
|
|
18201
18654
|
import { randomBytes } from "crypto";
|
|
18202
|
-
import { existsSync as
|
|
18655
|
+
import { existsSync as existsSync38, mkdirSync as mkdirSync24, readFileSync as readFileSync34, writeFileSync as writeFileSync26, unlinkSync as unlinkSync4, readdirSync as readdirSync15 } from "fs";
|
|
18203
18656
|
import { join as join41 } from "path";
|
|
18204
18657
|
|
|
18205
18658
|
// src/secret-compare.ts
|
|
@@ -18293,9 +18746,9 @@ function syncDaemonStatePath(stateDir, workspaceId) {
|
|
|
18293
18746
|
}
|
|
18294
18747
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
18295
18748
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
18296
|
-
if (!
|
|
18749
|
+
if (!existsSync38(path)) return void 0;
|
|
18297
18750
|
try {
|
|
18298
|
-
return JSON.parse(
|
|
18751
|
+
return JSON.parse(readFileSync34(path, "utf8"));
|
|
18299
18752
|
} catch {
|
|
18300
18753
|
return void 0;
|
|
18301
18754
|
}
|
|
@@ -18303,7 +18756,7 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
|
18303
18756
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
18304
18757
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
18305
18758
|
mkdirSync24(join41(stateDir, "passengers"), { recursive: true });
|
|
18306
|
-
|
|
18759
|
+
writeFileSync26(path, `${JSON.stringify(state, null, 2)}
|
|
18307
18760
|
`);
|
|
18308
18761
|
}
|
|
18309
18762
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -18324,7 +18777,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
18324
18777
|
}
|
|
18325
18778
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
18326
18779
|
const dir = join41(stateDir, "passengers");
|
|
18327
|
-
if (!
|
|
18780
|
+
if (!existsSync38(dir)) return [];
|
|
18328
18781
|
const suffix = "-sync.json";
|
|
18329
18782
|
return readdirSync15(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
18330
18783
|
}
|
|
@@ -18412,7 +18865,7 @@ function describeCause(err3) {
|
|
|
18412
18865
|
}
|
|
18413
18866
|
|
|
18414
18867
|
// src/daemon.ts
|
|
18415
|
-
import { existsSync as
|
|
18868
|
+
import { existsSync as existsSync39, mkdirSync as mkdirSync25, readFileSync as readFileSync35, readdirSync as readdirSync16, rmSync as rmSync17, statSync as statSync12, writeFileSync as writeFileSync27 } from "fs";
|
|
18416
18869
|
import { join as join42 } from "path";
|
|
18417
18870
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
18418
18871
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
@@ -18493,7 +18946,7 @@ function computeTreeSignature(paths) {
|
|
|
18493
18946
|
}
|
|
18494
18947
|
function makeStaleChecker(paths, opts = {}) {
|
|
18495
18948
|
const initial = computeTreeSignature(paths);
|
|
18496
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
18949
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync39(opts.reloadSentinel);
|
|
18497
18950
|
}
|
|
18498
18951
|
function reloadSentinelFor(stateDir) {
|
|
18499
18952
|
return join42(stateDir, "reload-requested");
|
|
@@ -18504,12 +18957,12 @@ function daemonStampFor(stateDir) {
|
|
|
18504
18957
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
18505
18958
|
mkdirSync25(stateDir, { recursive: true });
|
|
18506
18959
|
rmSync17(reloadSentinelFor(stateDir), { force: true });
|
|
18507
|
-
|
|
18960
|
+
writeFileSync27(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
18508
18961
|
`);
|
|
18509
18962
|
}
|
|
18510
18963
|
function readDaemonStarted(stateDir) {
|
|
18511
18964
|
try {
|
|
18512
|
-
const v = JSON.parse(
|
|
18965
|
+
const v = JSON.parse(readFileSync35(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
18513
18966
|
return typeof v === "number" ? v : null;
|
|
18514
18967
|
} catch {
|
|
18515
18968
|
return null;
|
|
@@ -18782,7 +19235,7 @@ async function reconcileForRoute(ctx, o) {
|
|
|
18782
19235
|
if (wt) dirty = status(wt.path).length > 0;
|
|
18783
19236
|
} catch {
|
|
18784
19237
|
}
|
|
18785
|
-
await tracker.postEvent(sc.ticketRecordId, interruptedComment({
|
|
19238
|
+
await tracker.postEvent(sc.ticketRecordId, result.killed ? runKilledComment({ limitMinutes: result.killed.limitMinutes, branch: wt?.branch ?? null, dirty }) : interruptedComment({
|
|
18786
19239
|
branch: wt?.branch ?? null,
|
|
18787
19240
|
dirty,
|
|
18788
19241
|
reason: result.interrupted === "daemon-exit" ? "the crew daemon exited" : "it exited without a result"
|
|
@@ -18805,6 +19258,23 @@ async function reconcileForRoute(ctx, o) {
|
|
|
18805
19258
|
}
|
|
18806
19259
|
});
|
|
18807
19260
|
}
|
|
19261
|
+
async function postRunKilled(route2, tracker, ticketId, memberId, limitMinutes, emit) {
|
|
19262
|
+
if (!ticketId || !memberId) return;
|
|
19263
|
+
try {
|
|
19264
|
+
const t = await tracker.ticket(ticketId);
|
|
19265
|
+
const target = repoTargetFor(route2, t.repo_id);
|
|
19266
|
+
const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
|
|
19267
|
+
const wt = target && num ? worktreeForNumber(target.dir, num) : null;
|
|
19268
|
+
let dirty = null;
|
|
19269
|
+
try {
|
|
19270
|
+
if (wt) dirty = status(wt.path).length > 0;
|
|
19271
|
+
} catch {
|
|
19272
|
+
}
|
|
19273
|
+
await tracker.postEvent(ticketId, runKilledComment({ limitMinutes, branch: wt?.branch ?? null, dirty }), memberId, "interrupted");
|
|
19274
|
+
} catch (e) {
|
|
19275
|
+
emit.warn(`could not post the wall-clock limit comment: ${e.message}`, { step: "agent" });
|
|
19276
|
+
}
|
|
19277
|
+
}
|
|
18808
19278
|
async function runRoleAgent(role, decision, ctx, hooks) {
|
|
18809
19279
|
const held = {};
|
|
18810
19280
|
try {
|
|
@@ -18909,7 +19379,7 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
|
|
|
18909
19379
|
const target = repoTargetFor(route2, result.ticket.repo_id);
|
|
18910
19380
|
ticketRepo = target?.name;
|
|
18911
19381
|
try {
|
|
18912
|
-
if (target && ensureRepoCheckout(target.dir, target.remote)) {
|
|
19382
|
+
if (target && ensureRepoCheckout(target.dir, target.remote, { scheme: ship.cloneScheme })) {
|
|
18913
19383
|
emit.emit(`cloned ${target.name} into ${target.dir}`, { step: "select", role });
|
|
18914
19384
|
}
|
|
18915
19385
|
} catch (e) {
|
|
@@ -18967,8 +19437,12 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
|
|
|
18967
19437
|
} catch {
|
|
18968
19438
|
}
|
|
18969
19439
|
}
|
|
18970
|
-
await spawnAgent(plan, emit, hintMoved);
|
|
18971
|
-
|
|
19440
|
+
const result = await spawnAgent(plan, emit, hintMoved);
|
|
19441
|
+
if (result.killed) {
|
|
19442
|
+
await postRunKilled(route2, tracker, workingId, memberId, result.killed.limitMinutes, emit);
|
|
19443
|
+
} else {
|
|
19444
|
+
await guardRunHandoff(route2, ship, tracker, role, workingId, emit, runStart);
|
|
19445
|
+
}
|
|
18972
19446
|
} finally {
|
|
18973
19447
|
await settleRunEnd({
|
|
18974
19448
|
tracker,
|
|
@@ -19010,7 +19484,7 @@ import { createHash as createHash9 } from "crypto";
|
|
|
19010
19484
|
import {
|
|
19011
19485
|
chmodSync as chmodSync4,
|
|
19012
19486
|
closeSync as closeSync5,
|
|
19013
|
-
existsSync as
|
|
19487
|
+
existsSync as existsSync40,
|
|
19014
19488
|
fstatSync,
|
|
19015
19489
|
openSync as openSync5,
|
|
19016
19490
|
readSync as readSync3,
|
|
@@ -19361,7 +19835,7 @@ function lastLines2(path, n) {
|
|
|
19361
19835
|
}
|
|
19362
19836
|
function readTail(live, opts) {
|
|
19363
19837
|
const max = opts.maxBytes ?? MAX_TAIL_BYTES;
|
|
19364
|
-
if (!
|
|
19838
|
+
if (!existsSync40(live)) {
|
|
19365
19839
|
return { lines: [], cursor: opts.after ?? encodeCursor({ dev: 0, ino: 0, off: 0 }), reset: false };
|
|
19366
19840
|
}
|
|
19367
19841
|
const cur = opts.after ? decodeCursor(opts.after) : null;
|
|
@@ -19518,7 +19992,7 @@ async function route(deps, method, url, req, abort) {
|
|
|
19518
19992
|
const run2 = decodeURIComponent(p.slice("/streams/".length));
|
|
19519
19993
|
if (!RUN_NAME_RE.test(run2) || run2.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run2)) return err2(400, "not a run name");
|
|
19520
19994
|
const file = join43(deps.stateDir, "streams", run2.endsWith(".jsonl") ? run2 : `${run2}.jsonl`);
|
|
19521
|
-
if (!
|
|
19995
|
+
if (!existsSync40(file)) return err2(404, "no such run");
|
|
19522
19996
|
return tailReply(file, url.searchParams, false, abort);
|
|
19523
19997
|
}
|
|
19524
19998
|
if ((p === "/pause" || p === "/resume") && post3) {
|
|
@@ -19562,7 +20036,7 @@ function send(res, r) {
|
|
|
19562
20036
|
res.end(body);
|
|
19563
20037
|
}
|
|
19564
20038
|
async function socketIsStale(path) {
|
|
19565
|
-
if (!
|
|
20039
|
+
if (!existsSync40(path)) return false;
|
|
19566
20040
|
return new Promise((resolve6) => {
|
|
19567
20041
|
const c = createConnection(path);
|
|
19568
20042
|
c.once("connect", () => {
|
|
@@ -19581,7 +20055,7 @@ async function startControlServer(deps) {
|
|
|
19581
20055
|
return null;
|
|
19582
20056
|
}
|
|
19583
20057
|
if (await socketIsStale(path)) rmSync18(path, { force: true });
|
|
19584
|
-
else if (
|
|
20058
|
+
else if (existsSync40(path)) {
|
|
19585
20059
|
log(`control API not started: ${path} is in use by another process`);
|
|
19586
20060
|
return null;
|
|
19587
20061
|
}
|
|
@@ -19790,6 +20264,9 @@ function usage(problem, requested = false) {
|
|
|
19790
20264
|
crew --version [--json] print the crew version on one line (also: crew version); --json adds home and compiled
|
|
19791
20265
|
crew status [route] [--json] paused/running state; --json for a machine reader
|
|
19792
20266
|
crew projects [--route R] [--json] the Projects rows each route can read (id, name, prefix, work mode)
|
|
20267
|
+
crew provision --key K --route WS[/PROJECT] [--forge-host H] [--ship-name N] [--json] one non-interactive run: connect, forge tokens,
|
|
20268
|
+
enable routes, install units with lingering, start the daemon; safe to re-run (CREW_CONNECT_KEY,
|
|
20269
|
+
CREW_PROVISION_ROUTES, CREW_SHIP_NAME, CREW_FORGE_TOKEN[_<HOST>] also read)
|
|
19793
20270
|
crew forge [set|rotate <host>] [--token T | --token-stdin] [--source file|keychain|env] this ship's git-forge tokens (ship.forges)
|
|
19794
20271
|
crew credential get|store|erase git credential helper answering from ship.forges (git runs it; not for hand use)
|
|
19795
20272
|
crew whoami [route] [--json] the signed-in identity, key role, operator Crew row and credential source per route
|
|
@@ -19890,6 +20367,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
19890
20367
|
"agents",
|
|
19891
20368
|
"connect",
|
|
19892
20369
|
"credential",
|
|
20370
|
+
"provision",
|
|
19893
20371
|
"forge",
|
|
19894
20372
|
"daemon",
|
|
19895
20373
|
"deploy",
|
|
@@ -19965,7 +20443,7 @@ function daemonConfigPaths(c) {
|
|
|
19965
20443
|
for (const r of c.routes) if (r.apiKeyFile && !files.includes(r.apiKeyFile)) files.push(r.apiKeyFile);
|
|
19966
20444
|
return files;
|
|
19967
20445
|
}
|
|
19968
|
-
var STATE_WRITING_COMMANDS = /* @__PURE__ */ new Set(["connect", "disconnect", "install", "uninstall", "reload", "release", "merge", "deploy"]);
|
|
20446
|
+
var STATE_WRITING_COMMANDS = /* @__PURE__ */ new Set(["connect", "provision", "disconnect", "install", "uninstall", "reload", "release", "merge", "deploy"]);
|
|
19969
20447
|
var STATE_WRITING_SUBCOMMANDS = {
|
|
19970
20448
|
agents: /* @__PURE__ */ new Set(["sync"]),
|
|
19971
20449
|
skills: /* @__PURE__ */ new Set(["sync"])
|
|
@@ -19979,6 +20457,19 @@ if (sessionRole && (STATE_WRITING_COMMANDS.has(command) || STATE_WRITING_SUBCOMM
|
|
|
19979
20457
|
);
|
|
19980
20458
|
process.exit(2);
|
|
19981
20459
|
}
|
|
20460
|
+
if (command === "provision") {
|
|
20461
|
+
const outcome = await runProvision(
|
|
20462
|
+
provisionOptionsFrom(argv, process.env),
|
|
20463
|
+
realProvisionDeps(CREW_HOME),
|
|
20464
|
+
(l) => {
|
|
20465
|
+
process.stdout.write(l);
|
|
20466
|
+
},
|
|
20467
|
+
(l) => {
|
|
20468
|
+
process.stderr.write(l);
|
|
20469
|
+
}
|
|
20470
|
+
);
|
|
20471
|
+
process.exit(outcome.code);
|
|
20472
|
+
}
|
|
19982
20473
|
if (command === "passenger-sync-daemon") {
|
|
19983
20474
|
runSyncDaemonFromEnv();
|
|
19984
20475
|
} else {
|
|
@@ -20056,7 +20547,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20056
20547
|
};
|
|
20057
20548
|
if (command === "connect" && !flag("sign-out") && usesFlagAnswers(wizardFlags, connectIsTTY) && shouldRunWizard({ isTTY: true, crewHome: CREW_HOME })) {
|
|
20058
20549
|
const flagged = flagWizardAnswers(wizardFlags, {
|
|
20059
|
-
hostname:
|
|
20550
|
+
hostname: hostname3(),
|
|
20060
20551
|
platform: hostPlatform(),
|
|
20061
20552
|
pathEnv: process.env.PATH ?? "",
|
|
20062
20553
|
nodePath: process.execPath,
|
|
@@ -20081,7 +20572,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20081
20572
|
let answers;
|
|
20082
20573
|
try {
|
|
20083
20574
|
answers = await runWizard(prompter, {
|
|
20084
|
-
hostname:
|
|
20575
|
+
hostname: hostname3(),
|
|
20085
20576
|
platform: hostPlatform(),
|
|
20086
20577
|
pathEnv: process.env.PATH ?? "",
|
|
20087
20578
|
nodePath: process.execPath,
|
|
@@ -20096,7 +20587,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20096
20587
|
} else {
|
|
20097
20588
|
const toleratesNoRoutes = command === "credential" || command === "forge" || command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
|
|
20098
20589
|
const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
|
|
20099
|
-
if (configless && !
|
|
20590
|
+
if (configless && !existsSync41(findConfigFile(CREW_HOME))) {
|
|
20100
20591
|
cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
|
|
20101
20592
|
} else {
|
|
20102
20593
|
cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
|
|
@@ -20111,7 +20602,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20111
20602
|
throw e;
|
|
20112
20603
|
}
|
|
20113
20604
|
if (command === "credential") {
|
|
20114
|
-
const r = runCredential(positional[1],
|
|
20605
|
+
const r = runCredential(positional[1], readFileSync36(0, "utf8"), cfg.ship.forges, cfg.ship.stateDir);
|
|
20115
20606
|
process.stdout.write(r.out);
|
|
20116
20607
|
process.stderr.write(r.err);
|
|
20117
20608
|
process.exit(r.code);
|
|
@@ -20123,9 +20614,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
20123
20614
|
forges: cfg.ship.forges,
|
|
20124
20615
|
stateDir: cfg.ship.stateDir,
|
|
20125
20616
|
configFile: cfg.configFile,
|
|
20126
|
-
readFile: (p) =>
|
|
20127
|
-
writeFile: (p, t) =>
|
|
20128
|
-
stdin: () =>
|
|
20617
|
+
readFile: (p) => readFileSync36(p, "utf8"),
|
|
20618
|
+
writeFile: (p, t) => writeFileSync28(p, t),
|
|
20619
|
+
stdin: () => readFileSync36(0, "utf8"),
|
|
20129
20620
|
env: process.env
|
|
20130
20621
|
}
|
|
20131
20622
|
);
|
|
@@ -20349,7 +20840,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20349
20840
|
}
|
|
20350
20841
|
const hasSystemd = detectSystemd();
|
|
20351
20842
|
for (const job of ["run", "release", "passengers"]) {
|
|
20352
|
-
const plan = planInstall(cfg.ship, CREW_HOME, host, hasSystemd, job);
|
|
20843
|
+
const plan = planInstall(cfg.ship, CREW_HOME, host, hasSystemd, job, installContext());
|
|
20353
20844
|
process.stdout.write(`installing ${job} via ${plan.mechanism} for ${host}${dryRun ? " (dry run)" : ""}
|
|
20354
20845
|
`);
|
|
20355
20846
|
await applyInstall(plan, CREW_HOME, dryRun, {
|
|
@@ -20362,13 +20853,13 @@ if (command === "passenger-sync-daemon") {
|
|
|
20362
20853
|
if (host === "linux" && hasSystemd) {
|
|
20363
20854
|
const label = labelFor(CREW_HOME, "run");
|
|
20364
20855
|
const staleTimer = join44(homedir10(), ".config", "systemd", "user", `${label}.timer`);
|
|
20365
|
-
if (
|
|
20856
|
+
if (existsSync41(staleTimer)) {
|
|
20366
20857
|
process.stdout.write(
|
|
20367
20858
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
20368
20859
|
`
|
|
20369
20860
|
);
|
|
20370
20861
|
if (!dryRun) {
|
|
20371
|
-
|
|
20862
|
+
spawnSync8("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
|
|
20372
20863
|
try {
|
|
20373
20864
|
unlinkSync5(staleTimer);
|
|
20374
20865
|
} catch {
|
|
@@ -20389,7 +20880,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20389
20880
|
const tracker = new Tracker(c, cfg.ship);
|
|
20390
20881
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
20391
20882
|
for (const r of await resolvedRepos(c)) {
|
|
20392
|
-
if (!
|
|
20883
|
+
if (!existsSync41(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
20393
20884
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
20394
20885
|
readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
20395
20886
|
}
|
|
@@ -20407,9 +20898,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
20407
20898
|
}
|
|
20408
20899
|
async function doctorFix(o) {
|
|
20409
20900
|
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))));
|
|
20410
|
-
const unitExists = (p) =>
|
|
20411
|
-
const crontab = o.host === "linux" ?
|
|
20412
|
-
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
20901
|
+
const unitExists = (p) => existsSync41(p);
|
|
20902
|
+
const crontab = o.host === "linux" ? spawnSync8("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
20903
|
+
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run", installContext()), unitExists, crontab) : true;
|
|
20413
20904
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
20414
20905
|
for (const n of plan.notes) process.stdout.write(`fix: ${n}
|
|
20415
20906
|
`);
|
|
@@ -20424,7 +20915,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
20424
20915
|
for (const a of plan.actions) {
|
|
20425
20916
|
if (a.kind === "enable") {
|
|
20426
20917
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
20427
|
-
|
|
20918
|
+
writeFileSync28(cfg.configFile, planEnable(readFileSync36(cfg.configFile, "utf8"), a.route));
|
|
20428
20919
|
enabledSound.add(a.route);
|
|
20429
20920
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
20430
20921
|
} else {
|
|
@@ -21166,7 +21657,7 @@ ${cu.tail.trimEnd()}
|
|
|
21166
21657
|
const target = repoTargetFor(w.route, result.ticket.repo_id);
|
|
21167
21658
|
fleetTicketRepo = target?.name;
|
|
21168
21659
|
try {
|
|
21169
|
-
if (target && ensureRepoCheckout(target.dir, target.remote)) {
|
|
21660
|
+
if (target && ensureRepoCheckout(target.dir, target.remote, { scheme: cfg.ship.cloneScheme })) {
|
|
21170
21661
|
wemit.emit(`cloned ${target.name} into ${target.dir}`, { step: "select", role: w.role });
|
|
21171
21662
|
}
|
|
21172
21663
|
} catch (e) {
|
|
@@ -21383,7 +21874,7 @@ ${cu.tail.trimEnd()}
|
|
|
21383
21874
|
const target = repoTargetFor(route2, result.ticket.repo_id);
|
|
21384
21875
|
ticketRepo = target?.name;
|
|
21385
21876
|
try {
|
|
21386
|
-
if (target && ensureRepoCheckout(target.dir, target.remote)) {
|
|
21877
|
+
if (target && ensureRepoCheckout(target.dir, target.remote, { scheme: cfg.ship.cloneScheme })) {
|
|
21387
21878
|
emit.emit(`cloned ${target.name} into ${target.dir}`, { step: "select", role: current });
|
|
21388
21879
|
}
|
|
21389
21880
|
} catch (e) {
|
|
@@ -21504,7 +21995,7 @@ ${cu.tail.trimEnd()}
|
|
|
21504
21995
|
}
|
|
21505
21996
|
let failed = false;
|
|
21506
21997
|
for (const cmd2 of commands) {
|
|
21507
|
-
const res2 =
|
|
21998
|
+
const res2 = spawnSync8(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
21508
21999
|
if (res2.stdout) process.stdout.write(res2.stdout);
|
|
21509
22000
|
if (res2.stderr) process.stderr.write(res2.stderr);
|
|
21510
22001
|
if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
|
|
@@ -21522,7 +22013,7 @@ ${cu.tail.trimEnd()}
|
|
|
21522
22013
|
`);
|
|
21523
22014
|
break;
|
|
21524
22015
|
}
|
|
21525
|
-
const res =
|
|
22016
|
+
const res = spawnSync8(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
21526
22017
|
if (res.stdout) process.stdout.write(res.stdout);
|
|
21527
22018
|
if (res.stderr) process.stderr.write(res.stderr);
|
|
21528
22019
|
if (daemonSub !== "status" && (res.error || res.status !== 0)) {
|
|
@@ -21806,10 +22297,10 @@ ${cu.tail.trimEnd()}
|
|
|
21806
22297
|
`);
|
|
21807
22298
|
if (!dryRun) {
|
|
21808
22299
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
21809
|
-
const raw =
|
|
22300
|
+
const raw = existsSync41(resolvedPath) ? JSON.parse(readFileSync36(resolvedPath, "utf8")) : {};
|
|
21810
22301
|
raw.agentSkills = result.agentSkills;
|
|
21811
22302
|
mkdirSync26(dirOf2(resolvedPath), { recursive: true });
|
|
21812
|
-
|
|
22303
|
+
writeFileSync28(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
21813
22304
|
`);
|
|
21814
22305
|
}
|
|
21815
22306
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -21832,7 +22323,7 @@ ${cu.tail.trimEnd()}
|
|
|
21832
22323
|
throw e;
|
|
21833
22324
|
}
|
|
21834
22325
|
const oldName = cfg.ship.name;
|
|
21835
|
-
|
|
22326
|
+
writeFileSync28(cfg.configFile, setShipNameInYaml(readFileSync36(cfg.configFile, "utf8"), newName));
|
|
21836
22327
|
cfg.ship.name = newName;
|
|
21837
22328
|
let renameMachineId;
|
|
21838
22329
|
try {
|
|
@@ -21879,7 +22370,7 @@ ${cu.tail.trimEnd()}
|
|
|
21879
22370
|
process.exit(2);
|
|
21880
22371
|
};
|
|
21881
22372
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
|
|
21882
|
-
const configText =
|
|
22373
|
+
const configText = readFileSync36(cfg.configFile, "utf8");
|
|
21883
22374
|
if (sub === "check") {
|
|
21884
22375
|
try {
|
|
21885
22376
|
const routeArg = value("route") ?? positional[2];
|
|
@@ -21910,8 +22401,8 @@ ${cu.tail.trimEnd()}
|
|
|
21910
22401
|
try {
|
|
21911
22402
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
21912
22403
|
const dir = repoDirOf(configText, dirname22(cfg.configFile), a.route, a.name);
|
|
21913
|
-
if (!
|
|
21914
|
-
const files = { exists: (r) =>
|
|
22404
|
+
if (!existsSync41(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
22405
|
+
const files = { exists: (r) => existsSync41(resolvePath(dir, r)), list: (r) => {
|
|
21915
22406
|
try {
|
|
21916
22407
|
return readdirSync18(resolvePath(dir, r));
|
|
21917
22408
|
} catch {
|
|
@@ -21919,7 +22410,7 @@ ${cu.tail.trimEnd()}
|
|
|
21919
22410
|
}
|
|
21920
22411
|
}, read: (r) => {
|
|
21921
22412
|
try {
|
|
21922
|
-
return
|
|
22413
|
+
return readFileSync36(resolvePath(dir, r), "utf8");
|
|
21923
22414
|
} catch {
|
|
21924
22415
|
return void 0;
|
|
21925
22416
|
}
|
|
@@ -21961,19 +22452,19 @@ ${cu.tail.trimEnd()}
|
|
|
21961
22452
|
let cleared;
|
|
21962
22453
|
if (clear2) {
|
|
21963
22454
|
const plan = clearRouteHooks(configText, a.route, a.name);
|
|
21964
|
-
if (plan.removed.length > 0)
|
|
22455
|
+
if (plan.removed.length > 0) writeFileSync28(cfg.configFile, plan.text);
|
|
21965
22456
|
cleared = { file: cfg.configFile, removed: plan.removed };
|
|
21966
22457
|
}
|
|
21967
22458
|
if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
21968
22459
|
if (write === "route") {
|
|
21969
22460
|
const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
|
|
21970
|
-
|
|
22461
|
+
writeFileSync28(cfg.configFile, plan.text);
|
|
21971
22462
|
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
|
|
21972
22463
|
} else if (write === "repo") {
|
|
21973
22464
|
const have = existingRepoConfig(files);
|
|
21974
22465
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
21975
22466
|
const file = resolvePath(dir, ".crew.yaml");
|
|
21976
|
-
|
|
22467
|
+
writeFileSync28(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
|
|
21977
22468
|
written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
|
|
21978
22469
|
}
|
|
21979
22470
|
if (flag("json")) {
|
|
@@ -22029,7 +22520,7 @@ ${cu.tail.trimEnd()}
|
|
|
22029
22520
|
if (sub === "list" && !routeName) usage2();
|
|
22030
22521
|
if (sub === "add") {
|
|
22031
22522
|
try {
|
|
22032
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
22523
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync41(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
|
|
22033
22524
|
routeName = a.route;
|
|
22034
22525
|
addPath = a.path;
|
|
22035
22526
|
addName = a.name;
|
|
@@ -22058,16 +22549,16 @@ ${cu.tail.trimEnd()}
|
|
|
22058
22549
|
}
|
|
22059
22550
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
22060
22551
|
let resolvedRemotes;
|
|
22061
|
-
if (
|
|
22552
|
+
if (existsSync41(resolvedFile)) {
|
|
22062
22553
|
try {
|
|
22063
|
-
resolvedRemotes = JSON.parse(
|
|
22554
|
+
resolvedRemotes = JSON.parse(readFileSync36(resolvedFile, "utf8"));
|
|
22064
22555
|
} catch {
|
|
22065
22556
|
}
|
|
22066
22557
|
}
|
|
22067
22558
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
22068
22559
|
const text = configText;
|
|
22069
22560
|
const base = dirname22(cfg.configFile);
|
|
22070
|
-
const isCheckout = (d) =>
|
|
22561
|
+
const isCheckout = (d) => existsSync41(`${d}/.git`);
|
|
22071
22562
|
try {
|
|
22072
22563
|
if (sub === "list") {
|
|
22073
22564
|
if (flag("json")) {
|
|
@@ -22077,11 +22568,11 @@ ${cu.tail.trimEnd()}
|
|
|
22077
22568
|
base,
|
|
22078
22569
|
resolved: resolvedRemotes,
|
|
22079
22570
|
originOf,
|
|
22080
|
-
exists: (d) =>
|
|
22571
|
+
exists: (d) => existsSync41(d),
|
|
22081
22572
|
isCheckout,
|
|
22082
22573
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
22083
22574
|
try {
|
|
22084
|
-
return Object.keys(parseRepoConfig(
|
|
22575
|
+
return Object.keys(parseRepoConfig(readFileSync36(resolvePath(d, f), "utf8"), f).hooks).length > 0;
|
|
22085
22576
|
} catch {
|
|
22086
22577
|
return false;
|
|
22087
22578
|
}
|
|
@@ -22098,7 +22589,7 @@ ${cu.tail.trimEnd()}
|
|
|
22098
22589
|
for (const r of repos) {
|
|
22099
22590
|
const origin2 = originOf(r.dir);
|
|
22100
22591
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
22101
|
-
const note = !
|
|
22592
|
+
const note = !existsSync41(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
22102
22593
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
22103
22594
|
`);
|
|
22104
22595
|
}
|
|
@@ -22110,9 +22601,9 @@ ${cu.tail.trimEnd()}
|
|
|
22110
22601
|
` : `${human}
|
|
22111
22602
|
`);
|
|
22112
22603
|
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
|
|
22113
|
-
if (
|
|
22604
|
+
if (existsSync41(plan2.target) && (existsSync41(`${plan2.target}/.git`) || readdirSync18(plan2.target).length > 0)) {
|
|
22114
22605
|
throw new ReposError(
|
|
22115
|
-
|
|
22606
|
+
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`
|
|
22116
22607
|
);
|
|
22117
22608
|
}
|
|
22118
22609
|
if (dryRun) {
|
|
@@ -22130,14 +22621,14 @@ ${cu.tail.trimEnd()}
|
|
|
22130
22621
|
throw new ReposError(`${e.message}${authHint}
|
|
22131
22622
|
crew.yaml was not changed`);
|
|
22132
22623
|
}
|
|
22133
|
-
|
|
22624
|
+
writeFileSync28(cfg.configFile, plan2.text);
|
|
22134
22625
|
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:
|
|
22135
22626
|
`);
|
|
22136
22627
|
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}`);
|
|
22137
22628
|
break;
|
|
22138
22629
|
}
|
|
22139
22630
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
|
|
22140
|
-
const top =
|
|
22631
|
+
const top = existsSync41(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
22141
22632
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
22142
22633
|
const origin = originOf(top);
|
|
22143
22634
|
const plan = planRepoAdd({
|
|
@@ -22189,15 +22680,15 @@ ${cu.tail.trimEnd()}
|
|
|
22189
22680
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
22190
22681
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
22191
22682
|
`);
|
|
22192
|
-
if (
|
|
22193
|
-
const raw = JSON.parse(
|
|
22683
|
+
if (existsSync41(resolvedFile)) {
|
|
22684
|
+
const raw = JSON.parse(readFileSync36(resolvedFile, "utf8"));
|
|
22194
22685
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
22195
22686
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
22196
|
-
|
|
22687
|
+
writeFileSync28(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
22197
22688
|
`);
|
|
22198
22689
|
}
|
|
22199
22690
|
}
|
|
22200
|
-
|
|
22691
|
+
writeFileSync28(cfg.configFile, plan.text);
|
|
22201
22692
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
22202
22693
|
`);
|
|
22203
22694
|
} catch (e) {
|
|
@@ -22408,7 +22899,7 @@ ${reconstruction.prompt}
|
|
|
22408
22899
|
let result;
|
|
22409
22900
|
try {
|
|
22410
22901
|
result = await loginWithDeviceCode(`${baseUrl}/api`, {
|
|
22411
|
-
deviceName: `crew on ${
|
|
22902
|
+
deviceName: `crew on ${hostname3()}`,
|
|
22412
22903
|
// Lets the approval page preselect the workspace this connect is for.
|
|
22413
22904
|
// A bare `crew connect` has none and passes nothing.
|
|
22414
22905
|
...workspace ? { workspaceSlug: workspace } : {},
|
|
@@ -22564,7 +23055,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22564
23055
|
process.exit(2);
|
|
22565
23056
|
}
|
|
22566
23057
|
if (found.shipNameAdopted && !dryRun) {
|
|
22567
|
-
|
|
23058
|
+
writeFileSync28(cfg.configFile, setShipNameInYaml(readFileSync36(cfg.configFile, "utf8"), found.shipNameAdopted));
|
|
22568
23059
|
cfg.ship.name = found.shipNameAdopted;
|
|
22569
23060
|
}
|
|
22570
23061
|
if (found.shipRow) {
|
|
@@ -22607,10 +23098,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22607
23098
|
}
|
|
22608
23099
|
}
|
|
22609
23100
|
let previous;
|
|
22610
|
-
const hadPreviousFile =
|
|
23101
|
+
const hadPreviousFile = existsSync41(resolvedPath);
|
|
22611
23102
|
if (hadPreviousFile) {
|
|
22612
23103
|
try {
|
|
22613
|
-
previous = JSON.parse(
|
|
23104
|
+
previous = JSON.parse(readFileSync36(resolvedPath, "utf8"));
|
|
22614
23105
|
} catch {
|
|
22615
23106
|
}
|
|
22616
23107
|
}
|
|
@@ -22742,7 +23233,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22742
23233
|
mkdirSync26(dirname22(resolvedPath), { recursive: true });
|
|
22743
23234
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
22744
23235
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
22745
|
-
|
|
23236
|
+
writeFileSync28(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
22746
23237
|
`);
|
|
22747
23238
|
renameSync4(tmpPath, resolvedPath);
|
|
22748
23239
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -22789,7 +23280,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22789
23280
|
`);
|
|
22790
23281
|
} else {
|
|
22791
23282
|
mkdirSync26(dirname22(apiKeyFile), { recursive: true });
|
|
22792
|
-
|
|
23283
|
+
writeFileSync28(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
22793
23284
|
`, { mode: 384 });
|
|
22794
23285
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
22795
23286
|
`);
|
|
@@ -23523,7 +24014,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
23523
24014
|
const shellR = hit && resolveShell(hit.repo.config.shell ?? cfg.ship.shell);
|
|
23524
24015
|
const resumed = await runPeriscopeResume({
|
|
23525
24016
|
find: async () => hit && { row: hit.row, tag: hit.tag },
|
|
23526
|
-
worktreeExists: (p) =>
|
|
24017
|
+
worktreeExists: (p) => existsSync41(p),
|
|
23527
24018
|
caps: periscopeCaps(cfg.ship),
|
|
23528
24019
|
runHook: async (name, cwd, env) => {
|
|
23529
24020
|
const script = hit?.repo.config.hooks[name];
|
|
@@ -23618,7 +24109,7 @@ ${where.join("\n")}`);
|
|
|
23618
24109
|
prepare: async (path) => {
|
|
23619
24110
|
for (const f of repoP.config.worktrees.copy) {
|
|
23620
24111
|
const src = join44(repoP.dir, f);
|
|
23621
|
-
if (!
|
|
24112
|
+
if (!existsSync41(src)) continue;
|
|
23622
24113
|
mkdirSync26(dirOf2(join44(path, f)), { recursive: true });
|
|
23623
24114
|
copyFileSync2(src, join44(path, f));
|
|
23624
24115
|
}
|
|
@@ -23706,7 +24197,7 @@ ${where.join("\n")}`);
|
|
|
23706
24197
|
process.stdout.write(upOutput(res, terminal));
|
|
23707
24198
|
const opener = openerFor(process.platform);
|
|
23708
24199
|
if (opener && shouldOpen({ open: flag("open"), noOpen: flag("no-open"), json: jsonP, terminal })) {
|
|
23709
|
-
const r =
|
|
24200
|
+
const r = spawnSync8(opener, [res.url], { stdio: "ignore" });
|
|
23710
24201
|
if (r.error || r.status !== 0) process.stderr.write(`crew periscope: could not open ${res.url} with ${opener}
|
|
23711
24202
|
`);
|
|
23712
24203
|
}
|
|
@@ -23895,7 +24386,7 @@ ${where.join("\n")}`);
|
|
|
23895
24386
|
};
|
|
23896
24387
|
const droppedRoutes = [];
|
|
23897
24388
|
try {
|
|
23898
|
-
const rawCfg = parseYaml2(
|
|
24389
|
+
const rawCfg = parseYaml2(readFileSync36(cfg.configFile, "utf8"));
|
|
23899
24390
|
for (const r of rawCfg?.routes ?? []) {
|
|
23900
24391
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
23901
24392
|
try {
|
|
@@ -23931,19 +24422,19 @@ ${where.join("\n")}`);
|
|
|
23931
24422
|
}
|
|
23932
24423
|
const host = hostPlatform();
|
|
23933
24424
|
const control = host === "windows" ? null : planDaemonControl(CREW_HOME, host, detectSystemd());
|
|
23934
|
-
const unitStatus = control?.statusCommand ?
|
|
24425
|
+
const unitStatus = control?.statusCommand ? spawnSync8(control.statusCommand[0], control.statusCommand.slice(1), { cwd: CREW_HOME, encoding: "utf8" }) : null;
|
|
23935
24426
|
const unitRunning = unitStatus !== null && !unitStatus.error && unitStatus.status === 0;
|
|
23936
24427
|
if (!unitRunning) {
|
|
23937
24428
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
23938
24429
|
break;
|
|
23939
24430
|
}
|
|
23940
24431
|
mkdirSync26(cfg.ship.stateDir, { recursive: true });
|
|
23941
|
-
|
|
24432
|
+
writeFileSync28(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
23942
24433
|
`);
|
|
23943
24434
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
23944
24435
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
23945
24436
|
for (const cmd of [control.stopCommand, control.startCommand]) {
|
|
23946
|
-
const res =
|
|
24437
|
+
const res = spawnSync8(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
23947
24438
|
if (cmd === control.startCommand && (res.error || res.status !== 0)) {
|
|
23948
24439
|
process.stderr.write(`crew reload: ${cmd.join(" ")} exited ${res.status ?? "error"}
|
|
23949
24440
|
`);
|
|
@@ -23989,7 +24480,7 @@ ${where.join("\n")}`);
|
|
|
23989
24480
|
}
|
|
23990
24481
|
case "log": {
|
|
23991
24482
|
try {
|
|
23992
|
-
const text =
|
|
24483
|
+
const text = readFileSync36(cfg.ship.logFile, "utf8").split("\n");
|
|
23993
24484
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
23994
24485
|
`);
|
|
23995
24486
|
} catch {
|
|
@@ -24012,7 +24503,7 @@ ${where.join("\n")}`);
|
|
|
24012
24503
|
};
|
|
24013
24504
|
if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
|
|
24014
24505
|
const tracker = new Tracker(route2, cfg.ship);
|
|
24015
|
-
const configText =
|
|
24506
|
+
const configText = readFileSync36(cfg.configFile, "utf8");
|
|
24016
24507
|
const cfgBase = dirname22(cfg.configFile);
|
|
24017
24508
|
const configured2 = configuredRepos(configText, route2.route, cfgBase);
|
|
24018
24509
|
const dir = configured2.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured2.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
|
|
@@ -24041,7 +24532,7 @@ ${where.join("\n")}`);
|
|
|
24041
24532
|
}
|
|
24042
24533
|
if (configured2.some((r) => r.dir === dir)) {
|
|
24043
24534
|
try {
|
|
24044
|
-
|
|
24535
|
+
writeFileSync28(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
|
|
24045
24536
|
} catch (e) {
|
|
24046
24537
|
if (e instanceof ReposError) fail(e.message);
|
|
24047
24538
|
throw e;
|
|
@@ -24049,31 +24540,31 @@ ${where.join("\n")}`);
|
|
|
24049
24540
|
}
|
|
24050
24541
|
if (row) {
|
|
24051
24542
|
await tracker.deleteRepoRow(row.id);
|
|
24052
|
-
if (
|
|
24053
|
-
const raw = JSON.parse(
|
|
24543
|
+
if (existsSync41(resolvedFile)) {
|
|
24544
|
+
const raw = JSON.parse(readFileSync36(resolvedFile, "utf8"));
|
|
24054
24545
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
24055
24546
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
24056
|
-
|
|
24547
|
+
writeFileSync28(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
24057
24548
|
`);
|
|
24058
24549
|
}
|
|
24059
24550
|
}
|
|
24060
|
-
if (
|
|
24551
|
+
if (existsSync41(join44(dir, ".git")) && basename13(dir) === SAMPLE_REPO) rmSync19(dir, { recursive: true, force: true });
|
|
24061
24552
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
24062
24553
|
break;
|
|
24063
24554
|
}
|
|
24064
24555
|
const projectId = route2.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
24065
|
-
const fresh = !
|
|
24066
|
-
if (
|
|
24556
|
+
const fresh = !existsSync41(join44(dir, ".git"));
|
|
24557
|
+
if (existsSync41(dir) && fresh && readdirSync18(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
24067
24558
|
if (dryRun) {
|
|
24068
24559
|
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`);
|
|
24069
24560
|
break;
|
|
24070
24561
|
}
|
|
24071
24562
|
if (fresh) {
|
|
24072
24563
|
mkdirSync26(dir, { recursive: true });
|
|
24073
|
-
|
|
24074
|
-
|
|
24564
|
+
writeFileSync28(join44(dir, "README.md"), SAMPLE_README);
|
|
24565
|
+
writeFileSync28(join44(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
24075
24566
|
const g = (...a) => {
|
|
24076
|
-
const r =
|
|
24567
|
+
const r = spawnSync8("git", a, { cwd: dir, encoding: "utf8" });
|
|
24077
24568
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
24078
24569
|
};
|
|
24079
24570
|
g("init", "-b", "main");
|
|
@@ -24089,17 +24580,17 @@ ${where.join("\n")}`);
|
|
|
24089
24580
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
24090
24581
|
});
|
|
24091
24582
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
24092
|
-
if (
|
|
24093
|
-
const raw = JSON.parse(
|
|
24583
|
+
if (existsSync41(resolvedFile)) {
|
|
24584
|
+
const raw = JSON.parse(readFileSync36(resolvedFile, "utf8"));
|
|
24094
24585
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
24095
24586
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
24096
|
-
|
|
24587
|
+
writeFileSync28(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
24097
24588
|
`);
|
|
24098
24589
|
}
|
|
24099
24590
|
}
|
|
24100
24591
|
if (!configured2.some((r) => r.dir === dir)) {
|
|
24101
24592
|
try {
|
|
24102
|
-
|
|
24593
|
+
writeFileSync28(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route2.route, path: dir, name: SAMPLE_REPO }).text);
|
|
24103
24594
|
} catch (e) {
|
|
24104
24595
|
if (e instanceof ReposError) fail(e.message);
|
|
24105
24596
|
throw e;
|
|
@@ -24403,7 +24894,7 @@ watermark: ${state.watermark()}
|
|
|
24403
24894
|
);
|
|
24404
24895
|
}
|
|
24405
24896
|
for (const x of runs) {
|
|
24406
|
-
process.stdout.write(`running: ${x.role} \u2014 ${x.ticket ?? "(no ticket)"}${x.repo ? ` in ${x.repo}` : ""} (pid ${x.pid}, since ${x.spawnedAt})
|
|
24897
|
+
process.stdout.write(`running: ${x.role} \u2014 ${x.ticket ?? "(no ticket)"}${x.repo ? ` in ${x.repo}` : ""} (pid ${x.pid}, since ${x.spawnedAt}${x.deadline ? `, deadline ${x.deadline}` : ""})
|
|
24407
24898
|
`);
|
|
24408
24899
|
}
|
|
24409
24900
|
if (shipRows.length) {
|
|
@@ -24434,7 +24925,7 @@ watermark: ${state.watermark()}
|
|
|
24434
24925
|
}
|
|
24435
24926
|
for (const r of repos) {
|
|
24436
24927
|
const remote = r.config.branch.remote;
|
|
24437
|
-
if (
|
|
24928
|
+
if (existsSync41(r.dir)) {
|
|
24438
24929
|
pushAuth.push({
|
|
24439
24930
|
route: c.route,
|
|
24440
24931
|
repo: r.name,
|
|
@@ -24519,7 +25010,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
24519
25010
|
}
|
|
24520
25011
|
try {
|
|
24521
25012
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
24522
|
-
const evSize =
|
|
25013
|
+
const evSize = existsSync41(evFile) ? statSync14(evFile).size : 0;
|
|
24523
25014
|
process.stdout.write(
|
|
24524
25015
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
24525
25016
|
` : "events log: ok\n"
|
|
@@ -24527,10 +25018,10 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
24527
25018
|
} catch {
|
|
24528
25019
|
}
|
|
24529
25020
|
if (host !== "windows") {
|
|
24530
|
-
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
25021
|
+
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run", installContext());
|
|
24531
25022
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
24532
25023
|
try {
|
|
24533
|
-
return
|
|
25024
|
+
return readFileSync36(p, "utf8");
|
|
24534
25025
|
} catch {
|
|
24535
25026
|
return null;
|
|
24536
25027
|
}
|
|
@@ -24613,6 +25104,11 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
24613
25104
|
const missingAuthorship = await tracker.missingAuthorshipColumns();
|
|
24614
25105
|
process.stdout.write(
|
|
24615
25106
|
missingAuthorship.length === 0 ? "authorship: filed_by_id, ship_id present\n" : `authorship: missing ${missingAuthorship.join(", ")} \u2014 update the Issues template
|
|
25107
|
+
`
|
|
25108
|
+
);
|
|
25109
|
+
const missingRuntime = await tracker.missingRuntimeColumns();
|
|
25110
|
+
process.stdout.write(
|
|
25111
|
+
missingRuntime.length === 0 ? "runtime columns: forge_host, kind, instance_class, relay_url present\n" : `runtime columns: missing ${missingRuntime.join(", ")} \u2014 read as their defaults; update the Issues template
|
|
24616
25112
|
`
|
|
24617
25113
|
);
|
|
24618
25114
|
process.stdout.write(
|