@tablation/crew 0.126.4 → 0.126.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +48 -7
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -690,9 +690,9 @@ function urlPort(url) {
|
|
|
690
690
|
return void 0;
|
|
691
691
|
}
|
|
692
692
|
}
|
|
693
|
-
function
|
|
693
|
+
function hostListening(port, host, timeoutMs) {
|
|
694
694
|
return new Promise((resolve8) => {
|
|
695
|
-
const s = connect({ port, host
|
|
695
|
+
const s = connect({ port, host });
|
|
696
696
|
const done = (ok) => {
|
|
697
697
|
s.destroy();
|
|
698
698
|
resolve8(ok);
|
|
@@ -702,6 +702,10 @@ function portListening(port, timeoutMs = 500) {
|
|
|
702
702
|
s.once("error", () => done(false));
|
|
703
703
|
});
|
|
704
704
|
}
|
|
705
|
+
async function portListening(port, timeoutMs = 500) {
|
|
706
|
+
if (await hostListening(port, "127.0.0.1", timeoutMs)) return true;
|
|
707
|
+
return hostListening(port, "::1", timeoutMs);
|
|
708
|
+
}
|
|
705
709
|
async function periscopesForStatus(stateDir, probe = portListening) {
|
|
706
710
|
return Promise.all(readPeriscopeSidecars(stateDir).map(async (sc) => {
|
|
707
711
|
const port = urlPort(sc.url);
|
|
@@ -15097,6 +15101,12 @@ function sshArgsFor(o) {
|
|
|
15097
15101
|
"ServerAliveCountMax=3",
|
|
15098
15102
|
"-o",
|
|
15099
15103
|
"ExitOnForwardFailure=yes",
|
|
15104
|
+
// Offer only the ship key. Without this OpenSSH offers every ssh-agent key
|
|
15105
|
+
// before the `-i` one, the relay hashes whichever it sees first, and a
|
|
15106
|
+
// slug owned by the ship key's fingerprint is rejected as a conflict
|
|
15107
|
+
// (CREW-2932).
|
|
15108
|
+
"-o",
|
|
15109
|
+
"IdentitiesOnly=yes",
|
|
15100
15110
|
"-i",
|
|
15101
15111
|
o.privateKeyPath,
|
|
15102
15112
|
"-p",
|
|
@@ -15173,6 +15183,7 @@ function isPidAlive(pid) {
|
|
|
15173
15183
|
}
|
|
15174
15184
|
}
|
|
15175
15185
|
var CONTROL_ONLY_HASH = "control-only";
|
|
15186
|
+
var TUNNEL_REJECTED_AFTER = 2;
|
|
15176
15187
|
function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, stateDir, mcpSecrets = /* @__PURE__ */ new Map(), deps = {}) {
|
|
15177
15188
|
const alive = deps.isPidAlive ?? isPidAlive;
|
|
15178
15189
|
const plannedIds = new Set(plans.map((p) => p.workspaceId));
|
|
@@ -15203,6 +15214,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
15203
15214
|
deps.onStatus?.(plan.workspaceId, "disconnected", existing);
|
|
15204
15215
|
continue;
|
|
15205
15216
|
}
|
|
15217
|
+
const diedQuickly = existing !== void 0 && existing.status === "connecting" && !alive(existing.pid) && controlUnchanged && secretUnchanged && slugUnchanged && existing.mountsHash === mountsHash;
|
|
15218
|
+
const quickExits = diedQuickly ? (existing.quickExits ?? 0) + 1 : 0;
|
|
15206
15219
|
let settledStatus = "connecting";
|
|
15207
15220
|
const tunnel = startTunnel({
|
|
15208
15221
|
relayHost,
|
|
@@ -15230,6 +15243,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
15230
15243
|
mcpSecret: currentSecret,
|
|
15231
15244
|
slug: slug2,
|
|
15232
15245
|
liveSlug: slug2,
|
|
15246
|
+
quickExits,
|
|
15233
15247
|
endpointId: existing?.endpointId,
|
|
15234
15248
|
registeredKey: existing?.registeredKey
|
|
15235
15249
|
};
|
|
@@ -15254,6 +15268,18 @@ function readAllPersistedWorkspaceIds(stateDir) {
|
|
|
15254
15268
|
if (!existsSync35(dir)) return [];
|
|
15255
15269
|
return readdirSync13(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
15256
15270
|
}
|
|
15271
|
+
var TUNNEL_REJECTED_KEY = "tunnel_rejected";
|
|
15272
|
+
function tunnelRejectedMessage(workspaceId, quickExits) {
|
|
15273
|
+
return `the relay tunnel for workspace ${workspaceId} exited before settling on ${quickExits} consecutive cycles, which is how a refused claim looks (the relay logs "slug already owned by another workspace/ssh key") \u2014 rotate the slug with \`crew rotate-passenger-url\` or check the relay journal`;
|
|
15274
|
+
}
|
|
15275
|
+
function rejectedTunnels(stateDir) {
|
|
15276
|
+
const out = [];
|
|
15277
|
+
for (const workspaceId of readAllPersistedWorkspaceIds(stateDir)) {
|
|
15278
|
+
const q2 = readPersistedTunnel(stateDir, workspaceId)?.quickExits ?? 0;
|
|
15279
|
+
if (q2 >= TUNNEL_REJECTED_AFTER) out.push({ workspaceId, quickExits: q2 });
|
|
15280
|
+
}
|
|
15281
|
+
return out;
|
|
15282
|
+
}
|
|
15257
15283
|
|
|
15258
15284
|
// src/pool-member-host.ts
|
|
15259
15285
|
function loadShip(crewHome) {
|
|
@@ -25442,6 +25468,15 @@ if (command === "passenger-sync-daemon") {
|
|
|
25442
25468
|
})
|
|
25443
25469
|
);
|
|
25444
25470
|
if (status2 === "disconnected") pending.push(unregisterEndpoint(workspaceId, info, reconcile));
|
|
25471
|
+
const rejectedKey = `${TUNNEL_REJECTED_KEY}:${workspaceId}`;
|
|
25472
|
+
if ((info?.quickExits ?? 0) >= TUNNEL_REJECTED_AFTER) {
|
|
25473
|
+
pending.push(raiseShipAttention({ state, emit, route: owningRoute, ship: cfg.ship }, {
|
|
25474
|
+
kind: TUNNEL_REJECTED_KEY,
|
|
25475
|
+
key: rejectedKey,
|
|
25476
|
+
message: tunnelRejectedMessage(workspaceId, info.quickExits),
|
|
25477
|
+
since: (/* @__PURE__ */ new Date()).toISOString()
|
|
25478
|
+
}).then(() => void 0).catch(() => void 0));
|
|
25479
|
+
} else if (status2 === "connected") clearShipAttention(state, rejectedKey);
|
|
25445
25480
|
}
|
|
25446
25481
|
});
|
|
25447
25482
|
for (const plan of containerPlans) pending.push(reconcileEndpoint(plan.workspaceId, reconcile));
|
|
@@ -28920,11 +28955,12 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
28920
28955
|
});
|
|
28921
28956
|
};
|
|
28922
28957
|
if (subP === "list") {
|
|
28923
|
-
const
|
|
28924
|
-
|
|
28925
|
-
|
|
28926
|
-
|
|
28927
|
-
);
|
|
28958
|
+
const listRows = (await Promise.all(routesP.map(gather))).flat().map((r) => ({ ...r, pointedAt: readPointedAt(r.worktree) }));
|
|
28959
|
+
const serving = new Map(await Promise.all(listRows.map(async (r) => {
|
|
28960
|
+
const port = urlPort(r.url);
|
|
28961
|
+
return [r.id, port === void 0 ? false : await portListening(port)];
|
|
28962
|
+
})));
|
|
28963
|
+
const entries = periscopeListEntries(listRows, /* @__PURE__ */ new Date(), (r) => serving.get(r.id) ?? false);
|
|
28928
28964
|
process.stdout.write(jsonP ? `${JSON.stringify(entries)}
|
|
28929
28965
|
` : `${formatPeriscopeList(entries)}
|
|
28930
28966
|
`);
|
|
@@ -30122,6 +30158,9 @@ watermark: ${state.watermark()}
|
|
|
30122
30158
|
if (!flag("offline")) {
|
|
30123
30159
|
for (const message of managedRuntimeWarnings(await doctorShipKind(route2, cfg.ship), cfg.ship)) problems.push({ severity: "attention", kind: "managed_runtime_default", message });
|
|
30124
30160
|
}
|
|
30161
|
+
for (const t of rejectedTunnels(cfg.ship.stateDir)) {
|
|
30162
|
+
problems.push({ severity: "attention", kind: TUNNEL_REJECTED_KEY, message: tunnelRejectedMessage(t.workspaceId, t.quickExits), remedy: "rotate the slug with `crew rotate-passenger-url`" });
|
|
30163
|
+
}
|
|
30125
30164
|
const previewProblem2 = await previewHostProblem(cfg);
|
|
30126
30165
|
if (previewProblem2) problems.push(previewHostFinding(previewProblem2));
|
|
30127
30166
|
if (cfg.routes.some((x) => x.route === route2.route && x.enabled && x.hostPeriscopes)) {
|
|
@@ -30142,6 +30181,8 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
30142
30181
|
`
|
|
30143
30182
|
);
|
|
30144
30183
|
for (const w of roleCapWarnings(cfg.ship)) process.stdout.write(`role concurrency: WARN ${w}
|
|
30184
|
+
`);
|
|
30185
|
+
for (const t of rejectedTunnels(cfg.ship.stateDir)) process.stdout.write(`relay tunnel: ATTENTION ${tunnelRejectedMessage(t.workspaceId, t.quickExits)}
|
|
30145
30186
|
`);
|
|
30146
30187
|
process.stdout.write(`periscopes: ${periscopeSummary(cfg)}
|
|
30147
30188
|
`);
|
package/package.json
CHANGED