@kybernesis/create 0.14.9 → 0.14.11
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/docker-prune-cron.d.ts +8 -0
- package/dist/docker-prune-cron.js +38 -0
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +17 -5
- package/dist/peer-tree.d.ts +37 -0
- package/dist/peer-tree.js +62 -0
- package/dist/upgrade.js +48 -13
- package/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function registeredDockerPruneAppDirs(installedContent: string | null): string[];
|
|
2
|
+
export interface DockerPruneCronArtifact {
|
|
3
|
+
content: Buffer;
|
|
4
|
+
appDirs: string[];
|
|
5
|
+
manualCommand: string;
|
|
6
|
+
}
|
|
7
|
+
/** Render the package script with a managed, mergeable app-directory header. */
|
|
8
|
+
export declare function dockerPruneCronArtifact(packageSource: string, currentAppDir: string, installedContent: string | null, targetPath: string): DockerPruneCronArtifact;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const MANAGED_PREFIX = "# kyb-managed-eve-app-dirs: ";
|
|
2
|
+
function shellSingleQuote(value) {
|
|
3
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
4
|
+
}
|
|
5
|
+
export function registeredDockerPruneAppDirs(installedContent) {
|
|
6
|
+
if (!installedContent)
|
|
7
|
+
return [];
|
|
8
|
+
const line = installedContent.split("\n").find((candidate) => candidate.startsWith(MANAGED_PREFIX));
|
|
9
|
+
if (!line)
|
|
10
|
+
return [];
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(line.slice(MANAGED_PREFIX.length));
|
|
13
|
+
return Array.isArray(parsed) && parsed.every((value) => typeof value === "string") ? parsed : [];
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Render the package script with a managed, mergeable app-directory header. */
|
|
20
|
+
export function dockerPruneCronArtifact(packageSource, currentAppDir, installedContent, targetPath) {
|
|
21
|
+
const appDirs = [...new Set([...registeredDockerPruneAppDirs(installedContent), currentAppDir])];
|
|
22
|
+
const newline = packageSource.indexOf("\n");
|
|
23
|
+
const shebang = newline === -1 ? packageSource : packageSource.slice(0, newline);
|
|
24
|
+
const body = newline === -1 ? "" : packageSource.slice(newline + 1);
|
|
25
|
+
const joined = appDirs.join(":");
|
|
26
|
+
const content = Buffer.from(`${shebang}\n` +
|
|
27
|
+
`${MANAGED_PREFIX}${JSON.stringify(appDirs)}\n` +
|
|
28
|
+
`if [ -z "\${EVE_APP_DIRS:-}" ]; then EVE_APP_DIRS=${shellSingleQuote(joined)}; fi\n` +
|
|
29
|
+
`export EVE_APP_DIRS\n` +
|
|
30
|
+
body);
|
|
31
|
+
const encoded = content.toString("base64");
|
|
32
|
+
return {
|
|
33
|
+
content,
|
|
34
|
+
appDirs,
|
|
35
|
+
manualCommand: `printf %s ${shellSingleQuote(encoded)} | base64 -d | ` +
|
|
36
|
+
`sudo install -m 0755 /dev/stdin ${shellSingleQuote(targetPath)}`,
|
|
37
|
+
};
|
|
38
|
+
}
|
package/dist/doctor.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface DockerTemplateInspection {
|
|
|
22
22
|
export declare function dockerTemplateDoctorChecks(result: DockerTemplateInspection): Check[];
|
|
23
23
|
export declare function modelReachDoctorCheck(authoredSource: string | null, compiledRouting: CompiledModelRouting): Check;
|
|
24
24
|
export declare function agentInputLimitDoctorCheck(limit: AgentInputLimit): Check;
|
|
25
|
+
export declare function diskUsageDoctorCheck(percent: number, dockerSummary: string | null): Check;
|
|
25
26
|
/**
|
|
26
27
|
* The inspector lives in @kybernesis/exe, next to the runtime that owns the
|
|
27
28
|
* templates; create already resolves that package's scripts out of
|
package/dist/doctor.js
CHANGED
|
@@ -84,6 +84,15 @@ export function agentInputLimitDoctorCheck(limit) {
|
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
+
export function diskUsageDoctorCheck(percent, dockerSummary) {
|
|
88
|
+
if (percent < 85)
|
|
89
|
+
return { verdict: "pass", label: `disk ${percent}% used` };
|
|
90
|
+
const detail = `Docker: ${dockerSummary?.trim() || "docker system df unavailable"}. ` +
|
|
91
|
+
`Run: sudo /etc/cron.daily/kyb-docker-prune`;
|
|
92
|
+
if (percent < 95)
|
|
93
|
+
return { verdict: "warn", label: `disk ${percent}% used`, detail };
|
|
94
|
+
return { verdict: "fail", label: `disk ${percent}% used`, detail };
|
|
95
|
+
}
|
|
87
96
|
/**
|
|
88
97
|
* The inspector lives in @kybernesis/exe, next to the runtime that owns the
|
|
89
98
|
* templates; create already resolves that package's scripts out of
|
|
@@ -406,14 +415,17 @@ export async function doctor() {
|
|
|
406
415
|
const job = capture("sh", ["-c", "test -x /etc/cron.daily/kyb-docker-prune && echo yes"])?.trim();
|
|
407
416
|
const percent = Number(capture("sh", ["-c", "df / | awk 'NR==2{print $5}' | tr -d '%'"])?.trim() ?? 0);
|
|
408
417
|
if (job !== "yes") {
|
|
409
|
-
add("warn", "no daily docker reclaim on this host", "sandbox templates, build cache, and generic stopped containers accumulate;
|
|
410
|
-
}
|
|
411
|
-
else if (percent >= 80) {
|
|
412
|
-
add("fail", `disk ${percent}% full despite the reclaim job`, "run it now: sudo /etc/cron.daily/kyb-docker-prune, and check what else is on this host");
|
|
418
|
+
add("warn", "no daily docker reclaim on this host", "sandbox templates, build cache, and generic stopped containers accumulate; `kyb upgrade` installs /etc/cron.daily/kyb-docker-prune");
|
|
413
419
|
}
|
|
414
420
|
else {
|
|
415
|
-
add("pass",
|
|
421
|
+
add("pass", "daily docker reclaim installed");
|
|
416
422
|
}
|
|
423
|
+
const dockerSummary = capture("sh", [
|
|
424
|
+
"-c",
|
|
425
|
+
"docker system df 2>/dev/null | head -3 | paste -sd ';' -",
|
|
426
|
+
]);
|
|
427
|
+
const diskCheck = diskUsageDoctorCheck(percent, dockerSummary);
|
|
428
|
+
add(diskCheck.verdict, diskCheck.label, diskCheck.detail);
|
|
417
429
|
}
|
|
418
430
|
/**
|
|
419
431
|
* Instructions that name a tool the agent does not have.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who, exactly, is unhappy about the installed eve.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* `kyb upgrade` refuses to touch a project whose peer tree is broken (KYB-538),
|
|
6
|
+
* because changing a project on a bad tree is how a half-upgraded host happens.
|
|
7
|
+
* That gate read `npm ls eve`'s exit code and nothing else, so any package
|
|
8
|
+
* anywhere in the tree could veto the whole upgrade.
|
|
9
|
+
*
|
|
10
|
+
* On 2026-09-08 one did. `@github-tools/eve-extension` still declares
|
|
11
|
+
* `>=0.44.0 <0.48.0` while every Kybernesis package declares the 0.49 line the
|
|
12
|
+
* host actually runs. The upgrade aborted before `repairHostArtifacts`, so
|
|
13
|
+
* kyber did not receive the KYB-542 reclaim job it was being upgraded for, and
|
|
14
|
+
* the job had to be installed by hand.
|
|
15
|
+
*
|
|
16
|
+
* A third-party range that trails the framework is not a broken install. A
|
|
17
|
+
* Kybernesis package disagreeing with the framework it is pinned against is.
|
|
18
|
+
* So the verdict distinguishes them, and anything it cannot attribute is
|
|
19
|
+
* treated as fatal rather than waved through.
|
|
20
|
+
*/
|
|
21
|
+
export interface PeerTreeVerdict {
|
|
22
|
+
/** Nothing in the tree objects to the installed version. */
|
|
23
|
+
readonly ok: boolean;
|
|
24
|
+
/** Packages whose declared range excludes the installed version, deduplicated and sorted. */
|
|
25
|
+
readonly offenders: readonly string[];
|
|
26
|
+
/** An objection from a Kybernesis package, or one that could not be attributed. */
|
|
27
|
+
readonly fatal: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Read `npm ls <pkg> --json` and say whether the upgrade may proceed.
|
|
31
|
+
*
|
|
32
|
+
* Unparseable output is fatal: not knowing the shape of the tree is exactly the
|
|
33
|
+
* case the gate exists for.
|
|
34
|
+
*/
|
|
35
|
+
export declare function assessPeerTree(stdout: string): PeerTreeVerdict;
|
|
36
|
+
/** What to print when a third-party range trails the framework but nothing else does. */
|
|
37
|
+
export declare function staleRangeWarning(offenders: readonly string[], dependency: string): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const OWNED_SCOPE = "@kybernesis/";
|
|
2
|
+
/** Every `from node_modules/<pkg>` in one npm `invalid` string. */
|
|
3
|
+
function requiredBy(invalid) {
|
|
4
|
+
const found = [];
|
|
5
|
+
const pattern = /from node_modules\/((?:@[^/\s,]+\/)?[^/\s,]+)/g;
|
|
6
|
+
let match = pattern.exec(invalid);
|
|
7
|
+
while (match !== null) {
|
|
8
|
+
if (match[1])
|
|
9
|
+
found.push(match[1]);
|
|
10
|
+
match = pattern.exec(invalid);
|
|
11
|
+
}
|
|
12
|
+
return found;
|
|
13
|
+
}
|
|
14
|
+
function collect(node, offenders, unattributed) {
|
|
15
|
+
if (!node || typeof node !== "object")
|
|
16
|
+
return;
|
|
17
|
+
const record = node;
|
|
18
|
+
if (typeof record.invalid === "string" && record.invalid.length > 0) {
|
|
19
|
+
const owners = requiredBy(record.invalid);
|
|
20
|
+
// An objection nobody signed is not evidence that the tree is fine.
|
|
21
|
+
if (owners.length === 0)
|
|
22
|
+
unattributed.seen = true;
|
|
23
|
+
for (const owner of owners)
|
|
24
|
+
offenders.add(owner);
|
|
25
|
+
}
|
|
26
|
+
const dependencies = record.dependencies;
|
|
27
|
+
if (dependencies && typeof dependencies === "object") {
|
|
28
|
+
for (const child of Object.values(dependencies)) {
|
|
29
|
+
collect(child, offenders, unattributed);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Read `npm ls <pkg> --json` and say whether the upgrade may proceed.
|
|
35
|
+
*
|
|
36
|
+
* Unparseable output is fatal: not knowing the shape of the tree is exactly the
|
|
37
|
+
* case the gate exists for.
|
|
38
|
+
*/
|
|
39
|
+
export function assessPeerTree(stdout) {
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(stdout);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return { ok: false, offenders: [], fatal: true };
|
|
46
|
+
}
|
|
47
|
+
const offenders = new Set();
|
|
48
|
+
const unattributed = { seen: false };
|
|
49
|
+
collect(parsed, offenders, unattributed);
|
|
50
|
+
if (offenders.size === 0 && !unattributed.seen)
|
|
51
|
+
return { ok: true, offenders: [], fatal: false };
|
|
52
|
+
const sorted = [...offenders].sort();
|
|
53
|
+
const ours = sorted.some((name) => name.startsWith(OWNED_SCOPE));
|
|
54
|
+
return { ok: false, offenders: sorted, fatal: ours || unattributed.seen };
|
|
55
|
+
}
|
|
56
|
+
/** What to print when a third-party range trails the framework but nothing else does. */
|
|
57
|
+
export function staleRangeWarning(offenders, dependency) {
|
|
58
|
+
const names = offenders.join(", ");
|
|
59
|
+
return (`${names} declare${offenders.length === 1 ? "s" : ""} a ${dependency} range that excludes the installed version. ` +
|
|
60
|
+
`Every Kybernesis package agrees with it, and the runtime already runs on it, so the upgrade continues. ` +
|
|
61
|
+
`Upgrade or replace ${offenders.length === 1 ? "that package" : "those packages"} when a compatible release exists.`);
|
|
62
|
+
}
|
package/dist/upgrade.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { pathToFileURL } from "node:url";
|
|
5
6
|
import { upsertEnv } from "./envfile.js";
|
|
6
7
|
import { reconcileHostArtifact } from "./host-artifacts.js";
|
|
8
|
+
import { assessPeerTree, staleRangeWarning } from "./peer-tree.js";
|
|
9
|
+
import { dockerPruneCronArtifact } from "./docker-prune-cron.js";
|
|
7
10
|
import { findMatchingAgentServiceUnit, repairManageRestart } from "./systemd.js";
|
|
8
11
|
import { repairTerminalSandboxCleanupHooks } from "./sandbox-cleanup.js";
|
|
9
12
|
import { repairRemovedDefaultTools as repairRemovedDefaultTools_ } from "./removed-default-tools.js";
|
|
@@ -246,7 +249,7 @@ function repairHostArtifacts(cwd, deps) {
|
|
|
246
249
|
return;
|
|
247
250
|
const scripts = join(cwd, "node_modules/@kybernesis/exe/scripts");
|
|
248
251
|
const pruneSource = join(scripts, "docker-prune.sh");
|
|
249
|
-
const pruneTarget = "/etc/cron.daily/kyb-docker-prune";
|
|
252
|
+
const pruneTarget = process.env.KYB_DOCKER_PRUNE_TARGET ?? "/etc/cron.daily/kyb-docker-prune";
|
|
250
253
|
const hasDocker = capture("sh", ["-c", "command -v docker >/dev/null && echo yes"])?.trim() === "yes";
|
|
251
254
|
// The host start script is package-owned too. It carried a 45-second
|
|
252
255
|
// verdict clock that called a cold sandbox template build a failure, and no
|
|
@@ -276,18 +279,37 @@ function repairHostArtifacts(cwd, deps) {
|
|
|
276
279
|
});
|
|
277
280
|
}
|
|
278
281
|
if (existsSync(pruneSource)) {
|
|
282
|
+
let installedPrune = null;
|
|
283
|
+
try {
|
|
284
|
+
if (existsSync(pruneTarget))
|
|
285
|
+
installedPrune = readFileSync(pruneTarget, "utf8");
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
// reconcileHostArtifact reports the unreadable target and refuses to replace it.
|
|
289
|
+
}
|
|
290
|
+
const pruneArtifact = dockerPruneCronArtifact(readFileSync(pruneSource, "utf8"), cwd, installedPrune, pruneTarget);
|
|
279
291
|
const pruneResult = reconcileHostArtifact({
|
|
280
292
|
targetPath: pruneTarget,
|
|
281
|
-
desiredContent:
|
|
293
|
+
desiredContent: pruneArtifact.content,
|
|
282
294
|
expectedMode: 0o755,
|
|
283
295
|
installIfMissing: hasDocker,
|
|
284
|
-
owner: "@kybernesis/exe/scripts/docker-prune.sh",
|
|
285
|
-
update: () =>
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
296
|
+
owner: "@kybernesis/exe/scripts/docker-prune.sh with registered app directories",
|
|
297
|
+
update: () => {
|
|
298
|
+
const stageDir = mkdtempSync(join(tmpdir(), "kyb-docker-prune-"));
|
|
299
|
+
const staged = join(stageDir, "docker-prune.sh");
|
|
300
|
+
try {
|
|
301
|
+
writeFileSync(staged, pruneArtifact.content, { mode: 0o755 });
|
|
302
|
+
return run("sudo", ["-n", "install", "-m", "0755", staged, pruneTarget], {
|
|
303
|
+
cwd,
|
|
304
|
+
allowFail: true,
|
|
305
|
+
quiet: true,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
rmSync(stageDir, { recursive: true, force: true });
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
manualCommand: pruneArtifact.manualCommand,
|
|
291
313
|
});
|
|
292
314
|
if ((pruneResult === "current" || pruneResult === "updated") && existsSync("/etc/cron.weekly/docker-prune")) {
|
|
293
315
|
const removed = run("sudo", ["-n", "rm", "-f", "/etc/cron.weekly/docker-prune"], {
|
|
@@ -800,9 +822,22 @@ export async function upgrade(options = {}) {
|
|
|
800
822
|
}
|
|
801
823
|
const validation = commandResult("npm", ["ls", "eve"], cwd);
|
|
802
824
|
if (validation.status !== 0) {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
825
|
+
/**
|
|
826
|
+
* Judge WHO objects, not merely that something did.
|
|
827
|
+
*
|
|
828
|
+
* A third-party extension whose declared range trails the framework is not
|
|
829
|
+
* a broken install, and letting it veto the upgrade left kyber without the
|
|
830
|
+
* host artifacts it was being upgraded for (KYB-548). A Kybernesis package
|
|
831
|
+
* disagreeing with the framework it is pinned against still stops
|
|
832
|
+
* everything, and so does an objection we cannot attribute.
|
|
833
|
+
*/
|
|
834
|
+
const verdict = assessPeerTree(commandResult("npm", ["ls", "eve", "--json"], cwd, false).stdout);
|
|
835
|
+
if (verdict.fatal || verdict.ok) {
|
|
836
|
+
fail("npm ls eve failed. The installed peer tree is not valid.");
|
|
837
|
+
recoveryGuidance(targets, stoppedBridge ?? undefined);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
console.log(yellow(` ! ${staleRangeWarning(verdict.offenders, "eve")}`));
|
|
806
841
|
}
|
|
807
842
|
if (stoppedBridge && !startStoppedBridge(cwd, stoppedBridge))
|
|
808
843
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kybernesis/create",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.11",
|
|
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",
|