create-op-node 0.10.8 → 0.10.10

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
@@ -577,22 +577,26 @@ var realDeps = {
577
577
  async function waitForApply(input, budgets = DEFAULT_BUDGETS, deps = realDeps) {
578
578
  let runId = input.runId;
579
579
  if (!runId) {
580
- deps.onProgress?.("discovery");
581
- const discoveryStart = deps.now();
582
- while (deps.now() - discoveryStart < budgets.discoveryMs) {
583
- await deps.sleep(budgets.pollMs);
584
- const ws = await deps.findWorkspace({
585
- token: input.token,
586
- organization: input.organization,
587
- tags: input.workspaceTags
588
- });
589
- if (ws?.currentRunId) {
590
- runId = ws.currentRunId;
591
- break;
592
- }
593
- }
580
+ runId = await discoverRunId(input, budgets, deps);
594
581
  if (!runId) return { kind: "no-run-started" };
595
582
  }
583
+ return waitForRunOutput(input, budgets, deps, runId);
584
+ }
585
+ async function discoverRunId(input, budgets, deps) {
586
+ deps.onProgress?.("discovery");
587
+ const discoveryStart = deps.now();
588
+ while (deps.now() - discoveryStart < budgets.discoveryMs) {
589
+ await deps.sleep(budgets.pollMs);
590
+ const ws = await deps.findWorkspace({
591
+ token: input.token,
592
+ organization: input.organization,
593
+ tags: input.workspaceTags
594
+ });
595
+ if (ws?.currentRunId) return ws.currentRunId;
596
+ }
597
+ return null;
598
+ }
599
+ async function waitForRunOutput(input, budgets, deps, runId) {
596
600
  deps.onProgress?.("run");
597
601
  const runStart = deps.now();
598
602
  while (deps.now() - runStart < budgets.runMs) {
@@ -1351,6 +1355,27 @@ async function writePgsodiumKeyFile(key, keyFile) {
1351
1355
  }
1352
1356
  var MODEL_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;
1353
1357
  function renderLaunchAgentPlist(input) {
1358
+ validatePlistInput(input);
1359
+ const command = buildSetenvCommand(input);
1360
+ return [
1361
+ '<?xml version="1.0" encoding="UTF-8"?>',
1362
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
1363
+ '<plist version="1.0">',
1364
+ "<dict>",
1365
+ ` <key>Label</key><string>${LAUNCH_AGENT_LABEL}</string>`,
1366
+ " <key>ProgramArguments</key>",
1367
+ " <array>",
1368
+ " <string>/bin/sh</string>",
1369
+ " <string>-c</string>",
1370
+ ` <string>${command}</string>`,
1371
+ " </array>",
1372
+ " <key>RunAtLoad</key><true/>",
1373
+ "</dict>",
1374
+ "</plist>",
1375
+ ""
1376
+ ].join("\n");
1377
+ }
1378
+ function validatePlistInput(input) {
1354
1379
  if (input.tunnelToken !== void 0 && !TUNNEL_TOKEN_RE.test(input.tunnelToken)) {
1355
1380
  throw new Error("Tunnel token contains characters outside the expected base64-url set");
1356
1381
  }
@@ -1378,9 +1403,7 @@ function renderLaunchAgentPlist(input) {
1378
1403
  ];
1379
1404
  for (const [name, value] of supabaseFields) {
1380
1405
  if (value !== void 0 && !SAFE_LAUNCHCTL_VALUE_RE.test(value)) {
1381
- throw new Error(
1382
- `${String(name)} contains characters not allowed in a launchd setenv value`
1383
- );
1406
+ throw new Error(`${String(name)} contains characters not allowed in a launchd setenv value`);
1384
1407
  }
1385
1408
  }
1386
1409
  if (input.supabaseUrl !== void 0 && !SAFE_URL_RE.test(input.supabaseUrl)) {
@@ -1388,7 +1411,9 @@ function renderLaunchAgentPlist(input) {
1388
1411
  `supabaseUrl ${JSON.stringify(input.supabaseUrl)} contains characters not allowed in a launchd setenv value`
1389
1412
  );
1390
1413
  }
1391
- const setenvLines = [
1414
+ }
1415
+ function buildSetenvCommand(input) {
1416
+ return [
1392
1417
  `launchctl setenv PGSODIUM_ROOT_KEY "$(cat ${input.keyFilePath})"`,
1393
1418
  ...input.tunnelToken !== void 0 ? [`launchctl setenv TUNNEL_TOKEN "${input.tunnelToken}"`] : [],
1394
1419
  ...input.llmModel !== void 0 ? [`launchctl setenv LLM_MODEL "${input.llmModel}"`] : [],
@@ -1399,25 +1424,7 @@ function renderLaunchAgentPlist(input) {
1399
1424
  ...input.supabaseServiceRoleKey !== void 0 ? [`launchctl setenv SUPABASE_SERVICE_ROLE_KEY "${input.supabaseServiceRoleKey}"`] : [],
1400
1425
  ...input.dashboardPassword !== void 0 ? [`launchctl setenv DASHBOARD_PASSWORD "${input.dashboardPassword}"`] : [],
1401
1426
  ...input.supabaseUrl !== void 0 ? [`launchctl setenv SUPABASE_URL "${input.supabaseUrl}"`] : []
1402
- ];
1403
- const command = setenvLines.join("; ");
1404
- return [
1405
- '<?xml version="1.0" encoding="UTF-8"?>',
1406
- '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
1407
- '<plist version="1.0">',
1408
- "<dict>",
1409
- ` <key>Label</key><string>${LAUNCH_AGENT_LABEL}</string>`,
1410
- " <key>ProgramArguments</key>",
1411
- " <array>",
1412
- " <string>/bin/sh</string>",
1413
- " <string>-c</string>",
1414
- ` <string>${command}</string>`,
1415
- " </array>",
1416
- " <key>RunAtLoad</key><true/>",
1417
- "</dict>",
1418
- "</plist>",
1419
- ""
1420
- ].join("\n");
1427
+ ].join("; ");
1421
1428
  }
1422
1429
  async function writeLaunchAgentPlist(plistFile, content) {
1423
1430
  try {
@@ -1443,6 +1450,20 @@ async function loadLaunchAgent(plistFile) {
1443
1450
  }
1444
1451
  return { ok: true };
1445
1452
  }
1453
+ function plistInputFromSetup(input, keyFilePath) {
1454
+ return {
1455
+ keyFilePath,
1456
+ ...input.tunnelToken !== void 0 ? { tunnelToken: input.tunnelToken } : {},
1457
+ ...input.llmModel !== void 0 ? { llmModel: input.llmModel } : {},
1458
+ ...input.embeddingModel !== void 0 ? { embeddingModel: input.embeddingModel } : {},
1459
+ ...input.postgresPassword !== void 0 ? { postgresPassword: input.postgresPassword } : {},
1460
+ ...input.jwtSecret !== void 0 ? { jwtSecret: input.jwtSecret } : {},
1461
+ ...input.supabaseAnonKey !== void 0 ? { supabaseAnonKey: input.supabaseAnonKey } : {},
1462
+ ...input.supabaseServiceRoleKey !== void 0 ? { supabaseServiceRoleKey: input.supabaseServiceRoleKey } : {},
1463
+ ...input.dashboardPassword !== void 0 ? { dashboardPassword: input.dashboardPassword } : {},
1464
+ ...input.supabaseUrl !== void 0 ? { supabaseUrl: input.supabaseUrl } : {}
1465
+ };
1466
+ }
1446
1467
  async function setupLaunchAgent(input) {
1447
1468
  const paths = input.paths ?? defaultPaths();
1448
1469
  const keyResult = await writePgsodiumKeyFile(input.pgsodiumKey, paths.keyFile);
@@ -1451,18 +1472,7 @@ async function setupLaunchAgent(input) {
1451
1472
  }
1452
1473
  let plistContent;
1453
1474
  try {
1454
- plistContent = renderLaunchAgentPlist({
1455
- keyFilePath: paths.keyFile,
1456
- ...input.tunnelToken !== void 0 ? { tunnelToken: input.tunnelToken } : {},
1457
- ...input.llmModel !== void 0 ? { llmModel: input.llmModel } : {},
1458
- ...input.embeddingModel !== void 0 ? { embeddingModel: input.embeddingModel } : {},
1459
- ...input.postgresPassword !== void 0 ? { postgresPassword: input.postgresPassword } : {},
1460
- ...input.jwtSecret !== void 0 ? { jwtSecret: input.jwtSecret } : {},
1461
- ...input.supabaseAnonKey !== void 0 ? { supabaseAnonKey: input.supabaseAnonKey } : {},
1462
- ...input.supabaseServiceRoleKey !== void 0 ? { supabaseServiceRoleKey: input.supabaseServiceRoleKey } : {},
1463
- ...input.dashboardPassword !== void 0 ? { dashboardPassword: input.dashboardPassword } : {},
1464
- ...input.supabaseUrl !== void 0 ? { supabaseUrl: input.supabaseUrl } : {}
1465
- });
1475
+ plistContent = renderLaunchAgentPlist(plistInputFromSetup(input, paths.keyFile));
1466
1476
  } catch (err) {
1467
1477
  return { ok: false, paths, step: "plist", reason: err.message };
1468
1478
  }
@@ -1812,6 +1822,15 @@ var realWaitDeps = {
1812
1822
  sleep: (ms) => new Promise((res) => setTimeout(res, ms)),
1813
1823
  now: () => Date.now()
1814
1824
  };
1825
+ function terminalOutcome(ps, requireHealthy) {
1826
+ if (ps.length === 0) return null;
1827
+ const verdict = assessHealth(ps, requireHealthy);
1828
+ if (verdict.kind === "healthy") return { kind: "healthy", snapshots: ps };
1829
+ if (verdict.kind === "unhealthy") {
1830
+ return { kind: "unhealthy", snapshots: ps, problem: verdict.problem ?? "unhealthy" };
1831
+ }
1832
+ return null;
1833
+ }
1815
1834
  async function waitForHealthy(composeOpts, options = {}, deps = realWaitDeps) {
1816
1835
  const timeoutMs = options.timeoutMs ?? 5 * 60 * 1e3;
1817
1836
  const basePollMs = options.pollMs ?? 5 * 1e3;
@@ -1824,17 +1843,8 @@ async function waitForHealthy(composeOpts, options = {}, deps = realWaitDeps) {
1824
1843
  if (ps !== null) {
1825
1844
  last = ps;
1826
1845
  options.onPoll?.(ps);
1827
- if (ps.length > 0) {
1828
- const verdict = assessHealth(ps, options.requireHealthy);
1829
- if (verdict.kind === "healthy") return { kind: "healthy", snapshots: ps };
1830
- if (verdict.kind === "unhealthy") {
1831
- return {
1832
- kind: "unhealthy",
1833
- snapshots: ps,
1834
- problem: verdict.problem ?? "unhealthy"
1835
- };
1836
- }
1837
- }
1846
+ const outcome = terminalOutcome(ps, options.requireHealthy);
1847
+ if (outcome) return outcome;
1838
1848
  }
1839
1849
  const interval = pollCount < 3 ? basePollMs : basePollMs * 2;
1840
1850
  await deps.sleep(interval);
@@ -1842,6 +1852,12 @@ async function waitForHealthy(composeOpts, options = {}, deps = realWaitDeps) {
1842
1852
  return { kind: "timeout", snapshots: last };
1843
1853
  }
1844
1854
  function assessHealth(snapshots, requireHealthy) {
1855
+ const hard = firstHardFailure(snapshots);
1856
+ if (hard) return hard;
1857
+ const required = requireHealthy ?? [];
1858
+ return required.length > 0 ? assessRequired(snapshots, required) : assessAllRunning(snapshots);
1859
+ }
1860
+ function firstHardFailure(snapshots) {
1845
1861
  for (const s of snapshots) {
1846
1862
  if (s.health === "unhealthy") {
1847
1863
  return { kind: "unhealthy", problem: `${s.name} reports unhealthy` };
@@ -1850,21 +1866,23 @@ function assessHealth(snapshots, requireHealthy) {
1850
1866
  return { kind: "unhealthy", problem: `${s.name} exited with code ${s.exitCode}` };
1851
1867
  }
1852
1868
  }
1853
- const required = requireHealthy ?? [];
1854
- if (required.length > 0) {
1855
- const byName = new Map(snapshots.map((s) => [s.name, s]));
1856
- for (const name of required) {
1857
- const s = byName.get(name);
1858
- if (!s) return { kind: "pending" };
1859
- if (s.state === "exited" && s.exitCode === 0) continue;
1860
- if (s.state !== "running") return { kind: "pending" };
1861
- if (s.health === "starting") return { kind: "pending" };
1862
- if (s.health === "unhealthy") {
1863
- return { kind: "unhealthy", problem: `${s.name} reports unhealthy` };
1864
- }
1869
+ return null;
1870
+ }
1871
+ function assessRequired(snapshots, required) {
1872
+ const byName = new Map(snapshots.map((s) => [s.name, s]));
1873
+ for (const name of required) {
1874
+ const s = byName.get(name);
1875
+ if (!s) return { kind: "pending" };
1876
+ if (s.state === "exited" && s.exitCode === 0) continue;
1877
+ if (s.state !== "running") return { kind: "pending" };
1878
+ if (s.health === "starting") return { kind: "pending" };
1879
+ if (s.health === "unhealthy") {
1880
+ return { kind: "unhealthy", problem: `${s.name} reports unhealthy` };
1865
1881
  }
1866
- return { kind: "healthy" };
1867
1882
  }
1883
+ return { kind: "healthy" };
1884
+ }
1885
+ function assessAllRunning(snapshots) {
1868
1886
  for (const s of snapshots) {
1869
1887
  if (s.state === "exited" && s.exitCode === 0) continue;
1870
1888
  if (s.state !== "running") return { kind: "pending" };
@@ -3428,36 +3446,47 @@ async function runVerify(input, deps = DEFAULT_DEPS2) {
3428
3446
  phases.push(ph);
3429
3447
  deps.onPhase?.(ph);
3430
3448
  };
3449
+ await verifyTlsPhase(input, deps, push);
3450
+ await verifyHealthPhase(input, deps, push);
3451
+ await verifyGraphqlPhase(input, deps, push);
3452
+ await verifyCloudflarePhase(input, deps, push);
3453
+ await verifyCosignPhase(input, deps, push);
3454
+ return { phases };
3455
+ }
3456
+ async function verifyTlsPhase(input, deps, push) {
3431
3457
  const tls2 = await deps.tls({ host: input.apiHost });
3432
3458
  if (!tls2.ok) {
3433
3459
  push({ name: "TLS handshake", status: "fail", detail: tls2.reason });
3434
- } else {
3435
- const line = `subject=${tls2.subject}, issuer=${tls2.issuer}, ${formatExpiry(tls2.daysToExpiry)}`;
3436
- const warn = tls2.daysToExpiry < input.certWarnDays;
3437
- if (warn) {
3438
- push({
3439
- name: "TLS handshake",
3440
- status: "warn",
3441
- detail: tls2.daysToExpiry < 0 ? line : `${line} (< warn threshold ${input.certWarnDays}d)`
3442
- });
3443
- } else {
3444
- push({ name: "TLS handshake", status: "ok", detail: line });
3445
- }
3460
+ return;
3461
+ }
3462
+ const line = `subject=${tls2.subject}, issuer=${tls2.issuer}, ${formatExpiry(tls2.daysToExpiry)}`;
3463
+ if (tls2.daysToExpiry < input.certWarnDays) {
3464
+ push({
3465
+ name: "TLS handshake",
3466
+ status: "warn",
3467
+ detail: tls2.daysToExpiry < 0 ? line : `${line} (< warn threshold ${input.certWarnDays}d)`
3468
+ });
3469
+ return;
3446
3470
  }
3447
- const healthUrl = `https://${input.apiHost}/health`;
3448
- const health = await deps.http({ url: healthUrl });
3471
+ push({ name: "TLS handshake", status: "ok", detail: line });
3472
+ }
3473
+ async function verifyHealthPhase(input, deps, push) {
3474
+ const health = await deps.http({ url: `https://${input.apiHost}/health` });
3449
3475
  if (health.ok) {
3450
3476
  push({ name: "GET /health", status: "ok", detail: `HTTP ${health.status}` });
3451
3477
  } else {
3452
3478
  push({ name: "GET /health", status: "fail", detail: health.reason });
3453
3479
  }
3454
- const gqlUrl = `https://${input.apiHost}/api`;
3455
- const gql = await deps.graphql({ url: gqlUrl });
3480
+ }
3481
+ async function verifyGraphqlPhase(input, deps, push) {
3482
+ const gql = await deps.graphql({ url: `https://${input.apiHost}/api` });
3456
3483
  if (gql.ok) {
3457
3484
  push({ name: "GraphQL { __typename }", status: "ok", detail: `typename=${gql.typename}` });
3458
3485
  } else {
3459
3486
  push({ name: "GraphQL { __typename }", status: "fail", detail: gql.reason });
3460
3487
  }
3488
+ }
3489
+ async function verifyCloudflarePhase(input, deps, push) {
3461
3490
  const cfFields = [
3462
3491
  ["--cf-token", input.cf?.token],
3463
3492
  ["--cf-account-id", input.cf?.accountId],
@@ -3470,54 +3499,57 @@ async function runVerify(input, deps = DEFAULT_DEPS2) {
3470
3499
  status: "skipped",
3471
3500
  detail: "pass --cf-token + --cf-account-id + --tunnel-id to enable"
3472
3501
  });
3473
- } else if (cfSet.length < cfFields.length) {
3502
+ return;
3503
+ }
3504
+ if (cfSet.length < cfFields.length) {
3474
3505
  const missing = cfFields.filter(([, v]) => v === void 0 || v === "").map(([name]) => name).join(", ");
3475
3506
  push({
3476
3507
  name: "Cloudflare Tunnel",
3477
3508
  status: "warn",
3478
3509
  detail: `partial CF config \u2014 missing ${missing}; check skipped`
3479
3510
  });
3511
+ return;
3512
+ }
3513
+ const tun = await deps.tunnel({
3514
+ token: input.cf.token,
3515
+ accountId: input.cf.accountId,
3516
+ tunnelId: input.cf.tunnelId
3517
+ });
3518
+ if (!tun.ok) {
3519
+ push({ name: "Cloudflare Tunnel", status: "fail", detail: tun.reason });
3520
+ } else if (tun.connections === 0) {
3521
+ push({
3522
+ name: "Cloudflare Tunnel",
3523
+ status: "warn",
3524
+ detail: `status=${tun.status}, 0 connections \u2014 cloudflared on the Studio appears offline`
3525
+ });
3480
3526
  } else {
3481
- const tun = await deps.tunnel({
3482
- token: input.cf.token,
3483
- accountId: input.cf.accountId,
3484
- tunnelId: input.cf.tunnelId
3527
+ push({
3528
+ name: "Cloudflare Tunnel",
3529
+ status: "ok",
3530
+ detail: `${tun.connections} connections, status=${tun.status}`
3485
3531
  });
3486
- if (!tun.ok) {
3487
- push({ name: "Cloudflare Tunnel", status: "fail", detail: tun.reason });
3488
- } else if (tun.connections === 0) {
3489
- push({
3490
- name: "Cloudflare Tunnel",
3491
- status: "warn",
3492
- detail: `status=${tun.status}, 0 connections \u2014 cloudflared on the Studio appears offline`
3493
- });
3494
- } else {
3495
- push({
3496
- name: "Cloudflare Tunnel",
3497
- status: "ok",
3498
- detail: `${tun.connections} connections, status=${tun.status}`
3499
- });
3500
- }
3501
3532
  }
3533
+ }
3534
+ async function verifyCosignPhase(input, deps, push) {
3502
3535
  if (input.images.length === 0) {
3503
3536
  push({
3504
3537
  name: "cosign verify",
3505
3538
  status: "skipped",
3506
3539
  detail: "pass --image <ref> (repeatable) to enable"
3507
3540
  });
3508
- } else {
3509
- for (const image of input.images) {
3510
- const cos = await deps.cosign({ image });
3511
- if (cos.ok) {
3512
- push({ name: `cosign verify ${image}`, status: "ok", detail: "signature valid" });
3513
- } else if (cos.skipped) {
3514
- push({ name: `cosign verify ${image}`, status: "skipped", detail: cos.reason });
3515
- } else {
3516
- push({ name: `cosign verify ${image}`, status: "fail", detail: cos.reason });
3517
- }
3541
+ return;
3542
+ }
3543
+ for (const image of input.images) {
3544
+ const cos = await deps.cosign({ image });
3545
+ if (cos.ok) {
3546
+ push({ name: `cosign verify ${image}`, status: "ok", detail: "signature valid" });
3547
+ } else if (cos.skipped) {
3548
+ push({ name: `cosign verify ${image}`, status: "skipped", detail: cos.reason });
3549
+ } else {
3550
+ push({ name: `cosign verify ${image}`, status: "fail", detail: cos.reason });
3518
3551
  }
3519
3552
  }
3520
- return { phases };
3521
3553
  }
3522
3554
  function readTokenFile(path) {
3523
3555
  const raw = readFileSync(path, "utf8").trim();
@@ -3542,6 +3574,53 @@ var verifyCommand = new Command("verify").description(
3542
3574
  new Option("--show-skipped", "Include skipped phases in the summary").default(false)
3543
3575
  ).action(async (opts) => {
3544
3576
  p3.intro(pc2.bgCyan(pc2.black(" create-op-node verify ")));
3577
+ const domain = await resolveDomain(opts);
3578
+ const certWarnDays = resolveCertWarnDays(opts);
3579
+ const cfToken = resolveCfToken(opts);
3580
+ const apiHost = opts.apiHost ?? `api.${domain}`;
3581
+ const images = opts.image ?? [];
3582
+ const totalPhases = 4 + (images.length === 0 ? 1 : images.length);
3583
+ const report = await runVerifyWithSpinner({
3584
+ apiHost,
3585
+ certWarnDays,
3586
+ cfToken,
3587
+ opts,
3588
+ images,
3589
+ totalPhases
3590
+ });
3591
+ renderVerifySummary(report, { opts, totalPhases });
3592
+ reportVerifyOutcome(report);
3593
+ });
3594
+ function phaseIcon2(ph) {
3595
+ switch (ph.status) {
3596
+ case "ok":
3597
+ return pc2.green("\u2713");
3598
+ case "warn":
3599
+ return pc2.yellow("\u26A0");
3600
+ case "skipped":
3601
+ return pc2.dim("\xB7");
3602
+ case "fail":
3603
+ return pc2.red("\u2717");
3604
+ }
3605
+ }
3606
+ function phaseColor(status) {
3607
+ switch (status) {
3608
+ case "ok":
3609
+ return pc2.green;
3610
+ case "warn":
3611
+ return pc2.yellow;
3612
+ case "skipped":
3613
+ return pc2.dim;
3614
+ case "fail":
3615
+ return pc2.red;
3616
+ }
3617
+ }
3618
+ function formatPhaseLine(prefix, ph) {
3619
+ const icon = phaseIcon2(ph);
3620
+ const colorize = phaseColor(ph.status);
3621
+ return colorize(`${icon} ${prefix}: ${ph.detail}`);
3622
+ }
3623
+ async function resolveDomain(opts) {
3545
3624
  const domain = opts.domain ? opts.domain : unwrap(
3546
3625
  await p3.text({
3547
3626
  message: "Public domain of the node?",
@@ -3553,28 +3632,33 @@ var verifyCommand = new Command("verify").description(
3553
3632
  p3.cancel(`--domain ${JSON.stringify(domain)} doesn't look like a domain.`);
3554
3633
  process.exit(2);
3555
3634
  }
3635
+ return domain;
3636
+ }
3637
+ function resolveCertWarnDays(opts) {
3556
3638
  const rawDays = opts.certWarnDays ?? "14";
3557
3639
  const certWarnDays = Number.parseInt(rawDays, 10);
3558
3640
  if (!Number.isFinite(certWarnDays) || certWarnDays < 0 || !/^\d+$/.test(rawDays)) {
3559
3641
  p3.cancel(`--cert-warn-days must be a non-negative integer (got ${JSON.stringify(rawDays)}).`);
3560
3642
  process.exit(2);
3561
3643
  }
3644
+ return certWarnDays;
3645
+ }
3646
+ function resolveCfToken(opts) {
3562
3647
  if (opts.cfToken && opts.cfTokenFile) {
3563
3648
  p3.cancel("Pass either --cf-token or --cf-token-file, not both.");
3564
3649
  process.exit(2);
3565
3650
  }
3566
- let cfToken = opts.cfToken;
3567
- if (!cfToken && opts.cfTokenFile) {
3568
- try {
3569
- cfToken = readTokenFile(opts.cfTokenFile);
3570
- } catch (err) {
3571
- p3.cancel(err.message);
3572
- process.exit(2);
3573
- }
3651
+ if (opts.cfToken) return opts.cfToken;
3652
+ if (!opts.cfTokenFile) return void 0;
3653
+ try {
3654
+ return readTokenFile(opts.cfTokenFile);
3655
+ } catch (err) {
3656
+ p3.cancel(err.message);
3657
+ process.exit(2);
3574
3658
  }
3575
- const apiHost = opts.apiHost ?? `api.${domain}`;
3576
- const images = opts.image ?? [];
3577
- const totalPhases = 4 + (images.length === 0 ? 1 : images.length);
3659
+ }
3660
+ async function runVerifyWithSpinner(args) {
3661
+ const { apiHost, certWarnDays, cfToken, opts, images, totalPhases } = args;
3578
3662
  let phaseIndex = 0;
3579
3663
  let activeSpin = null;
3580
3664
  const renderPhase = (ph) => {
@@ -3583,10 +3667,7 @@ var verifyCommand = new Command("verify").description(
3583
3667
  activeSpin?.stop(formatPhaseLine(prefix, ph));
3584
3668
  activeSpin = null;
3585
3669
  };
3586
- const deps = {
3587
- ...DEFAULT_DEPS2,
3588
- onPhase: renderPhase
3589
- };
3670
+ const deps = { ...DEFAULT_DEPS2, onPhase: renderPhase };
3590
3671
  activeSpin = p3.spinner();
3591
3672
  activeSpin.start("Running checks\u2026");
3592
3673
  const report = await runVerify(
@@ -3603,6 +3684,10 @@ var verifyCommand = new Command("verify").description(
3603
3684
  deps
3604
3685
  );
3605
3686
  activeSpin?.stop("Done.");
3687
+ return report;
3688
+ }
3689
+ function renderVerifySummary(report, args) {
3690
+ const { opts, totalPhases } = args;
3606
3691
  const visible = opts.showSkipped ?? false ? report.phases : report.phases.filter((ph) => ph.status !== "skipped");
3607
3692
  const lines = visible.map((ph) => {
3608
3693
  const idx = report.phases.indexOf(ph) + 1;
@@ -3613,47 +3698,19 @@ var verifyCommand = new Command("verify").description(
3613
3698
  lines.push(pc2.dim(`\xB7 ${skippedCount} phase${skippedCount === 1 ? "" : "s"} skipped (run with --show-skipped to see them)`));
3614
3699
  }
3615
3700
  p3.note(lines.join("\n"), "Summary");
3701
+ }
3702
+ function reportVerifyOutcome(report) {
3616
3703
  const summary = summarize(report);
3617
- if (summary.ok) {
3618
- if (summary.warned === 0) {
3619
- p3.outro(pc2.green("All checks passed."));
3620
- } else {
3621
- p3.outro(pc2.yellow(`Passed with ${summary.warned} warning${summary.warned === 1 ? "" : "s"}.`));
3622
- }
3623
- } else {
3704
+ if (!summary.ok) {
3624
3705
  p3.outro(pc2.red(`${summary.failed} check${summary.failed === 1 ? "" : "s"} failed.`));
3625
3706
  process.exit(1);
3626
3707
  }
3627
- });
3628
- function phaseIcon2(ph) {
3629
- switch (ph.status) {
3630
- case "ok":
3631
- return pc2.green("\u2713");
3632
- case "warn":
3633
- return pc2.yellow("\u26A0");
3634
- case "skipped":
3635
- return pc2.dim("\xB7");
3636
- case "fail":
3637
- return pc2.red("\u2717");
3638
- }
3639
- }
3640
- function phaseColor(status) {
3641
- switch (status) {
3642
- case "ok":
3643
- return pc2.green;
3644
- case "warn":
3645
- return pc2.yellow;
3646
- case "skipped":
3647
- return pc2.dim;
3648
- case "fail":
3649
- return pc2.red;
3708
+ if (summary.warned === 0) {
3709
+ p3.outro(pc2.green("All checks passed."));
3710
+ } else {
3711
+ p3.outro(pc2.yellow(`Passed with ${summary.warned} warning${summary.warned === 1 ? "" : "s"}.`));
3650
3712
  }
3651
3713
  }
3652
- function formatPhaseLine(prefix, ph) {
3653
- const icon = phaseIcon2(ph);
3654
- const colorize = phaseColor(ph.status);
3655
- return colorize(`${icon} ${prefix}: ${ph.detail}`);
3656
- }
3657
3714
 
3658
3715
  // src/lib/region-schema.json
3659
3716
  var region_schema_default = {
@@ -4387,6 +4444,14 @@ function regionFilePath(input) {
4387
4444
  }
4388
4445
  var schemaValidator = new Validator(region_schema_default, "7");
4389
4446
  function validateRegionConfig(file) {
4447
+ return [
4448
+ ...validateVersionAndIds(file),
4449
+ ...validateFipsCode(file),
4450
+ ...validateDataSourceUniqueness(file),
4451
+ ...validateAgainstSchema(file)
4452
+ ];
4453
+ }
4454
+ function validateVersionAndIds(file) {
4390
4455
  const issues = [];
4391
4456
  const { config } = file;
4392
4457
  if (!/^\d+\.\d+\.\d+$/.test(file.version)) {
@@ -4403,34 +4468,39 @@ function validateRegionConfig(file) {
4403
4468
  `county regionId "${config.regionId}" should be prefixed with its parent "${config.parentRegionId}-"`
4404
4469
  );
4405
4470
  }
4471
+ return issues;
4472
+ }
4473
+ function validateFipsCode(file) {
4474
+ const { config } = file;
4475
+ if (config.fipsCode === void 0) return [];
4406
4476
  const isCounty = config.parentRegionId !== void 0;
4407
- if (config.fipsCode !== void 0) {
4408
- const expected = isCounty ? 5 : 2;
4409
- if (!/^\d+$/.test(config.fipsCode) || config.fipsCode.length !== expected) {
4410
- issues.push(
4411
- `fipsCode "${config.fipsCode}" must be ${expected} digits for a ${isCounty ? "county" : "state"}`
4412
- );
4413
- }
4477
+ const expected = isCounty ? 5 : 2;
4478
+ if (!/^\d+$/.test(config.fipsCode) || config.fipsCode.length !== expected) {
4479
+ return [
4480
+ `fipsCode "${config.fipsCode}" must be ${expected} digits for a ${isCounty ? "county" : "state"}`
4481
+ ];
4414
4482
  }
4415
- if (Array.isArray(config.dataSources)) {
4416
- const seen = /* @__PURE__ */ new Set();
4417
- for (const src of config.dataSources) {
4418
- const key = `${src.dataType} ${src.url}`;
4419
- if (seen.has(key)) {
4420
- issues.push(`duplicate data source: ${src.dataType} ${src.url}`);
4421
- }
4422
- seen.add(key);
4423
- }
4424
- }
4425
- const result2 = schemaValidator.validate(file);
4426
- if (!result2.valid) {
4427
- for (const err of result2.errors) {
4428
- const path = err.instanceLocation || "<root>";
4429
- issues.push(`schema: ${path} ${err.error}`);
4483
+ return [];
4484
+ }
4485
+ function validateDataSourceUniqueness(file) {
4486
+ const { config } = file;
4487
+ if (!Array.isArray(config.dataSources)) return [];
4488
+ const issues = [];
4489
+ const seen = /* @__PURE__ */ new Set();
4490
+ for (const src of config.dataSources) {
4491
+ const key = `${src.dataType} ${src.url}`;
4492
+ if (seen.has(key)) {
4493
+ issues.push(`duplicate data source: ${src.dataType} ${src.url}`);
4430
4494
  }
4495
+ seen.add(key);
4431
4496
  }
4432
4497
  return issues;
4433
4498
  }
4499
+ function validateAgainstSchema(file) {
4500
+ const result2 = schemaValidator.validate(file);
4501
+ if (result2.valid) return [];
4502
+ return result2.errors.map((err) => `schema: ${err.instanceLocation || "<root>"} ${err.error}`);
4503
+ }
4434
4504
 
4435
4505
  // src/commands/region.ts
4436
4506
  var regionCommand = new Command("region").description(
@@ -4446,6 +4516,18 @@ var regionCommand = new Command("region").description(
4446
4516
  )
4447
4517
  ).addOption(new Option("-f, --force", "Overwrite an existing config file").default(false)).action(async (opts) => {
4448
4518
  p3.intro(pc2.bgMagenta(pc2.black(" create-op-node region ")));
4519
+ printRegionWelcome();
4520
+ const targetDir = await resolveTargetDir(opts);
4521
+ const level = await resolveLevel(opts);
4522
+ const identity = await collectIdentity(opts, level);
4523
+ const codes = await collectCodes(opts, level);
4524
+ const dataSources = await collectDataSources();
4525
+ const input = buildRegionInput({ level, identity, codes, dataSources });
4526
+ const file = buildAndValidateConfig(input);
4527
+ await writeRegionConfig({ opts, targetDir, level, identity, file });
4528
+ });
4529
+ var requireNonEmpty = (v) => v && v.trim().length > 0 ? void 0 : "Required";
4530
+ function printRegionWelcome() {
4449
4531
  p3.note(
4450
4532
  [
4451
4533
  "Scaffolds a declarative region config JSON for opuspopuli-regions \u2014",
@@ -4457,6 +4539,8 @@ var regionCommand = new Command("region").description(
4457
4539
  ].join("\n"),
4458
4540
  "Welcome"
4459
4541
  );
4542
+ }
4543
+ async function resolveTargetDir(opts) {
4460
4544
  const targetDir = opts.outDir ?? process.cwd();
4461
4545
  const looksRight = await looksLikeRegionsRepo(targetDir);
4462
4546
  if (!looksRight) {
@@ -4471,7 +4555,10 @@ var regionCommand = new Command("region").description(
4471
4555
  process.exit(0);
4472
4556
  }
4473
4557
  }
4474
- const level = opts.level === "state" || opts.level === "county" ? opts.level : unwrap(
4558
+ return targetDir;
4559
+ }
4560
+ async function resolveLevel(opts) {
4561
+ return opts.level === "state" || opts.level === "county" ? opts.level : unwrap(
4475
4562
  await p3.select({
4476
4563
  message: "What level is this region?",
4477
4564
  options: [
@@ -4480,27 +4567,31 @@ var regionCommand = new Command("region").description(
4480
4567
  ]
4481
4568
  })
4482
4569
  );
4570
+ }
4571
+ async function resolveParentSlug(opts, level) {
4572
+ if (level !== "county") return void 0;
4573
+ const parentSlug = opts.parent ?? unwrap(
4574
+ await p3.text({
4575
+ message: "Parent state slug?",
4576
+ placeholder: "california",
4577
+ validate: (v) => isValidSlug(v ?? "") ? void 0 : "kebab-case (lowercase letters, digits, hyphens)"
4578
+ })
4579
+ );
4580
+ if (!isValidSlug(parentSlug)) {
4581
+ p3.cancel(`Parent slug "${parentSlug}" must be kebab-case.`);
4582
+ process.exit(1);
4583
+ }
4584
+ return parentSlug;
4585
+ }
4586
+ async function collectIdentity(opts, level) {
4483
4587
  const rawName = opts.name ?? unwrap(
4484
4588
  await p3.text({
4485
4589
  message: level === "county" ? "County name?" : "State name?",
4486
4590
  placeholder: level === "county" ? "Alameda" : "California",
4487
- validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
4591
+ validate: requireNonEmpty
4488
4592
  })
4489
4593
  );
4490
- let parentSlug;
4491
- if (level === "county") {
4492
- parentSlug = opts.parent ?? unwrap(
4493
- await p3.text({
4494
- message: "Parent state slug?",
4495
- placeholder: "california",
4496
- validate: (v) => isValidSlug(v ?? "") ? void 0 : "kebab-case (lowercase letters, digits, hyphens)"
4497
- })
4498
- );
4499
- if (!isValidSlug(parentSlug)) {
4500
- p3.cancel(`Parent slug "${parentSlug}" must be kebab-case.`);
4501
- process.exit(1);
4502
- }
4503
- }
4594
+ const parentSlug = await resolveParentSlug(opts, level);
4504
4595
  const ownSlug = slugify(rawName);
4505
4596
  const regionId = level === "county" ? `${parentSlug}-${ownSlug}` : ownSlug;
4506
4597
  if (!isValidSlug(regionId)) {
@@ -4518,9 +4609,12 @@ var regionCommand = new Command("region").description(
4518
4609
  await p3.text({
4519
4610
  message: "One-line description of the data coverage?",
4520
4611
  placeholder: level === "county" ? `Civic data for ${rawName} County` : `Civic data for the state of ${rawName}`,
4521
- validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
4612
+ validate: requireNonEmpty
4522
4613
  })
4523
4614
  );
4615
+ return { rawName, parentSlug, ownSlug, regionId, regionName, description };
4616
+ }
4617
+ async function collectCodes(opts, level) {
4524
4618
  const stateCode = (opts.stateCode ?? unwrap(
4525
4619
  await p3.text({
4526
4620
  message: "Two-letter state code?",
@@ -4555,65 +4649,77 @@ var regionCommand = new Command("region").description(
4555
4649
  validate: (v) => isValidTimezone(v || "America/Los_Angeles") ? void 0 : "Not a recognized IANA zone (e.g. America/New_York)"
4556
4650
  })
4557
4651
  );
4652
+ return { stateCode, fipsCode, timezone };
4653
+ }
4654
+ async function collectOneDataSource(index) {
4655
+ const url = unwrap(
4656
+ await p3.text({
4657
+ message: `Data source #${index} \u2014 URL?`,
4658
+ placeholder: "https://example.gov/meetings",
4659
+ validate: (v) => /^https?:\/\//.test(v ?? "") ? void 0 : "Must start with http(s)://"
4660
+ })
4661
+ );
4662
+ const dataType = unwrap(
4663
+ await p3.select({
4664
+ message: "Data type?",
4665
+ options: DATA_TYPES.map((t) => ({ value: t, label: t }))
4666
+ })
4667
+ );
4668
+ const sourceType = unwrap(
4669
+ await p3.select({
4670
+ message: "Source type?",
4671
+ initialValue: "html_scrape",
4672
+ options: SOURCE_TYPES.map((t) => ({ value: t, label: t }))
4673
+ })
4674
+ );
4675
+ const contentGoal = unwrap(
4676
+ await p3.text({
4677
+ message: "Content goal (what should the scraper extract)?",
4678
+ placeholder: "Fetch upcoming board meeting agendas and minutes",
4679
+ validate: requireNonEmpty
4680
+ })
4681
+ );
4682
+ const category = unwrap(
4683
+ await p3.text({
4684
+ message: "Category label (optional)?",
4685
+ placeholder: "Board of Supervisors"
4686
+ })
4687
+ );
4688
+ const src = { url, dataType, sourceType, contentGoal };
4689
+ if (category && category.trim().length > 0) src.category = category.trim();
4690
+ return src;
4691
+ }
4692
+ async function collectDataSources() {
4558
4693
  const dataSources = [];
4559
4694
  do {
4560
- const url = unwrap(
4561
- await p3.text({
4562
- message: `Data source #${dataSources.length + 1} \u2014 URL?`,
4563
- placeholder: "https://example.gov/meetings",
4564
- validate: (v) => /^https?:\/\//.test(v ?? "") ? void 0 : "Must start with http(s)://"
4565
- })
4566
- );
4567
- const dataType = unwrap(
4568
- await p3.select({
4569
- message: "Data type?",
4570
- options: DATA_TYPES.map((t) => ({ value: t, label: t }))
4571
- })
4572
- );
4573
- const sourceType = unwrap(
4574
- await p3.select({
4575
- message: "Source type?",
4576
- initialValue: "html_scrape",
4577
- options: SOURCE_TYPES.map((t) => ({ value: t, label: t }))
4578
- })
4579
- );
4580
- const contentGoal = unwrap(
4581
- await p3.text({
4582
- message: "Content goal (what should the scraper extract)?",
4583
- placeholder: "Fetch upcoming board meeting agendas and minutes",
4584
- validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
4585
- })
4586
- );
4587
- const category = unwrap(
4588
- await p3.text({
4589
- message: "Category label (optional)?",
4590
- placeholder: "Board of Supervisors"
4591
- })
4592
- );
4593
- const src = { url, dataType, sourceType, contentGoal };
4594
- if (category && category.trim().length > 0) src.category = category.trim();
4595
- dataSources.push(src);
4695
+ dataSources.push(await collectOneDataSource(dataSources.length + 1));
4596
4696
  const again = unwrap(
4597
4697
  await p3.confirm({ message: "Add another data source?", initialValue: false })
4598
4698
  );
4599
4699
  if (!again) break;
4600
4700
  } while (true);
4601
- const input = {
4602
- regionId,
4603
- displayName: regionName,
4604
- regionName,
4605
- description,
4701
+ return dataSources;
4702
+ }
4703
+ function buildRegionInput(args) {
4704
+ const { level, identity, codes, dataSources } = args;
4705
+ return {
4706
+ level,
4707
+ regionId: identity.regionId,
4708
+ displayName: identity.regionName,
4709
+ regionName: identity.regionName,
4710
+ description: identity.description,
4606
4711
  // New region configs start at 0.1.0 — the documented convention in the
4607
- // regions repo's CLAUDE.md. Bump to a higher version manually as the
4608
- // config matures.
4712
+ // regions repo's CLAUDE.md. Bump manually as the config matures.
4609
4713
  version: "0.1.0",
4610
- timezone,
4611
- stateCode,
4612
- fipsCode,
4714
+ timezone: codes.timezone,
4715
+ stateCode: codes.stateCode,
4716
+ fipsCode: codes.fipsCode,
4613
4717
  dataSources,
4614
- ...parentSlug ? { parentRegionId: parentSlug } : {},
4615
- ...level === "county" ? { countySlug: ownSlug } : {}
4718
+ ...identity.parentSlug ? { parentRegionId: identity.parentSlug } : {},
4719
+ ...level === "county" ? { countySlug: identity.ownSlug } : {}
4616
4720
  };
4721
+ }
4722
+ function buildAndValidateConfig(input) {
4617
4723
  const file = buildRegionConfig(input);
4618
4724
  const issues = validateRegionConfig(file);
4619
4725
  if (issues.length > 0) {
@@ -4621,11 +4727,15 @@ var regionCommand = new Command("region").description(
4621
4727
  p3.cancel("The generated config would not pass the regions repo CI. Re-run and adjust.");
4622
4728
  process.exit(1);
4623
4729
  }
4730
+ return file;
4731
+ }
4732
+ async function writeRegionConfig(args) {
4733
+ const { opts, targetDir, level, identity, file } = args;
4624
4734
  const relPath = regionFilePath({
4625
4735
  level,
4626
- regionId,
4627
- ...parentSlug ? { parentRegionId: parentSlug } : {},
4628
- ...level === "county" ? { countySlug: ownSlug } : {}
4736
+ regionId: identity.regionId,
4737
+ ...identity.parentSlug ? { parentRegionId: identity.parentSlug } : {},
4738
+ ...level === "county" ? { countySlug: identity.ownSlug } : {}
4629
4739
  });
4630
4740
  const absPath = resolve(targetDir, relPath);
4631
4741
  const json = `${JSON.stringify(file, null, 2)}
@@ -4655,8 +4765,8 @@ var regionCommand = new Command("region").description(
4655
4765
  ].join("\n"),
4656
4766
  "Done"
4657
4767
  );
4658
- p3.outro(pc2.magenta(`Region scaffolded: ${regionId}`));
4659
- });
4768
+ p3.outro(pc2.magenta(`Region scaffolded: ${identity.regionId}`));
4769
+ }
4660
4770
  async function fileExists3(path) {
4661
4771
  try {
4662
4772
  await access(path);
@@ -4674,7 +4784,7 @@ async function looksLikeRegionsRepo(dir) {
4674
4784
  }
4675
4785
 
4676
4786
  // src/cli.ts
4677
- var VERSION = "0.10.8";
4787
+ var VERSION = "0.10.10";
4678
4788
  var program = new Command();
4679
4789
  program.name("create-op-node").description(
4680
4790
  "Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."