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 +237 -166
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
1828
|
-
|
|
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
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
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
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
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
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
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
|
-
|
|
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:
|
|
4591
|
+
validate: requireNonEmpty
|
|
4527
4592
|
})
|
|
4528
4593
|
);
|
|
4529
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
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
|
|
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.
|
|
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."
|