@odla-ai/cli 0.25.21 → 0.25.22
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/README.md +34 -2
- package/REQUIREMENTS.md +6 -0
- package/dist/bin.cjs +1130 -618
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-L2NG4UR4.js → chunk-2AFAXLKE.js} +1043 -529
- package/dist/chunk-2AFAXLKE.js.map +1 -0
- package/dist/index.cjs +1138 -618
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +169 -6
- package/dist/index.d.ts +169 -6
- package/dist/index.js +9 -1
- package/package.json +1 -1
- package/dist/chunk-L2NG4UR4.js.map +0 -1
package/dist/bin.cjs
CHANGED
|
@@ -43,7 +43,9 @@ function approvalLines(prompt) {
|
|
|
43
43
|
lines.push("");
|
|
44
44
|
lines.push(` ${prompt.approvalUrl}`);
|
|
45
45
|
lines.push("");
|
|
46
|
-
lines.push(
|
|
46
|
+
lines.push(
|
|
47
|
+
` ${prompt.approver ?? "The matching signed-in odla account"} opens that URL, checks the code matches, and approves.`
|
|
48
|
+
);
|
|
47
49
|
lines.push(" Opening the link without approving does nothing; this command waits until they do.");
|
|
48
50
|
if (prompt.browserAttempted) {
|
|
49
51
|
lines.push(" A browser launch was attempted, but it is best-effort and may have shown no tab.");
|
|
@@ -490,6 +492,7 @@ function audienceBoundEnvToken(token, platform) {
|
|
|
490
492
|
}
|
|
491
493
|
var SCOPE_PURPOSE = {
|
|
492
494
|
"platform:status:read": "read the platform fleet health and deployment snapshot",
|
|
495
|
+
"app:config:read": "compare checked-in intent with an exact-id app Registry configuration",
|
|
493
496
|
"platform:runbook:write": "add or edit odla's operational runbooks",
|
|
494
497
|
"platform:ai:policy:write": "change System AI model routing",
|
|
495
498
|
"platform:ai:policy:read": "read System AI model routing",
|
|
@@ -526,6 +529,7 @@ async function scopedToken(platform, scope, options, doFetch, out) {
|
|
|
526
529
|
approvalUrl,
|
|
527
530
|
minutesLeft: Math.floor(expiresIn / 60),
|
|
528
531
|
purpose: SCOPE_PURPOSE[scope] ?? `use ${scope}`,
|
|
532
|
+
approver: scope.startsWith("app:") ? "A signed-in app owner" : "A signed-in odla platform admin",
|
|
529
533
|
browserAttempted: browser.open,
|
|
530
534
|
browserSkipped: browser.reason
|
|
531
535
|
});
|
|
@@ -695,10 +699,10 @@ async function responseBody2(res) {
|
|
|
695
699
|
return { message: text.slice(0, 300) };
|
|
696
700
|
}
|
|
697
701
|
}
|
|
698
|
-
function apiError2(
|
|
702
|
+
function apiError2(action2, status, body) {
|
|
699
703
|
const error = isRecord2(body) && isRecord2(body.error) ? body.error : void 0;
|
|
700
704
|
const message2 = error && typeof error.message === "string" ? error.message : isRecord2(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
701
|
-
return `${
|
|
705
|
+
return `${action2} failed (${status}): ${message2}`;
|
|
702
706
|
}
|
|
703
707
|
function isRecord2(value2) {
|
|
704
708
|
return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
|
|
@@ -876,10 +880,10 @@ async function responseBody3(res) {
|
|
|
876
880
|
return { message: text.slice(0, 300) };
|
|
877
881
|
}
|
|
878
882
|
}
|
|
879
|
-
function apiError3(
|
|
883
|
+
function apiError3(action2, status, body) {
|
|
880
884
|
const error = isRecord3(body) && isRecord3(body.error) ? body.error : void 0;
|
|
881
885
|
const message2 = error && typeof error.message === "string" ? error.message : isRecord3(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
882
|
-
return `${
|
|
886
|
+
return `${action2} failed (${status}): ${message2}`;
|
|
883
887
|
}
|
|
884
888
|
function isRecord3(value2) {
|
|
885
889
|
return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
|
|
@@ -1503,21 +1507,21 @@ var SET_OPTIONS = [
|
|
|
1503
1507
|
];
|
|
1504
1508
|
async function adminCommand(parsed, deps = {}) {
|
|
1505
1509
|
const area = parsed.positionals[1];
|
|
1506
|
-
const
|
|
1507
|
-
const credentialSet =
|
|
1508
|
-
const credentials =
|
|
1509
|
-
const models =
|
|
1510
|
-
const usage =
|
|
1511
|
-
const audit =
|
|
1512
|
-
if (area !== "ai" ||
|
|
1510
|
+
const action2 = parsed.positionals[2];
|
|
1511
|
+
const credentialSet = action2 === "credential" && parsed.positionals[3] === "set";
|
|
1512
|
+
const credentials = action2 === "credentials";
|
|
1513
|
+
const models = action2 === "models";
|
|
1514
|
+
const usage = action2 === "usage";
|
|
1515
|
+
const audit = action2 === "audit";
|
|
1516
|
+
if (area !== "ai" || action2 !== "show" && action2 !== "set" && !credentialSet && !credentials && !models && !usage && !audit) {
|
|
1513
1517
|
throw new Error('unknown admin command. Try "odla-ai admin ai show".');
|
|
1514
1518
|
}
|
|
1515
|
-
const allowed = credentialSet ? [...CONTEXT_OPTIONS, "from-env", "stdin"] :
|
|
1516
|
-
assertArgs(parsed, allowed, credentialSet ? 5 :
|
|
1519
|
+
const allowed = credentialSet ? [...CONTEXT_OPTIONS, "from-env", "stdin"] : action2 === "set" ? SET_OPTIONS : models ? [...JSON_OPTIONS, "provider"] : usage ? [...JSON_OPTIONS, "app-id", "env", "run-id", "limit"] : audit ? [...JSON_OPTIONS, "limit"] : JSON_OPTIONS;
|
|
1520
|
+
assertArgs(parsed, allowed, credentialSet ? 5 : action2 === "set" ? 4 : 3);
|
|
1517
1521
|
const context = await resolveOperatorContext(parsed, { allowMissingConfig: true });
|
|
1518
1522
|
await adminAi({
|
|
1519
|
-
action: credentialSet ? "credential-set" : credentials ? "credentials" : models ? "models" : usage ? "usage" : audit ? "audit" :
|
|
1520
|
-
purpose:
|
|
1523
|
+
action: credentialSet ? "credential-set" : credentials ? "credentials" : models ? "models" : usage ? "usage" : audit ? "audit" : action2,
|
|
1524
|
+
purpose: action2 === "set" ? parsed.positionals[3] : void 0,
|
|
1521
1525
|
provider: stringOpt(parsed.options.provider),
|
|
1522
1526
|
model: stringOpt(parsed.options.model),
|
|
1523
1527
|
enabled: boolOpt(parsed.options.enabled),
|
|
@@ -1574,12 +1578,12 @@ function bothTenants(cfg) {
|
|
|
1574
1578
|
|
|
1575
1579
|
// src/agent-command.ts
|
|
1576
1580
|
async function agentCommand(parsed, deps = {}) {
|
|
1577
|
-
const
|
|
1578
|
-
if (
|
|
1579
|
-
throw new Error(`unknown agent action "${
|
|
1581
|
+
const action2 = parsed.positionals[1];
|
|
1582
|
+
if (action2 !== "jobs" && action2 !== "retry") {
|
|
1583
|
+
throw new Error(`unknown agent action "${action2 ?? ""}". Try "odla-ai agent jobs --json".`);
|
|
1580
1584
|
}
|
|
1581
|
-
assertArgs(parsed, ["config", "env", "state", "limit", "json", "token", "email"],
|
|
1582
|
-
if (
|
|
1585
|
+
assertArgs(parsed, ["config", "env", "state", "limit", "json", "token", "email"], action2 === "jobs" ? 2 : 3);
|
|
1586
|
+
if (action2 === "retry" && (parsed.options.state !== void 0 || parsed.options.limit !== void 0)) {
|
|
1583
1587
|
throw new Error('--state and --limit are supported only by "agent jobs"');
|
|
1584
1588
|
}
|
|
1585
1589
|
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
@@ -1599,7 +1603,7 @@ async function agentCommand(parsed, deps = {}) {
|
|
|
1599
1603
|
);
|
|
1600
1604
|
const base = `${cfg.dbEndpoint}/app/${encodeURIComponent(tenant)}/admin/agent-jobs`;
|
|
1601
1605
|
const headers = { authorization: `Bearer ${credential2}` };
|
|
1602
|
-
if (
|
|
1606
|
+
if (action2 === "retry") {
|
|
1603
1607
|
const id = parsed.positionals[2];
|
|
1604
1608
|
const res2 = await doFetch(`${base}/${encodeURIComponent(id)}/retry`, { method: "POST", headers });
|
|
1605
1609
|
const body2 = await readJson(res2);
|
|
@@ -2035,7 +2039,7 @@ async function copyFiles(cfg, token, route2, doFetch, out) {
|
|
|
2035
2039
|
}
|
|
2036
2040
|
|
|
2037
2041
|
// src/app-lifecycle.ts
|
|
2038
|
-
async function lifecycleCall(
|
|
2042
|
+
async function lifecycleCall(action2, options) {
|
|
2039
2043
|
const cfg = await loadProjectConfig(options.configPath);
|
|
2040
2044
|
const out = options.stdout ?? console;
|
|
2041
2045
|
const doFetch = options.fetch ?? fetch;
|
|
@@ -2045,13 +2049,13 @@ async function lifecycleCall(action, options) {
|
|
|
2045
2049
|
doFetch,
|
|
2046
2050
|
out
|
|
2047
2051
|
);
|
|
2048
|
-
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/${
|
|
2052
|
+
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/${action2}`, {
|
|
2049
2053
|
method: "POST",
|
|
2050
2054
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }
|
|
2051
2055
|
});
|
|
2052
2056
|
const body = await res.json().catch(() => ({}));
|
|
2053
2057
|
if (!res.ok || !body.ok) {
|
|
2054
|
-
throw new Error(`${
|
|
2058
|
+
throw new Error(`${action2} failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? `registry returned ${res.status}`}`);
|
|
2055
2059
|
}
|
|
2056
2060
|
return body;
|
|
2057
2061
|
}
|
|
@@ -2604,8 +2608,8 @@ function pollTimeout(value2 = 10 * 6e4) {
|
|
|
2604
2608
|
if (!Number.isSafeInteger(value2) || value2 < 1e3 || value2 > 60 * 6e4) throw new Error("calendar poll timeout must be 1000-3600000ms");
|
|
2605
2609
|
return value2;
|
|
2606
2610
|
}
|
|
2607
|
-
function productionConsent(env, yes,
|
|
2608
|
-
if ((env === "prod" || env === "production") && !yes) throw new Error(`refusing to ${
|
|
2611
|
+
function productionConsent(env, yes, action2) {
|
|
2612
|
+
if ((env === "prod" || env === "production") && !yes) throw new Error(`refusing to ${action2} for "${env}" without --yes`);
|
|
2609
2613
|
}
|
|
2610
2614
|
function printStatus(status, json, out) {
|
|
2611
2615
|
if (json) {
|
|
@@ -2635,6 +2639,7 @@ var CAPABILITIES = {
|
|
|
2635
2639
|
"save and explicitly select non-secret named operator contexts with isolated credential caches; resolve and explain platform, app, environment, and credential provenance without authenticating; then run PM, Discussions, o11y, runbook, and identity operations outside a project checkout",
|
|
2636
2640
|
"read one versioned o11y status envelope spanning application RED, exact Worker versions and Cloudflare colos observed in traffic, current live-sync freshness/load, the protected commit-to-visible canary, collector ingest/scheduler trust, provider-owned runtime metrics, account-scoped Durable Object, D1, and R2 evidence under odla-db, and a bounded machine verdict",
|
|
2637
2641
|
"read one canonical platform fleet snapshot over private service bindings, including release identities, probe latency, Cloudflare load/runtime freshness, explicit unknowns, and stable next actions through a read-only capability",
|
|
2642
|
+
"compare one project's checked-in Registry intent with live owner-visible state and freeze a secret-free plan digest through an exact app:config:read capability",
|
|
2638
2643
|
"inspect durable agent wakeups as a versioned JSON envelope and explicitly requeue one dead-lettered job with a scoped environment credential",
|
|
2639
2644
|
"run app-attributed hosted security discovery and independent validation without provider keys",
|
|
2640
2645
|
"connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
|
|
@@ -2681,15 +2686,542 @@ function printGroup(out, heading, items) {
|
|
|
2681
2686
|
out.log("");
|
|
2682
2687
|
}
|
|
2683
2688
|
|
|
2689
|
+
// src/config-reconcile-command.ts
|
|
2690
|
+
var import_apps6 = require("@odla-ai/apps");
|
|
2691
|
+
var import_node_path7 = require("path");
|
|
2692
|
+
|
|
2693
|
+
// src/config-reconcile-digest.ts
|
|
2694
|
+
var import_node_crypto = require("crypto");
|
|
2695
|
+
function canonicalJson(value2) {
|
|
2696
|
+
return JSON.stringify(canonicalValue(value2));
|
|
2697
|
+
}
|
|
2698
|
+
function configDigest(value2) {
|
|
2699
|
+
return `sha256:${(0, import_node_crypto.createHash)("sha256").update(canonicalJson(value2)).digest("hex")}`;
|
|
2700
|
+
}
|
|
2701
|
+
function canonicalValue(value2) {
|
|
2702
|
+
if (Array.isArray(value2)) return value2.map(canonicalValue);
|
|
2703
|
+
if (value2 && typeof value2 === "object") {
|
|
2704
|
+
return Object.fromEntries(
|
|
2705
|
+
Object.entries(value2).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, canonicalValue(entry)])
|
|
2706
|
+
);
|
|
2707
|
+
}
|
|
2708
|
+
return value2;
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
// src/config-reconcile-desired.ts
|
|
2712
|
+
var import_apps4 = require("@odla-ai/apps");
|
|
2713
|
+
|
|
2714
|
+
// src/provision-helpers.ts
|
|
2715
|
+
var import_ai = require("@odla-ai/ai");
|
|
2716
|
+
var import_apps3 = require("@odla-ai/apps");
|
|
2717
|
+
function defaultSecretName(provider) {
|
|
2718
|
+
const names = import_ai.DEFAULT_SECRET_NAMES;
|
|
2719
|
+
return names[provider] ?? `${provider}_api_key`;
|
|
2720
|
+
}
|
|
2721
|
+
async function assertTenantAdminAccess(doFetch, cfg, env, token) {
|
|
2722
|
+
const tenantId = (0, import_apps3.tenantIdFor)(cfg.app.id, env);
|
|
2723
|
+
const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/entitlements`, {
|
|
2724
|
+
headers: { authorization: `Bearer ${token}` }
|
|
2725
|
+
});
|
|
2726
|
+
if (res.ok || res.status === 404) return;
|
|
2727
|
+
if (res.status === 403) {
|
|
2728
|
+
throw new Error(
|
|
2729
|
+
`${env}: you are not an owner of "${cfg.app.id}" (tenant ${tenantId}) \u2014 nothing was minted or written; ask an existing owner to run "odla-ai app owners add <your-email>", then re-run provision`
|
|
2730
|
+
);
|
|
2731
|
+
}
|
|
2732
|
+
throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText3(res)}`);
|
|
2733
|
+
}
|
|
2734
|
+
async function postJson(doFetch, url, bearer, body) {
|
|
2735
|
+
const res = await doFetch(url, {
|
|
2736
|
+
method: "POST",
|
|
2737
|
+
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
2738
|
+
body: JSON.stringify(body)
|
|
2739
|
+
});
|
|
2740
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText3(res)}`);
|
|
2741
|
+
}
|
|
2742
|
+
function normalizeClerkConfig(value2) {
|
|
2743
|
+
if (!value2) return null;
|
|
2744
|
+
if (typeof value2 === "string") {
|
|
2745
|
+
const publishableKey2 = envValue(value2);
|
|
2746
|
+
return publishableKey2 ? { publishableKey: publishableKey2 } : null;
|
|
2747
|
+
}
|
|
2748
|
+
if (typeof value2 !== "object") return null;
|
|
2749
|
+
const cfg = value2;
|
|
2750
|
+
const publishableKey = envValue(cfg.publishableKey);
|
|
2751
|
+
return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
|
|
2752
|
+
}
|
|
2753
|
+
async function safeText3(res) {
|
|
2754
|
+
try {
|
|
2755
|
+
return redactSecrets((await res.text()).slice(0, 500));
|
|
2756
|
+
} catch {
|
|
2757
|
+
return "";
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
// src/config-reconcile-desired.ts
|
|
2762
|
+
function desiredRegistryState(cfg) {
|
|
2763
|
+
const environments = {};
|
|
2764
|
+
const services = (0, import_apps4.orderAppServices)(cfg.services);
|
|
2765
|
+
for (const env of cfg.envs) {
|
|
2766
|
+
const desiredServices = {};
|
|
2767
|
+
for (const service of services) {
|
|
2768
|
+
desiredServices[service] = {
|
|
2769
|
+
enabled: true,
|
|
2770
|
+
...managedServiceConfig(cfg, env, service)
|
|
2771
|
+
};
|
|
2772
|
+
}
|
|
2773
|
+
const authoredAuth = cfg.auth?.clerk && Object.hasOwn(cfg.auth.clerk, env) ? cfg.auth.clerk[env] : void 0;
|
|
2774
|
+
const auth = normalizeClerkConfig(authoredAuth);
|
|
2775
|
+
if (authoredAuth && !auth) {
|
|
2776
|
+
throw new Error(`auth.clerk.${env} could not resolve its publishable key`);
|
|
2777
|
+
}
|
|
2778
|
+
const linkManaged = !!cfg.links && Object.hasOwn(cfg.links, env);
|
|
2779
|
+
environments[env] = {
|
|
2780
|
+
services: desiredServices,
|
|
2781
|
+
...auth ? { auth } : {},
|
|
2782
|
+
...linkManaged ? { link: cfg.links?.[env] ?? null } : {}
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
return {
|
|
2786
|
+
appId: cfg.app.id,
|
|
2787
|
+
name: cfg.app.name,
|
|
2788
|
+
environments
|
|
2789
|
+
};
|
|
2790
|
+
}
|
|
2791
|
+
function managedServiceConfig(cfg, env, service) {
|
|
2792
|
+
if (service === "ai" && cfg.ai?.provider) {
|
|
2793
|
+
return {
|
|
2794
|
+
config: {
|
|
2795
|
+
provider: cfg.ai.provider,
|
|
2796
|
+
...cfg.ai.model ? { model: cfg.ai.model } : {}
|
|
2797
|
+
}
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
if (service === "calendar") return { config: calendarServiceConfig(cfg, env) };
|
|
2801
|
+
return {};
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
// src/config-reconcile.ts
|
|
2805
|
+
var import_apps5 = require("@odla-ai/apps");
|
|
2806
|
+
|
|
2807
|
+
// src/config-reconcile-values.ts
|
|
2808
|
+
function difference(path, desired, observed, status, reason, desiredSource, observedSource, env, service) {
|
|
2809
|
+
return { path, env, service, status, desired, observed, desiredSource, observedSource, reason };
|
|
2810
|
+
}
|
|
2811
|
+
function action(kind, path, before, after, risk, applySupport, reason) {
|
|
2812
|
+
return { kind, path, before, after, risk, requiresApproval: false, applySupport, reason };
|
|
2813
|
+
}
|
|
2814
|
+
function scopedAction(kind, path, env, service, before, after, reason, destructive = false) {
|
|
2815
|
+
const production = env === "prod" || env === "production";
|
|
2816
|
+
return {
|
|
2817
|
+
...action(
|
|
2818
|
+
kind,
|
|
2819
|
+
path,
|
|
2820
|
+
before,
|
|
2821
|
+
after,
|
|
2822
|
+
destructive ? "high" : production ? "medium" : "low",
|
|
2823
|
+
destructive ? "studio" : "provision",
|
|
2824
|
+
reason
|
|
2825
|
+
),
|
|
2826
|
+
env,
|
|
2827
|
+
...service ? { service } : {},
|
|
2828
|
+
requiresApproval: production || destructive
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
function observedProjection(app) {
|
|
2832
|
+
return {
|
|
2833
|
+
appId: app.appId,
|
|
2834
|
+
name: app.name,
|
|
2835
|
+
archivedAt: app.archivedAt,
|
|
2836
|
+
environments: Object.fromEntries(
|
|
2837
|
+
Object.entries(app.environments).sort(([left], [right]) => left.localeCompare(right)).map(([env, services]) => [
|
|
2838
|
+
env,
|
|
2839
|
+
Object.fromEntries(Object.entries(services).sort(([left], [right]) => left.localeCompare(right)))
|
|
2840
|
+
])
|
|
2841
|
+
),
|
|
2842
|
+
auth: app.auth,
|
|
2843
|
+
links: app.links
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
2846
|
+
function projectConfig(value2, keys) {
|
|
2847
|
+
return Object.fromEntries(keys.filter((key) => value2[key] !== void 0).map((key) => [key, value2[key]]));
|
|
2848
|
+
}
|
|
2849
|
+
function same(left, right) {
|
|
2850
|
+
return canonicalJson(left) === canonicalJson(right);
|
|
2851
|
+
}
|
|
2852
|
+
function actionId(actionValue) {
|
|
2853
|
+
return `action-${configDigest(actionValue).slice("sha256:".length, "sha256:".length + 16)}`;
|
|
2854
|
+
}
|
|
2855
|
+
function quoteArg(value2) {
|
|
2856
|
+
return `'${value2.replace(/'/g, `'\\''`)}'`;
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
// src/config-reconcile.ts
|
|
2860
|
+
var COVERAGE = {
|
|
2861
|
+
included: [
|
|
2862
|
+
"app display name",
|
|
2863
|
+
"environment service enablement",
|
|
2864
|
+
"AI provider/model and calendar booking service configuration",
|
|
2865
|
+
"auth configuration when explicitly declared",
|
|
2866
|
+
"deployment link when explicitly declared"
|
|
2867
|
+
],
|
|
2868
|
+
excluded: [
|
|
2869
|
+
{ path: "db.schema", reason: "owned by the database data plane" },
|
|
2870
|
+
{ path: "db.rules", reason: "owned by the database data plane" },
|
|
2871
|
+
{ path: "integrations.seeds", reason: "guarded data-plane writes are not Registry state" },
|
|
2872
|
+
{ path: "credentials", reason: "shown-once credentials are intentionally local" },
|
|
2873
|
+
{ path: "secrets", reason: "vault and Worker secrets are write-only" },
|
|
2874
|
+
{ path: "calendar.connection", reason: "OAuth connection state is provider-owned" },
|
|
2875
|
+
{ path: "service.config.opaque", reason: "controller-assigned fields remain service-owned" }
|
|
2876
|
+
]
|
|
2877
|
+
};
|
|
2878
|
+
function reconcileConfig(input) {
|
|
2879
|
+
const desiredSource = { kind: "project_config", location: input.configPath };
|
|
2880
|
+
const observedSource = {
|
|
2881
|
+
kind: "registry",
|
|
2882
|
+
location: `${input.platformUrl}/registry/apps/${encodeURIComponent(input.desired.appId)}`
|
|
2883
|
+
};
|
|
2884
|
+
const differences = [];
|
|
2885
|
+
const actions = [];
|
|
2886
|
+
const add = (difference2, action2) => {
|
|
2887
|
+
differences.push(difference2);
|
|
2888
|
+
if (action2) actions.push({ ...action2, id: actionId(action2) });
|
|
2889
|
+
};
|
|
2890
|
+
compareApp(input.desired, input.observed, desiredSource, observedSource, add);
|
|
2891
|
+
compareEnvironments(input.desired, input.observed, desiredSource, observedSource, add);
|
|
2892
|
+
const desiredRevision = configDigest(input.desired);
|
|
2893
|
+
const observedRevision = input.observed ? configDigest(observedProjection(input.observed)) : null;
|
|
2894
|
+
const different = differences.filter((entry) => entry.status === "different").length;
|
|
2895
|
+
const unmanaged = differences.length - different;
|
|
2896
|
+
return {
|
|
2897
|
+
generatedAt: input.generatedAt,
|
|
2898
|
+
scope: {
|
|
2899
|
+
appId: input.desired.appId,
|
|
2900
|
+
platformUrl: input.platformUrl,
|
|
2901
|
+
environments: Object.keys(input.desired.environments).sort()
|
|
2902
|
+
},
|
|
2903
|
+
sources: { desired: desiredSource, observed: observedSource },
|
|
2904
|
+
desiredRevision,
|
|
2905
|
+
observedRevision,
|
|
2906
|
+
status: different ? "different" : unmanaged ? "unmanaged" : "in_sync",
|
|
2907
|
+
summary: {
|
|
2908
|
+
differences: different,
|
|
2909
|
+
unmanaged,
|
|
2910
|
+
actions: actions.length,
|
|
2911
|
+
productionActions: actions.filter((action2) => action2.env === "prod" || action2.env === "production").length,
|
|
2912
|
+
highRiskActions: actions.filter((action2) => action2.risk === "high").length
|
|
2913
|
+
},
|
|
2914
|
+
coverage: COVERAGE,
|
|
2915
|
+
differences,
|
|
2916
|
+
actions
|
|
2917
|
+
};
|
|
2918
|
+
}
|
|
2919
|
+
function compareApp(desired, observed, desiredSource, observedSource, add) {
|
|
2920
|
+
if (!observed) {
|
|
2921
|
+
const path = "app";
|
|
2922
|
+
add(
|
|
2923
|
+
difference(path, desired, null, "different", "the app is absent from Registry", desiredSource, observedSource),
|
|
2924
|
+
action(
|
|
2925
|
+
"create_app",
|
|
2926
|
+
path,
|
|
2927
|
+
null,
|
|
2928
|
+
{ appId: desired.appId, name: desired.name },
|
|
2929
|
+
"low",
|
|
2930
|
+
"provision",
|
|
2931
|
+
"create the repository-declared app"
|
|
2932
|
+
)
|
|
2933
|
+
);
|
|
2934
|
+
return;
|
|
2935
|
+
}
|
|
2936
|
+
if (observed.name !== desired.name) {
|
|
2937
|
+
const path = "app.name";
|
|
2938
|
+
add(
|
|
2939
|
+
difference(path, desired.name, observed.name, "different", "the checked-in display name differs", desiredSource, observedSource),
|
|
2940
|
+
{
|
|
2941
|
+
...action("rename_app", path, observed.name, desired.name, "low", "command", "make Registry match the checked-in display name"),
|
|
2942
|
+
command: `odla-ai app rename ${quoteArg(desired.name)} --config ${quoteArg(desiredSource.location)}`
|
|
2943
|
+
}
|
|
2944
|
+
);
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
function compareEnvironments(desired, observed, desiredSource, observedSource, add) {
|
|
2948
|
+
const envs = /* @__PURE__ */ new Set([
|
|
2949
|
+
...Object.keys(desired.environments),
|
|
2950
|
+
...Object.keys(observed?.environments ?? {}).filter((env) => Object.values(observed?.environments[env] ?? {}).some((service) => service.enabled))
|
|
2951
|
+
]);
|
|
2952
|
+
for (const env of [...envs].sort()) {
|
|
2953
|
+
compareServices(env, desired, observed, desiredSource, observedSource, add);
|
|
2954
|
+
compareOptionalEnvironmentState(env, desired, observed, desiredSource, observedSource, add);
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
function compareServices(env, desired, observed, desiredSource, observedSource, add) {
|
|
2958
|
+
const wanted = desired.environments[env]?.services ?? {};
|
|
2959
|
+
const live = observed?.environments[env] ?? {};
|
|
2960
|
+
const knownOrder = (0, import_apps5.orderAppServices)((0, import_apps5.appServiceIds)());
|
|
2961
|
+
const services = [.../* @__PURE__ */ new Set([...knownOrder, ...Object.keys(wanted), ...Object.keys(live)])];
|
|
2962
|
+
for (const service of services) {
|
|
2963
|
+
const next = wanted[service];
|
|
2964
|
+
const current = live[service];
|
|
2965
|
+
const path = `environments.${env}.services.${service}`;
|
|
2966
|
+
if (next?.enabled && !current?.enabled) {
|
|
2967
|
+
add(
|
|
2968
|
+
difference(path, next, current ?? null, "different", "the repository enables a service Registry does not", desiredSource, observedSource, env, service),
|
|
2969
|
+
scopedAction("enable_service", path, env, service, current ?? null, next, "enable the repository-declared service")
|
|
2970
|
+
);
|
|
2971
|
+
continue;
|
|
2972
|
+
}
|
|
2973
|
+
if (next?.enabled && current?.enabled && next.config) {
|
|
2974
|
+
const projected = projectConfig(current.config, Object.keys(next.config));
|
|
2975
|
+
if (!same(next.config, projected)) {
|
|
2976
|
+
add(
|
|
2977
|
+
difference(`${path}.config`, next.config, projected, "different", "repository-managed service settings differ", desiredSource, observedSource, env, service),
|
|
2978
|
+
scopedAction(
|
|
2979
|
+
"configure_service",
|
|
2980
|
+
`${path}.config`,
|
|
2981
|
+
env,
|
|
2982
|
+
service,
|
|
2983
|
+
projected,
|
|
2984
|
+
next.config,
|
|
2985
|
+
"apply the repository-managed service settings"
|
|
2986
|
+
)
|
|
2987
|
+
);
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
for (const service of [...services].reverse()) {
|
|
2992
|
+
const next = wanted[service];
|
|
2993
|
+
const current = live[service];
|
|
2994
|
+
if (next?.enabled || !current?.enabled) continue;
|
|
2995
|
+
const path = `environments.${env}.services.${service}`;
|
|
2996
|
+
add(
|
|
2997
|
+
difference(path, null, { enabled: true }, "different", "Registry enables a service absent from repository intent", desiredSource, observedSource, env, service),
|
|
2998
|
+
scopedAction(
|
|
2999
|
+
"disable_service",
|
|
3000
|
+
path,
|
|
3001
|
+
env,
|
|
3002
|
+
service,
|
|
3003
|
+
{ enabled: true },
|
|
3004
|
+
{ enabled: false },
|
|
3005
|
+
"disablement is destructive to availability and requires an explicit Studio review",
|
|
3006
|
+
true
|
|
3007
|
+
)
|
|
3008
|
+
);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
function compareOptionalEnvironmentState(env, desired, observed, desiredSource, observedSource, add) {
|
|
3012
|
+
const wanted = desired.environments[env];
|
|
3013
|
+
const liveAuth = observed?.auth[env] ?? null;
|
|
3014
|
+
if (wanted?.auth) {
|
|
3015
|
+
const projected = liveAuth ? projectConfig(liveAuth, Object.keys(wanted.auth)) : null;
|
|
3016
|
+
if (!same(wanted.auth, projected)) {
|
|
3017
|
+
const path = `environments.${env}.auth`;
|
|
3018
|
+
add(
|
|
3019
|
+
difference(path, wanted.auth, projected, "different", "explicit repository auth settings differ", desiredSource, observedSource, env),
|
|
3020
|
+
scopedAction("set_auth", path, env, void 0, projected, wanted.auth, "apply explicit repository auth settings")
|
|
3021
|
+
);
|
|
3022
|
+
}
|
|
3023
|
+
} else if (liveAuth) {
|
|
3024
|
+
add(difference(
|
|
3025
|
+
`environments.${env}.auth`,
|
|
3026
|
+
null,
|
|
3027
|
+
projectConfig(liveAuth, ["publishableKey", "audience", "mode"]),
|
|
3028
|
+
"unmanaged",
|
|
3029
|
+
"live auth exists but this project config does not manage it",
|
|
3030
|
+
desiredSource,
|
|
3031
|
+
observedSource,
|
|
3032
|
+
env
|
|
3033
|
+
));
|
|
3034
|
+
}
|
|
3035
|
+
const managesLink = wanted && Object.hasOwn(wanted, "link");
|
|
3036
|
+
const liveLink = observed?.links[env] ?? null;
|
|
3037
|
+
if (managesLink && wanted.link !== liveLink) {
|
|
3038
|
+
const path = `environments.${env}.link`;
|
|
3039
|
+
add(
|
|
3040
|
+
difference(path, wanted.link ?? null, liveLink, "different", "explicit repository deployment link differs", desiredSource, observedSource, env),
|
|
3041
|
+
scopedAction("set_link", path, env, void 0, liveLink, wanted.link ?? null, "apply the repository deployment link")
|
|
3042
|
+
);
|
|
3043
|
+
} else if (!managesLink && liveLink) {
|
|
3044
|
+
add(difference(
|
|
3045
|
+
`environments.${env}.link`,
|
|
3046
|
+
null,
|
|
3047
|
+
liveLink,
|
|
3048
|
+
"unmanaged",
|
|
3049
|
+
"a live deployment link exists but this project config does not manage it",
|
|
3050
|
+
desiredSource,
|
|
3051
|
+
observedSource,
|
|
3052
|
+
env
|
|
3053
|
+
));
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
// src/config-reconcile-command.ts
|
|
3058
|
+
async function configDiff(options) {
|
|
3059
|
+
const reconciliation = await inspectConfig(options);
|
|
3060
|
+
const { actions: _actions, ...read3 } = reconciliation;
|
|
3061
|
+
const document2 = {
|
|
3062
|
+
schemaVersion: "odla.config-diff/v1",
|
|
3063
|
+
...read3,
|
|
3064
|
+
nextActions: diffNextActions(reconciliation, options.configPath)
|
|
3065
|
+
};
|
|
3066
|
+
printDiff(document2, options);
|
|
3067
|
+
return document2;
|
|
3068
|
+
}
|
|
3069
|
+
async function configPlan(options) {
|
|
3070
|
+
const reconciliation = await inspectConfig(options);
|
|
3071
|
+
const planDigest = configDigest({
|
|
3072
|
+
schemaVersion: "odla.config-plan/v1",
|
|
3073
|
+
desiredRevision: reconciliation.desiredRevision,
|
|
3074
|
+
observedRevision: reconciliation.observedRevision,
|
|
3075
|
+
actions: reconciliation.actions
|
|
3076
|
+
});
|
|
3077
|
+
const document2 = {
|
|
3078
|
+
schemaVersion: "odla.config-plan/v1",
|
|
3079
|
+
...reconciliation,
|
|
3080
|
+
planDigest,
|
|
3081
|
+
apply: {
|
|
3082
|
+
supported: false,
|
|
3083
|
+
reason: "conditional, resumable config apply is not available yet; use the exact reviewed commands below"
|
|
3084
|
+
},
|
|
3085
|
+
nextActions: planNextActions(reconciliation, options.configPath)
|
|
3086
|
+
};
|
|
3087
|
+
printPlan2(document2, options);
|
|
3088
|
+
return document2;
|
|
3089
|
+
}
|
|
3090
|
+
async function inspectConfig(options) {
|
|
3091
|
+
const out = options.stdout ?? console;
|
|
3092
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
3093
|
+
const doFetch = options.fetch ?? fetch;
|
|
3094
|
+
const token = await resolveAdminPlatformToken({
|
|
3095
|
+
platform: cfg.platformUrl,
|
|
3096
|
+
scope: "app:config:read",
|
|
3097
|
+
token: options.token,
|
|
3098
|
+
tokenFile: (0, import_node_path7.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
|
|
3099
|
+
rootDir: cfg.rootDir,
|
|
3100
|
+
email: options.email,
|
|
3101
|
+
open: options.open,
|
|
3102
|
+
fetch: doFetch,
|
|
3103
|
+
stdout: out,
|
|
3104
|
+
openApprovalUrl: options.openApprovalUrl,
|
|
3105
|
+
label: `odla CLI (${cfg.app.id} config read)`
|
|
3106
|
+
});
|
|
3107
|
+
const client = (0, import_apps6.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
|
|
3108
|
+
const observed = await client.resolveApp(cfg.app.id);
|
|
3109
|
+
return reconcileConfig({
|
|
3110
|
+
desired: desiredRegistryState(cfg),
|
|
3111
|
+
observed,
|
|
3112
|
+
configPath: cfg.configPath,
|
|
3113
|
+
platformUrl: cfg.platformUrl,
|
|
3114
|
+
generatedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
|
|
3115
|
+
});
|
|
3116
|
+
}
|
|
3117
|
+
function printDiff(document2, options) {
|
|
3118
|
+
const out = options.stdout ?? console;
|
|
3119
|
+
if (options.json) {
|
|
3120
|
+
out.log(JSON.stringify(document2, null, 2));
|
|
3121
|
+
return;
|
|
3122
|
+
}
|
|
3123
|
+
printHeader(out, "diff", document2);
|
|
3124
|
+
printDifferences(out, document2);
|
|
3125
|
+
printNext(out, document2.nextActions);
|
|
3126
|
+
}
|
|
3127
|
+
function printPlan2(document2, options) {
|
|
3128
|
+
const out = options.stdout ?? console;
|
|
3129
|
+
if (options.json) {
|
|
3130
|
+
out.log(JSON.stringify(document2, null, 2));
|
|
3131
|
+
return;
|
|
3132
|
+
}
|
|
3133
|
+
printHeader(out, "plan", document2);
|
|
3134
|
+
for (const entry of document2.actions) {
|
|
3135
|
+
const scope = [entry.env, entry.service].filter(Boolean).join("/");
|
|
3136
|
+
out.log(
|
|
3137
|
+
` ${entry.id} ${entry.kind}${scope ? ` (${scope})` : ""} ${entry.risk}${entry.requiresApproval ? ", approval required" : ""}`
|
|
3138
|
+
);
|
|
3139
|
+
out.log(` ${entry.reason}`);
|
|
3140
|
+
}
|
|
3141
|
+
if (!document2.actions.length) out.log(" no managed changes");
|
|
3142
|
+
out.log(`plan digest: ${document2.planDigest}`);
|
|
3143
|
+
out.log(`apply: unsupported \u2014 ${document2.apply.reason}`);
|
|
3144
|
+
printNext(out, document2.nextActions);
|
|
3145
|
+
}
|
|
3146
|
+
function printHeader(out, kind, document2) {
|
|
3147
|
+
out.log(`config ${kind}: ${document2.scope.appId} \u2014 ${document2.status}`);
|
|
3148
|
+
out.log(`platform: ${document2.scope.platformUrl}`);
|
|
3149
|
+
out.log(`desired: ${document2.desiredRevision}`);
|
|
3150
|
+
out.log(`observed: ${document2.observedRevision ?? "absent"}`);
|
|
3151
|
+
out.log(
|
|
3152
|
+
`summary: ${document2.summary.differences} different, ${document2.summary.unmanaged} unmanaged, ${document2.summary.actions} planned actions`
|
|
3153
|
+
);
|
|
3154
|
+
}
|
|
3155
|
+
function printDifferences(out, document2) {
|
|
3156
|
+
for (const entry of document2.differences) {
|
|
3157
|
+
out.log(` ${entry.status} ${entry.path} ${entry.reason}`);
|
|
3158
|
+
}
|
|
3159
|
+
if (!document2.differences.length) out.log(" managed Registry configuration is in sync");
|
|
3160
|
+
}
|
|
3161
|
+
function printNext(out, next) {
|
|
3162
|
+
if (!next.length) return;
|
|
3163
|
+
out.log("next:");
|
|
3164
|
+
for (const item of next) out.log(` ${item.command}
|
|
3165
|
+
${item.description}`);
|
|
3166
|
+
}
|
|
3167
|
+
function diffNextActions(reconciliation, configPath) {
|
|
3168
|
+
if (reconciliation.status === "in_sync") {
|
|
3169
|
+
return [{
|
|
3170
|
+
code: "verify_runtime",
|
|
3171
|
+
command: `odla-ai smoke --config ${quoteArg2(configPath)}`,
|
|
3172
|
+
description: "Registry intent matches; verify the service-owned data planes separately."
|
|
3173
|
+
}];
|
|
3174
|
+
}
|
|
3175
|
+
return [{
|
|
3176
|
+
code: "freeze_plan",
|
|
3177
|
+
command: `odla-ai config plan --config ${quoteArg2(configPath)} --json`,
|
|
3178
|
+
description: "Freeze the current desired and observed revisions into a reviewable action plan."
|
|
3179
|
+
}];
|
|
3180
|
+
}
|
|
3181
|
+
function planNextActions(reconciliation, configPath) {
|
|
3182
|
+
const next = [];
|
|
3183
|
+
if (reconciliation.actions.some((action2) => action2.applySupport === "provision")) {
|
|
3184
|
+
next.push({
|
|
3185
|
+
code: "review_provision",
|
|
3186
|
+
command: `odla-ai provision --config ${quoteArg2(configPath)} --dry-run`,
|
|
3187
|
+
description: "Review the existing provisioner's service/auth/link work before applying it."
|
|
3188
|
+
});
|
|
3189
|
+
}
|
|
3190
|
+
for (const action2 of reconciliation.actions.filter((entry) => entry.command)) {
|
|
3191
|
+
if (!next.some((entry) => entry.command === action2.command)) {
|
|
3192
|
+
next.push({ code: action2.kind, command: action2.command, description: action2.reason });
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
if (reconciliation.actions.some((action2) => action2.applySupport === "studio")) {
|
|
3196
|
+
const { appId, platformUrl } = reconciliation.scope;
|
|
3197
|
+
next.push({
|
|
3198
|
+
code: "review_destructive",
|
|
3199
|
+
command: `${platformUrl}/studio/apps/${encodeURIComponent(appId)}/settings`,
|
|
3200
|
+
description: "Review service disablement in the owning Studio scope; this plan will not apply it."
|
|
3201
|
+
});
|
|
3202
|
+
}
|
|
3203
|
+
if (!reconciliation.actions.length && reconciliation.summary.unmanaged) {
|
|
3204
|
+
next.push({
|
|
3205
|
+
code: "declare_or_accept_runtime_state",
|
|
3206
|
+
command: `${reconciliation.scope.platformUrl}/studio/apps/${encodeURIComponent(reconciliation.scope.appId)}/settings`,
|
|
3207
|
+
description: "Declare the live value in project config or keep it as an explicit runtime-managed setting."
|
|
3208
|
+
});
|
|
3209
|
+
}
|
|
3210
|
+
return next;
|
|
3211
|
+
}
|
|
3212
|
+
function quoteArg2(value2) {
|
|
3213
|
+
return `'${value2.replace(/'/g, `'\\''`)}'`;
|
|
3214
|
+
}
|
|
3215
|
+
|
|
2684
3216
|
// src/doctor-checks.ts
|
|
2685
3217
|
var import_node_child_process3 = require("child_process");
|
|
2686
3218
|
var import_node_fs10 = require("fs");
|
|
2687
|
-
var
|
|
3219
|
+
var import_node_path9 = require("path");
|
|
2688
3220
|
|
|
2689
3221
|
// src/wrangler.ts
|
|
2690
3222
|
var import_node_child_process2 = require("child_process");
|
|
2691
3223
|
var import_node_fs9 = require("fs");
|
|
2692
|
-
var
|
|
3224
|
+
var import_node_path8 = require("path");
|
|
2693
3225
|
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
2694
3226
|
const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
2695
3227
|
let stdout = "";
|
|
@@ -2703,7 +3235,7 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) =>
|
|
|
2703
3235
|
var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
2704
3236
|
function findWranglerConfig(rootDir) {
|
|
2705
3237
|
for (const name of WRANGLER_CONFIG_FILES) {
|
|
2706
|
-
const path = (0,
|
|
3238
|
+
const path = (0, import_node_path8.join)(rootDir, name);
|
|
2707
3239
|
if ((0, import_node_fs9.existsSync)(path)) return path;
|
|
2708
3240
|
}
|
|
2709
3241
|
return null;
|
|
@@ -2769,11 +3301,11 @@ function lintRules(rules, entities, publicRead) {
|
|
|
2769
3301
|
const warnings = [];
|
|
2770
3302
|
if (!rules) return warnings;
|
|
2771
3303
|
for (const [ns, actions] of Object.entries(rules)) {
|
|
2772
|
-
for (const [
|
|
3304
|
+
for (const [action2, expr] of Object.entries(actions)) {
|
|
2773
3305
|
if (typeof expr !== "string" || expr.trim() !== "true") continue;
|
|
2774
|
-
if (
|
|
3306
|
+
if (action2 === "view" && publicRead.includes(ns)) continue;
|
|
2775
3307
|
warnings.push(
|
|
2776
|
-
|
|
3308
|
+
action2 === "view" ? `rules.${ns}.view is "true" \u2014 public read; add "${ns}" to db.publicRead if intended` : `rules.${ns}.${action2} is "true" \u2014 any client can ${action2} ${ns} rows`
|
|
2777
3309
|
);
|
|
2778
3310
|
}
|
|
2779
3311
|
}
|
|
@@ -2809,10 +3341,10 @@ function wranglerWarnings(rootDir) {
|
|
|
2809
3341
|
for (const { label, block } of blocks) {
|
|
2810
3342
|
const assets = block.assets;
|
|
2811
3343
|
if (assets?.directory) {
|
|
2812
|
-
const dir = (0,
|
|
2813
|
-
if (dir === (0,
|
|
3344
|
+
const dir = (0, import_node_path9.resolve)(rootDir, assets.directory);
|
|
3345
|
+
if (dir === (0, import_node_path9.resolve)(rootDir)) {
|
|
2814
3346
|
warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
|
|
2815
|
-
} else if ((0, import_node_fs10.existsSync)((0,
|
|
3347
|
+
} else if ((0, import_node_fs10.existsSync)((0, import_node_path9.join)(dir, "node_modules"))) {
|
|
2816
3348
|
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
2817
3349
|
}
|
|
2818
3350
|
}
|
|
@@ -2847,7 +3379,7 @@ function o11yProjectWarnings(rootDir) {
|
|
|
2847
3379
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
|
|
2848
3380
|
return warnings;
|
|
2849
3381
|
}
|
|
2850
|
-
const main = typeof config.main === "string" ? (0,
|
|
3382
|
+
const main = typeof config.main === "string" ? (0, import_node_path9.resolve)(rootDir, config.main) : null;
|
|
2851
3383
|
if (!main || !(0, import_node_fs10.existsSync)(main)) {
|
|
2852
3384
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
|
|
2853
3385
|
} else {
|
|
@@ -2877,7 +3409,7 @@ function calendarProjectWarnings(rootDir) {
|
|
|
2877
3409
|
}
|
|
2878
3410
|
function readPackageJson(rootDir) {
|
|
2879
3411
|
try {
|
|
2880
|
-
return JSON.parse((0, import_node_fs10.readFileSync)((0,
|
|
3412
|
+
return JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path9.join)(rootDir, "package.json"), "utf8"));
|
|
2881
3413
|
} catch {
|
|
2882
3414
|
return null;
|
|
2883
3415
|
}
|
|
@@ -2933,8 +3465,8 @@ function integrationWarnings(integrations, schema, rules) {
|
|
|
2933
3465
|
warnings.push(`integration "${integration.id}" has no rules for "${ns}"`);
|
|
2934
3466
|
continue;
|
|
2935
3467
|
}
|
|
2936
|
-
for (const
|
|
2937
|
-
if (rule[
|
|
3468
|
+
for (const action2 of ["view", "create", "update", "delete"]) {
|
|
3469
|
+
if (rule[action2] === void 0) warnings.push(`integration "${integration.id}" rule "${ns}.${action2}" is missing`);
|
|
2938
3470
|
}
|
|
2939
3471
|
}
|
|
2940
3472
|
for (const seed of integration.seeds ?? []) {
|
|
@@ -3105,12 +3637,12 @@ function harnessOption(value2, flag) {
|
|
|
3105
3637
|
|
|
3106
3638
|
// src/init.ts
|
|
3107
3639
|
var import_node_fs11 = require("fs");
|
|
3108
|
-
var
|
|
3109
|
-
var
|
|
3640
|
+
var import_node_path10 = require("path");
|
|
3641
|
+
var import_apps7 = require("@odla-ai/apps");
|
|
3110
3642
|
function initProject(options) {
|
|
3111
3643
|
const out = options.stdout ?? console;
|
|
3112
|
-
const rootDir = (0,
|
|
3113
|
-
const configPath = (0,
|
|
3644
|
+
const rootDir = (0, import_node_path10.resolve)(options.rootDir ?? process.cwd());
|
|
3645
|
+
const configPath = (0, import_node_path10.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
|
|
3114
3646
|
if ((0, import_node_fs11.existsSync)(configPath) && !options.force) {
|
|
3115
3647
|
throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
|
|
3116
3648
|
}
|
|
@@ -3120,19 +3652,19 @@ function initProject(options) {
|
|
|
3120
3652
|
const envs = options.envs?.length ? options.envs : ["dev"];
|
|
3121
3653
|
const services = options.services?.length ? options.services : ["db", "ai"];
|
|
3122
3654
|
for (const service of services) {
|
|
3123
|
-
const definition = (0,
|
|
3124
|
-
if (!definition) throw new Error(`--services contains unknown service "${service}" (known: ${(0,
|
|
3655
|
+
const definition = (0, import_apps7.appServiceDefinition)(service);
|
|
3656
|
+
if (!definition) throw new Error(`--services contains unknown service "${service}" (known: ${(0, import_apps7.appServiceIds)().join(", ")})`);
|
|
3125
3657
|
for (const dependency of definition.requires) {
|
|
3126
3658
|
if (!services.includes(dependency)) throw new Error(`--services ${service} requires ${dependency}`);
|
|
3127
3659
|
}
|
|
3128
3660
|
}
|
|
3129
3661
|
const aiProvider = options.aiProvider ?? "anthropic";
|
|
3130
|
-
(0, import_node_fs11.mkdirSync)((0,
|
|
3131
|
-
(0, import_node_fs11.mkdirSync)((0,
|
|
3132
|
-
(0, import_node_fs11.mkdirSync)((0,
|
|
3662
|
+
(0, import_node_fs11.mkdirSync)((0, import_node_path10.dirname)(configPath), { recursive: true });
|
|
3663
|
+
(0, import_node_fs11.mkdirSync)((0, import_node_path10.resolve)(rootDir, "src/odla"), { recursive: true });
|
|
3664
|
+
(0, import_node_fs11.mkdirSync)((0, import_node_path10.resolve)(rootDir, ".odla"), { recursive: true });
|
|
3133
3665
|
(0, import_node_fs11.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
3134
|
-
writeIfMissing((0,
|
|
3135
|
-
writeIfMissing((0,
|
|
3666
|
+
writeIfMissing((0, import_node_path10.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
3667
|
+
writeIfMissing((0, import_node_path10.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
3136
3668
|
ensureGitignore(rootDir);
|
|
3137
3669
|
out.log(`created ${relativeDisplay(configPath, rootDir)}`);
|
|
3138
3670
|
out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
|
|
@@ -3314,8 +3846,8 @@ function assertWranglerConfig(cfg) {
|
|
|
3314
3846
|
}
|
|
3315
3847
|
|
|
3316
3848
|
// src/secrets-set.ts
|
|
3317
|
-
var
|
|
3318
|
-
var
|
|
3849
|
+
var import_ai2 = require("@odla-ai/ai");
|
|
3850
|
+
var import_apps8 = require("@odla-ai/apps");
|
|
3319
3851
|
var PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
|
|
3320
3852
|
async function secretsSet(options) {
|
|
3321
3853
|
const name = (options.name ?? "").trim();
|
|
@@ -3326,7 +3858,7 @@ async function secretsSet(options) {
|
|
|
3326
3858
|
const { cfg, tenantId, value: value2, doFetch, out } = await resolveVaultWrite(options);
|
|
3327
3859
|
const token = await getDeveloperToken(cfg, options, doFetch, out);
|
|
3328
3860
|
try {
|
|
3329
|
-
await (0,
|
|
3861
|
+
await (0, import_ai2.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, name, value2);
|
|
3330
3862
|
} catch (err) {
|
|
3331
3863
|
throw new Error(scrubValue(err instanceof Error ? err.message : String(err), value2));
|
|
3332
3864
|
}
|
|
@@ -3367,13 +3899,13 @@ async function resolveVaultWrite(options) {
|
|
|
3367
3899
|
throw new Error(`refusing to store a secret for "${options.env}" without --yes`);
|
|
3368
3900
|
}
|
|
3369
3901
|
const value2 = await secretInputValue(options, "secret");
|
|
3370
|
-
return { cfg, tenantId: (0,
|
|
3902
|
+
return { cfg, tenantId: (0, import_apps8.tenantIdFor)(cfg.app.id, options.env), value: value2, doFetch, out };
|
|
3371
3903
|
}
|
|
3372
3904
|
|
|
3373
3905
|
// src/skill.ts
|
|
3374
3906
|
var import_node_fs12 = require("fs");
|
|
3375
3907
|
var import_node_os2 = require("os");
|
|
3376
|
-
var
|
|
3908
|
+
var import_node_path11 = require("path");
|
|
3377
3909
|
var import_node_url2 = require("url");
|
|
3378
3910
|
|
|
3379
3911
|
// src/skill-adapters.ts
|
|
@@ -3452,8 +3984,8 @@ function installSkill(options = {}) {
|
|
|
3452
3984
|
const files = listFiles(sourceDir);
|
|
3453
3985
|
if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
|
|
3454
3986
|
const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
|
|
3455
|
-
const root = (0,
|
|
3456
|
-
const home = (0,
|
|
3987
|
+
const root = (0, import_node_path11.resolve)(options.dir ?? process.cwd());
|
|
3988
|
+
const home = (0, import_node_path11.resolve)(options.homeDir ?? (0, import_node_os2.homedir)());
|
|
3457
3989
|
const plans = /* @__PURE__ */ new Map();
|
|
3458
3990
|
const targets = /* @__PURE__ */ new Map();
|
|
3459
3991
|
const rememberTarget = (harness, target) => {
|
|
@@ -3467,48 +3999,48 @@ function installSkill(options = {}) {
|
|
|
3467
3999
|
plans.set(target, { target, content: content2, boundary, managedMerge });
|
|
3468
4000
|
};
|
|
3469
4001
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
3470
|
-
for (const rel of files) plan((0,
|
|
4002
|
+
for (const rel of files) plan((0, import_node_path11.join)(targetDir2, rel), (0, import_node_fs12.readFileSync)((0, import_node_path11.join)(sourceDir, rel), "utf8"), false, boundary);
|
|
3471
4003
|
};
|
|
3472
4004
|
let targetDir;
|
|
3473
4005
|
if (options.global) {
|
|
3474
|
-
const claudeRoot = (0,
|
|
3475
|
-
const codexRoot = (0,
|
|
4006
|
+
const claudeRoot = (0, import_node_path11.join)(home, ".claude", "skills");
|
|
4007
|
+
const codexRoot = (0, import_node_path11.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path11.join)(home, ".codex"), "skills");
|
|
3476
4008
|
targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
|
|
3477
4009
|
for (const harness of harnesses) {
|
|
3478
4010
|
const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
|
|
3479
|
-
planSkillTree(skillRoot, harness === "claude" ? home : (0,
|
|
4011
|
+
planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path11.dirname)((0, import_node_path11.dirname)(codexRoot)));
|
|
3480
4012
|
rememberTarget(harness, skillRoot);
|
|
3481
4013
|
}
|
|
3482
4014
|
} else {
|
|
3483
|
-
const sharedRoot = (0,
|
|
4015
|
+
const sharedRoot = (0, import_node_path11.join)(root, ".agents", "skills");
|
|
3484
4016
|
planSkillTree(sharedRoot);
|
|
3485
|
-
const claudeRoot = (0,
|
|
4017
|
+
const claudeRoot = (0, import_node_path11.join)(root, ".claude", "skills");
|
|
3486
4018
|
targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
|
|
3487
4019
|
for (const harness of harnesses) rememberTarget(harness, sharedRoot);
|
|
3488
4020
|
if (harnesses.includes("claude")) {
|
|
3489
4021
|
for (const skill of skillNames(files)) {
|
|
3490
|
-
const canonical = (0, import_node_fs12.readFileSync)((0,
|
|
3491
|
-
plan((0,
|
|
4022
|
+
const canonical = (0, import_node_fs12.readFileSync)((0, import_node_path11.join)(sourceDir, skill, "SKILL.md"), "utf8");
|
|
4023
|
+
plan((0, import_node_path11.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
|
|
3492
4024
|
}
|
|
3493
4025
|
rememberTarget("claude", claudeRoot);
|
|
3494
4026
|
}
|
|
3495
4027
|
if (harnesses.includes("cursor")) {
|
|
3496
|
-
const cursorRule = (0,
|
|
4028
|
+
const cursorRule = (0, import_node_path11.join)(root, ".cursor", "rules", "odla.mdc");
|
|
3497
4029
|
plan(cursorRule, CURSOR_RULE);
|
|
3498
4030
|
rememberTarget("cursor", cursorRule);
|
|
3499
4031
|
}
|
|
3500
4032
|
if (harnesses.includes("agents")) {
|
|
3501
|
-
const agentsFile = (0,
|
|
4033
|
+
const agentsFile = (0, import_node_path11.join)(root, "AGENTS.md");
|
|
3502
4034
|
plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
3503
4035
|
rememberTarget("agents", agentsFile);
|
|
3504
4036
|
}
|
|
3505
4037
|
if (harnesses.includes("copilot")) {
|
|
3506
|
-
const copilotFile = (0,
|
|
4038
|
+
const copilotFile = (0, import_node_path11.join)(root, ".github", "copilot-instructions.md");
|
|
3507
4039
|
plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
3508
4040
|
rememberTarget("copilot", copilotFile);
|
|
3509
4041
|
}
|
|
3510
4042
|
if (harnesses.includes("gemini")) {
|
|
3511
|
-
const geminiFile = (0,
|
|
4043
|
+
const geminiFile = (0, import_node_path11.join)(root, "GEMINI.md");
|
|
3512
4044
|
plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
3513
4045
|
rememberTarget("gemini", geminiFile);
|
|
3514
4046
|
}
|
|
@@ -3544,7 +4076,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
3544
4076
|
}
|
|
3545
4077
|
for (const file of plans.values()) {
|
|
3546
4078
|
if (!(0, import_node_fs12.existsSync)(file.target) || (0, import_node_fs12.readFileSync)(file.target, "utf8") !== file.content) {
|
|
3547
|
-
(0, import_node_fs12.mkdirSync)((0,
|
|
4079
|
+
(0, import_node_fs12.mkdirSync)((0, import_node_path11.dirname)(file.target), { recursive: true });
|
|
3548
4080
|
(0, import_node_fs12.writeFileSync)(file.target, file.content);
|
|
3549
4081
|
}
|
|
3550
4082
|
}
|
|
@@ -3564,7 +4096,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
3564
4096
|
};
|
|
3565
4097
|
}
|
|
3566
4098
|
function pathsUnder(root, paths) {
|
|
3567
|
-
return [...paths].map((path) => (0,
|
|
4099
|
+
return [...paths].map((path) => (0, import_node_path11.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path11.sep}`) && !(0, import_node_path11.isAbsolute)(path)).sort();
|
|
3568
4100
|
}
|
|
3569
4101
|
function normalizeHarnesses(values, global) {
|
|
3570
4102
|
const requested = values?.length ? values : ["claude"];
|
|
@@ -3609,13 +4141,13 @@ function managedFileContent(path, block, force, boundary) {
|
|
|
3609
4141
|
return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
|
|
3610
4142
|
}
|
|
3611
4143
|
function symlinkedComponent(boundary, target) {
|
|
3612
|
-
const rel = (0,
|
|
3613
|
-
if (rel === ".." || rel.startsWith(`..${
|
|
4144
|
+
const rel = (0, import_node_path11.relative)(boundary, target);
|
|
4145
|
+
if (rel === ".." || rel.startsWith(`..${import_node_path11.sep}`) || (0, import_node_path11.isAbsolute)(rel)) {
|
|
3614
4146
|
throw new Error(`agent setup target escapes its install root: ${target}`);
|
|
3615
4147
|
}
|
|
3616
4148
|
let current = boundary;
|
|
3617
|
-
for (const part of rel.split(
|
|
3618
|
-
current = (0,
|
|
4149
|
+
for (const part of rel.split(import_node_path11.sep).filter(Boolean)) {
|
|
4150
|
+
current = (0, import_node_path11.join)(current, part);
|
|
3619
4151
|
try {
|
|
3620
4152
|
if ((0, import_node_fs12.lstatSync)(current).isSymbolicLink()) return current;
|
|
3621
4153
|
} catch (error) {
|
|
@@ -3632,9 +4164,9 @@ function listFiles(dir) {
|
|
|
3632
4164
|
const results = [];
|
|
3633
4165
|
const walk = (current) => {
|
|
3634
4166
|
for (const entry of (0, import_node_fs12.readdirSync)(current, { withFileTypes: true })) {
|
|
3635
|
-
const path = (0,
|
|
4167
|
+
const path = (0, import_node_path11.join)(current, entry.name);
|
|
3636
4168
|
if (entry.isDirectory()) walk(path);
|
|
3637
|
-
else results.push((0,
|
|
4169
|
+
else results.push((0, import_node_path11.relative)(dir, path));
|
|
3638
4170
|
}
|
|
3639
4171
|
};
|
|
3640
4172
|
walk(dir);
|
|
@@ -3710,7 +4242,7 @@ async function smoke(options) {
|
|
|
3710
4242
|
out.log(` schema: ${liveEntities.length} entities`);
|
|
3711
4243
|
const aggregateEntity = expectedEntities[0] ?? liveEntities[0];
|
|
3712
4244
|
if (aggregateEntity) {
|
|
3713
|
-
const aggregate = await
|
|
4245
|
+
const aggregate = await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
|
|
3714
4246
|
ns: aggregateEntity,
|
|
3715
4247
|
aggregate: { count: true }
|
|
3716
4248
|
});
|
|
@@ -3754,16 +4286,16 @@ async function getJson(doFetch, url, bearer) {
|
|
|
3754
4286
|
const res = await doFetch(url, {
|
|
3755
4287
|
headers: bearer ? { authorization: `Bearer ${bearer}` } : void 0
|
|
3756
4288
|
});
|
|
3757
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await
|
|
4289
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText4(res)}`);
|
|
3758
4290
|
return res.json();
|
|
3759
4291
|
}
|
|
3760
|
-
async function
|
|
4292
|
+
async function postJson2(doFetch, url, bearer, body) {
|
|
3761
4293
|
const res = await doFetch(url, {
|
|
3762
4294
|
method: "POST",
|
|
3763
4295
|
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
3764
4296
|
body: JSON.stringify(body)
|
|
3765
4297
|
});
|
|
3766
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await
|
|
4298
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText4(res)}`);
|
|
3767
4299
|
return res.json();
|
|
3768
4300
|
}
|
|
3769
4301
|
function publicConfigUrl(platformUrl, appId, env) {
|
|
@@ -3771,7 +4303,7 @@ function publicConfigUrl(platformUrl, appId, env) {
|
|
|
3771
4303
|
url.searchParams.set("env", env);
|
|
3772
4304
|
return url.toString();
|
|
3773
4305
|
}
|
|
3774
|
-
async function
|
|
4306
|
+
async function safeText4(res) {
|
|
3775
4307
|
try {
|
|
3776
4308
|
return redactSecrets((await res.text()).slice(0, 500));
|
|
3777
4309
|
} catch {
|
|
@@ -3827,6 +4359,26 @@ async function secretsCommand(parsed, deps) {
|
|
|
3827
4359
|
});
|
|
3828
4360
|
}
|
|
3829
4361
|
async function projectCommand(command, parsed, deps) {
|
|
4362
|
+
if (command === "config") {
|
|
4363
|
+
const sub = parsed.positionals[1];
|
|
4364
|
+
if (sub !== "diff" && sub !== "plan") {
|
|
4365
|
+
throw new Error(`unknown config subcommand "${sub ?? ""}". Try "odla-ai config diff --json".`);
|
|
4366
|
+
}
|
|
4367
|
+
assertArgs(parsed, ["config", "token", "email", "open", "json"], 2);
|
|
4368
|
+
const options = {
|
|
4369
|
+
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
4370
|
+
token: stringOpt(parsed.options.token),
|
|
4371
|
+
email: stringOpt(parsed.options.email),
|
|
4372
|
+
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
4373
|
+
json: parsed.options.json === true,
|
|
4374
|
+
fetch: deps.fetch,
|
|
4375
|
+
openApprovalUrl: deps.openUrl,
|
|
4376
|
+
stdout: deps.stdout
|
|
4377
|
+
};
|
|
4378
|
+
if (sub === "diff") await configDiff(options);
|
|
4379
|
+
else await configPlan(options);
|
|
4380
|
+
return true;
|
|
4381
|
+
}
|
|
3830
4382
|
if (command === "init") {
|
|
3831
4383
|
assertArgs(parsed, ["app-id", "name", "config", "env", "services", "ai-provider", "force"], 1);
|
|
3832
4384
|
initProject({
|
|
@@ -3887,7 +4439,7 @@ async function projectCommand(command, parsed, deps) {
|
|
|
3887
4439
|
// src/code-connect.ts
|
|
3888
4440
|
var import_node_fs14 = require("fs");
|
|
3889
4441
|
var import_node_os4 = require("os");
|
|
3890
|
-
var
|
|
4442
|
+
var import_node_path13 = require("path");
|
|
3891
4443
|
|
|
3892
4444
|
// ../harness/dist/chunk-QTUEF2HZ.js
|
|
3893
4445
|
var HARNESS_PROTOCOL_VERSION = 1;
|
|
@@ -4505,7 +5057,7 @@ var CamelError = class extends Error {
|
|
|
4505
5057
|
};
|
|
4506
5058
|
|
|
4507
5059
|
// ../camel/dist/chunk-L5DYU2E2.js
|
|
4508
|
-
function
|
|
5060
|
+
function canonicalJson2(value2) {
|
|
4509
5061
|
return JSON.stringify(normalize(value2));
|
|
4510
5062
|
}
|
|
4511
5063
|
async function sha256Hex(value2) {
|
|
@@ -4517,7 +5069,7 @@ function utf8Length(value2) {
|
|
|
4517
5069
|
if (typeof value2 === "string") return new TextEncoder().encode(value2).byteLength;
|
|
4518
5070
|
if (value2 instanceof Uint8Array) return value2.byteLength;
|
|
4519
5071
|
try {
|
|
4520
|
-
return new TextEncoder().encode(
|
|
5072
|
+
return new TextEncoder().encode(canonicalJson2(value2)).byteLength;
|
|
4521
5073
|
} catch {
|
|
4522
5074
|
throw new CamelError("conversion_rejected", "Unsafe input could not be canonically measured.");
|
|
4523
5075
|
}
|
|
@@ -4643,7 +5195,7 @@ async function digestCodeVerificationReceipt(fields) {
|
|
|
4643
5195
|
changedTestsRequireReview: fields.changedTestsRequireReview,
|
|
4644
5196
|
outcome: fields.outcome
|
|
4645
5197
|
};
|
|
4646
|
-
return `sha256:${await sha256Hex(
|
|
5198
|
+
return `sha256:${await sha256Hex(canonicalJson2(canonical))}`;
|
|
4647
5199
|
}
|
|
4648
5200
|
function validate(fields) {
|
|
4649
5201
|
if (fields.schemaVersion !== 1 || typeof fields.verificationId !== "string" || !ID.test(fields.verificationId) || typeof fields.trustedBaseCommitSha !== "string" || !SHA.test(fields.trustedBaseCommitSha) || !DIGEST.test(fields.trustedBaseDigest) || !DIGEST.test(fields.patchDigest) || !DIGEST.test(fields.candidateDigest) || !DIGEST.test(fields.sourceDigest) || !DIGEST.test(fields.policyDigest) || !DIGEST.test(fields.changedTestSetDigest) || !Number.isSafeInteger(fields.changedTestCount) || fields.changedTestCount < 0 || fields.changedTestCount > 1e4 || fields.changedTestsRequireReview !== fields.changedTestCount > 0 || fields.recipes.length < 1 || fields.recipes.length > 64) {
|
|
@@ -4681,9 +5233,9 @@ async function createCodePortableCheckpoint(input) {
|
|
|
4681
5233
|
const state2 = normalizeState(input.state);
|
|
4682
5234
|
validateBaseAndPatch(input.baseCommitSha, input.patch);
|
|
4683
5235
|
const patchDigest = `sha256:${await sha256Hex(input.patch)}`;
|
|
4684
|
-
const stateDigest = `sha256:${await sha256Hex(
|
|
5236
|
+
const stateDigest = `sha256:${await sha256Hex(canonicalJson2(state2))}`;
|
|
4685
5237
|
const fields = { schemaVersion: 1, baseCommitSha: input.baseCommitSha, patchDigest, state: state2, stateDigest };
|
|
4686
|
-
const checkpointDigest = `sha256:${await sha256Hex(
|
|
5238
|
+
const checkpointDigest = `sha256:${await sha256Hex(canonicalJson2(fields))}`;
|
|
4687
5239
|
return freeze({ ...fields, patch: input.patch, checkpointDigest });
|
|
4688
5240
|
}
|
|
4689
5241
|
async function verifyCodePortableCheckpoint(value2) {
|
|
@@ -4801,7 +5353,7 @@ async function digestCodeRepositorySnapshot(snapshot, limits = DEFAULT_LIMITS) {
|
|
|
4801
5353
|
commitSha: snapshot.commitSha,
|
|
4802
5354
|
files: [...snapshot.files].map((file) => ({ path: file.path, content: file.content })).sort((left, right) => left.path.localeCompare(right.path))
|
|
4803
5355
|
};
|
|
4804
|
-
return `sha256:${await sha256Hex(
|
|
5356
|
+
return `sha256:${await sha256Hex(canonicalJson2(normalized))}`;
|
|
4805
5357
|
}
|
|
4806
5358
|
function validateSnapshot(snapshot, limits) {
|
|
4807
5359
|
if (!REPOSITORY.test(snapshot.repository) || !SHA4.test(snapshot.commitSha)) {
|
|
@@ -4843,10 +5395,10 @@ var import_path9 = require("path");
|
|
|
4843
5395
|
|
|
4844
5396
|
// ../camel/dist/chunk-LAXU2AVK.js
|
|
4845
5397
|
function conversionPolicyDigest(policy) {
|
|
4846
|
-
return sha256Hex(
|
|
5398
|
+
return sha256Hex(canonicalJson2(policy));
|
|
4847
5399
|
}
|
|
4848
5400
|
function registeredIdRegistryDigest(values) {
|
|
4849
|
-
return sha256Hex(
|
|
5401
|
+
return sha256Hex(canonicalJson2(values));
|
|
4850
5402
|
}
|
|
4851
5403
|
async function createConversionRegistry(config) {
|
|
4852
5404
|
const policies = /* @__PURE__ */ new Map();
|
|
@@ -5047,10 +5599,10 @@ function createEffectPolicy(config = {}) {
|
|
|
5047
5599
|
return Object.freeze({ evaluate: (input) => evaluate(input, registries, approvalEffects) });
|
|
5048
5600
|
}
|
|
5049
5601
|
function destinationRegistryDigest(values) {
|
|
5050
|
-
return sha256Hex(
|
|
5602
|
+
return sha256Hex(canonicalJson2([...values].sort()));
|
|
5051
5603
|
}
|
|
5052
5604
|
async function evaluate(input, registries, approvals) {
|
|
5053
|
-
const actionDigest = await sha256Hex(
|
|
5605
|
+
const actionDigest = await sha256Hex(canonicalJson2(input));
|
|
5054
5606
|
const decisionId = `decision_${actionDigest.slice(0, 24)}`;
|
|
5055
5607
|
const deny = (reasonCode) => ({ outcome: "deny", decisionId, reasonCode });
|
|
5056
5608
|
if (!input.planId || !input.tool.name || !input.tool.policyId || !Number.isSafeInteger(input.tool.version)) return deny("invalid_descriptor");
|
|
@@ -6771,268 +7323,19 @@ var CodePiRuntimeEngine = class {
|
|
|
6771
7323
|
}
|
|
6772
7324
|
};
|
|
6773
7325
|
|
|
6774
|
-
// src/
|
|
7326
|
+
// src/version.ts
|
|
6775
7327
|
var import_node_fs13 = require("fs");
|
|
6776
7328
|
function cliVersion() {
|
|
6777
7329
|
const pkg = JSON.parse((0, import_node_fs13.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
|
|
6778
7330
|
return pkg.version ?? "unknown";
|
|
6779
7331
|
}
|
|
6780
|
-
function printHelp(output = console) {
|
|
6781
|
-
output.log(`odla-ai
|
|
6782
7332
|
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
working from memory: runbooks are edited
|
|
6787
|
-
live, so your training data is out of date
|
|
6788
|
-
and this is not.
|
|
6789
|
-
odla-ai runbook impact After a change: which runbooks describe the
|
|
6790
|
-
code you just touched, so you can fix any
|
|
6791
|
-
step it made wrong.
|
|
6792
|
-
|
|
6793
|
-
Usage:
|
|
6794
|
-
odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
|
|
6795
|
-
odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
|
|
6796
|
-
odla-ai doctor [--config odla.config.mjs]
|
|
6797
|
-
odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
|
|
6798
|
-
odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
|
|
6799
|
-
odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
|
|
6800
|
-
odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
|
|
6801
|
-
odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
|
|
6802
|
-
odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
6803
|
-
odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
|
|
6804
|
-
odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
|
|
6805
|
-
odla-ai app refresh-sandbox [--include-identity] [--include-files] [--dry-run] [--json] --yes
|
|
6806
|
-
odla-ai app go-live [--include-identity] [--include-files] [--dry-run] [--json] --yes
|
|
6807
|
-
odla-ai app promote [--dry-run] [--json] --yes
|
|
6808
|
-
odla-ai app rename <name> [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
6809
|
-
odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
6810
|
-
odla-ai app owners add <email> [--email <odla-account>] [--json]
|
|
6811
|
-
odla-ai app owners remove <email> [--email <odla-account>] [--json]
|
|
6812
|
-
odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
6813
|
-
odla-ai pm task list [--app <id>] [--column <c>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
6814
|
-
odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
6815
|
-
odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
6816
|
-
odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
|
|
6817
|
-
odla-ai pm task add --app <id> --title <t> [--column <c>] [--goal <id>] [--assignee <id>] [--description <text>|--body <text>] [--due <epoch-ms>] [--mutation-id <id>] [--json]
|
|
6818
|
-
odla-ai pm decision add --app <id> --title <t> --body <text> [--status <s>] [--mutation-id <id>] [--json]
|
|
6819
|
-
odla-ai pm bug add --app <id> --title <t> (--description <text>|--body <text>) [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--mutation-id <id>] [--json]
|
|
6820
|
-
odla-ai pm <goal|task|decision|bug> get <id> [--json]
|
|
6821
|
-
odla-ai pm goal set <id> [--title <t>|--status <s>|--proof <text>|--no-proof|--target <pct>|--no-target] [--mutation-id <id>] [--json]
|
|
6822
|
-
odla-ai pm task set <id> [--title <t>|--column <c>|--rank <n>|--goal <id>|--no-goal|--assignee <id>|--no-assignee|--description <text>|--body <text>|--due <epoch-ms>|--no-due] [--mutation-id <id>] [--json]
|
|
6823
|
-
odla-ai pm decision set <id> [--title <t>|--status <s>|--body <text>] [--mutation-id <id>] [--json]
|
|
6824
|
-
odla-ai pm bug set <id> [--title <t>|--status <s>|--severity <s>|--goal <id>|--no-goal|--assignee <id>|--no-assignee|--decision <id>|--no-decision|--description <text>|--body <text>] [--mutation-id <id>] [--json]
|
|
6825
|
-
odla-ai pm <goal|task|decision> done <id> [--mutation-id <id>]
|
|
6826
|
-
odla-ai pm bug done <id> [--decision <accepted-decision-id>] [--mutation-id <id>]
|
|
6827
|
-
odla-ai pm <goal|task|decision|bug> comment <id> --body "..." [--mutation-id <id>]
|
|
6828
|
-
odla-ai pm <goal|task|decision|bug> comments <id> [--json]
|
|
6829
|
-
odla-ai pm <goal|task|decision|bug> rm <id>
|
|
6830
|
-
odla-ai pm handoff --app <id> [--json]
|
|
6831
|
-
odla-ai discuss groups [--json]
|
|
6832
|
-
odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
|
|
6833
|
-
odla-ai discuss read <topic> [--limit <n> --offset <n>] [--json]
|
|
6834
|
-
odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"] [--mutation-id <id>]
|
|
6835
|
-
odla-ai discuss reply <topic> --body "..." [--markup "..."] [--mutation-id <id>]
|
|
6836
|
-
odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
|
|
6837
|
-
odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
|
|
6838
|
-
odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
|
|
6839
|
-
odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
|
|
6840
|
-
odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
|
|
6841
|
-
odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
|
|
6842
|
-
odla-ai context list [--json]
|
|
6843
|
-
odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
|
|
6844
|
-
odla-ai context remove <name> --yes [--json]
|
|
6845
|
-
odla-ai o11y status [--app <id>] [--context <name>] [--platform https://odla.ai] [--env prod] [--minutes 60] [--json]
|
|
6846
|
-
odla-ai platform status [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
|
|
6847
|
-
odla-ai whoami [--context <name>] [--platform https://odla.ai] [--json]
|
|
6848
|
-
odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
|
|
6849
|
-
odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
|
|
6850
|
-
odla-ai runbook impact [--base origin/main] [--app <id>] [--all] [--limit <n>] [--json]
|
|
6851
|
-
odla-ai runbook lint [--app <id>] [--all] [--json]
|
|
6852
|
-
odla-ai runbook list [--app <id>] [--all] [--q <text>] [--json]
|
|
6853
|
-
odla-ai runbook comment <slug> --body "..." [--app <id>]
|
|
6854
|
-
odla-ai runbook get <slug> [--app <id>]
|
|
6855
|
-
odla-ai runbook new <slug> --title <t> --file <path|-> [--summary <s>] [--requires <specs>] [--app <id>]
|
|
6856
|
-
odla-ai runbook edit <slug> [--file <path|->|--body "..."] [--note <why>] [--requires <specs>] [--app <id>]
|
|
6857
|
-
odla-ai runbook import <dir> [--visibility operator|admin] [--dry-run] [--app <id>]
|
|
6858
|
-
odla-ai runbook publish <slug> [--app <id>]
|
|
6859
|
-
odla-ai runbook visibility <slug> <operator|admin> [--app <id>]
|
|
6860
|
-
odla-ai runbook archive <slug> [--app <id>]
|
|
6861
|
-
odla-ai runbook history <slug> [--app <id>] [--json]
|
|
6862
|
-
odla-ai runbook revert <slug> --version <n> [--app <id>]
|
|
6863
|
-
odla-ai runbook rm <slug> [--app <id>]
|
|
6864
|
-
odla-ai capabilities [--json]
|
|
6865
|
-
odla-ai code connect [--env dev|prod] [--email <odla-account>] [--engine auto|container|podman|docker] [--slots <1-64>] [--once]
|
|
6866
|
-
odla-ai admin ai show [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
|
|
6867
|
-
odla-ai admin ai models [--context <name>] [--provider <id>] [--json]
|
|
6868
|
-
odla-ai admin ai set <purpose> [--context <name>] [--provider <id>] [--model <id>] [--enabled|--no-enabled]
|
|
6869
|
-
[--max-input-bytes <n>] [--max-output-tokens <n>] [--max-calls-per-run <n>] [--json]
|
|
6870
|
-
odla-ai admin ai set security [--context <name>] --discovery-provider <id> --discovery-model <id>
|
|
6871
|
-
--validation-provider <id> --validation-model <id> [--enabled|--no-enabled] [--json]
|
|
6872
|
-
odla-ai admin ai credentials [--context <name>] [--json]
|
|
6873
|
-
odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
|
|
6874
|
-
odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
|
|
6875
|
-
odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
|
|
6876
|
-
odla-ai security github connect [--repo owner/name] [--env dev] [--email <odla-account>] [--no-open]
|
|
6877
|
-
odla-ai security github disconnect --source <id> [--env dev] [--yes]
|
|
6878
|
-
odla-ai security plan [--env dev] [--json]
|
|
6879
|
-
odla-ai security sources [--env dev] [--json]
|
|
6880
|
-
odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
|
|
6881
|
-
odla-ai security status <job-id> [--follow] [--json]
|
|
6882
|
-
odla-ai security report <job-id> [--json]
|
|
6883
|
-
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
6884
|
-
odla-ai security run [target] --self --ack-redacted-source
|
|
6885
|
-
odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
6886
|
-
odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
|
|
6887
|
-
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
6888
|
-
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
6889
|
-
odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
|
|
6890
|
-
odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
|
|
6891
|
-
odla-ai version
|
|
6892
|
-
|
|
6893
|
-
Commands:
|
|
6894
|
-
agent Inspect durable agent wakeups and explicitly requeue a
|
|
6895
|
-
dead-lettered job; JSON output is stable for remote operators.
|
|
6896
|
-
runbook odla's operational procedures, stored in the database and read at
|
|
6897
|
-
the moment they are followed. "ask" gives a written, cited answer;
|
|
6898
|
-
"search" the passages behind it; "get" the whole document.
|
|
6899
|
-
"impact" diffs your working tree against a base ref and names the
|
|
6900
|
-
runbooks that describe what you changed \u2014 run it after touching a
|
|
6901
|
-
package's exported API or JSDoc, or a Studio surface. "lint"
|
|
6902
|
-
checks the corpus the other way: every odla-ai command the
|
|
6903
|
-
runbooks name is held against this CLI's real command surface,
|
|
6904
|
-
and against the minimum versions each runbook declares. A wrong step
|
|
6905
|
-
is fixed with "edit", which takes effect immediately: a runbook is
|
|
6906
|
-
a row, not a release. Runbooks describe PROCEDURE; what an export
|
|
6907
|
-
does and what it guarantees is JSDoc, rendered per package at
|
|
6908
|
-
https://odla.ai/docs and shipped in the installed .d.ts. Answering
|
|
6909
|
-
a question usually needs both.
|
|
6910
|
-
whoami Report who this terminal is authenticated as, and whether it holds
|
|
6911
|
-
platform admin. A handshake-minted device token is never admin,
|
|
6912
|
-
however the human who approved it is configured.
|
|
6913
|
-
context Explain selected config, platform, app, environment, and
|
|
6914
|
-
developer-token provenance without printing credentials or
|
|
6915
|
-
starting a device handshake. Operator work can run outside a
|
|
6916
|
-
project checkout with explicit flags or ODLA_* environment
|
|
6917
|
-
variables.
|
|
6918
|
-
setup Install offline odla runbooks for common coding-agent harnesses.
|
|
6919
|
-
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
6920
|
-
doctor Validate and summarize the project config without network calls.
|
|
6921
|
-
calendar Inspect, connect, or disconnect the live Google booking connection.
|
|
6922
|
-
app Archive (suspend, data retained), restore, export, import, or
|
|
6923
|
-
manage the co-owners of the app. Archiving takes every
|
|
6924
|
-
environment's data plane down until restored; permanent deletion
|
|
6925
|
-
has NO CLI \u2014 it requires a signed-in owner in Studio, and no
|
|
6926
|
-
machine or agent credential can purge. "app export" downloads a
|
|
6927
|
-
portable gzipped-JSONL snapshot of one database (--fresh takes a
|
|
6928
|
-
new snapshot first) \u2014 your data is always yours to take.
|
|
6929
|
-
"app import" upserts rows back in from JSON, JSONL, or a
|
|
6930
|
-
{namespace: rows} map (the shape a query returns); it writes only
|
|
6931
|
-
with --yes, so the bare command is a dry run, and it refuses to
|
|
6932
|
-
guess an id mode that would duplicate rows on a re-run.
|
|
6933
|
-
"app refresh-sandbox" replaces the sandbox with a copy of live
|
|
6934
|
-
(the routine dev loop); "app go-live" copies the sandbox into an
|
|
6935
|
-
EMPTY live database, once, at launch; "app promote" pushes only
|
|
6936
|
-
schema, rules and gates up afterwards, leaving live's rows alone.
|
|
6937
|
-
Each prints its plan and writes nothing without --yes, so the
|
|
6938
|
-
bare command is the dry run. Clerk config, triggers, allowlists,
|
|
6939
|
-
secrets and API keys never travel; identity rows stay behind
|
|
6940
|
-
unless --include-identity.
|
|
6941
|
-
"app rename <name>" changes only the display name humans read;
|
|
6942
|
-
the app id is permanent (tenants, URLs and keys embed it), so a
|
|
6943
|
-
rename never re-provisions anything or invalidates a credential.
|
|
6944
|
-
"app owners add <email>" grants a signed-up odla member the SAME
|
|
6945
|
-
full access as you; they then run their own "provision" to mint
|
|
6946
|
-
their own credentials for the shared db (the live one included)
|
|
6947
|
-
\u2014 no secret is ever copied between people.
|
|
6948
|
-
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
6949
|
-
code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
|
|
6950
|
-
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
6951
|
-
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
6952
|
-
pm Project management (via @odla-ai/pm) shared across the apps you
|
|
6953
|
-
co-own: conformance goals, kanban tasks, decisions, and bugs. With
|
|
6954
|
-
no --app, "list" spans every co-owned project (the cross-project
|
|
6955
|
-
view); with --app it scopes to one. Same device-grant auth as
|
|
6956
|
-
"app". Entities: goal (alias conformance), task (alias kanban),
|
|
6957
|
-
decision, bug. Status changes and comments post to each item's
|
|
6958
|
-
@odla-ai/chat discussion thread.
|
|
6959
|
-
discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
|
|
6960
|
-
one group per project, topics with replies, @-mentions of people,
|
|
6961
|
-
agents, PM items, and projects. Built for unattended use \u2014 post a
|
|
6962
|
-
question, then "watch" it until someone answers. "watch" exits 75
|
|
6963
|
-
(not 1) when it times out with nothing new, so a script can tell
|
|
6964
|
-
"no answer yet" from a failure and just rerun. Use "who" to look
|
|
6965
|
-
up the exact @[Label](kind/id) markup for a mention.
|
|
6966
|
-
o11y Read one stable status envelope for application RED, current
|
|
6967
|
-
live-sync load/freshness, the protected commit-to-visible
|
|
6968
|
-
canary, collector ingest/scheduler trust, Cloudflare-owned
|
|
6969
|
-
runtime metrics, and a machine verdict.
|
|
6970
|
-
--json keeps auth progress on stderr for unattended agents.
|
|
6971
|
-
platform Read canonical fleet health, releases, provider load/freshness,
|
|
6972
|
-
explicit unknowns, and next actions through a read-only grant.
|
|
6973
|
-
provision Register services, compose integrations, persist credentials, optionally push secrets.
|
|
6974
|
-
smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
|
|
6975
|
-
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
6976
|
-
copilot, gemini, or agents (repeatable or comma-separated).
|
|
6977
|
-
secrets Push configured db/o11y secrets into the Worker via wrangler
|
|
6978
|
-
stdin; set stores a tenant-vault secret and set-clerk-key the
|
|
6979
|
-
reserved Clerk secret key, write-only from stdin or an env var.
|
|
6980
|
-
version Print the CLI version.
|
|
6981
|
-
|
|
6982
|
-
Safety:
|
|
6983
|
-
Every app has two databases on odla.ai: a sandbox (env "dev", tenant
|
|
6984
|
-
<appId>--dev) and a live one (env "prod", tenant <appId>). Both are served by
|
|
6985
|
-
production odla.ai \u2014 there is no separate odla to point at. New projects get
|
|
6986
|
-
the sandbox only. Add "prod" explicitly to odla.config.mjs and pass --yes to
|
|
6987
|
-
provision the live database; use --dry-run first to inspect the resolved plan.
|
|
6988
|
-
Provision caches the approved developer token and service credentials under
|
|
6989
|
-
.odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
|
|
6990
|
-
preflights Wrangler before any shown-once issuance or destructive rotation.
|
|
6991
|
-
Projectless PM, Discussions, o11y, runbook, and identity commands use
|
|
6992
|
-
--platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
|
|
6993
|
-
ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
|
|
6994
|
-
select it explicitly with --context or ODLA_CONTEXT. Each named context gets
|
|
6995
|
-
isolated developer and scoped-token caches. Override their locations with
|
|
6996
|
-
ODLA_DEV_TOKEN_FILE and ODLA_ADMIN_TOKEN_FILE; ODLA_CONTEXT_FILE relocates
|
|
6997
|
-
the metadata file. Flags and specific ODLA_* scope variables beat a selected
|
|
6998
|
-
context, which beats project config. There is no ambient current context.
|
|
6999
|
-
"context show" reports only provenance and cache state and never authenticates.
|
|
7000
|
-
Provision opens the approval page in your browser automatically whenever the
|
|
7001
|
-
machine can show one, including agent-driven runs; only CI, SSH, and
|
|
7002
|
-
display-less hosts skip it. Use --open to force or --no-open to suppress.
|
|
7003
|
-
Browser launch is best-effort: the printed approval URL is authoritative and
|
|
7004
|
-
agents must relay it to the human verbatim. A started handshake is persisted
|
|
7005
|
-
under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
|
|
7006
|
-
the same code. Outside an interactive terminal the wait is capped (90s by
|
|
7007
|
-
default, --wait <seconds> to change); a still-pending handshake then exits
|
|
7008
|
-
with code 75: relay the URL, wait for approval, and re-run to collect.
|
|
7009
|
-
A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
|
|
7010
|
-
The email is a non-secret identity hint: never provide a password or session
|
|
7011
|
-
token. The matching account must already exist, be signed in, explicitly
|
|
7012
|
-
review the exact code, and finish any current request before claiming another.
|
|
7013
|
-
Run Code from a GitHub checkout already connected to an app in Studio; an
|
|
7014
|
-
odla.config.mjs may select the app explicitly but is not required. Code host
|
|
7015
|
-
approval and credential hashes live in odla-ai/db. The host
|
|
7016
|
-
credential is never written under .odla/; it exists only in the foreground
|
|
7017
|
-
"code connect" process and is rotated by the next approved connection.
|
|
7018
|
-
Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
|
|
7019
|
-
OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
|
|
7020
|
-
GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
|
|
7021
|
-
for a PAT or provider key. GitHub read access is separate from the explicit
|
|
7022
|
-
--ack-redacted-source consent and the exact --plan-digest printed by security
|
|
7023
|
-
plan are required before bounded snippets reach System AI.
|
|
7024
|
-
Run security plan first to inspect the admin-selected providers, models,
|
|
7025
|
-
per-route bounds, credential readiness, retention, no-execution boundary,
|
|
7026
|
-
and digest that binds consent to that exact plan.
|
|
7027
|
-
`);
|
|
7028
|
-
}
|
|
7029
|
-
|
|
7030
|
-
// src/security-hosted-github.ts
|
|
7031
|
-
var import_node_child_process4 = require("child_process");
|
|
7032
|
-
var import_node_util2 = require("util");
|
|
7333
|
+
// src/security-hosted-github.ts
|
|
7334
|
+
var import_node_child_process4 = require("child_process");
|
|
7335
|
+
var import_node_util2 = require("util");
|
|
7033
7336
|
|
|
7034
7337
|
// src/security-hosted-request.ts
|
|
7035
|
-
async function requestHostedSecurityJson(options, path, init,
|
|
7338
|
+
async function requestHostedSecurityJson(options, path, init, action2) {
|
|
7036
7339
|
const platform = platformAudience(options.platform);
|
|
7037
7340
|
const token = hostedSecurityCredential(options.token);
|
|
7038
7341
|
const requestInit = {
|
|
@@ -7052,7 +7355,7 @@ async function requestHostedSecurityJson(options, path, init, action) {
|
|
|
7052
7355
|
if (!response2.ok) {
|
|
7053
7356
|
const code = optionalHostedText(body.error?.code, "error code", 128);
|
|
7054
7357
|
const message2 = optionalHostedText(body.error?.message, "error message", 300);
|
|
7055
|
-
throw new Error(`${
|
|
7358
|
+
throw new Error(`${action2} failed (${response2.status})${code ? ` ${code}` : ""}${message2 ? `: ${message2}` : ""}`);
|
|
7056
7359
|
}
|
|
7057
7360
|
return body;
|
|
7058
7361
|
}
|
|
@@ -7243,7 +7546,7 @@ async function defaultReadOrigin(cwd) {
|
|
|
7243
7546
|
|
|
7244
7547
|
// src/code-local-source.ts
|
|
7245
7548
|
var import_node_child_process5 = require("child_process");
|
|
7246
|
-
var
|
|
7549
|
+
var import_node_crypto2 = require("crypto");
|
|
7247
7550
|
var SOURCE_LIMITS2 = { maxFiles: 2e4, maxBytes: 512 * 1024 * 1024 };
|
|
7248
7551
|
async function prepareCodeLocalSource(cwd, repository, readHead = readGitHead) {
|
|
7249
7552
|
const headCommitSha = await readHead(cwd);
|
|
@@ -7294,15 +7597,15 @@ async function readGitHead(cwd) {
|
|
|
7294
7597
|
return value2;
|
|
7295
7598
|
}
|
|
7296
7599
|
function digestText(value2) {
|
|
7297
|
-
return `sha256:${(0,
|
|
7600
|
+
return `sha256:${(0, import_node_crypto2.createHash)("sha256").update(value2).digest("hex")}`;
|
|
7298
7601
|
}
|
|
7299
7602
|
|
|
7300
7603
|
// src/code-images.ts
|
|
7301
7604
|
var import_node_child_process6 = require("child_process");
|
|
7302
|
-
var
|
|
7605
|
+
var import_node_crypto3 = require("crypto");
|
|
7303
7606
|
var import_promises10 = require("fs/promises");
|
|
7304
7607
|
var import_node_os3 = require("os");
|
|
7305
|
-
var
|
|
7608
|
+
var import_node_path12 = require("path");
|
|
7306
7609
|
var import_node_url3 = require("url");
|
|
7307
7610
|
|
|
7308
7611
|
// src/code-runtime-config.ts
|
|
@@ -7385,13 +7688,13 @@ async function embeddedPiImageName() {
|
|
|
7385
7688
|
const bundle = await (0, import_promises10.readFile)(embeddedPiAssetPath()).catch(() => {
|
|
7386
7689
|
throw new Error("CLI-embedded Pi runtime is missing; reinstall this exact @odla-ai/cli version");
|
|
7387
7690
|
});
|
|
7388
|
-
return `odla-ai/pi-agent:embedded-sha256-${(0,
|
|
7691
|
+
return `odla-ai/pi-agent:embedded-sha256-${(0, import_node_crypto3.createHash)("sha256").update(bundle).digest("hex")}`;
|
|
7389
7692
|
}
|
|
7390
7693
|
async function buildEmbeddedPiImage(engine, image, run) {
|
|
7391
|
-
const context = await (0, import_promises10.mkdtemp)((0,
|
|
7694
|
+
const context = await (0, import_promises10.mkdtemp)((0, import_node_path12.join)((0, import_node_os3.tmpdir)(), "odla-code-pi-"));
|
|
7392
7695
|
try {
|
|
7393
|
-
await (0, import_promises10.copyFile)(embeddedPiAssetPath(), (0,
|
|
7394
|
-
await (0, import_promises10.writeFile)((0,
|
|
7696
|
+
await (0, import_promises10.copyFile)(embeddedPiAssetPath(), (0, import_node_path12.join)(context, "pi-agent.js"));
|
|
7697
|
+
await (0, import_promises10.writeFile)((0, import_node_path12.join)(context, "Dockerfile"), [
|
|
7395
7698
|
`FROM ${CODE_NODE_IMAGE}`,
|
|
7396
7699
|
"COPY pi-agent.js /opt/odla/pi-agent.js",
|
|
7397
7700
|
"WORKDIR /workspace",
|
|
@@ -7407,7 +7710,7 @@ async function buildEmbeddedPiImage(engine, image, run) {
|
|
|
7407
7710
|
// src/code-connect.ts
|
|
7408
7711
|
async function codeConnect(options) {
|
|
7409
7712
|
const cwd = options.cwd ?? process.cwd();
|
|
7410
|
-
const configPath = (0,
|
|
7713
|
+
const configPath = (0, import_node_path13.resolve)(cwd, options.configPath);
|
|
7411
7714
|
const cfg = (0, import_node_fs14.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
|
|
7412
7715
|
const requestedAppId = options.appId?.trim();
|
|
7413
7716
|
if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
|
|
@@ -7575,9 +7878,9 @@ function parseConnection(value2, appId, appEnv) {
|
|
|
7575
7878
|
}
|
|
7576
7879
|
return root;
|
|
7577
7880
|
}
|
|
7578
|
-
function apiFailure(
|
|
7881
|
+
function apiFailure(action2, status, value2) {
|
|
7579
7882
|
const message2 = record4(record4(value2)?.error)?.message;
|
|
7580
|
-
return `${
|
|
7883
|
+
return `${action2} failed (${status})${typeof message2 === "string" ? `: ${message2}` : ""}`;
|
|
7581
7884
|
}
|
|
7582
7885
|
function record4(value2) {
|
|
7583
7886
|
return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
|
|
@@ -7644,136 +7947,391 @@ function clean3(value2) {
|
|
|
7644
7947
|
return normalized || void 0;
|
|
7645
7948
|
}
|
|
7646
7949
|
|
|
7647
|
-
// src/context-command.ts
|
|
7648
|
-
async function contextCommand(parsed, deps = {}) {
|
|
7649
|
-
assertArgs(
|
|
7650
|
-
parsed,
|
|
7651
|
-
[
|
|
7652
|
-
"config",
|
|
7653
|
-
"context",
|
|
7654
|
-
"platform",
|
|
7655
|
-
"app",
|
|
7656
|
-
"env",
|
|
7657
|
-
"token",
|
|
7658
|
-
"json",
|
|
7659
|
-
"yes"
|
|
7660
|
-
],
|
|
7661
|
-
3
|
|
7662
|
-
);
|
|
7663
|
-
const
|
|
7664
|
-
const out = deps.stdout ?? console;
|
|
7665
|
-
if (
|
|
7666
|
-
const profiles = listOperatorProfiles();
|
|
7667
|
-
if (parsed.options.json === true) {
|
|
7668
|
-
out.log(JSON.stringify({ schemaVersion: 1, profiles }, null, 2));
|
|
7669
|
-
return;
|
|
7670
|
-
}
|
|
7671
|
-
if (profiles.length === 0) {
|
|
7672
|
-
out.log("No named operator contexts.");
|
|
7673
|
-
return;
|
|
7674
|
-
}
|
|
7675
|
-
out.log("name platform app environment");
|
|
7676
|
-
for (const profile of profiles) {
|
|
7677
|
-
out.log(
|
|
7678
|
-
`${profile.name} ${profile.platform} ${profile.app ?? ""} ${profile.environment ?? ""}`
|
|
7679
|
-
);
|
|
7680
|
-
}
|
|
7681
|
-
return;
|
|
7682
|
-
}
|
|
7683
|
-
if (
|
|
7684
|
-
if (parsed.options.token !== void 0) {
|
|
7685
|
-
throw new Error(
|
|
7686
|
-
"context save does not accept --token; contexts store scope metadata only"
|
|
7687
|
-
);
|
|
7688
|
-
}
|
|
7689
|
-
const name = requireName(parsed);
|
|
7690
|
-
const context2 = await resolveOperatorContext(parsed, {
|
|
7691
|
-
allowMissingConfig: true
|
|
7692
|
-
});
|
|
7693
|
-
const profile = {
|
|
7694
|
-
platform: context2.platform.value,
|
|
7695
|
-
...context2.app.value ? { app: context2.app.value } : {},
|
|
7696
|
-
...context2.environment.value ? { environment: context2.environment.value } : {}
|
|
7697
|
-
};
|
|
7698
|
-
saveOperatorProfile(name, profile, context2.profile.file);
|
|
7699
|
-
if (parsed.options.json === true) {
|
|
7700
|
-
out.log(JSON.stringify({ schemaVersion: 1, name, ...profile }, null, 2));
|
|
7701
|
-
} else {
|
|
7702
|
-
out.log(`saved operator context "${name}" (${profile.platform})`);
|
|
7703
|
-
}
|
|
7704
|
-
return;
|
|
7705
|
-
}
|
|
7706
|
-
if (
|
|
7707
|
-
const name = requireName(parsed);
|
|
7708
|
-
if (parsed.options.yes !== true) {
|
|
7709
|
-
throw new Error(`context remove "${name}" requires --yes`);
|
|
7710
|
-
}
|
|
7711
|
-
const removed = removeOperatorProfile(name);
|
|
7712
|
-
const result2 = {
|
|
7713
|
-
schemaVersion: 1,
|
|
7714
|
-
name,
|
|
7715
|
-
removed,
|
|
7716
|
-
credentialCachesRemoved: false,
|
|
7717
|
-
grantsRevoked: false
|
|
7718
|
-
};
|
|
7719
|
-
if (parsed.options.json === true) out.log(JSON.stringify(result2, null, 2));
|
|
7720
|
-
else {
|
|
7721
|
-
const status = removed ? `removed operator context "${name}"` : `operator context "${name}" was already absent`;
|
|
7722
|
-
out.log(`${status}; credential caches were not deleted or revoked`);
|
|
7723
|
-
}
|
|
7724
|
-
return;
|
|
7725
|
-
}
|
|
7726
|
-
if (
|
|
7727
|
-
throw new Error(
|
|
7728
|
-
`unknown context action "${
|
|
7729
|
-
);
|
|
7730
|
-
}
|
|
7731
|
-
const context = await resolveOperatorContext(parsed, {
|
|
7732
|
-
allowMissingConfig: true,
|
|
7733
|
-
defaultEnvironment: "prod"
|
|
7734
|
-
});
|
|
7735
|
-
const token = developerTokenStatus(context, parsed);
|
|
7736
|
-
const result = {
|
|
7737
|
-
schemaVersion: 1,
|
|
7738
|
-
profile: context.profile,
|
|
7739
|
-
config: context.config,
|
|
7740
|
-
platform: context.platform,
|
|
7741
|
-
app: context.app,
|
|
7742
|
-
environment: context.environment,
|
|
7743
|
-
authentication: {
|
|
7744
|
-
developerToken: token,
|
|
7745
|
-
scopedTokenCacheFile: context.credentials.scopedTokenFile,
|
|
7746
|
-
handshakeStarted: false
|
|
7747
|
-
}
|
|
7748
|
-
};
|
|
7749
|
-
if (parsed.options.json === true) {
|
|
7750
|
-
out.log(JSON.stringify(result, null, 2));
|
|
7751
|
-
return;
|
|
7752
|
-
}
|
|
7753
|
-
out.log(`config: ${context.config.status} (${context.config.path})`);
|
|
7754
|
-
out.log(
|
|
7755
|
-
`context: ${context.profile.name ?? "(none)"} (${context.profile.source}; ${context.profile.file})`
|
|
7756
|
-
);
|
|
7757
|
-
out.log(
|
|
7758
|
-
`platform: ${context.platform.value} (${context.platform.source})`
|
|
7759
|
-
);
|
|
7760
|
-
out.log(
|
|
7761
|
-
`app: ${context.app.value ?? "(unresolved)"} (${context.app.source})`
|
|
7762
|
-
);
|
|
7763
|
-
out.log(
|
|
7764
|
-
`env: ${context.environment.value ?? "(unresolved)"} (${context.environment.source})`
|
|
7765
|
-
);
|
|
7766
|
-
out.log(
|
|
7767
|
-
`auth: ${token.source} (cache ${token.cacheStatus}: ${token.cacheFile})`
|
|
7768
|
-
);
|
|
7769
|
-
out.log("handshake: not started");
|
|
7770
|
-
}
|
|
7771
|
-
function requireName(parsed) {
|
|
7772
|
-
const name = parsed.positionals[2]?.trim();
|
|
7773
|
-
if (!name) {
|
|
7774
|
-
throw new Error(`context ${parsed.positionals[1]} needs a profile name`);
|
|
7775
|
-
}
|
|
7776
|
-
return name;
|
|
7950
|
+
// src/context-command.ts
|
|
7951
|
+
async function contextCommand(parsed, deps = {}) {
|
|
7952
|
+
assertArgs(
|
|
7953
|
+
parsed,
|
|
7954
|
+
[
|
|
7955
|
+
"config",
|
|
7956
|
+
"context",
|
|
7957
|
+
"platform",
|
|
7958
|
+
"app",
|
|
7959
|
+
"env",
|
|
7960
|
+
"token",
|
|
7961
|
+
"json",
|
|
7962
|
+
"yes"
|
|
7963
|
+
],
|
|
7964
|
+
3
|
|
7965
|
+
);
|
|
7966
|
+
const action2 = parsed.positionals[1];
|
|
7967
|
+
const out = deps.stdout ?? console;
|
|
7968
|
+
if (action2 === "list") {
|
|
7969
|
+
const profiles = listOperatorProfiles();
|
|
7970
|
+
if (parsed.options.json === true) {
|
|
7971
|
+
out.log(JSON.stringify({ schemaVersion: 1, profiles }, null, 2));
|
|
7972
|
+
return;
|
|
7973
|
+
}
|
|
7974
|
+
if (profiles.length === 0) {
|
|
7975
|
+
out.log("No named operator contexts.");
|
|
7976
|
+
return;
|
|
7977
|
+
}
|
|
7978
|
+
out.log("name platform app environment");
|
|
7979
|
+
for (const profile of profiles) {
|
|
7980
|
+
out.log(
|
|
7981
|
+
`${profile.name} ${profile.platform} ${profile.app ?? ""} ${profile.environment ?? ""}`
|
|
7982
|
+
);
|
|
7983
|
+
}
|
|
7984
|
+
return;
|
|
7985
|
+
}
|
|
7986
|
+
if (action2 === "save") {
|
|
7987
|
+
if (parsed.options.token !== void 0) {
|
|
7988
|
+
throw new Error(
|
|
7989
|
+
"context save does not accept --token; contexts store scope metadata only"
|
|
7990
|
+
);
|
|
7991
|
+
}
|
|
7992
|
+
const name = requireName(parsed);
|
|
7993
|
+
const context2 = await resolveOperatorContext(parsed, {
|
|
7994
|
+
allowMissingConfig: true
|
|
7995
|
+
});
|
|
7996
|
+
const profile = {
|
|
7997
|
+
platform: context2.platform.value,
|
|
7998
|
+
...context2.app.value ? { app: context2.app.value } : {},
|
|
7999
|
+
...context2.environment.value ? { environment: context2.environment.value } : {}
|
|
8000
|
+
};
|
|
8001
|
+
saveOperatorProfile(name, profile, context2.profile.file);
|
|
8002
|
+
if (parsed.options.json === true) {
|
|
8003
|
+
out.log(JSON.stringify({ schemaVersion: 1, name, ...profile }, null, 2));
|
|
8004
|
+
} else {
|
|
8005
|
+
out.log(`saved operator context "${name}" (${profile.platform})`);
|
|
8006
|
+
}
|
|
8007
|
+
return;
|
|
8008
|
+
}
|
|
8009
|
+
if (action2 === "remove") {
|
|
8010
|
+
const name = requireName(parsed);
|
|
8011
|
+
if (parsed.options.yes !== true) {
|
|
8012
|
+
throw new Error(`context remove "${name}" requires --yes`);
|
|
8013
|
+
}
|
|
8014
|
+
const removed = removeOperatorProfile(name);
|
|
8015
|
+
const result2 = {
|
|
8016
|
+
schemaVersion: 1,
|
|
8017
|
+
name,
|
|
8018
|
+
removed,
|
|
8019
|
+
credentialCachesRemoved: false,
|
|
8020
|
+
grantsRevoked: false
|
|
8021
|
+
};
|
|
8022
|
+
if (parsed.options.json === true) out.log(JSON.stringify(result2, null, 2));
|
|
8023
|
+
else {
|
|
8024
|
+
const status = removed ? `removed operator context "${name}"` : `operator context "${name}" was already absent`;
|
|
8025
|
+
out.log(`${status}; credential caches were not deleted or revoked`);
|
|
8026
|
+
}
|
|
8027
|
+
return;
|
|
8028
|
+
}
|
|
8029
|
+
if (action2 !== "show") {
|
|
8030
|
+
throw new Error(
|
|
8031
|
+
`unknown context action "${action2 ?? ""}". Try show|list|save|remove.`
|
|
8032
|
+
);
|
|
8033
|
+
}
|
|
8034
|
+
const context = await resolveOperatorContext(parsed, {
|
|
8035
|
+
allowMissingConfig: true,
|
|
8036
|
+
defaultEnvironment: "prod"
|
|
8037
|
+
});
|
|
8038
|
+
const token = developerTokenStatus(context, parsed);
|
|
8039
|
+
const result = {
|
|
8040
|
+
schemaVersion: 1,
|
|
8041
|
+
profile: context.profile,
|
|
8042
|
+
config: context.config,
|
|
8043
|
+
platform: context.platform,
|
|
8044
|
+
app: context.app,
|
|
8045
|
+
environment: context.environment,
|
|
8046
|
+
authentication: {
|
|
8047
|
+
developerToken: token,
|
|
8048
|
+
scopedTokenCacheFile: context.credentials.scopedTokenFile,
|
|
8049
|
+
handshakeStarted: false
|
|
8050
|
+
}
|
|
8051
|
+
};
|
|
8052
|
+
if (parsed.options.json === true) {
|
|
8053
|
+
out.log(JSON.stringify(result, null, 2));
|
|
8054
|
+
return;
|
|
8055
|
+
}
|
|
8056
|
+
out.log(`config: ${context.config.status} (${context.config.path})`);
|
|
8057
|
+
out.log(
|
|
8058
|
+
`context: ${context.profile.name ?? "(none)"} (${context.profile.source}; ${context.profile.file})`
|
|
8059
|
+
);
|
|
8060
|
+
out.log(
|
|
8061
|
+
`platform: ${context.platform.value} (${context.platform.source})`
|
|
8062
|
+
);
|
|
8063
|
+
out.log(
|
|
8064
|
+
`app: ${context.app.value ?? "(unresolved)"} (${context.app.source})`
|
|
8065
|
+
);
|
|
8066
|
+
out.log(
|
|
8067
|
+
`env: ${context.environment.value ?? "(unresolved)"} (${context.environment.source})`
|
|
8068
|
+
);
|
|
8069
|
+
out.log(
|
|
8070
|
+
`auth: ${token.source} (cache ${token.cacheStatus}: ${token.cacheFile})`
|
|
8071
|
+
);
|
|
8072
|
+
out.log("handshake: not started");
|
|
8073
|
+
}
|
|
8074
|
+
function requireName(parsed) {
|
|
8075
|
+
const name = parsed.positionals[2]?.trim();
|
|
8076
|
+
if (!name) {
|
|
8077
|
+
throw new Error(`context ${parsed.positionals[1]} needs a profile name`);
|
|
8078
|
+
}
|
|
8079
|
+
return name;
|
|
8080
|
+
}
|
|
8081
|
+
|
|
8082
|
+
// src/help.ts
|
|
8083
|
+
function printHelp(output = console) {
|
|
8084
|
+
output.log(`odla-ai
|
|
8085
|
+
|
|
8086
|
+
Start here:
|
|
8087
|
+
odla-ai runbook ask "<question>" The current procedure, from odla's own
|
|
8088
|
+
runbooks. Ask BEFORE searching the web or
|
|
8089
|
+
working from memory: runbooks are edited
|
|
8090
|
+
live, so your training data is out of date
|
|
8091
|
+
and this is not.
|
|
8092
|
+
odla-ai runbook impact After a change: which runbooks describe the
|
|
8093
|
+
code you just touched, so you can fix any
|
|
8094
|
+
step it made wrong.
|
|
8095
|
+
|
|
8096
|
+
Usage:
|
|
8097
|
+
odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
|
|
8098
|
+
odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
|
|
8099
|
+
odla-ai doctor [--config odla.config.mjs]
|
|
8100
|
+
odla-ai config diff [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
8101
|
+
odla-ai config plan [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
8102
|
+
odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
|
|
8103
|
+
odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
|
|
8104
|
+
odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
|
|
8105
|
+
odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
|
|
8106
|
+
odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
|
|
8107
|
+
odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
8108
|
+
odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
|
|
8109
|
+
odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
|
|
8110
|
+
odla-ai app refresh-sandbox [--include-identity] [--include-files] [--dry-run] [--json] --yes
|
|
8111
|
+
odla-ai app go-live [--include-identity] [--include-files] [--dry-run] [--json] --yes
|
|
8112
|
+
odla-ai app promote [--dry-run] [--json] --yes
|
|
8113
|
+
odla-ai app rename <name> [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
8114
|
+
odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
8115
|
+
odla-ai app owners add <email> [--email <odla-account>] [--json]
|
|
8116
|
+
odla-ai app owners remove <email> [--email <odla-account>] [--json]
|
|
8117
|
+
odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
8118
|
+
odla-ai pm task list [--app <id>] [--column <c>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
8119
|
+
odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
8120
|
+
odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
8121
|
+
odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
|
|
8122
|
+
odla-ai pm task add --app <id> --title <t> [--column <c>] [--goal <id>] [--assignee <id>] [--description <text>|--body <text>] [--due <epoch-ms>] [--mutation-id <id>] [--json]
|
|
8123
|
+
odla-ai pm decision add --app <id> --title <t> --body <text> [--status <s>] [--mutation-id <id>] [--json]
|
|
8124
|
+
odla-ai pm bug add --app <id> --title <t> (--description <text>|--body <text>) [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--mutation-id <id>] [--json]
|
|
8125
|
+
odla-ai pm <goal|task|decision|bug> get <id> [--json]
|
|
8126
|
+
odla-ai pm goal set <id> [--title <t>|--status <s>|--proof <text>|--no-proof|--target <pct>|--no-target] [--mutation-id <id>] [--json]
|
|
8127
|
+
odla-ai pm task set <id> [--title <t>|--column <c>|--rank <n>|--goal <id>|--no-goal|--assignee <id>|--no-assignee|--description <text>|--body <text>|--due <epoch-ms>|--no-due] [--mutation-id <id>] [--json]
|
|
8128
|
+
odla-ai pm decision set <id> [--title <t>|--status <s>|--body <text>] [--mutation-id <id>] [--json]
|
|
8129
|
+
odla-ai pm bug set <id> [--title <t>|--status <s>|--severity <s>|--goal <id>|--no-goal|--assignee <id>|--no-assignee|--decision <id>|--no-decision|--description <text>|--body <text>] [--mutation-id <id>] [--json]
|
|
8130
|
+
odla-ai pm <goal|task|decision> done <id> [--mutation-id <id>]
|
|
8131
|
+
odla-ai pm bug done <id> [--decision <accepted-decision-id>] [--mutation-id <id>]
|
|
8132
|
+
odla-ai pm <goal|task|decision|bug> comment <id> --body "..." [--mutation-id <id>]
|
|
8133
|
+
odla-ai pm <goal|task|decision|bug> comments <id> [--json]
|
|
8134
|
+
odla-ai pm <goal|task|decision|bug> rm <id>
|
|
8135
|
+
odla-ai pm handoff --app <id> [--json]
|
|
8136
|
+
odla-ai discuss groups [--json]
|
|
8137
|
+
odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
|
|
8138
|
+
odla-ai discuss read <topic> [--limit <n> --offset <n>] [--json]
|
|
8139
|
+
odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"] [--mutation-id <id>]
|
|
8140
|
+
odla-ai discuss reply <topic> --body "..." [--markup "..."] [--mutation-id <id>]
|
|
8141
|
+
odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
|
|
8142
|
+
odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
|
|
8143
|
+
odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
|
|
8144
|
+
odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
|
|
8145
|
+
odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
|
|
8146
|
+
odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
|
|
8147
|
+
odla-ai context list [--json]
|
|
8148
|
+
odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
|
|
8149
|
+
odla-ai context remove <name> --yes [--json]
|
|
8150
|
+
odla-ai o11y status [--app <id>] [--context <name>] [--platform https://odla.ai] [--env prod] [--minutes 60] [--json]
|
|
8151
|
+
odla-ai platform status [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
|
|
8152
|
+
odla-ai whoami [--context <name>] [--platform https://odla.ai] [--json]
|
|
8153
|
+
odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
|
|
8154
|
+
odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
|
|
8155
|
+
odla-ai runbook impact [--base origin/main] [--app <id>] [--all] [--limit <n>] [--json]
|
|
8156
|
+
odla-ai runbook lint [--app <id>] [--all] [--json]
|
|
8157
|
+
odla-ai runbook list [--app <id>] [--all] [--q <text>] [--json]
|
|
8158
|
+
odla-ai runbook comment <slug> --body "..." [--app <id>]
|
|
8159
|
+
odla-ai runbook get <slug> [--app <id>]
|
|
8160
|
+
odla-ai runbook new <slug> --title <t> --file <path|-> [--summary <s>] [--requires <specs>] [--app <id>]
|
|
8161
|
+
odla-ai runbook edit <slug> [--file <path|->|--body "..."] [--note <why>] [--requires <specs>] [--app <id>]
|
|
8162
|
+
odla-ai runbook import <dir> [--visibility operator|admin] [--dry-run] [--app <id>]
|
|
8163
|
+
odla-ai runbook publish <slug> [--app <id>]
|
|
8164
|
+
odla-ai runbook visibility <slug> <operator|admin> [--app <id>]
|
|
8165
|
+
odla-ai runbook archive <slug> [--app <id>]
|
|
8166
|
+
odla-ai runbook history <slug> [--app <id>] [--json]
|
|
8167
|
+
odla-ai runbook revert <slug> --version <n> [--app <id>]
|
|
8168
|
+
odla-ai runbook rm <slug> [--app <id>]
|
|
8169
|
+
odla-ai capabilities [--json]
|
|
8170
|
+
odla-ai code connect [--env dev|prod] [--email <odla-account>] [--engine auto|container|podman|docker] [--slots <1-64>] [--once]
|
|
8171
|
+
odla-ai admin ai show [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
|
|
8172
|
+
odla-ai admin ai models [--context <name>] [--provider <id>] [--json]
|
|
8173
|
+
odla-ai admin ai set <purpose> [--context <name>] [--provider <id>] [--model <id>] [--enabled|--no-enabled]
|
|
8174
|
+
[--max-input-bytes <n>] [--max-output-tokens <n>] [--max-calls-per-run <n>] [--json]
|
|
8175
|
+
odla-ai admin ai set security [--context <name>] --discovery-provider <id> --discovery-model <id>
|
|
8176
|
+
--validation-provider <id> --validation-model <id> [--enabled|--no-enabled] [--json]
|
|
8177
|
+
odla-ai admin ai credentials [--context <name>] [--json]
|
|
8178
|
+
odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
|
|
8179
|
+
odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
|
|
8180
|
+
odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
|
|
8181
|
+
odla-ai security github connect [--repo owner/name] [--env dev] [--email <odla-account>] [--no-open]
|
|
8182
|
+
odla-ai security github disconnect --source <id> [--env dev] [--yes]
|
|
8183
|
+
odla-ai security plan [--env dev] [--json]
|
|
8184
|
+
odla-ai security sources [--env dev] [--json]
|
|
8185
|
+
odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
|
|
8186
|
+
odla-ai security status <job-id> [--follow] [--json]
|
|
8187
|
+
odla-ai security report <job-id> [--json]
|
|
8188
|
+
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
8189
|
+
odla-ai security run [target] --self --ack-redacted-source
|
|
8190
|
+
odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
8191
|
+
odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
|
|
8192
|
+
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
8193
|
+
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
8194
|
+
odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
|
|
8195
|
+
odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
|
|
8196
|
+
odla-ai version
|
|
8197
|
+
|
|
8198
|
+
Commands:
|
|
8199
|
+
agent Inspect durable agent wakeups and explicitly requeue a
|
|
8200
|
+
dead-lettered job; JSON output is stable for remote operators.
|
|
8201
|
+
runbook odla's operational procedures, stored in the database and read at
|
|
8202
|
+
the moment they are followed. "ask" gives a written, cited answer;
|
|
8203
|
+
"search" the passages behind it; "get" the whole document.
|
|
8204
|
+
"impact" diffs your working tree against a base ref and names the
|
|
8205
|
+
runbooks that describe what you changed \u2014 run it after touching a
|
|
8206
|
+
package's exported API or JSDoc, or a Studio surface. "lint"
|
|
8207
|
+
checks the corpus the other way: every odla-ai command the
|
|
8208
|
+
runbooks name is held against this CLI's real command surface,
|
|
8209
|
+
and against the minimum versions each runbook declares. A wrong step
|
|
8210
|
+
is fixed with "edit", which takes effect immediately: a runbook is
|
|
8211
|
+
a row, not a release. Runbooks describe PROCEDURE; what an export
|
|
8212
|
+
does and what it guarantees is JSDoc, rendered per package at
|
|
8213
|
+
https://odla.ai/docs and shipped in the installed .d.ts. Answering
|
|
8214
|
+
a question usually needs both.
|
|
8215
|
+
whoami Report who this terminal is authenticated as, and whether it holds
|
|
8216
|
+
platform admin. A handshake-minted device token is never admin,
|
|
8217
|
+
however the human who approved it is configured.
|
|
8218
|
+
context Explain selected config, platform, app, environment, and
|
|
8219
|
+
developer-token provenance without printing credentials or
|
|
8220
|
+
starting a device handshake. Operator work can run outside a
|
|
8221
|
+
project checkout with explicit flags or ODLA_* environment
|
|
8222
|
+
variables.
|
|
8223
|
+
setup Install offline odla runbooks for common coding-agent harnesses.
|
|
8224
|
+
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
8225
|
+
doctor Validate and summarize the project config without network calls.
|
|
8226
|
+
config Read-only diff and revision-bound plan for checked-in Registry
|
|
8227
|
+
intent; runtime-owned fields and excluded coverage stay explicit.
|
|
8228
|
+
calendar Inspect, connect, or disconnect the live Google booking connection.
|
|
8229
|
+
app Archive (suspend, data retained), restore, export, import, or
|
|
8230
|
+
manage the co-owners of the app. Archiving takes every
|
|
8231
|
+
environment's data plane down until restored; permanent deletion
|
|
8232
|
+
has NO CLI \u2014 it requires a signed-in owner in Studio, and no
|
|
8233
|
+
machine or agent credential can purge. "app export" downloads a
|
|
8234
|
+
portable gzipped-JSONL snapshot of one database (--fresh takes a
|
|
8235
|
+
new snapshot first) \u2014 your data is always yours to take.
|
|
8236
|
+
"app import" upserts rows back in from JSON, JSONL, or a
|
|
8237
|
+
{namespace: rows} map (the shape a query returns); it writes only
|
|
8238
|
+
with --yes, so the bare command is a dry run, and it refuses to
|
|
8239
|
+
guess an id mode that would duplicate rows on a re-run.
|
|
8240
|
+
"app refresh-sandbox" replaces the sandbox with a copy of live
|
|
8241
|
+
(the routine dev loop); "app go-live" copies the sandbox into an
|
|
8242
|
+
EMPTY live database, once, at launch; "app promote" pushes only
|
|
8243
|
+
schema, rules and gates up afterwards, leaving live's rows alone.
|
|
8244
|
+
Each prints its plan and writes nothing without --yes, so the
|
|
8245
|
+
bare command is the dry run. Clerk config, triggers, allowlists,
|
|
8246
|
+
secrets and API keys never travel; identity rows stay behind
|
|
8247
|
+
unless --include-identity.
|
|
8248
|
+
"app rename <name>" changes only the display name humans read;
|
|
8249
|
+
the app id is permanent (tenants, URLs and keys embed it), so a
|
|
8250
|
+
rename never re-provisions anything or invalidates a credential.
|
|
8251
|
+
"app owners add <email>" grants a signed-up odla member the SAME
|
|
8252
|
+
full access as you; they then run their own "provision" to mint
|
|
8253
|
+
their own credentials for the shared db (the live one included)
|
|
8254
|
+
\u2014 no secret is ever copied between people.
|
|
8255
|
+
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
8256
|
+
code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
|
|
8257
|
+
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
8258
|
+
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
8259
|
+
pm Project management (via @odla-ai/pm) shared across the apps you
|
|
8260
|
+
co-own: conformance goals, kanban tasks, decisions, and bugs. With
|
|
8261
|
+
no --app, "list" spans every co-owned project (the cross-project
|
|
8262
|
+
view); with --app it scopes to one. Same device-grant auth as
|
|
8263
|
+
"app". Entities: goal (alias conformance), task (alias kanban),
|
|
8264
|
+
decision, bug. Status changes and comments post to each item's
|
|
8265
|
+
@odla-ai/chat discussion thread.
|
|
8266
|
+
discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
|
|
8267
|
+
one group per project, topics with replies, @-mentions of people,
|
|
8268
|
+
agents, PM items, and projects. Built for unattended use \u2014 post a
|
|
8269
|
+
question, then "watch" it until someone answers. "watch" exits 75
|
|
8270
|
+
(not 1) when it times out with nothing new, so a script can tell
|
|
8271
|
+
"no answer yet" from a failure and just rerun. Use "who" to look
|
|
8272
|
+
up the exact @[Label](kind/id) markup for a mention.
|
|
8273
|
+
o11y Read one stable status envelope for application RED, current
|
|
8274
|
+
live-sync load/freshness, the protected commit-to-visible
|
|
8275
|
+
canary, collector ingest/scheduler trust, Cloudflare-owned
|
|
8276
|
+
runtime metrics, and a machine verdict.
|
|
8277
|
+
--json keeps auth progress on stderr for unattended agents.
|
|
8278
|
+
platform Read canonical fleet health, releases, provider load/freshness,
|
|
8279
|
+
explicit unknowns, and next actions through a read-only grant.
|
|
8280
|
+
provision Register services, compose integrations, persist credentials, optionally push secrets.
|
|
8281
|
+
smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
|
|
8282
|
+
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
8283
|
+
copilot, gemini, or agents (repeatable or comma-separated).
|
|
8284
|
+
secrets Push configured db/o11y secrets into the Worker via wrangler
|
|
8285
|
+
stdin; set stores a tenant-vault secret and set-clerk-key the
|
|
8286
|
+
reserved Clerk secret key, write-only from stdin or an env var.
|
|
8287
|
+
version Print the CLI version.
|
|
8288
|
+
|
|
8289
|
+
Safety:
|
|
8290
|
+
Every app has two databases on odla.ai: a sandbox (env "dev", tenant
|
|
8291
|
+
<appId>--dev) and a live one (env "prod", tenant <appId>). Both are served by
|
|
8292
|
+
production odla.ai \u2014 there is no separate odla to point at. New projects get
|
|
8293
|
+
the sandbox only. Add "prod" explicitly to odla.config.mjs and pass --yes to
|
|
8294
|
+
provision the live database; use --dry-run first to inspect the resolved plan.
|
|
8295
|
+
Provision caches the approved developer token and service credentials under
|
|
8296
|
+
.odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
|
|
8297
|
+
preflights Wrangler before any shown-once issuance or destructive rotation.
|
|
8298
|
+
Projectless PM, Discussions, o11y, runbook, and identity commands use
|
|
8299
|
+
--platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
|
|
8300
|
+
ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
|
|
8301
|
+
select it explicitly with --context or ODLA_CONTEXT. Each named context gets
|
|
8302
|
+
isolated developer and scoped-token caches. Override their locations with
|
|
8303
|
+
ODLA_DEV_TOKEN_FILE and ODLA_ADMIN_TOKEN_FILE; ODLA_CONTEXT_FILE relocates
|
|
8304
|
+
the metadata file. Flags and specific ODLA_* scope variables beat a selected
|
|
8305
|
+
context, which beats project config. There is no ambient current context.
|
|
8306
|
+
"context show" reports only provenance and cache state and never authenticates.
|
|
8307
|
+
Provision opens the approval page in your browser automatically whenever the
|
|
8308
|
+
machine can show one, including agent-driven runs; only CI, SSH, and
|
|
8309
|
+
display-less hosts skip it. Use --open to force or --no-open to suppress.
|
|
8310
|
+
Browser launch is best-effort: the printed approval URL is authoritative and
|
|
8311
|
+
agents must relay it to the human verbatim. A started handshake is persisted
|
|
8312
|
+
under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
|
|
8313
|
+
the same code. Outside an interactive terminal the wait is capped (90s by
|
|
8314
|
+
default, --wait <seconds> to change); a still-pending handshake then exits
|
|
8315
|
+
with code 75: relay the URL, wait for approval, and re-run to collect.
|
|
8316
|
+
A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
|
|
8317
|
+
The email is a non-secret identity hint: never provide a password or session
|
|
8318
|
+
token. The matching account must already exist, be signed in, explicitly
|
|
8319
|
+
review the exact code, and finish any current request before claiming another.
|
|
8320
|
+
Run Code from a GitHub checkout already connected to an app in Studio; an
|
|
8321
|
+
odla.config.mjs may select the app explicitly but is not required. Code host
|
|
8322
|
+
approval and credential hashes live in odla-ai/db. The host
|
|
8323
|
+
credential is never written under .odla/; it exists only in the foreground
|
|
8324
|
+
"code connect" process and is rotated by the next approved connection.
|
|
8325
|
+
Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
|
|
8326
|
+
OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
|
|
8327
|
+
GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
|
|
8328
|
+
for a PAT or provider key. GitHub read access is separate from the explicit
|
|
8329
|
+
--ack-redacted-source consent and the exact --plan-digest printed by security
|
|
8330
|
+
plan are required before bounded snippets reach System AI.
|
|
8331
|
+
Run security plan first to inspect the admin-selected providers, models,
|
|
8332
|
+
per-route bounds, credential readiness, retention, no-execution boundary,
|
|
8333
|
+
and digest that binds consent to that exact plan.
|
|
8334
|
+
`);
|
|
7777
8335
|
}
|
|
7778
8336
|
|
|
7779
8337
|
// src/discuss-actions.ts
|
|
@@ -8197,8 +8755,8 @@ var ALLOWED = [
|
|
|
8197
8755
|
"platform",
|
|
8198
8756
|
"context"
|
|
8199
8757
|
];
|
|
8200
|
-
function requireId(id,
|
|
8201
|
-
if (!id) throw new Error(`"discuss ${
|
|
8758
|
+
function requireId(id, action2) {
|
|
8759
|
+
if (!id) throw new Error(`"discuss ${action2}" needs a topic id`);
|
|
8202
8760
|
return id;
|
|
8203
8761
|
}
|
|
8204
8762
|
async function buildContext(parsed, deps) {
|
|
@@ -8232,11 +8790,11 @@ async function buildContext(parsed, deps) {
|
|
|
8232
8790
|
}
|
|
8233
8791
|
async function discussCommand(parsed, deps = {}) {
|
|
8234
8792
|
assertArgs(parsed, ALLOWED, 3);
|
|
8235
|
-
const
|
|
8793
|
+
const action2 = parsed.positionals[1];
|
|
8236
8794
|
const id = parsed.positionals[2];
|
|
8237
|
-
if (!
|
|
8795
|
+
if (!action2) throw new Error('"discuss" needs an action. Run "odla-ai help".');
|
|
8238
8796
|
const ctx = await buildContext(parsed, deps);
|
|
8239
|
-
switch (
|
|
8797
|
+
switch (action2) {
|
|
8240
8798
|
case "groups":
|
|
8241
8799
|
return discussGroups(ctx);
|
|
8242
8800
|
case "list":
|
|
@@ -8258,7 +8816,7 @@ async function discussCommand(parsed, deps = {}) {
|
|
|
8258
8816
|
return;
|
|
8259
8817
|
}
|
|
8260
8818
|
default:
|
|
8261
|
-
throw new Error(`unknown discuss action "${
|
|
8819
|
+
throw new Error(`unknown discuss action "${action2}". Run "odla-ai help".`);
|
|
8262
8820
|
}
|
|
8263
8821
|
}
|
|
8264
8822
|
|
|
@@ -8519,18 +9077,18 @@ var ENTITY_OPTIONS = {
|
|
|
8519
9077
|
done: ["decision"]
|
|
8520
9078
|
}
|
|
8521
9079
|
};
|
|
8522
|
-
function canonicalAction(
|
|
8523
|
-
if (
|
|
8524
|
-
if (
|
|
8525
|
-
if (
|
|
8526
|
-
return
|
|
8527
|
-
}
|
|
8528
|
-
function allowedOptions(entity,
|
|
8529
|
-
const entityOptions =
|
|
8530
|
-
return [...COMMON_OPTIONS, ...ACTION_OPTIONS[
|
|
8531
|
-
}
|
|
8532
|
-
function requireId2(id,
|
|
8533
|
-
if (!id) throw new Error(`"pm ... ${
|
|
9080
|
+
function canonicalAction(action2) {
|
|
9081
|
+
if (action2 === "create") return "add";
|
|
9082
|
+
if (action2 === "update" || action2 === "status" || action2 === "move") return "set";
|
|
9083
|
+
if (action2 === "delete") return "rm";
|
|
9084
|
+
return action2 in ACTION_OPTIONS ? action2 : null;
|
|
9085
|
+
}
|
|
9086
|
+
function allowedOptions(entity, action2) {
|
|
9087
|
+
const entityOptions = action2 === "list" || action2 === "add" || action2 === "set" || action2 === "done" ? ENTITY_OPTIONS[entity][action2] : [];
|
|
9088
|
+
return [...COMMON_OPTIONS, ...ACTION_OPTIONS[action2], ...entityOptions];
|
|
9089
|
+
}
|
|
9090
|
+
function requireId2(id, action2) {
|
|
9091
|
+
if (!id) throw new Error(`"pm ... ${action2}" needs an item id`);
|
|
8534
9092
|
return id;
|
|
8535
9093
|
}
|
|
8536
9094
|
async function buildContext2(parsed, deps) {
|
|
@@ -8566,28 +9124,28 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
8566
9124
|
const entity = ALIASES[word];
|
|
8567
9125
|
if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
|
|
8568
9126
|
const requestedAction = parsed.positionals[2] ?? "list";
|
|
8569
|
-
const
|
|
8570
|
-
if (!
|
|
8571
|
-
assertArgs(parsed, allowedOptions(entity,
|
|
9127
|
+
const action2 = canonicalAction(requestedAction);
|
|
9128
|
+
if (!action2) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|comment|comments|rm.`);
|
|
9129
|
+
assertArgs(parsed, allowedOptions(entity, action2), 4);
|
|
8572
9130
|
const ctx = await buildContext2(parsed, deps);
|
|
8573
9131
|
const id = parsed.positionals[3];
|
|
8574
|
-
switch (
|
|
9132
|
+
switch (action2) {
|
|
8575
9133
|
case "list":
|
|
8576
9134
|
return pmList(ctx, entity, parsed);
|
|
8577
9135
|
case "add":
|
|
8578
9136
|
return pmAdd(ctx, entity, parsed);
|
|
8579
9137
|
case "get":
|
|
8580
|
-
return pmGet(ctx, entity, requireId2(id,
|
|
9138
|
+
return pmGet(ctx, entity, requireId2(id, action2));
|
|
8581
9139
|
case "set":
|
|
8582
|
-
return pmSet(ctx, entity, requireId2(id,
|
|
9140
|
+
return pmSet(ctx, entity, requireId2(id, action2), parsed);
|
|
8583
9141
|
case "done":
|
|
8584
|
-
return pmDone(ctx, entity, requireId2(id,
|
|
9142
|
+
return pmDone(ctx, entity, requireId2(id, action2), parsed);
|
|
8585
9143
|
case "comment":
|
|
8586
|
-
return pmComment(ctx, entity, requireId2(id,
|
|
9144
|
+
return pmComment(ctx, entity, requireId2(id, action2), parsed);
|
|
8587
9145
|
case "comments":
|
|
8588
|
-
return pmComments(ctx, entity, requireId2(id,
|
|
9146
|
+
return pmComments(ctx, entity, requireId2(id, action2));
|
|
8589
9147
|
case "rm":
|
|
8590
|
-
return pmRemove(ctx, entity, requireId2(id,
|
|
9148
|
+
return pmRemove(ctx, entity, requireId2(id, action2));
|
|
8591
9149
|
}
|
|
8592
9150
|
}
|
|
8593
9151
|
|
|
@@ -8620,8 +9178,8 @@ function printPlatformStatus(status, out) {
|
|
|
8620
9178
|
if (status.verdict.reasons.length) {
|
|
8621
9179
|
out.log(`reasons ${status.verdict.reasons.join(", ")}`);
|
|
8622
9180
|
}
|
|
8623
|
-
for (const
|
|
8624
|
-
out.log(`next ${
|
|
9181
|
+
for (const action2 of status.nextActions) {
|
|
9182
|
+
out.log(`next ${action2.service} ${action2.code} ${action2.message}`);
|
|
8625
9183
|
}
|
|
8626
9184
|
}
|
|
8627
9185
|
function value(input) {
|
|
@@ -8641,10 +9199,10 @@ async function platformCommand(parsed, deps = {}) {
|
|
|
8641
9199
|
["config", "context", "platform", "token", "email", "open", "json"],
|
|
8642
9200
|
2
|
|
8643
9201
|
);
|
|
8644
|
-
const
|
|
8645
|
-
if (
|
|
9202
|
+
const action2 = parsed.positionals[1];
|
|
9203
|
+
if (action2 !== "status") {
|
|
8646
9204
|
throw new Error(
|
|
8647
|
-
`unknown platform action "${
|
|
9205
|
+
`unknown platform action "${action2 ?? ""}". Try "odla-ai platform status --json".`
|
|
8648
9206
|
);
|
|
8649
9207
|
}
|
|
8650
9208
|
const context = await resolveOperatorContext(parsed, {
|
|
@@ -8973,10 +9531,10 @@ async function o11yCommand(parsed, deps = {}) {
|
|
|
8973
9531
|
],
|
|
8974
9532
|
2
|
|
8975
9533
|
);
|
|
8976
|
-
const
|
|
8977
|
-
if (
|
|
9534
|
+
const action2 = parsed.positionals[1];
|
|
9535
|
+
if (action2 !== "status") {
|
|
8978
9536
|
throw new Error(
|
|
8979
|
-
`unknown o11y action "${
|
|
9537
|
+
`unknown o11y action "${action2 ?? ""}". Try "odla-ai o11y status --json".`
|
|
8980
9538
|
);
|
|
8981
9539
|
}
|
|
8982
9540
|
const minutes = statusMinutes(
|
|
@@ -9105,7 +9663,7 @@ async function read2(url, headers, doFetch) {
|
|
|
9105
9663
|
}
|
|
9106
9664
|
|
|
9107
9665
|
// src/provision.ts
|
|
9108
|
-
var
|
|
9666
|
+
var import_apps10 = require("@odla-ai/apps");
|
|
9109
9667
|
var import_ai3 = require("@odla-ai/ai");
|
|
9110
9668
|
var import_node_process10 = __toESM(require("process"), 1);
|
|
9111
9669
|
|
|
@@ -9115,7 +9673,7 @@ async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, int
|
|
|
9115
9673
|
const base = `${endpoint}/app/${encodeURIComponent(tenantId)}`;
|
|
9116
9674
|
for (const integration of integrations) {
|
|
9117
9675
|
for (const seed of integration.seeds ?? []) {
|
|
9118
|
-
const payload = await
|
|
9676
|
+
const payload = await postJson3(doFetch, `${base}/query`, dbKey, {
|
|
9119
9677
|
query: { [seed.ns]: { $: { where: { [seed.key.attr]: seed.key.value }, limit: 1 } } }
|
|
9120
9678
|
});
|
|
9121
9679
|
const rows = isRecord7(payload) && isRecord7(payload.result) ? payload.result[seed.ns] : void 0;
|
|
@@ -9126,7 +9684,7 @@ async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, int
|
|
|
9126
9684
|
out.log(`${env}: integration ${integration.id} seed ${seed.id} already exists`);
|
|
9127
9685
|
continue;
|
|
9128
9686
|
}
|
|
9129
|
-
await
|
|
9687
|
+
await postJson3(doFetch, `${base}/transact`, dbKey, {
|
|
9130
9688
|
mutationId: `integration:${integration.id}:seed:${seed.id}`,
|
|
9131
9689
|
ops: [{
|
|
9132
9690
|
t: "update",
|
|
@@ -9141,7 +9699,7 @@ async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, int
|
|
|
9141
9699
|
}
|
|
9142
9700
|
}
|
|
9143
9701
|
}
|
|
9144
|
-
async function
|
|
9702
|
+
async function postJson3(doFetch, url, bearer, body) {
|
|
9145
9703
|
const res = await doFetch(url, {
|
|
9146
9704
|
method: "POST",
|
|
9147
9705
|
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
@@ -9162,9 +9720,9 @@ async function responseText(res) {
|
|
|
9162
9720
|
}
|
|
9163
9721
|
|
|
9164
9722
|
// src/provision-credentials.ts
|
|
9165
|
-
var
|
|
9723
|
+
var import_apps9 = require("@odla-ai/apps");
|
|
9166
9724
|
async function provisionEnvCredentials(opts) {
|
|
9167
|
-
const tenantId = (0,
|
|
9725
|
+
const tenantId = (0, import_apps9.tenantIdFor)(opts.cfg.app.id, opts.env);
|
|
9168
9726
|
const prior = opts.credentials?.envs[opts.env];
|
|
9169
9727
|
let credentials = opts.credentials;
|
|
9170
9728
|
let dbKey = opts.cfg.services.includes("db") && !opts.rotateDb ? prior?.dbKey : void 0;
|
|
@@ -9220,14 +9778,14 @@ async function mintDbKey(opts, tenantId) {
|
|
|
9220
9778
|
appId: tenantId
|
|
9221
9779
|
})
|
|
9222
9780
|
});
|
|
9223
|
-
if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await
|
|
9781
|
+
if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText5(created)}`);
|
|
9224
9782
|
res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/keys`, {
|
|
9225
9783
|
method: "POST",
|
|
9226
9784
|
headers,
|
|
9227
9785
|
body: "{}"
|
|
9228
9786
|
});
|
|
9229
9787
|
}
|
|
9230
|
-
if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await
|
|
9788
|
+
if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText5(res)}`);
|
|
9231
9789
|
const body = await res.json();
|
|
9232
9790
|
if (!body.key) throw new Error(`db key mint (${tenantId}) returned no key`);
|
|
9233
9791
|
return body.key;
|
|
@@ -9243,58 +9801,11 @@ async function issueO11yToken(opts) {
|
|
|
9243
9801
|
`o11y token already exists for env "${opts.env}", but its shown-once value is not in the local credentials file; run "odla-ai provision --rotate-o11y-token --push-secrets" to replace it explicitly`
|
|
9244
9802
|
);
|
|
9245
9803
|
}
|
|
9246
|
-
if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await
|
|
9804
|
+
if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText5(res)}`);
|
|
9247
9805
|
const body = await res.json();
|
|
9248
9806
|
if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
|
|
9249
9807
|
return body.token;
|
|
9250
9808
|
}
|
|
9251
|
-
async function safeText4(res) {
|
|
9252
|
-
try {
|
|
9253
|
-
return redactSecrets((await res.text()).slice(0, 500));
|
|
9254
|
-
} catch {
|
|
9255
|
-
return "";
|
|
9256
|
-
}
|
|
9257
|
-
}
|
|
9258
|
-
|
|
9259
|
-
// src/provision-helpers.ts
|
|
9260
|
-
var import_ai2 = require("@odla-ai/ai");
|
|
9261
|
-
var import_apps6 = require("@odla-ai/apps");
|
|
9262
|
-
function defaultSecretName(provider) {
|
|
9263
|
-
const names = import_ai2.DEFAULT_SECRET_NAMES;
|
|
9264
|
-
return names[provider] ?? `${provider}_api_key`;
|
|
9265
|
-
}
|
|
9266
|
-
async function assertTenantAdminAccess(doFetch, cfg, env, token) {
|
|
9267
|
-
const tenantId = (0, import_apps6.tenantIdFor)(cfg.app.id, env);
|
|
9268
|
-
const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/entitlements`, {
|
|
9269
|
-
headers: { authorization: `Bearer ${token}` }
|
|
9270
|
-
});
|
|
9271
|
-
if (res.ok || res.status === 404) return;
|
|
9272
|
-
if (res.status === 403) {
|
|
9273
|
-
throw new Error(
|
|
9274
|
-
`${env}: you are not an owner of "${cfg.app.id}" (tenant ${tenantId}) \u2014 nothing was minted or written; ask an existing owner to run "odla-ai app owners add <your-email>", then re-run provision`
|
|
9275
|
-
);
|
|
9276
|
-
}
|
|
9277
|
-
throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText5(res)}`);
|
|
9278
|
-
}
|
|
9279
|
-
async function postJson3(doFetch, url, bearer, body) {
|
|
9280
|
-
const res = await doFetch(url, {
|
|
9281
|
-
method: "POST",
|
|
9282
|
-
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
9283
|
-
body: JSON.stringify(body)
|
|
9284
|
-
});
|
|
9285
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText5(res)}`);
|
|
9286
|
-
}
|
|
9287
|
-
function normalizeClerkConfig(value2) {
|
|
9288
|
-
if (!value2) return null;
|
|
9289
|
-
if (typeof value2 === "string") {
|
|
9290
|
-
const publishableKey2 = envValue(value2);
|
|
9291
|
-
return publishableKey2 ? { publishableKey: publishableKey2 } : null;
|
|
9292
|
-
}
|
|
9293
|
-
if (typeof value2 !== "object") return null;
|
|
9294
|
-
const cfg = value2;
|
|
9295
|
-
const publishableKey = envValue(cfg.publishableKey);
|
|
9296
|
-
return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
|
|
9297
|
-
}
|
|
9298
9809
|
async function safeText5(res) {
|
|
9299
9810
|
try {
|
|
9300
9811
|
return redactSecrets((await res.text()).slice(0, 500));
|
|
@@ -9373,7 +9884,7 @@ async function provision(options) {
|
|
|
9373
9884
|
}
|
|
9374
9885
|
const doFetch = options.fetch ?? fetch;
|
|
9375
9886
|
const token = await getDeveloperToken(cfg, options, doFetch, out);
|
|
9376
|
-
const apps = (0,
|
|
9887
|
+
const apps = (0, import_apps10.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
|
|
9377
9888
|
const existing = await apps.resolveApp(cfg.app.id);
|
|
9378
9889
|
if (existing) {
|
|
9379
9890
|
out.log(`app: ${cfg.app.id} already exists`);
|
|
@@ -9384,7 +9895,7 @@ async function provision(options) {
|
|
|
9384
9895
|
for (const env of cfg.envs) {
|
|
9385
9896
|
await assertTenantAdminAccess(doFetch, cfg, env, token);
|
|
9386
9897
|
}
|
|
9387
|
-
const serviceOrder = (0,
|
|
9898
|
+
const serviceOrder = (0, import_apps10.orderAppServices)(cfg.services);
|
|
9388
9899
|
for (const env of cfg.envs) {
|
|
9389
9900
|
for (const service of serviceOrder) {
|
|
9390
9901
|
if (service === "ai") {
|
|
@@ -9417,7 +9928,7 @@ async function provision(options) {
|
|
|
9417
9928
|
}
|
|
9418
9929
|
}
|
|
9419
9930
|
for (const env of cfg.envs) {
|
|
9420
|
-
const tenantId = (0,
|
|
9931
|
+
const tenantId = (0, import_apps10.tenantIdFor)(cfg.app.id, env);
|
|
9421
9932
|
credentials = await provisionEnvCredentials({
|
|
9422
9933
|
cfg,
|
|
9423
9934
|
env,
|
|
@@ -9450,11 +9961,11 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
9450
9961
|
}
|
|
9451
9962
|
}
|
|
9452
9963
|
if (schema && dbKey) {
|
|
9453
|
-
await
|
|
9964
|
+
await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
|
|
9454
9965
|
out.log(`${env}: schema pushed`);
|
|
9455
9966
|
}
|
|
9456
9967
|
if (rules) {
|
|
9457
|
-
await
|
|
9968
|
+
await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/admin/rules`, token, rules);
|
|
9458
9969
|
out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
|
|
9459
9970
|
}
|
|
9460
9971
|
if (dbKey) {
|
|
@@ -9551,6 +10062,7 @@ var COMMAND_SURFACE = {
|
|
|
9551
10062
|
calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
|
|
9552
10063
|
capabilities: {},
|
|
9553
10064
|
code: { connect: {} },
|
|
10065
|
+
config: { diff: {}, plan: {} },
|
|
9554
10066
|
context: { show: {}, list: {}, save: {}, remove: {} },
|
|
9555
10067
|
// `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
|
|
9556
10068
|
discuss: {
|
|
@@ -9835,7 +10347,7 @@ async function runbookRemove(ctx, slug) {
|
|
|
9835
10347
|
|
|
9836
10348
|
// src/runbook-import.ts
|
|
9837
10349
|
var import_node_fs17 = require("fs");
|
|
9838
|
-
var
|
|
10350
|
+
var import_node_path14 = require("path");
|
|
9839
10351
|
function parseRunbook(text, slug) {
|
|
9840
10352
|
let rest = text;
|
|
9841
10353
|
const meta = {};
|
|
@@ -9864,8 +10376,8 @@ function readRunbookDir(dir) {
|
|
|
9864
10376
|
const files = (0, import_node_fs17.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
|
|
9865
10377
|
if (!files.length) throw new Error(`no .md files in ${dir}`);
|
|
9866
10378
|
return files.map((file) => {
|
|
9867
|
-
const slug = (0,
|
|
9868
|
-
const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0,
|
|
10379
|
+
const slug = (0, import_node_path14.basename)(file, ".md");
|
|
10380
|
+
const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0, import_node_path14.join)(dir, file), "utf8"), slug);
|
|
9869
10381
|
return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
|
|
9870
10382
|
});
|
|
9871
10383
|
}
|
|
@@ -9938,7 +10450,7 @@ async function upsert(ctx, r, visibility) {
|
|
|
9938
10450
|
// src/runbook-impact.ts
|
|
9939
10451
|
var import_node_child_process7 = require("child_process");
|
|
9940
10452
|
var import_node_fs18 = require("fs");
|
|
9941
|
-
var
|
|
10453
|
+
var import_node_path15 = require("path");
|
|
9942
10454
|
|
|
9943
10455
|
// src/runbook-impact-scan.ts
|
|
9944
10456
|
var DECL = /^[+-]\s*export\s+(?:declare\s+)?(?:default\s+)?(?:abstract\s+)?(?:async\s+)?(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z_$][\w$]*)/;
|
|
@@ -10107,7 +10619,7 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
|
|
|
10107
10619
|
}
|
|
10108
10620
|
function manifestLabeller(root) {
|
|
10109
10621
|
return (workspace) => {
|
|
10110
|
-
const manifest = (0,
|
|
10622
|
+
const manifest = (0, import_node_path15.join)(root, workspace, "package.json");
|
|
10111
10623
|
if (!(0, import_node_fs18.existsSync)(manifest)) return void 0;
|
|
10112
10624
|
try {
|
|
10113
10625
|
const name = JSON.parse((0, import_node_fs18.readFileSync)(manifest, "utf8")).name;
|
|
@@ -10177,7 +10689,7 @@ function report3(ctx, impacts) {
|
|
|
10177
10689
|
async function runbookImpact(ctx, options, deps = {}) {
|
|
10178
10690
|
const cwd = deps.cwd ?? process.cwd();
|
|
10179
10691
|
const runGit = deps.runGit ?? gitRunner(cwd);
|
|
10180
|
-
const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0,
|
|
10692
|
+
const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0, import_node_path15.join)(cwd, path), "utf8"));
|
|
10181
10693
|
const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
|
|
10182
10694
|
if (!surfaces.length) {
|
|
10183
10695
|
return ctx.out.log(
|
|
@@ -10312,7 +10824,7 @@ async function runbookComment(ctx, slug, body) {
|
|
|
10312
10824
|
var import_node_child_process8 = require("child_process");
|
|
10313
10825
|
var import_node_fs19 = require("fs");
|
|
10314
10826
|
var import_node_os5 = require("os");
|
|
10315
|
-
var
|
|
10827
|
+
var import_node_path16 = require("path");
|
|
10316
10828
|
var import_node_process12 = __toESM(require("process"), 1);
|
|
10317
10829
|
var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
|
|
10318
10830
|
function resolveEditor(env = import_node_process12.default.env) {
|
|
@@ -10338,8 +10850,8 @@ function editText(initial, slug, deps = {}) {
|
|
|
10338
10850
|
);
|
|
10339
10851
|
if (!interactive())
|
|
10340
10852
|
throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
|
|
10341
|
-
const dir = (0, import_node_fs19.mkdtempSync)((0,
|
|
10342
|
-
const file = (0,
|
|
10853
|
+
const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path16.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
|
|
10854
|
+
const file = (0, import_node_path16.join)(dir, `${slug}.md`);
|
|
10343
10855
|
try {
|
|
10344
10856
|
(0, import_node_fs19.writeFileSync)(file, initial, { mode: 384 });
|
|
10345
10857
|
const code = defaultRunOrInjected(deps)(editor, file);
|
|
@@ -10447,12 +10959,12 @@ var ALLOWED2 = [
|
|
|
10447
10959
|
"platform",
|
|
10448
10960
|
"context"
|
|
10449
10961
|
];
|
|
10450
|
-
function requireSlug(slug,
|
|
10451
|
-
if (!slug) throw new Error(`"runbook ${
|
|
10962
|
+
function requireSlug(slug, action2) {
|
|
10963
|
+
if (!slug) throw new Error(`"runbook ${action2}" needs a slug, e.g. "odla-ai runbook ${action2} release"`);
|
|
10452
10964
|
return slug;
|
|
10453
10965
|
}
|
|
10454
10966
|
var WRITES = /* @__PURE__ */ new Set(["new", "edit", "publish", "archive", "visibility", "revert", "rm", "import"]);
|
|
10455
|
-
async function buildContext3(parsed, deps,
|
|
10967
|
+
async function buildContext3(parsed, deps, action2) {
|
|
10456
10968
|
const appIdOption = stringOpt(parsed.options.app);
|
|
10457
10969
|
const context = await resolveOperatorContext(parsed, {
|
|
10458
10970
|
allowMissingConfig: true
|
|
@@ -10462,7 +10974,7 @@ async function buildContext3(parsed, deps, action) {
|
|
|
10462
10974
|
const out = deps.stdout ?? console;
|
|
10463
10975
|
const appId = appIdOption ?? (context.app.source === "environment" || context.app.source === "profile" ? context.app.value : null) ?? PLATFORM_SCOPE;
|
|
10464
10976
|
const dryRun = parsed.options["dry-run"] === true;
|
|
10465
|
-
if (
|
|
10977
|
+
if (action2 === "import" && dryRun) {
|
|
10466
10978
|
return {
|
|
10467
10979
|
platformUrl: cfg.platformUrl,
|
|
10468
10980
|
token: "",
|
|
@@ -10472,12 +10984,12 @@ async function buildContext3(parsed, deps, action) {
|
|
|
10472
10984
|
appId
|
|
10473
10985
|
};
|
|
10474
10986
|
}
|
|
10475
|
-
const needsCapability = WRITES.has(
|
|
10987
|
+
const needsCapability = WRITES.has(action2) && !dryRun && appId === PLATFORM_SCOPE && !stringOpt(parsed.options.token);
|
|
10476
10988
|
const token = needsCapability ? await getScopedPlatformToken({
|
|
10477
10989
|
platform: cfg.platformUrl,
|
|
10478
10990
|
scope: "platform:runbook:write",
|
|
10479
10991
|
email: stringOpt(parsed.options.email),
|
|
10480
|
-
label: `odla CLI (runbook ${
|
|
10992
|
+
label: `odla CLI (runbook ${action2})`,
|
|
10481
10993
|
fetch: doFetch,
|
|
10482
10994
|
stdout: out,
|
|
10483
10995
|
openApprovalUrl: deps.openUrl,
|
|
@@ -10512,11 +11024,11 @@ async function buildContext3(parsed, deps, action) {
|
|
|
10512
11024
|
};
|
|
10513
11025
|
}
|
|
10514
11026
|
async function runbookCommand(parsed, deps = {}) {
|
|
10515
|
-
const
|
|
10516
|
-
assertArgs(parsed, ALLOWED2,
|
|
10517
|
-
const ctx = await buildContext3(parsed, deps,
|
|
11027
|
+
const action2 = parsed.positionals[1] ?? "list";
|
|
11028
|
+
assertArgs(parsed, ALLOWED2, action2 === "ask" || action2 === "search" ? 64 : 4);
|
|
11029
|
+
const ctx = await buildContext3(parsed, deps, action2);
|
|
10518
11030
|
const slug = parsed.positionals[2];
|
|
10519
|
-
switch (
|
|
11031
|
+
switch (action2) {
|
|
10520
11032
|
case "list":
|
|
10521
11033
|
return runbookList(ctx, parsed.options.all === true, stringOpt(parsed.options.q));
|
|
10522
11034
|
case "ask": {
|
|
@@ -10601,7 +11113,7 @@ async function runbookCommand(parsed, deps = {}) {
|
|
|
10601
11113
|
case "rm":
|
|
10602
11114
|
return runbookRemove(ctx, requireSlug(slug, "rm"));
|
|
10603
11115
|
default:
|
|
10604
|
-
throw new Error(`unknown runbook action "${
|
|
11116
|
+
throw new Error(`unknown runbook action "${action2}". Try ${acceptedAfter(["runbook"]).join(", ")}.`);
|
|
10605
11117
|
}
|
|
10606
11118
|
}
|
|
10607
11119
|
|
|
@@ -10759,7 +11271,7 @@ function hostedSeverity(value2, flag) {
|
|
|
10759
11271
|
var import_security2 = require("@odla-ai/security");
|
|
10760
11272
|
|
|
10761
11273
|
// src/security.ts
|
|
10762
|
-
var
|
|
11274
|
+
var import_node_path17 = require("path");
|
|
10763
11275
|
var import_security = require("@odla-ai/security");
|
|
10764
11276
|
var import_node3 = require("@odla-ai/security/node");
|
|
10765
11277
|
async function runHostedSecurity(options) {
|
|
@@ -10771,9 +11283,9 @@ async function runHostedSecurity(options) {
|
|
|
10771
11283
|
const appId = selfAudit ? "odla-ai" : cfg.app.id;
|
|
10772
11284
|
const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
|
|
10773
11285
|
const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
|
|
10774
|
-
const target = (0,
|
|
10775
|
-
const output = (0,
|
|
10776
|
-
const outputRelative = (0,
|
|
11286
|
+
const target = (0, import_node_path17.resolve)(options.target ?? cfg?.rootDir ?? ".");
|
|
11287
|
+
const output = (0, import_node_path17.resolve)(options.out ?? (0, import_node_path17.resolve)(target, ".odla/security/hosted"));
|
|
11288
|
+
const outputRelative = (0, import_node_path17.relative)(target, output).split(import_node_path17.sep).join("/");
|
|
10777
11289
|
if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
|
|
10778
11290
|
const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
|
|
10779
11291
|
const tokenRequest = {
|
|
@@ -10785,7 +11297,7 @@ async function runHostedSecurity(options) {
|
|
|
10785
11297
|
};
|
|
10786
11298
|
const token = await injectedToken(options, tokenRequest);
|
|
10787
11299
|
const snapshot = await (0, import_node3.snapshotDirectory)(target, {
|
|
10788
|
-
exclude: !outputRelative.startsWith("../") && !(0,
|
|
11300
|
+
exclude: !outputRelative.startsWith("../") && !(0, import_node_path17.isAbsolute)(outputRelative) ? [outputRelative] : []
|
|
10789
11301
|
});
|
|
10790
11302
|
const hosted = await (0, import_security.createPlatformSecurityReasoners)({
|
|
10791
11303
|
platform,
|
|
@@ -10803,7 +11315,7 @@ async function runHostedSecurity(options) {
|
|
|
10803
11315
|
});
|
|
10804
11316
|
const harness = (0, import_security.createSecurityHarness)({
|
|
10805
11317
|
profile,
|
|
10806
|
-
store: new import_node3.FileRunStore((0,
|
|
11318
|
+
store: new import_node3.FileRunStore((0, import_node_path17.resolve)(output, "state")),
|
|
10807
11319
|
discoveryReasoner: hosted.discoveryReasoner,
|
|
10808
11320
|
validationReasoner: hosted.validationReasoner,
|
|
10809
11321
|
policy: {
|
|
@@ -10827,7 +11339,7 @@ async function runHostedSecurity(options) {
|
|
|
10827
11339
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
10828
11340
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
10829
11341
|
if (!env || !declared.includes(env)) {
|
|
10830
|
-
const shown = (0,
|
|
11342
|
+
const shown = (0, import_node_path17.relative)(rootDir, configPath) || configPath;
|
|
10831
11343
|
throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
|
|
10832
11344
|
}
|
|
10833
11345
|
return env;
|
|
@@ -10856,7 +11368,7 @@ function printSummary(out, appId, env, run, report4, output) {
|
|
|
10856
11368
|
out.log(` coverage: ${report4.coverageStatus} ${complete}/${report4.coverage.length} blocked=${report4.metrics.blockedCells} shallow=${report4.metrics.shallowCells} unscheduled=${report4.metrics.unscheduledCells} budget_exhausted=${report4.metrics.budgetExhaustedCells}`);
|
|
10857
11369
|
if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
|
|
10858
11370
|
out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
|
|
10859
|
-
out.log(` report: ${(0,
|
|
11371
|
+
out.log(` report: ${(0, import_node_path17.resolve)(output, "REPORT.md")}`);
|
|
10860
11372
|
}
|
|
10861
11373
|
function formatBudget(usage) {
|
|
10862
11374
|
return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
|
|
@@ -11222,8 +11734,8 @@ async function securityCommand(parsed, dependencies) {
|
|
|
11222
11734
|
else await runLocalSecurityCommand(parsed, dependencies);
|
|
11223
11735
|
}
|
|
11224
11736
|
async function githubSecurityCommand(parsed, dependencies) {
|
|
11225
|
-
const
|
|
11226
|
-
if (
|
|
11737
|
+
const action2 = parsed.positionals[2];
|
|
11738
|
+
if (action2 === "disconnect") {
|
|
11227
11739
|
assertArgs(parsed, ["config", "env", "platform", "source", "email", "open", "yes"], 3);
|
|
11228
11740
|
const context2 = await hostedSecurityContext(parsed, dependencies);
|
|
11229
11741
|
const sourceId = requiredString(parsed.options.source, "--source");
|
|
@@ -11238,7 +11750,7 @@ async function githubSecurityCommand(parsed, dependencies) {
|
|
|
11238
11750
|
context2.stdout.log(`github: disconnected ${sourceId} from ${context2.appId}/${context2.env}`);
|
|
11239
11751
|
return;
|
|
11240
11752
|
}
|
|
11241
|
-
if (
|
|
11753
|
+
if (action2 !== "connect") {
|
|
11242
11754
|
throw new Error('unknown security github command. Try "odla-ai security github connect".');
|
|
11243
11755
|
}
|
|
11244
11756
|
assertArgs(parsed, ["config", "env", "platform", "repo", "email", "open"], 3);
|