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 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, rm, chmod } from 'fs/promises';
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 \`docker compose -f ${composeFile.join(" -f ")} ${profileFlag}pull && docker compose -f ${composeFile.join(" -f ")} ${profileFlag}up -d\` from ${repoPath} when ready.`
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 REGION_RE = /^[a-z0-9-]{2,32}$/;
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) => REGION_RE.test(v ?? "") ? void 0 : "lowercase letters, digits, hyphens; 2\u201332 chars"
2651
+ validate: (v) => REGION_RE2.test(v ?? "") ? void 0 : "lowercase letters, digits, hyphens; 2\u201332 chars"
2533
2652
  })
2534
2653
  );
2535
- if (!REGION_RE.test(region)) {
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.0";
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."