garaje 0.1.3 → 0.1.5
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/host/doctor.js +71 -4
- package/dist/host/provider.js +43 -0
- package/dist/host/sync.js +34 -12
- package/package.json +1 -1
- package/templates/.env.example +14 -2
- package/templates/.pi/settings.json +4 -3
- package/templates/compose.framework.yaml +15 -1
- package/templates/gitignore +4 -0
package/dist/host/doctor.js
CHANGED
|
@@ -5,6 +5,7 @@ import { driverContext, getDriver } from "../driver/index.js";
|
|
|
5
5
|
import { baysArtifactState, discoverBays, manifestBays } from "../park/index.js";
|
|
6
6
|
import { basePinSource } from "../upgrade/pins.js";
|
|
7
7
|
import { bayStatuses, statusLine } from "./sync.js";
|
|
8
|
+
import { classifyModelList, defaultModelOf, hasInferenceCredential } from "./provider.js";
|
|
8
9
|
/** `garaje doctor` — host-side sanity checks. Exit code = failed-check count. */
|
|
9
10
|
export function runDoctor(root) {
|
|
10
11
|
let fails = 0;
|
|
@@ -59,10 +60,33 @@ export function runDoctor(root) {
|
|
|
59
60
|
else
|
|
60
61
|
fail(`@garaje/base path pin "${source}" missing — no package.json at ${dir}`);
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
// .env is the garaje's single credential surface (compose forwards it via
|
|
64
|
+
// env_file). Provider-neutral: any non-empty *_API_KEY counts, so adding a
|
|
65
|
+
// new provider needs no change here.
|
|
66
|
+
const envPath = join(root, ".env");
|
|
67
|
+
if (!existsSync(envPath)) {
|
|
68
|
+
warn(".env missing — copy .env.example and add a provider key (e.g. MIXLAYER_API_KEY)");
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// .env could be a directory (stray mkdir, misconfigured tool) or otherwise
|
|
72
|
+
// unreadable; report that as a failed check rather than crashing (same
|
|
73
|
+
// contract as the garaje.yaml and recipe checks below).
|
|
74
|
+
let envText;
|
|
75
|
+
try {
|
|
76
|
+
envText = readFileSync(envPath, "utf8");
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
fail(`.env unreadable — ${e.message}`);
|
|
80
|
+
}
|
|
81
|
+
if (envText !== undefined) {
|
|
82
|
+
if (hasInferenceCredential(envText)) {
|
|
83
|
+
pass(".env carries an inference credential");
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
warn(".env has no non-empty *_API_KEY — pi will start but cannot answer");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
66
90
|
// --- runtime state ---
|
|
67
91
|
section("runtime");
|
|
68
92
|
if (driver.configOk())
|
|
@@ -139,6 +163,49 @@ export function runDoctor(root) {
|
|
|
139
163
|
pass(`pi binary works: ${piver}`);
|
|
140
164
|
else
|
|
141
165
|
fail("pi --version did not print a version");
|
|
166
|
+
// A key stored by `/login` outranks .env: model-registry resolves
|
|
167
|
+
// `apiKeyFromAuthStorage ?? providerConfig.apiKey`. Existence only — doctor
|
|
168
|
+
// never reads this file.
|
|
169
|
+
if (driver.execCapture("pi", ["test", "-f", "/root/.pi/agent/auth.json"]).status === 0) {
|
|
170
|
+
warn("/root/.pi/agent/auth.json exists — a stored `/login` credential OVERRIDES .env");
|
|
171
|
+
}
|
|
172
|
+
// Proves the provider extension loaded AND a credential is present. It
|
|
173
|
+
// does NOT validate the key: pi lists models for a bogus key exactly as it
|
|
174
|
+
// would for a real one — but with NO credential at all, `pi --list-models`
|
|
175
|
+
// exits 0 and prints "No models available" instead of a table. That is a
|
|
176
|
+
// separate condition from a model genuinely missing from a populated
|
|
177
|
+
// table, so it must warn, not fail (a freshly scaffolded garaje with no
|
|
178
|
+
// key yet must not report a hard failure here).
|
|
179
|
+
const model = existsSync(settings)
|
|
180
|
+
? defaultModelOf(readFileSync(settings, "utf8"))
|
|
181
|
+
: undefined;
|
|
182
|
+
if (!model) {
|
|
183
|
+
warn("no defaultModel in .pi/settings.json — pi will prompt for a model");
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const listed = driver.execCapture("pi", ["pi", "--list-models"]);
|
|
187
|
+
if (listed.status !== 0) {
|
|
188
|
+
fail(`\`pi --list-models\` exited ${listed.status} — could not check defaultModel`);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
const verdict = classifyModelList(listed.stdout, model);
|
|
192
|
+
if (verdict === "no-models") {
|
|
193
|
+
// A credential can exist (e.g. a key for a provider this garaje
|
|
194
|
+
// isn't configured to use) while no *loaded* provider has a usable
|
|
195
|
+
// one — CI's exact case: MIXLAYER_API_KEY set, but this repo's
|
|
196
|
+
// settings select anthropic and no mixlayer extension is pinned.
|
|
197
|
+
// Naming only "no credential" would misdirect toward the key when
|
|
198
|
+
// the real cause may be a missing provider extension instead.
|
|
199
|
+
warn(`pi lists no models — no loaded provider has a usable credential (key missing, or its provider extension isn't installed); defaultModel "${model}" cannot be verified`);
|
|
200
|
+
}
|
|
201
|
+
else if (verdict === "listed") {
|
|
202
|
+
pass(`defaultModel registered: ${model} (extension loaded, credential present; key NOT validated)`);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
fail(`defaultModel "${model}" absent from \`pi --list-models\` — provider extension missing or model id wrong`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
142
209
|
}
|
|
143
210
|
// --- summary ---
|
|
144
211
|
section("summary");
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Pure helpers behind `garaje doctor`'s inference-provider checks. No fs, no
|
|
2
|
+
* spawn — doctor's container probes are covered by scripts/ci-smoke instead. */
|
|
3
|
+
/** True when a dotenv body sets any *_API_KEY to a non-empty value. A blank
|
|
4
|
+
* assignment doesn't count: a scaffolded `.env.example` copied verbatim has
|
|
5
|
+
* `MIXLAYER_API_KEY=` and therefore no credential. Lines may optionally be
|
|
6
|
+
* prefixed with `export ` (for shell sourcing compatibility). */
|
|
7
|
+
export function hasInferenceCredential(envText) {
|
|
8
|
+
for (const raw of envText.split(/\r?\n/)) {
|
|
9
|
+
const line = raw.trim();
|
|
10
|
+
if (!line || line.startsWith("#"))
|
|
11
|
+
continue;
|
|
12
|
+
const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*_API_KEY)\s*=\s*(.*)$/.exec(line);
|
|
13
|
+
if (!m)
|
|
14
|
+
continue;
|
|
15
|
+
if (m[2].trim().replace(/^["']|["']$/g, "").length > 0)
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
/** The `defaultModel` a garaje's committed pi settings select, if any. */
|
|
21
|
+
export function defaultModelOf(settingsText) {
|
|
22
|
+
try {
|
|
23
|
+
const data = JSON.parse(settingsText);
|
|
24
|
+
return typeof data.defaultModel === "string" && data.defaultModel
|
|
25
|
+
? data.defaultModel
|
|
26
|
+
: undefined;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Whether `pi --list-models` output registers the given model id. */
|
|
33
|
+
export function modelListed(listOutput, model) {
|
|
34
|
+
return listOutput.split(/\r?\n/).some((l) => l.includes(model));
|
|
35
|
+
}
|
|
36
|
+
/** Classify `pi --list-models` output. pi prints "No models available" when no
|
|
37
|
+
* provider credential is present — a different condition from a model that is
|
|
38
|
+
* genuinely missing from a populated table. */
|
|
39
|
+
export function classifyModelList(listOutput, model) {
|
|
40
|
+
if (/no models available/i.test(listOutput))
|
|
41
|
+
return "no-models";
|
|
42
|
+
return modelListed(listOutput, model) ? "listed" : "absent";
|
|
43
|
+
}
|
package/dist/host/sync.js
CHANGED
|
@@ -5,17 +5,34 @@ import { readManifest } from "../manifest.js";
|
|
|
5
5
|
function git(args) {
|
|
6
6
|
return spawnSync("git", args, { stdio: "inherit" }).status ?? 1;
|
|
7
7
|
}
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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",
|
|
18
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));
|
|
19
36
|
}
|
|
20
37
|
/** One human-readable line per bay. Pure — doctor reuses this. */
|
|
21
38
|
export function statusLine(b) {
|
|
@@ -34,7 +51,10 @@ export function renderList(rows) {
|
|
|
34
51
|
}
|
|
35
52
|
/** `garaje list` — show the manifest and which bays are materialized. */
|
|
36
53
|
export function runList(root) {
|
|
37
|
-
|
|
54
|
+
const entries = readOrReport(root);
|
|
55
|
+
if (!entries)
|
|
56
|
+
return 1;
|
|
57
|
+
process.stdout.write(renderList(entries.map((b) => bayStatus(root, b))));
|
|
38
58
|
return 0;
|
|
39
59
|
}
|
|
40
60
|
/** `garaje sync [name...]` — clone/update the manifest's bays into bays/. */
|
|
@@ -43,7 +63,9 @@ export function runSync(root, names) {
|
|
|
43
63
|
process.stderr.write("error: git not found in PATH\n");
|
|
44
64
|
return 1;
|
|
45
65
|
}
|
|
46
|
-
const entries =
|
|
66
|
+
const entries = readOrReport(root);
|
|
67
|
+
if (!entries)
|
|
68
|
+
return 1;
|
|
47
69
|
const unknown = names.filter((n) => !entries.some((b) => b.name === n));
|
|
48
70
|
if (unknown.length) {
|
|
49
71
|
process.stderr.write(`error: not in garaje.yaml: ${unknown.join(", ")} (declare the bay in the manifest first)\n`);
|
package/package.json
CHANGED
package/templates/.env.example
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
-
# Copy to .env
|
|
2
|
-
#
|
|
1
|
+
# Copy to .env, then fill in one provider key.
|
|
2
|
+
#
|
|
3
|
+
# This file is the garaje's ONLY credential surface: compose forwards .env into
|
|
4
|
+
# the pi container (env_file). A variable exported in your shell does NOT reach
|
|
5
|
+
# the agent.
|
|
6
|
+
|
|
7
|
+
# The scaffolded provider. Create a key at https://console.mixlayer.com/app/api-keys
|
|
8
|
+
MIXLAYER_API_KEY=
|
|
9
|
+
|
|
10
|
+
# Only needed if you switch .pi/settings.json back to defaultProvider "anthropic".
|
|
3
11
|
ANTHROPIC_API_KEY=
|
|
12
|
+
|
|
13
|
+
# NOTE: a key stored by `/login` inside pi is written to ~/.pi/agent/auth.json on
|
|
14
|
+
# the pi-config volume, survives rebuilds, and TAKES PRECEDENCE over this file.
|
|
15
|
+
# `garaje doctor` warns when that file exists.
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://pi.dev/schemas/settings.json",
|
|
3
|
-
"defaultProvider": "
|
|
4
|
-
"defaultModel": "
|
|
3
|
+
"defaultProvider": "mixlayer",
|
|
4
|
+
"defaultModel": "qwen/qwen3.5-397b-a17b",
|
|
5
5
|
"defaultThinkingLevel": "medium",
|
|
6
6
|
"theme": "dark",
|
|
7
7
|
"packages": [
|
|
8
8
|
{
|
|
9
9
|
"source": "__BASE_PIN__",
|
|
10
10
|
"extensions": ["extensions/**", "!extensions/session-name.ts"]
|
|
11
|
-
}
|
|
11
|
+
},
|
|
12
|
+
"npm:pi-mixlayer@0.4.0"
|
|
12
13
|
]
|
|
13
14
|
}
|
|
@@ -14,8 +14,18 @@ services:
|
|
|
14
14
|
- ./:/workspace
|
|
15
15
|
- pi-config:/root/.pi/agent
|
|
16
16
|
- garaje-auth:/root/.garaje-auth
|
|
17
|
+
# Provider credentials come from .env — a garaje's single credential surface.
|
|
18
|
+
# Any pi provider extension (pi-mixlayer, pi-openai, …) resolves its key from
|
|
19
|
+
# the container environment, so adding a provider needs no framework release.
|
|
20
|
+
# A shell-exported variable does NOT reach the agent; put it in .env.
|
|
21
|
+
# required:false keeps `garaje up` working before .env exists.
|
|
22
|
+
env_file:
|
|
23
|
+
- path: .env
|
|
24
|
+
required: false
|
|
17
25
|
environment:
|
|
18
|
-
-
|
|
26
|
+
# Framework-owned. `environment:` overrides `env_file`, so a stray .env
|
|
27
|
+
# entry cannot hijack any of these.
|
|
28
|
+
#
|
|
19
29
|
# The agent's ONLY docker endpoint: the whitelisting proxy below.
|
|
20
30
|
# The raw socket is never mounted here (north-star §5.1 layer 1).
|
|
21
31
|
- DOCKER_HOST=tcp://docker-proxy:2375
|
|
@@ -26,6 +36,10 @@ services:
|
|
|
26
36
|
# bind mounts use ${GARAJE_HOST_ROOT:-.} because bind sources resolve
|
|
27
37
|
# on the daemon (host) side even when compose runs in-container.
|
|
28
38
|
- GARAJE_HOST_ROOT=${PWD:-}
|
|
39
|
+
# pi/Dockerfile sets this via ENV, which env_file (unlike environment:)
|
|
40
|
+
# does NOT outrank — restated here so a stray .env line cannot redirect
|
|
41
|
+
# the agent's gh config directory (and its git/gh credential wiring).
|
|
42
|
+
- GH_CONFIG_DIR=/root/.garaje-auth/gh
|
|
29
43
|
depends_on:
|
|
30
44
|
- docker-proxy
|
|
31
45
|
|
package/templates/gitignore
CHANGED
|
@@ -24,3 +24,7 @@ bays/*
|
|
|
24
24
|
|
|
25
25
|
# Dev-local garaje overrides (per-bay bind-mount paths, personal settings).
|
|
26
26
|
garaje.local.yaml
|
|
27
|
+
|
|
28
|
+
# Pi packages installed into project scope by `pi install -l`. The package
|
|
29
|
+
# manifest lives in the committed .pi/settings.json; these are its artifacts.
|
|
30
|
+
/.pi/npm/
|