create-op-node 0.12.4 → 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 +205 -42
- 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';
|
|
@@ -1502,7 +1502,6 @@ async function writePgsodiumKeyFile(key, keyFile) {
|
|
|
1502
1502
|
return { ok: false, reason: `writing ${keyFile} failed: ${err.message}` };
|
|
1503
1503
|
}
|
|
1504
1504
|
}
|
|
1505
|
-
var MODEL_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;
|
|
1506
1505
|
function renderLaunchAgentPlist(input) {
|
|
1507
1506
|
validatePlistInput(input);
|
|
1508
1507
|
const command = buildSetenvCommand(input);
|
|
@@ -1528,16 +1527,6 @@ function validatePlistInput(input) {
|
|
|
1528
1527
|
if (input.tunnelToken !== void 0 && !TUNNEL_TOKEN_RE.test(input.tunnelToken)) {
|
|
1529
1528
|
throw new Error("Tunnel token contains characters outside the expected base64-url set");
|
|
1530
1529
|
}
|
|
1531
|
-
if (input.llmModel !== void 0 && !MODEL_NAME_RE.test(input.llmModel)) {
|
|
1532
|
-
throw new Error(
|
|
1533
|
-
`llmModel ${JSON.stringify(input.llmModel)} contains characters not allowed in a launchd setenv value`
|
|
1534
|
-
);
|
|
1535
|
-
}
|
|
1536
|
-
if (input.embeddingModel !== void 0 && !MODEL_NAME_RE.test(input.embeddingModel)) {
|
|
1537
|
-
throw new Error(
|
|
1538
|
-
`embeddingModel ${JSON.stringify(input.embeddingModel)} contains characters not allowed in a launchd setenv value`
|
|
1539
|
-
);
|
|
1540
|
-
}
|
|
1541
1530
|
if (!SAFE_PATH_RE.test(input.keyFilePath)) {
|
|
1542
1531
|
throw new Error(
|
|
1543
1532
|
`keyFilePath ${JSON.stringify(input.keyFilePath)} contains characters not allowed in a launchd path interpolation`
|
|
@@ -1571,8 +1560,6 @@ function buildSetenvCommand(input) {
|
|
|
1571
1560
|
// of — this is word-split safety, not new injection surface. (#36)
|
|
1572
1561
|
`launchctl setenv PGSODIUM_ROOT_KEY "$(cat "${input.keyFilePath}")"`,
|
|
1573
1562
|
...input.tunnelToken !== void 0 ? [`launchctl setenv TUNNEL_TOKEN "${input.tunnelToken}"`] : [],
|
|
1574
|
-
...input.llmModel !== void 0 ? [`launchctl setenv LLM_MODEL "${input.llmModel}"`] : [],
|
|
1575
|
-
...input.embeddingModel !== void 0 ? [`launchctl setenv EMBEDDINGS_MODEL "${input.embeddingModel}"`] : [],
|
|
1576
1563
|
...input.postgresPassword !== void 0 ? [`launchctl setenv POSTGRES_PASSWORD "${input.postgresPassword}"`] : [],
|
|
1577
1564
|
...input.jwtSecret !== void 0 ? [`launchctl setenv JWT_SECRET "${input.jwtSecret}"`] : [],
|
|
1578
1565
|
...input.supabaseAnonKey !== void 0 ? [`launchctl setenv SUPABASE_ANON_KEY "${input.supabaseAnonKey}"`] : [],
|
|
@@ -1611,8 +1598,6 @@ function plistInputFromSetup(input, keyFilePath) {
|
|
|
1611
1598
|
return {
|
|
1612
1599
|
keyFilePath,
|
|
1613
1600
|
...input.tunnelToken !== void 0 ? { tunnelToken: input.tunnelToken } : {},
|
|
1614
|
-
...input.llmModel !== void 0 ? { llmModel: input.llmModel } : {},
|
|
1615
|
-
...input.embeddingModel !== void 0 ? { embeddingModel: input.embeddingModel } : {},
|
|
1616
1601
|
...input.postgresPassword !== void 0 ? { postgresPassword: input.postgresPassword } : {},
|
|
1617
1602
|
...input.jwtSecret !== void 0 ? { jwtSecret: input.jwtSecret } : {},
|
|
1618
1603
|
...input.supabaseAnonKey !== void 0 ? { supabaseAnonKey: input.supabaseAnonKey } : {},
|
|
@@ -2107,8 +2092,9 @@ async function cosignVerifyImage(input) {
|
|
|
2107
2092
|
var OLLAMA_URL = "http://localhost:11434";
|
|
2108
2093
|
var OLLAMA_HEALTH_TIMEOUT_MS = 5e3;
|
|
2109
2094
|
var OLLAMA_WARM_TIMEOUT_MS = 12e4;
|
|
2110
|
-
var DEFAULT_LLM_MODEL = "
|
|
2095
|
+
var DEFAULT_LLM_MODEL = "qwen2.5:7b";
|
|
2111
2096
|
var DEFAULT_EMBEDDING_MODEL = "nomic-embed-text";
|
|
2097
|
+
var DEFAULT_EMBEDDINGS_PROVIDER = "xenova";
|
|
2112
2098
|
var PROBE_ALPINE_TAG = "3.20";
|
|
2113
2099
|
async function startOllamaService() {
|
|
2114
2100
|
const res = await safeExeca("brew", ["services", "start", "ollama"]);
|
|
@@ -2227,6 +2213,138 @@ async function setupModels(models, onProgress) {
|
|
|
2227
2213
|
}
|
|
2228
2214
|
return report;
|
|
2229
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
|
+
}
|
|
2230
2348
|
var NODE_REPO_MARKERS = [
|
|
2231
2349
|
"docker-compose-prod.yml",
|
|
2232
2350
|
"supabase/init/pgsodium_getkey_env.sh"
|
|
@@ -2300,6 +2418,11 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2300
2418
|
"--embedding-model <model>",
|
|
2301
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).`
|
|
2302
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"])
|
|
2303
2426
|
).addOption(
|
|
2304
2427
|
new Option(
|
|
2305
2428
|
"--supabase-url <url>",
|
|
@@ -2342,6 +2465,7 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2342
2465
|
llmModel: llmModelChoice,
|
|
2343
2466
|
...opts.embeddingModel !== void 0 ? { embeddingModel: opts.embeddingModel } : {}
|
|
2344
2467
|
});
|
|
2468
|
+
const embeddingsProvider = opts.embeddingsProvider ?? DEFAULT_EMBEDDINGS_PROVIDER;
|
|
2345
2469
|
await runSystemChecksPhase();
|
|
2346
2470
|
await runBrewPhase(opts);
|
|
2347
2471
|
await ensureGhAuth();
|
|
@@ -2349,11 +2473,18 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2349
2473
|
const repoPath = await locateRegionRepoPhase(opts, owner, repoName);
|
|
2350
2474
|
await unlockKeychainPhase();
|
|
2351
2475
|
const secrets = await collectSecretsPhase({ region, nodeType, opts });
|
|
2352
|
-
await runLaunchAgentPhase({ opts, secrets
|
|
2476
|
+
await runLaunchAgentPhase({ opts, secrets });
|
|
2353
2477
|
await installWrapperPhase({ repoPath, region, promptServiceUrl: secrets.promptServiceUrl });
|
|
2354
2478
|
await loginGhcrPhase();
|
|
2355
2479
|
await runOllamaPhase({ opts, embeddingModel, llmModel });
|
|
2356
|
-
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 });
|
|
2357
2488
|
});
|
|
2358
2489
|
function defaultComposeFiles(opts) {
|
|
2359
2490
|
return opts.localOnly ? ["docker-compose-prod.yml"] : ["docker-compose-prod.yml", "docker-compose-backup.yml"];
|
|
@@ -2717,15 +2848,13 @@ async function collectSecretsPhase(args) {
|
|
|
2717
2848
|
return { ...core, ...promptSecrets, promptServiceUrl, supabaseUrl };
|
|
2718
2849
|
}
|
|
2719
2850
|
async function runLaunchAgentPhase(args) {
|
|
2720
|
-
const { opts, secrets
|
|
2851
|
+
const { opts, secrets } = args;
|
|
2721
2852
|
if (opts.skipLaunchAgent) return;
|
|
2722
2853
|
const laSpin = p3.spinner();
|
|
2723
2854
|
laSpin.start("Writing pgsodium key file + LaunchAgent plist\u2026");
|
|
2724
2855
|
const la = await setupLaunchAgent({
|
|
2725
2856
|
pgsodiumKey: secrets.pgsodiumKey,
|
|
2726
2857
|
...secrets.tunnelToken !== void 0 ? { tunnelToken: secrets.tunnelToken } : {},
|
|
2727
|
-
llmModel,
|
|
2728
|
-
embeddingModel,
|
|
2729
2858
|
postgresPassword: secrets.postgresPassword,
|
|
2730
2859
|
jwtSecret: secrets.jwtSecret,
|
|
2731
2860
|
supabaseAnonKey: secrets.supabaseAnonKey,
|
|
@@ -2810,8 +2939,33 @@ async function runOllamaPhase(args) {
|
|
|
2810
2939
|
p3.note(`${pc2.yellow("\u26A0")} ${probe.reason}`, "Docker host networking");
|
|
2811
2940
|
}
|
|
2812
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
|
+
}
|
|
2813
2967
|
function buildComposeEnv(args) {
|
|
2814
|
-
const { secrets
|
|
2968
|
+
const { secrets } = args;
|
|
2815
2969
|
return {
|
|
2816
2970
|
PGSODIUM_ROOT_KEY: secrets.pgsodiumKey,
|
|
2817
2971
|
POSTGRES_PASSWORD: secrets.postgresPassword,
|
|
@@ -2839,9 +2993,12 @@ function buildComposeEnv(args) {
|
|
|
2839
2993
|
...secrets.promptsDbPassword !== void 0 ? { PROMPTS_DB_PASSWORD: secrets.promptsDbPassword } : {},
|
|
2840
2994
|
...secrets.promptServiceApiKey !== void 0 ? { PROMPT_SERVICE_API_KEY: secrets.promptServiceApiKey } : {},
|
|
2841
2995
|
...secrets.promptServiceApiKeys !== void 0 ? { PROMPT_SERVICE_API_KEYS: secrets.promptServiceApiKeys } : {},
|
|
2842
|
-
...secrets.promptServiceAdminApiKeys !== void 0 ? { PROMPT_SERVICE_ADMIN_API_KEYS: secrets.promptServiceAdminApiKeys } : {}
|
|
2843
|
-
|
|
2844
|
-
|
|
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.
|
|
2845
3002
|
};
|
|
2846
3003
|
}
|
|
2847
3004
|
function checkPublicProfileSecrets(env) {
|
|
@@ -2921,7 +3078,7 @@ cosign is required for the fail-closed signature gate \u2014 install it (\`brew
|
|
|
2921
3078
|
);
|
|
2922
3079
|
}
|
|
2923
3080
|
async function runStackPhase(args) {
|
|
2924
|
-
const { opts, repoPath, region, composeFile, secrets
|
|
3081
|
+
const { opts, repoPath, region, composeFile, secrets } = args;
|
|
2925
3082
|
if (opts.skipStack) {
|
|
2926
3083
|
const profileFlag = opts.localOnly ? "" : "--profile public ";
|
|
2927
3084
|
p3.outro(
|
|
@@ -2932,7 +3089,7 @@ async function runStackPhase(args) {
|
|
|
2932
3089
|
return;
|
|
2933
3090
|
}
|
|
2934
3091
|
const composeFiles = resolveComposeFiles(repoPath, composeFile);
|
|
2935
|
-
const composeEnv = buildComposeEnv({ secrets
|
|
3092
|
+
const composeEnv = buildComposeEnv({ secrets });
|
|
2936
3093
|
const composeOpts = {
|
|
2937
3094
|
files: composeFiles,
|
|
2938
3095
|
cwd: repoPath,
|
|
@@ -3056,32 +3213,38 @@ function resolveModels(opts) {
|
|
|
3056
3213
|
}
|
|
3057
3214
|
var LLM_MODEL_CHOICES = [
|
|
3058
3215
|
{
|
|
3059
|
-
value: "
|
|
3060
|
-
label: "
|
|
3061
|
-
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."
|
|
3062
3224
|
},
|
|
3063
3225
|
{
|
|
3064
|
-
value: "qwen2.5:
|
|
3065
|
-
label: "qwen2.5:
|
|
3066
|
-
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."
|
|
3067
3229
|
},
|
|
3068
3230
|
{
|
|
3069
|
-
value: "
|
|
3070
|
-
label: "
|
|
3071
|
-
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."
|
|
3072
3234
|
}
|
|
3073
3235
|
];
|
|
3074
3236
|
var OTHER_SENTINEL = "__OTHER__";
|
|
3075
3237
|
function recommendLlmModel(ramGB) {
|
|
3076
3238
|
if (ramGB === null) return null;
|
|
3077
|
-
if (ramGB >=
|
|
3078
|
-
if (ramGB >=
|
|
3079
|
-
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";
|
|
3080
3243
|
}
|
|
3081
3244
|
async function selectLlmModel(opts) {
|
|
3082
3245
|
if (opts.yes) return DEFAULT_LLM_MODEL;
|
|
3083
3246
|
const ramGB = await detectUnifiedMemoryGB();
|
|
3084
|
-
const recommended = recommendLlmModel(ramGB) ??
|
|
3247
|
+
const recommended = recommendLlmModel(ramGB) ?? DEFAULT_LLM_MODEL;
|
|
3085
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).`;
|
|
3086
3249
|
p3.note(ramNote, "Hardware");
|
|
3087
3250
|
const choice = unwrap(
|
|
@@ -5155,7 +5318,7 @@ function withDefaultSubcommand(argv, known) {
|
|
|
5155
5318
|
}
|
|
5156
5319
|
|
|
5157
5320
|
// src/cli.ts
|
|
5158
|
-
var VERSION = "0.
|
|
5321
|
+
var VERSION = "0.13.0";
|
|
5159
5322
|
var program = new Command();
|
|
5160
5323
|
program.name("create-op-node").description(
|
|
5161
5324
|
"Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."
|