garaje 0.1.2 → 0.1.4

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 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 { runSyncList } from "./host/sync.js";
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 runSyncList(root, "sync", rest);
50
+ return runSync(root, rest);
51
51
  case "list":
52
- return runSyncList(root, "list", rest);
52
+ return runList(root);
53
53
  case "park":
54
54
  return runPark(root, rest).code;
55
55
  case "sessions-index":
@@ -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 (delegate to the retained bash list) ---
78
+ // --- bays ---
61
79
  section("bays");
62
- const list = spawnSync("bash", [join(root, "bin", "garaje-sync"), "list"], { encoding: "utf8" });
63
- const bayLines = (list.stdout ?? "").split(/\r?\n/).slice(1).filter((l) => l.trim());
64
- if (bayLines.length)
65
- bayLines.forEach((l) => pass(l));
66
- else
67
- warn("no bays parked (add one to garaje.yaml, then garaje sync)");
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 synced bays — artifact staleness not checkable");
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
- const unsynced = manifestBays(root).filter((b) => !discoverBays(root).includes(b));
90
- if (unsynced.length) {
91
- warn(`manifest bay(s) not synced: ${unsynced.join(", ")} — the artifact excludes them (run garaje sync)`);
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,110 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { existsSync, mkdirSync } from "node:fs";
2
3
  import { join } from "node:path";
3
- /** `garaje sync` / `garaje list` — delegate to the retained bash helper. */
4
- export function runSyncList(root, sub, extra) {
5
- const helper = join(root, "bin", "garaje-sync");
6
- return spawnSync("bash", [helper, sub, ...extra], { cwd: root, stdio: "inherit" }).status ?? 1;
4
+ import { readManifest } from "../manifest.js";
5
+ function git(args) {
6
+ return spawnSync("git", args, { stdio: "inherit" }).status ?? 1;
7
+ }
8
+ /** Read the manifest, reporting a bad one as an error rather than a stack
9
+ * trace. A `bays: []` line followed by list items is the common way to get
10
+ * here — it looks like an append but is not valid YAML. */
11
+ function readOrReport(root) {
12
+ try {
13
+ return readManifest(root);
14
+ }
15
+ catch (e) {
16
+ const first = e.message.split("\n")[0];
17
+ process.stderr.write(`error: garaje.yaml: ${first}\n`);
18
+ process.stderr.write("hint: bays must be a YAML list; an empty `bays: []` cannot be followed by `- name:` items\n");
19
+ return undefined;
20
+ }
21
+ }
22
+ /** Whether one manifest bay is materialized, and at which commit. */
23
+ function bayStatus(root, bay) {
24
+ const dest = join(root, "bays", bay.name);
25
+ if (!existsSync(join(dest, ".git")))
26
+ return { ...bay, synced: false };
27
+ const rev = spawnSync("git", ["-C", dest, "rev-parse", "--short", "HEAD"], {
28
+ encoding: "utf8",
29
+ });
30
+ return { ...bay, synced: true, head: rev.status === 0 ? rev.stdout.trim() : "?" };
31
+ }
32
+ /** Cross the manifest with what is actually checked out under bays/. Throws on
33
+ * a bad manifest; doctor reports that as a failed check. */
34
+ export function bayStatuses(root) {
35
+ return readManifest(root).map((bay) => bayStatus(root, bay));
36
+ }
37
+ /** One human-readable line per bay. Pure — doctor reuses this. */
38
+ export function statusLine(b) {
39
+ return b.synced ? `synced @ ${b.head}` : "not synced";
40
+ }
41
+ /** The `garaje list` table. Pure. */
42
+ export function renderList(rows) {
43
+ const w = (header, pick) => Math.max(header.length, ...rows.map((b) => pick(b).length || 1));
44
+ const repo = (b) => b.repo || "-";
45
+ const wName = w("BAY", (b) => b.name);
46
+ const wRepo = w("REPO", repo);
47
+ const wRef = w("REF", (b) => b.ref);
48
+ const line = (a, b, c, d) => `${a.padEnd(wName)} ${b.padEnd(wRepo)} ${c.padEnd(wRef)} ${d}\n`;
49
+ return (line("BAY", "REPO", "REF", "STATUS") +
50
+ rows.map((b) => line(b.name, repo(b), b.ref, statusLine(b))).join(""));
51
+ }
52
+ /** `garaje list` — show the manifest and which bays are materialized. */
53
+ export function runList(root) {
54
+ const entries = readOrReport(root);
55
+ if (!entries)
56
+ return 1;
57
+ process.stdout.write(renderList(entries.map((b) => bayStatus(root, b))));
58
+ return 0;
59
+ }
60
+ /** `garaje sync [name...]` — clone/update the manifest's bays into bays/. */
61
+ export function runSync(root, names) {
62
+ if (spawnSync("git", ["--version"], { stdio: "ignore" }).status !== 0) {
63
+ process.stderr.write("error: git not found in PATH\n");
64
+ return 1;
65
+ }
66
+ const entries = readOrReport(root);
67
+ if (!entries)
68
+ return 1;
69
+ const unknown = names.filter((n) => !entries.some((b) => b.name === n));
70
+ if (unknown.length) {
71
+ process.stderr.write(`error: not in garaje.yaml: ${unknown.join(", ")} (declare the bay in the manifest first)\n`);
72
+ return 1;
73
+ }
74
+ const wanted = names.length ? entries.filter((b) => names.includes(b.name)) : entries;
75
+ mkdirSync(join(root, "bays"), { recursive: true });
76
+ let synced = 0;
77
+ for (const bay of wanted) {
78
+ if (!bay.repo) {
79
+ process.stderr.write(`skip ${bay.name}: no repo in manifest\n`);
80
+ continue;
81
+ }
82
+ const dest = join(root, "bays", bay.name);
83
+ if (existsSync(join(dest, ".git"))) {
84
+ process.stdout.write(`==> ${bay.name}: fetching (${bay.ref})\n`);
85
+ if (git(["-C", dest, "fetch", "--quiet", "origin", bay.ref]) !== 0 ||
86
+ git(["-C", dest, "checkout", "--quiet", bay.ref]) !== 0) {
87
+ process.stderr.write(`error: ${bay.name}: fetch/checkout of ${bay.ref} failed\n`);
88
+ return 1;
89
+ }
90
+ // Fast-forward only when ref names a branch. A tag or sha leaves HEAD
91
+ // detached, and there is nothing to advance — that is not a failure.
92
+ const onBranch = spawnSync("git", ["-C", dest, "symbolic-ref", "-q", "HEAD"], { stdio: "ignore" })
93
+ .status === 0;
94
+ if (onBranch && git(["-C", dest, "merge", "--quiet", "--ff-only", "FETCH_HEAD"]) !== 0) {
95
+ process.stderr.write(`error: ${bay.name}: ${bay.ref} is not fast-forwardable\n`);
96
+ return 1;
97
+ }
98
+ }
99
+ else {
100
+ process.stdout.write(`==> ${bay.name}: cloning ${bay.repo} (${bay.ref})\n`);
101
+ if (git(["clone", "--quiet", "--branch", bay.ref, bay.repo, dest]) !== 0) {
102
+ process.stderr.write(`error: ${bay.name}: clone failed\n`);
103
+ return 1;
104
+ }
105
+ }
106
+ synced += 1;
107
+ }
108
+ process.stdout.write(`synced ${synced} bay(s) into bays/\n`);
109
+ return 0;
7
110
  }
@@ -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
+ }
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { parse as parseYaml } from "yaml";
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
- const data = parseYaml(readFileSync(path, "utf8"));
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 [];
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "garaje",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",