garaje 0.1.0
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/LICENSE +21 -0
- package/dist/cli.js +74 -0
- package/dist/driver/compose.js +127 -0
- package/dist/driver/index.js +33 -0
- package/dist/driver/types.js +7 -0
- package/dist/host/attach.js +12 -0
- package/dist/host/auth-presets.js +13 -0
- package/dist/host/auth-resolvers.js +46 -0
- package/dist/host/auth.js +140 -0
- package/dist/host/bay.js +61 -0
- package/dist/host/compose.js +65 -0
- package/dist/host/docker.js +15 -0
- package/dist/host/doctor.js +119 -0
- package/dist/host/down.js +17 -0
- package/dist/host/init.js +131 -0
- package/dist/host/logs.js +12 -0
- package/dist/host/pi.js +15 -0
- package/dist/host/sync.js +7 -0
- package/dist/host/up.js +34 -0
- package/dist/park/compose.js +190 -0
- package/dist/park/dockerfile.js +23 -0
- package/dist/park/index.js +77 -0
- package/dist/park/ports.js +11 -0
- package/dist/park/recipe.js +12 -0
- package/dist/sessions/collect.js +86 -0
- package/dist/sessions/render.js +24 -0
- package/dist/sessions/run.js +14 -0
- package/dist/upgrade/classify.js +137 -0
- package/dist/upgrade/index.js +94 -0
- package/dist/upgrade/pins.js +13 -0
- package/package.json +26 -0
- package/templates/.agent/README.md +17 -0
- package/templates/.agent/commands/.gitkeep +0 -0
- package/templates/.agent/rules/.gitkeep +0 -0
- package/templates/.agent/skills/.gitkeep +0 -0
- package/templates/.env.example +3 -0
- package/templates/.garaje-version +1 -0
- package/templates/.pi/README.md +17 -0
- package/templates/.pi/settings.json +13 -0
- package/templates/AGENTS.md +38 -0
- package/templates/compose.framework.yaml +60 -0
- package/templates/docker-compose.yml +13 -0
- package/templates/garaje.yaml +14 -0
- package/templates/gitignore +26 -0
- package/templates/pi/Dockerfile +56 -0
- package/templates/pi/PI_VERSION +1 -0
- package/templates/pi/entrypoint.sh +49 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const PREVIEW_LEN = 60;
|
|
4
|
+
function clean(text) {
|
|
5
|
+
return text.replace(/[\r\n]+/g, " ").trim().split(/\s+/).join(" ");
|
|
6
|
+
}
|
|
7
|
+
/** Parse one .jsonl session file into a row, or null if it isn't a session. */
|
|
8
|
+
function parseSession(path) {
|
|
9
|
+
let lines;
|
|
10
|
+
try {
|
|
11
|
+
lines = readFileSync(path, "utf8").split(/\r?\n/);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
let header;
|
|
17
|
+
try {
|
|
18
|
+
header = JSON.parse(lines[0] ?? "");
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (!header || header.type !== "session")
|
|
24
|
+
return null;
|
|
25
|
+
let messages = 0;
|
|
26
|
+
let cost = 0;
|
|
27
|
+
let preview = "";
|
|
28
|
+
for (const line of lines.slice(1)) {
|
|
29
|
+
if (!line)
|
|
30
|
+
continue;
|
|
31
|
+
let obj;
|
|
32
|
+
try {
|
|
33
|
+
obj = JSON.parse(line);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (obj?.type !== "message")
|
|
39
|
+
continue;
|
|
40
|
+
messages += 1;
|
|
41
|
+
const msg = obj.message ?? {};
|
|
42
|
+
if (msg.role === "user" && !preview) {
|
|
43
|
+
const content = msg.content;
|
|
44
|
+
if (typeof content === "string")
|
|
45
|
+
preview = content;
|
|
46
|
+
else if (Array.isArray(content)) {
|
|
47
|
+
const block = content.find((b) => b && b.type === "text");
|
|
48
|
+
if (block)
|
|
49
|
+
preview = block.text ?? "";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (msg.role === "assistant") {
|
|
53
|
+
cost += Number(msg.usage?.cost?.total ?? 0) || 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const mtime = statSync(path).mtimeMs;
|
|
57
|
+
return {
|
|
58
|
+
id: header.id ?? "",
|
|
59
|
+
last: new Date(mtime).toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
60
|
+
messages,
|
|
61
|
+
cost,
|
|
62
|
+
preview: clean(preview).slice(0, PREVIEW_LEN),
|
|
63
|
+
mtime,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// All parseable sessions under <root>/sessions/*/*.jsonl, newest-first.
|
|
67
|
+
export function collectSessions(root) {
|
|
68
|
+
const dir = join(root, "sessions");
|
|
69
|
+
if (!existsSync(dir))
|
|
70
|
+
return [];
|
|
71
|
+
const rows = [];
|
|
72
|
+
for (const sub of readdirSync(dir, { withFileTypes: true })) {
|
|
73
|
+
if (!sub.isDirectory())
|
|
74
|
+
continue;
|
|
75
|
+
const subPath = join(dir, sub.name);
|
|
76
|
+
for (const f of readdirSync(subPath)) {
|
|
77
|
+
if (!f.endsWith(".jsonl"))
|
|
78
|
+
continue;
|
|
79
|
+
const parsed = parseSession(join(subPath, f));
|
|
80
|
+
if (parsed)
|
|
81
|
+
rows.push(parsed);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
rows.sort((a, b) => b.mtime - a.mtime);
|
|
85
|
+
return rows.map(({ mtime, ...row }) => row);
|
|
86
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const MAX_ROWS = 100;
|
|
2
|
+
/** Render sessions/INDEX.md. Pure: caller supplies rows + the generated-at stamp. */
|
|
3
|
+
export function renderIndex(rows, generatedAt) {
|
|
4
|
+
const lines = [
|
|
5
|
+
"# Sessions index",
|
|
6
|
+
"",
|
|
7
|
+
`_Auto-generated by \`garaje sessions-index\` at ${generatedAt}._ Regenerated on every \`garaje up\`.`,
|
|
8
|
+
"",
|
|
9
|
+
`${rows.length} session(s) total. Showing newest ${Math.min(rows.length, MAX_ROWS)}.`,
|
|
10
|
+
"",
|
|
11
|
+
"Resume by short id with `garaje pi --session <id>`, or pick interactively with `garaje pi -r`.",
|
|
12
|
+
"",
|
|
13
|
+
"| last activity | id | msgs | cost | preview |",
|
|
14
|
+
"|---|---|---:|---:|---|",
|
|
15
|
+
];
|
|
16
|
+
for (const s of rows.slice(0, MAX_ROWS)) {
|
|
17
|
+
const shortId = s.id ? s.id.slice(0, 8) : "?";
|
|
18
|
+
const preview = (s.preview || "_(no user messages yet)_").replace(/\|/g, "\\|");
|
|
19
|
+
const cost = s.cost ? `$${s.cost.toFixed(4)}` : "—";
|
|
20
|
+
lines.push(`| ${s.last} | \`${shortId}\` | ${s.messages} | ${cost} | ${preview} |`);
|
|
21
|
+
}
|
|
22
|
+
lines.push("");
|
|
23
|
+
return lines.join("\n");
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { collectSessions } from "./collect.js";
|
|
4
|
+
import { renderIndex } from "./render.js";
|
|
5
|
+
/** Regenerate <root>/sessions/INDEX.md. Safe to re-run; idempotent. */
|
|
6
|
+
export function runSessionsIndex(root) {
|
|
7
|
+
const dir = join(root, "sessions");
|
|
8
|
+
mkdirSync(dir, { recursive: true });
|
|
9
|
+
const rows = collectSessions(root);
|
|
10
|
+
const generatedAt = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
11
|
+
writeFileSync(join(dir, "INDEX.md"), renderIndex(rows, generatedAt));
|
|
12
|
+
process.stderr.write(`sessions-index: wrote sessions/INDEX.md (${rows.length} sessions)\n`);
|
|
13
|
+
return { code: 0 };
|
|
14
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/** name (relative, forward-slashed) -> file content, for *.md under dir. */
|
|
4
|
+
function scanMd(dir) {
|
|
5
|
+
const out = new Map();
|
|
6
|
+
const walk = (d, base = "") => {
|
|
7
|
+
let entries;
|
|
8
|
+
try {
|
|
9
|
+
entries = readdirSync(d, { withFileTypes: true });
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return; // missing dir -> no resources
|
|
13
|
+
}
|
|
14
|
+
for (const e of entries) {
|
|
15
|
+
const rel = base ? `${base}/${e.name}` : e.name;
|
|
16
|
+
if (e.isDirectory())
|
|
17
|
+
walk(join(d, e.name), rel);
|
|
18
|
+
else if (e.isFile() && e.name.endsWith(".md"))
|
|
19
|
+
out.set(rel, readFileSync(join(d, e.name), "utf8"));
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
walk(dir);
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
function readJsonObj(file) {
|
|
26
|
+
try {
|
|
27
|
+
const v = JSON.parse(readFileSync(file, "utf8"));
|
|
28
|
+
return v && typeof v === "object" ? v : {};
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Named-file resources (rules, agents): shadow = local hides a changed/added
|
|
35
|
+
* base file; orphan = local file whose base counterpart was removed. */
|
|
36
|
+
function classifyNamedFiles(oldDir, newDir, localDir, resource) {
|
|
37
|
+
const oldF = scanMd(oldDir);
|
|
38
|
+
const newF = scanMd(newDir);
|
|
39
|
+
const local = new Set(scanMd(localDir).keys());
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const [name, newText] of newF) {
|
|
42
|
+
const changed = !oldF.has(name) || oldF.get(name) !== newText;
|
|
43
|
+
if (changed && local.has(name)) {
|
|
44
|
+
const verb = oldF.has(name) ? "changed" : "added";
|
|
45
|
+
out.push({
|
|
46
|
+
kind: "shadow",
|
|
47
|
+
resource,
|
|
48
|
+
name,
|
|
49
|
+
detail: `base ${verb} ${resource} '${name}', but a local override shadows it — the update is hidden until you reconcile`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const name of local) {
|
|
54
|
+
if (oldF.has(name) && !newF.has(name)) {
|
|
55
|
+
out.push({
|
|
56
|
+
kind: "orphan",
|
|
57
|
+
resource,
|
|
58
|
+
name,
|
|
59
|
+
detail: `local ${resource} '${name}' overrode a base file removed in the new version — the override is now dead`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
/** presets.json: keys, not files. Shadow = local key hides a changed/added base
|
|
66
|
+
* key; orphan = local key whose base key was removed. */
|
|
67
|
+
function classifyPresets(oldFile, newFile, localFile) {
|
|
68
|
+
const oldK = readJsonObj(oldFile);
|
|
69
|
+
const newK = readJsonObj(newFile);
|
|
70
|
+
const local = readJsonObj(localFile);
|
|
71
|
+
const out = [];
|
|
72
|
+
for (const [k, v] of Object.entries(newK)) {
|
|
73
|
+
const changed = !(k in oldK) || JSON.stringify(oldK[k]) !== JSON.stringify(v);
|
|
74
|
+
if (changed && k in local) {
|
|
75
|
+
const verb = k in oldK ? "changed" : "added";
|
|
76
|
+
out.push({
|
|
77
|
+
kind: "shadow",
|
|
78
|
+
resource: "preset",
|
|
79
|
+
name: k,
|
|
80
|
+
detail: `base ${verb} preset '${k}', but a local preset shadows it`,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const k of Object.keys(local)) {
|
|
85
|
+
if (k in oldK && !(k in newK)) {
|
|
86
|
+
out.push({
|
|
87
|
+
kind: "orphan",
|
|
88
|
+
resource: "preset",
|
|
89
|
+
name: k,
|
|
90
|
+
detail: `local preset '${k}' overrode a base preset removed in the new version — dead override`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
/** Pull the extension basenames a garaje excludes via `!extensions/<x>` in its
|
|
97
|
+
* base-pin `extensions` filter array. */
|
|
98
|
+
export function extractExtensionFilters(settingsFile) {
|
|
99
|
+
const s = readJsonObj(settingsFile);
|
|
100
|
+
const pkgs = Array.isArray(s.packages) ? s.packages : [];
|
|
101
|
+
const out = [];
|
|
102
|
+
for (const p of pkgs) {
|
|
103
|
+
if (p && typeof p === "object" && Array.isArray(p.extensions)) {
|
|
104
|
+
for (const g of p.extensions) {
|
|
105
|
+
if (typeof g === "string" && g.startsWith("!extensions/")) {
|
|
106
|
+
out.push(g.slice("!extensions/".length));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
/** A `!`-filter orphans when its target extension is gone in the new base — the
|
|
114
|
+
* intended exclusion silently stops applying, so a base extension may now load. */
|
|
115
|
+
function classifyFilters(newExtDir, filters) {
|
|
116
|
+
const out = [];
|
|
117
|
+
for (const f of filters) {
|
|
118
|
+
if (!existsSync(join(newExtDir, f))) {
|
|
119
|
+
out.push({
|
|
120
|
+
kind: "orphan",
|
|
121
|
+
resource: "filter",
|
|
122
|
+
name: f,
|
|
123
|
+
detail: `.pi/settings.json filters out base extension '${f}', which the new version renamed or removed — the exclusion no longer applies`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
/** Diff old-base vs new-base against the garaje's small local override layer. */
|
|
130
|
+
export function classify(oldBaseDir, newBaseDir, cwd) {
|
|
131
|
+
return [
|
|
132
|
+
...classifyNamedFiles(join(oldBaseDir, "rules"), join(newBaseDir, "rules"), join(cwd, ".agent/rules"), "rule"),
|
|
133
|
+
...classifyNamedFiles(join(oldBaseDir, "agents"), join(newBaseDir, "agents"), join(cwd, ".pi/agents"), "agent"),
|
|
134
|
+
...classifyPresets(join(oldBaseDir, "presets.json"), join(newBaseDir, "presets.json"), join(cwd, ".pi/presets.json")),
|
|
135
|
+
...classifyFilters(join(newBaseDir, "extensions"), extractExtensionFilters(join(cwd, ".pi/settings.json"))),
|
|
136
|
+
];
|
|
137
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { classify } from "./classify.js";
|
|
6
|
+
import { currentBaseVersion, rewriteBasePin, rewriteDockerfileCliPin } from "./pins.js";
|
|
7
|
+
/** Resolve @garaje/base@<version> into a directory tree via `npm pack`, or
|
|
8
|
+
* undefined if it can't be fetched (e.g. pre-publish / offline). */
|
|
9
|
+
function fetchBaseTree(version) {
|
|
10
|
+
const dir = mkdtempSync(join(tmpdir(), `garaje-base-${version}-`));
|
|
11
|
+
const packed = spawnSync("npm", ["pack", `@garaje/base@${version}`, "--pack-destination", dir], {
|
|
12
|
+
stdio: "ignore",
|
|
13
|
+
});
|
|
14
|
+
if (packed.status !== 0) {
|
|
15
|
+
rmSync(dir, { recursive: true, force: true });
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const tgz = spawnSync("sh", ["-c", `ls "${dir}"/*.tgz`], { encoding: "utf8" }).stdout.trim();
|
|
19
|
+
const untar = spawnSync("tar", ["-xzf", tgz, "-C", dir], { stdio: "ignore" });
|
|
20
|
+
if (untar.status !== 0) {
|
|
21
|
+
rmSync(dir, { recursive: true, force: true });
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return join(dir, "package"); // npm tarballs root everything under package/
|
|
25
|
+
}
|
|
26
|
+
function parseTo(args) {
|
|
27
|
+
const i = args.indexOf("--to");
|
|
28
|
+
if (i >= 0)
|
|
29
|
+
return args[i + 1];
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
/** `garaje upgrade [--to <version>]` — bump the base + CLI pins and classify. */
|
|
33
|
+
export function runUpgrade(root, args) {
|
|
34
|
+
const settingsPath = join(root, ".pi/settings.json");
|
|
35
|
+
const dockerfilePath = join(root, "pi/Dockerfile");
|
|
36
|
+
const versionPath = join(root, ".garaje-version");
|
|
37
|
+
if (!existsSync(settingsPath)) {
|
|
38
|
+
process.stderr.write("garaje upgrade: no .pi/settings.json — run inside a garaje\n");
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
const to = parseTo(args) ?? resolveLatest();
|
|
42
|
+
if (!to) {
|
|
43
|
+
process.stderr.write("garaje upgrade: could not resolve a target version (pass --to <version>)\n");
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
if (!/^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.]+)?$/.test(to)) {
|
|
47
|
+
process.stderr.write(`garaje upgrade: invalid --to target '${to}' — expected a semver version\n`);
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
const settingsText = readFileSync(settingsPath, "utf8");
|
|
51
|
+
const from = currentBaseVersion(settingsText);
|
|
52
|
+
// Classify old vs new base against the local override layer, if both trees resolve.
|
|
53
|
+
let findings = [];
|
|
54
|
+
let classified = false;
|
|
55
|
+
const newTree = fetchBaseTree(to);
|
|
56
|
+
const oldTree = from ? fetchBaseTree(from) : undefined;
|
|
57
|
+
if (newTree && oldTree) {
|
|
58
|
+
findings = classify(oldTree, newTree, root);
|
|
59
|
+
classified = true;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
process.stderr.write(`garaje upgrade: could not fetch @garaje/base@${from ?? "?"}/${to} to classify — ` +
|
|
63
|
+
`skipping the shadow/orphan check (run it after the packages are published)\n`);
|
|
64
|
+
}
|
|
65
|
+
// Always clean up any temp tree we fetched (the tree path ends in /package).
|
|
66
|
+
for (const t of [oldTree, newTree]) {
|
|
67
|
+
if (t)
|
|
68
|
+
rmSync(t.replace(/\/package$/, ""), { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
// Rewrite the three committed pins.
|
|
71
|
+
writeFileSync(settingsPath, rewriteBasePin(settingsText, to));
|
|
72
|
+
if (existsSync(dockerfilePath)) {
|
|
73
|
+
writeFileSync(dockerfilePath, rewriteDockerfileCliPin(readFileSync(dockerfilePath, "utf8"), to));
|
|
74
|
+
}
|
|
75
|
+
writeFileSync(versionPath, `${to}\n`);
|
|
76
|
+
// Report.
|
|
77
|
+
process.stdout.write(`garaje upgrade: pins bumped ${from ?? "(local)"} -> ${to}\n`);
|
|
78
|
+
for (const f of findings) {
|
|
79
|
+
process.stdout.write(` ${f.kind.toUpperCase()} [${f.resource}] ${f.name}: ${f.detail}\n`);
|
|
80
|
+
}
|
|
81
|
+
const nextHeader = classified
|
|
82
|
+
? ` # review the ${findings.length} override(s) above, then:\n`
|
|
83
|
+
: ` # classification was skipped (base trees unavailable) — re-run this after the ` +
|
|
84
|
+
`packages are published to check for shadow/orphan overrides, then:\n`;
|
|
85
|
+
process.stdout.write(`\nNext:\n${nextHeader}` +
|
|
86
|
+
` npm i -g garaje@${to}\n garaje up --build\n git add -A && git commit -m 'upgrade garaje to ${to}'\n`);
|
|
87
|
+
return findings.length > 0 ? 1 : 0;
|
|
88
|
+
}
|
|
89
|
+
/** Latest published `garaje` version, or undefined (pre-publish / offline). */
|
|
90
|
+
function resolveLatest() {
|
|
91
|
+
const r = spawnSync("npm", ["view", "garaje", "version"], { encoding: "utf8" });
|
|
92
|
+
const v = (r.stdout || "").trim();
|
|
93
|
+
return r.status === 0 && v ? v : undefined;
|
|
94
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Replace the pinned @garaje/base npm version; leave a local-path source alone. */
|
|
2
|
+
export function rewriteBasePin(settingsText, newVersion) {
|
|
3
|
+
return settingsText.replace(/(npm:@garaje\/base@)[0-9A-Za-z.\-]+/g, `$1${newVersion}`);
|
|
4
|
+
}
|
|
5
|
+
/** Replace the `garaje@<v>` CLI install literal in a scaffolded pi/Dockerfile. */
|
|
6
|
+
export function rewriteDockerfileCliPin(dockerfileText, newVersion) {
|
|
7
|
+
return dockerfileText.replace(/(garaje@)[0-9A-Za-z.\-]+/g, `$1${newVersion}`);
|
|
8
|
+
}
|
|
9
|
+
/** The version currently pinned for @garaje/base, or undefined for a local path. */
|
|
10
|
+
export function currentBaseVersion(settingsText) {
|
|
11
|
+
const m = settingsText.match(/npm:@garaje\/base@([0-9A-Za-z.\-]+)/);
|
|
12
|
+
return m ? m[1] : undefined;
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "garaje",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/teammates-work/garaje.git",
|
|
8
|
+
"directory": "packages/cli"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"bin": {
|
|
12
|
+
"garaje": "dist/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"templates"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"yaml": "^2.4.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# `.agent/` — local override layer
|
|
2
|
+
|
|
3
|
+
This directory is the LOCAL override layer over the `@garaje/base` framework
|
|
4
|
+
substrate. The substrate itself — the extension bundle, base rules, canonical
|
|
5
|
+
role prompts, and generated presets/agents — ships in the `@garaje/base` pi
|
|
6
|
+
package. Consumption is declarative: `.pi/settings.json` pins `@garaje/base` as
|
|
7
|
+
a `packages` source, and pi merges its content with what's here at load time.
|
|
8
|
+
|
|
9
|
+
`.agent/rules/*.md` are local overrides layered over `@garaje/base`'s `rules/`:
|
|
10
|
+
a file here whose name matches a base rule file shadows it entirely (local
|
|
11
|
+
wins), and a file here with a new name is layered in additively. `commands/`
|
|
12
|
+
and `skills/` are for local, repo-specific slash commands and pi skills that
|
|
13
|
+
supplement the base bundle; both start empty.
|
|
14
|
+
|
|
15
|
+
`AGENTS.md` at the repo root is always loaded first; base rules and local rules
|
|
16
|
+
in `.agent/rules/` layer on top, merged in filename order with local overrides
|
|
17
|
+
winning on name collisions.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__GARAJE_VERSION__
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# `.pi/` — project settings + optional local overrides
|
|
2
|
+
|
|
3
|
+
`settings.json` is this garaje's pi configuration, including the `packages`
|
|
4
|
+
pin that consumes `@garaje/base` — the extension bundle, rules, prompts/roles,
|
|
5
|
+
and generated presets/agents all live in that package. Hand-edit
|
|
6
|
+
`settings.json` directly; it's committed as-is.
|
|
7
|
+
|
|
8
|
+
This directory can optionally hold project-local overrides that merge over the
|
|
9
|
+
base package's at load time:
|
|
10
|
+
|
|
11
|
+
- `presets.json` — project-only presets, merged with the base package's
|
|
12
|
+
(consumed by the `preset` extension, `/preset <name>`).
|
|
13
|
+
- `agents/<role>.md` — project-only subagent definitions, merged with the base
|
|
14
|
+
package's `agents/` (consumed by the `subagent` extension).
|
|
15
|
+
|
|
16
|
+
Neither is generated — they're empty unless this garaje needs a preset or role
|
|
17
|
+
that doesn't belong in the shared `@garaje/base` package.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://pi.dev/schemas/settings.json",
|
|
3
|
+
"defaultProvider": "anthropic",
|
|
4
|
+
"defaultModel": "claude-opus-4-8",
|
|
5
|
+
"defaultThinkingLevel": "medium",
|
|
6
|
+
"theme": "dark",
|
|
7
|
+
"packages": [
|
|
8
|
+
{
|
|
9
|
+
"source": "__BASE_PIN__",
|
|
10
|
+
"extensions": ["extensions/**", "!extensions/session-name.ts"]
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Garaje workspace
|
|
2
|
+
|
|
3
|
+
You are running inside the `pi` container of a docker-compose project — a
|
|
4
|
+
**garaje**. Your working directory `/workspace` is the repo root, bind-mounted
|
|
5
|
+
read-write from the host. Editing a file here changes it on the host and inside
|
|
6
|
+
the running containers simultaneously.
|
|
7
|
+
|
|
8
|
+
A garaje parks one or more **codebases** into `bays/` and runs them for
|
|
9
|
+
agentic dev. Each parked codebase declares how it runs in its own `recipe.yaml`.
|
|
10
|
+
|
|
11
|
+
## Agent contract
|
|
12
|
+
|
|
13
|
+
This file is the canonical entry point. Layered guidance lives in
|
|
14
|
+
[`.agent/rules/`](.agent/rules/) and in the pinned `@garaje/base` package's
|
|
15
|
+
rules; they merge alphabetically with local rules winning on name collisions.
|
|
16
|
+
See [`.agent/README.md`](.agent/README.md) for the directory contract.
|
|
17
|
+
|
|
18
|
+
## Layout
|
|
19
|
+
|
|
20
|
+
- `bays/<name>/` — parked codebases, materialized by `garaje sync` from
|
|
21
|
+
`garaje.yaml`. Gitignored; each is its own git checkout. This is where the
|
|
22
|
+
application code you work on lives.
|
|
23
|
+
- `compose.framework.yaml` — the pi service itself. **You cannot edit this**
|
|
24
|
+
(protected). `docker-compose.yml` is likewise protected.
|
|
25
|
+
- `compose.bays.yaml` — generated from each bay's `recipe.yaml` by
|
|
26
|
+
`garaje park`; generated + committed. Don't hand-edit it — change the recipe.
|
|
27
|
+
- `.agent/` — the local override layer (rules, commands, skills) over the
|
|
28
|
+
`@garaje/base` package.
|
|
29
|
+
- `scratch/` — host-shared dropbox for screenshots, logs, paste-ins (gitignored).
|
|
30
|
+
- `sessions/` — pi session transcripts, host-visible (gitignored).
|
|
31
|
+
|
|
32
|
+
## Working on a parked codebase
|
|
33
|
+
|
|
34
|
+
Each bay carries a `recipe.yaml` describing how it runs (toolchain, app
|
|
35
|
+
processes, backing services, env, secrets). After editing a bay's source, the
|
|
36
|
+
services live-reload per that codebase's own dev setup; prove a change by
|
|
37
|
+
hitting the affected process (e.g. `curl` the app's health endpoint) from
|
|
38
|
+
inside this container over the compose network.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Framework-owned. Defines the pi agent container itself + its docker proxy.
|
|
2
|
+
#
|
|
3
|
+
# WRITE-PROTECTED: the agent cannot edit this file (enforced by
|
|
4
|
+
# packages/base/extensions/protected-paths.ts). Parked-codebase services are generated
|
|
5
|
+
# into compose.bays.yaml by `garaje park` and layered in by the bin tooling.
|
|
6
|
+
#
|
|
7
|
+
# This file names no codebase on purpose — the pi service is codebase-agnostic.
|
|
8
|
+
|
|
9
|
+
services:
|
|
10
|
+
pi:
|
|
11
|
+
build: ./pi
|
|
12
|
+
working_dir: /workspace
|
|
13
|
+
volumes:
|
|
14
|
+
- ./:/workspace
|
|
15
|
+
- pi-config:/root/.pi/agent
|
|
16
|
+
- garaje-auth:/root/.garaje-auth
|
|
17
|
+
environment:
|
|
18
|
+
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
|
|
19
|
+
# The agent's ONLY docker endpoint: the whitelisting proxy below.
|
|
20
|
+
# The raw socket is never mounted here (north-star §5.1 layer 1).
|
|
21
|
+
- DOCKER_HOST=tcp://docker-proxy:2375
|
|
22
|
+
# Marker for `garaje` in-container behavior: host-only verbs refuse,
|
|
23
|
+
# bay verbs run in the bays-only driver scope.
|
|
24
|
+
- GARAJE_IN_CONTAINER=1
|
|
25
|
+
# Host path of this garaje root, baked at `garaje up` time. Generated
|
|
26
|
+
# bind mounts use ${GARAJE_HOST_ROOT:-.} because bind sources resolve
|
|
27
|
+
# on the daemon (host) side even when compose runs in-container.
|
|
28
|
+
- GARAJE_HOST_ROOT=${PWD:-}
|
|
29
|
+
depends_on:
|
|
30
|
+
- docker-proxy
|
|
31
|
+
|
|
32
|
+
# Whitelisting socket proxy (tecnativa/docker-socket-proxy): default-deny;
|
|
33
|
+
# the env flags below open exactly what parking a bay needs — build, image,
|
|
34
|
+
# container lifecycle, exec, networks, volumes. Swarm/secrets/system stay
|
|
35
|
+
# denied. NOTE: the proxy filters by ENDPOINT, not by target container —
|
|
36
|
+
# per-container protection of the pi service is layers 2+3 (compose split,
|
|
37
|
+
# command-intent guard). No published ports: only compose-network peers
|
|
38
|
+
# (the pi service) can reach it.
|
|
39
|
+
docker-proxy:
|
|
40
|
+
image: tecnativa/docker-socket-proxy:0.3.0
|
|
41
|
+
volumes:
|
|
42
|
+
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
43
|
+
environment:
|
|
44
|
+
BUILD: 1
|
|
45
|
+
CONTAINERS: 1
|
|
46
|
+
EXEC: 1
|
|
47
|
+
GRPC: 1
|
|
48
|
+
IMAGES: 1
|
|
49
|
+
INFO: 1
|
|
50
|
+
NETWORKS: 1
|
|
51
|
+
POST: 1
|
|
52
|
+
SESSION: 1
|
|
53
|
+
VOLUMES: 1
|
|
54
|
+
ALLOW_START: 1
|
|
55
|
+
ALLOW_STOP: 1
|
|
56
|
+
ALLOW_RESTARTS: 1
|
|
57
|
+
|
|
58
|
+
volumes:
|
|
59
|
+
pi-config:
|
|
60
|
+
garaje-auth:
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Framework-owned, WRITE-PROTECTED (see packages/base/extensions/protected-paths.ts).
|
|
2
|
+
#
|
|
3
|
+
# Thin composition root. It wires in the framework layer (the pi service).
|
|
4
|
+
# Parked-codebase services live in compose.bays.yaml, which is GENERATED from
|
|
5
|
+
# each bay's recipe.yaml by `garaje park` and layered in by the bin tooling
|
|
6
|
+
# (see bin/_lib.sh). Both files sit at the repo root so build contexts and
|
|
7
|
+
# bind mounts resolve unambiguously.
|
|
8
|
+
#
|
|
9
|
+
# The compose project name is NOT hardcoded here — it comes from the committed
|
|
10
|
+
# `name:` in garaje.yaml, passed as `docker compose -p <name>` by bin/_lib.sh,
|
|
11
|
+
# so each garaje owns its own identity (and stable volume prefix).
|
|
12
|
+
include:
|
|
13
|
+
- compose.framework.yaml
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# garaje.yaml — the parking manifest. COMMITTED.
|
|
2
|
+
#
|
|
3
|
+
# Lists which codebases this garaje parks. `garaje sync` clones each entry
|
|
4
|
+
# into bays/<name> (gitignored), the bind-mounted folder the agent works in.
|
|
5
|
+
# The garaje commits this manifest, not the code (no submodules).
|
|
6
|
+
#
|
|
7
|
+
# A dev who already has a checkout can override per-bay to bind-mount their
|
|
8
|
+
# local path instead — see garaje.local.yaml (gitignored). [override: TODO]
|
|
9
|
+
|
|
10
|
+
# This garaje's name (set at `garaje init`). Drives the docker compose project
|
|
11
|
+
# name, and thus the volume prefix where auth.json lives (<name>_pi-config).
|
|
12
|
+
name: __GARAJE_NAME__
|
|
13
|
+
|
|
14
|
+
bays: []
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Local secrets
|
|
2
|
+
.env
|
|
3
|
+
|
|
4
|
+
# Node (only appears if a service is run outside its container)
|
|
5
|
+
node_modules/
|
|
6
|
+
|
|
7
|
+
# Python
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.pyc
|
|
10
|
+
|
|
11
|
+
# JetBrains IDE
|
|
12
|
+
.idea/
|
|
13
|
+
|
|
14
|
+
# Agent runtime artifacts — host-visible but not committed.
|
|
15
|
+
/sessions/*
|
|
16
|
+
!/sessions/.gitkeep
|
|
17
|
+
scratch/*
|
|
18
|
+
!scratch/.gitkeep
|
|
19
|
+
|
|
20
|
+
# Parked codebases — materialized by `garaje sync` from garaje.yaml.
|
|
21
|
+
# The garaje commits the manifest (garaje.yaml), not the code.
|
|
22
|
+
bays/*
|
|
23
|
+
!bays/.gitkeep
|
|
24
|
+
|
|
25
|
+
# Dev-local garaje overrides (per-bay bind-mount paths, personal settings).
|
|
26
|
+
garaje.local.yaml
|