create-op-node 0.12.3 → 0.13.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/README.md +48 -31
- package/dist/cli.js +207 -43
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,18 +32,21 @@ Configures macOS power settings, installs Homebrew + the CLI tool list, sets up
|
|
|
32
32
|
|
|
33
33
|
When you run `bootstrap` interactively (no `-y`, no `--llm-model` flag),
|
|
34
34
|
you'll get a curated picker that **pre-selects** a model based on the
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
- `
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
node's detected unified memory. Tiers are conservative — the LLM shares
|
|
36
|
+
memory with ~22 containers + Postgres, so they size on *total* footprint,
|
|
37
|
+
not param count:
|
|
38
|
+
|
|
39
|
+
- `qwen3.6:35b-a3b` — 35B MoE (~3B active, fast on Apple Silicon),
|
|
40
|
+
~24 GB footprint. Pre-selected on 64 GB+ nodes. Best Spanish +
|
|
41
|
+
multilingual quality.
|
|
42
|
+
- `qwen3:14b` — ~9 GB footprint. Pre-selected on 32–64 GB nodes.
|
|
43
|
+
- `qwen2.5:7b` — ~5 GB footprint. Pre-selected on 16–32 GB nodes, and
|
|
44
|
+
the default for non-interactive `-y` runs (see below).
|
|
45
|
+
- `qwen2.5:3b` — ~2–3 GB footprint. Pre-selected on ≤ 16 GB nodes (Mac Mini).
|
|
43
46
|
- `Other…` — any Ollama model name you specify.
|
|
44
47
|
|
|
45
|
-
(If memory detection fails, the picker falls back to pre-selecting
|
|
46
|
-
`qwen2.5:
|
|
48
|
+
(If memory detection fails, the picker falls back to pre-selecting the
|
|
49
|
+
conservative scripted default, `qwen2.5:7b`.)
|
|
47
50
|
|
|
48
51
|
The picker is Qwen-only because the Opus Populi platform serves
|
|
49
52
|
Spanish-speaking civic users and Qwen has the strongest Spanish (and
|
|
@@ -51,7 +54,7 @@ broader multilingual) capability of the open-weight models in this
|
|
|
51
54
|
size class. Pick "Other…" for non-Qwen models.
|
|
52
55
|
|
|
53
56
|
For non-interactive runs (`-y`) or scripted invocations, the LLM
|
|
54
|
-
defaults to `
|
|
57
|
+
defaults to `qwen2.5:7b` (small, fast) — pass `--llm-model` to override:
|
|
55
58
|
|
|
56
59
|
```bash
|
|
57
60
|
# Just swap the LLM, keep the default embedding model:
|
|
@@ -69,29 +72,43 @@ The chosen models flow two places:
|
|
|
69
72
|
1. **Ollama**: bootstrap pulls + warms them so the daemon has them
|
|
70
73
|
resident before the stack comes up. The embedding model pulls first
|
|
71
74
|
(small, fast feedback); the LLM pulls second (can be tens of GB).
|
|
72
|
-
2. **
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
>
|
|
82
|
-
|
|
83
|
-
>
|
|
84
|
-
>
|
|
85
|
-
>
|
|
86
|
-
>
|
|
87
|
-
>
|
|
75
|
+
2. **The node `.env`**: bootstrap writes `LLM_MODEL`, `EMBEDDINGS_MODEL`,
|
|
76
|
+
and `EMBEDDINGS_PROVIDER` into a managed block of the region repo's
|
|
77
|
+
`.env`, which docker compose auto-loads. This is the **single source of
|
|
78
|
+
truth** — every service (and every future partial recreate) resolves
|
|
79
|
+
`${LLM_MODEL:-…}` to the same value, regardless of shell/launchd env.
|
|
80
|
+
Model config is deliberately *not* exported via the LaunchAgent: a
|
|
81
|
+
`launchctl setenv` value would shadow `.env` at interpolation time
|
|
82
|
+
(shell env > `.env`), reintroducing the exact drift the `.env` prevents.
|
|
83
|
+
|
|
84
|
+
> **Managed block, not full ownership.** bootstrap owns only the region
|
|
85
|
+
> between `# >>> op-node managed >>>` and `# <<< op-node managed <<<` in
|
|
86
|
+
> your `.env` — anything you hand-add outside it is preserved. On first
|
|
87
|
+
> run against a pre-existing `.env`, your existing model values are adopted
|
|
88
|
+
> into the block rather than overwritten; the block is only rewritten when
|
|
89
|
+
> you explicitly (re)select a model. Secrets never go in `.env` — they stay
|
|
90
|
+
> in the macOS Keychain and are hydrated by `bin/op-compose`.
|
|
91
|
+
|
|
92
|
+
> **`--embeddings-provider` picks where embeddings run.** The default is
|
|
93
|
+
> `xenova` (in-process), which bundles its own embedding model and ignores
|
|
94
|
+
> `EMBEDDINGS_MODEL`. Pass `--embeddings-provider ollama` to use the host
|
|
95
|
+
> daemon with `--embedding-model` — the value is written to `.env` as the
|
|
96
|
+
> single source of truth. See `docs/provider-pattern.md`.
|
|
97
|
+
|
|
98
|
+
> **Template contract**: for `LLM_MODEL` to actually change the running
|
|
99
|
+
> model, the region repo's `docker-compose-prod.yml` must use
|
|
100
|
+
> `${LLM_MODEL:-…}` on the knowledge service's `environment:` block. The
|
|
101
|
+
> current `opuspopuli-node` template does; a fork that hardcodes the value
|
|
102
|
+
> would ignore the `.env` silently.
|
|
88
103
|
|
|
89
104
|
For RAM sizing, the [Docker resources doc](https://github.com/OpusPopuli/opuspopuli-node/blob/main/docs/docker-resources.md)
|
|
90
|
-
has a tier table:
|
|
91
|
-
|
|
105
|
+
has a tier table: a 7B-class model needs ~5 GB, the 35B-a3b MoE ~24 GB.
|
|
106
|
+
Allocate Docker the remainder of unified memory.
|
|
92
107
|
|
|
93
|
-
To switch models post-bootstrap, re-run with the new
|
|
94
|
-
`
|
|
108
|
+
To switch models post-bootstrap, either re-run bootstrap with the new
|
|
109
|
+
`--llm-model` (rewrites the managed block) or edit `LLM_MODEL` in the
|
|
110
|
+
managed block of your `.env` directly, then `./bin/op-compose -f
|
|
111
|
+
docker-compose-prod.yml up -d` to pick up the change.
|
|
95
112
|
|
|
96
113
|
### Local-only mode (no Cloudflare)
|
|
97
114
|
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { Octokit } from '@octokit/rest';
|
|
|
7
7
|
import _sodium from 'libsodium-wrappers';
|
|
8
8
|
import { randomBytes, createHmac } from 'crypto';
|
|
9
9
|
import { join, resolve, dirname } from 'path';
|
|
10
|
-
import { readFile, mkdir, writeFile, access, chmod, rename, rm } from 'fs/promises';
|
|
10
|
+
import { readFile, mkdir, writeFile, access, chmod, rename, stat, rm } from 'fs/promises';
|
|
11
11
|
import { homedir } from 'os';
|
|
12
12
|
import { readFileSync } from 'fs';
|
|
13
13
|
import * as tls from 'tls';
|
|
@@ -1493,6 +1493,7 @@ async function writePgsodiumKeyFile(key, keyFile) {
|
|
|
1493
1493
|
try {
|
|
1494
1494
|
const dir = dirname(keyFile);
|
|
1495
1495
|
await mkdir(dir, { recursive: true, mode: 448 });
|
|
1496
|
+
await rm(keyFile, { force: true });
|
|
1496
1497
|
await writeFile(keyFile, key, { mode: 256 });
|
|
1497
1498
|
await chmod(dir, 448);
|
|
1498
1499
|
await chmod(keyFile, 256);
|
|
@@ -1501,7 +1502,6 @@ async function writePgsodiumKeyFile(key, keyFile) {
|
|
|
1501
1502
|
return { ok: false, reason: `writing ${keyFile} failed: ${err.message}` };
|
|
1502
1503
|
}
|
|
1503
1504
|
}
|
|
1504
|
-
var MODEL_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;
|
|
1505
1505
|
function renderLaunchAgentPlist(input) {
|
|
1506
1506
|
validatePlistInput(input);
|
|
1507
1507
|
const command = buildSetenvCommand(input);
|
|
@@ -1527,16 +1527,6 @@ function validatePlistInput(input) {
|
|
|
1527
1527
|
if (input.tunnelToken !== void 0 && !TUNNEL_TOKEN_RE.test(input.tunnelToken)) {
|
|
1528
1528
|
throw new Error("Tunnel token contains characters outside the expected base64-url set");
|
|
1529
1529
|
}
|
|
1530
|
-
if (input.llmModel !== void 0 && !MODEL_NAME_RE.test(input.llmModel)) {
|
|
1531
|
-
throw new Error(
|
|
1532
|
-
`llmModel ${JSON.stringify(input.llmModel)} contains characters not allowed in a launchd setenv value`
|
|
1533
|
-
);
|
|
1534
|
-
}
|
|
1535
|
-
if (input.embeddingModel !== void 0 && !MODEL_NAME_RE.test(input.embeddingModel)) {
|
|
1536
|
-
throw new Error(
|
|
1537
|
-
`embeddingModel ${JSON.stringify(input.embeddingModel)} contains characters not allowed in a launchd setenv value`
|
|
1538
|
-
);
|
|
1539
|
-
}
|
|
1540
1530
|
if (!SAFE_PATH_RE.test(input.keyFilePath)) {
|
|
1541
1531
|
throw new Error(
|
|
1542
1532
|
`keyFilePath ${JSON.stringify(input.keyFilePath)} contains characters not allowed in a launchd path interpolation`
|
|
@@ -1570,8 +1560,6 @@ function buildSetenvCommand(input) {
|
|
|
1570
1560
|
// of — this is word-split safety, not new injection surface. (#36)
|
|
1571
1561
|
`launchctl setenv PGSODIUM_ROOT_KEY "$(cat "${input.keyFilePath}")"`,
|
|
1572
1562
|
...input.tunnelToken !== void 0 ? [`launchctl setenv TUNNEL_TOKEN "${input.tunnelToken}"`] : [],
|
|
1573
|
-
...input.llmModel !== void 0 ? [`launchctl setenv LLM_MODEL "${input.llmModel}"`] : [],
|
|
1574
|
-
...input.embeddingModel !== void 0 ? [`launchctl setenv EMBEDDINGS_MODEL "${input.embeddingModel}"`] : [],
|
|
1575
1563
|
...input.postgresPassword !== void 0 ? [`launchctl setenv POSTGRES_PASSWORD "${input.postgresPassword}"`] : [],
|
|
1576
1564
|
...input.jwtSecret !== void 0 ? [`launchctl setenv JWT_SECRET "${input.jwtSecret}"`] : [],
|
|
1577
1565
|
...input.supabaseAnonKey !== void 0 ? [`launchctl setenv SUPABASE_ANON_KEY "${input.supabaseAnonKey}"`] : [],
|
|
@@ -1610,8 +1598,6 @@ function plistInputFromSetup(input, keyFilePath) {
|
|
|
1610
1598
|
return {
|
|
1611
1599
|
keyFilePath,
|
|
1612
1600
|
...input.tunnelToken !== void 0 ? { tunnelToken: input.tunnelToken } : {},
|
|
1613
|
-
...input.llmModel !== void 0 ? { llmModel: input.llmModel } : {},
|
|
1614
|
-
...input.embeddingModel !== void 0 ? { embeddingModel: input.embeddingModel } : {},
|
|
1615
1601
|
...input.postgresPassword !== void 0 ? { postgresPassword: input.postgresPassword } : {},
|
|
1616
1602
|
...input.jwtSecret !== void 0 ? { jwtSecret: input.jwtSecret } : {},
|
|
1617
1603
|
...input.supabaseAnonKey !== void 0 ? { supabaseAnonKey: input.supabaseAnonKey } : {},
|
|
@@ -2073,7 +2059,7 @@ function assessAllRunning(snapshots) {
|
|
|
2073
2059
|
|
|
2074
2060
|
// src/lib/cosign.ts
|
|
2075
2061
|
var COSIGN_OIDC_ISSUER = "https://token.actions.githubusercontent.com";
|
|
2076
|
-
var DEFAULT_IDENTITY_REGEXP = "^https://github\\.com/OpusPopuli/opuspopuli/\\.github/workflows/release\\.yml@refs/heads/.*$";
|
|
2062
|
+
var DEFAULT_IDENTITY_REGEXP = "^https://github\\.com/OpusPopuli/(?:opuspopuli|prompt-service)/\\.github/workflows/release\\.yml@refs/heads/.*$";
|
|
2077
2063
|
async function cosignVerifyImage(input) {
|
|
2078
2064
|
const idRegex = input.certificateIdentityRegexp ?? DEFAULT_IDENTITY_REGEXP;
|
|
2079
2065
|
const issuer = input.certificateOidcIssuer ?? COSIGN_OIDC_ISSUER;
|
|
@@ -2106,8 +2092,9 @@ async function cosignVerifyImage(input) {
|
|
|
2106
2092
|
var OLLAMA_URL = "http://localhost:11434";
|
|
2107
2093
|
var OLLAMA_HEALTH_TIMEOUT_MS = 5e3;
|
|
2108
2094
|
var OLLAMA_WARM_TIMEOUT_MS = 12e4;
|
|
2109
|
-
var DEFAULT_LLM_MODEL = "
|
|
2095
|
+
var DEFAULT_LLM_MODEL = "qwen2.5:7b";
|
|
2110
2096
|
var DEFAULT_EMBEDDING_MODEL = "nomic-embed-text";
|
|
2097
|
+
var DEFAULT_EMBEDDINGS_PROVIDER = "xenova";
|
|
2111
2098
|
var PROBE_ALPINE_TAG = "3.20";
|
|
2112
2099
|
async function startOllamaService() {
|
|
2113
2100
|
const res = await safeExeca("brew", ["services", "start", "ollama"]);
|
|
@@ -2226,6 +2213,138 @@ async function setupModels(models, onProgress) {
|
|
|
2226
2213
|
}
|
|
2227
2214
|
return report;
|
|
2228
2215
|
}
|
|
2216
|
+
var MANAGED_BEGIN = "# >>> op-node managed >>>";
|
|
2217
|
+
var MANAGED_END = "# <<< op-node managed <<<";
|
|
2218
|
+
var MANAGED_KEYS = [
|
|
2219
|
+
"LLM_MODEL",
|
|
2220
|
+
"EMBEDDINGS_PROVIDER",
|
|
2221
|
+
"EMBEDDINGS_MODEL",
|
|
2222
|
+
"NODE_ENV"
|
|
2223
|
+
];
|
|
2224
|
+
var ENV_VALUE_RE = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;
|
|
2225
|
+
function selectionToPairs(sel) {
|
|
2226
|
+
return [
|
|
2227
|
+
["LLM_MODEL", sel.llmModel],
|
|
2228
|
+
["EMBEDDINGS_PROVIDER", sel.embeddingsProvider],
|
|
2229
|
+
["EMBEDDINGS_MODEL", sel.embeddingModel],
|
|
2230
|
+
["NODE_ENV", sel.nodeEnv]
|
|
2231
|
+
];
|
|
2232
|
+
}
|
|
2233
|
+
function parseEnvContent(content) {
|
|
2234
|
+
const out = /* @__PURE__ */ new Map();
|
|
2235
|
+
for (const raw of content.split("\n")) {
|
|
2236
|
+
const line = raw.trim();
|
|
2237
|
+
if (line.length === 0 || line.startsWith("#")) continue;
|
|
2238
|
+
const eq = line.indexOf("=");
|
|
2239
|
+
if (eq <= 0) continue;
|
|
2240
|
+
out.set(line.slice(0, eq).trim(), line.slice(eq + 1).trim());
|
|
2241
|
+
}
|
|
2242
|
+
return out;
|
|
2243
|
+
}
|
|
2244
|
+
function splitManaged(content) {
|
|
2245
|
+
const lines = content.split("\n");
|
|
2246
|
+
const beginIdx = lines.findIndex((l) => l.trim() === MANAGED_BEGIN);
|
|
2247
|
+
if (beginIdx === -1) {
|
|
2248
|
+
return { before: lines, after: [], blockValues: /* @__PURE__ */ new Map(), hasBlock: false };
|
|
2249
|
+
}
|
|
2250
|
+
const endIdx = lines.findIndex((l, i) => i > beginIdx && l.trim() === MANAGED_END);
|
|
2251
|
+
if (endIdx === -1) {
|
|
2252
|
+
return {
|
|
2253
|
+
before: lines.slice(0, beginIdx),
|
|
2254
|
+
after: [],
|
|
2255
|
+
blockValues: parseEnvContent(lines.slice(beginIdx + 1).join("\n")),
|
|
2256
|
+
hasBlock: true
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
return {
|
|
2260
|
+
before: lines.slice(0, beginIdx),
|
|
2261
|
+
after: lines.slice(endIdx + 1),
|
|
2262
|
+
blockValues: parseEnvContent(lines.slice(beginIdx + 1, endIdx).join("\n")),
|
|
2263
|
+
hasBlock: true
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
function stripManagedKeyLines(region) {
|
|
2267
|
+
return region.filter((raw) => {
|
|
2268
|
+
const line = raw.trim();
|
|
2269
|
+
if (line.length === 0 || line.startsWith("#")) return true;
|
|
2270
|
+
const eq = line.indexOf("=");
|
|
2271
|
+
if (eq <= 0) return true;
|
|
2272
|
+
return !MANAGED_KEYS.includes(line.slice(0, eq).trim());
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
function renderBlock(values) {
|
|
2276
|
+
return [
|
|
2277
|
+
MANAGED_BEGIN,
|
|
2278
|
+
"# Auto-generated by `create-op-node bootstrap`. Non-secret model config,",
|
|
2279
|
+
"# read by docker compose via `.env` auto-load. Edit LLM_MODEL here to",
|
|
2280
|
+
"# change the model for every service; re-running bootstrap preserves your",
|
|
2281
|
+
"# edits unless you explicitly re-select a model. Secrets do NOT live here.",
|
|
2282
|
+
...values.map(([k, v]) => `${k}=${v}`),
|
|
2283
|
+
MANAGED_END
|
|
2284
|
+
];
|
|
2285
|
+
}
|
|
2286
|
+
function buildManagedEnvContent(existing, selection, opts = {}) {
|
|
2287
|
+
const split = splitManaged(existing);
|
|
2288
|
+
const importedBefore = filterManaged(parseEnvContent(split.before.join("\n")));
|
|
2289
|
+
const importedAfter = filterManaged(parseEnvContent(split.after.join("\n")));
|
|
2290
|
+
const resolved = [];
|
|
2291
|
+
for (const [key, selected] of selectionToPairs(selection)) {
|
|
2292
|
+
const existingVal = split.blockValues.get(key) ?? importedBefore.get(key) ?? importedAfter.get(key);
|
|
2293
|
+
const final = opts.overwrite ? selected ?? existingVal : existingVal ?? selected;
|
|
2294
|
+
if (final === void 0) continue;
|
|
2295
|
+
if (!ENV_VALUE_RE.test(final)) {
|
|
2296
|
+
return { error: `${key} value ${JSON.stringify(final)} is not safe for a .env line` };
|
|
2297
|
+
}
|
|
2298
|
+
resolved.push([key, final]);
|
|
2299
|
+
}
|
|
2300
|
+
const before = stripManagedKeyLines(split.before);
|
|
2301
|
+
const after = stripManagedKeyLines(split.after);
|
|
2302
|
+
const block = renderBlock(resolved);
|
|
2303
|
+
const parts = [joinTrimmed(before), block.join("\n"), joinTrimmed(after)].filter(
|
|
2304
|
+
(s) => s.length > 0
|
|
2305
|
+
);
|
|
2306
|
+
return { content: parts.join("\n\n") + "\n" };
|
|
2307
|
+
}
|
|
2308
|
+
function filterManaged(all) {
|
|
2309
|
+
const out = /* @__PURE__ */ new Map();
|
|
2310
|
+
for (const key of MANAGED_KEYS) {
|
|
2311
|
+
const v = all.get(key);
|
|
2312
|
+
if (v !== void 0) out.set(key, v);
|
|
2313
|
+
}
|
|
2314
|
+
return out;
|
|
2315
|
+
}
|
|
2316
|
+
function joinTrimmed(region) {
|
|
2317
|
+
const joined = region.join("\n");
|
|
2318
|
+
let start = 0;
|
|
2319
|
+
let end = joined.length;
|
|
2320
|
+
while (start < end && joined[start] === "\n") start++;
|
|
2321
|
+
while (end > start && joined[end - 1] === "\n") end--;
|
|
2322
|
+
return joined.slice(start, end);
|
|
2323
|
+
}
|
|
2324
|
+
async function writeManagedEnv(repoDir, selection, opts = {}) {
|
|
2325
|
+
const target = join(repoDir, ".env");
|
|
2326
|
+
let existing = "";
|
|
2327
|
+
let existingMode;
|
|
2328
|
+
try {
|
|
2329
|
+
existing = await readFile(target, "utf8");
|
|
2330
|
+
existingMode = (await stat(target)).mode & 511;
|
|
2331
|
+
} catch {
|
|
2332
|
+
}
|
|
2333
|
+
const built = buildManagedEnvContent(existing, selection, opts);
|
|
2334
|
+
if ("error" in built) return { ok: false, reason: built.error };
|
|
2335
|
+
if (built.content === existing) return { ok: true, path: target, unchanged: true };
|
|
2336
|
+
const mode = existingMode ?? 420;
|
|
2337
|
+
const tmp = `${target}.tmp.${process.pid}`;
|
|
2338
|
+
try {
|
|
2339
|
+
await mkdir(dirname(target), { recursive: true });
|
|
2340
|
+
await writeFile(tmp, built.content, { mode });
|
|
2341
|
+
await chmod(tmp, mode);
|
|
2342
|
+
await rename(tmp, target);
|
|
2343
|
+
return { ok: true, path: target };
|
|
2344
|
+
} catch (err) {
|
|
2345
|
+
return { ok: false, reason: `writing ${target} failed: ${err.message}` };
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2229
2348
|
var NODE_REPO_MARKERS = [
|
|
2230
2349
|
"docker-compose-prod.yml",
|
|
2231
2350
|
"supabase/init/pgsodium_getkey_env.sh"
|
|
@@ -2299,6 +2418,11 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2299
2418
|
"--embedding-model <model>",
|
|
2300
2419
|
`Ollama embedding model. Default: ${DEFAULT_EMBEDDING_MODEL}. Only takes effect when the knowledge service runs with EMBEDDINGS_PROVIDER=ollama (otherwise embeddings are computed in-process via xenova).`
|
|
2301
2420
|
)
|
|
2421
|
+
).addOption(
|
|
2422
|
+
new Option(
|
|
2423
|
+
"--embeddings-provider <provider>",
|
|
2424
|
+
`Where embeddings are computed. Default: ${DEFAULT_EMBEDDINGS_PROVIDER} (in-process). \`ollama\` uses the host daemon with the embedding model. Written to the node's .env as the single source of truth.`
|
|
2425
|
+
).choices(["xenova", "ollama"])
|
|
2302
2426
|
).addOption(
|
|
2303
2427
|
new Option(
|
|
2304
2428
|
"--supabase-url <url>",
|
|
@@ -2341,6 +2465,7 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2341
2465
|
llmModel: llmModelChoice,
|
|
2342
2466
|
...opts.embeddingModel !== void 0 ? { embeddingModel: opts.embeddingModel } : {}
|
|
2343
2467
|
});
|
|
2468
|
+
const embeddingsProvider = opts.embeddingsProvider ?? DEFAULT_EMBEDDINGS_PROVIDER;
|
|
2344
2469
|
await runSystemChecksPhase();
|
|
2345
2470
|
await runBrewPhase(opts);
|
|
2346
2471
|
await ensureGhAuth();
|
|
@@ -2348,11 +2473,18 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2348
2473
|
const repoPath = await locateRegionRepoPhase(opts, owner, repoName);
|
|
2349
2474
|
await unlockKeychainPhase();
|
|
2350
2475
|
const secrets = await collectSecretsPhase({ region, nodeType, opts });
|
|
2351
|
-
await runLaunchAgentPhase({ opts, secrets
|
|
2476
|
+
await runLaunchAgentPhase({ opts, secrets });
|
|
2352
2477
|
await installWrapperPhase({ repoPath, region, promptServiceUrl: secrets.promptServiceUrl });
|
|
2353
2478
|
await loginGhcrPhase();
|
|
2354
2479
|
await runOllamaPhase({ opts, embeddingModel, llmModel });
|
|
2355
|
-
await
|
|
2480
|
+
await runEnvFilePhase({
|
|
2481
|
+
repoPath,
|
|
2482
|
+
llmModel,
|
|
2483
|
+
embeddingModel,
|
|
2484
|
+
embeddingsProvider,
|
|
2485
|
+
localOnly: Boolean(opts.localOnly)
|
|
2486
|
+
});
|
|
2487
|
+
await runStackPhase({ opts, repoPath, region, composeFile, secrets });
|
|
2356
2488
|
});
|
|
2357
2489
|
function defaultComposeFiles(opts) {
|
|
2358
2490
|
return opts.localOnly ? ["docker-compose-prod.yml"] : ["docker-compose-prod.yml", "docker-compose-backup.yml"];
|
|
@@ -2716,15 +2848,13 @@ async function collectSecretsPhase(args) {
|
|
|
2716
2848
|
return { ...core, ...promptSecrets, promptServiceUrl, supabaseUrl };
|
|
2717
2849
|
}
|
|
2718
2850
|
async function runLaunchAgentPhase(args) {
|
|
2719
|
-
const { opts, secrets
|
|
2851
|
+
const { opts, secrets } = args;
|
|
2720
2852
|
if (opts.skipLaunchAgent) return;
|
|
2721
2853
|
const laSpin = p3.spinner();
|
|
2722
2854
|
laSpin.start("Writing pgsodium key file + LaunchAgent plist\u2026");
|
|
2723
2855
|
const la = await setupLaunchAgent({
|
|
2724
2856
|
pgsodiumKey: secrets.pgsodiumKey,
|
|
2725
2857
|
...secrets.tunnelToken !== void 0 ? { tunnelToken: secrets.tunnelToken } : {},
|
|
2726
|
-
llmModel,
|
|
2727
|
-
embeddingModel,
|
|
2728
2858
|
postgresPassword: secrets.postgresPassword,
|
|
2729
2859
|
jwtSecret: secrets.jwtSecret,
|
|
2730
2860
|
supabaseAnonKey: secrets.supabaseAnonKey,
|
|
@@ -2809,8 +2939,33 @@ async function runOllamaPhase(args) {
|
|
|
2809
2939
|
p3.note(`${pc2.yellow("\u26A0")} ${probe.reason}`, "Docker host networking");
|
|
2810
2940
|
}
|
|
2811
2941
|
}
|
|
2942
|
+
async function runEnvFilePhase(args) {
|
|
2943
|
+
const { repoPath, llmModel, embeddingModel, embeddingsProvider, localOnly } = args;
|
|
2944
|
+
const envSpin = p3.spinner();
|
|
2945
|
+
envSpin.start("Writing model config to the node .env\u2026");
|
|
2946
|
+
const res = await writeManagedEnv(
|
|
2947
|
+
repoPath,
|
|
2948
|
+
{
|
|
2949
|
+
llmModel,
|
|
2950
|
+
embeddingModel,
|
|
2951
|
+
embeddingsProvider,
|
|
2952
|
+
// NODE_ENV=development only for local-dev nodes; production nodes leave
|
|
2953
|
+
// it unset (the compose default / container images decide).
|
|
2954
|
+
...localOnly ? { nodeEnv: "development" } : {}
|
|
2955
|
+
},
|
|
2956
|
+
{ overwrite: true }
|
|
2957
|
+
);
|
|
2958
|
+
if (!res.ok) {
|
|
2959
|
+
envSpin.stop(pc2.red("\u2717 Failed to write the node .env."));
|
|
2960
|
+
p3.cancel(res.reason ?? "writing .env failed.");
|
|
2961
|
+
process.exit(1);
|
|
2962
|
+
}
|
|
2963
|
+
envSpin.stop(
|
|
2964
|
+
res.unchanged ? pc2.green(`\u2713 Model config already current in ${res.path}.`) : pc2.green(`\u2713 Wrote model config to ${res.path} (LLM_MODEL=${llmModel}).`)
|
|
2965
|
+
);
|
|
2966
|
+
}
|
|
2812
2967
|
function buildComposeEnv(args) {
|
|
2813
|
-
const { secrets
|
|
2968
|
+
const { secrets } = args;
|
|
2814
2969
|
return {
|
|
2815
2970
|
PGSODIUM_ROOT_KEY: secrets.pgsodiumKey,
|
|
2816
2971
|
POSTGRES_PASSWORD: secrets.postgresPassword,
|
|
@@ -2838,9 +2993,12 @@ function buildComposeEnv(args) {
|
|
|
2838
2993
|
...secrets.promptsDbPassword !== void 0 ? { PROMPTS_DB_PASSWORD: secrets.promptsDbPassword } : {},
|
|
2839
2994
|
...secrets.promptServiceApiKey !== void 0 ? { PROMPT_SERVICE_API_KEY: secrets.promptServiceApiKey } : {},
|
|
2840
2995
|
...secrets.promptServiceApiKeys !== void 0 ? { PROMPT_SERVICE_API_KEYS: secrets.promptServiceApiKeys } : {},
|
|
2841
|
-
...secrets.promptServiceAdminApiKeys !== void 0 ? { PROMPT_SERVICE_ADMIN_API_KEYS: secrets.promptServiceAdminApiKeys } : {}
|
|
2842
|
-
|
|
2843
|
-
|
|
2996
|
+
...secrets.promptServiceAdminApiKeys !== void 0 ? { PROMPT_SERVICE_ADMIN_API_KEYS: secrets.promptServiceAdminApiKeys } : {}
|
|
2997
|
+
// Model config (LLM_MODEL / EMBEDDINGS_MODEL / NODE_ENV) is intentionally
|
|
2998
|
+
// NOT injected here — it lives in the node's `.env` (runEnvFilePhase),
|
|
2999
|
+
// which docker compose auto-loads. Injecting it into the subprocess env
|
|
3000
|
+
// would shadow `.env` (shell env > .env) and reintroduce the drift the
|
|
3001
|
+
// single-source `.env` exists to eliminate.
|
|
2844
3002
|
};
|
|
2845
3003
|
}
|
|
2846
3004
|
function checkPublicProfileSecrets(env) {
|
|
@@ -2920,7 +3078,7 @@ cosign is required for the fail-closed signature gate \u2014 install it (\`brew
|
|
|
2920
3078
|
);
|
|
2921
3079
|
}
|
|
2922
3080
|
async function runStackPhase(args) {
|
|
2923
|
-
const { opts, repoPath, region, composeFile, secrets
|
|
3081
|
+
const { opts, repoPath, region, composeFile, secrets } = args;
|
|
2924
3082
|
if (opts.skipStack) {
|
|
2925
3083
|
const profileFlag = opts.localOnly ? "" : "--profile public ";
|
|
2926
3084
|
p3.outro(
|
|
@@ -2931,7 +3089,7 @@ async function runStackPhase(args) {
|
|
|
2931
3089
|
return;
|
|
2932
3090
|
}
|
|
2933
3091
|
const composeFiles = resolveComposeFiles(repoPath, composeFile);
|
|
2934
|
-
const composeEnv = buildComposeEnv({ secrets
|
|
3092
|
+
const composeEnv = buildComposeEnv({ secrets });
|
|
2935
3093
|
const composeOpts = {
|
|
2936
3094
|
files: composeFiles,
|
|
2937
3095
|
cwd: repoPath,
|
|
@@ -3055,32 +3213,38 @@ function resolveModels(opts) {
|
|
|
3055
3213
|
}
|
|
3056
3214
|
var LLM_MODEL_CHOICES = [
|
|
3057
3215
|
{
|
|
3058
|
-
value: "
|
|
3059
|
-
label: "
|
|
3060
|
-
hint: "
|
|
3216
|
+
value: "qwen3.6:35b-a3b",
|
|
3217
|
+
label: "qwen3.6:35b-a3b",
|
|
3218
|
+
hint: "35B MoE, ~3B active \u2014 fast on Apple Silicon. ~24 GB footprint. Pre-selected on 64 GB+ nodes. Best Spanish + multilingual quality. Pull ~24 GB, 30\u201360 min."
|
|
3219
|
+
},
|
|
3220
|
+
{
|
|
3221
|
+
value: "qwen3:14b",
|
|
3222
|
+
label: "qwen3:14b",
|
|
3223
|
+
hint: "14B, ~9 GB footprint. Pre-selected on 32\u201364 GB nodes. Solid Spanish, faster than the 35B MoE. Pull ~9 GB, 10\u201320 min."
|
|
3061
3224
|
},
|
|
3062
3225
|
{
|
|
3063
|
-
value: "qwen2.5:
|
|
3064
|
-
label: "qwen2.5:
|
|
3065
|
-
hint: "
|
|
3226
|
+
value: "qwen2.5:7b",
|
|
3227
|
+
label: "qwen2.5:7b",
|
|
3228
|
+
hint: "7B, ~5 GB footprint. Pre-selected on 16\u201332 GB nodes; also the -y / scripted default. Pull ~5 GB, 3\u20135 min."
|
|
3066
3229
|
},
|
|
3067
3230
|
{
|
|
3068
|
-
value: "
|
|
3069
|
-
label: "
|
|
3070
|
-
hint: "
|
|
3231
|
+
value: "qwen2.5:3b",
|
|
3232
|
+
label: "qwen2.5:3b",
|
|
3233
|
+
hint: "3B, ~2\u20133 GB footprint. Pre-selected on \u2264 16 GB nodes (Mac Mini). Lightest option. Pull ~2 GB, 2\u20133 min."
|
|
3071
3234
|
}
|
|
3072
3235
|
];
|
|
3073
3236
|
var OTHER_SENTINEL = "__OTHER__";
|
|
3074
3237
|
function recommendLlmModel(ramGB) {
|
|
3075
3238
|
if (ramGB === null) return null;
|
|
3076
|
-
if (ramGB >=
|
|
3077
|
-
if (ramGB >=
|
|
3078
|
-
return "
|
|
3239
|
+
if (ramGB >= 64) return "qwen3.6:35b-a3b";
|
|
3240
|
+
if (ramGB >= 32) return "qwen3:14b";
|
|
3241
|
+
if (ramGB >= 16) return "qwen2.5:7b";
|
|
3242
|
+
return "qwen2.5:3b";
|
|
3079
3243
|
}
|
|
3080
3244
|
async function selectLlmModel(opts) {
|
|
3081
3245
|
if (opts.yes) return DEFAULT_LLM_MODEL;
|
|
3082
3246
|
const ramGB = await detectUnifiedMemoryGB();
|
|
3083
|
-
const recommended = recommendLlmModel(ramGB) ??
|
|
3247
|
+
const recommended = recommendLlmModel(ramGB) ?? DEFAULT_LLM_MODEL;
|
|
3084
3248
|
const ramNote = ramGB !== null ? `Detected ${ramGB} GB unified memory \u2014 pre-selecting ${recommended}.` : `Couldn't detect Studio memory \u2014 defaulting to ${recommended} (override if your config differs).`;
|
|
3085
3249
|
p3.note(ramNote, "Hardware");
|
|
3086
3250
|
const choice = unwrap(
|
|
@@ -5154,7 +5318,7 @@ function withDefaultSubcommand(argv, known) {
|
|
|
5154
5318
|
}
|
|
5155
5319
|
|
|
5156
5320
|
// src/cli.ts
|
|
5157
|
-
var VERSION = "0.
|
|
5321
|
+
var VERSION = "0.13.0";
|
|
5158
5322
|
var program = new Command();
|
|
5159
5323
|
program.name("create-op-node").description(
|
|
5160
5324
|
"Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."
|