garaje 0.1.2 → 0.1.3
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 +3 -3
- package/dist/host/doctor.js +45 -14
- package/dist/host/sync.js +85 -4
- package/dist/manifest.js +24 -0
- package/dist/park/index.js +5 -9
- package/dist/upgrade/pins.js +15 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import { runLogs } from "./host/logs.js";
|
|
|
9
9
|
import { runPi } from "./host/pi.js";
|
|
10
10
|
import { runAttach } from "./host/attach.js";
|
|
11
11
|
import { runDoctor } from "./host/doctor.js";
|
|
12
|
-
import {
|
|
12
|
+
import { runList, runSync } from "./host/sync.js";
|
|
13
13
|
import { runAuth } from "./host/auth.js";
|
|
14
14
|
import { runSessionsIndex } from "./sessions/run.js";
|
|
15
15
|
import { inContainer } from "./driver/index.js";
|
|
@@ -47,9 +47,9 @@ export function main(argv) {
|
|
|
47
47
|
case "doctor":
|
|
48
48
|
return runDoctor(root);
|
|
49
49
|
case "sync":
|
|
50
|
-
return
|
|
50
|
+
return runSync(root, rest);
|
|
51
51
|
case "list":
|
|
52
|
-
return
|
|
52
|
+
return runList(root);
|
|
53
53
|
case "park":
|
|
54
54
|
return runPark(root, rest).code;
|
|
55
55
|
case "sessions-index":
|
package/dist/host/doctor.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
4
|
import { driverContext, getDriver } from "../driver/index.js";
|
|
5
5
|
import { baysArtifactState, discoverBays, manifestBays } from "../park/index.js";
|
|
6
|
+
import { basePinSource } from "../upgrade/pins.js";
|
|
7
|
+
import { bayStatuses, statusLine } from "./sync.js";
|
|
6
8
|
/** `garaje doctor` — host-side sanity checks. Exit code = failed-check count. */
|
|
7
9
|
export function runDoctor(root) {
|
|
8
10
|
let fails = 0;
|
|
@@ -33,7 +35,6 @@ export function runDoctor(root) {
|
|
|
33
35
|
"garaje.yaml",
|
|
34
36
|
"pi/Dockerfile",
|
|
35
37
|
"pi/entrypoint.sh",
|
|
36
|
-
"packages/base/package.json",
|
|
37
38
|
".pi/settings.json",
|
|
38
39
|
]) {
|
|
39
40
|
if (existsSync(join(root, f)))
|
|
@@ -41,6 +42,23 @@ export function runDoctor(root) {
|
|
|
41
42
|
else
|
|
42
43
|
fail(`${f} missing`);
|
|
43
44
|
}
|
|
45
|
+
// The base package is an npm pin in a scaffolded garaje and a relative path
|
|
46
|
+
// in the framework monorepo. Only the path form has anything on disk to check.
|
|
47
|
+
const settings = join(root, ".pi", "settings.json");
|
|
48
|
+
const source = existsSync(settings)
|
|
49
|
+
? basePinSource(readFileSync(settings, "utf8"))
|
|
50
|
+
: undefined;
|
|
51
|
+
if (!source)
|
|
52
|
+
fail("@garaje/base is not pinned in .pi/settings.json");
|
|
53
|
+
else if (source.startsWith("npm:"))
|
|
54
|
+
pass(`@garaje/base pinned: ${source}`);
|
|
55
|
+
else {
|
|
56
|
+
const dir = resolve(join(root, ".pi"), source);
|
|
57
|
+
if (existsSync(join(dir, "package.json")))
|
|
58
|
+
pass(`@garaje/base pinned: ${source}`);
|
|
59
|
+
else
|
|
60
|
+
fail(`@garaje/base path pin "${source}" missing — no package.json at ${dir}`);
|
|
61
|
+
}
|
|
44
62
|
if (existsSync(join(root, ".env")))
|
|
45
63
|
pass(".env present");
|
|
46
64
|
else
|
|
@@ -57,14 +75,24 @@ export function runDoctor(root) {
|
|
|
57
75
|
pass("pi service running");
|
|
58
76
|
else
|
|
59
77
|
fail("pi service NOT running (try garaje up)");
|
|
60
|
-
// --- bays
|
|
78
|
+
// --- bays ---
|
|
61
79
|
section("bays");
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
80
|
+
// readManifest throws on a missing/corrupt garaje.yaml; doctor reports that
|
|
81
|
+
// as a failed check rather than crashing (same contract as the artifact check).
|
|
82
|
+
try {
|
|
83
|
+
const bays = bayStatuses(root);
|
|
84
|
+
if (!bays.length)
|
|
85
|
+
warn("no bays parked (add one to garaje.yaml, then garaje sync)");
|
|
86
|
+
for (const b of bays) {
|
|
87
|
+
if (b.synced)
|
|
88
|
+
pass(`${b.name}: ${statusLine(b)} (${b.ref})`);
|
|
89
|
+
else
|
|
90
|
+
warn(`${b.name}: ${statusLine(b)} — run \`garaje sync ${b.name}\``);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
fail(`bay status check errored — garaje.yaml failed to parse: ${e.message}`);
|
|
95
|
+
}
|
|
68
96
|
const unhealthy = driver.psText()
|
|
69
97
|
.split(/\r?\n/)
|
|
70
98
|
.filter((l) => /unhealthy/i.test(l));
|
|
@@ -77,7 +105,7 @@ export function runDoctor(root) {
|
|
|
77
105
|
if (artifact === "fresh")
|
|
78
106
|
pass("bays artifact matches the recipes (regenerate-and-diff)");
|
|
79
107
|
else if (artifact === "no-bays")
|
|
80
|
-
warn("no
|
|
108
|
+
warn("no parked bays (no recipe.yaml) — artifact staleness not checkable");
|
|
81
109
|
else if (artifact === "missing")
|
|
82
110
|
fail("bays artifact missing — run `garaje park` and commit the result");
|
|
83
111
|
else
|
|
@@ -86,9 +114,12 @@ export function runDoctor(root) {
|
|
|
86
114
|
catch (e) {
|
|
87
115
|
fail(`bays artifact check errored — a recipe failed to parse: ${e.message}`);
|
|
88
116
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
117
|
+
// "Parked" (has a recipe.yaml) is a stricter state than "synced" (cloned);
|
|
118
|
+
// only parked bays contribute to the artifact.
|
|
119
|
+
const parked = discoverBays(root);
|
|
120
|
+
const unparked = manifestBays(root).filter((b) => !parked.includes(b));
|
|
121
|
+
if (unparked.length) {
|
|
122
|
+
warn(`manifest bay(s) with no recipe.yaml: ${unparked.join(", ")} — the artifact excludes them`);
|
|
92
123
|
}
|
|
93
124
|
// --- pi container ---
|
|
94
125
|
section("pi");
|
package/dist/host/sync.js
CHANGED
|
@@ -1,7 +1,88 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import { readManifest } from "../manifest.js";
|
|
5
|
+
function git(args) {
|
|
6
|
+
return spawnSync("git", args, { stdio: "inherit" }).status ?? 1;
|
|
7
|
+
}
|
|
8
|
+
/** Cross the manifest with what is actually checked out under bays/. */
|
|
9
|
+
export function bayStatuses(root) {
|
|
10
|
+
return readManifest(root).map((bay) => {
|
|
11
|
+
const dest = join(root, "bays", bay.name);
|
|
12
|
+
if (!existsSync(join(dest, ".git")))
|
|
13
|
+
return { ...bay, synced: false };
|
|
14
|
+
const rev = spawnSync("git", ["-C", dest, "rev-parse", "--short", "HEAD"], {
|
|
15
|
+
encoding: "utf8",
|
|
16
|
+
});
|
|
17
|
+
return { ...bay, synced: true, head: rev.status === 0 ? rev.stdout.trim() : "?" };
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** One human-readable line per bay. Pure — doctor reuses this. */
|
|
21
|
+
export function statusLine(b) {
|
|
22
|
+
return b.synced ? `synced @ ${b.head}` : "not synced";
|
|
23
|
+
}
|
|
24
|
+
/** The `garaje list` table. Pure. */
|
|
25
|
+
export function renderList(rows) {
|
|
26
|
+
const w = (header, pick) => Math.max(header.length, ...rows.map((b) => pick(b).length || 1));
|
|
27
|
+
const repo = (b) => b.repo || "-";
|
|
28
|
+
const wName = w("BAY", (b) => b.name);
|
|
29
|
+
const wRepo = w("REPO", repo);
|
|
30
|
+
const wRef = w("REF", (b) => b.ref);
|
|
31
|
+
const line = (a, b, c, d) => `${a.padEnd(wName)} ${b.padEnd(wRepo)} ${c.padEnd(wRef)} ${d}\n`;
|
|
32
|
+
return (line("BAY", "REPO", "REF", "STATUS") +
|
|
33
|
+
rows.map((b) => line(b.name, repo(b), b.ref, statusLine(b))).join(""));
|
|
34
|
+
}
|
|
35
|
+
/** `garaje list` — show the manifest and which bays are materialized. */
|
|
36
|
+
export function runList(root) {
|
|
37
|
+
process.stdout.write(renderList(bayStatuses(root)));
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
/** `garaje sync [name...]` — clone/update the manifest's bays into bays/. */
|
|
41
|
+
export function runSync(root, names) {
|
|
42
|
+
if (spawnSync("git", ["--version"], { stdio: "ignore" }).status !== 0) {
|
|
43
|
+
process.stderr.write("error: git not found in PATH\n");
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
const entries = readManifest(root);
|
|
47
|
+
const unknown = names.filter((n) => !entries.some((b) => b.name === n));
|
|
48
|
+
if (unknown.length) {
|
|
49
|
+
process.stderr.write(`error: not in garaje.yaml: ${unknown.join(", ")} (declare the bay in the manifest first)\n`);
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
const wanted = names.length ? entries.filter((b) => names.includes(b.name)) : entries;
|
|
53
|
+
mkdirSync(join(root, "bays"), { recursive: true });
|
|
54
|
+
let synced = 0;
|
|
55
|
+
for (const bay of wanted) {
|
|
56
|
+
if (!bay.repo) {
|
|
57
|
+
process.stderr.write(`skip ${bay.name}: no repo in manifest\n`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const dest = join(root, "bays", bay.name);
|
|
61
|
+
if (existsSync(join(dest, ".git"))) {
|
|
62
|
+
process.stdout.write(`==> ${bay.name}: fetching (${bay.ref})\n`);
|
|
63
|
+
if (git(["-C", dest, "fetch", "--quiet", "origin", bay.ref]) !== 0 ||
|
|
64
|
+
git(["-C", dest, "checkout", "--quiet", bay.ref]) !== 0) {
|
|
65
|
+
process.stderr.write(`error: ${bay.name}: fetch/checkout of ${bay.ref} failed\n`);
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
// Fast-forward only when ref names a branch. A tag or sha leaves HEAD
|
|
69
|
+
// detached, and there is nothing to advance — that is not a failure.
|
|
70
|
+
const onBranch = spawnSync("git", ["-C", dest, "symbolic-ref", "-q", "HEAD"], { stdio: "ignore" })
|
|
71
|
+
.status === 0;
|
|
72
|
+
if (onBranch && git(["-C", dest, "merge", "--quiet", "--ff-only", "FETCH_HEAD"]) !== 0) {
|
|
73
|
+
process.stderr.write(`error: ${bay.name}: ${bay.ref} is not fast-forwardable\n`);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
process.stdout.write(`==> ${bay.name}: cloning ${bay.repo} (${bay.ref})\n`);
|
|
79
|
+
if (git(["clone", "--quiet", "--branch", bay.ref, bay.repo, dest]) !== 0) {
|
|
80
|
+
process.stderr.write(`error: ${bay.name}: clone failed\n`);
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
synced += 1;
|
|
85
|
+
}
|
|
86
|
+
process.stdout.write(`synced ${synced} bay(s) into bays/\n`);
|
|
87
|
+
return 0;
|
|
7
88
|
}
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parse as parseYaml } from "yaml";
|
|
4
|
+
/** Parse the `bays:` list out of a garaje.yaml. Entries without a name are
|
|
5
|
+
* dropped; `ref` defaults to main. Pure — throws only on malformed YAML. */
|
|
6
|
+
export function parseManifest(text) {
|
|
7
|
+
const data = parseYaml(text);
|
|
8
|
+
const bays = Array.isArray(data?.bays) ? data.bays : [];
|
|
9
|
+
return bays
|
|
10
|
+
.filter((b) => Boolean(b) && typeof b === "object")
|
|
11
|
+
.map((b) => ({
|
|
12
|
+
name: String(b.name ?? "").trim(),
|
|
13
|
+
repo: String(b.repo ?? "").trim(),
|
|
14
|
+
ref: String(b.ref ?? "main").trim() || "main",
|
|
15
|
+
}))
|
|
16
|
+
.filter((b) => b.name);
|
|
17
|
+
}
|
|
18
|
+
/** Read <root>/garaje.yaml. Throws if it is missing or unparseable. */
|
|
19
|
+
export function readManifest(root) {
|
|
20
|
+
const path = join(root, "garaje.yaml");
|
|
21
|
+
if (!existsSync(path))
|
|
22
|
+
throw new Error("no garaje.yaml at the garaje root");
|
|
23
|
+
return parseManifest(readFileSync(path, "utf8"));
|
|
24
|
+
}
|
package/dist/park/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { readManifest } from "../manifest.js";
|
|
4
4
|
import { parseRecipe } from "./recipe.js";
|
|
5
5
|
import { generate } from "./compose.js";
|
|
6
6
|
/** Bay folder names under <root>/bays that contain a recipe.yaml, sorted. */
|
|
@@ -13,16 +13,12 @@ export function discoverBays(root) {
|
|
|
13
13
|
.map((e) => e.name)
|
|
14
14
|
.sort();
|
|
15
15
|
}
|
|
16
|
-
/** Bay names declared in the committed manifest (garaje.yaml).
|
|
16
|
+
/** Bay names declared in the committed manifest (garaje.yaml). Lenient: a
|
|
17
|
+
* missing or corrupt manifest yields no bays rather than throwing, because
|
|
18
|
+
* every caller here is a check that reports on it separately. */
|
|
17
19
|
export function manifestBays(root) {
|
|
18
|
-
const path = join(root, "garaje.yaml");
|
|
19
|
-
if (!existsSync(path))
|
|
20
|
-
return [];
|
|
21
20
|
try {
|
|
22
|
-
|
|
23
|
-
return (data?.bays ?? [])
|
|
24
|
-
.map((b) => b?.name)
|
|
25
|
-
.filter((n) => Boolean(n));
|
|
21
|
+
return readManifest(root).map((b) => b.name);
|
|
26
22
|
}
|
|
27
23
|
catch {
|
|
28
24
|
return [];
|
package/dist/upgrade/pins.js
CHANGED
|
@@ -11,3 +11,18 @@ export function currentBaseVersion(settingsText) {
|
|
|
11
11
|
const m = settingsText.match(/npm:@garaje\/base@([0-9A-Za-z.\-]+)/);
|
|
12
12
|
return m ? m[1] : undefined;
|
|
13
13
|
}
|
|
14
|
+
/** The @garaje/base package source pinned in .pi/settings.json: an `npm:` spec
|
|
15
|
+
* in a scaffolded garaje, a relative path in the framework monorepo. */
|
|
16
|
+
export function basePinSource(settingsText) {
|
|
17
|
+
let sources;
|
|
18
|
+
try {
|
|
19
|
+
const data = JSON.parse(settingsText);
|
|
20
|
+
sources = (data.packages ?? [])
|
|
21
|
+
.map((p) => p?.source)
|
|
22
|
+
.filter((s) => Boolean(s));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
return sources.find((s) => /@garaje\/base|packages\/base/.test(s)) ?? sources[0];
|
|
28
|
+
}
|