create-op-node 0.13.0 → 0.14.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 +24 -3
- package/dist/cli.js +119 -9
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -238,7 +238,7 @@ npx create-op-node verify --domain your-domain.example
|
|
|
238
238
|
```
|
|
239
239
|
|
|
240
240
|
Off-LAN health probe of a live node, runnable from anywhere with internet
|
|
241
|
-
access.
|
|
241
|
+
access. Six phases:
|
|
242
242
|
|
|
243
243
|
1. **TLS handshake** to `api.<domain>:443` — surfaces cert subject, issuer,
|
|
244
244
|
and days-to-expiry. Warns when the cert is within `--cert-warn-days`
|
|
@@ -248,12 +248,22 @@ access. Five phases:
|
|
|
248
248
|
3. **`POST https://api.<domain>/api`** with `{ __typename }` must return a
|
|
249
249
|
valid GraphQL envelope (catches the "TLS green, but a misconfigured
|
|
250
250
|
proxy returns HTML" case).
|
|
251
|
-
4. **
|
|
251
|
+
4. **Ollama model presence** (optional, node-local) — asserts the configured
|
|
252
|
+
`LLM_MODEL` is actually pulled into the local Ollama, failing loudly with
|
|
253
|
+
the exact `ollama pull <model>` remedy if not. This catches the
|
|
254
|
+
config↔runtime drift that otherwise 404s at inference time (a node set to
|
|
255
|
+
a model that was never downloaded). The model is read from `--llm-model`
|
|
256
|
+
or, when omitted, the node's `.env` (`--repo-dir`, default cwd) — so
|
|
257
|
+
running `verify` **on the node** needs no flags. **Skipped** when no model
|
|
258
|
+
resolves, which keeps an off-LAN `verify --domain …` from tripping it. The
|
|
259
|
+
embedding model is asserted only under `--embeddings-provider ollama`
|
|
260
|
+
(xenova computes embeddings in-process).
|
|
261
|
+
5. **Cloudflare Tunnel status** (optional) — looks up `connections` via
|
|
252
262
|
the CF API. Zero connectors registered → warning that cloudflared on
|
|
253
263
|
the Studio is offline. Requires all three of `--cf-token` (or
|
|
254
264
|
`--cf-token-file`), `--cf-account-id`, `--tunnel-id`; partial
|
|
255
265
|
configuration warns + names the missing flag.
|
|
256
|
-
|
|
266
|
+
6. **`cosign verify`** (optional, repeatable `--image`) — keyless
|
|
257
267
|
verification against the GitHub Actions OIDC issuer + Fulcio +
|
|
258
268
|
the Rekor transparency log. Silently skipped when `cosign` isn't on
|
|
259
269
|
`PATH` (install with `brew install cosign` to enable).
|
|
@@ -281,6 +291,17 @@ invocations — the latter ends up in `ps` output, the former doesn't.
|
|
|
281
291
|
Use `--api-host <host>` to override the default `api.<domain>`
|
|
282
292
|
construction when your node exposes the API at a different subdomain.
|
|
283
293
|
|
|
294
|
+
Run it **on the node** (from the region repo dir) to also catch model
|
|
295
|
+
drift with zero extra flags — the Ollama phase reads `LLM_MODEL` from the
|
|
296
|
+
`.env` bootstrap wrote:
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
cd ~/Development/opuspopuli-node-us-ca
|
|
300
|
+
npx create-op-node verify --domain yournode.example.org
|
|
301
|
+
# or pin the model explicitly from anywhere:
|
|
302
|
+
npx create-op-node verify --domain yournode.example.org --llm-model qwen3.6:35b-a3b
|
|
303
|
+
```
|
|
304
|
+
|
|
284
305
|
## Bootstrapping a region config
|
|
285
306
|
|
|
286
307
|
A node serves data; **what** data it serves is defined by a declarative region
|
package/dist/cli.js
CHANGED
|
@@ -2122,6 +2122,11 @@ async function checkOllamaHealth(url = OLLAMA_URL) {
|
|
|
2122
2122
|
clearTimeout(timer);
|
|
2123
2123
|
}
|
|
2124
2124
|
}
|
|
2125
|
+
function modelPresent(configured, installed) {
|
|
2126
|
+
const norm = (m) => m.includes(":") ? m : `${m}:latest`;
|
|
2127
|
+
const target = norm(configured);
|
|
2128
|
+
return installed.some((m) => norm(m) === target);
|
|
2129
|
+
}
|
|
2125
2130
|
async function pullModel(name) {
|
|
2126
2131
|
const res = await safeExeca("ollama", ["pull", name]);
|
|
2127
2132
|
if (res === null) return { ok: false, reason: "`ollama` not on PATH" };
|
|
@@ -2321,6 +2326,25 @@ function joinTrimmed(region) {
|
|
|
2321
2326
|
while (end > start && joined[end - 1] === "\n") end--;
|
|
2322
2327
|
return joined.slice(start, end);
|
|
2323
2328
|
}
|
|
2329
|
+
async function readEnvModelConfig(repoDir) {
|
|
2330
|
+
let content;
|
|
2331
|
+
try {
|
|
2332
|
+
content = await readFile(join(repoDir, ".env"), "utf8");
|
|
2333
|
+
} catch {
|
|
2334
|
+
return {};
|
|
2335
|
+
}
|
|
2336
|
+
const map = parseEnvContent(content);
|
|
2337
|
+
const cfg = {};
|
|
2338
|
+
const llm = map.get("LLM_MODEL");
|
|
2339
|
+
const emb = map.get("EMBEDDINGS_MODEL");
|
|
2340
|
+
const prov = map.get("EMBEDDINGS_PROVIDER");
|
|
2341
|
+
const node = map.get("NODE_ENV");
|
|
2342
|
+
if (llm !== void 0) cfg.llmModel = llm;
|
|
2343
|
+
if (emb !== void 0) cfg.embeddingModel = emb;
|
|
2344
|
+
if (prov !== void 0) cfg.embeddingsProvider = prov;
|
|
2345
|
+
if (node !== void 0) cfg.nodeEnv = node;
|
|
2346
|
+
return cfg;
|
|
2347
|
+
}
|
|
2324
2348
|
async function writeManagedEnv(repoDir, selection, opts = {}) {
|
|
2325
2349
|
const target = join(repoDir, ".env");
|
|
2326
2350
|
let existing = "";
|
|
@@ -2476,7 +2500,7 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2476
2500
|
await runLaunchAgentPhase({ opts, secrets });
|
|
2477
2501
|
await installWrapperPhase({ repoPath, region, promptServiceUrl: secrets.promptServiceUrl });
|
|
2478
2502
|
await loginGhcrPhase();
|
|
2479
|
-
await runOllamaPhase({ opts, embeddingModel, llmModel });
|
|
2503
|
+
await runOllamaPhase({ opts, embeddingModel, llmModel, embeddingsProvider });
|
|
2480
2504
|
await runEnvFilePhase({
|
|
2481
2505
|
repoPath,
|
|
2482
2506
|
llmModel,
|
|
@@ -2898,8 +2922,12 @@ async function loginGhcrPhase() {
|
|
|
2898
2922
|
}
|
|
2899
2923
|
ghcrSpin.stop(pc2.green("\u2713 Logged in to ghcr.io."));
|
|
2900
2924
|
}
|
|
2925
|
+
function modelsToPull(args) {
|
|
2926
|
+
const { provider, llmModel, embeddingModel } = args;
|
|
2927
|
+
return provider === "ollama" ? [embeddingModel, llmModel] : [llmModel];
|
|
2928
|
+
}
|
|
2901
2929
|
async function runOllamaPhase(args) {
|
|
2902
|
-
const { opts, embeddingModel, llmModel } = args;
|
|
2930
|
+
const { opts, embeddingModel, llmModel, embeddingsProvider } = args;
|
|
2903
2931
|
if (opts.skipOllama) return;
|
|
2904
2932
|
const olSpin = p3.spinner();
|
|
2905
2933
|
olSpin.start(`Pulling + warming Ollama models\u2026 (${estimatedPullTime(llmModel)})`);
|
|
@@ -2926,7 +2954,8 @@ async function runOllamaPhase(args) {
|
|
|
2926
2954
|
process.exit(1);
|
|
2927
2955
|
}
|
|
2928
2956
|
}
|
|
2929
|
-
const
|
|
2957
|
+
const toPull = modelsToPull({ provider: embeddingsProvider, llmModel, embeddingModel });
|
|
2958
|
+
const modelReport = await setupModels(toPull, (model, status) => {
|
|
2930
2959
|
olSpin.message(`${status}: ${model}`);
|
|
2931
2960
|
});
|
|
2932
2961
|
olSpin.stop(
|
|
@@ -2934,6 +2963,12 @@ async function runOllamaPhase(args) {
|
|
|
2934
2963
|
`\u2713 ${modelReport.pulled.length} pulled, ${modelReport.alreadyPresent.length} present, ${modelReport.warmed.length} warmed`
|
|
2935
2964
|
) : pc2.yellow(`\u26A0 ${modelReport.failed.length} model pull(s) failed`)
|
|
2936
2965
|
);
|
|
2966
|
+
if (embeddingsProvider !== "ollama") {
|
|
2967
|
+
p3.note(
|
|
2968
|
+
`${pc2.dim("\xB7")} Skipped pulling the embedding model \u2014 EMBEDDINGS_PROVIDER=${embeddingsProvider} computes embeddings in-process (no Ollama model needed).`,
|
|
2969
|
+
"Embeddings"
|
|
2970
|
+
);
|
|
2971
|
+
}
|
|
2937
2972
|
const probe = await probeHostDockerInternal();
|
|
2938
2973
|
if (!probe.ok) {
|
|
2939
2974
|
p3.note(`${pc2.yellow("\u26A0")} ${probe.reason}`, "Docker host networking");
|
|
@@ -3962,7 +3997,8 @@ var DEFAULT_DEPS2 = {
|
|
|
3962
3997
|
http: httpProbe,
|
|
3963
3998
|
graphql: graphqlProbe,
|
|
3964
3999
|
tunnel: tunnelStatus,
|
|
3965
|
-
cosign: cosignVerifyImage
|
|
4000
|
+
cosign: cosignVerifyImage,
|
|
4001
|
+
ollama: checkOllamaHealth
|
|
3966
4002
|
};
|
|
3967
4003
|
async function runVerify(input, deps = DEFAULT_DEPS2) {
|
|
3968
4004
|
const phases = [];
|
|
@@ -3973,6 +4009,7 @@ async function runVerify(input, deps = DEFAULT_DEPS2) {
|
|
|
3973
4009
|
await verifyTlsPhase(input, deps, push);
|
|
3974
4010
|
await verifyHealthPhase(input, deps, push);
|
|
3975
4011
|
await verifyGraphqlPhase(input, deps, push);
|
|
4012
|
+
await verifyOllamaModelsPhase(input, deps, push);
|
|
3976
4013
|
await verifyCloudflarePhase(input, deps, push);
|
|
3977
4014
|
await verifyCosignPhase(input, deps, push);
|
|
3978
4015
|
return { phases };
|
|
@@ -4010,6 +4047,39 @@ async function verifyGraphqlPhase(input, deps, push) {
|
|
|
4010
4047
|
push({ name: "GraphQL { __typename }", status: "fail", detail: gql.reason });
|
|
4011
4048
|
}
|
|
4012
4049
|
}
|
|
4050
|
+
async function verifyOllamaModelsPhase(input, deps, push) {
|
|
4051
|
+
const cfg = input.ollama;
|
|
4052
|
+
if (!cfg?.llmModel) {
|
|
4053
|
+
push({
|
|
4054
|
+
name: "Ollama models",
|
|
4055
|
+
status: "skipped",
|
|
4056
|
+
detail: "pass --llm-model (or run on the node so its .env is read) to enable"
|
|
4057
|
+
});
|
|
4058
|
+
return;
|
|
4059
|
+
}
|
|
4060
|
+
const health = await deps.ollama();
|
|
4061
|
+
if (!health.reachable) {
|
|
4062
|
+
push({
|
|
4063
|
+
name: "Ollama models",
|
|
4064
|
+
status: "fail",
|
|
4065
|
+
detail: "configured model set but the local Ollama daemon is unreachable on :11434 \u2014 start it with `brew services start ollama` (run this check on the node)"
|
|
4066
|
+
});
|
|
4067
|
+
return;
|
|
4068
|
+
}
|
|
4069
|
+
const required = [cfg.llmModel];
|
|
4070
|
+
if (cfg.provider === "ollama" && cfg.embeddingModel) required.push(cfg.embeddingModel);
|
|
4071
|
+
const missing = required.filter((m) => !modelPresent(m, health.models));
|
|
4072
|
+
if (missing.length > 0) {
|
|
4073
|
+
const remedy = missing.map((m) => `ollama pull ${m}`).join(" && ");
|
|
4074
|
+
push({
|
|
4075
|
+
name: "Ollama models",
|
|
4076
|
+
status: "fail",
|
|
4077
|
+
detail: `not installed: ${missing.join(", ")} \u2014 remedy: ${remedy}`
|
|
4078
|
+
});
|
|
4079
|
+
return;
|
|
4080
|
+
}
|
|
4081
|
+
push({ name: "Ollama models", status: "ok", detail: `${required.join(", ")} present` });
|
|
4082
|
+
}
|
|
4013
4083
|
async function verifyCloudflarePhase(input, deps, push) {
|
|
4014
4084
|
const cfFields = [
|
|
4015
4085
|
["--cf-token", input.cf?.token],
|
|
@@ -4094,6 +4164,17 @@ var verifyCommand = new Command("verify").description(
|
|
|
4094
4164
|
)
|
|
4095
4165
|
).addOption(
|
|
4096
4166
|
new Option("--cert-warn-days <n>", "Warn if cert expires within N days").default("14")
|
|
4167
|
+
).addOption(
|
|
4168
|
+
new Option(
|
|
4169
|
+
"--llm-model <model>",
|
|
4170
|
+
"Assert this model is present in the local Ollama (catches config\u2194runtime drift). Defaults to LLM_MODEL from the node .env when run on the node."
|
|
4171
|
+
)
|
|
4172
|
+
).addOption(
|
|
4173
|
+
new Option("--embedding-model <model>", "Also assert this embedding model is present (only checked when --embeddings-provider=ollama).")
|
|
4174
|
+
).addOption(
|
|
4175
|
+
new Option("--embeddings-provider <provider>", "Embeddings provider the node runs; the embedding model is only asserted for `ollama`.").choices(["xenova", "ollama"])
|
|
4176
|
+
).addOption(
|
|
4177
|
+
new Option("--repo-dir <path>", "Node repo dir whose .env supplies the model config when --llm-model is omitted (default: cwd).")
|
|
4097
4178
|
).addOption(
|
|
4098
4179
|
new Option("--show-skipped", "Include skipped phases in the summary").default(false)
|
|
4099
4180
|
).action(async (opts) => {
|
|
@@ -4103,14 +4184,16 @@ var verifyCommand = new Command("verify").description(
|
|
|
4103
4184
|
const cfToken = resolveCfToken(opts);
|
|
4104
4185
|
const apiHost = opts.apiHost ?? `api.${domain}`;
|
|
4105
4186
|
const images = opts.image ?? [];
|
|
4106
|
-
const
|
|
4187
|
+
const ollama = await resolveOllamaVerifyInput(opts);
|
|
4188
|
+
const totalPhases = 5 + (images.length === 0 ? 1 : images.length);
|
|
4107
4189
|
const report = await runVerifyWithSpinner({
|
|
4108
4190
|
apiHost,
|
|
4109
4191
|
certWarnDays,
|
|
4110
4192
|
cfToken,
|
|
4111
4193
|
opts,
|
|
4112
4194
|
images,
|
|
4113
|
-
totalPhases
|
|
4195
|
+
totalPhases,
|
|
4196
|
+
...ollama ? { ollama } : {}
|
|
4114
4197
|
});
|
|
4115
4198
|
renderVerifySummary(report, { opts, totalPhases });
|
|
4116
4199
|
reportVerifyOutcome(report);
|
|
@@ -4181,8 +4264,34 @@ function resolveCfToken(opts) {
|
|
|
4181
4264
|
process.exit(2);
|
|
4182
4265
|
}
|
|
4183
4266
|
}
|
|
4267
|
+
function normalizeProvider(v) {
|
|
4268
|
+
return v === "ollama" || v === "xenova" ? v : void 0;
|
|
4269
|
+
}
|
|
4270
|
+
function mergeOllamaModelConfig(opts, envCfg) {
|
|
4271
|
+
const llmModel = opts.llmModel ?? envCfg.llmModel;
|
|
4272
|
+
if (!llmModel) return void 0;
|
|
4273
|
+
const embeddingModel = opts.embeddingModel ?? envCfg.embeddingModel;
|
|
4274
|
+
const provider = normalizeProvider(opts.embeddingsProvider ?? envCfg.embeddingsProvider);
|
|
4275
|
+
return {
|
|
4276
|
+
llmModel,
|
|
4277
|
+
...embeddingModel ? { embeddingModel } : {},
|
|
4278
|
+
...provider ? { provider } : {}
|
|
4279
|
+
};
|
|
4280
|
+
}
|
|
4281
|
+
async function resolveOllamaVerifyInput(opts) {
|
|
4282
|
+
const explicit = opts.repoDir !== void 0;
|
|
4283
|
+
const dir = opts.repoDir ?? process.cwd();
|
|
4284
|
+
const envCfg = explicit || await looksLikeNodeRepo(dir) ? await readEnvModelConfig(dir) : {};
|
|
4285
|
+
if (opts.embeddingsProvider === void 0 && envCfg.embeddingsProvider !== void 0 && normalizeProvider(envCfg.embeddingsProvider) === void 0) {
|
|
4286
|
+
p3.note(
|
|
4287
|
+
`${pc2.yellow("\u26A0")} Unrecognized EMBEDDINGS_PROVIDER=${envCfg.embeddingsProvider} in .env \u2014 expected xenova|ollama; the embedding-model check is disabled.`,
|
|
4288
|
+
"verify"
|
|
4289
|
+
);
|
|
4290
|
+
}
|
|
4291
|
+
return mergeOllamaModelConfig(opts, envCfg);
|
|
4292
|
+
}
|
|
4184
4293
|
async function runVerifyWithSpinner(args) {
|
|
4185
|
-
const { apiHost, certWarnDays, cfToken, opts, images, totalPhases } = args;
|
|
4294
|
+
const { apiHost, certWarnDays, cfToken, opts, images, totalPhases, ollama } = args;
|
|
4186
4295
|
let phaseIndex = 0;
|
|
4187
4296
|
let activeSpin = null;
|
|
4188
4297
|
const renderPhase = (ph) => {
|
|
@@ -4203,7 +4312,8 @@ async function runVerifyWithSpinner(args) {
|
|
|
4203
4312
|
...opts.cfAccountId ? { accountId: opts.cfAccountId } : {},
|
|
4204
4313
|
...opts.tunnelId ? { tunnelId: opts.tunnelId } : {}
|
|
4205
4314
|
},
|
|
4206
|
-
images
|
|
4315
|
+
images,
|
|
4316
|
+
...ollama ? { ollama } : {}
|
|
4207
4317
|
},
|
|
4208
4318
|
deps
|
|
4209
4319
|
);
|
|
@@ -5318,7 +5428,7 @@ function withDefaultSubcommand(argv, known) {
|
|
|
5318
5428
|
}
|
|
5319
5429
|
|
|
5320
5430
|
// src/cli.ts
|
|
5321
|
-
var VERSION = "0.
|
|
5431
|
+
var VERSION = "0.14.0";
|
|
5322
5432
|
var program = new Command();
|
|
5323
5433
|
program.name("create-op-node").description(
|
|
5324
5434
|
"Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."
|