create-op-node 0.8.0 → 0.9.1
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 +36 -0
- package/dist/cli.js +294 -18
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -557,6 +557,42 @@ Releases are automated via [release-please](https://github.com/googleapis/releas
|
|
|
557
557
|
|
|
558
558
|
No manual version bumps, no manual tagging. To trigger a release with no functional changes (re-publishing after a CI fix, etc.), push a commit with `chore: trigger release` — release-please will skip it but still update the PR.
|
|
559
559
|
|
|
560
|
+
#### One-time PAT setup
|
|
561
|
+
|
|
562
|
+
GitHub deliberately doesn't fire workflows on branches or tags pushed by
|
|
563
|
+
`GITHUB_TOKEN`. Without a PAT, two things bite every release:
|
|
564
|
+
|
|
565
|
+
- The release PR opens but its CI run never starts (sits in
|
|
566
|
+
*Expected — Waiting for status to be reported*). You can't merge.
|
|
567
|
+
- After you eventually merge, `publish.yml`'s tag-push trigger doesn't
|
|
568
|
+
fire either, so npm never gets the new version.
|
|
569
|
+
|
|
570
|
+
Both go away when release-please uses a PAT instead. One-time setup:
|
|
571
|
+
|
|
572
|
+
1. **Create a fine-grained PAT** at
|
|
573
|
+
[github.com/settings/personal-access-tokens/new](https://github.com/settings/personal-access-tokens/new):
|
|
574
|
+
- *Resource owner*: **OpusPopuli** (not your personal account —
|
|
575
|
+
fine-grained PATs are scoped at creation and can't be moved)
|
|
576
|
+
- *Expiration*: your call (90 days for safety; you'll be reminded
|
|
577
|
+
when it's near expiry)
|
|
578
|
+
- *Repository access*: **Only select repositories** →
|
|
579
|
+
`create-op-node`
|
|
580
|
+
- *Repository permissions*:
|
|
581
|
+
- **Contents**: Read and write
|
|
582
|
+
- **Pull requests**: Read and write
|
|
583
|
+
- **Workflows**: Read and write *(release-please updates the
|
|
584
|
+
version line in `release-please.yml`'s manifest output;
|
|
585
|
+
without this permission the action fails to write)*
|
|
586
|
+
2. **Add it as a repo secret**: repo → Settings → Secrets and
|
|
587
|
+
variables → Actions → New repository secret →
|
|
588
|
+
`RELEASE_PLEASE_TOKEN`, paste the PAT value.
|
|
589
|
+
3. **Done.** Next release-please run uses the PAT, CI runs normally on
|
|
590
|
+
the release PR, and `publish.yml` fires automatically on the tag.
|
|
591
|
+
|
|
592
|
+
The workflow falls back to `GITHUB_TOKEN` when the secret isn't
|
|
593
|
+
present, so it doesn't break in repos that haven't done the PAT setup
|
|
594
|
+
yet — it just goes back to needing the manual workarounds.
|
|
595
|
+
|
|
560
596
|
## License
|
|
561
597
|
|
|
562
598
|
[AGPL-3.0-or-later](./LICENSE). The Opus Populi platform code is AGPL-3.0 + dual commercial; this CLI inherits the AGPL-3.0 terms.
|
package/dist/cli.js
CHANGED
|
@@ -4,9 +4,9 @@ import pc2 from 'picocolors';
|
|
|
4
4
|
import * as p3 from '@clack/prompts';
|
|
5
5
|
import { execa } from 'execa';
|
|
6
6
|
import { Octokit } from '@octokit/rest';
|
|
7
|
-
import { randomBytes } from 'crypto';
|
|
7
|
+
import { randomBytes, createHmac } from 'crypto';
|
|
8
8
|
import { join, resolve, dirname } from 'path';
|
|
9
|
-
import { mkdir, writeFile, access,
|
|
9
|
+
import { mkdir, writeFile, access, chmod, rename, rm } from 'fs/promises';
|
|
10
10
|
import { homedir } from 'os';
|
|
11
11
|
import { readFileSync } from 'fs';
|
|
12
12
|
import * as tls from 'tls';
|
|
@@ -230,9 +230,17 @@ var SERVICE_PREFIX = "org.opuspopuli";
|
|
|
230
230
|
function serviceFor(region) {
|
|
231
231
|
return `${SERVICE_PREFIX}.${region}`;
|
|
232
232
|
}
|
|
233
|
+
var FRIENDLY = {
|
|
234
|
+
"pgsodium-root-key": "pgsodium root key",
|
|
235
|
+
"tunnel-token": "Cloudflare Tunnel token",
|
|
236
|
+
"postgres-password": "Postgres password",
|
|
237
|
+
"jwt-secret": "JWT signing secret",
|
|
238
|
+
"supabase-anon-key": "Supabase anon key",
|
|
239
|
+
"supabase-service-role-key": "Supabase service role key",
|
|
240
|
+
"dashboard-password": "Supabase Studio dashboard password"
|
|
241
|
+
};
|
|
233
242
|
function labelFor(coords) {
|
|
234
|
-
|
|
235
|
-
return `Opus Populi (${coords.region}) \u2014 ${friendly}`;
|
|
243
|
+
return `Opus Populi (${coords.region}) \u2014 ${FRIENDLY[coords.account]}`;
|
|
236
244
|
}
|
|
237
245
|
async function detectKeychain() {
|
|
238
246
|
const res = await safeExeca("security", ["-h"]);
|
|
@@ -430,6 +438,50 @@ async function openPullRequest(input) {
|
|
|
430
438
|
function generatePgsodiumRootKey() {
|
|
431
439
|
return randomBytes(32).toString("hex");
|
|
432
440
|
}
|
|
441
|
+
function base64url(input) {
|
|
442
|
+
const buf = typeof input === "string" ? Buffer.from(input) : input;
|
|
443
|
+
return buf.toString("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
444
|
+
}
|
|
445
|
+
function generatePostgresPassword() {
|
|
446
|
+
return base64url(randomBytes(32));
|
|
447
|
+
}
|
|
448
|
+
function generateDashboardPassword() {
|
|
449
|
+
return base64url(randomBytes(24));
|
|
450
|
+
}
|
|
451
|
+
function generateJwtSecret() {
|
|
452
|
+
return randomBytes(48).toString("base64");
|
|
453
|
+
}
|
|
454
|
+
function verifySupabaseJwt(token, secret) {
|
|
455
|
+
const parts = token.split(".");
|
|
456
|
+
if (parts.length !== 3) return false;
|
|
457
|
+
const [h, p7, sig] = parts;
|
|
458
|
+
const expected = base64url(createHmac("sha256", secret).update(`${h}.${p7}`).digest());
|
|
459
|
+
if (sig.length !== expected.length) return false;
|
|
460
|
+
let mismatch = 0;
|
|
461
|
+
for (let i = 0; i < sig.length; i++) {
|
|
462
|
+
mismatch |= sig.charCodeAt(i) ^ expected.charCodeAt(i);
|
|
463
|
+
}
|
|
464
|
+
return mismatch === 0;
|
|
465
|
+
}
|
|
466
|
+
function signSupabaseJwt(input) {
|
|
467
|
+
const iat = input.issuedAtSeconds ?? 17e8;
|
|
468
|
+
const ttl = input.ttlSeconds ?? 10 * 365 * 24 * 60 * 60;
|
|
469
|
+
const issuer = input.issuer ?? "supabase";
|
|
470
|
+
const header = { alg: "HS256", typ: "JWT" };
|
|
471
|
+
const payload = {
|
|
472
|
+
role: input.role,
|
|
473
|
+
iss: issuer,
|
|
474
|
+
iat,
|
|
475
|
+
exp: iat + ttl
|
|
476
|
+
};
|
|
477
|
+
const encodedHeader = base64url(JSON.stringify(header));
|
|
478
|
+
const encodedPayload = base64url(JSON.stringify(payload));
|
|
479
|
+
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
480
|
+
const signature = base64url(
|
|
481
|
+
createHmac("sha256", input.secret).update(signingInput).digest()
|
|
482
|
+
);
|
|
483
|
+
return `${signingInput}.${signature}`;
|
|
484
|
+
}
|
|
433
485
|
function renderProdTfvars(input) {
|
|
434
486
|
const project = input.project ?? "opuspopuli";
|
|
435
487
|
const api = input.apiSubdomain ?? "api";
|
|
@@ -1042,6 +1094,9 @@ async function waitForApplyAndFetchTunnelToken(input) {
|
|
|
1042
1094
|
var PGSODIUM_KEY_RE = /^[a-f0-9]{64}$/;
|
|
1043
1095
|
var TUNNEL_TOKEN_RE = /^[A-Za-z0-9_\-.=]+$/;
|
|
1044
1096
|
var SAFE_PATH_RE = /^[A-Za-z0-9_\-./ ]+$/;
|
|
1097
|
+
var SAFE_LAUNCHCTL_VALUE_RE = /^[A-Za-z0-9+/=._-]+$/;
|
|
1098
|
+
var URL_SAFE_PASSWORD_RE = /^[A-Za-z0-9_-]+$/;
|
|
1099
|
+
var SAFE_URL_RE = /^[A-Za-z0-9:/_.\-]+$/;
|
|
1045
1100
|
var VERIFY_NETWORK_TIMEOUT_MS = 1e4;
|
|
1046
1101
|
var BODY_PREVIEW_MAX = 200;
|
|
1047
1102
|
|
|
@@ -1216,11 +1271,36 @@ function renderLaunchAgentPlist(input) {
|
|
|
1216
1271
|
`keyFilePath ${JSON.stringify(input.keyFilePath)} contains characters not allowed in a launchd path interpolation`
|
|
1217
1272
|
);
|
|
1218
1273
|
}
|
|
1274
|
+
const supabaseFields = [
|
|
1275
|
+
["postgresPassword", input.postgresPassword],
|
|
1276
|
+
["jwtSecret", input.jwtSecret],
|
|
1277
|
+
["supabaseAnonKey", input.supabaseAnonKey],
|
|
1278
|
+
["supabaseServiceRoleKey", input.supabaseServiceRoleKey],
|
|
1279
|
+
["dashboardPassword", input.dashboardPassword]
|
|
1280
|
+
];
|
|
1281
|
+
for (const [name, value] of supabaseFields) {
|
|
1282
|
+
if (value !== void 0 && !SAFE_LAUNCHCTL_VALUE_RE.test(value)) {
|
|
1283
|
+
throw new Error(
|
|
1284
|
+
`${String(name)} contains characters not allowed in a launchd setenv value`
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
if (input.supabaseUrl !== void 0 && !SAFE_URL_RE.test(input.supabaseUrl)) {
|
|
1289
|
+
throw new Error(
|
|
1290
|
+
`supabaseUrl ${JSON.stringify(input.supabaseUrl)} contains characters not allowed in a launchd setenv value`
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1219
1293
|
const setenvLines = [
|
|
1220
1294
|
`launchctl setenv PGSODIUM_ROOT_KEY "$(cat ${input.keyFilePath})"`,
|
|
1221
1295
|
...input.tunnelToken !== void 0 ? [`launchctl setenv TUNNEL_TOKEN "${input.tunnelToken}"`] : [],
|
|
1222
1296
|
...input.llmModel !== void 0 ? [`launchctl setenv LLM_MODEL "${input.llmModel}"`] : [],
|
|
1223
|
-
...input.embeddingModel !== void 0 ? [`launchctl setenv EMBEDDINGS_MODEL "${input.embeddingModel}"`] : []
|
|
1297
|
+
...input.embeddingModel !== void 0 ? [`launchctl setenv EMBEDDINGS_MODEL "${input.embeddingModel}"`] : [],
|
|
1298
|
+
...input.postgresPassword !== void 0 ? [`launchctl setenv POSTGRES_PASSWORD "${input.postgresPassword}"`] : [],
|
|
1299
|
+
...input.jwtSecret !== void 0 ? [`launchctl setenv JWT_SECRET "${input.jwtSecret}"`] : [],
|
|
1300
|
+
...input.supabaseAnonKey !== void 0 ? [`launchctl setenv SUPABASE_ANON_KEY "${input.supabaseAnonKey}"`] : [],
|
|
1301
|
+
...input.supabaseServiceRoleKey !== void 0 ? [`launchctl setenv SUPABASE_SERVICE_ROLE_KEY "${input.supabaseServiceRoleKey}"`] : [],
|
|
1302
|
+
...input.dashboardPassword !== void 0 ? [`launchctl setenv DASHBOARD_PASSWORD "${input.dashboardPassword}"`] : [],
|
|
1303
|
+
...input.supabaseUrl !== void 0 ? [`launchctl setenv SUPABASE_URL "${input.supabaseUrl}"`] : []
|
|
1224
1304
|
];
|
|
1225
1305
|
const command = setenvLines.join("; ");
|
|
1226
1306
|
return [
|
|
@@ -1277,7 +1357,13 @@ async function setupLaunchAgent(input) {
|
|
|
1277
1357
|
keyFilePath: paths.keyFile,
|
|
1278
1358
|
...input.tunnelToken !== void 0 ? { tunnelToken: input.tunnelToken } : {},
|
|
1279
1359
|
...input.llmModel !== void 0 ? { llmModel: input.llmModel } : {},
|
|
1280
|
-
...input.embeddingModel !== void 0 ? { embeddingModel: input.embeddingModel } : {}
|
|
1360
|
+
...input.embeddingModel !== void 0 ? { embeddingModel: input.embeddingModel } : {},
|
|
1361
|
+
...input.postgresPassword !== void 0 ? { postgresPassword: input.postgresPassword } : {},
|
|
1362
|
+
...input.jwtSecret !== void 0 ? { jwtSecret: input.jwtSecret } : {},
|
|
1363
|
+
...input.supabaseAnonKey !== void 0 ? { supabaseAnonKey: input.supabaseAnonKey } : {},
|
|
1364
|
+
...input.supabaseServiceRoleKey !== void 0 ? { supabaseServiceRoleKey: input.supabaseServiceRoleKey } : {},
|
|
1365
|
+
...input.dashboardPassword !== void 0 ? { dashboardPassword: input.dashboardPassword } : {},
|
|
1366
|
+
...input.supabaseUrl !== void 0 ? { supabaseUrl: input.supabaseUrl } : {}
|
|
1281
1367
|
});
|
|
1282
1368
|
} catch (err) {
|
|
1283
1369
|
return { ok: false, paths, step: "plist", reason: err.message };
|
|
@@ -1317,6 +1403,99 @@ async function teardownLaunchAgent(paths, opts = {}) {
|
|
|
1317
1403
|
return { ok: steps.every((s) => s.ok), steps };
|
|
1318
1404
|
}
|
|
1319
1405
|
|
|
1406
|
+
// src/lib/op-compose-script.ts
|
|
1407
|
+
var REGION_RE = /^[a-z0-9-]{2,32}$/;
|
|
1408
|
+
function renderOpComposeScript(input) {
|
|
1409
|
+
if (!REGION_RE.test(input.region)) {
|
|
1410
|
+
throw new Error(
|
|
1411
|
+
`region ${JSON.stringify(input.region)} contains characters not allowed in a launchd / Keychain service identifier`
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
return `#!/bin/bash
|
|
1415
|
+
# op-compose \u2014 wrapper that reads region secrets from macOS Keychain just
|
|
1416
|
+
# before invoking \`docker compose\`. Auto-generated by
|
|
1417
|
+
# \`create-op-node bootstrap --region ${input.region}\`. Do not edit by
|
|
1418
|
+
# hand; re-run bootstrap to regenerate.
|
|
1419
|
+
#
|
|
1420
|
+
# Why: launchctl setenv only propagates to launchd-spawned processes, so
|
|
1421
|
+
# SSH shells and pre-existing Terminal.app instances never see the env
|
|
1422
|
+
# vars bootstrap exports. This wrapper hydrates the docker compose
|
|
1423
|
+
# subprocess directly, so every invocation works regardless of how the
|
|
1424
|
+
# operator's shell was started.
|
|
1425
|
+
#
|
|
1426
|
+
# Usage:
|
|
1427
|
+
# ./bin/op-compose -f docker-compose-prod.yml up -d
|
|
1428
|
+
# ./bin/op-compose -f docker-compose-prod.yml pull
|
|
1429
|
+
# ./bin/op-compose -f docker-compose-prod.yml down
|
|
1430
|
+
|
|
1431
|
+
set -euo pipefail
|
|
1432
|
+
|
|
1433
|
+
SVC="org.opuspopuli.${input.region}"
|
|
1434
|
+
|
|
1435
|
+
require_secret() {
|
|
1436
|
+
local account="$1"
|
|
1437
|
+
local value
|
|
1438
|
+
if ! value=$(security find-generic-password -s "$SVC" -a "$account" -w 2>/dev/null); then
|
|
1439
|
+
echo "op-compose: missing Keychain entry '$account' under service '$SVC'." >&2
|
|
1440
|
+
echo "Run: create-op-node bootstrap --region ${input.region}" >&2
|
|
1441
|
+
exit 1
|
|
1442
|
+
fi
|
|
1443
|
+
printf '%s' "$value"
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
optional_secret() {
|
|
1447
|
+
local account="$1"
|
|
1448
|
+
security find-generic-password -s "$SVC" -a "$account" -w 2>/dev/null || true
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
# Bootstrap-critical: Postgres + Supabase admin credentials.
|
|
1452
|
+
export PGSODIUM_ROOT_KEY="$(require_secret pgsodium-root-key)"
|
|
1453
|
+
export POSTGRES_PASSWORD="$(require_secret postgres-password)"
|
|
1454
|
+
export JWT_SECRET="$(require_secret jwt-secret)"
|
|
1455
|
+
export SUPABASE_ANON_KEY="$(require_secret supabase-anon-key)"
|
|
1456
|
+
export SUPABASE_SERVICE_ROLE_KEY="$(require_secret supabase-service-role-key)"
|
|
1457
|
+
export DASHBOARD_PASSWORD="$(require_secret dashboard-password)"
|
|
1458
|
+
|
|
1459
|
+
# AUTH_JWT_SECRET historically equals JWT_SECRET in dev/UAT \u2014 supply that
|
|
1460
|
+
# default if the operator hasn't set one independently.
|
|
1461
|
+
export AUTH_JWT_SECRET="\${AUTH_JWT_SECRET:-$JWT_SECRET}"
|
|
1462
|
+
|
|
1463
|
+
# Optional: Cloudflare Tunnel token (only present in non-local-only mode).
|
|
1464
|
+
TUNNEL_TOKEN_VAL="$(optional_secret tunnel-token)"
|
|
1465
|
+
if [ -n "$TUNNEL_TOKEN_VAL" ]; then
|
|
1466
|
+
export TUNNEL_TOKEN="$TUNNEL_TOKEN_VAL"
|
|
1467
|
+
fi
|
|
1468
|
+
|
|
1469
|
+
# SUPABASE_URL is not a secret \u2014 read from env if the operator set one,
|
|
1470
|
+
# default to localhost for --local-only.
|
|
1471
|
+
export SUPABASE_URL="\${SUPABASE_URL:-http://localhost:8000}"
|
|
1472
|
+
|
|
1473
|
+
exec docker compose "$@"
|
|
1474
|
+
`;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// src/lib/op-compose-install.ts
|
|
1478
|
+
async function installOpComposeWrapper(input) {
|
|
1479
|
+
let content;
|
|
1480
|
+
try {
|
|
1481
|
+
content = renderOpComposeScript({ region: input.region });
|
|
1482
|
+
} catch (err) {
|
|
1483
|
+
return { ok: false, reason: err.message };
|
|
1484
|
+
}
|
|
1485
|
+
const binDir = join(input.repoDir, "bin");
|
|
1486
|
+
const target = join(binDir, "op-compose");
|
|
1487
|
+
const tmp = `${target}.tmp.${process.pid}`;
|
|
1488
|
+
try {
|
|
1489
|
+
await mkdir(binDir, { recursive: true });
|
|
1490
|
+
await writeFile(tmp, content, { mode: 493 });
|
|
1491
|
+
await chmod(tmp, 493);
|
|
1492
|
+
await rename(tmp, target);
|
|
1493
|
+
return { ok: true, path: target };
|
|
1494
|
+
} catch (err) {
|
|
1495
|
+
return { ok: false, reason: `writing ${target} failed: ${err.message}` };
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1320
1499
|
// src/lib/docker.ts
|
|
1321
1500
|
var GHCR_REGISTRY = "ghcr.io";
|
|
1322
1501
|
async function loginToGhcr() {
|
|
@@ -1363,16 +1542,19 @@ function composeArgs(opts, sub) {
|
|
|
1363
1542
|
const profiles = (opts.profiles ?? []).flatMap((pr) => ["--profile", pr]);
|
|
1364
1543
|
return ["compose", ...files, ...env, ...profiles, ...sub];
|
|
1365
1544
|
}
|
|
1545
|
+
function execOpts(opts) {
|
|
1546
|
+
return opts.env ? { cwd: opts.cwd, env: opts.env } : { cwd: opts.cwd };
|
|
1547
|
+
}
|
|
1366
1548
|
async function composePull(opts) {
|
|
1367
|
-
const res = await safeExeca("docker", composeArgs(opts, ["pull"]));
|
|
1549
|
+
const res = await safeExeca("docker", composeArgs(opts, ["pull"]), execOpts(opts));
|
|
1368
1550
|
return result(res, "compose pull");
|
|
1369
1551
|
}
|
|
1370
1552
|
async function composeUp(opts) {
|
|
1371
|
-
const res = await safeExeca("docker", composeArgs(opts, ["up", "-d", "--remove-orphans"]));
|
|
1553
|
+
const res = await safeExeca("docker", composeArgs(opts, ["up", "-d", "--remove-orphans"]), execOpts(opts));
|
|
1372
1554
|
return result(res, "compose up");
|
|
1373
1555
|
}
|
|
1374
1556
|
async function composeRemoveService(opts, service) {
|
|
1375
|
-
const res = await safeExeca("docker", composeArgs(opts, ["rm", "-sfv", service]));
|
|
1557
|
+
const res = await safeExeca("docker", composeArgs(opts, ["rm", "-sfv", service]), execOpts(opts));
|
|
1376
1558
|
return result(res, `compose rm ${service}`);
|
|
1377
1559
|
}
|
|
1378
1560
|
async function composeDown(opts) {
|
|
@@ -1385,7 +1567,7 @@ async function composeDown(opts) {
|
|
|
1385
1567
|
opts.wipeVolumes ? "-v" : "",
|
|
1386
1568
|
opts.removeImages ? `--rmi ${opts.removeImages}` : ""
|
|
1387
1569
|
].filter(Boolean).join(" ");
|
|
1388
|
-
const res = await safeExeca("docker", composeArgs(opts, flags));
|
|
1570
|
+
const res = await safeExeca("docker", composeArgs(opts, flags), execOpts(opts));
|
|
1389
1571
|
return result(res, label);
|
|
1390
1572
|
}
|
|
1391
1573
|
async function dockerLogout(registry = GHCR_REGISTRY) {
|
|
@@ -1431,7 +1613,7 @@ function normalize(raw) {
|
|
|
1431
1613
|
return { name, state, health, exitCode };
|
|
1432
1614
|
}
|
|
1433
1615
|
async function composePs(opts) {
|
|
1434
|
-
const res = await safeExeca("docker", composeArgs(opts, ["ps", "--format", "json"]));
|
|
1616
|
+
const res = await safeExeca("docker", composeArgs(opts, ["ps", "--format", "json"]), execOpts(opts));
|
|
1435
1617
|
if (res === null || res.exitCode !== 0) return null;
|
|
1436
1618
|
return parseComposePs(res.stdout);
|
|
1437
1619
|
}
|
|
@@ -1685,6 +1867,11 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
1685
1867
|
"--embedding-model <model>",
|
|
1686
1868
|
`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).`
|
|
1687
1869
|
)
|
|
1870
|
+
).addOption(
|
|
1871
|
+
new Option(
|
|
1872
|
+
"--supabase-url <url>",
|
|
1873
|
+
"Public-facing Supabase URL (what browsers + microservices use). Default: `http://localhost:8000` in --local-only mode, otherwise `https://supabase.<domain>` (which the operator can override)."
|
|
1874
|
+
)
|
|
1688
1875
|
).addOption(new Option("--skip-stack", "Stop before `docker compose pull && up`").default(false)).addOption(
|
|
1689
1876
|
new Option(
|
|
1690
1877
|
"--local-only",
|
|
@@ -1838,6 +2025,65 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
1838
2025
|
placeholder: "JWT-style base64url string from `terraform output tunnel_token`",
|
|
1839
2026
|
validate: (v) => TUNNEL_TOKEN_RE.test(v) ? void 0 : "tunnel token must be a base64-url JWT"
|
|
1840
2027
|
});
|
|
2028
|
+
const postgresPassword = await loadSecret({
|
|
2029
|
+
region,
|
|
2030
|
+
account: "postgres-password",
|
|
2031
|
+
label: "Postgres password",
|
|
2032
|
+
// URL-safe alphabet required — value lands in postgres:// URIs where
|
|
2033
|
+
// `+`/`/`/`=` cause parser ambiguity in gotrue/postgrest/storage.
|
|
2034
|
+
validate: (v) => URL_SAFE_PASSWORD_RE.test(v) ? void 0 : "must be base64url chars only (no + / =)",
|
|
2035
|
+
generate: generatePostgresPassword
|
|
2036
|
+
});
|
|
2037
|
+
const jwtSecret = await loadSecret({
|
|
2038
|
+
region,
|
|
2039
|
+
account: "jwt-secret",
|
|
2040
|
+
label: "JWT signing secret",
|
|
2041
|
+
validate: (v) => SAFE_LAUNCHCTL_VALUE_RE.test(v) && v.length >= 32 ? void 0 : "must be \u226532 base64 characters",
|
|
2042
|
+
generate: generateJwtSecret
|
|
2043
|
+
});
|
|
2044
|
+
const supabaseAnonKey = await loadSecret({
|
|
2045
|
+
region,
|
|
2046
|
+
account: "supabase-anon-key",
|
|
2047
|
+
label: "Supabase anon key",
|
|
2048
|
+
validate: (v) => SAFE_LAUNCHCTL_VALUE_RE.test(v) && verifySupabaseJwt(v, jwtSecret) ? void 0 : "must be a base64url JWT signed by the current JWT_SECRET",
|
|
2049
|
+
generate: () => signSupabaseJwt({ role: "anon", secret: jwtSecret })
|
|
2050
|
+
});
|
|
2051
|
+
const supabaseServiceRoleKey = await loadSecret({
|
|
2052
|
+
region,
|
|
2053
|
+
account: "supabase-service-role-key",
|
|
2054
|
+
label: "Supabase service role key",
|
|
2055
|
+
validate: (v) => SAFE_LAUNCHCTL_VALUE_RE.test(v) && verifySupabaseJwt(v, jwtSecret) ? void 0 : "must be a base64url JWT signed by the current JWT_SECRET",
|
|
2056
|
+
generate: () => signSupabaseJwt({ role: "service_role", secret: jwtSecret })
|
|
2057
|
+
});
|
|
2058
|
+
const dashboardPassword = await loadSecret({
|
|
2059
|
+
region,
|
|
2060
|
+
account: "dashboard-password",
|
|
2061
|
+
label: "Supabase Studio dashboard password",
|
|
2062
|
+
// URL-safe alphabet required — value lands in basic-auth headers via
|
|
2063
|
+
// kong's declarative config. Avoid `+`/`/`/`=` for the same reasons.
|
|
2064
|
+
validate: (v) => URL_SAFE_PASSWORD_RE.test(v) ? void 0 : "must be base64url chars only (no + / =)",
|
|
2065
|
+
generate: generateDashboardPassword
|
|
2066
|
+
});
|
|
2067
|
+
let supabaseUrl;
|
|
2068
|
+
if (opts.supabaseUrl) {
|
|
2069
|
+
supabaseUrl = opts.supabaseUrl;
|
|
2070
|
+
} else if (opts.localOnly) {
|
|
2071
|
+
supabaseUrl = "http://localhost:8000";
|
|
2072
|
+
} else {
|
|
2073
|
+
supabaseUrl = unwrap(
|
|
2074
|
+
await p3.text({
|
|
2075
|
+
message: "Public-facing Supabase URL (what browsers + microservices use to reach kong)?",
|
|
2076
|
+
placeholder: "https://supabase.civicfeed.tx",
|
|
2077
|
+
validate: (v) => !v ? "required" : SAFE_URL_RE.test(v) ? void 0 : "contains characters outside the allowed URL set"
|
|
2078
|
+
})
|
|
2079
|
+
);
|
|
2080
|
+
}
|
|
2081
|
+
if (!SAFE_URL_RE.test(supabaseUrl)) {
|
|
2082
|
+
p3.cancel(
|
|
2083
|
+
`--supabase-url ${JSON.stringify(supabaseUrl)} contains characters outside the allowed URL set`
|
|
2084
|
+
);
|
|
2085
|
+
process.exit(1);
|
|
2086
|
+
}
|
|
1841
2087
|
if (!opts.skipLaunchAgent) {
|
|
1842
2088
|
const laSpin = p3.spinner();
|
|
1843
2089
|
laSpin.start("Writing pgsodium key file + LaunchAgent plist\u2026");
|
|
@@ -1845,7 +2091,13 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
1845
2091
|
pgsodiumKey,
|
|
1846
2092
|
...tunnelToken !== void 0 ? { tunnelToken } : {},
|
|
1847
2093
|
llmModel,
|
|
1848
|
-
embeddingModel
|
|
2094
|
+
embeddingModel,
|
|
2095
|
+
postgresPassword,
|
|
2096
|
+
jwtSecret,
|
|
2097
|
+
supabaseAnonKey,
|
|
2098
|
+
supabaseServiceRoleKey,
|
|
2099
|
+
dashboardPassword,
|
|
2100
|
+
supabaseUrl
|
|
1849
2101
|
});
|
|
1850
2102
|
if (!la.ok) {
|
|
1851
2103
|
laSpin.stop(pc2.red(`\u2717 LaunchAgent step ${la.step} failed.`));
|
|
@@ -1858,6 +2110,15 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
1858
2110
|
)
|
|
1859
2111
|
);
|
|
1860
2112
|
}
|
|
2113
|
+
const wrapperSpin = p3.spinner();
|
|
2114
|
+
wrapperSpin.start("Installing bin/op-compose wrapper\u2026");
|
|
2115
|
+
const wrapper = await installOpComposeWrapper({ repoDir: repoPath, region });
|
|
2116
|
+
if (!wrapper.ok) {
|
|
2117
|
+
wrapperSpin.stop(pc2.red("\u2717 op-compose wrapper install failed."));
|
|
2118
|
+
p3.cancel(wrapper.reason ?? "op-compose wrapper install failed.");
|
|
2119
|
+
process.exit(1);
|
|
2120
|
+
}
|
|
2121
|
+
wrapperSpin.stop(pc2.green(`\u2713 Installed ${wrapper.path} (mode 0755).`));
|
|
1861
2122
|
const ghcrSpin = p3.spinner();
|
|
1862
2123
|
ghcrSpin.start("Authenticating Docker to ghcr.io\u2026");
|
|
1863
2124
|
const ghcr = await loginToGhcr();
|
|
@@ -1910,17 +2171,31 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
1910
2171
|
const profileFlag = opts.localOnly ? "" : "--profile public ";
|
|
1911
2172
|
p3.outro(
|
|
1912
2173
|
pc2.cyan(
|
|
1913
|
-
`Stack-up skipped. Run
|
|
2174
|
+
`Stack-up skipped. Run \`./bin/op-compose -f ${composeFile.join(" -f ")} ${profileFlag}pull && ./bin/op-compose -f ${composeFile.join(" -f ")} ${profileFlag}up -d\` from ${repoPath} when ready. (op-compose hydrates Keychain secrets per-invocation; use it instead of raw \`docker compose\`.)`
|
|
1914
2175
|
)
|
|
1915
2176
|
);
|
|
1916
2177
|
return;
|
|
1917
2178
|
}
|
|
1918
2179
|
const composeFiles = resolveComposeFiles(repoPath, composeFile);
|
|
2180
|
+
const composeEnv = {
|
|
2181
|
+
PGSODIUM_ROOT_KEY: pgsodiumKey,
|
|
2182
|
+
POSTGRES_PASSWORD: postgresPassword,
|
|
2183
|
+
JWT_SECRET: jwtSecret,
|
|
2184
|
+
SUPABASE_ANON_KEY: supabaseAnonKey,
|
|
2185
|
+
SUPABASE_SERVICE_ROLE_KEY: supabaseServiceRoleKey,
|
|
2186
|
+
DASHBOARD_PASSWORD: dashboardPassword,
|
|
2187
|
+
SUPABASE_URL: supabaseUrl,
|
|
2188
|
+
AUTH_JWT_SECRET: process.env["AUTH_JWT_SECRET"] ?? jwtSecret,
|
|
2189
|
+
...tunnelToken !== void 0 ? { TUNNEL_TOKEN: tunnelToken } : {},
|
|
2190
|
+
...llmModel ? { LLM_MODEL: llmModel } : {},
|
|
2191
|
+
...embeddingModel ? { EMBEDDINGS_MODEL: embeddingModel } : {}
|
|
2192
|
+
};
|
|
1919
2193
|
const composeOpts = {
|
|
1920
2194
|
files: composeFiles,
|
|
1921
2195
|
cwd: repoPath,
|
|
1922
2196
|
...opts.envFile ? { envFile: opts.envFile } : {},
|
|
1923
|
-
profiles: opts.localOnly ? LOCAL_PROFILES : PUBLIC_PROFILES
|
|
2197
|
+
profiles: opts.localOnly ? LOCAL_PROFILES : PUBLIC_PROFILES,
|
|
2198
|
+
env: composeEnv
|
|
1924
2199
|
};
|
|
1925
2200
|
if (opts.localOnly) {
|
|
1926
2201
|
const evict = p3.spinner();
|
|
@@ -2216,7 +2491,7 @@ async function loadSecret(input) {
|
|
|
2216
2491
|
}
|
|
2217
2492
|
return pasted;
|
|
2218
2493
|
}
|
|
2219
|
-
var
|
|
2494
|
+
var REGION_RE2 = /^[a-z0-9-]{2,32}$/;
|
|
2220
2495
|
var RESET_PHASES = {
|
|
2221
2496
|
STOP_STACK: "Stop stack",
|
|
2222
2497
|
LAUNCH_AGENT: "LaunchAgent",
|
|
@@ -2373,10 +2648,10 @@ var resetCommand = new Command("reset").description(
|
|
|
2373
2648
|
await p3.text({
|
|
2374
2649
|
message: "Region label (the slug used during init \u2014 e.g. us-ca)?",
|
|
2375
2650
|
placeholder: "us-ca",
|
|
2376
|
-
validate: (v) =>
|
|
2651
|
+
validate: (v) => REGION_RE2.test(v ?? "") ? void 0 : "lowercase letters, digits, hyphens; 2\u201332 chars"
|
|
2377
2652
|
})
|
|
2378
2653
|
);
|
|
2379
|
-
if (!
|
|
2654
|
+
if (!REGION_RE2.test(region)) {
|
|
2380
2655
|
p3.cancel(`--region ${JSON.stringify(region)} is not a valid region slug.`);
|
|
2381
2656
|
process.exit(2);
|
|
2382
2657
|
}
|
|
@@ -2421,6 +2696,7 @@ var resetCommand = new Command("reset").description(
|
|
|
2421
2696
|
if (repoPath) {
|
|
2422
2697
|
runningContainers = await composePs({
|
|
2423
2698
|
files: resolveComposeFiles(repoPath, opts.composeFile),
|
|
2699
|
+
cwd: repoPath,
|
|
2424
2700
|
...opts.envFile ? { envFile: opts.envFile } : {}
|
|
2425
2701
|
});
|
|
2426
2702
|
}
|
|
@@ -3936,7 +4212,7 @@ async function looksLikeRegionsRepo(dir) {
|
|
|
3936
4212
|
}
|
|
3937
4213
|
|
|
3938
4214
|
// src/cli.ts
|
|
3939
|
-
var VERSION = "0.
|
|
4215
|
+
var VERSION = "0.9.1";
|
|
3940
4216
|
var program = new Command();
|
|
3941
4217
|
program.name("create-op-node").description(
|
|
3942
4218
|
"Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."
|