@odla-ai/cli 0.7.0 → 0.8.0

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/bin.cjs CHANGED
@@ -27,9 +27,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
28
28
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
29
29
 
30
- // src/cli.ts
31
- var import_security2 = require("@odla-ai/security");
32
-
33
30
  // src/admin-ai.ts
34
31
  var import_node_process4 = __toESM(require("process"), 1);
35
32
 
@@ -747,6 +744,65 @@ function addOption(options, name, value) {
747
744
  else options[name] = [String(current), String(value)];
748
745
  }
749
746
 
747
+ // src/admin-command.ts
748
+ async function adminCommand(parsed) {
749
+ const area = parsed.positionals[1];
750
+ const action = parsed.positionals[2];
751
+ const credentialSet = action === "credential" && parsed.positionals[3] === "set";
752
+ const credentials = action === "credentials";
753
+ const models = action === "models";
754
+ const usage = action === "usage";
755
+ const audit = action === "audit";
756
+ if (area !== "ai" || action !== "show" && action !== "set" && !credentialSet && !credentials && !models && !usage && !audit) {
757
+ throw new Error('unknown admin command. Try "odla-ai admin ai show".');
758
+ }
759
+ assertArgs(parsed, [
760
+ "platform",
761
+ "json",
762
+ "open",
763
+ "provider",
764
+ "model",
765
+ "enabled",
766
+ "max-input-bytes",
767
+ "max-output-tokens",
768
+ "max-calls-per-run",
769
+ "from-env",
770
+ "stdin",
771
+ "discovery-provider",
772
+ "discovery-model",
773
+ "validation-provider",
774
+ "validation-model",
775
+ "app-id",
776
+ "env",
777
+ "run-id",
778
+ "limit"
779
+ ], credentialSet ? 5 : action === "set" ? 4 : 3);
780
+ await adminAi({
781
+ action: credentialSet ? "credential-set" : credentials ? "credentials" : models ? "models" : usage ? "usage" : audit ? "audit" : action,
782
+ purpose: action === "set" ? parsed.positionals[3] : void 0,
783
+ provider: stringOpt(parsed.options.provider),
784
+ model: stringOpt(parsed.options.model),
785
+ enabled: boolOpt(parsed.options.enabled),
786
+ maxInputBytes: numberOpt(parsed.options["max-input-bytes"], "--max-input-bytes"),
787
+ maxOutputTokens: numberOpt(parsed.options["max-output-tokens"], "--max-output-tokens"),
788
+ maxCallsPerRun: numberOpt(parsed.options["max-calls-per-run"], "--max-calls-per-run"),
789
+ platform: stringOpt(parsed.options.platform),
790
+ json: parsed.options.json === true,
791
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
792
+ credentialProvider: credentialSet ? parsed.positionals[4] : void 0,
793
+ fromEnv: stringOpt(parsed.options["from-env"]),
794
+ stdin: parsed.options.stdin === true,
795
+ discoveryProvider: stringOpt(parsed.options["discovery-provider"]),
796
+ discoveryModel: stringOpt(parsed.options["discovery-model"]),
797
+ validationProvider: stringOpt(parsed.options["validation-provider"]),
798
+ validationModel: stringOpt(parsed.options["validation-model"]),
799
+ appId: stringOpt(parsed.options["app-id"]),
800
+ env: stringOpt(parsed.options.env),
801
+ runId: stringOpt(parsed.options["run-id"]),
802
+ limit: numberOpt(parsed.options.limit, "--limit")
803
+ });
804
+ }
805
+
750
806
  // src/capabilities.ts
751
807
  var CAPABILITIES = {
752
808
  cli: [
@@ -756,6 +812,7 @@ var CAPABILITIES = {
756
812
  "push db schema/rules and configure platform AI, auth, and deployment links",
757
813
  "validate config offline and smoke-test a provisioned db environment",
758
814
  "run app-attributed hosted security discovery and independent validation without provider keys",
815
+ "connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
759
816
  "let admins manage system AI policy and credentials, inspect attributed usage, and read immutable admin changes through separate short-lived capability-only approvals"
760
817
  ],
761
818
  agent: [
@@ -768,12 +825,14 @@ var CAPABILITIES = {
768
825
  "consent to production changes with --yes",
769
826
  "request destructive credential rotation explicitly",
770
827
  "review application semantics, security findings, releases, and merges",
771
- "approve redacted-source disclosure before hosted security reasoning"
828
+ "approve redacted-source disclosure before hosted security reasoning",
829
+ "approve GitHub App repository access separately from redacted-snippet disclosure"
772
830
  ],
773
831
  studio: [
774
832
  "view telemetry and environment state",
775
833
  "perform manual credential recovery when the CLI's local shown-once copy is unavailable",
776
- "configure system AI purposes and view app/environment/run-attributed usage"
834
+ "configure system AI purposes and view app/environment/run-attributed usage",
835
+ "connect/revoke GitHub security sources and inspect ref-to-SHA jobs, routes, retention, coverage, and normalized reports"
777
836
  ]
778
837
  };
779
838
  function printCapabilities(json = false, out = console) {
@@ -1170,14 +1229,103 @@ async function doctor(options) {
1170
1229
  }
1171
1230
  }
1172
1231
 
1173
- // src/init.ts
1232
+ // src/help.ts
1174
1233
  var import_node_fs6 = require("fs");
1234
+ function cliVersion() {
1235
+ const pkg = JSON.parse((0, import_node_fs6.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
1236
+ return pkg.version ?? "unknown";
1237
+ }
1238
+ function printHelp() {
1239
+ console.log(`odla-ai
1240
+
1241
+ Usage:
1242
+ odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
1243
+ odla-ai init --app-id <id> --name <name> [--services db,ai,o11y] [--env dev --env prod]
1244
+ odla-ai doctor [--config odla.config.mjs]
1245
+ odla-ai capabilities [--json]
1246
+ odla-ai admin ai show [--platform https://odla.ai] [--json]
1247
+ odla-ai admin ai models [--provider <id>] [--json]
1248
+ odla-ai admin ai set <purpose> [--provider <id>] [--model <id>] [--enabled|--no-enabled]
1249
+ odla-ai admin ai set security --discovery-provider <id> --discovery-model <id>
1250
+ --validation-provider <id> --validation-model <id> [--enabled|--no-enabled]
1251
+ odla-ai admin ai credentials [--json]
1252
+ odla-ai admin ai credential set <provider> (--from-env <NAME>|--stdin)
1253
+ odla-ai admin ai usage [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
1254
+ odla-ai admin ai audit [--limit <1-200>] [--json]
1255
+ odla-ai security github connect [--repo owner/name] [--env dev] [--no-open]
1256
+ odla-ai security github disconnect --source <id> [--env dev] [--yes]
1257
+ odla-ai security plan [--env dev] [--json]
1258
+ odla-ai security sources [--env dev] [--json]
1259
+ odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
1260
+ odla-ai security status <job-id> [--follow] [--json]
1261
+ odla-ai security report <job-id> [--json]
1262
+ odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
1263
+ odla-ai security run [target] --self --ack-redacted-source
1264
+ odla-ai provision [--config odla.config.mjs] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
1265
+ odla-ai smoke [--config odla.config.mjs] [--env dev]
1266
+ odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
1267
+ odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
1268
+ odla-ai version
1269
+
1270
+ Commands:
1271
+ setup Install offline odla runbooks for common coding-agent harnesses.
1272
+ init Create a generic odla.config.mjs plus starter schema/rules files.
1273
+ doctor Validate and summarize the project config without network calls.
1274
+ capabilities Show what the CLI automates vs agent edits and human checkpoints.
1275
+ admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
1276
+ security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
1277
+ provision Register services, configure them, persist credentials, optionally push secrets.
1278
+ smoke Verify local credentials, public-config, live schema, and db aggregate.
1279
+ skill Same installer; --agent accepts all, claude, codex, cursor,
1280
+ copilot, gemini, or agents (repeatable or comma-separated).
1281
+ secrets Push configured db/o11y secrets into the Worker via wrangler stdin.
1282
+ version Print the CLI version.
1283
+
1284
+ Safety:
1285
+ New projects target dev only. Add prod explicitly to odla.config.mjs and pass
1286
+ --yes to provision it; use --dry-run first to inspect the resolved plan.
1287
+ Provision caches the approved developer token and service credentials under
1288
+ .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
1289
+ preflights Wrangler before any shown-once issuance or destructive rotation.
1290
+ Provision opens the approval page automatically in interactive terminals;
1291
+ use --open to force browser launch or --no-open to suppress it.
1292
+ GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
1293
+ for a PAT or provider key. GitHub read access is separate from the explicit
1294
+ --ack-redacted-source consent and the exact --plan-digest printed by security
1295
+ plan are required before bounded snippets reach System AI.
1296
+ Run security plan first to inspect the admin-selected providers, models,
1297
+ per-route bounds, credential readiness, retention, no-execution boundary,
1298
+ and digest that binds consent to that exact plan.
1299
+ `);
1300
+ }
1301
+
1302
+ // src/harness-options.ts
1303
+ function harnessList(parsed) {
1304
+ const selected = [
1305
+ ...harnessOption(parsed.options.agent, "--agent"),
1306
+ ...harnessOption(parsed.options.harness, "--harness")
1307
+ ];
1308
+ return selected.length ? selected : ["all"];
1309
+ }
1310
+ function harnessOption(value, flag) {
1311
+ if (value === void 0) return [];
1312
+ if (typeof value === "boolean") throw new Error(`${flag} requires a harness name`);
1313
+ const raw = Array.isArray(value) ? value : [value];
1314
+ const parts = raw.flatMap((item) => item.split(","));
1315
+ if (parts.some((item) => !item.trim())) {
1316
+ throw new Error(`${flag} requires a harness name`);
1317
+ }
1318
+ return parts.map((item) => item.trim());
1319
+ }
1320
+
1321
+ // src/init.ts
1322
+ var import_node_fs7 = require("fs");
1175
1323
  var import_node_path5 = require("path");
1176
1324
  function initProject(options) {
1177
1325
  const out = options.stdout ?? console;
1178
1326
  const rootDir = (0, import_node_path5.resolve)(options.rootDir ?? process.cwd());
1179
1327
  const configPath = (0, import_node_path5.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
1180
- if ((0, import_node_fs6.existsSync)(configPath) && !options.force) {
1328
+ if ((0, import_node_fs7.existsSync)(configPath) && !options.force) {
1181
1329
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
1182
1330
  }
1183
1331
  if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
@@ -1186,10 +1334,10 @@ function initProject(options) {
1186
1334
  const envs = options.envs?.length ? options.envs : ["dev"];
1187
1335
  const services = options.services?.length ? options.services : ["db", "ai"];
1188
1336
  const aiProvider = options.aiProvider ?? "anthropic";
1189
- (0, import_node_fs6.mkdirSync)((0, import_node_path5.dirname)(configPath), { recursive: true });
1190
- (0, import_node_fs6.mkdirSync)((0, import_node_path5.resolve)(rootDir, "src/odla"), { recursive: true });
1191
- (0, import_node_fs6.mkdirSync)((0, import_node_path5.resolve)(rootDir, ".odla"), { recursive: true });
1192
- (0, import_node_fs6.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
1337
+ (0, import_node_fs7.mkdirSync)((0, import_node_path5.dirname)(configPath), { recursive: true });
1338
+ (0, import_node_fs7.mkdirSync)((0, import_node_path5.resolve)(rootDir, "src/odla"), { recursive: true });
1339
+ (0, import_node_fs7.mkdirSync)((0, import_node_path5.resolve)(rootDir, ".odla"), { recursive: true });
1340
+ (0, import_node_fs7.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
1193
1341
  writeIfMissing((0, import_node_path5.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
1194
1342
  writeIfMissing((0, import_node_path5.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
1195
1343
  ensureGitignore(rootDir);
@@ -1198,8 +1346,8 @@ function initProject(options) {
1198
1346
  out.log("updated .gitignore for local odla credentials");
1199
1347
  }
1200
1348
  function writeIfMissing(path, text) {
1201
- if ((0, import_node_fs6.existsSync)(path)) return;
1202
- (0, import_node_fs6.writeFileSync)(path, text);
1349
+ if ((0, import_node_fs7.existsSync)(path)) return;
1350
+ (0, import_node_fs7.writeFileSync)(path, text);
1203
1351
  }
1204
1352
  function configTemplate(input) {
1205
1353
  return `export default {
@@ -1652,6 +1800,162 @@ async function safeText2(res) {
1652
1800
  }
1653
1801
  }
1654
1802
 
1803
+ // src/security-command-context.ts
1804
+ var import_promises = require("readline/promises");
1805
+ async function hostedSecurityContext(parsed, dependencies) {
1806
+ const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
1807
+ const cfg = await loadProjectConfig(configPath);
1808
+ const env = stringOpt(parsed.options.env) ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
1809
+ if (!env || !cfg.envs.includes(env)) {
1810
+ throw new Error(`env "${env ?? ""}" is not declared in ${configPath}`);
1811
+ }
1812
+ const platform = platformAudience(stringOpt(parsed.options.platform) ?? cfg.platformUrl);
1813
+ if (platformAudience(cfg.platformUrl) !== platform) {
1814
+ throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
1815
+ }
1816
+ const doFetch = dependencies.fetch ?? fetch;
1817
+ const stdout = dependencies.stdout ?? console;
1818
+ const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
1819
+ const token = await getDeveloperToken(
1820
+ cfg,
1821
+ { configPath, open, openApprovalUrl: dependencies.openUrl },
1822
+ doFetch,
1823
+ stdout
1824
+ );
1825
+ return { platform, token, appId: cfg.app.id, env, fetch: doFetch, stdout };
1826
+ }
1827
+ async function interactiveConfirmation(message, dependencies) {
1828
+ if (dependencies.confirm) return dependencies.confirm(message);
1829
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
1830
+ const prompt = (0, import_promises.createInterface)({ input: process.stdin, output: process.stdout });
1831
+ try {
1832
+ const answer = await prompt.question(`${message} [y/N] `);
1833
+ return /^y(?:es)?$/i.test(answer.trim());
1834
+ } finally {
1835
+ prompt.close();
1836
+ }
1837
+ }
1838
+ function requiredSecurityPositional(parsed, index, label) {
1839
+ const value = parsed.positionals[index];
1840
+ if (!value) throw new Error(`${label} is required`);
1841
+ return value;
1842
+ }
1843
+ function securityProfile(value) {
1844
+ if (value === void 0) return void 0;
1845
+ if (value === "odla" || value === "cloudflare-app" || value === "generic") return value;
1846
+ throw new Error("--profile must be odla, cloudflare-app, or generic");
1847
+ }
1848
+
1849
+ // src/security-command-output.ts
1850
+ function printHostedSecurityPlan(out, plan, appId) {
1851
+ out.log(`Hosted security plan for ${appId}/${plan.env}: ${plan.ready ? "ready" : "not ready"}`);
1852
+ out.log(` plan digest: ${plan.planDigest}`);
1853
+ out.log(` consent/report/redaction: ${plan.consentContract} \xB7 ${plan.reportProjection} \xB7 ${plan.redactionContract}`);
1854
+ out.log(` prompt bundle: ${plan.promptBundle}`);
1855
+ printHostedSecurityPlanRoute(out, "discovery", plan.routes.discovery);
1856
+ printHostedSecurityPlanRoute(out, "validation", plan.routes.validation);
1857
+ out.log(` independent validation: ${plan.independent ? "yes" : "no"}`);
1858
+ out.log(" source disclosure: bounded best-effort credential-pattern-redacted snippets may be sent through odla.ai to the selected providers");
1859
+ out.log(` retained report: model-derived prose and repository-relative paths for up to ${plan.reportRetentionDays} days; treat it as sensitive`);
1860
+ out.log(" target execution: disabled");
1861
+ out.log(` to approve this exact plan, run security run --source <id> --plan-digest ${plan.planDigest} --ack-redacted-source`);
1862
+ }
1863
+ function printHostedSecurityIntent(out, intent) {
1864
+ out.log(`Hosted security execution intent for ${intent.appId}/${intent.env}:`);
1865
+ out.log(` execution digest: ${intent.executionDigest}`);
1866
+ out.log(` contract: ${intent.executionContract}`);
1867
+ out.log(` source: ${intent.repository} (${intent.sourceId})`);
1868
+ out.log(` ref: ${intent.requestedRef ?? `default branch (${intent.defaultBranch})`}`);
1869
+ out.log(` profile: ${intent.profile}`);
1870
+ out.log(` disclosure: ${intent.sourceDisclosure} \xB7 plan ${intent.planDigest}`);
1871
+ }
1872
+ function assertHostedSecurityPlanReady(plan) {
1873
+ const reasons = [];
1874
+ for (const [label, route] of Object.entries(plan.routes)) {
1875
+ if (!route.enabled) reasons.push(`${label} is disabled`);
1876
+ if (!route.credentialReady) reasons.push(`${label} provider credential is unavailable`);
1877
+ }
1878
+ if (!plan.independent) reasons.push("discovery and validation are not independently routed");
1879
+ if (plan.ready && reasons.length === 0) return;
1880
+ if (!plan.ready && reasons.length === 0) reasons.push("the platform marked the plan unavailable");
1881
+ throw new Error(`hosted security is not ready${reasons.length ? `: ${reasons.join("; ")}` : ""}. Ask a platform admin to update System AI security policy or credentials`);
1882
+ }
1883
+ function printHostedJob(out, job, platform, appId) {
1884
+ out.log(`security job ${job.jobId}: ${job.status}`);
1885
+ out.log(` source: ${job.repository} ${job.requestedRef ?? "default"} -> ${job.commitSha ?? "resolving"}`);
1886
+ if (job.routes) {
1887
+ out.log(` discovery: ${routeLabel(job.routes.discovery)}`);
1888
+ out.log(` validation: ${routeLabel(job.routes.validation)}`);
1889
+ } else {
1890
+ out.log(" routes: platform System AI policy (assigned when analysis starts)");
1891
+ }
1892
+ out.log(` disclosure: ${job.sourceDisclosure} snippets \xB7 metadata retained up to ${job.retentionDays} days`);
1893
+ out.log(` consent: plan ${job.planDigest} \xB7 execution ${job.executionDigest} (${job.executionContract})`);
1894
+ if (job.coverageStatus || job.coverage) printHostedCoverage(out, job);
1895
+ if (job.counts) {
1896
+ out.log(` findings: confirmed=${job.counts.confirmed} needs_reproduction=${job.counts.needsReproduction} candidates=${job.counts.candidates}`);
1897
+ }
1898
+ if (job.errorCode) out.log(` failure: ${job.errorCode}`);
1899
+ const url = new URL("/studio", platform);
1900
+ url.searchParams.set("app", appId);
1901
+ url.searchParams.set("env", job.env);
1902
+ url.searchParams.set("tab", "security");
1903
+ url.searchParams.set("job", job.jobId);
1904
+ out.log(` Studio: ${url.toString()}`);
1905
+ }
1906
+ function printHostedReport(out, report) {
1907
+ out.log(`security report ${report.jobId}: ${report.repository}@${report.revision}`);
1908
+ out.log(` coverage: ${report.coverageStatus} cells=${report.metrics.coverageCells} shallow=${report.metrics.shallowCells} blocked=${report.metrics.blockedCells} unscheduled=${report.metrics.unscheduledCells} budget_exhausted=${report.metrics.budgetExhaustedCells}`);
1909
+ out.log(` findings: confirmed=${report.metrics.confirmed} needs_reproduction=${report.metrics.needsReproduction} candidates=${report.metrics.candidates} rejected=${report.metrics.rejected}`);
1910
+ out.log(` discovery: ${report.provenance.discovery?.provider ?? "unknown"}/${report.provenance.discovery?.model ?? "unknown"}`);
1911
+ out.log(` validation: ${report.provenance.validation?.provider ?? "unknown"}/${report.provenance.validation?.model ?? "unknown"} independent=${String(report.provenance.independentValidation)}`);
1912
+ for (const finding of report.findings) {
1913
+ const location = finding.locations[0];
1914
+ out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
1915
+ }
1916
+ for (const limitation of report.limitations) out.log(` limitation: ${limitation}`);
1917
+ }
1918
+ function enforceHostedReportGate(report, parsed, out, emitSuccess) {
1919
+ const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
1920
+ const candidateValue = parsed.options["fail-on-candidates"];
1921
+ const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
1922
+ const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
1923
+ const confirmed = report.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
1924
+ const leads = failOnCandidates ? report.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
1925
+ const incomplete = report.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
1926
+ if (confirmed.length || leads.length || incomplete) {
1927
+ throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report.coverageStatus}` : ""}`);
1928
+ }
1929
+ if (emitSuccess) {
1930
+ out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report.coverageStatus}. This is not proof that the application is secure.`);
1931
+ }
1932
+ }
1933
+ function printHostedSecurityPlanRoute(out, label, route) {
1934
+ const readiness = route.enabled && route.credentialReady ? "ready" : [
1935
+ route.enabled ? void 0 : "disabled",
1936
+ route.credentialReady ? void 0 : "credential unavailable"
1937
+ ].filter(Boolean).join(", ");
1938
+ out.log(` ${label}: ${route.provider}/${route.model} \xB7 policy v${route.policyVersion} \xB7 ${readiness}`);
1939
+ out.log(` bounds: ${route.maxCallsPerRun} calls/run \xB7 ${route.maxInputBytes} input bytes/call \xB7 ${route.maxOutputTokens} output tokens/call`);
1940
+ }
1941
+ function printHostedCoverage(out, job) {
1942
+ const coverage = job.coverage;
1943
+ out.log(` coverage: ${job.coverageStatus ?? "pending"}${coverage?.completeCells !== void 0 ? ` ${coverage.completeCells}/${coverage.totalCells ?? "?"}` : ""}${coverage?.shallowCells ? ` shallow=${coverage.shallowCells}` : ""}${coverage?.blockedCells ? ` blocked=${coverage.blockedCells}` : ""}${coverage?.unscheduledCells ? ` unscheduled=${coverage.unscheduledCells}` : ""}${coverage?.budgetExhaustedCells ? ` budget_exhausted=${coverage.budgetExhaustedCells}` : ""}`);
1944
+ }
1945
+ function routeLabel(route) {
1946
+ return `${route.provider}/${route.model}${route.policyVersion ? ` policy v${route.policyVersion}` : ""}`;
1947
+ }
1948
+ var HOSTED_SEVERITIES = ["informational", "low", "medium", "high", "critical"];
1949
+ function hostedSeverity(value, flag) {
1950
+ if (HOSTED_SEVERITIES.includes(value)) {
1951
+ return value;
1952
+ }
1953
+ throw new Error(`${flag} must be informational, low, medium, high, or critical`);
1954
+ }
1955
+
1956
+ // src/security-run-command.ts
1957
+ var import_security2 = require("@odla-ai/security");
1958
+
1655
1959
  // src/security.ts
1656
1960
  var import_node_path6 = require("path");
1657
1961
  var import_security = require("@odla-ai/security");
@@ -1756,60 +2060,646 @@ function formatBudget(usage) {
1756
2060
  return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
1757
2061
  }
1758
2062
 
1759
- // src/help.ts
1760
- var import_node_fs7 = require("fs");
1761
- function cliVersion() {
1762
- const pkg = JSON.parse((0, import_node_fs7.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
1763
- return pkg.version ?? "unknown";
2063
+ // src/security-hosted-github.ts
2064
+ var import_node_child_process4 = require("child_process");
2065
+ var import_node_util = require("util");
2066
+
2067
+ // src/security-hosted-request.ts
2068
+ async function requestHostedSecurityJson(options, path, init, action) {
2069
+ const platform = platformAudience(options.platform);
2070
+ const token = hostedSecurityCredential(options.token);
2071
+ const requestInit = {
2072
+ ...init,
2073
+ redirect: "error",
2074
+ credentials: "omit",
2075
+ cache: "no-store",
2076
+ signal: options.signal,
2077
+ headers: {
2078
+ authorization: `Bearer ${token}`,
2079
+ "content-type": "application/json",
2080
+ ...init.headers
2081
+ }
2082
+ };
2083
+ const response = await (options.fetch ?? fetch)(new URL(path, platform), requestInit);
2084
+ const body = await response.json().catch(() => ({}));
2085
+ if (!response.ok) {
2086
+ const code = optionalHostedText(body.error?.code, "error code", 128);
2087
+ const message = optionalHostedText(body.error?.message, "error message", 300);
2088
+ throw new Error(`${action} failed (${response.status})${code ? ` ${code}` : ""}${message ? `: ${message}` : ""}`);
2089
+ }
2090
+ return body;
2091
+ }
2092
+ function hostedIdentifier(value, name) {
2093
+ const result = optionalHostedText(value, name, 180);
2094
+ if (!result || !/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(result)) {
2095
+ throw new Error(`${name} is invalid`);
2096
+ }
2097
+ return result;
2098
+ }
2099
+ function positivePolicyVersion(value, name) {
2100
+ if (!Number.isSafeInteger(value) || value < 1) {
2101
+ throw new Error(`${name} is invalid`);
2102
+ }
2103
+ return value;
2104
+ }
2105
+ function optionalHostedText(value, name, maxLength) {
2106
+ if (value === void 0 || value === null || value === "") return void 0;
2107
+ if (typeof value !== "string" || value.length > maxLength || /[\u0000-\u001f\u007f]/.test(value)) {
2108
+ throw new Error(`${name} is invalid`);
2109
+ }
2110
+ return value;
2111
+ }
2112
+ function githubRepositoryName(value) {
2113
+ if (typeof value !== "string" || value.length > 201) {
2114
+ throw new Error("repository must be owner/name");
2115
+ }
2116
+ const parts = value.split("/");
2117
+ const owner = parts[0] ?? "";
2118
+ const name = (parts[1] ?? "").replace(/\.git$/i, "");
2119
+ if (parts.length !== 2 || !/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,98}[A-Za-z0-9])?$/.test(owner) || !/^[A-Za-z0-9_.-]{1,100}$/.test(name) || name === "." || name === "..") {
2120
+ throw new Error("repository must be owner/name");
2121
+ }
2122
+ return `${owner}/${name}`;
2123
+ }
2124
+ function trustedGitHubInstallUrl(value) {
2125
+ let url;
2126
+ try {
2127
+ url = new URL(value);
2128
+ } catch {
2129
+ throw new Error("odla.ai returned an invalid GitHub installation URL");
2130
+ }
2131
+ if (url.protocol !== "https:" || url.hostname !== "github.com" || url.username || url.password) {
2132
+ throw new Error("odla.ai returned an untrusted GitHub installation URL");
2133
+ }
2134
+ return url.toString();
2135
+ }
2136
+ function hostedPollInterval(value = 2e3) {
2137
+ if (!Number.isSafeInteger(value) || value < 100 || value > 3e4) {
2138
+ throw new Error("poll interval must be 100-30000ms");
2139
+ }
2140
+ return value;
2141
+ }
2142
+ function hostedPollTimeout(value = 10 * 6e4) {
2143
+ if (!Number.isSafeInteger(value) || value < 1e3 || value > 60 * 6e4) {
2144
+ throw new Error("poll timeout must be 1000-3600000ms");
2145
+ }
2146
+ return value;
2147
+ }
2148
+ async function waitForHostedPoll(milliseconds, signal) {
2149
+ if (signal?.aborted) throw signal.reason ?? new DOMException("aborted", "AbortError");
2150
+ await new Promise((resolve7, reject) => {
2151
+ const timer = setTimeout(resolve7, milliseconds);
2152
+ signal?.addEventListener("abort", () => {
2153
+ clearTimeout(timer);
2154
+ reject(signal.reason ?? new DOMException("aborted", "AbortError"));
2155
+ }, { once: true });
2156
+ });
2157
+ }
2158
+ function isValidHostedSecurityPlan(value, env) {
2159
+ if (!value || value.env !== env || !/^sha256:[a-f0-9]{64}$/.test(value.planDigest) || value.consentContract !== "odla.hosted-security-consent.v1" || value.reportProjection !== "odla.hosted-security-report.v1" || value.redactionContract !== "odla.best-effort-credential-pattern-redaction.v1" || typeof value.promptBundle !== "string" || value.promptBundle.length < 1 || value.promptBundle.length > 100 || typeof value.ready !== "boolean" || typeof value.independent !== "boolean" || value.sourceDisclosure !== "redacted" || value.targetExecution !== false || !Number.isSafeInteger(value.reportRetentionDays) || value.reportRetentionDays < 1) return false;
2160
+ const validRoute = (route, purpose) => !!route && route.purpose === purpose && typeof route.enabled === "boolean" && typeof route.credentialReady === "boolean" && typeof route.provider === "string" && route.provider.length > 0 && route.provider.length <= 100 && typeof route.model === "string" && route.model.length > 0 && route.model.length <= 200 && Number.isSafeInteger(route.policyVersion) && route.policyVersion >= 1 && Number.isSafeInteger(route.maxCallsPerRun) && route.maxCallsPerRun >= 1 && Number.isSafeInteger(route.maxInputBytes) && route.maxInputBytes >= 1 && Number.isSafeInteger(route.maxOutputTokens) && route.maxOutputTokens >= 1;
2161
+ return validRoute(value.routes?.discovery, "security.discovery") && validRoute(value.routes?.validation, "security.validation");
2162
+ }
2163
+ function hostedSecurityCredential(value) {
2164
+ if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
2165
+ throw new Error("Hosted security requires an injected odla developer token");
2166
+ }
2167
+ return value;
1764
2168
  }
1765
- function printHelp() {
1766
- console.log(`odla-ai
1767
2169
 
1768
- Usage:
1769
- odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
1770
- odla-ai init --app-id <id> --name <name> [--services db,ai,o11y] [--env dev --env prod]
1771
- odla-ai doctor [--config odla.config.mjs]
1772
- odla-ai capabilities [--json]
1773
- odla-ai admin ai show [--platform https://odla.ai] [--json]
1774
- odla-ai admin ai models [--provider <id>] [--json]
1775
- odla-ai admin ai set <purpose> [--provider <id>] [--model <id>] [--enabled|--no-enabled]
1776
- odla-ai admin ai set security --discovery-provider <id> --discovery-model <id>
1777
- --validation-provider <id> --validation-model <id> [--enabled|--no-enabled]
1778
- odla-ai admin ai credentials [--json]
1779
- odla-ai admin ai credential set <provider> (--from-env <NAME>|--stdin)
1780
- odla-ai admin ai usage [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
1781
- odla-ai admin ai audit [--limit <1-200>] [--json]
1782
- odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
1783
- odla-ai security run [target] --self --ack-redacted-source
1784
- odla-ai provision [--config odla.config.mjs] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
1785
- odla-ai smoke [--config odla.config.mjs] [--env dev]
1786
- odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
1787
- odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
1788
- odla-ai version
2170
+ // src/security-hosted-github.ts
2171
+ async function connectGitHubSecuritySource(options) {
2172
+ const out = options.stdout ?? console;
2173
+ const repository = githubRepositoryName(options.repository);
2174
+ const attempt = await requestHostedSecurityJson(
2175
+ options,
2176
+ "/registry/github/connect",
2177
+ {
2178
+ method: "POST",
2179
+ body: JSON.stringify({
2180
+ appId: hostedIdentifier(options.appId, "appId"),
2181
+ env: hostedIdentifier(options.env, "env"),
2182
+ repository
2183
+ })
2184
+ },
2185
+ "start GitHub connection"
2186
+ );
2187
+ const serverExpiry = Date.parse(attempt?.expiresAt ?? "");
2188
+ if (!attempt?.attemptId || !attempt.installUrl || !Number.isFinite(serverExpiry)) {
2189
+ throw new Error("odla.ai returned an invalid GitHub connection attempt");
2190
+ }
2191
+ const installUrl = trustedGitHubInstallUrl(attempt.installUrl);
2192
+ out.log(`GitHub approval: ${installUrl}`);
2193
+ if (options.open !== false) {
2194
+ await (options.openInstallUrl ?? openUrl)(installUrl);
2195
+ out.log("github: opened installation approval in your browser");
2196
+ }
2197
+ const interval = hostedPollInterval(options.pollIntervalMs);
2198
+ const now = options.now ?? Date.now;
2199
+ const deadline = Math.min(serverExpiry, now() + hostedPollTimeout(options.pollTimeoutMs));
2200
+ const wait = options.wait ?? waitForHostedPoll;
2201
+ while (true) {
2202
+ const state = await requestHostedSecurityJson(
2203
+ options,
2204
+ `/registry/github/connect/${encodeURIComponent(attempt.attemptId)}`,
2205
+ {},
2206
+ "check GitHub connection"
2207
+ );
2208
+ if (state.status !== "pending") {
2209
+ if (state.status === "connected") return state;
2210
+ throw new Error(`GitHub connection ${state.status}${state.failureCode ? `: ${state.failureCode}` : ""}`);
2211
+ }
2212
+ if (now() >= deadline) throw new Error("GitHub connection approval timed out");
2213
+ await wait(Math.min(interval, Math.max(0, deadline - now())), options.signal);
2214
+ }
2215
+ }
2216
+ async function listGitHubSecuritySources(options) {
2217
+ const appId = hostedIdentifier(options.appId, "appId");
2218
+ const env = hostedIdentifier(options.env, "env");
2219
+ const body = await requestHostedSecurityJson(
2220
+ options,
2221
+ `/registry/apps/${encodeURIComponent(appId)}/github/sources?env=${encodeURIComponent(env)}`,
2222
+ {},
2223
+ "list GitHub security sources"
2224
+ );
2225
+ return Array.isArray(body.sources) ? body.sources : [];
2226
+ }
2227
+ async function disconnectGitHubSecuritySource(options) {
2228
+ const appId = hostedIdentifier(options.appId, "appId");
2229
+ const sourceId = hostedIdentifier(options.sourceId, "sourceId");
2230
+ await requestHostedSecurityJson(
2231
+ options,
2232
+ `/registry/apps/${encodeURIComponent(appId)}/github/sources/${encodeURIComponent(sourceId)}`,
2233
+ { method: "DELETE" },
2234
+ "disconnect GitHub security source"
2235
+ );
2236
+ }
2237
+ function repositoryFromGitRemote(remoteInput) {
2238
+ const remote = remoteInput.trim();
2239
+ const scp = /^git@github\.com:([^/\s]+)\/([^/\s]+?)\/?$/.exec(remote);
2240
+ if (scp) return githubRepositoryName(`${scp[1]}/${scp[2].replace(/\.git$/, "")}`);
2241
+ let url;
2242
+ try {
2243
+ url = new URL(remote);
2244
+ } catch {
2245
+ throw new Error("origin must be a github.com HTTPS or SSH repository URL");
2246
+ }
2247
+ if (url.hostname !== "github.com" || url.protocol !== "https:" && url.protocol !== "ssh:") {
2248
+ throw new Error("origin must be a github.com HTTPS or SSH repository URL");
2249
+ }
2250
+ if (url.password || url.protocol === "https:" && url.username || url.search || url.hash) {
2251
+ throw new Error("credential-bearing GitHub origin URLs are not accepted");
2252
+ }
2253
+ const parts = url.pathname.replace(/^\/+|\/+$/g, "").split("/");
2254
+ if (parts.length !== 2) {
2255
+ throw new Error("origin must identify one GitHub owner/name repository");
2256
+ }
2257
+ return githubRepositoryName(`${parts[0]}/${parts[1].replace(/\.git$/, "")}`);
2258
+ }
2259
+ async function inferGitHubRepository(cwd = process.cwd(), readOrigin = defaultReadOrigin) {
2260
+ let remote;
2261
+ try {
2262
+ remote = await readOrigin(cwd);
2263
+ } catch {
2264
+ throw new Error("Could not read git origin; pass --repo owner/name");
2265
+ }
2266
+ return repositoryFromGitRemote(remote);
2267
+ }
2268
+ async function defaultReadOrigin(cwd) {
2269
+ const result = await (0, import_node_util.promisify)(import_node_child_process4.execFile)(
2270
+ "git",
2271
+ ["remote", "get-url", "origin"],
2272
+ { cwd, encoding: "utf8" }
2273
+ );
2274
+ return result.stdout;
2275
+ }
1789
2276
 
1790
- Commands:
1791
- setup Install offline odla runbooks for common coding-agent harnesses.
1792
- init Create a generic odla.config.mjs plus starter schema/rules files.
1793
- doctor Validate and summarize the project config without network calls.
1794
- capabilities Show what the CLI automates vs agent edits and human checkpoints.
1795
- admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
1796
- security Run hosted discovery + independent validation with app/run attribution.
1797
- provision Register services, configure them, persist credentials, optionally push secrets.
1798
- smoke Verify local credentials, public-config, live schema, and db aggregate.
1799
- skill Same installer; --agent accepts all, claude, codex, cursor,
1800
- copilot, gemini, or agents (repeatable or comma-separated).
1801
- secrets Push configured db/o11y secrets into the Worker via wrangler stdin.
1802
- version Print the CLI version.
2277
+ // src/security-hosted-intent.ts
2278
+ async function getHostedSecurityIntent(options) {
2279
+ const appId = hostedIdentifier(options.appId, "appId");
2280
+ const env = hostedIdentifier(options.env, "env");
2281
+ const sourceId = hostedIdentifier(options.sourceId, "sourceId");
2282
+ const ref = optionalHostedText(options.ref, "ref", 255);
2283
+ const query = new URLSearchParams({ env, sourceId });
2284
+ if (ref) query.set("ref", ref);
2285
+ if (options.profile) query.set("profile", options.profile);
2286
+ const response = await requestHostedSecurityJson(
2287
+ options,
2288
+ `/registry/apps/${encodeURIComponent(appId)}/security/intent?${query.toString()}`,
2289
+ {},
2290
+ "read hosted security execution intent"
2291
+ );
2292
+ if (!isValidHostedSecurityPlan(response.plan, env) || !isValidIntent(response.intent, { appId, env, sourceId })) {
2293
+ throw new Error("odla.ai returned an invalid hosted security execution intent");
2294
+ }
2295
+ if (response.intent.planDigest !== response.plan.planDigest) {
2296
+ throw new Error("odla.ai returned a hosted security intent for a different processing plan");
2297
+ }
2298
+ return response;
2299
+ }
2300
+ function isValidIntent(value, expected) {
2301
+ return !!value && value.executionContract === "odla.hosted-security-execution-consent.v1" && /^sha256:[a-f0-9]{64}$/.test(value.executionDigest) && /^sha256:[a-f0-9]{64}$/.test(value.planDigest) && value.appId === expected.appId && value.env === expected.env && value.sourceId === expected.sourceId && typeof value.repository === "string" && value.repository.length > 2 && value.repository.length <= 201 && typeof value.defaultBranch === "string" && value.defaultBranch.length > 0 && value.defaultBranch.length <= 255 && typeof value.requestedRef === "string" && value.requestedRef.length > 0 && value.requestedRef.length <= 255 && !/[\u0000-\u001f\u007f]/.test(value.requestedRef) && ["odla", "cloudflare-app", "generic"].includes(value.profile) && value.sourceDisclosure === "redacted";
2302
+ }
1803
2303
 
1804
- Safety:
1805
- New projects target dev only. Add prod explicitly to odla.config.mjs and pass
1806
- --yes to provision it; use --dry-run first to inspect the resolved plan.
1807
- Provision caches the approved developer token and service credentials under
1808
- .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
1809
- preflights Wrangler before any shown-once issuance or destructive rotation.
1810
- Provision opens the approval page automatically in interactive terminals;
1811
- use --open to force browser launch or --no-open to suppress it.
1812
- `);
2304
+ // src/security-hosted-jobs.ts
2305
+ async function getHostedSecurityPlan(options) {
2306
+ const appId = hostedIdentifier(options.appId, "appId");
2307
+ const env = hostedIdentifier(options.env, "env");
2308
+ const plan = await requestHostedSecurityJson(
2309
+ options,
2310
+ `/registry/apps/${encodeURIComponent(appId)}/security/plan?env=${encodeURIComponent(env)}`,
2311
+ {},
2312
+ "read hosted security disclosure plan"
2313
+ );
2314
+ if (!isValidHostedSecurityPlan(plan, env)) {
2315
+ throw new Error("odla.ai returned an invalid hosted security disclosure plan");
2316
+ }
2317
+ return plan;
2318
+ }
2319
+ async function startHostedSecurityJob(options) {
2320
+ if (options.sourceDisclosureAck !== "redacted") {
2321
+ throw new Error("Hosted GitHub security requires sourceDisclosureAck=redacted; repository access alone is not disclosure consent");
2322
+ }
2323
+ const appId = hostedIdentifier(options.appId, "appId");
2324
+ const env = hostedIdentifier(options.env, "env");
2325
+ const sourceId = hostedIdentifier(options.sourceId, "sourceId");
2326
+ const ref = optionalHostedText(options.ref, "ref", 255);
2327
+ const expectedPlanDigest = securityPlanDigest(options.expectedPlanDigest);
2328
+ const expectedExecutionDigest = securityExecutionDigest(options.expectedExecutionDigest);
2329
+ const expectedPolicyVersions = {
2330
+ discovery: positivePolicyVersion(
2331
+ options.expectedPolicyVersions?.discovery,
2332
+ "expected discovery policy version"
2333
+ ),
2334
+ validation: positivePolicyVersion(
2335
+ options.expectedPolicyVersions?.validation,
2336
+ "expected validation policy version"
2337
+ )
2338
+ };
2339
+ let body;
2340
+ try {
2341
+ body = await requestHostedSecurityJson(
2342
+ options,
2343
+ `/registry/apps/${encodeURIComponent(appId)}/security/jobs`,
2344
+ {
2345
+ method: "POST",
2346
+ body: JSON.stringify({
2347
+ env,
2348
+ sourceId,
2349
+ ...ref ? { ref } : {},
2350
+ ...options.profile ? { profile: options.profile } : {},
2351
+ ...options.clientRequestId ? { clientRequestId: hostedIdentifier(options.clientRequestId, "clientRequestId") } : {},
2352
+ sourceDisclosure: "redacted",
2353
+ expectedPlanDigest,
2354
+ expectedExecutionDigest,
2355
+ expectedPolicyVersions
2356
+ })
2357
+ },
2358
+ "start hosted security job"
2359
+ );
2360
+ } catch (error) {
2361
+ const message = error instanceof Error ? error.message : String(error);
2362
+ if (/\b(?:plan_conflict|policy_conflict|intent_conflict|security_ai_not_ready)\b/.test(message)) {
2363
+ throw new Error("Hosted security plan or execution intent changed or became unavailable after review; fetch a fresh intent and explicitly acknowledge its digest before enqueueing again", { cause: error });
2364
+ }
2365
+ throw error;
2366
+ }
2367
+ if (!body.job?.jobId) {
2368
+ throw new Error("odla.ai returned an invalid hosted security job");
2369
+ }
2370
+ return body.job;
2371
+ }
2372
+ async function getHostedSecurityJob(options) {
2373
+ const body = await requestHostedSecurityJson(
2374
+ options,
2375
+ hostedJobPath(options.appId, options.jobId),
2376
+ {},
2377
+ "read hosted security job"
2378
+ );
2379
+ if (!body.job?.jobId) throw new Error("odla.ai returned an invalid hosted security job");
2380
+ return body.job;
2381
+ }
2382
+ async function followHostedSecurityJob(options) {
2383
+ const interval = hostedPollInterval(options.pollIntervalMs);
2384
+ const now = options.now ?? Date.now;
2385
+ const deadline = now() + hostedPollTimeout(options.pollTimeoutMs ?? 60 * 6e4);
2386
+ const wait = options.wait ?? waitForHostedPoll;
2387
+ let prior = "";
2388
+ while (true) {
2389
+ const job = await getHostedSecurityJob(options);
2390
+ const fingerprint = `${job.status}:${job.updatedAt}`;
2391
+ if (fingerprint !== prior) options.onUpdate?.(Object.freeze({ ...job }));
2392
+ prior = fingerprint;
2393
+ if (isTerminalHostedSecurityStatus(job.status)) return job;
2394
+ if (now() >= deadline) {
2395
+ throw new Error(`Hosted security job ${job.jobId} did not finish before the polling deadline`);
2396
+ }
2397
+ await wait(Math.min(interval, Math.max(0, deadline - now())), options.signal);
2398
+ }
2399
+ }
2400
+ async function getHostedSecurityReport(options) {
2401
+ return requestHostedSecurityJson(
2402
+ options,
2403
+ `${hostedJobPath(options.appId, options.jobId)}/report`,
2404
+ {},
2405
+ "read hosted security report"
2406
+ );
2407
+ }
2408
+ function isTerminalHostedSecurityStatus(status) {
2409
+ return status === "completed" || status === "failed" || status === "cancelled";
2410
+ }
2411
+ function hostedJobPath(appIdInput, jobIdInput) {
2412
+ const appId = hostedIdentifier(appIdInput, "appId");
2413
+ const jobId = hostedIdentifier(jobIdInput, "jobId");
2414
+ return `/registry/apps/${encodeURIComponent(appId)}/security/jobs/${encodeURIComponent(jobId)}`;
2415
+ }
2416
+ function securityPlanDigest(value) {
2417
+ if (!/^sha256:[a-f0-9]{64}$/.test(value)) {
2418
+ throw new Error("expected plan digest must be a sha256 digest from security plan");
2419
+ }
2420
+ return value;
2421
+ }
2422
+ function securityExecutionDigest(value) {
2423
+ if (!/^sha256:[a-f0-9]{64}$/.test(value)) {
2424
+ throw new Error("expected execution digest must be a sha256 digest from security intent");
2425
+ }
2426
+ return value;
2427
+ }
2428
+
2429
+ // src/security-run-command.ts
2430
+ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
2431
+ assertArgs(parsed, [
2432
+ "config",
2433
+ "env",
2434
+ "profile",
2435
+ "platform",
2436
+ "source",
2437
+ "ref",
2438
+ "follow",
2439
+ "open",
2440
+ "json",
2441
+ "ack-redacted-source",
2442
+ "plan-digest",
2443
+ "fail-on",
2444
+ "fail-on-candidates",
2445
+ "allow-incomplete"
2446
+ ], 2);
2447
+ const follow = parsed.options.follow !== false;
2448
+ if (!follow && (parsed.options["fail-on"] !== void 0 || parsed.options["fail-on-candidates"] !== void 0 || parsed.options["allow-incomplete"] !== void 0)) {
2449
+ throw new Error("hosted security gate options require following the job; remove --no-follow");
2450
+ }
2451
+ const acknowledgedDigest = requiredString(parsed.options["plan-digest"], "--plan-digest");
2452
+ const context = await hostedSecurityContext(parsed, dependencies);
2453
+ const plan = await getHostedSecurityPlan(context);
2454
+ if (parsed.options.json !== true) printHostedSecurityPlan(context.stdout, plan, context.appId);
2455
+ assertHostedSecurityPlanReady(plan);
2456
+ if (acknowledgedDigest !== plan.planDigest) {
2457
+ throw new Error(`--plan-digest does not match the current hosted security plan (${plan.planDigest}); review and explicitly acknowledge the current digest`);
2458
+ }
2459
+ const requestedRef = stringOpt(parsed.options.ref);
2460
+ const requestedProfile = securityProfile(stringOpt(parsed.options.profile));
2461
+ const preview = await getHostedSecurityIntent({
2462
+ ...context,
2463
+ sourceId,
2464
+ ref: requestedRef,
2465
+ profile: requestedProfile
2466
+ });
2467
+ if (parsed.options.json !== true) printHostedSecurityIntent(context.stdout, preview.intent);
2468
+ if (preview.plan.planDigest !== plan.planDigest || preview.intent.planDigest !== plan.planDigest) {
2469
+ throw new Error(`hosted security plan changed while previewing execution (${preview.plan.planDigest}); review and explicitly acknowledge the current plan digest`);
2470
+ }
2471
+ const job = await startHostedSecurityJob({
2472
+ ...context,
2473
+ sourceId,
2474
+ ref: requestedRef,
2475
+ profile: requestedProfile,
2476
+ clientRequestId: crypto.randomUUID(),
2477
+ sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
2478
+ expectedPlanDigest: plan.planDigest,
2479
+ expectedExecutionDigest: preview.intent.executionDigest,
2480
+ expectedPolicyVersions: {
2481
+ discovery: plan.routes.discovery.policyVersion,
2482
+ validation: plan.routes.validation.policyVersion
2483
+ }
2484
+ });
2485
+ const result = follow ? await followHostedSecurityJob({
2486
+ ...context,
2487
+ jobId: job.jobId,
2488
+ wait: dependencies.pollWait,
2489
+ onUpdate: parsed.options.json === true ? void 0 : (value) => printHostedJob(context.stdout, value, context.platform, context.appId)
2490
+ }) : job;
2491
+ if (!follow) {
2492
+ if (parsed.options.json === true) {
2493
+ context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result }, null, 2));
2494
+ } else {
2495
+ printHostedJob(context.stdout, result, context.platform, context.appId);
2496
+ }
2497
+ return;
2498
+ }
2499
+ if (result.status !== "completed") {
2500
+ if (parsed.options.json === true) {
2501
+ context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result }, null, 2));
2502
+ }
2503
+ throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
2504
+ }
2505
+ const report = await getHostedSecurityReport({ ...context, jobId: result.jobId });
2506
+ if (parsed.options.json === true) {
2507
+ context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report }, null, 2));
2508
+ } else {
2509
+ printHostedReport(context.stdout, report);
2510
+ }
2511
+ enforceHostedReportGate(report, parsed, context.stdout, parsed.options.json !== true);
2512
+ }
2513
+ async function runLocalSecurityCommand(parsed, dependencies) {
2514
+ if (parsed.options.source === true) {
2515
+ throw new Error("--source requires a source id; run security sources first");
2516
+ }
2517
+ assertArgs(parsed, [
2518
+ "config",
2519
+ "env",
2520
+ "out",
2521
+ "profile",
2522
+ "max-hunt-tasks",
2523
+ "run-id",
2524
+ "platform",
2525
+ "self",
2526
+ "ack-redacted-source",
2527
+ "open",
2528
+ "fail-on",
2529
+ "fail-on-candidates",
2530
+ "allow-incomplete"
2531
+ ], 3);
2532
+ const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
2533
+ const selfAudit = parsed.options.self === true;
2534
+ const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
2535
+ const platform = stringOpt(parsed.options.platform);
2536
+ const doFetch = dependencies.fetch ?? fetch;
2537
+ const out = dependencies.stdout ?? console;
2538
+ const result = await runHostedSecurity({
2539
+ configPath,
2540
+ selfAudit,
2541
+ target: parsed.positionals[2],
2542
+ out: stringOpt(parsed.options.out),
2543
+ env: stringOpt(parsed.options.env),
2544
+ profile: securityProfile(stringOpt(parsed.options.profile)),
2545
+ maxHuntTasks: numberOpt(parsed.options["max-hunt-tasks"], "--max-hunt-tasks"),
2546
+ runId: stringOpt(parsed.options["run-id"]),
2547
+ platform,
2548
+ sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
2549
+ fetch: doFetch,
2550
+ stdout: out,
2551
+ getToken: async (request) => {
2552
+ if (request.scope === "platform:security:self") {
2553
+ return getScopedPlatformToken({
2554
+ platform: request.platform,
2555
+ scope: request.scope,
2556
+ open,
2557
+ fetch: doFetch,
2558
+ stdout: out,
2559
+ openApprovalUrl: dependencies.openUrl
2560
+ });
2561
+ }
2562
+ const cfg = await loadProjectConfig(configPath);
2563
+ if (platformAudience(cfg.platformUrl) !== platformAudience(request.platform)) {
2564
+ throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
2565
+ }
2566
+ return getDeveloperToken(
2567
+ cfg,
2568
+ { configPath, open, openApprovalUrl: dependencies.openUrl },
2569
+ doFetch,
2570
+ out
2571
+ );
2572
+ }
2573
+ });
2574
+ enforceLocalGate(result.report, parsed);
2575
+ }
2576
+ function enforceLocalGate(report, parsed) {
2577
+ const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
2578
+ const candidateValue = parsed.options["fail-on-candidates"];
2579
+ const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
2580
+ const confirmed = (0, import_security2.findingsAtOrAbove)(report, failOn);
2581
+ const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
2582
+ const incomplete = report.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
2583
+ if (confirmed.length || leads.length || incomplete) {
2584
+ throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
2585
+ }
2586
+ }
2587
+ function severityOpt(value, flag) {
2588
+ if (["informational", "low", "medium", "high", "critical"].includes(value)) {
2589
+ return value;
2590
+ }
2591
+ throw new Error(`${flag} must be informational, low, medium, high, or critical`);
2592
+ }
2593
+
2594
+ // src/security-command.ts
2595
+ async function securityCommand(parsed, dependencies) {
2596
+ const sub = parsed.positionals[1];
2597
+ if (sub === "github") {
2598
+ await githubSecurityCommand(parsed, dependencies);
2599
+ return;
2600
+ }
2601
+ if (sub === "plan") {
2602
+ assertArgs(parsed, ["config", "env", "platform", "open", "json"], 2);
2603
+ const context = await hostedSecurityContext(parsed, dependencies);
2604
+ const plan = await getHostedSecurityPlan(context);
2605
+ if (parsed.options.json === true) context.stdout.log(JSON.stringify(plan, null, 2));
2606
+ else printHostedSecurityPlan(context.stdout, plan, context.appId);
2607
+ return;
2608
+ }
2609
+ if (sub === "sources") {
2610
+ await listSecuritySources(parsed, dependencies);
2611
+ return;
2612
+ }
2613
+ if (sub === "status") {
2614
+ await securityStatus(parsed, dependencies);
2615
+ return;
2616
+ }
2617
+ if (sub === "report") {
2618
+ assertArgs(parsed, ["config", "env", "platform", "open", "json"], 3);
2619
+ const jobId = requiredSecurityPositional(parsed, 2, "job id");
2620
+ const context = await hostedSecurityContext(parsed, dependencies);
2621
+ const report = await getHostedSecurityReport({ ...context, jobId });
2622
+ if (parsed.options.json === true) context.stdout.log(JSON.stringify(report, null, 2));
2623
+ else printHostedReport(context.stdout, report);
2624
+ return;
2625
+ }
2626
+ if (sub !== "run") {
2627
+ throw new Error('unknown security command. Try "odla-ai security plan", "security sources", or "security run".');
2628
+ }
2629
+ const sourceId = stringOpt(parsed.options.source);
2630
+ if (sourceId) await runSourceSecurityCommand(parsed, dependencies, sourceId);
2631
+ else await runLocalSecurityCommand(parsed, dependencies);
2632
+ }
2633
+ async function githubSecurityCommand(parsed, dependencies) {
2634
+ const action = parsed.positionals[2];
2635
+ if (action === "disconnect") {
2636
+ assertArgs(parsed, ["config", "env", "platform", "source", "open", "yes"], 3);
2637
+ const context2 = await hostedSecurityContext(parsed, dependencies);
2638
+ const sourceId = requiredString(parsed.options.source, "--source");
2639
+ const confirmed = parsed.options.yes === true || await interactiveConfirmation(
2640
+ `Disconnect GitHub security source ${sourceId} from ${context2.appId}/${context2.env}?`,
2641
+ dependencies
2642
+ );
2643
+ if (!confirmed) {
2644
+ throw new Error("GitHub source disconnect cancelled; pass --yes in a non-interactive shell");
2645
+ }
2646
+ await disconnectGitHubSecuritySource({ ...context2, sourceId });
2647
+ context2.stdout.log(`github: disconnected ${sourceId} from ${context2.appId}/${context2.env}`);
2648
+ return;
2649
+ }
2650
+ if (action !== "connect") {
2651
+ throw new Error('unknown security github command. Try "odla-ai security github connect".');
2652
+ }
2653
+ assertArgs(parsed, ["config", "env", "platform", "repo", "open"], 3);
2654
+ const context = await hostedSecurityContext(parsed, dependencies);
2655
+ const repository = stringOpt(parsed.options.repo) ?? await inferGitHubRepository(process.cwd(), dependencies.readGitOrigin);
2656
+ const connection = await connectGitHubSecuritySource({
2657
+ ...context,
2658
+ repository,
2659
+ open: parsed.options.open !== false,
2660
+ openInstallUrl: dependencies.openUrl ?? openUrl,
2661
+ wait: dependencies.pollWait,
2662
+ stdout: context.stdout
2663
+ });
2664
+ context.stdout.log(`github: connected ${connection.repository ?? repository} (${connection.sourceId ?? "source pending"})`);
2665
+ context.stdout.log("github: odla.ai stores the installation; no PAT or GitHub token is written locally");
2666
+ }
2667
+ async function listSecuritySources(parsed, dependencies) {
2668
+ assertArgs(parsed, ["config", "env", "platform", "open", "json"], 2);
2669
+ const context = await hostedSecurityContext(parsed, dependencies);
2670
+ const [plan, sources] = await Promise.all([
2671
+ getHostedSecurityPlan(context),
2672
+ listGitHubSecuritySources(context)
2673
+ ]);
2674
+ if (parsed.options.json === true) {
2675
+ context.stdout.log(JSON.stringify({ plan, sources }, null, 2));
2676
+ return;
2677
+ }
2678
+ printHostedSecurityPlan(context.stdout, plan, context.appId);
2679
+ if (!sources.length) {
2680
+ context.stdout.log(`No GitHub security sources connected for ${context.appId}/${context.env}.`);
2681
+ return;
2682
+ }
2683
+ context.stdout.log(`GitHub security sources for ${context.appId}/${context.env}
2684
+ source repository default ref status`);
2685
+ for (const source of sources) {
2686
+ context.stdout.log(`${source.sourceId} ${source.repository} ${source.defaultBranch} ${source.status}`);
2687
+ }
2688
+ }
2689
+ async function securityStatus(parsed, dependencies) {
2690
+ assertArgs(parsed, ["config", "env", "platform", "open", "json", "follow"], 3);
2691
+ const jobId = requiredSecurityPositional(parsed, 2, "job id");
2692
+ const context = await hostedSecurityContext(parsed, dependencies);
2693
+ const job = parsed.options.follow === true ? await followHostedSecurityJob({
2694
+ ...context,
2695
+ jobId,
2696
+ wait: dependencies.pollWait,
2697
+ onUpdate: parsed.options.json === true ? void 0 : (value) => printHostedJob(context.stdout, value, context.platform, context.appId)
2698
+ }) : await getHostedSecurityJob({ ...context, jobId });
2699
+ if (parsed.options.json === true) context.stdout.log(JSON.stringify(job, null, 2));
2700
+ else if (parsed.options.follow !== true) {
2701
+ printHostedJob(context.stdout, job, context.platform, context.appId);
2702
+ }
1813
2703
  }
1814
2704
 
1815
2705
  // src/skill.ts
@@ -2147,7 +3037,7 @@ async function safeText3(res) {
2147
3037
  }
2148
3038
 
2149
3039
  // src/cli.ts
2150
- async function runCli(argv = process.argv.slice(2)) {
3040
+ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
2151
3041
  const parsed = parseArgv(argv);
2152
3042
  const command = parsed.positionals[0] ?? "help";
2153
3043
  if (command === "version" || command === "-v" || parsed.options.version === true) {
@@ -2161,7 +3051,15 @@ async function runCli(argv = process.argv.slice(2)) {
2161
3051
  return;
2162
3052
  }
2163
3053
  if (command === "init") {
2164
- assertArgs(parsed, ["app-id", "name", "config", "env", "services", "ai-provider", "force"], 1);
3054
+ assertArgs(parsed, [
3055
+ "app-id",
3056
+ "name",
3057
+ "config",
3058
+ "env",
3059
+ "services",
3060
+ "ai-provider",
3061
+ "force"
3062
+ ], 1);
2165
3063
  initProject({
2166
3064
  appId: requiredString(parsed.options["app-id"], "--app-id"),
2167
3065
  name: requiredString(parsed.options.name, "--name"),
@@ -2184,149 +3082,15 @@ async function runCli(argv = process.argv.slice(2)) {
2184
3082
  return;
2185
3083
  }
2186
3084
  if (command === "admin") {
2187
- const area = parsed.positionals[1];
2188
- const action = parsed.positionals[2];
2189
- const credentialSet = action === "credential" && parsed.positionals[3] === "set";
2190
- const credentials = action === "credentials";
2191
- const models = action === "models";
2192
- const usage = action === "usage";
2193
- const audit = action === "audit";
2194
- if (area !== "ai" || action !== "show" && action !== "set" && !credentialSet && !credentials && !models && !usage && !audit) {
2195
- throw new Error('unknown admin command. Try "odla-ai admin ai show".');
2196
- }
2197
- assertArgs(parsed, [
2198
- "platform",
2199
- "json",
2200
- "open",
2201
- "provider",
2202
- "model",
2203
- "enabled",
2204
- "max-input-bytes",
2205
- "max-output-tokens",
2206
- "max-calls-per-run",
2207
- "from-env",
2208
- "stdin",
2209
- "discovery-provider",
2210
- "discovery-model",
2211
- "validation-provider",
2212
- "validation-model",
2213
- "app-id",
2214
- "env",
2215
- "run-id",
2216
- "limit"
2217
- ], credentialSet ? 5 : action === "set" ? 4 : 3);
2218
- await adminAi({
2219
- action: credentialSet ? "credential-set" : credentials ? "credentials" : models ? "models" : usage ? "usage" : audit ? "audit" : action,
2220
- purpose: action === "set" ? parsed.positionals[3] : void 0,
2221
- provider: stringOpt(parsed.options.provider),
2222
- model: stringOpt(parsed.options.model),
2223
- enabled: boolOpt(parsed.options.enabled),
2224
- maxInputBytes: numberOpt(parsed.options["max-input-bytes"], "--max-input-bytes"),
2225
- maxOutputTokens: numberOpt(parsed.options["max-output-tokens"], "--max-output-tokens"),
2226
- maxCallsPerRun: numberOpt(parsed.options["max-calls-per-run"], "--max-calls-per-run"),
2227
- platform: stringOpt(parsed.options.platform),
2228
- json: parsed.options.json === true,
2229
- open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
2230
- credentialProvider: credentialSet ? parsed.positionals[4] : void 0,
2231
- fromEnv: stringOpt(parsed.options["from-env"]),
2232
- stdin: parsed.options.stdin === true,
2233
- discoveryProvider: stringOpt(parsed.options["discovery-provider"]),
2234
- discoveryModel: stringOpt(parsed.options["discovery-model"]),
2235
- validationProvider: stringOpt(parsed.options["validation-provider"]),
2236
- validationModel: stringOpt(parsed.options["validation-model"]),
2237
- appId: stringOpt(parsed.options["app-id"]),
2238
- env: stringOpt(parsed.options.env),
2239
- runId: stringOpt(parsed.options["run-id"]),
2240
- limit: numberOpt(parsed.options.limit, "--limit")
2241
- });
3085
+ await adminCommand(parsed);
2242
3086
  return;
2243
3087
  }
2244
3088
  if (command === "security") {
2245
- const sub = parsed.positionals[1];
2246
- if (sub !== "run") throw new Error('unknown security command. Try "odla-ai security run".');
2247
- assertArgs(parsed, [
2248
- "config",
2249
- "env",
2250
- "out",
2251
- "profile",
2252
- "max-hunt-tasks",
2253
- "run-id",
2254
- "platform",
2255
- "self",
2256
- "ack-redacted-source",
2257
- "open",
2258
- "fail-on",
2259
- "fail-on-candidates",
2260
- "allow-incomplete"
2261
- ], 3);
2262
- const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
2263
- const selfAudit = parsed.options.self === true;
2264
- const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
2265
- const platform = stringOpt(parsed.options.platform);
2266
- const result = await runHostedSecurity({
2267
- configPath,
2268
- selfAudit,
2269
- target: parsed.positionals[2],
2270
- out: stringOpt(parsed.options.out),
2271
- env: stringOpt(parsed.options.env),
2272
- profile: securityProfile(stringOpt(parsed.options.profile)),
2273
- maxHuntTasks: numberOpt(parsed.options["max-hunt-tasks"], "--max-hunt-tasks"),
2274
- runId: stringOpt(parsed.options["run-id"]),
2275
- platform,
2276
- sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
2277
- getToken: async (request) => {
2278
- if (request.scope === "platform:security:self") {
2279
- return getScopedPlatformToken({ platform: request.platform, scope: request.scope, open });
2280
- }
2281
- const cfg = await loadProjectConfig(configPath);
2282
- if (platformAudience(cfg.platformUrl) !== platformAudience(request.platform)) {
2283
- throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
2284
- }
2285
- return getDeveloperToken(cfg, { configPath, open }, fetch, console);
2286
- }
2287
- });
2288
- const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
2289
- const candidateValue = parsed.options["fail-on-candidates"];
2290
- const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
2291
- const confirmed = (0, import_security2.findingsAtOrAbove)(result.report, failOn);
2292
- const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(result.report, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
2293
- const incomplete = result.report.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
2294
- if (confirmed.length || leads.length || incomplete) {
2295
- throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
2296
- }
3089
+ await securityCommand(parsed, dependencies);
2297
3090
  return;
2298
3091
  }
2299
3092
  if (command === "provision") {
2300
- assertArgs(
2301
- parsed,
2302
- [
2303
- "config",
2304
- "dry-run",
2305
- "rotate-keys",
2306
- "rotate-o11y-token",
2307
- "push-secrets",
2308
- "write-credentials",
2309
- "write-dev-vars",
2310
- "token",
2311
- "open",
2312
- "yes"
2313
- ],
2314
- 1
2315
- );
2316
- const writeDevVars2 = parsed.options["write-dev-vars"];
2317
- const opts = {
2318
- configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
2319
- dryRun: parsed.options["dry-run"] === true,
2320
- rotateKeys: parsed.options["rotate-keys"] === true,
2321
- rotateO11yToken: parsed.options["rotate-o11y-token"] === true,
2322
- pushSecrets: parsed.options["push-secrets"] === true,
2323
- writeCredentials: parsed.options["write-credentials"] !== false,
2324
- writeDevVars: typeof writeDevVars2 === "string" ? writeDevVars2 : writeDevVars2 === true,
2325
- token: stringOpt(parsed.options.token),
2326
- open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
2327
- yes: parsed.options.yes === true
2328
- };
2329
- await provision(opts);
3093
+ await provisionCommand(parsed);
2330
3094
  return;
2331
3095
  }
2332
3096
  if (command === "smoke") {
@@ -2352,7 +3116,9 @@ async function runCli(argv = process.argv.slice(2)) {
2352
3116
  }
2353
3117
  if (command === "skill") {
2354
3118
  const sub = parsed.positionals[1];
2355
- if (sub !== "install") throw new Error(`unknown skill subcommand "${sub ?? ""}". Try "odla-ai skill install".`);
3119
+ if (sub !== "install") {
3120
+ throw new Error(`unknown skill subcommand "${sub ?? ""}". Try "odla-ai skill install".`);
3121
+ }
2356
3122
  assertArgs(parsed, ["dir", "global", "force", "agent", "harness"], 2);
2357
3123
  installSkill({
2358
3124
  dir: stringOpt(parsed.options.dir),
@@ -2364,7 +3130,9 @@ async function runCli(argv = process.argv.slice(2)) {
2364
3130
  }
2365
3131
  if (command === "secrets") {
2366
3132
  const sub = parsed.positionals[1];
2367
- if (sub !== "push") throw new Error(`unknown secrets subcommand "${sub ?? ""}". Try "odla-ai secrets push --env dev".`);
3133
+ if (sub !== "push") {
3134
+ throw new Error(`unknown secrets subcommand "${sub ?? ""}". Try "odla-ai secrets push --env dev".`);
3135
+ }
2368
3136
  assertArgs(parsed, ["config", "env", "dry-run", "yes"], 2);
2369
3137
  await secretsPush({
2370
3138
  configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
@@ -2376,29 +3144,33 @@ async function runCli(argv = process.argv.slice(2)) {
2376
3144
  }
2377
3145
  throw new Error(`unknown command "${command}". Run "odla-ai help".`);
2378
3146
  }
2379
- function securityProfile(value) {
2380
- if (value === void 0) return void 0;
2381
- if (value === "odla" || value === "cloudflare-app" || value === "generic") return value;
2382
- throw new Error("--profile must be odla, cloudflare-app, or generic");
2383
- }
2384
- function severityOpt(value, flag) {
2385
- if (["informational", "low", "medium", "high", "critical"].includes(value)) return value;
2386
- throw new Error(`${flag} must be informational, low, medium, high, or critical`);
2387
- }
2388
- function harnessList(parsed) {
2389
- const selected = [
2390
- ...harnessOption(parsed.options.agent, "--agent"),
2391
- ...harnessOption(parsed.options.harness, "--harness")
2392
- ];
2393
- return selected.length ? selected : ["all"];
2394
- }
2395
- function harnessOption(value, flag) {
2396
- if (value === void 0) return [];
2397
- if (typeof value === "boolean") throw new Error(`${flag} requires a harness name`);
2398
- const raw = Array.isArray(value) ? value : [value];
2399
- const parts = raw.flatMap((item) => item.split(","));
2400
- if (parts.some((item) => !item.trim())) throw new Error(`${flag} requires a harness name`);
2401
- return parts.map((item) => item.trim());
3147
+ async function provisionCommand(parsed) {
3148
+ assertArgs(parsed, [
3149
+ "config",
3150
+ "dry-run",
3151
+ "rotate-keys",
3152
+ "rotate-o11y-token",
3153
+ "push-secrets",
3154
+ "write-credentials",
3155
+ "write-dev-vars",
3156
+ "token",
3157
+ "open",
3158
+ "yes"
3159
+ ], 1);
3160
+ const writeDevVars2 = parsed.options["write-dev-vars"];
3161
+ const options = {
3162
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
3163
+ dryRun: parsed.options["dry-run"] === true,
3164
+ rotateKeys: parsed.options["rotate-keys"] === true,
3165
+ rotateO11yToken: parsed.options["rotate-o11y-token"] === true,
3166
+ pushSecrets: parsed.options["push-secrets"] === true,
3167
+ writeCredentials: parsed.options["write-credentials"] !== false,
3168
+ writeDevVars: typeof writeDevVars2 === "string" ? writeDevVars2 : writeDevVars2 === true,
3169
+ token: stringOpt(parsed.options.token),
3170
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
3171
+ yes: parsed.options.yes === true
3172
+ };
3173
+ await provision(options);
2402
3174
  }
2403
3175
 
2404
3176
  // src/bin.ts