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 +373 -263
- 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" };
|
|
@@ -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
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
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
|
-
|
|
3448
|
-
|
|
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
|
-
|
|
3455
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
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
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
}
|
|
3514
|
-
|
|
3515
|
-
}
|
|
3516
|
-
|
|
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
|
-
|
|
3567
|
-
if (!
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
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
|
-
|
|
3576
|
-
|
|
3577
|
-
const
|
|
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
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
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
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
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
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
}
|
|
4424
|
-
|
|
4425
|
-
|
|
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
|
-
|
|
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:
|
|
4591
|
+
validate: requireNonEmpty
|
|
4488
4592
|
})
|
|
4489
4593
|
);
|
|
4490
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
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
|
|
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.
|
|
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."
|