garaje 0.1.1 → 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/README.md +110 -0
- 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/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# garaje
|
|
2
|
+
|
|
3
|
+
**Park your codebases. Let the agent drive.**
|
|
4
|
+
|
|
5
|
+
`garaje` is a command-line tool for running the [pi](https://pi.dev) coding
|
|
6
|
+
agent against the codebases you actually work on — each one containerized with
|
|
7
|
+
its own datastores, alongside the agent, in a single Docker Compose project
|
|
8
|
+
called a **garaje**. You scaffold a garaje, hand it your repos, and the
|
|
9
|
+
in-container agent wires each one up for agentic development.
|
|
10
|
+
|
|
11
|
+
The design goal is **portability**: your dev environment is declared as data
|
|
12
|
+
(a manifest + per-codebase recipes), the agent harness is a swappable package,
|
|
13
|
+
and the same garaje runs locally on Docker today (Kubernetes is on the roadmap).
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g garaje
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
> Requires **Docker** (Desktop or Engine) on the host. The agent and its tools
|
|
20
|
+
> run in containers; `garaje` itself is a thin, dependency-light wrapper over
|
|
21
|
+
> `docker compose`.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# 1. Scaffold a new garaje (a git repo that hosts your parked codebases)
|
|
29
|
+
garaje init my-workspace
|
|
30
|
+
cd my-workspace
|
|
31
|
+
|
|
32
|
+
# 2. Start it — builds the agent container and brings the project up
|
|
33
|
+
garaje up --build
|
|
34
|
+
|
|
35
|
+
# 3. Drop into the agent
|
|
36
|
+
garaje pi
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then, from inside the agent, point it at a codebase and ask it to **park** it —
|
|
40
|
+
the agent introspects the repo, writes a `recipe.yaml` describing how it runs
|
|
41
|
+
(toolchain, processes, datastores, env), generates the container config, and
|
|
42
|
+
commits it. From then on that codebase is a first-class, live-reloading,
|
|
43
|
+
agent-assisted dev target in your garaje.
|
|
44
|
+
|
|
45
|
+
Stop without losing your work or your credentials:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
garaje down # refuses -v, so the agent's auth + provisioned tokens survive
|
|
49
|
+
garaje up
|
|
50
|
+
garaje pi -c # continue your most recent session
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Concepts
|
|
54
|
+
|
|
55
|
+
| Term | What it is |
|
|
56
|
+
|------|-----------|
|
|
57
|
+
| **garaje** | A git repo + Docker Compose project that runs the pi agent next to the codebases it works on. One per developer, or one shared per team. |
|
|
58
|
+
| **bay** | A parked codebase inside a garaje (`bays/<name>`), materialized from the manifest. The garaje commits the *manifest*, not the code. |
|
|
59
|
+
| **`recipe.yaml`** | A platform-neutral description of how one codebase runs — its toolchain, app processes, backing services, env, and secrets. Lives in the codebase; the agent authors it by introspection. |
|
|
60
|
+
| **[`@garaje/base`](https://www.npmjs.com/package/@garaje/base)** | The agent-side substrate — the persona, rules, roles, and skills the in-container agent loads. Pinned declaratively in `.pi/settings.json`; installed automatically in-container. |
|
|
61
|
+
|
|
62
|
+
## Commands
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
garaje init <name> [dir] scaffold a new garaje
|
|
66
|
+
garaje up [--build] build + start the garaje (host-only)
|
|
67
|
+
garaje down stop it (refuses -v; auth/tokens persist)
|
|
68
|
+
garaje pi [-c] open the pi agent TUI (-c continues the last session)
|
|
69
|
+
garaje attach attach to the running agent
|
|
70
|
+
garaje logs tail container logs
|
|
71
|
+
garaje doctor health-check the garaje
|
|
72
|
+
garaje sync [name] clone the manifest's codebases into bays/
|
|
73
|
+
garaje list list parked bays
|
|
74
|
+
garaje park [name] regenerate the compose from the bays' recipes
|
|
75
|
+
garaje bay <up|down|logs|ps> operate a parked bay's services
|
|
76
|
+
garaje auth <add|list|remove> provision git / gh / integration credentials
|
|
77
|
+
garaje upgrade [--to <v>] bump the framework + base pins, flag override drift
|
|
78
|
+
garaje sessions-index index the agent's session transcripts
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## How it works
|
|
82
|
+
|
|
83
|
+
- **Compose split by ownership.** A thin top-level `docker-compose.yml` includes
|
|
84
|
+
the framework layer (the agent container); parked codebases live in a
|
|
85
|
+
generated `compose.bays.yaml`. The agent shapes services by editing recipes,
|
|
86
|
+
never the compose files it can't reconfigure itself.
|
|
87
|
+
- **Bind-mounted, live-reloading.** The repo is mounted into the containers, so
|
|
88
|
+
edits the agent makes take effect immediately — no image rebuilds for code
|
|
89
|
+
changes.
|
|
90
|
+
- **Least-privilege Docker.** The agent reaches Docker only through a
|
|
91
|
+
default-deny socket proxy, never the raw socket.
|
|
92
|
+
- **Layered, declarative config.** The shared `@garaje/base` package is pinned
|
|
93
|
+
and consumed read-only; a garaje customizes by layering local overrides on
|
|
94
|
+
top — so upgrading the framework is a pin bump, not a merge.
|
|
95
|
+
|
|
96
|
+
## Framework & harness portability
|
|
97
|
+
|
|
98
|
+
garaje separates the *dev environment* (yours, committed as data) from the
|
|
99
|
+
*agent harness* (a package). Today the harness is pi via `@garaje/base`; the
|
|
100
|
+
seam is deliberately thin so a different harness is a different base package,
|
|
101
|
+
not a rewrite. The runtime is Docker Compose locally, with a Kubernetes driver
|
|
102
|
+
planned for the cloud — the same recipe, two runtimes.
|
|
103
|
+
|
|
104
|
+
`garaje` and `@garaje/base` are released in lockstep. Upgrade a garaje with
|
|
105
|
+
`garaje upgrade`, which bumps the pins and flags any local override that now
|
|
106
|
+
shadows or orphans a changed base resource.
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
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
|
+
}
|