@kybernesis/create 0.14.7 → 0.14.8
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/deploy.js +46 -12
- package/dist/doctor.js +6 -0
- package/dist/sandbox-orphans.d.ts +56 -0
- package/dist/sandbox-orphans.js +77 -0
- package/dist/upgrade.js +28 -1
- package/package.json +1 -1
package/dist/deploy.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
+
import { TEMPLATE_CONTAINER_PROBE, assessRestart, parseTemplateContainers } from "./sandbox-orphans.js";
|
|
2
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { upsertEnv } from "./envfile.js";
|
|
@@ -239,6 +240,14 @@ export async function deploy(options) {
|
|
|
239
240
|
' SCRIPT="scripts/eve-server.sh"',
|
|
240
241
|
"fi",
|
|
241
242
|
'if [ -z "$SCRIPT" ]; then echo "FAILED: no restart script on the host"; exit 1; fi',
|
|
243
|
+
// An orphaned sandbox template container (a start killed mid-build; only
|
|
244
|
+
// `sleep` inside) makes every eve start hang forever. The packaged start
|
|
245
|
+
// script clears it too, but a host still running its own older copy
|
|
246
|
+
// would not, so the deploy does it first and says so (KYB-531).
|
|
247
|
+
"for c in $(command -v docker >/dev/null 2>&1 && docker ps --filter name=eve-sbx-tpl- --format '{{.Names}}' 2>/dev/null); do",
|
|
248
|
+
" live=$(docker top \"$c\" 2>/dev/null | tail -n +2 | awk '{print $8}' | grep -vc '^sleep$')",
|
|
249
|
+
' if [ "${live:-0}" = "0" ]; then echo "removing orphaned sandbox template container $c (only sleep inside; it would block every start)"; docker rm -f "$c" >/dev/null 2>&1 || true; fi',
|
|
250
|
+
"done",
|
|
242
251
|
'echo "using $SCRIPT"',
|
|
243
252
|
'setsid nohup bash "$SCRIPT" > /tmp/kyb-deploy.log 2>&1 < /dev/null &',
|
|
244
253
|
"sleep 2",
|
|
@@ -266,23 +275,45 @@ export async function deploy(options) {
|
|
|
266
275
|
let last = "";
|
|
267
276
|
let seen = "";
|
|
268
277
|
let lastChange = Date.now();
|
|
278
|
+
let lastBuildingNote = 0;
|
|
279
|
+
let stuck = null;
|
|
269
280
|
while (Date.now() - startedAt < CEILING_MS) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
281
|
+
// Two witnesses: the log, and the sandbox template containers. A cold
|
|
282
|
+
// template build is silent in the log for longer than the quiet limit,
|
|
283
|
+
// and judging it on the log alone is how a fine build got killed (KYB-531).
|
|
284
|
+
const out = execFileSync("ssh", [target, `tail -40 /tmp/kyb-deploy.log 2>/dev/null || true; echo __TEMPLATES__; ${TEMPLATE_CONTAINER_PROBE}`], { encoding: "utf8" });
|
|
285
|
+
const [logPart, probePart = ""] = out.split("__TEMPLATES__");
|
|
286
|
+
last = logPart;
|
|
287
|
+
if (logPart !== seen) {
|
|
288
|
+
for (const line of logPart.split("\n")) {
|
|
289
|
+
if (line && !seen.includes(line) && /npm|install|build|SOURCE IS NEWER|waiting|pid=|health:|FAILED|template|removing/i.test(line)) {
|
|
277
290
|
console.log(dim(` ${line.trim().slice(0, 120)}`));
|
|
278
291
|
}
|
|
279
292
|
}
|
|
280
|
-
seen =
|
|
293
|
+
seen = logPart;
|
|
294
|
+
lastChange = Date.now();
|
|
295
|
+
}
|
|
296
|
+
const verdict = assessRestart({
|
|
297
|
+
log: logPart,
|
|
298
|
+
templates: parseTemplateContainers(probePart),
|
|
299
|
+
quietMs: Date.now() - lastChange,
|
|
300
|
+
quietLimitMs: QUIET_LIMIT_MS,
|
|
301
|
+
});
|
|
302
|
+
if (verdict.state === "healthy" || verdict.state === "failed")
|
|
303
|
+
break;
|
|
304
|
+
if (verdict.state === "building") {
|
|
281
305
|
lastChange = Date.now();
|
|
306
|
+
if (Date.now() - lastBuildingNote > 60_000) {
|
|
307
|
+
console.log(dim(` still building the sandbox template (${Math.round((Date.now() - startedAt) / 60_000)} min): ${verdict.detail}`));
|
|
308
|
+
lastBuildingNote = Date.now();
|
|
309
|
+
}
|
|
282
310
|
}
|
|
283
|
-
if (
|
|
311
|
+
else if (verdict.state === "orphaned") {
|
|
312
|
+
stuck = verdict.detail ?? null;
|
|
313
|
+
console.log(yellow(`\n ! an orphaned sandbox template container is blocking the start: ${stuck}`));
|
|
284
314
|
break;
|
|
285
|
-
|
|
315
|
+
}
|
|
316
|
+
else if (verdict.state === "quiet") {
|
|
286
317
|
console.log(yellow(`\n ! nothing new on the host for ${Math.round(QUIET_LIMIT_MS / 60_000)} minutes — giving up on the wait`));
|
|
287
318
|
break;
|
|
288
319
|
}
|
|
@@ -296,8 +327,11 @@ export async function deploy(options) {
|
|
|
296
327
|
.join("\n"));
|
|
297
328
|
console.log(healthy
|
|
298
329
|
? green("\n ✓ deployed and serving the current build")
|
|
299
|
-
:
|
|
300
|
-
|
|
330
|
+
: stuck
|
|
331
|
+
? yellow(`\n ! the restart cannot finish while that container exists. Remove it on the host, then run kyb deploy again.\n` +
|
|
332
|
+
` ssh ${target} '${stuck.replace(/^.*remove it: /, "").replace(/\).*$/, "")}'`)
|
|
333
|
+
: yellow("\n ! the restart did not report health. It may still be building — this stops watching, it does not stop the host.\n" +
|
|
334
|
+
` Watch it: ssh ${target} 'tail -f /tmp/kyb-deploy.log'`));
|
|
301
335
|
if (!healthy)
|
|
302
336
|
process.exitCode = 1;
|
|
303
337
|
}
|
package/dist/doctor.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { TEMPLATE_CONTAINER_PROBE, parseTemplateContainers, templateContainerDoctorChecks } from "./sandbox-orphans.js";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { pathToFileURL } from "node:url";
|
|
4
5
|
import { bold, capture, dim, green, parseEnv, red, yellow } from "./util.js";
|
|
@@ -377,6 +378,11 @@ export async function doctor() {
|
|
|
377
378
|
* eventually produces looks like anything except a full disk.
|
|
378
379
|
*/
|
|
379
380
|
if (capture("sh", ["-c", "command -v docker >/dev/null && echo yes"])?.trim() === "yes") {
|
|
381
|
+
// A template container left behind by a killed start blocks every
|
|
382
|
+
// later start forever; a live one means "do not restart yet" (KYB-531).
|
|
383
|
+
for (const check of templateContainerDoctorChecks(parseTemplateContainers(capture("sh", ["-c", TEMPLATE_CONTAINER_PROBE])))) {
|
|
384
|
+
checks.push(check);
|
|
385
|
+
}
|
|
380
386
|
const job = capture("sh", ["-c", "test -x /etc/cron.daily/kyb-docker-prune && echo yes"])?.trim();
|
|
381
387
|
const percent = Number(capture("sh", ["-c", "df / | awk 'NR==2{print $5}' | tr -d '%'"])?.trim() ?? 0);
|
|
382
388
|
if (job !== "yes") {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Check } from "./doctor.js";
|
|
2
|
+
/**
|
|
3
|
+
* Docker sandbox template containers: the ones still building, and the ones
|
|
4
|
+
* left behind.
|
|
5
|
+
*
|
|
6
|
+
* eve builds each Docker sandbox template inside an `eve-sbx-tpl-*` container
|
|
7
|
+
* on first start. A cold build (apt-get, pnpm, playwright with its browsers)
|
|
8
|
+
* runs for minutes and prints nothing to the deploy log while it does. Two
|
|
9
|
+
* things went wrong with that on one deployment (KYB-531):
|
|
10
|
+
*
|
|
11
|
+
* - the host start script judged the start on a 45-second clock and printed
|
|
12
|
+
* "FAILED to start" while the template was building, so the operator killed
|
|
13
|
+
* the start;
|
|
14
|
+
* - the killed start left the container behind with `sleep 2147483647` as its
|
|
15
|
+
* only process, and while it existed every later `eve start` blocked forever
|
|
16
|
+
* at "initializing N sandbox templates" with nothing in any log to say why.
|
|
17
|
+
*
|
|
18
|
+
* Everything here reads one probe: each template container's name and how
|
|
19
|
+
* many processes inside it are not `sleep`. Building means at least one; an
|
|
20
|
+
* orphan means none.
|
|
21
|
+
*/
|
|
22
|
+
export interface TemplateContainer {
|
|
23
|
+
name: string;
|
|
24
|
+
/** Processes inside the container other than `sleep`. */
|
|
25
|
+
liveProcesses: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Shell that prints one `name count` line per `eve-sbx-tpl-*` container, or
|
|
29
|
+
* nothing when Docker is absent. The same probe serves doctor (locally), the
|
|
30
|
+
* deploy (over ssh), and the host start script.
|
|
31
|
+
*/
|
|
32
|
+
export declare const TEMPLATE_CONTAINER_PROBE: string;
|
|
33
|
+
export declare function parseTemplateContainers(output: string | null | undefined): TemplateContainer[];
|
|
34
|
+
export declare function orphanedTemplates(containers: readonly TemplateContainer[]): TemplateContainer[];
|
|
35
|
+
export declare function buildingTemplates(containers: readonly TemplateContainer[]): TemplateContainer[];
|
|
36
|
+
/** The exact command that clears an orphan; printed as the remedy and run by the start script. */
|
|
37
|
+
export declare function removeOrphanCommand(name: string): string;
|
|
38
|
+
export declare function templateContainerDoctorChecks(containers: readonly TemplateContainer[]): Check[];
|
|
39
|
+
export type RestartState = "healthy" | "failed" | "building" | "orphaned" | "quiet" | "waiting";
|
|
40
|
+
/**
|
|
41
|
+
* What a deploy should conclude from the host right now.
|
|
42
|
+
*
|
|
43
|
+
* The log alone was the whole verdict before, and a template build is silent
|
|
44
|
+
* in it. The template probe is the second witness: while something is building,
|
|
45
|
+
* silence is progress and the wait goes on; an orphan with nothing building
|
|
46
|
+
* and no server is a stuck host with a known remedy, not a slow one.
|
|
47
|
+
*/
|
|
48
|
+
export declare function assessRestart(input: {
|
|
49
|
+
log: string;
|
|
50
|
+
templates: readonly TemplateContainer[];
|
|
51
|
+
quietMs: number;
|
|
52
|
+
quietLimitMs: number;
|
|
53
|
+
}): {
|
|
54
|
+
state: RestartState;
|
|
55
|
+
detail?: string;
|
|
56
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell that prints one `name count` line per `eve-sbx-tpl-*` container, or
|
|
3
|
+
* nothing when Docker is absent. The same probe serves doctor (locally), the
|
|
4
|
+
* deploy (over ssh), and the host start script.
|
|
5
|
+
*/
|
|
6
|
+
export const TEMPLATE_CONTAINER_PROBE = [
|
|
7
|
+
"command -v docker >/dev/null 2>&1 || exit 0",
|
|
8
|
+
"for c in $(docker ps --filter name=eve-sbx-tpl- --format '{{.Names}}' 2>/dev/null); do",
|
|
9
|
+
" live=$(docker top \"$c\" 2>/dev/null | tail -n +2 | awk '{print $8}' | grep -vc '^sleep$')",
|
|
10
|
+
' echo "$c ${live:-0}"',
|
|
11
|
+
"done",
|
|
12
|
+
].join("\n");
|
|
13
|
+
export function parseTemplateContainers(output) {
|
|
14
|
+
return (output ?? "")
|
|
15
|
+
.split("\n")
|
|
16
|
+
.map((line) => line.trim())
|
|
17
|
+
.filter((line) => line.startsWith("eve-sbx-tpl-"))
|
|
18
|
+
.map((line) => {
|
|
19
|
+
const [name, count] = line.split(/\s+/);
|
|
20
|
+
return { name, liveProcesses: Number(count) || 0 };
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function orphanedTemplates(containers) {
|
|
24
|
+
return containers.filter((c) => c.liveProcesses === 0);
|
|
25
|
+
}
|
|
26
|
+
export function buildingTemplates(containers) {
|
|
27
|
+
return containers.filter((c) => c.liveProcesses > 0);
|
|
28
|
+
}
|
|
29
|
+
/** The exact command that clears an orphan; printed as the remedy and run by the start script. */
|
|
30
|
+
export function removeOrphanCommand(name) {
|
|
31
|
+
return `docker rm -f ${name}`;
|
|
32
|
+
}
|
|
33
|
+
export function templateContainerDoctorChecks(containers) {
|
|
34
|
+
const orphans = orphanedTemplates(containers);
|
|
35
|
+
const building = buildingTemplates(containers);
|
|
36
|
+
const checks = orphans.map((c) => ({
|
|
37
|
+
verdict: "fail",
|
|
38
|
+
label: `orphaned sandbox template container ${c.name}: every eve start will hang until it is removed`,
|
|
39
|
+
detail: `its only process is sleep; a start was killed mid-build. Remove it: ${removeOrphanCommand(c.name)}`,
|
|
40
|
+
}));
|
|
41
|
+
if (building.length) {
|
|
42
|
+
checks.push({
|
|
43
|
+
verdict: "warn",
|
|
44
|
+
label: `sandbox template building: ${building.map((c) => c.name).join(", ")}`,
|
|
45
|
+
detail: "a cold build takes minutes and the server binds its port only after it finishes; do not restart",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return checks;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* What a deploy should conclude from the host right now.
|
|
52
|
+
*
|
|
53
|
+
* The log alone was the whole verdict before, and a template build is silent
|
|
54
|
+
* in it. The template probe is the second witness: while something is building,
|
|
55
|
+
* silence is progress and the wait goes on; an orphan with nothing building
|
|
56
|
+
* and no server is a stuck host with a known remedy, not a slow one.
|
|
57
|
+
*/
|
|
58
|
+
export function assessRestart(input) {
|
|
59
|
+
if (/health:\s*200/.test(input.log))
|
|
60
|
+
return { state: "healthy" };
|
|
61
|
+
if (/FAILED/.test(input.log))
|
|
62
|
+
return { state: "failed" };
|
|
63
|
+
const building = buildingTemplates(input.templates);
|
|
64
|
+
if (building.length) {
|
|
65
|
+
return { state: "building", detail: building.map((c) => c.name).join(", ") };
|
|
66
|
+
}
|
|
67
|
+
const orphans = orphanedTemplates(input.templates);
|
|
68
|
+
if (orphans.length) {
|
|
69
|
+
return {
|
|
70
|
+
state: "orphaned",
|
|
71
|
+
detail: orphans.map((c) => `${c.name} (remove it: ${removeOrphanCommand(c.name)})`).join("; "),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (input.quietMs > input.quietLimitMs)
|
|
75
|
+
return { state: "quiet" };
|
|
76
|
+
return { state: "waiting" };
|
|
77
|
+
}
|
package/dist/upgrade.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { upsertEnv } from "./envfile.js";
|
|
@@ -237,6 +237,33 @@ function repairHostArtifacts(cwd, deps) {
|
|
|
237
237
|
const pruneSource = join(scripts, "docker-prune.sh");
|
|
238
238
|
const pruneTarget = "/etc/cron.daily/kyb-docker-prune";
|
|
239
239
|
const hasDocker = capture("sh", ["-c", "command -v docker >/dev/null && echo yes"])?.trim() === "yes";
|
|
240
|
+
// The host start script is package-owned too. It carried a 45-second
|
|
241
|
+
// verdict clock that called a cold sandbox template build a failure, and no
|
|
242
|
+
// sweep for the orphaned template container that a killed start leaves
|
|
243
|
+
// behind (KYB-531). An agent still running the copy `kyb init` made keeps
|
|
244
|
+
// both faults until something refreshes it; this does, on every upgrade.
|
|
245
|
+
const serverSource = join(scripts, "eve-server.sh");
|
|
246
|
+
const serverTarget = join(cwd, "scripts/eve-server.sh");
|
|
247
|
+
if (existsSync(serverSource) && existsSync(serverTarget)) {
|
|
248
|
+
reconcileHostArtifact({
|
|
249
|
+
targetPath: serverTarget,
|
|
250
|
+
desiredContent: readFileSync(serverSource),
|
|
251
|
+
expectedMode: 0o755,
|
|
252
|
+
installIfMissing: false,
|
|
253
|
+
owner: "@kybernesis/exe/scripts/eve-server.sh",
|
|
254
|
+
update: () => {
|
|
255
|
+
try {
|
|
256
|
+
copyFileSync(serverSource, serverTarget);
|
|
257
|
+
chmodSync(serverTarget, 0o755);
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
manualCommand: `cp ${shellQuote(serverSource)} scripts/eve-server.sh && chmod +x scripts/eve-server.sh`,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
240
267
|
if (existsSync(pruneSource)) {
|
|
241
268
|
const pruneResult = reconcileHostArtifact({
|
|
242
269
|
targetPath: pruneTarget,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kybernesis/create",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.8",
|
|
4
4
|
"description": "The Kybernesis agent scaffolder and FDE toolkit: one command to a governed, remembering, multiplayer, self-testing eve agent — plus doctor and upgrade.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|