@tablation/crew 0.5.0 → 0.6.1
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/README.md +1 -1
- package/dist/cli.js +104 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -207,7 +207,7 @@ crew ports [route] which checkout owns which ports, and what is up
|
|
|
207
207
|
crew reap [route] kill servers left behind by removed worktrees
|
|
208
208
|
crew drop [route] NNN remove a merged ticket's worktree and branch
|
|
209
209
|
crew sync [route] fast-forward the checkout and its worktrees from the remote
|
|
210
|
-
crew pause|resume [route] [R] pause everything, or
|
|
210
|
+
crew pause|resume [route] [R|release] pause everything, one role, or releases only (passengers keep running)
|
|
211
211
|
crew log [route] tail the log
|
|
212
212
|
crew inbox [--member NAME] your tickets across every workspace
|
|
213
213
|
crew connect WS[/PROJECT] resolve a workspace/project's ids into the state tree
|
package/dist/cli.js
CHANGED
|
@@ -578,7 +578,8 @@ var SHIP_KEYS = /* @__PURE__ */ new Set([
|
|
|
578
578
|
"relayHost",
|
|
579
579
|
"relayPort",
|
|
580
580
|
"relayHttpPort",
|
|
581
|
-
"relayPublicDomain"
|
|
581
|
+
"relayPublicDomain",
|
|
582
|
+
"passengerImage"
|
|
582
583
|
]);
|
|
583
584
|
var ROUTE_KEYS = /* @__PURE__ */ new Set([
|
|
584
585
|
"route",
|
|
@@ -912,7 +913,8 @@ Create the first of those:
|
|
|
912
913
|
// 2222 matches the relay's own RELAY_SSH_PORT default (crew-relay README).
|
|
913
914
|
relayPort: shipRaw.relayPort !== void 0 ? Number(shipRaw.relayPort) : 2222,
|
|
914
915
|
relayHttpPort: shipRaw.relayHttpPort !== void 0 ? Number(shipRaw.relayHttpPort) : 443,
|
|
915
|
-
relayPublicDomain: shipRaw.relayPublicDomain ? String(shipRaw.relayPublicDomain) : void 0
|
|
916
|
+
relayPublicDomain: shipRaw.relayPublicDomain ? String(shipRaw.relayPublicDomain) : void 0,
|
|
917
|
+
passengerImage: shipRaw.passengerImage ? String(shipRaw.passengerImage) : void 0
|
|
916
918
|
},
|
|
917
919
|
routes,
|
|
918
920
|
crewHome,
|
|
@@ -1217,6 +1219,31 @@ var State = class _State {
|
|
|
1217
1219
|
resume(role) {
|
|
1218
1220
|
rmSync(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
|
|
1219
1221
|
}
|
|
1222
|
+
/**
|
|
1223
|
+
* The release phase (merge, deploy, release) has its own sentinel, one
|
|
1224
|
+
* level below the whole-crew pause: `crew pause release` stops shipping
|
|
1225
|
+
* while agents keep working.
|
|
1226
|
+
*/
|
|
1227
|
+
isReleasePaused() {
|
|
1228
|
+
return existsSync3(this.path(".release-paused"));
|
|
1229
|
+
}
|
|
1230
|
+
setReleasePaused(paused) {
|
|
1231
|
+
if (paused) writeFileSync(this.path(".release-paused"), "");
|
|
1232
|
+
else rmSync(this.path(".release-paused"), { force: true });
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Why a release cycle must not run right now, or null when it may. Every
|
|
1236
|
+
* release entry point checks this first — the release timer, `crew release`,
|
|
1237
|
+
* `crew merge`, `crew deploy` and the inline release a `run` cycle used to
|
|
1238
|
+
* do — so a paused ship cannot merge, deploy, or file release alerts.
|
|
1239
|
+
* A release already in flight when the pause lands finishes; only new
|
|
1240
|
+
* cycles are refused.
|
|
1241
|
+
*/
|
|
1242
|
+
releaseSkipReason() {
|
|
1243
|
+
if (this.isPaused()) return "the crew is paused";
|
|
1244
|
+
if (this.isReleasePaused()) return "releases are paused";
|
|
1245
|
+
return null;
|
|
1246
|
+
}
|
|
1220
1247
|
/**
|
|
1221
1248
|
* Take an exclusive lock, or report who holds it.
|
|
1222
1249
|
*
|
|
@@ -7775,7 +7802,21 @@ function dockerAvailable(exec = realExec) {
|
|
|
7775
7802
|
return false;
|
|
7776
7803
|
}
|
|
7777
7804
|
}
|
|
7778
|
-
var
|
|
7805
|
+
var PASSENGER_IMAGE_REPO = "tablation/crew-passenger-mcp";
|
|
7806
|
+
function passengerImageRef(cfg) {
|
|
7807
|
+
return process.env.CREW_PASSENGER_IMAGE || cfg.ship.passengerImage || `${PASSENGER_IMAGE_REPO}:${crewVersion(cfg.crewHome)}`;
|
|
7808
|
+
}
|
|
7809
|
+
function imagePresent(image, exec = realExec) {
|
|
7810
|
+
try {
|
|
7811
|
+
exec("docker", ["image", "inspect", image]);
|
|
7812
|
+
return true;
|
|
7813
|
+
} catch {
|
|
7814
|
+
return false;
|
|
7815
|
+
}
|
|
7816
|
+
}
|
|
7817
|
+
function ensureImage(image, exec = realExec) {
|
|
7818
|
+
if (!imagePresent(image, exec)) exec("docker", ["pull", image]);
|
|
7819
|
+
}
|
|
7779
7820
|
function containerNameFor(workspaceId) {
|
|
7780
7821
|
return `crew-passenger-${workspaceId}`;
|
|
7781
7822
|
}
|
|
@@ -7783,12 +7824,13 @@ function portFor(workspaceId, base = 28765, span = 1e3) {
|
|
|
7783
7824
|
const digest = createHash4("sha256").update(workspaceId).digest();
|
|
7784
7825
|
return base + digest.readUInt32BE(0) % span;
|
|
7785
7826
|
}
|
|
7786
|
-
function mountsDigest(mounts) {
|
|
7827
|
+
function mountsDigest(mounts, image) {
|
|
7787
7828
|
const sorted2 = [...mounts].sort((a, b) => a.containerPath.localeCompare(b.containerPath));
|
|
7788
|
-
return createHash4("sha256").update(JSON.stringify(sorted2)).digest("hex").slice(0, 16);
|
|
7829
|
+
return createHash4("sha256").update(JSON.stringify({ mounts: sorted2, image })).digest("hex").slice(0, 16);
|
|
7789
7830
|
}
|
|
7790
7831
|
function planContainers(cfg) {
|
|
7791
7832
|
const byWorkspace = /* @__PURE__ */ new Map();
|
|
7833
|
+
const image = passengerImageRef(cfg);
|
|
7792
7834
|
for (const route of cfg.routes) {
|
|
7793
7835
|
if (!route.enabled || !route.hostPassengers || !route.resolved?.workspaceId) continue;
|
|
7794
7836
|
const workspaceId = route.resolved.workspaceId;
|
|
@@ -7804,8 +7846,9 @@ function planContainers(cfg) {
|
|
|
7804
7846
|
workspaceId,
|
|
7805
7847
|
containerName: containerNameFor(workspaceId),
|
|
7806
7848
|
port: portFor(workspaceId),
|
|
7849
|
+
image,
|
|
7807
7850
|
mounts: mountList,
|
|
7808
|
-
mountsHash: mountsDigest(mountList)
|
|
7851
|
+
mountsHash: mountsDigest(mountList, image)
|
|
7809
7852
|
};
|
|
7810
7853
|
});
|
|
7811
7854
|
}
|
|
@@ -7861,7 +7904,7 @@ function startContainer(plan, tablationApiBaseUrl, sync, exec = realExec) {
|
|
|
7861
7904
|
);
|
|
7862
7905
|
}
|
|
7863
7906
|
for (const m of plan.mounts) args.push("-v", `${m.hostPath}:${m.containerPath}:ro`);
|
|
7864
|
-
args.push(
|
|
7907
|
+
args.push(plan.image);
|
|
7865
7908
|
exec("docker", args);
|
|
7866
7909
|
}
|
|
7867
7910
|
function syncPassengerContainers(cfg, tablationApiBaseUrl, exec = realExec, syncEndpoints = /* @__PURE__ */ new Map()) {
|
|
@@ -7872,19 +7915,27 @@ function syncPassengerContainers(cfg, tablationApiBaseUrl, exec = realExec, sync
|
|
|
7872
7915
|
const started = [];
|
|
7873
7916
|
const recreated = [];
|
|
7874
7917
|
const stopped = [];
|
|
7918
|
+
const ensured = /* @__PURE__ */ new Set();
|
|
7919
|
+
const start = (plan, sync) => {
|
|
7920
|
+
if (!ensured.has(plan.image)) {
|
|
7921
|
+
ensureImage(plan.image, exec);
|
|
7922
|
+
ensured.add(plan.image);
|
|
7923
|
+
}
|
|
7924
|
+
startContainer(plan, tablationApiBaseUrl, sync, exec);
|
|
7925
|
+
};
|
|
7875
7926
|
for (const plan of plans) {
|
|
7876
7927
|
const sync = syncEndpoints.get(plan.workspaceId);
|
|
7877
7928
|
const current = existingByName.get(plan.containerName);
|
|
7878
7929
|
if (!current) {
|
|
7879
|
-
|
|
7930
|
+
start(plan, sync);
|
|
7880
7931
|
started.push(plan.containerName);
|
|
7881
7932
|
} else if (!current.running) {
|
|
7882
7933
|
stopContainer(plan.containerName, exec);
|
|
7883
|
-
|
|
7934
|
+
start(plan, sync);
|
|
7884
7935
|
started.push(plan.containerName);
|
|
7885
7936
|
} else if (currentMountsHash(plan.containerName, exec) !== plan.mountsHash) {
|
|
7886
7937
|
stopContainer(plan.containerName, exec);
|
|
7887
|
-
|
|
7938
|
+
start(plan, sync);
|
|
7888
7939
|
recreated.push(plan.containerName);
|
|
7889
7940
|
}
|
|
7890
7941
|
}
|
|
@@ -8499,6 +8550,7 @@ var SpawnBreaker = class {
|
|
|
8499
8550
|
}
|
|
8500
8551
|
};
|
|
8501
8552
|
async function runOnePass(o) {
|
|
8553
|
+
if (o.state.isPaused()) return { pending: [], started: [], skipped: [] };
|
|
8502
8554
|
await hydrateApiKeys([o.route], o.getSessionStore);
|
|
8503
8555
|
const emit = o.newEmitter();
|
|
8504
8556
|
const decide = o.decide ?? decideCycle;
|
|
@@ -8826,7 +8878,8 @@ function usage() {
|
|
|
8826
8878
|
crew rotate-passenger-url [route] force a Host Passengers tunnel to reconnect with a fresh public URL
|
|
8827
8879
|
crew unassign [route] NNN hand back a session's ticket \u2014 clears assignee, next cycle picks it up
|
|
8828
8880
|
crew sync [route] fast-forward the checkout and its worktrees from the remote
|
|
8829
|
-
crew pause|resume [route] [R] pause everything, or
|
|
8881
|
+
crew pause|resume [route] [R|release] pause everything, one role, or releases only (merge/deploy/release; passengers keeps running).
|
|
8882
|
+
resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
|
|
8830
8883
|
crew log [route] tail the log
|
|
8831
8884
|
crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
|
|
8832
8885
|
crew connect resolve a workspace's ids into a crew.yaml block
|
|
@@ -8927,6 +8980,11 @@ if (command === "passenger-sync-daemon") {
|
|
|
8927
8980
|
urlFor: (slug) => publicUrlFor(slug, relayHost, cfg.ship.relayHttpPort, cfg.ship.relayPublicDomain),
|
|
8928
8981
|
warn: (m) => emit.warn(m, { step: "passengers" })
|
|
8929
8982
|
};
|
|
8983
|
+
}, releaseRefused = function() {
|
|
8984
|
+
const why = state.releaseSkipReason();
|
|
8985
|
+
if (!why) return false;
|
|
8986
|
+
emit.emit(`${why} \u2014 release skipped`, { step: "release" });
|
|
8987
|
+
return true;
|
|
8930
8988
|
}, reportUnplaceable = function(c, unplaceable, verified, emit2) {
|
|
8931
8989
|
if (unplaceableReported.has(c.route)) return;
|
|
8932
8990
|
unplaceableReported.add(c.route);
|
|
@@ -8935,7 +8993,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
8935
8993
|
else emit2.emit(note.message, { step: "release" });
|
|
8936
8994
|
}
|
|
8937
8995
|
};
|
|
8938
|
-
requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, endpointReconcileDeps2 = endpointReconcileDeps, reportUnplaceable2 = reportUnplaceable;
|
|
8996
|
+
requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, reportUnplaceable2 = reportUnplaceable;
|
|
8939
8997
|
let cfg;
|
|
8940
8998
|
let firstRun;
|
|
8941
8999
|
try {
|
|
@@ -8977,7 +9035,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
8977
9035
|
const releaseFleetWide = ["merge", "deploy", "release"].includes(command) && flag("fleet");
|
|
8978
9036
|
const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
|
|
8979
9037
|
const named = command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
|
|
8980
|
-
const fleetWide = command === "inbox" || command === "connect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || releaseFleetWide || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1;
|
|
9038
|
+
const fleetWide = command === "inbox" || command === "connect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || releaseFleetWide || command === "pause" || command === "resume" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1;
|
|
8981
9039
|
let route;
|
|
8982
9040
|
try {
|
|
8983
9041
|
route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
|
|
@@ -9067,6 +9125,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
9067
9125
|
}
|
|
9068
9126
|
}
|
|
9069
9127
|
async function releaseFleet(opts = {}) {
|
|
9128
|
+
if (releaseRefused()) return;
|
|
9070
9129
|
const summaries = [];
|
|
9071
9130
|
if (!dryRun) state.releaseHeartbeat().touch();
|
|
9072
9131
|
for (const c of cfg.routes) {
|
|
@@ -9077,6 +9136,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
9077
9136
|
if (!dryRun) state.releaseHeartbeat().touch();
|
|
9078
9137
|
}
|
|
9079
9138
|
async function releaseTargets(opts = {}) {
|
|
9139
|
+
if (releaseRefused()) return;
|
|
9080
9140
|
const only = value("repo");
|
|
9081
9141
|
const targets = reposOf(route);
|
|
9082
9142
|
const chosen = only ? targets.filter((t) => t.name === only) : targets;
|
|
@@ -9129,6 +9189,23 @@ if (command === "passenger-sync-daemon") {
|
|
|
9129
9189
|
process.stderr.write("crew install: no scheduler support yet for Windows (Task Scheduler is planned, not built)\n");
|
|
9130
9190
|
process.exit(2);
|
|
9131
9191
|
}
|
|
9192
|
+
if (!dryRun && cfg.routes.some((r) => r.enabled && r.hostPassengers) && dockerAvailable()) {
|
|
9193
|
+
const image = passengerImageRef(cfg);
|
|
9194
|
+
if (imagePresent(image)) {
|
|
9195
|
+
process.stdout.write(`Host Passengers image ${image} is already present
|
|
9196
|
+
`);
|
|
9197
|
+
} else {
|
|
9198
|
+
process.stdout.write(`pulling Host Passengers image ${image}
|
|
9199
|
+
`);
|
|
9200
|
+
try {
|
|
9201
|
+
ensureImage(image);
|
|
9202
|
+
} catch (e) {
|
|
9203
|
+
process.stderr.write(`crew install: could not pull Host Passengers image ${image}: ${e.message}
|
|
9204
|
+
`);
|
|
9205
|
+
process.exitCode = 1;
|
|
9206
|
+
}
|
|
9207
|
+
}
|
|
9208
|
+
}
|
|
9132
9209
|
const hasSystemd = detectSystemd();
|
|
9133
9210
|
for (const job of ["run", "release", "passengers"]) {
|
|
9134
9211
|
const plan = planInstall(cfg.ship, CREW_HOME, host, hasSystemd, job);
|
|
@@ -10979,6 +11056,12 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
10979
11056
|
}
|
|
10980
11057
|
case "pause":
|
|
10981
11058
|
case "resume": {
|
|
11059
|
+
if (positional[1] === "release" || positional[2] === "release") {
|
|
11060
|
+
state.setReleasePaused(command === "pause");
|
|
11061
|
+
process.stdout.write(`${command}d releases
|
|
11062
|
+
`);
|
|
11063
|
+
break;
|
|
11064
|
+
}
|
|
10982
11065
|
const role = positional[2] ?? (["dev", "design", "qa"].includes(positional[1] ?? "") ? positional[1] : void 0);
|
|
10983
11066
|
if (command === "pause") state.pause(role);
|
|
10984
11067
|
else state.resume(role);
|
|
@@ -11070,6 +11153,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
11070
11153
|
},
|
|
11071
11154
|
crew: {
|
|
11072
11155
|
paused: state.isPaused(),
|
|
11156
|
+
// The release timer honors the whole-crew pause too, so this is
|
|
11157
|
+
// true whenever releases are stopped for either reason.
|
|
11158
|
+
releasePaused: state.releaseSkipReason() !== null,
|
|
11073
11159
|
roles: ROLE_NAMES.map((r) => ({ name: r, label: ROLE_LABEL[r], paused: state.isRolePaused(r) })),
|
|
11074
11160
|
seats: seats2
|
|
11075
11161
|
},
|
|
@@ -11113,6 +11199,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
11113
11199
|
route: ${route.route} -> ${route.dir}
|
|
11114
11200
|
enabled: ${route.enabled}
|
|
11115
11201
|
crew: ${state.isPaused() ? "paused" : "active"}
|
|
11202
|
+
release: ${state.releaseSkipReason() ? "paused" : "active"}
|
|
11116
11203
|
watermark: ${state.watermark()}
|
|
11117
11204
|
`
|
|
11118
11205
|
);
|
|
@@ -11253,10 +11340,13 @@ git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler P
|
|
|
11253
11340
|
} else {
|
|
11254
11341
|
const dockerOk = dockerAvailable();
|
|
11255
11342
|
const keygenOk = sshKeygenAvailable();
|
|
11343
|
+
const image = passengerImageRef(cfg);
|
|
11344
|
+
const imageOk = dockerOk && imagePresent(image);
|
|
11256
11345
|
process.stdout.write(
|
|
11257
11346
|
`host passengers: ON for ${passengerRoutes.map((r) => r.route).join(", ")}
|
|
11258
11347
|
docker: ${dockerOk ? "OK" : "NOT AVAILABLE"}
|
|
11259
11348
|
ssh-keygen: ${keygenOk ? "OK" : "NOT AVAILABLE"}
|
|
11349
|
+
image: ${image} (${imageOk ? "present" : "missing"})
|
|
11260
11350
|
relay host: ${cfg.ship.relayHost ? `${cfg.ship.relayHost}:${cfg.ship.relayPort}` : "not set (ship.relayHost) \u2014 containers will run with no tunnel"}
|
|
11261
11351
|
`
|
|
11262
11352
|
);
|
|
@@ -11311,4 +11401,5 @@ var parseTicketNumber2;
|
|
|
11311
11401
|
var passengerApiKeys2;
|
|
11312
11402
|
var endpointApiFor2;
|
|
11313
11403
|
var endpointReconcileDeps2;
|
|
11404
|
+
var releaseRefused2;
|
|
11314
11405
|
var reportUnplaceable2;
|
package/package.json
CHANGED