create-op-node 0.9.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/dist/cli.js +132 -12
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { execa } from 'execa';
|
|
|
6
6
|
import { Octokit } from '@octokit/rest';
|
|
7
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';
|
|
@@ -1403,6 +1403,99 @@ async function teardownLaunchAgent(paths, opts = {}) {
|
|
|
1403
1403
|
return { ok: steps.every((s) => s.ok), steps };
|
|
1404
1404
|
}
|
|
1405
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
|
+
|
|
1406
1499
|
// src/lib/docker.ts
|
|
1407
1500
|
var GHCR_REGISTRY = "ghcr.io";
|
|
1408
1501
|
async function loginToGhcr() {
|
|
@@ -1449,16 +1542,19 @@ function composeArgs(opts, sub) {
|
|
|
1449
1542
|
const profiles = (opts.profiles ?? []).flatMap((pr) => ["--profile", pr]);
|
|
1450
1543
|
return ["compose", ...files, ...env, ...profiles, ...sub];
|
|
1451
1544
|
}
|
|
1545
|
+
function execOpts(opts) {
|
|
1546
|
+
return opts.env ? { cwd: opts.cwd, env: opts.env } : { cwd: opts.cwd };
|
|
1547
|
+
}
|
|
1452
1548
|
async function composePull(opts) {
|
|
1453
|
-
const res = await safeExeca("docker", composeArgs(opts, ["pull"]));
|
|
1549
|
+
const res = await safeExeca("docker", composeArgs(opts, ["pull"]), execOpts(opts));
|
|
1454
1550
|
return result(res, "compose pull");
|
|
1455
1551
|
}
|
|
1456
1552
|
async function composeUp(opts) {
|
|
1457
|
-
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));
|
|
1458
1554
|
return result(res, "compose up");
|
|
1459
1555
|
}
|
|
1460
1556
|
async function composeRemoveService(opts, service) {
|
|
1461
|
-
const res = await safeExeca("docker", composeArgs(opts, ["rm", "-sfv", service]));
|
|
1557
|
+
const res = await safeExeca("docker", composeArgs(opts, ["rm", "-sfv", service]), execOpts(opts));
|
|
1462
1558
|
return result(res, `compose rm ${service}`);
|
|
1463
1559
|
}
|
|
1464
1560
|
async function composeDown(opts) {
|
|
@@ -1471,7 +1567,7 @@ async function composeDown(opts) {
|
|
|
1471
1567
|
opts.wipeVolumes ? "-v" : "",
|
|
1472
1568
|
opts.removeImages ? `--rmi ${opts.removeImages}` : ""
|
|
1473
1569
|
].filter(Boolean).join(" ");
|
|
1474
|
-
const res = await safeExeca("docker", composeArgs(opts, flags));
|
|
1570
|
+
const res = await safeExeca("docker", composeArgs(opts, flags), execOpts(opts));
|
|
1475
1571
|
return result(res, label);
|
|
1476
1572
|
}
|
|
1477
1573
|
async function dockerLogout(registry = GHCR_REGISTRY) {
|
|
@@ -1517,7 +1613,7 @@ function normalize(raw) {
|
|
|
1517
1613
|
return { name, state, health, exitCode };
|
|
1518
1614
|
}
|
|
1519
1615
|
async function composePs(opts) {
|
|
1520
|
-
const res = await safeExeca("docker", composeArgs(opts, ["ps", "--format", "json"]));
|
|
1616
|
+
const res = await safeExeca("docker", composeArgs(opts, ["ps", "--format", "json"]), execOpts(opts));
|
|
1521
1617
|
if (res === null || res.exitCode !== 0) return null;
|
|
1522
1618
|
return parseComposePs(res.stdout);
|
|
1523
1619
|
}
|
|
@@ -2014,6 +2110,15 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2014
2110
|
)
|
|
2015
2111
|
);
|
|
2016
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).`));
|
|
2017
2122
|
const ghcrSpin = p3.spinner();
|
|
2018
2123
|
ghcrSpin.start("Authenticating Docker to ghcr.io\u2026");
|
|
2019
2124
|
const ghcr = await loginToGhcr();
|
|
@@ -2066,17 +2171,31 @@ var bootstrapCommand = new Command("bootstrap").description(
|
|
|
2066
2171
|
const profileFlag = opts.localOnly ? "" : "--profile public ";
|
|
2067
2172
|
p3.outro(
|
|
2068
2173
|
pc2.cyan(
|
|
2069
|
-
`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\`.)`
|
|
2070
2175
|
)
|
|
2071
2176
|
);
|
|
2072
2177
|
return;
|
|
2073
2178
|
}
|
|
2074
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
|
+
};
|
|
2075
2193
|
const composeOpts = {
|
|
2076
2194
|
files: composeFiles,
|
|
2077
2195
|
cwd: repoPath,
|
|
2078
2196
|
...opts.envFile ? { envFile: opts.envFile } : {},
|
|
2079
|
-
profiles: opts.localOnly ? LOCAL_PROFILES : PUBLIC_PROFILES
|
|
2197
|
+
profiles: opts.localOnly ? LOCAL_PROFILES : PUBLIC_PROFILES,
|
|
2198
|
+
env: composeEnv
|
|
2080
2199
|
};
|
|
2081
2200
|
if (opts.localOnly) {
|
|
2082
2201
|
const evict = p3.spinner();
|
|
@@ -2372,7 +2491,7 @@ async function loadSecret(input) {
|
|
|
2372
2491
|
}
|
|
2373
2492
|
return pasted;
|
|
2374
2493
|
}
|
|
2375
|
-
var
|
|
2494
|
+
var REGION_RE2 = /^[a-z0-9-]{2,32}$/;
|
|
2376
2495
|
var RESET_PHASES = {
|
|
2377
2496
|
STOP_STACK: "Stop stack",
|
|
2378
2497
|
LAUNCH_AGENT: "LaunchAgent",
|
|
@@ -2529,10 +2648,10 @@ var resetCommand = new Command("reset").description(
|
|
|
2529
2648
|
await p3.text({
|
|
2530
2649
|
message: "Region label (the slug used during init \u2014 e.g. us-ca)?",
|
|
2531
2650
|
placeholder: "us-ca",
|
|
2532
|
-
validate: (v) =>
|
|
2651
|
+
validate: (v) => REGION_RE2.test(v ?? "") ? void 0 : "lowercase letters, digits, hyphens; 2\u201332 chars"
|
|
2533
2652
|
})
|
|
2534
2653
|
);
|
|
2535
|
-
if (!
|
|
2654
|
+
if (!REGION_RE2.test(region)) {
|
|
2536
2655
|
p3.cancel(`--region ${JSON.stringify(region)} is not a valid region slug.`);
|
|
2537
2656
|
process.exit(2);
|
|
2538
2657
|
}
|
|
@@ -2577,6 +2696,7 @@ var resetCommand = new Command("reset").description(
|
|
|
2577
2696
|
if (repoPath) {
|
|
2578
2697
|
runningContainers = await composePs({
|
|
2579
2698
|
files: resolveComposeFiles(repoPath, opts.composeFile),
|
|
2699
|
+
cwd: repoPath,
|
|
2580
2700
|
...opts.envFile ? { envFile: opts.envFile } : {}
|
|
2581
2701
|
});
|
|
2582
2702
|
}
|
|
@@ -4092,7 +4212,7 @@ async function looksLikeRegionsRepo(dir) {
|
|
|
4092
4212
|
}
|
|
4093
4213
|
|
|
4094
4214
|
// src/cli.ts
|
|
4095
|
-
var VERSION = "0.9.
|
|
4215
|
+
var VERSION = "0.9.1";
|
|
4096
4216
|
var program = new Command();
|
|
4097
4217
|
program.name("create-op-node").description(
|
|
4098
4218
|
"Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."
|