create-op-node 0.10.9 → 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" };
@@ -4426,6 +4444,14 @@ function regionFilePath(input) {
4426
4444
  }
4427
4445
  var schemaValidator = new Validator(region_schema_default, "7");
4428
4446
  function validateRegionConfig(file) {
4447
+ return [
4448
+ ...validateVersionAndIds(file),
4449
+ ...validateFipsCode(file),
4450
+ ...validateDataSourceUniqueness(file),
4451
+ ...validateAgainstSchema(file)
4452
+ ];
4453
+ }
4454
+ function validateVersionAndIds(file) {
4429
4455
  const issues = [];
4430
4456
  const { config } = file;
4431
4457
  if (!/^\d+\.\d+\.\d+$/.test(file.version)) {
@@ -4442,34 +4468,39 @@ function validateRegionConfig(file) {
4442
4468
  `county regionId "${config.regionId}" should be prefixed with its parent "${config.parentRegionId}-"`
4443
4469
  );
4444
4470
  }
4471
+ return issues;
4472
+ }
4473
+ function validateFipsCode(file) {
4474
+ const { config } = file;
4475
+ if (config.fipsCode === void 0) return [];
4445
4476
  const isCounty = config.parentRegionId !== void 0;
4446
- if (config.fipsCode !== void 0) {
4447
- const expected = isCounty ? 5 : 2;
4448
- if (!/^\d+$/.test(config.fipsCode) || config.fipsCode.length !== expected) {
4449
- issues.push(
4450
- `fipsCode "${config.fipsCode}" must be ${expected} digits for a ${isCounty ? "county" : "state"}`
4451
- );
4452
- }
4453
- }
4454
- if (Array.isArray(config.dataSources)) {
4455
- const seen = /* @__PURE__ */ new Set();
4456
- for (const src of config.dataSources) {
4457
- const key = `${src.dataType} ${src.url}`;
4458
- if (seen.has(key)) {
4459
- issues.push(`duplicate data source: ${src.dataType} ${src.url}`);
4460
- }
4461
- seen.add(key);
4462
- }
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
+ ];
4463
4482
  }
4464
- const result2 = schemaValidator.validate(file);
4465
- if (!result2.valid) {
4466
- for (const err of result2.errors) {
4467
- const path = err.instanceLocation || "<root>";
4468
- 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}`);
4469
4494
  }
4495
+ seen.add(key);
4470
4496
  }
4471
4497
  return issues;
4472
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
+ }
4473
4504
 
4474
4505
  // src/commands/region.ts
4475
4506
  var regionCommand = new Command("region").description(
@@ -4485,6 +4516,18 @@ var regionCommand = new Command("region").description(
4485
4516
  )
4486
4517
  ).addOption(new Option("-f, --force", "Overwrite an existing config file").default(false)).action(async (opts) => {
4487
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() {
4488
4531
  p3.note(
4489
4532
  [
4490
4533
  "Scaffolds a declarative region config JSON for opuspopuli-regions \u2014",
@@ -4496,6 +4539,8 @@ var regionCommand = new Command("region").description(
4496
4539
  ].join("\n"),
4497
4540
  "Welcome"
4498
4541
  );
4542
+ }
4543
+ async function resolveTargetDir(opts) {
4499
4544
  const targetDir = opts.outDir ?? process.cwd();
4500
4545
  const looksRight = await looksLikeRegionsRepo(targetDir);
4501
4546
  if (!looksRight) {
@@ -4510,7 +4555,10 @@ var regionCommand = new Command("region").description(
4510
4555
  process.exit(0);
4511
4556
  }
4512
4557
  }
4513
- 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(
4514
4562
  await p3.select({
4515
4563
  message: "What level is this region?",
4516
4564
  options: [
@@ -4519,27 +4567,31 @@ var regionCommand = new Command("region").description(
4519
4567
  ]
4520
4568
  })
4521
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) {
4522
4587
  const rawName = opts.name ?? unwrap(
4523
4588
  await p3.text({
4524
4589
  message: level === "county" ? "County name?" : "State name?",
4525
4590
  placeholder: level === "county" ? "Alameda" : "California",
4526
- validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
4591
+ validate: requireNonEmpty
4527
4592
  })
4528
4593
  );
4529
- let parentSlug;
4530
- if (level === "county") {
4531
- parentSlug = opts.parent ?? unwrap(
4532
- await p3.text({
4533
- message: "Parent state slug?",
4534
- placeholder: "california",
4535
- validate: (v) => isValidSlug(v ?? "") ? void 0 : "kebab-case (lowercase letters, digits, hyphens)"
4536
- })
4537
- );
4538
- if (!isValidSlug(parentSlug)) {
4539
- p3.cancel(`Parent slug "${parentSlug}" must be kebab-case.`);
4540
- process.exit(1);
4541
- }
4542
- }
4594
+ const parentSlug = await resolveParentSlug(opts, level);
4543
4595
  const ownSlug = slugify(rawName);
4544
4596
  const regionId = level === "county" ? `${parentSlug}-${ownSlug}` : ownSlug;
4545
4597
  if (!isValidSlug(regionId)) {
@@ -4557,9 +4609,12 @@ var regionCommand = new Command("region").description(
4557
4609
  await p3.text({
4558
4610
  message: "One-line description of the data coverage?",
4559
4611
  placeholder: level === "county" ? `Civic data for ${rawName} County` : `Civic data for the state of ${rawName}`,
4560
- validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
4612
+ validate: requireNonEmpty
4561
4613
  })
4562
4614
  );
4615
+ return { rawName, parentSlug, ownSlug, regionId, regionName, description };
4616
+ }
4617
+ async function collectCodes(opts, level) {
4563
4618
  const stateCode = (opts.stateCode ?? unwrap(
4564
4619
  await p3.text({
4565
4620
  message: "Two-letter state code?",
@@ -4594,65 +4649,77 @@ var regionCommand = new Command("region").description(
4594
4649
  validate: (v) => isValidTimezone(v || "America/Los_Angeles") ? void 0 : "Not a recognized IANA zone (e.g. America/New_York)"
4595
4650
  })
4596
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() {
4597
4693
  const dataSources = [];
4598
4694
  do {
4599
- const url = unwrap(
4600
- await p3.text({
4601
- message: `Data source #${dataSources.length + 1} \u2014 URL?`,
4602
- placeholder: "https://example.gov/meetings",
4603
- validate: (v) => /^https?:\/\//.test(v ?? "") ? void 0 : "Must start with http(s)://"
4604
- })
4605
- );
4606
- const dataType = unwrap(
4607
- await p3.select({
4608
- message: "Data type?",
4609
- options: DATA_TYPES.map((t) => ({ value: t, label: t }))
4610
- })
4611
- );
4612
- const sourceType = unwrap(
4613
- await p3.select({
4614
- message: "Source type?",
4615
- initialValue: "html_scrape",
4616
- options: SOURCE_TYPES.map((t) => ({ value: t, label: t }))
4617
- })
4618
- );
4619
- const contentGoal = unwrap(
4620
- await p3.text({
4621
- message: "Content goal (what should the scraper extract)?",
4622
- placeholder: "Fetch upcoming board meeting agendas and minutes",
4623
- validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
4624
- })
4625
- );
4626
- const category = unwrap(
4627
- await p3.text({
4628
- message: "Category label (optional)?",
4629
- placeholder: "Board of Supervisors"
4630
- })
4631
- );
4632
- const src = { url, dataType, sourceType, contentGoal };
4633
- if (category && category.trim().length > 0) src.category = category.trim();
4634
- dataSources.push(src);
4695
+ dataSources.push(await collectOneDataSource(dataSources.length + 1));
4635
4696
  const again = unwrap(
4636
4697
  await p3.confirm({ message: "Add another data source?", initialValue: false })
4637
4698
  );
4638
4699
  if (!again) break;
4639
4700
  } while (true);
4640
- const input = {
4641
- regionId,
4642
- displayName: regionName,
4643
- regionName,
4644
- 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,
4645
4711
  // New region configs start at 0.1.0 — the documented convention in the
4646
- // regions repo's CLAUDE.md. Bump to a higher version manually as the
4647
- // config matures.
4712
+ // regions repo's CLAUDE.md. Bump manually as the config matures.
4648
4713
  version: "0.1.0",
4649
- timezone,
4650
- stateCode,
4651
- fipsCode,
4714
+ timezone: codes.timezone,
4715
+ stateCode: codes.stateCode,
4716
+ fipsCode: codes.fipsCode,
4652
4717
  dataSources,
4653
- ...parentSlug ? { parentRegionId: parentSlug } : {},
4654
- ...level === "county" ? { countySlug: ownSlug } : {}
4718
+ ...identity.parentSlug ? { parentRegionId: identity.parentSlug } : {},
4719
+ ...level === "county" ? { countySlug: identity.ownSlug } : {}
4655
4720
  };
4721
+ }
4722
+ function buildAndValidateConfig(input) {
4656
4723
  const file = buildRegionConfig(input);
4657
4724
  const issues = validateRegionConfig(file);
4658
4725
  if (issues.length > 0) {
@@ -4660,11 +4727,15 @@ var regionCommand = new Command("region").description(
4660
4727
  p3.cancel("The generated config would not pass the regions repo CI. Re-run and adjust.");
4661
4728
  process.exit(1);
4662
4729
  }
4730
+ return file;
4731
+ }
4732
+ async function writeRegionConfig(args) {
4733
+ const { opts, targetDir, level, identity, file } = args;
4663
4734
  const relPath = regionFilePath({
4664
4735
  level,
4665
- regionId,
4666
- ...parentSlug ? { parentRegionId: parentSlug } : {},
4667
- ...level === "county" ? { countySlug: ownSlug } : {}
4736
+ regionId: identity.regionId,
4737
+ ...identity.parentSlug ? { parentRegionId: identity.parentSlug } : {},
4738
+ ...level === "county" ? { countySlug: identity.ownSlug } : {}
4668
4739
  });
4669
4740
  const absPath = resolve(targetDir, relPath);
4670
4741
  const json = `${JSON.stringify(file, null, 2)}
@@ -4694,8 +4765,8 @@ var regionCommand = new Command("region").description(
4694
4765
  ].join("\n"),
4695
4766
  "Done"
4696
4767
  );
4697
- p3.outro(pc2.magenta(`Region scaffolded: ${regionId}`));
4698
- });
4768
+ p3.outro(pc2.magenta(`Region scaffolded: ${identity.regionId}`));
4769
+ }
4699
4770
  async function fileExists3(path) {
4700
4771
  try {
4701
4772
  await access(path);
@@ -4713,7 +4784,7 @@ async function looksLikeRegionsRepo(dir) {
4713
4784
  }
4714
4785
 
4715
4786
  // src/cli.ts
4716
- var VERSION = "0.10.9";
4787
+ var VERSION = "0.10.10";
4717
4788
  var program = new Command();
4718
4789
  program.name("create-op-node").description(
4719
4790
  "Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."