@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/index.cjs
CHANGED
|
@@ -47,8 +47,11 @@ __export(index_exports, {
|
|
|
47
47
|
calendarServiceConfig: () => calendarServiceConfig,
|
|
48
48
|
calendarStatus: () => calendarStatus,
|
|
49
49
|
codeConnect: () => codeConnect,
|
|
50
|
+
configDiff: () => configDiff,
|
|
51
|
+
configPlan: () => configPlan,
|
|
50
52
|
connectGitHubSecuritySource: () => connectGitHubSecuritySource,
|
|
51
53
|
describeProblem: () => describeProblem,
|
|
54
|
+
desiredRegistryState: () => desiredRegistryState,
|
|
52
55
|
disconnectGitHubSecuritySource: () => disconnectGitHubSecuritySource,
|
|
53
56
|
doctor: () => doctor,
|
|
54
57
|
exitCodeFor: () => exitCodeFor,
|
|
@@ -68,6 +71,7 @@ __export(index_exports, {
|
|
|
68
71
|
prepareCodeImages: () => prepareCodeImages,
|
|
69
72
|
printCapabilities: () => printCapabilities,
|
|
70
73
|
provision: () => provision,
|
|
74
|
+
reconcileConfig: () => reconcileConfig,
|
|
71
75
|
redactSecrets: () => redactSecrets,
|
|
72
76
|
repositoryFromGitRemote: () => repositoryFromGitRemote,
|
|
73
77
|
runCli: () => runCli,
|
|
@@ -103,7 +107,9 @@ function approvalLines(prompt) {
|
|
|
103
107
|
lines.push("");
|
|
104
108
|
lines.push(` ${prompt.approvalUrl}`);
|
|
105
109
|
lines.push("");
|
|
106
|
-
lines.push(
|
|
110
|
+
lines.push(
|
|
111
|
+
` ${prompt.approver ?? "The matching signed-in odla account"} opens that URL, checks the code matches, and approves.`
|
|
112
|
+
);
|
|
107
113
|
lines.push(" Opening the link without approving does nothing; this command waits until they do.");
|
|
108
114
|
if (prompt.browserAttempted) {
|
|
109
115
|
lines.push(" A browser launch was attempted, but it is best-effort and may have shown no tab.");
|
|
@@ -550,6 +556,7 @@ function audienceBoundEnvToken(token, platform) {
|
|
|
550
556
|
}
|
|
551
557
|
var SCOPE_PURPOSE = {
|
|
552
558
|
"platform:status:read": "read the platform fleet health and deployment snapshot",
|
|
559
|
+
"app:config:read": "compare checked-in intent with an exact-id app Registry configuration",
|
|
553
560
|
"platform:runbook:write": "add or edit odla's operational runbooks",
|
|
554
561
|
"platform:ai:policy:write": "change System AI model routing",
|
|
555
562
|
"platform:ai:policy:read": "read System AI model routing",
|
|
@@ -586,6 +593,7 @@ async function scopedToken(platform, scope, options, doFetch, out) {
|
|
|
586
593
|
approvalUrl,
|
|
587
594
|
minutesLeft: Math.floor(expiresIn / 60),
|
|
588
595
|
purpose: SCOPE_PURPOSE[scope] ?? `use ${scope}`,
|
|
596
|
+
approver: scope.startsWith("app:") ? "A signed-in app owner" : "A signed-in odla platform admin",
|
|
589
597
|
browserAttempted: browser.open,
|
|
590
598
|
browserSkipped: browser.reason
|
|
591
599
|
});
|
|
@@ -755,10 +763,10 @@ async function responseBody2(res) {
|
|
|
755
763
|
return { message: text.slice(0, 300) };
|
|
756
764
|
}
|
|
757
765
|
}
|
|
758
|
-
function apiError2(
|
|
766
|
+
function apiError2(action2, status, body) {
|
|
759
767
|
const error = isRecord2(body) && isRecord2(body.error) ? body.error : void 0;
|
|
760
768
|
const message2 = error && typeof error.message === "string" ? error.message : isRecord2(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
761
|
-
return `${
|
|
769
|
+
return `${action2} failed (${status}): ${message2}`;
|
|
762
770
|
}
|
|
763
771
|
function isRecord2(value2) {
|
|
764
772
|
return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
|
|
@@ -936,10 +944,10 @@ async function responseBody3(res) {
|
|
|
936
944
|
return { message: text.slice(0, 300) };
|
|
937
945
|
}
|
|
938
946
|
}
|
|
939
|
-
function apiError3(
|
|
947
|
+
function apiError3(action2, status, body) {
|
|
940
948
|
const error = isRecord3(body) && isRecord3(body.error) ? body.error : void 0;
|
|
941
949
|
const message2 = error && typeof error.message === "string" ? error.message : isRecord3(body) && typeof body.message === "string" ? body.message : "request failed";
|
|
942
|
-
return `${
|
|
950
|
+
return `${action2} failed (${status}): ${message2}`;
|
|
943
951
|
}
|
|
944
952
|
function isRecord3(value2) {
|
|
945
953
|
return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
|
|
@@ -1563,21 +1571,21 @@ var SET_OPTIONS = [
|
|
|
1563
1571
|
];
|
|
1564
1572
|
async function adminCommand(parsed, deps = {}) {
|
|
1565
1573
|
const area = parsed.positionals[1];
|
|
1566
|
-
const
|
|
1567
|
-
const credentialSet =
|
|
1568
|
-
const credentials =
|
|
1569
|
-
const models =
|
|
1570
|
-
const usage =
|
|
1571
|
-
const audit =
|
|
1572
|
-
if (area !== "ai" ||
|
|
1574
|
+
const action2 = parsed.positionals[2];
|
|
1575
|
+
const credentialSet = action2 === "credential" && parsed.positionals[3] === "set";
|
|
1576
|
+
const credentials = action2 === "credentials";
|
|
1577
|
+
const models = action2 === "models";
|
|
1578
|
+
const usage = action2 === "usage";
|
|
1579
|
+
const audit = action2 === "audit";
|
|
1580
|
+
if (area !== "ai" || action2 !== "show" && action2 !== "set" && !credentialSet && !credentials && !models && !usage && !audit) {
|
|
1573
1581
|
throw new Error('unknown admin command. Try "odla-ai admin ai show".');
|
|
1574
1582
|
}
|
|
1575
|
-
const allowed = credentialSet ? [...CONTEXT_OPTIONS, "from-env", "stdin"] :
|
|
1576
|
-
assertArgs(parsed, allowed, credentialSet ? 5 :
|
|
1583
|
+
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;
|
|
1584
|
+
assertArgs(parsed, allowed, credentialSet ? 5 : action2 === "set" ? 4 : 3);
|
|
1577
1585
|
const context = await resolveOperatorContext(parsed, { allowMissingConfig: true });
|
|
1578
1586
|
await adminAi({
|
|
1579
|
-
action: credentialSet ? "credential-set" : credentials ? "credentials" : models ? "models" : usage ? "usage" : audit ? "audit" :
|
|
1580
|
-
purpose:
|
|
1587
|
+
action: credentialSet ? "credential-set" : credentials ? "credentials" : models ? "models" : usage ? "usage" : audit ? "audit" : action2,
|
|
1588
|
+
purpose: action2 === "set" ? parsed.positionals[3] : void 0,
|
|
1581
1589
|
provider: stringOpt(parsed.options.provider),
|
|
1582
1590
|
model: stringOpt(parsed.options.model),
|
|
1583
1591
|
enabled: boolOpt(parsed.options.enabled),
|
|
@@ -1634,12 +1642,12 @@ function bothTenants(cfg) {
|
|
|
1634
1642
|
|
|
1635
1643
|
// src/agent-command.ts
|
|
1636
1644
|
async function agentCommand(parsed, deps = {}) {
|
|
1637
|
-
const
|
|
1638
|
-
if (
|
|
1639
|
-
throw new Error(`unknown agent action "${
|
|
1645
|
+
const action2 = parsed.positionals[1];
|
|
1646
|
+
if (action2 !== "jobs" && action2 !== "retry") {
|
|
1647
|
+
throw new Error(`unknown agent action "${action2 ?? ""}". Try "odla-ai agent jobs --json".`);
|
|
1640
1648
|
}
|
|
1641
|
-
assertArgs(parsed, ["config", "env", "state", "limit", "json", "token", "email"],
|
|
1642
|
-
if (
|
|
1649
|
+
assertArgs(parsed, ["config", "env", "state", "limit", "json", "token", "email"], action2 === "jobs" ? 2 : 3);
|
|
1650
|
+
if (action2 === "retry" && (parsed.options.state !== void 0 || parsed.options.limit !== void 0)) {
|
|
1643
1651
|
throw new Error('--state and --limit are supported only by "agent jobs"');
|
|
1644
1652
|
}
|
|
1645
1653
|
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
@@ -1659,7 +1667,7 @@ async function agentCommand(parsed, deps = {}) {
|
|
|
1659
1667
|
);
|
|
1660
1668
|
const base = `${cfg.dbEndpoint}/app/${encodeURIComponent(tenant)}/admin/agent-jobs`;
|
|
1661
1669
|
const headers = { authorization: `Bearer ${credential2}` };
|
|
1662
|
-
if (
|
|
1670
|
+
if (action2 === "retry") {
|
|
1663
1671
|
const id = parsed.positionals[2];
|
|
1664
1672
|
const res2 = await doFetch(`${base}/${encodeURIComponent(id)}/retry`, { method: "POST", headers });
|
|
1665
1673
|
const body2 = await readJson(res2);
|
|
@@ -2095,7 +2103,7 @@ async function copyFiles(cfg, token, route2, doFetch, out) {
|
|
|
2095
2103
|
}
|
|
2096
2104
|
|
|
2097
2105
|
// src/app-lifecycle.ts
|
|
2098
|
-
async function lifecycleCall(
|
|
2106
|
+
async function lifecycleCall(action2, options) {
|
|
2099
2107
|
const cfg = await loadProjectConfig(options.configPath);
|
|
2100
2108
|
const out = options.stdout ?? console;
|
|
2101
2109
|
const doFetch = options.fetch ?? fetch;
|
|
@@ -2105,13 +2113,13 @@ async function lifecycleCall(action, options) {
|
|
|
2105
2113
|
doFetch,
|
|
2106
2114
|
out
|
|
2107
2115
|
);
|
|
2108
|
-
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/${
|
|
2116
|
+
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/${action2}`, {
|
|
2109
2117
|
method: "POST",
|
|
2110
2118
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }
|
|
2111
2119
|
});
|
|
2112
2120
|
const body = await res.json().catch(() => ({}));
|
|
2113
2121
|
if (!res.ok || !body.ok) {
|
|
2114
|
-
throw new Error(`${
|
|
2122
|
+
throw new Error(`${action2} failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? `registry returned ${res.status}`}`);
|
|
2115
2123
|
}
|
|
2116
2124
|
return body;
|
|
2117
2125
|
}
|
|
@@ -2664,8 +2672,8 @@ function pollTimeout(value2 = 10 * 6e4) {
|
|
|
2664
2672
|
if (!Number.isSafeInteger(value2) || value2 < 1e3 || value2 > 60 * 6e4) throw new Error("calendar poll timeout must be 1000-3600000ms");
|
|
2665
2673
|
return value2;
|
|
2666
2674
|
}
|
|
2667
|
-
function productionConsent(env, yes,
|
|
2668
|
-
if ((env === "prod" || env === "production") && !yes) throw new Error(`refusing to ${
|
|
2675
|
+
function productionConsent(env, yes, action2) {
|
|
2676
|
+
if ((env === "prod" || env === "production") && !yes) throw new Error(`refusing to ${action2} for "${env}" without --yes`);
|
|
2669
2677
|
}
|
|
2670
2678
|
function printStatus(status, json, out) {
|
|
2671
2679
|
if (json) {
|
|
@@ -2695,6 +2703,7 @@ var CAPABILITIES = {
|
|
|
2695
2703
|
"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",
|
|
2696
2704
|
"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",
|
|
2697
2705
|
"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",
|
|
2706
|
+
"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",
|
|
2698
2707
|
"inspect durable agent wakeups as a versioned JSON envelope and explicitly requeue one dead-lettered job with a scoped environment credential",
|
|
2699
2708
|
"run app-attributed hosted security discovery and independent validation without provider keys",
|
|
2700
2709
|
"connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
|
|
@@ -2741,15 +2750,542 @@ function printGroup(out, heading, items) {
|
|
|
2741
2750
|
out.log("");
|
|
2742
2751
|
}
|
|
2743
2752
|
|
|
2753
|
+
// src/config-reconcile-command.ts
|
|
2754
|
+
var import_apps6 = require("@odla-ai/apps");
|
|
2755
|
+
var import_node_path7 = require("path");
|
|
2756
|
+
|
|
2757
|
+
// src/config-reconcile-digest.ts
|
|
2758
|
+
var import_node_crypto = require("crypto");
|
|
2759
|
+
function canonicalJson(value2) {
|
|
2760
|
+
return JSON.stringify(canonicalValue(value2));
|
|
2761
|
+
}
|
|
2762
|
+
function configDigest(value2) {
|
|
2763
|
+
return `sha256:${(0, import_node_crypto.createHash)("sha256").update(canonicalJson(value2)).digest("hex")}`;
|
|
2764
|
+
}
|
|
2765
|
+
function canonicalValue(value2) {
|
|
2766
|
+
if (Array.isArray(value2)) return value2.map(canonicalValue);
|
|
2767
|
+
if (value2 && typeof value2 === "object") {
|
|
2768
|
+
return Object.fromEntries(
|
|
2769
|
+
Object.entries(value2).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, canonicalValue(entry)])
|
|
2770
|
+
);
|
|
2771
|
+
}
|
|
2772
|
+
return value2;
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
// src/config-reconcile-desired.ts
|
|
2776
|
+
var import_apps4 = require("@odla-ai/apps");
|
|
2777
|
+
|
|
2778
|
+
// src/provision-helpers.ts
|
|
2779
|
+
var import_ai = require("@odla-ai/ai");
|
|
2780
|
+
var import_apps3 = require("@odla-ai/apps");
|
|
2781
|
+
function defaultSecretName(provider) {
|
|
2782
|
+
const names = import_ai.DEFAULT_SECRET_NAMES;
|
|
2783
|
+
return names[provider] ?? `${provider}_api_key`;
|
|
2784
|
+
}
|
|
2785
|
+
async function assertTenantAdminAccess(doFetch, cfg, env, token) {
|
|
2786
|
+
const tenantId = (0, import_apps3.tenantIdFor)(cfg.app.id, env);
|
|
2787
|
+
const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/entitlements`, {
|
|
2788
|
+
headers: { authorization: `Bearer ${token}` }
|
|
2789
|
+
});
|
|
2790
|
+
if (res.ok || res.status === 404) return;
|
|
2791
|
+
if (res.status === 403) {
|
|
2792
|
+
throw new Error(
|
|
2793
|
+
`${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`
|
|
2794
|
+
);
|
|
2795
|
+
}
|
|
2796
|
+
throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText3(res)}`);
|
|
2797
|
+
}
|
|
2798
|
+
async function postJson(doFetch, url, bearer, body) {
|
|
2799
|
+
const res = await doFetch(url, {
|
|
2800
|
+
method: "POST",
|
|
2801
|
+
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
2802
|
+
body: JSON.stringify(body)
|
|
2803
|
+
});
|
|
2804
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText3(res)}`);
|
|
2805
|
+
}
|
|
2806
|
+
function normalizeClerkConfig(value2) {
|
|
2807
|
+
if (!value2) return null;
|
|
2808
|
+
if (typeof value2 === "string") {
|
|
2809
|
+
const publishableKey2 = envValue(value2);
|
|
2810
|
+
return publishableKey2 ? { publishableKey: publishableKey2 } : null;
|
|
2811
|
+
}
|
|
2812
|
+
if (typeof value2 !== "object") return null;
|
|
2813
|
+
const cfg = value2;
|
|
2814
|
+
const publishableKey = envValue(cfg.publishableKey);
|
|
2815
|
+
return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
|
|
2816
|
+
}
|
|
2817
|
+
async function safeText3(res) {
|
|
2818
|
+
try {
|
|
2819
|
+
return redactSecrets((await res.text()).slice(0, 500));
|
|
2820
|
+
} catch {
|
|
2821
|
+
return "";
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
// src/config-reconcile-desired.ts
|
|
2826
|
+
function desiredRegistryState(cfg) {
|
|
2827
|
+
const environments = {};
|
|
2828
|
+
const services = (0, import_apps4.orderAppServices)(cfg.services);
|
|
2829
|
+
for (const env of cfg.envs) {
|
|
2830
|
+
const desiredServices = {};
|
|
2831
|
+
for (const service of services) {
|
|
2832
|
+
desiredServices[service] = {
|
|
2833
|
+
enabled: true,
|
|
2834
|
+
...managedServiceConfig(cfg, env, service)
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
const authoredAuth = cfg.auth?.clerk && Object.hasOwn(cfg.auth.clerk, env) ? cfg.auth.clerk[env] : void 0;
|
|
2838
|
+
const auth = normalizeClerkConfig(authoredAuth);
|
|
2839
|
+
if (authoredAuth && !auth) {
|
|
2840
|
+
throw new Error(`auth.clerk.${env} could not resolve its publishable key`);
|
|
2841
|
+
}
|
|
2842
|
+
const linkManaged = !!cfg.links && Object.hasOwn(cfg.links, env);
|
|
2843
|
+
environments[env] = {
|
|
2844
|
+
services: desiredServices,
|
|
2845
|
+
...auth ? { auth } : {},
|
|
2846
|
+
...linkManaged ? { link: cfg.links?.[env] ?? null } : {}
|
|
2847
|
+
};
|
|
2848
|
+
}
|
|
2849
|
+
return {
|
|
2850
|
+
appId: cfg.app.id,
|
|
2851
|
+
name: cfg.app.name,
|
|
2852
|
+
environments
|
|
2853
|
+
};
|
|
2854
|
+
}
|
|
2855
|
+
function managedServiceConfig(cfg, env, service) {
|
|
2856
|
+
if (service === "ai" && cfg.ai?.provider) {
|
|
2857
|
+
return {
|
|
2858
|
+
config: {
|
|
2859
|
+
provider: cfg.ai.provider,
|
|
2860
|
+
...cfg.ai.model ? { model: cfg.ai.model } : {}
|
|
2861
|
+
}
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2864
|
+
if (service === "calendar") return { config: calendarServiceConfig(cfg, env) };
|
|
2865
|
+
return {};
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
// src/config-reconcile.ts
|
|
2869
|
+
var import_apps5 = require("@odla-ai/apps");
|
|
2870
|
+
|
|
2871
|
+
// src/config-reconcile-values.ts
|
|
2872
|
+
function difference(path, desired, observed, status, reason, desiredSource, observedSource, env, service) {
|
|
2873
|
+
return { path, env, service, status, desired, observed, desiredSource, observedSource, reason };
|
|
2874
|
+
}
|
|
2875
|
+
function action(kind, path, before, after, risk, applySupport, reason) {
|
|
2876
|
+
return { kind, path, before, after, risk, requiresApproval: false, applySupport, reason };
|
|
2877
|
+
}
|
|
2878
|
+
function scopedAction(kind, path, env, service, before, after, reason, destructive = false) {
|
|
2879
|
+
const production = env === "prod" || env === "production";
|
|
2880
|
+
return {
|
|
2881
|
+
...action(
|
|
2882
|
+
kind,
|
|
2883
|
+
path,
|
|
2884
|
+
before,
|
|
2885
|
+
after,
|
|
2886
|
+
destructive ? "high" : production ? "medium" : "low",
|
|
2887
|
+
destructive ? "studio" : "provision",
|
|
2888
|
+
reason
|
|
2889
|
+
),
|
|
2890
|
+
env,
|
|
2891
|
+
...service ? { service } : {},
|
|
2892
|
+
requiresApproval: production || destructive
|
|
2893
|
+
};
|
|
2894
|
+
}
|
|
2895
|
+
function observedProjection(app) {
|
|
2896
|
+
return {
|
|
2897
|
+
appId: app.appId,
|
|
2898
|
+
name: app.name,
|
|
2899
|
+
archivedAt: app.archivedAt,
|
|
2900
|
+
environments: Object.fromEntries(
|
|
2901
|
+
Object.entries(app.environments).sort(([left], [right]) => left.localeCompare(right)).map(([env, services]) => [
|
|
2902
|
+
env,
|
|
2903
|
+
Object.fromEntries(Object.entries(services).sort(([left], [right]) => left.localeCompare(right)))
|
|
2904
|
+
])
|
|
2905
|
+
),
|
|
2906
|
+
auth: app.auth,
|
|
2907
|
+
links: app.links
|
|
2908
|
+
};
|
|
2909
|
+
}
|
|
2910
|
+
function projectConfig(value2, keys) {
|
|
2911
|
+
return Object.fromEntries(keys.filter((key) => value2[key] !== void 0).map((key) => [key, value2[key]]));
|
|
2912
|
+
}
|
|
2913
|
+
function same(left, right) {
|
|
2914
|
+
return canonicalJson(left) === canonicalJson(right);
|
|
2915
|
+
}
|
|
2916
|
+
function actionId(actionValue) {
|
|
2917
|
+
return `action-${configDigest(actionValue).slice("sha256:".length, "sha256:".length + 16)}`;
|
|
2918
|
+
}
|
|
2919
|
+
function quoteArg(value2) {
|
|
2920
|
+
return `'${value2.replace(/'/g, `'\\''`)}'`;
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
// src/config-reconcile.ts
|
|
2924
|
+
var COVERAGE = {
|
|
2925
|
+
included: [
|
|
2926
|
+
"app display name",
|
|
2927
|
+
"environment service enablement",
|
|
2928
|
+
"AI provider/model and calendar booking service configuration",
|
|
2929
|
+
"auth configuration when explicitly declared",
|
|
2930
|
+
"deployment link when explicitly declared"
|
|
2931
|
+
],
|
|
2932
|
+
excluded: [
|
|
2933
|
+
{ path: "db.schema", reason: "owned by the database data plane" },
|
|
2934
|
+
{ path: "db.rules", reason: "owned by the database data plane" },
|
|
2935
|
+
{ path: "integrations.seeds", reason: "guarded data-plane writes are not Registry state" },
|
|
2936
|
+
{ path: "credentials", reason: "shown-once credentials are intentionally local" },
|
|
2937
|
+
{ path: "secrets", reason: "vault and Worker secrets are write-only" },
|
|
2938
|
+
{ path: "calendar.connection", reason: "OAuth connection state is provider-owned" },
|
|
2939
|
+
{ path: "service.config.opaque", reason: "controller-assigned fields remain service-owned" }
|
|
2940
|
+
]
|
|
2941
|
+
};
|
|
2942
|
+
function reconcileConfig(input) {
|
|
2943
|
+
const desiredSource = { kind: "project_config", location: input.configPath };
|
|
2944
|
+
const observedSource = {
|
|
2945
|
+
kind: "registry",
|
|
2946
|
+
location: `${input.platformUrl}/registry/apps/${encodeURIComponent(input.desired.appId)}`
|
|
2947
|
+
};
|
|
2948
|
+
const differences = [];
|
|
2949
|
+
const actions = [];
|
|
2950
|
+
const add = (difference2, action2) => {
|
|
2951
|
+
differences.push(difference2);
|
|
2952
|
+
if (action2) actions.push({ ...action2, id: actionId(action2) });
|
|
2953
|
+
};
|
|
2954
|
+
compareApp(input.desired, input.observed, desiredSource, observedSource, add);
|
|
2955
|
+
compareEnvironments(input.desired, input.observed, desiredSource, observedSource, add);
|
|
2956
|
+
const desiredRevision = configDigest(input.desired);
|
|
2957
|
+
const observedRevision = input.observed ? configDigest(observedProjection(input.observed)) : null;
|
|
2958
|
+
const different = differences.filter((entry) => entry.status === "different").length;
|
|
2959
|
+
const unmanaged = differences.length - different;
|
|
2960
|
+
return {
|
|
2961
|
+
generatedAt: input.generatedAt,
|
|
2962
|
+
scope: {
|
|
2963
|
+
appId: input.desired.appId,
|
|
2964
|
+
platformUrl: input.platformUrl,
|
|
2965
|
+
environments: Object.keys(input.desired.environments).sort()
|
|
2966
|
+
},
|
|
2967
|
+
sources: { desired: desiredSource, observed: observedSource },
|
|
2968
|
+
desiredRevision,
|
|
2969
|
+
observedRevision,
|
|
2970
|
+
status: different ? "different" : unmanaged ? "unmanaged" : "in_sync",
|
|
2971
|
+
summary: {
|
|
2972
|
+
differences: different,
|
|
2973
|
+
unmanaged,
|
|
2974
|
+
actions: actions.length,
|
|
2975
|
+
productionActions: actions.filter((action2) => action2.env === "prod" || action2.env === "production").length,
|
|
2976
|
+
highRiskActions: actions.filter((action2) => action2.risk === "high").length
|
|
2977
|
+
},
|
|
2978
|
+
coverage: COVERAGE,
|
|
2979
|
+
differences,
|
|
2980
|
+
actions
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
function compareApp(desired, observed, desiredSource, observedSource, add) {
|
|
2984
|
+
if (!observed) {
|
|
2985
|
+
const path = "app";
|
|
2986
|
+
add(
|
|
2987
|
+
difference(path, desired, null, "different", "the app is absent from Registry", desiredSource, observedSource),
|
|
2988
|
+
action(
|
|
2989
|
+
"create_app",
|
|
2990
|
+
path,
|
|
2991
|
+
null,
|
|
2992
|
+
{ appId: desired.appId, name: desired.name },
|
|
2993
|
+
"low",
|
|
2994
|
+
"provision",
|
|
2995
|
+
"create the repository-declared app"
|
|
2996
|
+
)
|
|
2997
|
+
);
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
if (observed.name !== desired.name) {
|
|
3001
|
+
const path = "app.name";
|
|
3002
|
+
add(
|
|
3003
|
+
difference(path, desired.name, observed.name, "different", "the checked-in display name differs", desiredSource, observedSource),
|
|
3004
|
+
{
|
|
3005
|
+
...action("rename_app", path, observed.name, desired.name, "low", "command", "make Registry match the checked-in display name"),
|
|
3006
|
+
command: `odla-ai app rename ${quoteArg(desired.name)} --config ${quoteArg(desiredSource.location)}`
|
|
3007
|
+
}
|
|
3008
|
+
);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
function compareEnvironments(desired, observed, desiredSource, observedSource, add) {
|
|
3012
|
+
const envs = /* @__PURE__ */ new Set([
|
|
3013
|
+
...Object.keys(desired.environments),
|
|
3014
|
+
...Object.keys(observed?.environments ?? {}).filter((env) => Object.values(observed?.environments[env] ?? {}).some((service) => service.enabled))
|
|
3015
|
+
]);
|
|
3016
|
+
for (const env of [...envs].sort()) {
|
|
3017
|
+
compareServices(env, desired, observed, desiredSource, observedSource, add);
|
|
3018
|
+
compareOptionalEnvironmentState(env, desired, observed, desiredSource, observedSource, add);
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
function compareServices(env, desired, observed, desiredSource, observedSource, add) {
|
|
3022
|
+
const wanted = desired.environments[env]?.services ?? {};
|
|
3023
|
+
const live = observed?.environments[env] ?? {};
|
|
3024
|
+
const knownOrder = (0, import_apps5.orderAppServices)((0, import_apps5.appServiceIds)());
|
|
3025
|
+
const services = [.../* @__PURE__ */ new Set([...knownOrder, ...Object.keys(wanted), ...Object.keys(live)])];
|
|
3026
|
+
for (const service of services) {
|
|
3027
|
+
const next = wanted[service];
|
|
3028
|
+
const current = live[service];
|
|
3029
|
+
const path = `environments.${env}.services.${service}`;
|
|
3030
|
+
if (next?.enabled && !current?.enabled) {
|
|
3031
|
+
add(
|
|
3032
|
+
difference(path, next, current ?? null, "different", "the repository enables a service Registry does not", desiredSource, observedSource, env, service),
|
|
3033
|
+
scopedAction("enable_service", path, env, service, current ?? null, next, "enable the repository-declared service")
|
|
3034
|
+
);
|
|
3035
|
+
continue;
|
|
3036
|
+
}
|
|
3037
|
+
if (next?.enabled && current?.enabled && next.config) {
|
|
3038
|
+
const projected = projectConfig(current.config, Object.keys(next.config));
|
|
3039
|
+
if (!same(next.config, projected)) {
|
|
3040
|
+
add(
|
|
3041
|
+
difference(`${path}.config`, next.config, projected, "different", "repository-managed service settings differ", desiredSource, observedSource, env, service),
|
|
3042
|
+
scopedAction(
|
|
3043
|
+
"configure_service",
|
|
3044
|
+
`${path}.config`,
|
|
3045
|
+
env,
|
|
3046
|
+
service,
|
|
3047
|
+
projected,
|
|
3048
|
+
next.config,
|
|
3049
|
+
"apply the repository-managed service settings"
|
|
3050
|
+
)
|
|
3051
|
+
);
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
for (const service of [...services].reverse()) {
|
|
3056
|
+
const next = wanted[service];
|
|
3057
|
+
const current = live[service];
|
|
3058
|
+
if (next?.enabled || !current?.enabled) continue;
|
|
3059
|
+
const path = `environments.${env}.services.${service}`;
|
|
3060
|
+
add(
|
|
3061
|
+
difference(path, null, { enabled: true }, "different", "Registry enables a service absent from repository intent", desiredSource, observedSource, env, service),
|
|
3062
|
+
scopedAction(
|
|
3063
|
+
"disable_service",
|
|
3064
|
+
path,
|
|
3065
|
+
env,
|
|
3066
|
+
service,
|
|
3067
|
+
{ enabled: true },
|
|
3068
|
+
{ enabled: false },
|
|
3069
|
+
"disablement is destructive to availability and requires an explicit Studio review",
|
|
3070
|
+
true
|
|
3071
|
+
)
|
|
3072
|
+
);
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
function compareOptionalEnvironmentState(env, desired, observed, desiredSource, observedSource, add) {
|
|
3076
|
+
const wanted = desired.environments[env];
|
|
3077
|
+
const liveAuth = observed?.auth[env] ?? null;
|
|
3078
|
+
if (wanted?.auth) {
|
|
3079
|
+
const projected = liveAuth ? projectConfig(liveAuth, Object.keys(wanted.auth)) : null;
|
|
3080
|
+
if (!same(wanted.auth, projected)) {
|
|
3081
|
+
const path = `environments.${env}.auth`;
|
|
3082
|
+
add(
|
|
3083
|
+
difference(path, wanted.auth, projected, "different", "explicit repository auth settings differ", desiredSource, observedSource, env),
|
|
3084
|
+
scopedAction("set_auth", path, env, void 0, projected, wanted.auth, "apply explicit repository auth settings")
|
|
3085
|
+
);
|
|
3086
|
+
}
|
|
3087
|
+
} else if (liveAuth) {
|
|
3088
|
+
add(difference(
|
|
3089
|
+
`environments.${env}.auth`,
|
|
3090
|
+
null,
|
|
3091
|
+
projectConfig(liveAuth, ["publishableKey", "audience", "mode"]),
|
|
3092
|
+
"unmanaged",
|
|
3093
|
+
"live auth exists but this project config does not manage it",
|
|
3094
|
+
desiredSource,
|
|
3095
|
+
observedSource,
|
|
3096
|
+
env
|
|
3097
|
+
));
|
|
3098
|
+
}
|
|
3099
|
+
const managesLink = wanted && Object.hasOwn(wanted, "link");
|
|
3100
|
+
const liveLink = observed?.links[env] ?? null;
|
|
3101
|
+
if (managesLink && wanted.link !== liveLink) {
|
|
3102
|
+
const path = `environments.${env}.link`;
|
|
3103
|
+
add(
|
|
3104
|
+
difference(path, wanted.link ?? null, liveLink, "different", "explicit repository deployment link differs", desiredSource, observedSource, env),
|
|
3105
|
+
scopedAction("set_link", path, env, void 0, liveLink, wanted.link ?? null, "apply the repository deployment link")
|
|
3106
|
+
);
|
|
3107
|
+
} else if (!managesLink && liveLink) {
|
|
3108
|
+
add(difference(
|
|
3109
|
+
`environments.${env}.link`,
|
|
3110
|
+
null,
|
|
3111
|
+
liveLink,
|
|
3112
|
+
"unmanaged",
|
|
3113
|
+
"a live deployment link exists but this project config does not manage it",
|
|
3114
|
+
desiredSource,
|
|
3115
|
+
observedSource,
|
|
3116
|
+
env
|
|
3117
|
+
));
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
// src/config-reconcile-command.ts
|
|
3122
|
+
async function configDiff(options) {
|
|
3123
|
+
const reconciliation = await inspectConfig(options);
|
|
3124
|
+
const { actions: _actions, ...read3 } = reconciliation;
|
|
3125
|
+
const document2 = {
|
|
3126
|
+
schemaVersion: "odla.config-diff/v1",
|
|
3127
|
+
...read3,
|
|
3128
|
+
nextActions: diffNextActions(reconciliation, options.configPath)
|
|
3129
|
+
};
|
|
3130
|
+
printDiff(document2, options);
|
|
3131
|
+
return document2;
|
|
3132
|
+
}
|
|
3133
|
+
async function configPlan(options) {
|
|
3134
|
+
const reconciliation = await inspectConfig(options);
|
|
3135
|
+
const planDigest = configDigest({
|
|
3136
|
+
schemaVersion: "odla.config-plan/v1",
|
|
3137
|
+
desiredRevision: reconciliation.desiredRevision,
|
|
3138
|
+
observedRevision: reconciliation.observedRevision,
|
|
3139
|
+
actions: reconciliation.actions
|
|
3140
|
+
});
|
|
3141
|
+
const document2 = {
|
|
3142
|
+
schemaVersion: "odla.config-plan/v1",
|
|
3143
|
+
...reconciliation,
|
|
3144
|
+
planDigest,
|
|
3145
|
+
apply: {
|
|
3146
|
+
supported: false,
|
|
3147
|
+
reason: "conditional, resumable config apply is not available yet; use the exact reviewed commands below"
|
|
3148
|
+
},
|
|
3149
|
+
nextActions: planNextActions(reconciliation, options.configPath)
|
|
3150
|
+
};
|
|
3151
|
+
printPlan2(document2, options);
|
|
3152
|
+
return document2;
|
|
3153
|
+
}
|
|
3154
|
+
async function inspectConfig(options) {
|
|
3155
|
+
const out = options.stdout ?? console;
|
|
3156
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
3157
|
+
const doFetch = options.fetch ?? fetch;
|
|
3158
|
+
const token = await resolveAdminPlatformToken({
|
|
3159
|
+
platform: cfg.platformUrl,
|
|
3160
|
+
scope: "app:config:read",
|
|
3161
|
+
token: options.token,
|
|
3162
|
+
tokenFile: (0, import_node_path7.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
|
|
3163
|
+
rootDir: cfg.rootDir,
|
|
3164
|
+
email: options.email,
|
|
3165
|
+
open: options.open,
|
|
3166
|
+
fetch: doFetch,
|
|
3167
|
+
stdout: out,
|
|
3168
|
+
openApprovalUrl: options.openApprovalUrl,
|
|
3169
|
+
label: `odla CLI (${cfg.app.id} config read)`
|
|
3170
|
+
});
|
|
3171
|
+
const client = (0, import_apps6.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
|
|
3172
|
+
const observed = await client.resolveApp(cfg.app.id);
|
|
3173
|
+
return reconcileConfig({
|
|
3174
|
+
desired: desiredRegistryState(cfg),
|
|
3175
|
+
observed,
|
|
3176
|
+
configPath: cfg.configPath,
|
|
3177
|
+
platformUrl: cfg.platformUrl,
|
|
3178
|
+
generatedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
|
|
3179
|
+
});
|
|
3180
|
+
}
|
|
3181
|
+
function printDiff(document2, options) {
|
|
3182
|
+
const out = options.stdout ?? console;
|
|
3183
|
+
if (options.json) {
|
|
3184
|
+
out.log(JSON.stringify(document2, null, 2));
|
|
3185
|
+
return;
|
|
3186
|
+
}
|
|
3187
|
+
printHeader(out, "diff", document2);
|
|
3188
|
+
printDifferences(out, document2);
|
|
3189
|
+
printNext(out, document2.nextActions);
|
|
3190
|
+
}
|
|
3191
|
+
function printPlan2(document2, options) {
|
|
3192
|
+
const out = options.stdout ?? console;
|
|
3193
|
+
if (options.json) {
|
|
3194
|
+
out.log(JSON.stringify(document2, null, 2));
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
printHeader(out, "plan", document2);
|
|
3198
|
+
for (const entry of document2.actions) {
|
|
3199
|
+
const scope = [entry.env, entry.service].filter(Boolean).join("/");
|
|
3200
|
+
out.log(
|
|
3201
|
+
` ${entry.id} ${entry.kind}${scope ? ` (${scope})` : ""} ${entry.risk}${entry.requiresApproval ? ", approval required" : ""}`
|
|
3202
|
+
);
|
|
3203
|
+
out.log(` ${entry.reason}`);
|
|
3204
|
+
}
|
|
3205
|
+
if (!document2.actions.length) out.log(" no managed changes");
|
|
3206
|
+
out.log(`plan digest: ${document2.planDigest}`);
|
|
3207
|
+
out.log(`apply: unsupported \u2014 ${document2.apply.reason}`);
|
|
3208
|
+
printNext(out, document2.nextActions);
|
|
3209
|
+
}
|
|
3210
|
+
function printHeader(out, kind, document2) {
|
|
3211
|
+
out.log(`config ${kind}: ${document2.scope.appId} \u2014 ${document2.status}`);
|
|
3212
|
+
out.log(`platform: ${document2.scope.platformUrl}`);
|
|
3213
|
+
out.log(`desired: ${document2.desiredRevision}`);
|
|
3214
|
+
out.log(`observed: ${document2.observedRevision ?? "absent"}`);
|
|
3215
|
+
out.log(
|
|
3216
|
+
`summary: ${document2.summary.differences} different, ${document2.summary.unmanaged} unmanaged, ${document2.summary.actions} planned actions`
|
|
3217
|
+
);
|
|
3218
|
+
}
|
|
3219
|
+
function printDifferences(out, document2) {
|
|
3220
|
+
for (const entry of document2.differences) {
|
|
3221
|
+
out.log(` ${entry.status} ${entry.path} ${entry.reason}`);
|
|
3222
|
+
}
|
|
3223
|
+
if (!document2.differences.length) out.log(" managed Registry configuration is in sync");
|
|
3224
|
+
}
|
|
3225
|
+
function printNext(out, next) {
|
|
3226
|
+
if (!next.length) return;
|
|
3227
|
+
out.log("next:");
|
|
3228
|
+
for (const item of next) out.log(` ${item.command}
|
|
3229
|
+
${item.description}`);
|
|
3230
|
+
}
|
|
3231
|
+
function diffNextActions(reconciliation, configPath) {
|
|
3232
|
+
if (reconciliation.status === "in_sync") {
|
|
3233
|
+
return [{
|
|
3234
|
+
code: "verify_runtime",
|
|
3235
|
+
command: `odla-ai smoke --config ${quoteArg2(configPath)}`,
|
|
3236
|
+
description: "Registry intent matches; verify the service-owned data planes separately."
|
|
3237
|
+
}];
|
|
3238
|
+
}
|
|
3239
|
+
return [{
|
|
3240
|
+
code: "freeze_plan",
|
|
3241
|
+
command: `odla-ai config plan --config ${quoteArg2(configPath)} --json`,
|
|
3242
|
+
description: "Freeze the current desired and observed revisions into a reviewable action plan."
|
|
3243
|
+
}];
|
|
3244
|
+
}
|
|
3245
|
+
function planNextActions(reconciliation, configPath) {
|
|
3246
|
+
const next = [];
|
|
3247
|
+
if (reconciliation.actions.some((action2) => action2.applySupport === "provision")) {
|
|
3248
|
+
next.push({
|
|
3249
|
+
code: "review_provision",
|
|
3250
|
+
command: `odla-ai provision --config ${quoteArg2(configPath)} --dry-run`,
|
|
3251
|
+
description: "Review the existing provisioner's service/auth/link work before applying it."
|
|
3252
|
+
});
|
|
3253
|
+
}
|
|
3254
|
+
for (const action2 of reconciliation.actions.filter((entry) => entry.command)) {
|
|
3255
|
+
if (!next.some((entry) => entry.command === action2.command)) {
|
|
3256
|
+
next.push({ code: action2.kind, command: action2.command, description: action2.reason });
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
if (reconciliation.actions.some((action2) => action2.applySupport === "studio")) {
|
|
3260
|
+
const { appId, platformUrl } = reconciliation.scope;
|
|
3261
|
+
next.push({
|
|
3262
|
+
code: "review_destructive",
|
|
3263
|
+
command: `${platformUrl}/studio/apps/${encodeURIComponent(appId)}/settings`,
|
|
3264
|
+
description: "Review service disablement in the owning Studio scope; this plan will not apply it."
|
|
3265
|
+
});
|
|
3266
|
+
}
|
|
3267
|
+
if (!reconciliation.actions.length && reconciliation.summary.unmanaged) {
|
|
3268
|
+
next.push({
|
|
3269
|
+
code: "declare_or_accept_runtime_state",
|
|
3270
|
+
command: `${reconciliation.scope.platformUrl}/studio/apps/${encodeURIComponent(reconciliation.scope.appId)}/settings`,
|
|
3271
|
+
description: "Declare the live value in project config or keep it as an explicit runtime-managed setting."
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3274
|
+
return next;
|
|
3275
|
+
}
|
|
3276
|
+
function quoteArg2(value2) {
|
|
3277
|
+
return `'${value2.replace(/'/g, `'\\''`)}'`;
|
|
3278
|
+
}
|
|
3279
|
+
|
|
2744
3280
|
// src/doctor-checks.ts
|
|
2745
3281
|
var import_node_child_process3 = require("child_process");
|
|
2746
3282
|
var import_node_fs10 = require("fs");
|
|
2747
|
-
var
|
|
3283
|
+
var import_node_path9 = require("path");
|
|
2748
3284
|
|
|
2749
3285
|
// src/wrangler.ts
|
|
2750
3286
|
var import_node_child_process2 = require("child_process");
|
|
2751
3287
|
var import_node_fs9 = require("fs");
|
|
2752
|
-
var
|
|
3288
|
+
var import_node_path8 = require("path");
|
|
2753
3289
|
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
2754
3290
|
const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
2755
3291
|
let stdout = "";
|
|
@@ -2763,7 +3299,7 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) =>
|
|
|
2763
3299
|
var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
2764
3300
|
function findWranglerConfig(rootDir) {
|
|
2765
3301
|
for (const name of WRANGLER_CONFIG_FILES) {
|
|
2766
|
-
const path = (0,
|
|
3302
|
+
const path = (0, import_node_path8.join)(rootDir, name);
|
|
2767
3303
|
if ((0, import_node_fs9.existsSync)(path)) return path;
|
|
2768
3304
|
}
|
|
2769
3305
|
return null;
|
|
@@ -2829,11 +3365,11 @@ function lintRules(rules, entities, publicRead) {
|
|
|
2829
3365
|
const warnings = [];
|
|
2830
3366
|
if (!rules) return warnings;
|
|
2831
3367
|
for (const [ns, actions] of Object.entries(rules)) {
|
|
2832
|
-
for (const [
|
|
3368
|
+
for (const [action2, expr] of Object.entries(actions)) {
|
|
2833
3369
|
if (typeof expr !== "string" || expr.trim() !== "true") continue;
|
|
2834
|
-
if (
|
|
3370
|
+
if (action2 === "view" && publicRead.includes(ns)) continue;
|
|
2835
3371
|
warnings.push(
|
|
2836
|
-
|
|
3372
|
+
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`
|
|
2837
3373
|
);
|
|
2838
3374
|
}
|
|
2839
3375
|
}
|
|
@@ -2869,10 +3405,10 @@ function wranglerWarnings(rootDir) {
|
|
|
2869
3405
|
for (const { label, block } of blocks) {
|
|
2870
3406
|
const assets = block.assets;
|
|
2871
3407
|
if (assets?.directory) {
|
|
2872
|
-
const dir = (0,
|
|
2873
|
-
if (dir === (0,
|
|
3408
|
+
const dir = (0, import_node_path9.resolve)(rootDir, assets.directory);
|
|
3409
|
+
if (dir === (0, import_node_path9.resolve)(rootDir)) {
|
|
2874
3410
|
warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
|
|
2875
|
-
} else if ((0, import_node_fs10.existsSync)((0,
|
|
3411
|
+
} else if ((0, import_node_fs10.existsSync)((0, import_node_path9.join)(dir, "node_modules"))) {
|
|
2876
3412
|
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
2877
3413
|
}
|
|
2878
3414
|
}
|
|
@@ -2907,7 +3443,7 @@ function o11yProjectWarnings(rootDir) {
|
|
|
2907
3443
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
|
|
2908
3444
|
return warnings;
|
|
2909
3445
|
}
|
|
2910
|
-
const main = typeof config.main === "string" ? (0,
|
|
3446
|
+
const main = typeof config.main === "string" ? (0, import_node_path9.resolve)(rootDir, config.main) : null;
|
|
2911
3447
|
if (!main || !(0, import_node_fs10.existsSync)(main)) {
|
|
2912
3448
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
|
|
2913
3449
|
} else {
|
|
@@ -2937,7 +3473,7 @@ function calendarProjectWarnings(rootDir) {
|
|
|
2937
3473
|
}
|
|
2938
3474
|
function readPackageJson(rootDir) {
|
|
2939
3475
|
try {
|
|
2940
|
-
return JSON.parse((0, import_node_fs10.readFileSync)((0,
|
|
3476
|
+
return JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path9.join)(rootDir, "package.json"), "utf8"));
|
|
2941
3477
|
} catch {
|
|
2942
3478
|
return null;
|
|
2943
3479
|
}
|
|
@@ -2993,8 +3529,8 @@ function integrationWarnings(integrations, schema, rules) {
|
|
|
2993
3529
|
warnings.push(`integration "${integration.id}" has no rules for "${ns}"`);
|
|
2994
3530
|
continue;
|
|
2995
3531
|
}
|
|
2996
|
-
for (const
|
|
2997
|
-
if (rule[
|
|
3532
|
+
for (const action2 of ["view", "create", "update", "delete"]) {
|
|
3533
|
+
if (rule[action2] === void 0) warnings.push(`integration "${integration.id}" rule "${ns}.${action2}" is missing`);
|
|
2998
3534
|
}
|
|
2999
3535
|
}
|
|
3000
3536
|
for (const seed of integration.seeds ?? []) {
|
|
@@ -3165,12 +3701,12 @@ function harnessOption(value2, flag) {
|
|
|
3165
3701
|
|
|
3166
3702
|
// src/init.ts
|
|
3167
3703
|
var import_node_fs11 = require("fs");
|
|
3168
|
-
var
|
|
3169
|
-
var
|
|
3704
|
+
var import_node_path10 = require("path");
|
|
3705
|
+
var import_apps7 = require("@odla-ai/apps");
|
|
3170
3706
|
function initProject(options) {
|
|
3171
3707
|
const out = options.stdout ?? console;
|
|
3172
|
-
const rootDir = (0,
|
|
3173
|
-
const configPath = (0,
|
|
3708
|
+
const rootDir = (0, import_node_path10.resolve)(options.rootDir ?? process.cwd());
|
|
3709
|
+
const configPath = (0, import_node_path10.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
|
|
3174
3710
|
if ((0, import_node_fs11.existsSync)(configPath) && !options.force) {
|
|
3175
3711
|
throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
|
|
3176
3712
|
}
|
|
@@ -3180,19 +3716,19 @@ function initProject(options) {
|
|
|
3180
3716
|
const envs = options.envs?.length ? options.envs : ["dev"];
|
|
3181
3717
|
const services = options.services?.length ? options.services : ["db", "ai"];
|
|
3182
3718
|
for (const service of services) {
|
|
3183
|
-
const definition = (0,
|
|
3184
|
-
if (!definition) throw new Error(`--services contains unknown service "${service}" (known: ${(0,
|
|
3719
|
+
const definition = (0, import_apps7.appServiceDefinition)(service);
|
|
3720
|
+
if (!definition) throw new Error(`--services contains unknown service "${service}" (known: ${(0, import_apps7.appServiceIds)().join(", ")})`);
|
|
3185
3721
|
for (const dependency of definition.requires) {
|
|
3186
3722
|
if (!services.includes(dependency)) throw new Error(`--services ${service} requires ${dependency}`);
|
|
3187
3723
|
}
|
|
3188
3724
|
}
|
|
3189
3725
|
const aiProvider = options.aiProvider ?? "anthropic";
|
|
3190
|
-
(0, import_node_fs11.mkdirSync)((0,
|
|
3191
|
-
(0, import_node_fs11.mkdirSync)((0,
|
|
3192
|
-
(0, import_node_fs11.mkdirSync)((0,
|
|
3726
|
+
(0, import_node_fs11.mkdirSync)((0, import_node_path10.dirname)(configPath), { recursive: true });
|
|
3727
|
+
(0, import_node_fs11.mkdirSync)((0, import_node_path10.resolve)(rootDir, "src/odla"), { recursive: true });
|
|
3728
|
+
(0, import_node_fs11.mkdirSync)((0, import_node_path10.resolve)(rootDir, ".odla"), { recursive: true });
|
|
3193
3729
|
(0, import_node_fs11.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
3194
|
-
writeIfMissing((0,
|
|
3195
|
-
writeIfMissing((0,
|
|
3730
|
+
writeIfMissing((0, import_node_path10.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
3731
|
+
writeIfMissing((0, import_node_path10.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
3196
3732
|
ensureGitignore(rootDir);
|
|
3197
3733
|
out.log(`created ${relativeDisplay(configPath, rootDir)}`);
|
|
3198
3734
|
out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
|
|
@@ -3374,8 +3910,8 @@ function assertWranglerConfig(cfg) {
|
|
|
3374
3910
|
}
|
|
3375
3911
|
|
|
3376
3912
|
// src/secrets-set.ts
|
|
3377
|
-
var
|
|
3378
|
-
var
|
|
3913
|
+
var import_ai2 = require("@odla-ai/ai");
|
|
3914
|
+
var import_apps8 = require("@odla-ai/apps");
|
|
3379
3915
|
var PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
|
|
3380
3916
|
async function secretsSet(options) {
|
|
3381
3917
|
const name = (options.name ?? "").trim();
|
|
@@ -3386,7 +3922,7 @@ async function secretsSet(options) {
|
|
|
3386
3922
|
const { cfg, tenantId, value: value2, doFetch, out } = await resolveVaultWrite(options);
|
|
3387
3923
|
const token = await getDeveloperToken(cfg, options, doFetch, out);
|
|
3388
3924
|
try {
|
|
3389
|
-
await (0,
|
|
3925
|
+
await (0, import_ai2.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, name, value2);
|
|
3390
3926
|
} catch (err) {
|
|
3391
3927
|
throw new Error(scrubValue(err instanceof Error ? err.message : String(err), value2));
|
|
3392
3928
|
}
|
|
@@ -3427,13 +3963,13 @@ async function resolveVaultWrite(options) {
|
|
|
3427
3963
|
throw new Error(`refusing to store a secret for "${options.env}" without --yes`);
|
|
3428
3964
|
}
|
|
3429
3965
|
const value2 = await secretInputValue(options, "secret");
|
|
3430
|
-
return { cfg, tenantId: (0,
|
|
3966
|
+
return { cfg, tenantId: (0, import_apps8.tenantIdFor)(cfg.app.id, options.env), value: value2, doFetch, out };
|
|
3431
3967
|
}
|
|
3432
3968
|
|
|
3433
3969
|
// src/skill.ts
|
|
3434
3970
|
var import_node_fs12 = require("fs");
|
|
3435
3971
|
var import_node_os2 = require("os");
|
|
3436
|
-
var
|
|
3972
|
+
var import_node_path11 = require("path");
|
|
3437
3973
|
var import_node_url2 = require("url");
|
|
3438
3974
|
|
|
3439
3975
|
// src/skill-adapters.ts
|
|
@@ -3512,8 +4048,8 @@ function installSkill(options = {}) {
|
|
|
3512
4048
|
const files = listFiles(sourceDir);
|
|
3513
4049
|
if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
|
|
3514
4050
|
const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
|
|
3515
|
-
const root = (0,
|
|
3516
|
-
const home = (0,
|
|
4051
|
+
const root = (0, import_node_path11.resolve)(options.dir ?? process.cwd());
|
|
4052
|
+
const home = (0, import_node_path11.resolve)(options.homeDir ?? (0, import_node_os2.homedir)());
|
|
3517
4053
|
const plans = /* @__PURE__ */ new Map();
|
|
3518
4054
|
const targets = /* @__PURE__ */ new Map();
|
|
3519
4055
|
const rememberTarget = (harness, target) => {
|
|
@@ -3527,48 +4063,48 @@ function installSkill(options = {}) {
|
|
|
3527
4063
|
plans.set(target, { target, content: content2, boundary, managedMerge });
|
|
3528
4064
|
};
|
|
3529
4065
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
3530
|
-
for (const rel of files) plan((0,
|
|
4066
|
+
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);
|
|
3531
4067
|
};
|
|
3532
4068
|
let targetDir;
|
|
3533
4069
|
if (options.global) {
|
|
3534
|
-
const claudeRoot = (0,
|
|
3535
|
-
const codexRoot = (0,
|
|
4070
|
+
const claudeRoot = (0, import_node_path11.join)(home, ".claude", "skills");
|
|
4071
|
+
const codexRoot = (0, import_node_path11.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path11.join)(home, ".codex"), "skills");
|
|
3536
4072
|
targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
|
|
3537
4073
|
for (const harness of harnesses) {
|
|
3538
4074
|
const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
|
|
3539
|
-
planSkillTree(skillRoot, harness === "claude" ? home : (0,
|
|
4075
|
+
planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path11.dirname)((0, import_node_path11.dirname)(codexRoot)));
|
|
3540
4076
|
rememberTarget(harness, skillRoot);
|
|
3541
4077
|
}
|
|
3542
4078
|
} else {
|
|
3543
|
-
const sharedRoot = (0,
|
|
4079
|
+
const sharedRoot = (0, import_node_path11.join)(root, ".agents", "skills");
|
|
3544
4080
|
planSkillTree(sharedRoot);
|
|
3545
|
-
const claudeRoot = (0,
|
|
4081
|
+
const claudeRoot = (0, import_node_path11.join)(root, ".claude", "skills");
|
|
3546
4082
|
targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
|
|
3547
4083
|
for (const harness of harnesses) rememberTarget(harness, sharedRoot);
|
|
3548
4084
|
if (harnesses.includes("claude")) {
|
|
3549
4085
|
for (const skill of skillNames(files)) {
|
|
3550
|
-
const canonical = (0, import_node_fs12.readFileSync)((0,
|
|
3551
|
-
plan((0,
|
|
4086
|
+
const canonical = (0, import_node_fs12.readFileSync)((0, import_node_path11.join)(sourceDir, skill, "SKILL.md"), "utf8");
|
|
4087
|
+
plan((0, import_node_path11.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
|
|
3552
4088
|
}
|
|
3553
4089
|
rememberTarget("claude", claudeRoot);
|
|
3554
4090
|
}
|
|
3555
4091
|
if (harnesses.includes("cursor")) {
|
|
3556
|
-
const cursorRule = (0,
|
|
4092
|
+
const cursorRule = (0, import_node_path11.join)(root, ".cursor", "rules", "odla.mdc");
|
|
3557
4093
|
plan(cursorRule, CURSOR_RULE);
|
|
3558
4094
|
rememberTarget("cursor", cursorRule);
|
|
3559
4095
|
}
|
|
3560
4096
|
if (harnesses.includes("agents")) {
|
|
3561
|
-
const agentsFile = (0,
|
|
4097
|
+
const agentsFile = (0, import_node_path11.join)(root, "AGENTS.md");
|
|
3562
4098
|
plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
3563
4099
|
rememberTarget("agents", agentsFile);
|
|
3564
4100
|
}
|
|
3565
4101
|
if (harnesses.includes("copilot")) {
|
|
3566
|
-
const copilotFile = (0,
|
|
4102
|
+
const copilotFile = (0, import_node_path11.join)(root, ".github", "copilot-instructions.md");
|
|
3567
4103
|
plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
3568
4104
|
rememberTarget("copilot", copilotFile);
|
|
3569
4105
|
}
|
|
3570
4106
|
if (harnesses.includes("gemini")) {
|
|
3571
|
-
const geminiFile = (0,
|
|
4107
|
+
const geminiFile = (0, import_node_path11.join)(root, "GEMINI.md");
|
|
3572
4108
|
plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
3573
4109
|
rememberTarget("gemini", geminiFile);
|
|
3574
4110
|
}
|
|
@@ -3604,7 +4140,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
3604
4140
|
}
|
|
3605
4141
|
for (const file of plans.values()) {
|
|
3606
4142
|
if (!(0, import_node_fs12.existsSync)(file.target) || (0, import_node_fs12.readFileSync)(file.target, "utf8") !== file.content) {
|
|
3607
|
-
(0, import_node_fs12.mkdirSync)((0,
|
|
4143
|
+
(0, import_node_fs12.mkdirSync)((0, import_node_path11.dirname)(file.target), { recursive: true });
|
|
3608
4144
|
(0, import_node_fs12.writeFileSync)(file.target, file.content);
|
|
3609
4145
|
}
|
|
3610
4146
|
}
|
|
@@ -3624,7 +4160,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
3624
4160
|
};
|
|
3625
4161
|
}
|
|
3626
4162
|
function pathsUnder(root, paths) {
|
|
3627
|
-
return [...paths].map((path) => (0,
|
|
4163
|
+
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();
|
|
3628
4164
|
}
|
|
3629
4165
|
function normalizeHarnesses(values, global) {
|
|
3630
4166
|
const requested = values?.length ? values : ["claude"];
|
|
@@ -3669,13 +4205,13 @@ function managedFileContent(path, block, force, boundary) {
|
|
|
3669
4205
|
return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
|
|
3670
4206
|
}
|
|
3671
4207
|
function symlinkedComponent(boundary, target) {
|
|
3672
|
-
const rel = (0,
|
|
3673
|
-
if (rel === ".." || rel.startsWith(`..${
|
|
4208
|
+
const rel = (0, import_node_path11.relative)(boundary, target);
|
|
4209
|
+
if (rel === ".." || rel.startsWith(`..${import_node_path11.sep}`) || (0, import_node_path11.isAbsolute)(rel)) {
|
|
3674
4210
|
throw new Error(`agent setup target escapes its install root: ${target}`);
|
|
3675
4211
|
}
|
|
3676
4212
|
let current = boundary;
|
|
3677
|
-
for (const part of rel.split(
|
|
3678
|
-
current = (0,
|
|
4213
|
+
for (const part of rel.split(import_node_path11.sep).filter(Boolean)) {
|
|
4214
|
+
current = (0, import_node_path11.join)(current, part);
|
|
3679
4215
|
try {
|
|
3680
4216
|
if ((0, import_node_fs12.lstatSync)(current).isSymbolicLink()) return current;
|
|
3681
4217
|
} catch (error) {
|
|
@@ -3692,9 +4228,9 @@ function listFiles(dir) {
|
|
|
3692
4228
|
const results = [];
|
|
3693
4229
|
const walk = (current) => {
|
|
3694
4230
|
for (const entry of (0, import_node_fs12.readdirSync)(current, { withFileTypes: true })) {
|
|
3695
|
-
const path = (0,
|
|
4231
|
+
const path = (0, import_node_path11.join)(current, entry.name);
|
|
3696
4232
|
if (entry.isDirectory()) walk(path);
|
|
3697
|
-
else results.push((0,
|
|
4233
|
+
else results.push((0, import_node_path11.relative)(dir, path));
|
|
3698
4234
|
}
|
|
3699
4235
|
};
|
|
3700
4236
|
walk(dir);
|
|
@@ -3770,7 +4306,7 @@ async function smoke(options) {
|
|
|
3770
4306
|
out.log(` schema: ${liveEntities.length} entities`);
|
|
3771
4307
|
const aggregateEntity = expectedEntities[0] ?? liveEntities[0];
|
|
3772
4308
|
if (aggregateEntity) {
|
|
3773
|
-
const aggregate = await
|
|
4309
|
+
const aggregate = await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
|
|
3774
4310
|
ns: aggregateEntity,
|
|
3775
4311
|
aggregate: { count: true }
|
|
3776
4312
|
});
|
|
@@ -3814,16 +4350,16 @@ async function getJson(doFetch, url, bearer) {
|
|
|
3814
4350
|
const res = await doFetch(url, {
|
|
3815
4351
|
headers: bearer ? { authorization: `Bearer ${bearer}` } : void 0
|
|
3816
4352
|
});
|
|
3817
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await
|
|
4353
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText4(res)}`);
|
|
3818
4354
|
return res.json();
|
|
3819
4355
|
}
|
|
3820
|
-
async function
|
|
4356
|
+
async function postJson2(doFetch, url, bearer, body) {
|
|
3821
4357
|
const res = await doFetch(url, {
|
|
3822
4358
|
method: "POST",
|
|
3823
4359
|
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
3824
4360
|
body: JSON.stringify(body)
|
|
3825
4361
|
});
|
|
3826
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await
|
|
4362
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText4(res)}`);
|
|
3827
4363
|
return res.json();
|
|
3828
4364
|
}
|
|
3829
4365
|
function publicConfigUrl(platformUrl, appId, env) {
|
|
@@ -3831,7 +4367,7 @@ function publicConfigUrl(platformUrl, appId, env) {
|
|
|
3831
4367
|
url.searchParams.set("env", env);
|
|
3832
4368
|
return url.toString();
|
|
3833
4369
|
}
|
|
3834
|
-
async function
|
|
4370
|
+
async function safeText4(res) {
|
|
3835
4371
|
try {
|
|
3836
4372
|
return redactSecrets((await res.text()).slice(0, 500));
|
|
3837
4373
|
} catch {
|
|
@@ -3887,6 +4423,26 @@ async function secretsCommand(parsed, deps) {
|
|
|
3887
4423
|
});
|
|
3888
4424
|
}
|
|
3889
4425
|
async function projectCommand(command, parsed, deps) {
|
|
4426
|
+
if (command === "config") {
|
|
4427
|
+
const sub = parsed.positionals[1];
|
|
4428
|
+
if (sub !== "diff" && sub !== "plan") {
|
|
4429
|
+
throw new Error(`unknown config subcommand "${sub ?? ""}". Try "odla-ai config diff --json".`);
|
|
4430
|
+
}
|
|
4431
|
+
assertArgs(parsed, ["config", "token", "email", "open", "json"], 2);
|
|
4432
|
+
const options = {
|
|
4433
|
+
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
4434
|
+
token: stringOpt(parsed.options.token),
|
|
4435
|
+
email: stringOpt(parsed.options.email),
|
|
4436
|
+
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
4437
|
+
json: parsed.options.json === true,
|
|
4438
|
+
fetch: deps.fetch,
|
|
4439
|
+
openApprovalUrl: deps.openUrl,
|
|
4440
|
+
stdout: deps.stdout
|
|
4441
|
+
};
|
|
4442
|
+
if (sub === "diff") await configDiff(options);
|
|
4443
|
+
else await configPlan(options);
|
|
4444
|
+
return true;
|
|
4445
|
+
}
|
|
3890
4446
|
if (command === "init") {
|
|
3891
4447
|
assertArgs(parsed, ["app-id", "name", "config", "env", "services", "ai-provider", "force"], 1);
|
|
3892
4448
|
initProject({
|
|
@@ -3947,7 +4503,7 @@ async function projectCommand(command, parsed, deps) {
|
|
|
3947
4503
|
// src/code-connect.ts
|
|
3948
4504
|
var import_node_fs14 = require("fs");
|
|
3949
4505
|
var import_node_os4 = require("os");
|
|
3950
|
-
var
|
|
4506
|
+
var import_node_path13 = require("path");
|
|
3951
4507
|
|
|
3952
4508
|
// ../harness/dist/chunk-QTUEF2HZ.js
|
|
3953
4509
|
var HARNESS_PROTOCOL_VERSION = 1;
|
|
@@ -4565,7 +5121,7 @@ var CamelError = class extends Error {
|
|
|
4565
5121
|
};
|
|
4566
5122
|
|
|
4567
5123
|
// ../camel/dist/chunk-L5DYU2E2.js
|
|
4568
|
-
function
|
|
5124
|
+
function canonicalJson2(value2) {
|
|
4569
5125
|
return JSON.stringify(normalize(value2));
|
|
4570
5126
|
}
|
|
4571
5127
|
async function sha256Hex(value2) {
|
|
@@ -4577,7 +5133,7 @@ function utf8Length(value2) {
|
|
|
4577
5133
|
if (typeof value2 === "string") return new TextEncoder().encode(value2).byteLength;
|
|
4578
5134
|
if (value2 instanceof Uint8Array) return value2.byteLength;
|
|
4579
5135
|
try {
|
|
4580
|
-
return new TextEncoder().encode(
|
|
5136
|
+
return new TextEncoder().encode(canonicalJson2(value2)).byteLength;
|
|
4581
5137
|
} catch {
|
|
4582
5138
|
throw new CamelError("conversion_rejected", "Unsafe input could not be canonically measured.");
|
|
4583
5139
|
}
|
|
@@ -4703,7 +5259,7 @@ async function digestCodeVerificationReceipt(fields) {
|
|
|
4703
5259
|
changedTestsRequireReview: fields.changedTestsRequireReview,
|
|
4704
5260
|
outcome: fields.outcome
|
|
4705
5261
|
};
|
|
4706
|
-
return `sha256:${await sha256Hex(
|
|
5262
|
+
return `sha256:${await sha256Hex(canonicalJson2(canonical))}`;
|
|
4707
5263
|
}
|
|
4708
5264
|
function validate(fields) {
|
|
4709
5265
|
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) {
|
|
@@ -4741,9 +5297,9 @@ async function createCodePortableCheckpoint(input) {
|
|
|
4741
5297
|
const state2 = normalizeState(input.state);
|
|
4742
5298
|
validateBaseAndPatch(input.baseCommitSha, input.patch);
|
|
4743
5299
|
const patchDigest = `sha256:${await sha256Hex(input.patch)}`;
|
|
4744
|
-
const stateDigest = `sha256:${await sha256Hex(
|
|
5300
|
+
const stateDigest = `sha256:${await sha256Hex(canonicalJson2(state2))}`;
|
|
4745
5301
|
const fields = { schemaVersion: 1, baseCommitSha: input.baseCommitSha, patchDigest, state: state2, stateDigest };
|
|
4746
|
-
const checkpointDigest = `sha256:${await sha256Hex(
|
|
5302
|
+
const checkpointDigest = `sha256:${await sha256Hex(canonicalJson2(fields))}`;
|
|
4747
5303
|
return freeze({ ...fields, patch: input.patch, checkpointDigest });
|
|
4748
5304
|
}
|
|
4749
5305
|
async function verifyCodePortableCheckpoint(value2) {
|
|
@@ -4861,7 +5417,7 @@ async function digestCodeRepositorySnapshot(snapshot, limits = DEFAULT_LIMITS) {
|
|
|
4861
5417
|
commitSha: snapshot.commitSha,
|
|
4862
5418
|
files: [...snapshot.files].map((file) => ({ path: file.path, content: file.content })).sort((left, right) => left.path.localeCompare(right.path))
|
|
4863
5419
|
};
|
|
4864
|
-
return `sha256:${await sha256Hex(
|
|
5420
|
+
return `sha256:${await sha256Hex(canonicalJson2(normalized))}`;
|
|
4865
5421
|
}
|
|
4866
5422
|
function validateSnapshot(snapshot, limits) {
|
|
4867
5423
|
if (!REPOSITORY.test(snapshot.repository) || !SHA4.test(snapshot.commitSha)) {
|
|
@@ -4903,10 +5459,10 @@ var import_path9 = require("path");
|
|
|
4903
5459
|
|
|
4904
5460
|
// ../camel/dist/chunk-LAXU2AVK.js
|
|
4905
5461
|
function conversionPolicyDigest(policy) {
|
|
4906
|
-
return sha256Hex(
|
|
5462
|
+
return sha256Hex(canonicalJson2(policy));
|
|
4907
5463
|
}
|
|
4908
5464
|
function registeredIdRegistryDigest(values) {
|
|
4909
|
-
return sha256Hex(
|
|
5465
|
+
return sha256Hex(canonicalJson2(values));
|
|
4910
5466
|
}
|
|
4911
5467
|
async function createConversionRegistry(config) {
|
|
4912
5468
|
const policies = /* @__PURE__ */ new Map();
|
|
@@ -5107,10 +5663,10 @@ function createEffectPolicy(config = {}) {
|
|
|
5107
5663
|
return Object.freeze({ evaluate: (input) => evaluate(input, registries, approvalEffects) });
|
|
5108
5664
|
}
|
|
5109
5665
|
function destinationRegistryDigest(values) {
|
|
5110
|
-
return sha256Hex(
|
|
5666
|
+
return sha256Hex(canonicalJson2([...values].sort()));
|
|
5111
5667
|
}
|
|
5112
5668
|
async function evaluate(input, registries, approvals) {
|
|
5113
|
-
const actionDigest = await sha256Hex(
|
|
5669
|
+
const actionDigest = await sha256Hex(canonicalJson2(input));
|
|
5114
5670
|
const decisionId = `decision_${actionDigest.slice(0, 24)}`;
|
|
5115
5671
|
const deny = (reasonCode) => ({ outcome: "deny", decisionId, reasonCode });
|
|
5116
5672
|
if (!input.planId || !input.tool.name || !input.tool.policyId || !Number.isSafeInteger(input.tool.version)) return deny("invalid_descriptor");
|
|
@@ -6831,268 +7387,19 @@ var CodePiRuntimeEngine = class {
|
|
|
6831
7387
|
}
|
|
6832
7388
|
};
|
|
6833
7389
|
|
|
6834
|
-
// src/
|
|
7390
|
+
// src/version.ts
|
|
6835
7391
|
var import_node_fs13 = require("fs");
|
|
6836
7392
|
function cliVersion() {
|
|
6837
7393
|
const pkg = JSON.parse((0, import_node_fs13.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
|
|
6838
7394
|
return pkg.version ?? "unknown";
|
|
6839
7395
|
}
|
|
6840
|
-
function printHelp(output = console) {
|
|
6841
|
-
output.log(`odla-ai
|
|
6842
7396
|
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
working from memory: runbooks are edited
|
|
6847
|
-
live, so your training data is out of date
|
|
6848
|
-
and this is not.
|
|
6849
|
-
odla-ai runbook impact After a change: which runbooks describe the
|
|
6850
|
-
code you just touched, so you can fix any
|
|
6851
|
-
step it made wrong.
|
|
6852
|
-
|
|
6853
|
-
Usage:
|
|
6854
|
-
odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
|
|
6855
|
-
odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
|
|
6856
|
-
odla-ai doctor [--config odla.config.mjs]
|
|
6857
|
-
odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
|
|
6858
|
-
odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
|
|
6859
|
-
odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
|
|
6860
|
-
odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
|
|
6861
|
-
odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
|
|
6862
|
-
odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
6863
|
-
odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
|
|
6864
|
-
odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
|
|
6865
|
-
odla-ai app refresh-sandbox [--include-identity] [--include-files] [--dry-run] [--json] --yes
|
|
6866
|
-
odla-ai app go-live [--include-identity] [--include-files] [--dry-run] [--json] --yes
|
|
6867
|
-
odla-ai app promote [--dry-run] [--json] --yes
|
|
6868
|
-
odla-ai app rename <name> [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
6869
|
-
odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
6870
|
-
odla-ai app owners add <email> [--email <odla-account>] [--json]
|
|
6871
|
-
odla-ai app owners remove <email> [--email <odla-account>] [--json]
|
|
6872
|
-
odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
6873
|
-
odla-ai pm task list [--app <id>] [--column <c>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
6874
|
-
odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
6875
|
-
odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
6876
|
-
odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
|
|
6877
|
-
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]
|
|
6878
|
-
odla-ai pm decision add --app <id> --title <t> --body <text> [--status <s>] [--mutation-id <id>] [--json]
|
|
6879
|
-
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]
|
|
6880
|
-
odla-ai pm <goal|task|decision|bug> get <id> [--json]
|
|
6881
|
-
odla-ai pm goal set <id> [--title <t>|--status <s>|--proof <text>|--no-proof|--target <pct>|--no-target] [--mutation-id <id>] [--json]
|
|
6882
|
-
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]
|
|
6883
|
-
odla-ai pm decision set <id> [--title <t>|--status <s>|--body <text>] [--mutation-id <id>] [--json]
|
|
6884
|
-
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]
|
|
6885
|
-
odla-ai pm <goal|task|decision> done <id> [--mutation-id <id>]
|
|
6886
|
-
odla-ai pm bug done <id> [--decision <accepted-decision-id>] [--mutation-id <id>]
|
|
6887
|
-
odla-ai pm <goal|task|decision|bug> comment <id> --body "..." [--mutation-id <id>]
|
|
6888
|
-
odla-ai pm <goal|task|decision|bug> comments <id> [--json]
|
|
6889
|
-
odla-ai pm <goal|task|decision|bug> rm <id>
|
|
6890
|
-
odla-ai pm handoff --app <id> [--json]
|
|
6891
|
-
odla-ai discuss groups [--json]
|
|
6892
|
-
odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
|
|
6893
|
-
odla-ai discuss read <topic> [--limit <n> --offset <n>] [--json]
|
|
6894
|
-
odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"] [--mutation-id <id>]
|
|
6895
|
-
odla-ai discuss reply <topic> --body "..." [--markup "..."] [--mutation-id <id>]
|
|
6896
|
-
odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
|
|
6897
|
-
odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
|
|
6898
|
-
odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
|
|
6899
|
-
odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
|
|
6900
|
-
odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
|
|
6901
|
-
odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
|
|
6902
|
-
odla-ai context list [--json]
|
|
6903
|
-
odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
|
|
6904
|
-
odla-ai context remove <name> --yes [--json]
|
|
6905
|
-
odla-ai o11y status [--app <id>] [--context <name>] [--platform https://odla.ai] [--env prod] [--minutes 60] [--json]
|
|
6906
|
-
odla-ai platform status [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
|
|
6907
|
-
odla-ai whoami [--context <name>] [--platform https://odla.ai] [--json]
|
|
6908
|
-
odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
|
|
6909
|
-
odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
|
|
6910
|
-
odla-ai runbook impact [--base origin/main] [--app <id>] [--all] [--limit <n>] [--json]
|
|
6911
|
-
odla-ai runbook lint [--app <id>] [--all] [--json]
|
|
6912
|
-
odla-ai runbook list [--app <id>] [--all] [--q <text>] [--json]
|
|
6913
|
-
odla-ai runbook comment <slug> --body "..." [--app <id>]
|
|
6914
|
-
odla-ai runbook get <slug> [--app <id>]
|
|
6915
|
-
odla-ai runbook new <slug> --title <t> --file <path|-> [--summary <s>] [--requires <specs>] [--app <id>]
|
|
6916
|
-
odla-ai runbook edit <slug> [--file <path|->|--body "..."] [--note <why>] [--requires <specs>] [--app <id>]
|
|
6917
|
-
odla-ai runbook import <dir> [--visibility operator|admin] [--dry-run] [--app <id>]
|
|
6918
|
-
odla-ai runbook publish <slug> [--app <id>]
|
|
6919
|
-
odla-ai runbook visibility <slug> <operator|admin> [--app <id>]
|
|
6920
|
-
odla-ai runbook archive <slug> [--app <id>]
|
|
6921
|
-
odla-ai runbook history <slug> [--app <id>] [--json]
|
|
6922
|
-
odla-ai runbook revert <slug> --version <n> [--app <id>]
|
|
6923
|
-
odla-ai runbook rm <slug> [--app <id>]
|
|
6924
|
-
odla-ai capabilities [--json]
|
|
6925
|
-
odla-ai code connect [--env dev|prod] [--email <odla-account>] [--engine auto|container|podman|docker] [--slots <1-64>] [--once]
|
|
6926
|
-
odla-ai admin ai show [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
|
|
6927
|
-
odla-ai admin ai models [--context <name>] [--provider <id>] [--json]
|
|
6928
|
-
odla-ai admin ai set <purpose> [--context <name>] [--provider <id>] [--model <id>] [--enabled|--no-enabled]
|
|
6929
|
-
[--max-input-bytes <n>] [--max-output-tokens <n>] [--max-calls-per-run <n>] [--json]
|
|
6930
|
-
odla-ai admin ai set security [--context <name>] --discovery-provider <id> --discovery-model <id>
|
|
6931
|
-
--validation-provider <id> --validation-model <id> [--enabled|--no-enabled] [--json]
|
|
6932
|
-
odla-ai admin ai credentials [--context <name>] [--json]
|
|
6933
|
-
odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
|
|
6934
|
-
odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
|
|
6935
|
-
odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
|
|
6936
|
-
odla-ai security github connect [--repo owner/name] [--env dev] [--email <odla-account>] [--no-open]
|
|
6937
|
-
odla-ai security github disconnect --source <id> [--env dev] [--yes]
|
|
6938
|
-
odla-ai security plan [--env dev] [--json]
|
|
6939
|
-
odla-ai security sources [--env dev] [--json]
|
|
6940
|
-
odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
|
|
6941
|
-
odla-ai security status <job-id> [--follow] [--json]
|
|
6942
|
-
odla-ai security report <job-id> [--json]
|
|
6943
|
-
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
6944
|
-
odla-ai security run [target] --self --ack-redacted-source
|
|
6945
|
-
odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
6946
|
-
odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
|
|
6947
|
-
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
6948
|
-
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
6949
|
-
odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
|
|
6950
|
-
odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
|
|
6951
|
-
odla-ai version
|
|
6952
|
-
|
|
6953
|
-
Commands:
|
|
6954
|
-
agent Inspect durable agent wakeups and explicitly requeue a
|
|
6955
|
-
dead-lettered job; JSON output is stable for remote operators.
|
|
6956
|
-
runbook odla's operational procedures, stored in the database and read at
|
|
6957
|
-
the moment they are followed. "ask" gives a written, cited answer;
|
|
6958
|
-
"search" the passages behind it; "get" the whole document.
|
|
6959
|
-
"impact" diffs your working tree against a base ref and names the
|
|
6960
|
-
runbooks that describe what you changed \u2014 run it after touching a
|
|
6961
|
-
package's exported API or JSDoc, or a Studio surface. "lint"
|
|
6962
|
-
checks the corpus the other way: every odla-ai command the
|
|
6963
|
-
runbooks name is held against this CLI's real command surface,
|
|
6964
|
-
and against the minimum versions each runbook declares. A wrong step
|
|
6965
|
-
is fixed with "edit", which takes effect immediately: a runbook is
|
|
6966
|
-
a row, not a release. Runbooks describe PROCEDURE; what an export
|
|
6967
|
-
does and what it guarantees is JSDoc, rendered per package at
|
|
6968
|
-
https://odla.ai/docs and shipped in the installed .d.ts. Answering
|
|
6969
|
-
a question usually needs both.
|
|
6970
|
-
whoami Report who this terminal is authenticated as, and whether it holds
|
|
6971
|
-
platform admin. A handshake-minted device token is never admin,
|
|
6972
|
-
however the human who approved it is configured.
|
|
6973
|
-
context Explain selected config, platform, app, environment, and
|
|
6974
|
-
developer-token provenance without printing credentials or
|
|
6975
|
-
starting a device handshake. Operator work can run outside a
|
|
6976
|
-
project checkout with explicit flags or ODLA_* environment
|
|
6977
|
-
variables.
|
|
6978
|
-
setup Install offline odla runbooks for common coding-agent harnesses.
|
|
6979
|
-
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
6980
|
-
doctor Validate and summarize the project config without network calls.
|
|
6981
|
-
calendar Inspect, connect, or disconnect the live Google booking connection.
|
|
6982
|
-
app Archive (suspend, data retained), restore, export, import, or
|
|
6983
|
-
manage the co-owners of the app. Archiving takes every
|
|
6984
|
-
environment's data plane down until restored; permanent deletion
|
|
6985
|
-
has NO CLI \u2014 it requires a signed-in owner in Studio, and no
|
|
6986
|
-
machine or agent credential can purge. "app export" downloads a
|
|
6987
|
-
portable gzipped-JSONL snapshot of one database (--fresh takes a
|
|
6988
|
-
new snapshot first) \u2014 your data is always yours to take.
|
|
6989
|
-
"app import" upserts rows back in from JSON, JSONL, or a
|
|
6990
|
-
{namespace: rows} map (the shape a query returns); it writes only
|
|
6991
|
-
with --yes, so the bare command is a dry run, and it refuses to
|
|
6992
|
-
guess an id mode that would duplicate rows on a re-run.
|
|
6993
|
-
"app refresh-sandbox" replaces the sandbox with a copy of live
|
|
6994
|
-
(the routine dev loop); "app go-live" copies the sandbox into an
|
|
6995
|
-
EMPTY live database, once, at launch; "app promote" pushes only
|
|
6996
|
-
schema, rules and gates up afterwards, leaving live's rows alone.
|
|
6997
|
-
Each prints its plan and writes nothing without --yes, so the
|
|
6998
|
-
bare command is the dry run. Clerk config, triggers, allowlists,
|
|
6999
|
-
secrets and API keys never travel; identity rows stay behind
|
|
7000
|
-
unless --include-identity.
|
|
7001
|
-
"app rename <name>" changes only the display name humans read;
|
|
7002
|
-
the app id is permanent (tenants, URLs and keys embed it), so a
|
|
7003
|
-
rename never re-provisions anything or invalidates a credential.
|
|
7004
|
-
"app owners add <email>" grants a signed-up odla member the SAME
|
|
7005
|
-
full access as you; they then run their own "provision" to mint
|
|
7006
|
-
their own credentials for the shared db (the live one included)
|
|
7007
|
-
\u2014 no secret is ever copied between people.
|
|
7008
|
-
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
7009
|
-
code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
|
|
7010
|
-
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
7011
|
-
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
7012
|
-
pm Project management (via @odla-ai/pm) shared across the apps you
|
|
7013
|
-
co-own: conformance goals, kanban tasks, decisions, and bugs. With
|
|
7014
|
-
no --app, "list" spans every co-owned project (the cross-project
|
|
7015
|
-
view); with --app it scopes to one. Same device-grant auth as
|
|
7016
|
-
"app". Entities: goal (alias conformance), task (alias kanban),
|
|
7017
|
-
decision, bug. Status changes and comments post to each item's
|
|
7018
|
-
@odla-ai/chat discussion thread.
|
|
7019
|
-
discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
|
|
7020
|
-
one group per project, topics with replies, @-mentions of people,
|
|
7021
|
-
agents, PM items, and projects. Built for unattended use \u2014 post a
|
|
7022
|
-
question, then "watch" it until someone answers. "watch" exits 75
|
|
7023
|
-
(not 1) when it times out with nothing new, so a script can tell
|
|
7024
|
-
"no answer yet" from a failure and just rerun. Use "who" to look
|
|
7025
|
-
up the exact @[Label](kind/id) markup for a mention.
|
|
7026
|
-
o11y Read one stable status envelope for application RED, current
|
|
7027
|
-
live-sync load/freshness, the protected commit-to-visible
|
|
7028
|
-
canary, collector ingest/scheduler trust, Cloudflare-owned
|
|
7029
|
-
runtime metrics, and a machine verdict.
|
|
7030
|
-
--json keeps auth progress on stderr for unattended agents.
|
|
7031
|
-
platform Read canonical fleet health, releases, provider load/freshness,
|
|
7032
|
-
explicit unknowns, and next actions through a read-only grant.
|
|
7033
|
-
provision Register services, compose integrations, persist credentials, optionally push secrets.
|
|
7034
|
-
smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
|
|
7035
|
-
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
7036
|
-
copilot, gemini, or agents (repeatable or comma-separated).
|
|
7037
|
-
secrets Push configured db/o11y secrets into the Worker via wrangler
|
|
7038
|
-
stdin; set stores a tenant-vault secret and set-clerk-key the
|
|
7039
|
-
reserved Clerk secret key, write-only from stdin or an env var.
|
|
7040
|
-
version Print the CLI version.
|
|
7041
|
-
|
|
7042
|
-
Safety:
|
|
7043
|
-
Every app has two databases on odla.ai: a sandbox (env "dev", tenant
|
|
7044
|
-
<appId>--dev) and a live one (env "prod", tenant <appId>). Both are served by
|
|
7045
|
-
production odla.ai \u2014 there is no separate odla to point at. New projects get
|
|
7046
|
-
the sandbox only. Add "prod" explicitly to odla.config.mjs and pass --yes to
|
|
7047
|
-
provision the live database; use --dry-run first to inspect the resolved plan.
|
|
7048
|
-
Provision caches the approved developer token and service credentials under
|
|
7049
|
-
.odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
|
|
7050
|
-
preflights Wrangler before any shown-once issuance or destructive rotation.
|
|
7051
|
-
Projectless PM, Discussions, o11y, runbook, and identity commands use
|
|
7052
|
-
--platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
|
|
7053
|
-
ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
|
|
7054
|
-
select it explicitly with --context or ODLA_CONTEXT. Each named context gets
|
|
7055
|
-
isolated developer and scoped-token caches. Override their locations with
|
|
7056
|
-
ODLA_DEV_TOKEN_FILE and ODLA_ADMIN_TOKEN_FILE; ODLA_CONTEXT_FILE relocates
|
|
7057
|
-
the metadata file. Flags and specific ODLA_* scope variables beat a selected
|
|
7058
|
-
context, which beats project config. There is no ambient current context.
|
|
7059
|
-
"context show" reports only provenance and cache state and never authenticates.
|
|
7060
|
-
Provision opens the approval page in your browser automatically whenever the
|
|
7061
|
-
machine can show one, including agent-driven runs; only CI, SSH, and
|
|
7062
|
-
display-less hosts skip it. Use --open to force or --no-open to suppress.
|
|
7063
|
-
Browser launch is best-effort: the printed approval URL is authoritative and
|
|
7064
|
-
agents must relay it to the human verbatim. A started handshake is persisted
|
|
7065
|
-
under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
|
|
7066
|
-
the same code. Outside an interactive terminal the wait is capped (90s by
|
|
7067
|
-
default, --wait <seconds> to change); a still-pending handshake then exits
|
|
7068
|
-
with code 75: relay the URL, wait for approval, and re-run to collect.
|
|
7069
|
-
A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
|
|
7070
|
-
The email is a non-secret identity hint: never provide a password or session
|
|
7071
|
-
token. The matching account must already exist, be signed in, explicitly
|
|
7072
|
-
review the exact code, and finish any current request before claiming another.
|
|
7073
|
-
Run Code from a GitHub checkout already connected to an app in Studio; an
|
|
7074
|
-
odla.config.mjs may select the app explicitly but is not required. Code host
|
|
7075
|
-
approval and credential hashes live in odla-ai/db. The host
|
|
7076
|
-
credential is never written under .odla/; it exists only in the foreground
|
|
7077
|
-
"code connect" process and is rotated by the next approved connection.
|
|
7078
|
-
Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
|
|
7079
|
-
OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
|
|
7080
|
-
GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
|
|
7081
|
-
for a PAT or provider key. GitHub read access is separate from the explicit
|
|
7082
|
-
--ack-redacted-source consent and the exact --plan-digest printed by security
|
|
7083
|
-
plan are required before bounded snippets reach System AI.
|
|
7084
|
-
Run security plan first to inspect the admin-selected providers, models,
|
|
7085
|
-
per-route bounds, credential readiness, retention, no-execution boundary,
|
|
7086
|
-
and digest that binds consent to that exact plan.
|
|
7087
|
-
`);
|
|
7088
|
-
}
|
|
7089
|
-
|
|
7090
|
-
// src/security-hosted-github.ts
|
|
7091
|
-
var import_node_child_process4 = require("child_process");
|
|
7092
|
-
var import_node_util2 = require("util");
|
|
7397
|
+
// src/security-hosted-github.ts
|
|
7398
|
+
var import_node_child_process4 = require("child_process");
|
|
7399
|
+
var import_node_util2 = require("util");
|
|
7093
7400
|
|
|
7094
7401
|
// src/security-hosted-request.ts
|
|
7095
|
-
async function requestHostedSecurityJson(options, path, init,
|
|
7402
|
+
async function requestHostedSecurityJson(options, path, init, action2) {
|
|
7096
7403
|
const platform = platformAudience(options.platform);
|
|
7097
7404
|
const token = hostedSecurityCredential(options.token);
|
|
7098
7405
|
const requestInit = {
|
|
@@ -7112,7 +7419,7 @@ async function requestHostedSecurityJson(options, path, init, action) {
|
|
|
7112
7419
|
if (!response2.ok) {
|
|
7113
7420
|
const code = optionalHostedText(body.error?.code, "error code", 128);
|
|
7114
7421
|
const message2 = optionalHostedText(body.error?.message, "error message", 300);
|
|
7115
|
-
throw new Error(`${
|
|
7422
|
+
throw new Error(`${action2} failed (${response2.status})${code ? ` ${code}` : ""}${message2 ? `: ${message2}` : ""}`);
|
|
7116
7423
|
}
|
|
7117
7424
|
return body;
|
|
7118
7425
|
}
|
|
@@ -7303,7 +7610,7 @@ async function defaultReadOrigin(cwd) {
|
|
|
7303
7610
|
|
|
7304
7611
|
// src/code-local-source.ts
|
|
7305
7612
|
var import_node_child_process5 = require("child_process");
|
|
7306
|
-
var
|
|
7613
|
+
var import_node_crypto2 = require("crypto");
|
|
7307
7614
|
var SOURCE_LIMITS2 = { maxFiles: 2e4, maxBytes: 512 * 1024 * 1024 };
|
|
7308
7615
|
async function prepareCodeLocalSource(cwd, repository, readHead = readGitHead) {
|
|
7309
7616
|
const headCommitSha = await readHead(cwd);
|
|
@@ -7354,15 +7661,15 @@ async function readGitHead(cwd) {
|
|
|
7354
7661
|
return value2;
|
|
7355
7662
|
}
|
|
7356
7663
|
function digestText(value2) {
|
|
7357
|
-
return `sha256:${(0,
|
|
7664
|
+
return `sha256:${(0, import_node_crypto2.createHash)("sha256").update(value2).digest("hex")}`;
|
|
7358
7665
|
}
|
|
7359
7666
|
|
|
7360
7667
|
// src/code-images.ts
|
|
7361
7668
|
var import_node_child_process6 = require("child_process");
|
|
7362
|
-
var
|
|
7669
|
+
var import_node_crypto3 = require("crypto");
|
|
7363
7670
|
var import_promises10 = require("fs/promises");
|
|
7364
7671
|
var import_node_os3 = require("os");
|
|
7365
|
-
var
|
|
7672
|
+
var import_node_path12 = require("path");
|
|
7366
7673
|
var import_node_url3 = require("url");
|
|
7367
7674
|
|
|
7368
7675
|
// src/code-runtime-config.ts
|
|
@@ -7445,13 +7752,13 @@ async function embeddedPiImageName() {
|
|
|
7445
7752
|
const bundle = await (0, import_promises10.readFile)(embeddedPiAssetPath()).catch(() => {
|
|
7446
7753
|
throw new Error("CLI-embedded Pi runtime is missing; reinstall this exact @odla-ai/cli version");
|
|
7447
7754
|
});
|
|
7448
|
-
return `odla-ai/pi-agent:embedded-sha256-${(0,
|
|
7755
|
+
return `odla-ai/pi-agent:embedded-sha256-${(0, import_node_crypto3.createHash)("sha256").update(bundle).digest("hex")}`;
|
|
7449
7756
|
}
|
|
7450
7757
|
async function buildEmbeddedPiImage(engine, image, run) {
|
|
7451
|
-
const context = await (0, import_promises10.mkdtemp)((0,
|
|
7758
|
+
const context = await (0, import_promises10.mkdtemp)((0, import_node_path12.join)((0, import_node_os3.tmpdir)(), "odla-code-pi-"));
|
|
7452
7759
|
try {
|
|
7453
|
-
await (0, import_promises10.copyFile)(embeddedPiAssetPath(), (0,
|
|
7454
|
-
await (0, import_promises10.writeFile)((0,
|
|
7760
|
+
await (0, import_promises10.copyFile)(embeddedPiAssetPath(), (0, import_node_path12.join)(context, "pi-agent.js"));
|
|
7761
|
+
await (0, import_promises10.writeFile)((0, import_node_path12.join)(context, "Dockerfile"), [
|
|
7455
7762
|
`FROM ${CODE_NODE_IMAGE}`,
|
|
7456
7763
|
"COPY pi-agent.js /opt/odla/pi-agent.js",
|
|
7457
7764
|
"WORKDIR /workspace",
|
|
@@ -7467,7 +7774,7 @@ async function buildEmbeddedPiImage(engine, image, run) {
|
|
|
7467
7774
|
// src/code-connect.ts
|
|
7468
7775
|
async function codeConnect(options) {
|
|
7469
7776
|
const cwd = options.cwd ?? process.cwd();
|
|
7470
|
-
const configPath = (0,
|
|
7777
|
+
const configPath = (0, import_node_path13.resolve)(cwd, options.configPath);
|
|
7471
7778
|
const cfg = (0, import_node_fs14.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
|
|
7472
7779
|
const requestedAppId = options.appId?.trim();
|
|
7473
7780
|
if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
|
|
@@ -7635,9 +7942,9 @@ function parseConnection(value2, appId, appEnv) {
|
|
|
7635
7942
|
}
|
|
7636
7943
|
return root;
|
|
7637
7944
|
}
|
|
7638
|
-
function apiFailure(
|
|
7945
|
+
function apiFailure(action2, status, value2) {
|
|
7639
7946
|
const message2 = record4(record4(value2)?.error)?.message;
|
|
7640
|
-
return `${
|
|
7947
|
+
return `${action2} failed (${status})${typeof message2 === "string" ? `: ${message2}` : ""}`;
|
|
7641
7948
|
}
|
|
7642
7949
|
function record4(value2) {
|
|
7643
7950
|
return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
|
|
@@ -7704,136 +8011,391 @@ function clean3(value2) {
|
|
|
7704
8011
|
return normalized || void 0;
|
|
7705
8012
|
}
|
|
7706
8013
|
|
|
7707
|
-
// src/context-command.ts
|
|
7708
|
-
async function contextCommand(parsed, deps = {}) {
|
|
7709
|
-
assertArgs(
|
|
7710
|
-
parsed,
|
|
7711
|
-
[
|
|
7712
|
-
"config",
|
|
7713
|
-
"context",
|
|
7714
|
-
"platform",
|
|
7715
|
-
"app",
|
|
7716
|
-
"env",
|
|
7717
|
-
"token",
|
|
7718
|
-
"json",
|
|
7719
|
-
"yes"
|
|
7720
|
-
],
|
|
7721
|
-
3
|
|
7722
|
-
);
|
|
7723
|
-
const
|
|
7724
|
-
const out = deps.stdout ?? console;
|
|
7725
|
-
if (
|
|
7726
|
-
const profiles = listOperatorProfiles();
|
|
7727
|
-
if (parsed.options.json === true) {
|
|
7728
|
-
out.log(JSON.stringify({ schemaVersion: 1, profiles }, null, 2));
|
|
7729
|
-
return;
|
|
7730
|
-
}
|
|
7731
|
-
if (profiles.length === 0) {
|
|
7732
|
-
out.log("No named operator contexts.");
|
|
7733
|
-
return;
|
|
7734
|
-
}
|
|
7735
|
-
out.log("name platform app environment");
|
|
7736
|
-
for (const profile of profiles) {
|
|
7737
|
-
out.log(
|
|
7738
|
-
`${profile.name} ${profile.platform} ${profile.app ?? ""} ${profile.environment ?? ""}`
|
|
7739
|
-
);
|
|
7740
|
-
}
|
|
7741
|
-
return;
|
|
7742
|
-
}
|
|
7743
|
-
if (
|
|
7744
|
-
if (parsed.options.token !== void 0) {
|
|
7745
|
-
throw new Error(
|
|
7746
|
-
"context save does not accept --token; contexts store scope metadata only"
|
|
7747
|
-
);
|
|
7748
|
-
}
|
|
7749
|
-
const name = requireName(parsed);
|
|
7750
|
-
const context2 = await resolveOperatorContext(parsed, {
|
|
7751
|
-
allowMissingConfig: true
|
|
7752
|
-
});
|
|
7753
|
-
const profile = {
|
|
7754
|
-
platform: context2.platform.value,
|
|
7755
|
-
...context2.app.value ? { app: context2.app.value } : {},
|
|
7756
|
-
...context2.environment.value ? { environment: context2.environment.value } : {}
|
|
7757
|
-
};
|
|
7758
|
-
saveOperatorProfile(name, profile, context2.profile.file);
|
|
7759
|
-
if (parsed.options.json === true) {
|
|
7760
|
-
out.log(JSON.stringify({ schemaVersion: 1, name, ...profile }, null, 2));
|
|
7761
|
-
} else {
|
|
7762
|
-
out.log(`saved operator context "${name}" (${profile.platform})`);
|
|
7763
|
-
}
|
|
7764
|
-
return;
|
|
7765
|
-
}
|
|
7766
|
-
if (
|
|
7767
|
-
const name = requireName(parsed);
|
|
7768
|
-
if (parsed.options.yes !== true) {
|
|
7769
|
-
throw new Error(`context remove "${name}" requires --yes`);
|
|
7770
|
-
}
|
|
7771
|
-
const removed = removeOperatorProfile(name);
|
|
7772
|
-
const result2 = {
|
|
7773
|
-
schemaVersion: 1,
|
|
7774
|
-
name,
|
|
7775
|
-
removed,
|
|
7776
|
-
credentialCachesRemoved: false,
|
|
7777
|
-
grantsRevoked: false
|
|
7778
|
-
};
|
|
7779
|
-
if (parsed.options.json === true) out.log(JSON.stringify(result2, null, 2));
|
|
7780
|
-
else {
|
|
7781
|
-
const status = removed ? `removed operator context "${name}"` : `operator context "${name}" was already absent`;
|
|
7782
|
-
out.log(`${status}; credential caches were not deleted or revoked`);
|
|
7783
|
-
}
|
|
7784
|
-
return;
|
|
7785
|
-
}
|
|
7786
|
-
if (
|
|
7787
|
-
throw new Error(
|
|
7788
|
-
`unknown context action "${
|
|
7789
|
-
);
|
|
7790
|
-
}
|
|
7791
|
-
const context = await resolveOperatorContext(parsed, {
|
|
7792
|
-
allowMissingConfig: true,
|
|
7793
|
-
defaultEnvironment: "prod"
|
|
7794
|
-
});
|
|
7795
|
-
const token = developerTokenStatus(context, parsed);
|
|
7796
|
-
const result = {
|
|
7797
|
-
schemaVersion: 1,
|
|
7798
|
-
profile: context.profile,
|
|
7799
|
-
config: context.config,
|
|
7800
|
-
platform: context.platform,
|
|
7801
|
-
app: context.app,
|
|
7802
|
-
environment: context.environment,
|
|
7803
|
-
authentication: {
|
|
7804
|
-
developerToken: token,
|
|
7805
|
-
scopedTokenCacheFile: context.credentials.scopedTokenFile,
|
|
7806
|
-
handshakeStarted: false
|
|
7807
|
-
}
|
|
7808
|
-
};
|
|
7809
|
-
if (parsed.options.json === true) {
|
|
7810
|
-
out.log(JSON.stringify(result, null, 2));
|
|
7811
|
-
return;
|
|
7812
|
-
}
|
|
7813
|
-
out.log(`config: ${context.config.status} (${context.config.path})`);
|
|
7814
|
-
out.log(
|
|
7815
|
-
`context: ${context.profile.name ?? "(none)"} (${context.profile.source}; ${context.profile.file})`
|
|
7816
|
-
);
|
|
7817
|
-
out.log(
|
|
7818
|
-
`platform: ${context.platform.value} (${context.platform.source})`
|
|
7819
|
-
);
|
|
7820
|
-
out.log(
|
|
7821
|
-
`app: ${context.app.value ?? "(unresolved)"} (${context.app.source})`
|
|
7822
|
-
);
|
|
7823
|
-
out.log(
|
|
7824
|
-
`env: ${context.environment.value ?? "(unresolved)"} (${context.environment.source})`
|
|
7825
|
-
);
|
|
7826
|
-
out.log(
|
|
7827
|
-
`auth: ${token.source} (cache ${token.cacheStatus}: ${token.cacheFile})`
|
|
7828
|
-
);
|
|
7829
|
-
out.log("handshake: not started");
|
|
7830
|
-
}
|
|
7831
|
-
function requireName(parsed) {
|
|
7832
|
-
const name = parsed.positionals[2]?.trim();
|
|
7833
|
-
if (!name) {
|
|
7834
|
-
throw new Error(`context ${parsed.positionals[1]} needs a profile name`);
|
|
7835
|
-
}
|
|
7836
|
-
return name;
|
|
8014
|
+
// src/context-command.ts
|
|
8015
|
+
async function contextCommand(parsed, deps = {}) {
|
|
8016
|
+
assertArgs(
|
|
8017
|
+
parsed,
|
|
8018
|
+
[
|
|
8019
|
+
"config",
|
|
8020
|
+
"context",
|
|
8021
|
+
"platform",
|
|
8022
|
+
"app",
|
|
8023
|
+
"env",
|
|
8024
|
+
"token",
|
|
8025
|
+
"json",
|
|
8026
|
+
"yes"
|
|
8027
|
+
],
|
|
8028
|
+
3
|
|
8029
|
+
);
|
|
8030
|
+
const action2 = parsed.positionals[1];
|
|
8031
|
+
const out = deps.stdout ?? console;
|
|
8032
|
+
if (action2 === "list") {
|
|
8033
|
+
const profiles = listOperatorProfiles();
|
|
8034
|
+
if (parsed.options.json === true) {
|
|
8035
|
+
out.log(JSON.stringify({ schemaVersion: 1, profiles }, null, 2));
|
|
8036
|
+
return;
|
|
8037
|
+
}
|
|
8038
|
+
if (profiles.length === 0) {
|
|
8039
|
+
out.log("No named operator contexts.");
|
|
8040
|
+
return;
|
|
8041
|
+
}
|
|
8042
|
+
out.log("name platform app environment");
|
|
8043
|
+
for (const profile of profiles) {
|
|
8044
|
+
out.log(
|
|
8045
|
+
`${profile.name} ${profile.platform} ${profile.app ?? ""} ${profile.environment ?? ""}`
|
|
8046
|
+
);
|
|
8047
|
+
}
|
|
8048
|
+
return;
|
|
8049
|
+
}
|
|
8050
|
+
if (action2 === "save") {
|
|
8051
|
+
if (parsed.options.token !== void 0) {
|
|
8052
|
+
throw new Error(
|
|
8053
|
+
"context save does not accept --token; contexts store scope metadata only"
|
|
8054
|
+
);
|
|
8055
|
+
}
|
|
8056
|
+
const name = requireName(parsed);
|
|
8057
|
+
const context2 = await resolveOperatorContext(parsed, {
|
|
8058
|
+
allowMissingConfig: true
|
|
8059
|
+
});
|
|
8060
|
+
const profile = {
|
|
8061
|
+
platform: context2.platform.value,
|
|
8062
|
+
...context2.app.value ? { app: context2.app.value } : {},
|
|
8063
|
+
...context2.environment.value ? { environment: context2.environment.value } : {}
|
|
8064
|
+
};
|
|
8065
|
+
saveOperatorProfile(name, profile, context2.profile.file);
|
|
8066
|
+
if (parsed.options.json === true) {
|
|
8067
|
+
out.log(JSON.stringify({ schemaVersion: 1, name, ...profile }, null, 2));
|
|
8068
|
+
} else {
|
|
8069
|
+
out.log(`saved operator context "${name}" (${profile.platform})`);
|
|
8070
|
+
}
|
|
8071
|
+
return;
|
|
8072
|
+
}
|
|
8073
|
+
if (action2 === "remove") {
|
|
8074
|
+
const name = requireName(parsed);
|
|
8075
|
+
if (parsed.options.yes !== true) {
|
|
8076
|
+
throw new Error(`context remove "${name}" requires --yes`);
|
|
8077
|
+
}
|
|
8078
|
+
const removed = removeOperatorProfile(name);
|
|
8079
|
+
const result2 = {
|
|
8080
|
+
schemaVersion: 1,
|
|
8081
|
+
name,
|
|
8082
|
+
removed,
|
|
8083
|
+
credentialCachesRemoved: false,
|
|
8084
|
+
grantsRevoked: false
|
|
8085
|
+
};
|
|
8086
|
+
if (parsed.options.json === true) out.log(JSON.stringify(result2, null, 2));
|
|
8087
|
+
else {
|
|
8088
|
+
const status = removed ? `removed operator context "${name}"` : `operator context "${name}" was already absent`;
|
|
8089
|
+
out.log(`${status}; credential caches were not deleted or revoked`);
|
|
8090
|
+
}
|
|
8091
|
+
return;
|
|
8092
|
+
}
|
|
8093
|
+
if (action2 !== "show") {
|
|
8094
|
+
throw new Error(
|
|
8095
|
+
`unknown context action "${action2 ?? ""}". Try show|list|save|remove.`
|
|
8096
|
+
);
|
|
8097
|
+
}
|
|
8098
|
+
const context = await resolveOperatorContext(parsed, {
|
|
8099
|
+
allowMissingConfig: true,
|
|
8100
|
+
defaultEnvironment: "prod"
|
|
8101
|
+
});
|
|
8102
|
+
const token = developerTokenStatus(context, parsed);
|
|
8103
|
+
const result = {
|
|
8104
|
+
schemaVersion: 1,
|
|
8105
|
+
profile: context.profile,
|
|
8106
|
+
config: context.config,
|
|
8107
|
+
platform: context.platform,
|
|
8108
|
+
app: context.app,
|
|
8109
|
+
environment: context.environment,
|
|
8110
|
+
authentication: {
|
|
8111
|
+
developerToken: token,
|
|
8112
|
+
scopedTokenCacheFile: context.credentials.scopedTokenFile,
|
|
8113
|
+
handshakeStarted: false
|
|
8114
|
+
}
|
|
8115
|
+
};
|
|
8116
|
+
if (parsed.options.json === true) {
|
|
8117
|
+
out.log(JSON.stringify(result, null, 2));
|
|
8118
|
+
return;
|
|
8119
|
+
}
|
|
8120
|
+
out.log(`config: ${context.config.status} (${context.config.path})`);
|
|
8121
|
+
out.log(
|
|
8122
|
+
`context: ${context.profile.name ?? "(none)"} (${context.profile.source}; ${context.profile.file})`
|
|
8123
|
+
);
|
|
8124
|
+
out.log(
|
|
8125
|
+
`platform: ${context.platform.value} (${context.platform.source})`
|
|
8126
|
+
);
|
|
8127
|
+
out.log(
|
|
8128
|
+
`app: ${context.app.value ?? "(unresolved)"} (${context.app.source})`
|
|
8129
|
+
);
|
|
8130
|
+
out.log(
|
|
8131
|
+
`env: ${context.environment.value ?? "(unresolved)"} (${context.environment.source})`
|
|
8132
|
+
);
|
|
8133
|
+
out.log(
|
|
8134
|
+
`auth: ${token.source} (cache ${token.cacheStatus}: ${token.cacheFile})`
|
|
8135
|
+
);
|
|
8136
|
+
out.log("handshake: not started");
|
|
8137
|
+
}
|
|
8138
|
+
function requireName(parsed) {
|
|
8139
|
+
const name = parsed.positionals[2]?.trim();
|
|
8140
|
+
if (!name) {
|
|
8141
|
+
throw new Error(`context ${parsed.positionals[1]} needs a profile name`);
|
|
8142
|
+
}
|
|
8143
|
+
return name;
|
|
8144
|
+
}
|
|
8145
|
+
|
|
8146
|
+
// src/help.ts
|
|
8147
|
+
function printHelp(output = console) {
|
|
8148
|
+
output.log(`odla-ai
|
|
8149
|
+
|
|
8150
|
+
Start here:
|
|
8151
|
+
odla-ai runbook ask "<question>" The current procedure, from odla's own
|
|
8152
|
+
runbooks. Ask BEFORE searching the web or
|
|
8153
|
+
working from memory: runbooks are edited
|
|
8154
|
+
live, so your training data is out of date
|
|
8155
|
+
and this is not.
|
|
8156
|
+
odla-ai runbook impact After a change: which runbooks describe the
|
|
8157
|
+
code you just touched, so you can fix any
|
|
8158
|
+
step it made wrong.
|
|
8159
|
+
|
|
8160
|
+
Usage:
|
|
8161
|
+
odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
|
|
8162
|
+
odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
|
|
8163
|
+
odla-ai doctor [--config odla.config.mjs]
|
|
8164
|
+
odla-ai config diff [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
8165
|
+
odla-ai config plan [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
8166
|
+
odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
|
|
8167
|
+
odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
|
|
8168
|
+
odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
|
|
8169
|
+
odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
|
|
8170
|
+
odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
|
|
8171
|
+
odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
8172
|
+
odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
|
|
8173
|
+
odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
|
|
8174
|
+
odla-ai app refresh-sandbox [--include-identity] [--include-files] [--dry-run] [--json] --yes
|
|
8175
|
+
odla-ai app go-live [--include-identity] [--include-files] [--dry-run] [--json] --yes
|
|
8176
|
+
odla-ai app promote [--dry-run] [--json] --yes
|
|
8177
|
+
odla-ai app rename <name> [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
8178
|
+
odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
8179
|
+
odla-ai app owners add <email> [--email <odla-account>] [--json]
|
|
8180
|
+
odla-ai app owners remove <email> [--email <odla-account>] [--json]
|
|
8181
|
+
odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
8182
|
+
odla-ai pm task list [--app <id>] [--column <c>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
8183
|
+
odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
8184
|
+
odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
8185
|
+
odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
|
|
8186
|
+
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]
|
|
8187
|
+
odla-ai pm decision add --app <id> --title <t> --body <text> [--status <s>] [--mutation-id <id>] [--json]
|
|
8188
|
+
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]
|
|
8189
|
+
odla-ai pm <goal|task|decision|bug> get <id> [--json]
|
|
8190
|
+
odla-ai pm goal set <id> [--title <t>|--status <s>|--proof <text>|--no-proof|--target <pct>|--no-target] [--mutation-id <id>] [--json]
|
|
8191
|
+
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]
|
|
8192
|
+
odla-ai pm decision set <id> [--title <t>|--status <s>|--body <text>] [--mutation-id <id>] [--json]
|
|
8193
|
+
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]
|
|
8194
|
+
odla-ai pm <goal|task|decision> done <id> [--mutation-id <id>]
|
|
8195
|
+
odla-ai pm bug done <id> [--decision <accepted-decision-id>] [--mutation-id <id>]
|
|
8196
|
+
odla-ai pm <goal|task|decision|bug> comment <id> --body "..." [--mutation-id <id>]
|
|
8197
|
+
odla-ai pm <goal|task|decision|bug> comments <id> [--json]
|
|
8198
|
+
odla-ai pm <goal|task|decision|bug> rm <id>
|
|
8199
|
+
odla-ai pm handoff --app <id> [--json]
|
|
8200
|
+
odla-ai discuss groups [--json]
|
|
8201
|
+
odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
|
|
8202
|
+
odla-ai discuss read <topic> [--limit <n> --offset <n>] [--json]
|
|
8203
|
+
odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"] [--mutation-id <id>]
|
|
8204
|
+
odla-ai discuss reply <topic> --body "..." [--markup "..."] [--mutation-id <id>]
|
|
8205
|
+
odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
|
|
8206
|
+
odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
|
|
8207
|
+
odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
|
|
8208
|
+
odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
|
|
8209
|
+
odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
|
|
8210
|
+
odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
|
|
8211
|
+
odla-ai context list [--json]
|
|
8212
|
+
odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
|
|
8213
|
+
odla-ai context remove <name> --yes [--json]
|
|
8214
|
+
odla-ai o11y status [--app <id>] [--context <name>] [--platform https://odla.ai] [--env prod] [--minutes 60] [--json]
|
|
8215
|
+
odla-ai platform status [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
|
|
8216
|
+
odla-ai whoami [--context <name>] [--platform https://odla.ai] [--json]
|
|
8217
|
+
odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
|
|
8218
|
+
odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
|
|
8219
|
+
odla-ai runbook impact [--base origin/main] [--app <id>] [--all] [--limit <n>] [--json]
|
|
8220
|
+
odla-ai runbook lint [--app <id>] [--all] [--json]
|
|
8221
|
+
odla-ai runbook list [--app <id>] [--all] [--q <text>] [--json]
|
|
8222
|
+
odla-ai runbook comment <slug> --body "..." [--app <id>]
|
|
8223
|
+
odla-ai runbook get <slug> [--app <id>]
|
|
8224
|
+
odla-ai runbook new <slug> --title <t> --file <path|-> [--summary <s>] [--requires <specs>] [--app <id>]
|
|
8225
|
+
odla-ai runbook edit <slug> [--file <path|->|--body "..."] [--note <why>] [--requires <specs>] [--app <id>]
|
|
8226
|
+
odla-ai runbook import <dir> [--visibility operator|admin] [--dry-run] [--app <id>]
|
|
8227
|
+
odla-ai runbook publish <slug> [--app <id>]
|
|
8228
|
+
odla-ai runbook visibility <slug> <operator|admin> [--app <id>]
|
|
8229
|
+
odla-ai runbook archive <slug> [--app <id>]
|
|
8230
|
+
odla-ai runbook history <slug> [--app <id>] [--json]
|
|
8231
|
+
odla-ai runbook revert <slug> --version <n> [--app <id>]
|
|
8232
|
+
odla-ai runbook rm <slug> [--app <id>]
|
|
8233
|
+
odla-ai capabilities [--json]
|
|
8234
|
+
odla-ai code connect [--env dev|prod] [--email <odla-account>] [--engine auto|container|podman|docker] [--slots <1-64>] [--once]
|
|
8235
|
+
odla-ai admin ai show [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
|
|
8236
|
+
odla-ai admin ai models [--context <name>] [--provider <id>] [--json]
|
|
8237
|
+
odla-ai admin ai set <purpose> [--context <name>] [--provider <id>] [--model <id>] [--enabled|--no-enabled]
|
|
8238
|
+
[--max-input-bytes <n>] [--max-output-tokens <n>] [--max-calls-per-run <n>] [--json]
|
|
8239
|
+
odla-ai admin ai set security [--context <name>] --discovery-provider <id> --discovery-model <id>
|
|
8240
|
+
--validation-provider <id> --validation-model <id> [--enabled|--no-enabled] [--json]
|
|
8241
|
+
odla-ai admin ai credentials [--context <name>] [--json]
|
|
8242
|
+
odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
|
|
8243
|
+
odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
|
|
8244
|
+
odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
|
|
8245
|
+
odla-ai security github connect [--repo owner/name] [--env dev] [--email <odla-account>] [--no-open]
|
|
8246
|
+
odla-ai security github disconnect --source <id> [--env dev] [--yes]
|
|
8247
|
+
odla-ai security plan [--env dev] [--json]
|
|
8248
|
+
odla-ai security sources [--env dev] [--json]
|
|
8249
|
+
odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
|
|
8250
|
+
odla-ai security status <job-id> [--follow] [--json]
|
|
8251
|
+
odla-ai security report <job-id> [--json]
|
|
8252
|
+
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
8253
|
+
odla-ai security run [target] --self --ack-redacted-source
|
|
8254
|
+
odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
8255
|
+
odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
|
|
8256
|
+
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
8257
|
+
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
8258
|
+
odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
|
|
8259
|
+
odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
|
|
8260
|
+
odla-ai version
|
|
8261
|
+
|
|
8262
|
+
Commands:
|
|
8263
|
+
agent Inspect durable agent wakeups and explicitly requeue a
|
|
8264
|
+
dead-lettered job; JSON output is stable for remote operators.
|
|
8265
|
+
runbook odla's operational procedures, stored in the database and read at
|
|
8266
|
+
the moment they are followed. "ask" gives a written, cited answer;
|
|
8267
|
+
"search" the passages behind it; "get" the whole document.
|
|
8268
|
+
"impact" diffs your working tree against a base ref and names the
|
|
8269
|
+
runbooks that describe what you changed \u2014 run it after touching a
|
|
8270
|
+
package's exported API or JSDoc, or a Studio surface. "lint"
|
|
8271
|
+
checks the corpus the other way: every odla-ai command the
|
|
8272
|
+
runbooks name is held against this CLI's real command surface,
|
|
8273
|
+
and against the minimum versions each runbook declares. A wrong step
|
|
8274
|
+
is fixed with "edit", which takes effect immediately: a runbook is
|
|
8275
|
+
a row, not a release. Runbooks describe PROCEDURE; what an export
|
|
8276
|
+
does and what it guarantees is JSDoc, rendered per package at
|
|
8277
|
+
https://odla.ai/docs and shipped in the installed .d.ts. Answering
|
|
8278
|
+
a question usually needs both.
|
|
8279
|
+
whoami Report who this terminal is authenticated as, and whether it holds
|
|
8280
|
+
platform admin. A handshake-minted device token is never admin,
|
|
8281
|
+
however the human who approved it is configured.
|
|
8282
|
+
context Explain selected config, platform, app, environment, and
|
|
8283
|
+
developer-token provenance without printing credentials or
|
|
8284
|
+
starting a device handshake. Operator work can run outside a
|
|
8285
|
+
project checkout with explicit flags or ODLA_* environment
|
|
8286
|
+
variables.
|
|
8287
|
+
setup Install offline odla runbooks for common coding-agent harnesses.
|
|
8288
|
+
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
8289
|
+
doctor Validate and summarize the project config without network calls.
|
|
8290
|
+
config Read-only diff and revision-bound plan for checked-in Registry
|
|
8291
|
+
intent; runtime-owned fields and excluded coverage stay explicit.
|
|
8292
|
+
calendar Inspect, connect, or disconnect the live Google booking connection.
|
|
8293
|
+
app Archive (suspend, data retained), restore, export, import, or
|
|
8294
|
+
manage the co-owners of the app. Archiving takes every
|
|
8295
|
+
environment's data plane down until restored; permanent deletion
|
|
8296
|
+
has NO CLI \u2014 it requires a signed-in owner in Studio, and no
|
|
8297
|
+
machine or agent credential can purge. "app export" downloads a
|
|
8298
|
+
portable gzipped-JSONL snapshot of one database (--fresh takes a
|
|
8299
|
+
new snapshot first) \u2014 your data is always yours to take.
|
|
8300
|
+
"app import" upserts rows back in from JSON, JSONL, or a
|
|
8301
|
+
{namespace: rows} map (the shape a query returns); it writes only
|
|
8302
|
+
with --yes, so the bare command is a dry run, and it refuses to
|
|
8303
|
+
guess an id mode that would duplicate rows on a re-run.
|
|
8304
|
+
"app refresh-sandbox" replaces the sandbox with a copy of live
|
|
8305
|
+
(the routine dev loop); "app go-live" copies the sandbox into an
|
|
8306
|
+
EMPTY live database, once, at launch; "app promote" pushes only
|
|
8307
|
+
schema, rules and gates up afterwards, leaving live's rows alone.
|
|
8308
|
+
Each prints its plan and writes nothing without --yes, so the
|
|
8309
|
+
bare command is the dry run. Clerk config, triggers, allowlists,
|
|
8310
|
+
secrets and API keys never travel; identity rows stay behind
|
|
8311
|
+
unless --include-identity.
|
|
8312
|
+
"app rename <name>" changes only the display name humans read;
|
|
8313
|
+
the app id is permanent (tenants, URLs and keys embed it), so a
|
|
8314
|
+
rename never re-provisions anything or invalidates a credential.
|
|
8315
|
+
"app owners add <email>" grants a signed-up odla member the SAME
|
|
8316
|
+
full access as you; they then run their own "provision" to mint
|
|
8317
|
+
their own credentials for the shared db (the live one included)
|
|
8318
|
+
\u2014 no secret is ever copied between people.
|
|
8319
|
+
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
8320
|
+
code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
|
|
8321
|
+
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
8322
|
+
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
8323
|
+
pm Project management (via @odla-ai/pm) shared across the apps you
|
|
8324
|
+
co-own: conformance goals, kanban tasks, decisions, and bugs. With
|
|
8325
|
+
no --app, "list" spans every co-owned project (the cross-project
|
|
8326
|
+
view); with --app it scopes to one. Same device-grant auth as
|
|
8327
|
+
"app". Entities: goal (alias conformance), task (alias kanban),
|
|
8328
|
+
decision, bug. Status changes and comments post to each item's
|
|
8329
|
+
@odla-ai/chat discussion thread.
|
|
8330
|
+
discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
|
|
8331
|
+
one group per project, topics with replies, @-mentions of people,
|
|
8332
|
+
agents, PM items, and projects. Built for unattended use \u2014 post a
|
|
8333
|
+
question, then "watch" it until someone answers. "watch" exits 75
|
|
8334
|
+
(not 1) when it times out with nothing new, so a script can tell
|
|
8335
|
+
"no answer yet" from a failure and just rerun. Use "who" to look
|
|
8336
|
+
up the exact @[Label](kind/id) markup for a mention.
|
|
8337
|
+
o11y Read one stable status envelope for application RED, current
|
|
8338
|
+
live-sync load/freshness, the protected commit-to-visible
|
|
8339
|
+
canary, collector ingest/scheduler trust, Cloudflare-owned
|
|
8340
|
+
runtime metrics, and a machine verdict.
|
|
8341
|
+
--json keeps auth progress on stderr for unattended agents.
|
|
8342
|
+
platform Read canonical fleet health, releases, provider load/freshness,
|
|
8343
|
+
explicit unknowns, and next actions through a read-only grant.
|
|
8344
|
+
provision Register services, compose integrations, persist credentials, optionally push secrets.
|
|
8345
|
+
smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
|
|
8346
|
+
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
8347
|
+
copilot, gemini, or agents (repeatable or comma-separated).
|
|
8348
|
+
secrets Push configured db/o11y secrets into the Worker via wrangler
|
|
8349
|
+
stdin; set stores a tenant-vault secret and set-clerk-key the
|
|
8350
|
+
reserved Clerk secret key, write-only from stdin or an env var.
|
|
8351
|
+
version Print the CLI version.
|
|
8352
|
+
|
|
8353
|
+
Safety:
|
|
8354
|
+
Every app has two databases on odla.ai: a sandbox (env "dev", tenant
|
|
8355
|
+
<appId>--dev) and a live one (env "prod", tenant <appId>). Both are served by
|
|
8356
|
+
production odla.ai \u2014 there is no separate odla to point at. New projects get
|
|
8357
|
+
the sandbox only. Add "prod" explicitly to odla.config.mjs and pass --yes to
|
|
8358
|
+
provision the live database; use --dry-run first to inspect the resolved plan.
|
|
8359
|
+
Provision caches the approved developer token and service credentials under
|
|
8360
|
+
.odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
|
|
8361
|
+
preflights Wrangler before any shown-once issuance or destructive rotation.
|
|
8362
|
+
Projectless PM, Discussions, o11y, runbook, and identity commands use
|
|
8363
|
+
--platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
|
|
8364
|
+
ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
|
|
8365
|
+
select it explicitly with --context or ODLA_CONTEXT. Each named context gets
|
|
8366
|
+
isolated developer and scoped-token caches. Override their locations with
|
|
8367
|
+
ODLA_DEV_TOKEN_FILE and ODLA_ADMIN_TOKEN_FILE; ODLA_CONTEXT_FILE relocates
|
|
8368
|
+
the metadata file. Flags and specific ODLA_* scope variables beat a selected
|
|
8369
|
+
context, which beats project config. There is no ambient current context.
|
|
8370
|
+
"context show" reports only provenance and cache state and never authenticates.
|
|
8371
|
+
Provision opens the approval page in your browser automatically whenever the
|
|
8372
|
+
machine can show one, including agent-driven runs; only CI, SSH, and
|
|
8373
|
+
display-less hosts skip it. Use --open to force or --no-open to suppress.
|
|
8374
|
+
Browser launch is best-effort: the printed approval URL is authoritative and
|
|
8375
|
+
agents must relay it to the human verbatim. A started handshake is persisted
|
|
8376
|
+
under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
|
|
8377
|
+
the same code. Outside an interactive terminal the wait is capped (90s by
|
|
8378
|
+
default, --wait <seconds> to change); a still-pending handshake then exits
|
|
8379
|
+
with code 75: relay the URL, wait for approval, and re-run to collect.
|
|
8380
|
+
A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
|
|
8381
|
+
The email is a non-secret identity hint: never provide a password or session
|
|
8382
|
+
token. The matching account must already exist, be signed in, explicitly
|
|
8383
|
+
review the exact code, and finish any current request before claiming another.
|
|
8384
|
+
Run Code from a GitHub checkout already connected to an app in Studio; an
|
|
8385
|
+
odla.config.mjs may select the app explicitly but is not required. Code host
|
|
8386
|
+
approval and credential hashes live in odla-ai/db. The host
|
|
8387
|
+
credential is never written under .odla/; it exists only in the foreground
|
|
8388
|
+
"code connect" process and is rotated by the next approved connection.
|
|
8389
|
+
Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
|
|
8390
|
+
OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
|
|
8391
|
+
GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
|
|
8392
|
+
for a PAT or provider key. GitHub read access is separate from the explicit
|
|
8393
|
+
--ack-redacted-source consent and the exact --plan-digest printed by security
|
|
8394
|
+
plan are required before bounded snippets reach System AI.
|
|
8395
|
+
Run security plan first to inspect the admin-selected providers, models,
|
|
8396
|
+
per-route bounds, credential readiness, retention, no-execution boundary,
|
|
8397
|
+
and digest that binds consent to that exact plan.
|
|
8398
|
+
`);
|
|
7837
8399
|
}
|
|
7838
8400
|
|
|
7839
8401
|
// src/discuss-actions.ts
|
|
@@ -8257,8 +8819,8 @@ var ALLOWED = [
|
|
|
8257
8819
|
"platform",
|
|
8258
8820
|
"context"
|
|
8259
8821
|
];
|
|
8260
|
-
function requireId(id,
|
|
8261
|
-
if (!id) throw new Error(`"discuss ${
|
|
8822
|
+
function requireId(id, action2) {
|
|
8823
|
+
if (!id) throw new Error(`"discuss ${action2}" needs a topic id`);
|
|
8262
8824
|
return id;
|
|
8263
8825
|
}
|
|
8264
8826
|
async function buildContext(parsed, deps) {
|
|
@@ -8292,11 +8854,11 @@ async function buildContext(parsed, deps) {
|
|
|
8292
8854
|
}
|
|
8293
8855
|
async function discussCommand(parsed, deps = {}) {
|
|
8294
8856
|
assertArgs(parsed, ALLOWED, 3);
|
|
8295
|
-
const
|
|
8857
|
+
const action2 = parsed.positionals[1];
|
|
8296
8858
|
const id = parsed.positionals[2];
|
|
8297
|
-
if (!
|
|
8859
|
+
if (!action2) throw new Error('"discuss" needs an action. Run "odla-ai help".');
|
|
8298
8860
|
const ctx = await buildContext(parsed, deps);
|
|
8299
|
-
switch (
|
|
8861
|
+
switch (action2) {
|
|
8300
8862
|
case "groups":
|
|
8301
8863
|
return discussGroups(ctx);
|
|
8302
8864
|
case "list":
|
|
@@ -8318,7 +8880,7 @@ async function discussCommand(parsed, deps = {}) {
|
|
|
8318
8880
|
return;
|
|
8319
8881
|
}
|
|
8320
8882
|
default:
|
|
8321
|
-
throw new Error(`unknown discuss action "${
|
|
8883
|
+
throw new Error(`unknown discuss action "${action2}". Run "odla-ai help".`);
|
|
8322
8884
|
}
|
|
8323
8885
|
}
|
|
8324
8886
|
|
|
@@ -8579,18 +9141,18 @@ var ENTITY_OPTIONS = {
|
|
|
8579
9141
|
done: ["decision"]
|
|
8580
9142
|
}
|
|
8581
9143
|
};
|
|
8582
|
-
function canonicalAction(
|
|
8583
|
-
if (
|
|
8584
|
-
if (
|
|
8585
|
-
if (
|
|
8586
|
-
return
|
|
8587
|
-
}
|
|
8588
|
-
function allowedOptions(entity,
|
|
8589
|
-
const entityOptions =
|
|
8590
|
-
return [...COMMON_OPTIONS, ...ACTION_OPTIONS[
|
|
8591
|
-
}
|
|
8592
|
-
function requireId2(id,
|
|
8593
|
-
if (!id) throw new Error(`"pm ... ${
|
|
9144
|
+
function canonicalAction(action2) {
|
|
9145
|
+
if (action2 === "create") return "add";
|
|
9146
|
+
if (action2 === "update" || action2 === "status" || action2 === "move") return "set";
|
|
9147
|
+
if (action2 === "delete") return "rm";
|
|
9148
|
+
return action2 in ACTION_OPTIONS ? action2 : null;
|
|
9149
|
+
}
|
|
9150
|
+
function allowedOptions(entity, action2) {
|
|
9151
|
+
const entityOptions = action2 === "list" || action2 === "add" || action2 === "set" || action2 === "done" ? ENTITY_OPTIONS[entity][action2] : [];
|
|
9152
|
+
return [...COMMON_OPTIONS, ...ACTION_OPTIONS[action2], ...entityOptions];
|
|
9153
|
+
}
|
|
9154
|
+
function requireId2(id, action2) {
|
|
9155
|
+
if (!id) throw new Error(`"pm ... ${action2}" needs an item id`);
|
|
8594
9156
|
return id;
|
|
8595
9157
|
}
|
|
8596
9158
|
async function buildContext2(parsed, deps) {
|
|
@@ -8626,28 +9188,28 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
8626
9188
|
const entity = ALIASES[word];
|
|
8627
9189
|
if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
|
|
8628
9190
|
const requestedAction = parsed.positionals[2] ?? "list";
|
|
8629
|
-
const
|
|
8630
|
-
if (!
|
|
8631
|
-
assertArgs(parsed, allowedOptions(entity,
|
|
9191
|
+
const action2 = canonicalAction(requestedAction);
|
|
9192
|
+
if (!action2) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|comment|comments|rm.`);
|
|
9193
|
+
assertArgs(parsed, allowedOptions(entity, action2), 4);
|
|
8632
9194
|
const ctx = await buildContext2(parsed, deps);
|
|
8633
9195
|
const id = parsed.positionals[3];
|
|
8634
|
-
switch (
|
|
9196
|
+
switch (action2) {
|
|
8635
9197
|
case "list":
|
|
8636
9198
|
return pmList(ctx, entity, parsed);
|
|
8637
9199
|
case "add":
|
|
8638
9200
|
return pmAdd(ctx, entity, parsed);
|
|
8639
9201
|
case "get":
|
|
8640
|
-
return pmGet(ctx, entity, requireId2(id,
|
|
9202
|
+
return pmGet(ctx, entity, requireId2(id, action2));
|
|
8641
9203
|
case "set":
|
|
8642
|
-
return pmSet(ctx, entity, requireId2(id,
|
|
9204
|
+
return pmSet(ctx, entity, requireId2(id, action2), parsed);
|
|
8643
9205
|
case "done":
|
|
8644
|
-
return pmDone(ctx, entity, requireId2(id,
|
|
9206
|
+
return pmDone(ctx, entity, requireId2(id, action2), parsed);
|
|
8645
9207
|
case "comment":
|
|
8646
|
-
return pmComment(ctx, entity, requireId2(id,
|
|
9208
|
+
return pmComment(ctx, entity, requireId2(id, action2), parsed);
|
|
8647
9209
|
case "comments":
|
|
8648
|
-
return pmComments(ctx, entity, requireId2(id,
|
|
9210
|
+
return pmComments(ctx, entity, requireId2(id, action2));
|
|
8649
9211
|
case "rm":
|
|
8650
|
-
return pmRemove(ctx, entity, requireId2(id,
|
|
9212
|
+
return pmRemove(ctx, entity, requireId2(id, action2));
|
|
8651
9213
|
}
|
|
8652
9214
|
}
|
|
8653
9215
|
|
|
@@ -8680,8 +9242,8 @@ function printPlatformStatus(status, out) {
|
|
|
8680
9242
|
if (status.verdict.reasons.length) {
|
|
8681
9243
|
out.log(`reasons ${status.verdict.reasons.join(", ")}`);
|
|
8682
9244
|
}
|
|
8683
|
-
for (const
|
|
8684
|
-
out.log(`next ${
|
|
9245
|
+
for (const action2 of status.nextActions) {
|
|
9246
|
+
out.log(`next ${action2.service} ${action2.code} ${action2.message}`);
|
|
8685
9247
|
}
|
|
8686
9248
|
}
|
|
8687
9249
|
function value(input) {
|
|
@@ -8701,10 +9263,10 @@ async function platformCommand(parsed, deps = {}) {
|
|
|
8701
9263
|
["config", "context", "platform", "token", "email", "open", "json"],
|
|
8702
9264
|
2
|
|
8703
9265
|
);
|
|
8704
|
-
const
|
|
8705
|
-
if (
|
|
9266
|
+
const action2 = parsed.positionals[1];
|
|
9267
|
+
if (action2 !== "status") {
|
|
8706
9268
|
throw new Error(
|
|
8707
|
-
`unknown platform action "${
|
|
9269
|
+
`unknown platform action "${action2 ?? ""}". Try "odla-ai platform status --json".`
|
|
8708
9270
|
);
|
|
8709
9271
|
}
|
|
8710
9272
|
const context = await resolveOperatorContext(parsed, {
|
|
@@ -9033,10 +9595,10 @@ async function o11yCommand(parsed, deps = {}) {
|
|
|
9033
9595
|
],
|
|
9034
9596
|
2
|
|
9035
9597
|
);
|
|
9036
|
-
const
|
|
9037
|
-
if (
|
|
9598
|
+
const action2 = parsed.positionals[1];
|
|
9599
|
+
if (action2 !== "status") {
|
|
9038
9600
|
throw new Error(
|
|
9039
|
-
`unknown o11y action "${
|
|
9601
|
+
`unknown o11y action "${action2 ?? ""}". Try "odla-ai o11y status --json".`
|
|
9040
9602
|
);
|
|
9041
9603
|
}
|
|
9042
9604
|
const minutes = statusMinutes(
|
|
@@ -9165,7 +9727,7 @@ async function read2(url, headers, doFetch) {
|
|
|
9165
9727
|
}
|
|
9166
9728
|
|
|
9167
9729
|
// src/provision.ts
|
|
9168
|
-
var
|
|
9730
|
+
var import_apps10 = require("@odla-ai/apps");
|
|
9169
9731
|
var import_ai3 = require("@odla-ai/ai");
|
|
9170
9732
|
var import_node_process10 = __toESM(require("process"), 1);
|
|
9171
9733
|
|
|
@@ -9175,7 +9737,7 @@ async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, int
|
|
|
9175
9737
|
const base = `${endpoint}/app/${encodeURIComponent(tenantId)}`;
|
|
9176
9738
|
for (const integration of integrations) {
|
|
9177
9739
|
for (const seed of integration.seeds ?? []) {
|
|
9178
|
-
const payload = await
|
|
9740
|
+
const payload = await postJson3(doFetch, `${base}/query`, dbKey, {
|
|
9179
9741
|
query: { [seed.ns]: { $: { where: { [seed.key.attr]: seed.key.value }, limit: 1 } } }
|
|
9180
9742
|
});
|
|
9181
9743
|
const rows = isRecord7(payload) && isRecord7(payload.result) ? payload.result[seed.ns] : void 0;
|
|
@@ -9186,7 +9748,7 @@ async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, int
|
|
|
9186
9748
|
out.log(`${env}: integration ${integration.id} seed ${seed.id} already exists`);
|
|
9187
9749
|
continue;
|
|
9188
9750
|
}
|
|
9189
|
-
await
|
|
9751
|
+
await postJson3(doFetch, `${base}/transact`, dbKey, {
|
|
9190
9752
|
mutationId: `integration:${integration.id}:seed:${seed.id}`,
|
|
9191
9753
|
ops: [{
|
|
9192
9754
|
t: "update",
|
|
@@ -9201,7 +9763,7 @@ async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, int
|
|
|
9201
9763
|
}
|
|
9202
9764
|
}
|
|
9203
9765
|
}
|
|
9204
|
-
async function
|
|
9766
|
+
async function postJson3(doFetch, url, bearer, body) {
|
|
9205
9767
|
const res = await doFetch(url, {
|
|
9206
9768
|
method: "POST",
|
|
9207
9769
|
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
@@ -9222,9 +9784,9 @@ async function responseText(res) {
|
|
|
9222
9784
|
}
|
|
9223
9785
|
|
|
9224
9786
|
// src/provision-credentials.ts
|
|
9225
|
-
var
|
|
9787
|
+
var import_apps9 = require("@odla-ai/apps");
|
|
9226
9788
|
async function provisionEnvCredentials(opts) {
|
|
9227
|
-
const tenantId = (0,
|
|
9789
|
+
const tenantId = (0, import_apps9.tenantIdFor)(opts.cfg.app.id, opts.env);
|
|
9228
9790
|
const prior = opts.credentials?.envs[opts.env];
|
|
9229
9791
|
let credentials = opts.credentials;
|
|
9230
9792
|
let dbKey = opts.cfg.services.includes("db") && !opts.rotateDb ? prior?.dbKey : void 0;
|
|
@@ -9280,14 +9842,14 @@ async function mintDbKey(opts, tenantId) {
|
|
|
9280
9842
|
appId: tenantId
|
|
9281
9843
|
})
|
|
9282
9844
|
});
|
|
9283
|
-
if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await
|
|
9845
|
+
if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText5(created)}`);
|
|
9284
9846
|
res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/keys`, {
|
|
9285
9847
|
method: "POST",
|
|
9286
9848
|
headers,
|
|
9287
9849
|
body: "{}"
|
|
9288
9850
|
});
|
|
9289
9851
|
}
|
|
9290
|
-
if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await
|
|
9852
|
+
if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText5(res)}`);
|
|
9291
9853
|
const body = await res.json();
|
|
9292
9854
|
if (!body.key) throw new Error(`db key mint (${tenantId}) returned no key`);
|
|
9293
9855
|
return body.key;
|
|
@@ -9303,58 +9865,11 @@ async function issueO11yToken(opts) {
|
|
|
9303
9865
|
`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`
|
|
9304
9866
|
);
|
|
9305
9867
|
}
|
|
9306
|
-
if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await
|
|
9868
|
+
if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText5(res)}`);
|
|
9307
9869
|
const body = await res.json();
|
|
9308
9870
|
if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
|
|
9309
9871
|
return body.token;
|
|
9310
9872
|
}
|
|
9311
|
-
async function safeText4(res) {
|
|
9312
|
-
try {
|
|
9313
|
-
return redactSecrets((await res.text()).slice(0, 500));
|
|
9314
|
-
} catch {
|
|
9315
|
-
return "";
|
|
9316
|
-
}
|
|
9317
|
-
}
|
|
9318
|
-
|
|
9319
|
-
// src/provision-helpers.ts
|
|
9320
|
-
var import_ai2 = require("@odla-ai/ai");
|
|
9321
|
-
var import_apps6 = require("@odla-ai/apps");
|
|
9322
|
-
function defaultSecretName(provider) {
|
|
9323
|
-
const names = import_ai2.DEFAULT_SECRET_NAMES;
|
|
9324
|
-
return names[provider] ?? `${provider}_api_key`;
|
|
9325
|
-
}
|
|
9326
|
-
async function assertTenantAdminAccess(doFetch, cfg, env, token) {
|
|
9327
|
-
const tenantId = (0, import_apps6.tenantIdFor)(cfg.app.id, env);
|
|
9328
|
-
const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/entitlements`, {
|
|
9329
|
-
headers: { authorization: `Bearer ${token}` }
|
|
9330
|
-
});
|
|
9331
|
-
if (res.ok || res.status === 404) return;
|
|
9332
|
-
if (res.status === 403) {
|
|
9333
|
-
throw new Error(
|
|
9334
|
-
`${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`
|
|
9335
|
-
);
|
|
9336
|
-
}
|
|
9337
|
-
throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText5(res)}`);
|
|
9338
|
-
}
|
|
9339
|
-
async function postJson3(doFetch, url, bearer, body) {
|
|
9340
|
-
const res = await doFetch(url, {
|
|
9341
|
-
method: "POST",
|
|
9342
|
-
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
9343
|
-
body: JSON.stringify(body)
|
|
9344
|
-
});
|
|
9345
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText5(res)}`);
|
|
9346
|
-
}
|
|
9347
|
-
function normalizeClerkConfig(value2) {
|
|
9348
|
-
if (!value2) return null;
|
|
9349
|
-
if (typeof value2 === "string") {
|
|
9350
|
-
const publishableKey2 = envValue(value2);
|
|
9351
|
-
return publishableKey2 ? { publishableKey: publishableKey2 } : null;
|
|
9352
|
-
}
|
|
9353
|
-
if (typeof value2 !== "object") return null;
|
|
9354
|
-
const cfg = value2;
|
|
9355
|
-
const publishableKey = envValue(cfg.publishableKey);
|
|
9356
|
-
return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
|
|
9357
|
-
}
|
|
9358
9873
|
async function safeText5(res) {
|
|
9359
9874
|
try {
|
|
9360
9875
|
return redactSecrets((await res.text()).slice(0, 500));
|
|
@@ -9433,7 +9948,7 @@ async function provision(options) {
|
|
|
9433
9948
|
}
|
|
9434
9949
|
const doFetch = options.fetch ?? fetch;
|
|
9435
9950
|
const token = await getDeveloperToken(cfg, options, doFetch, out);
|
|
9436
|
-
const apps = (0,
|
|
9951
|
+
const apps = (0, import_apps10.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
|
|
9437
9952
|
const existing = await apps.resolveApp(cfg.app.id);
|
|
9438
9953
|
if (existing) {
|
|
9439
9954
|
out.log(`app: ${cfg.app.id} already exists`);
|
|
@@ -9444,7 +9959,7 @@ async function provision(options) {
|
|
|
9444
9959
|
for (const env of cfg.envs) {
|
|
9445
9960
|
await assertTenantAdminAccess(doFetch, cfg, env, token);
|
|
9446
9961
|
}
|
|
9447
|
-
const serviceOrder = (0,
|
|
9962
|
+
const serviceOrder = (0, import_apps10.orderAppServices)(cfg.services);
|
|
9448
9963
|
for (const env of cfg.envs) {
|
|
9449
9964
|
for (const service of serviceOrder) {
|
|
9450
9965
|
if (service === "ai") {
|
|
@@ -9477,7 +9992,7 @@ async function provision(options) {
|
|
|
9477
9992
|
}
|
|
9478
9993
|
}
|
|
9479
9994
|
for (const env of cfg.envs) {
|
|
9480
|
-
const tenantId = (0,
|
|
9995
|
+
const tenantId = (0, import_apps10.tenantIdFor)(cfg.app.id, env);
|
|
9481
9996
|
credentials = await provisionEnvCredentials({
|
|
9482
9997
|
cfg,
|
|
9483
9998
|
env,
|
|
@@ -9510,11 +10025,11 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
9510
10025
|
}
|
|
9511
10026
|
}
|
|
9512
10027
|
if (schema && dbKey) {
|
|
9513
|
-
await
|
|
10028
|
+
await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
|
|
9514
10029
|
out.log(`${env}: schema pushed`);
|
|
9515
10030
|
}
|
|
9516
10031
|
if (rules) {
|
|
9517
|
-
await
|
|
10032
|
+
await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/admin/rules`, token, rules);
|
|
9518
10033
|
out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
|
|
9519
10034
|
}
|
|
9520
10035
|
if (dbKey) {
|
|
@@ -9611,6 +10126,7 @@ var COMMAND_SURFACE = {
|
|
|
9611
10126
|
calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
|
|
9612
10127
|
capabilities: {},
|
|
9613
10128
|
code: { connect: {} },
|
|
10129
|
+
config: { diff: {}, plan: {} },
|
|
9614
10130
|
context: { show: {}, list: {}, save: {}, remove: {} },
|
|
9615
10131
|
// `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
|
|
9616
10132
|
discuss: {
|
|
@@ -9904,7 +10420,7 @@ async function runbookRemove(ctx, slug) {
|
|
|
9904
10420
|
|
|
9905
10421
|
// src/runbook-import.ts
|
|
9906
10422
|
var import_node_fs17 = require("fs");
|
|
9907
|
-
var
|
|
10423
|
+
var import_node_path14 = require("path");
|
|
9908
10424
|
function parseRunbook(text, slug) {
|
|
9909
10425
|
let rest = text;
|
|
9910
10426
|
const meta = {};
|
|
@@ -9933,8 +10449,8 @@ function readRunbookDir(dir) {
|
|
|
9933
10449
|
const files = (0, import_node_fs17.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
|
|
9934
10450
|
if (!files.length) throw new Error(`no .md files in ${dir}`);
|
|
9935
10451
|
return files.map((file) => {
|
|
9936
|
-
const slug = (0,
|
|
9937
|
-
const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0,
|
|
10452
|
+
const slug = (0, import_node_path14.basename)(file, ".md");
|
|
10453
|
+
const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0, import_node_path14.join)(dir, file), "utf8"), slug);
|
|
9938
10454
|
return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
|
|
9939
10455
|
});
|
|
9940
10456
|
}
|
|
@@ -10007,7 +10523,7 @@ async function upsert(ctx, r, visibility) {
|
|
|
10007
10523
|
// src/runbook-impact.ts
|
|
10008
10524
|
var import_node_child_process7 = require("child_process");
|
|
10009
10525
|
var import_node_fs18 = require("fs");
|
|
10010
|
-
var
|
|
10526
|
+
var import_node_path15 = require("path");
|
|
10011
10527
|
|
|
10012
10528
|
// src/runbook-impact-scan.ts
|
|
10013
10529
|
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$]*)/;
|
|
@@ -10176,7 +10692,7 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
|
|
|
10176
10692
|
}
|
|
10177
10693
|
function manifestLabeller(root) {
|
|
10178
10694
|
return (workspace) => {
|
|
10179
|
-
const manifest = (0,
|
|
10695
|
+
const manifest = (0, import_node_path15.join)(root, workspace, "package.json");
|
|
10180
10696
|
if (!(0, import_node_fs18.existsSync)(manifest)) return void 0;
|
|
10181
10697
|
try {
|
|
10182
10698
|
const name = JSON.parse((0, import_node_fs18.readFileSync)(manifest, "utf8")).name;
|
|
@@ -10246,7 +10762,7 @@ function report3(ctx, impacts) {
|
|
|
10246
10762
|
async function runbookImpact(ctx, options, deps = {}) {
|
|
10247
10763
|
const cwd = deps.cwd ?? process.cwd();
|
|
10248
10764
|
const runGit = deps.runGit ?? gitRunner(cwd);
|
|
10249
|
-
const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0,
|
|
10765
|
+
const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0, import_node_path15.join)(cwd, path), "utf8"));
|
|
10250
10766
|
const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
|
|
10251
10767
|
if (!surfaces.length) {
|
|
10252
10768
|
return ctx.out.log(
|
|
@@ -10381,7 +10897,7 @@ async function runbookComment(ctx, slug, body) {
|
|
|
10381
10897
|
var import_node_child_process8 = require("child_process");
|
|
10382
10898
|
var import_node_fs19 = require("fs");
|
|
10383
10899
|
var import_node_os5 = require("os");
|
|
10384
|
-
var
|
|
10900
|
+
var import_node_path16 = require("path");
|
|
10385
10901
|
var import_node_process12 = __toESM(require("process"), 1);
|
|
10386
10902
|
var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
|
|
10387
10903
|
function resolveEditor(env = import_node_process12.default.env) {
|
|
@@ -10407,8 +10923,8 @@ function editText(initial, slug, deps = {}) {
|
|
|
10407
10923
|
);
|
|
10408
10924
|
if (!interactive())
|
|
10409
10925
|
throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
|
|
10410
|
-
const dir = (0, import_node_fs19.mkdtempSync)((0,
|
|
10411
|
-
const file = (0,
|
|
10926
|
+
const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path16.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
|
|
10927
|
+
const file = (0, import_node_path16.join)(dir, `${slug}.md`);
|
|
10412
10928
|
try {
|
|
10413
10929
|
(0, import_node_fs19.writeFileSync)(file, initial, { mode: 384 });
|
|
10414
10930
|
const code = defaultRunOrInjected(deps)(editor, file);
|
|
@@ -10516,12 +11032,12 @@ var ALLOWED2 = [
|
|
|
10516
11032
|
"platform",
|
|
10517
11033
|
"context"
|
|
10518
11034
|
];
|
|
10519
|
-
function requireSlug(slug,
|
|
10520
|
-
if (!slug) throw new Error(`"runbook ${
|
|
11035
|
+
function requireSlug(slug, action2) {
|
|
11036
|
+
if (!slug) throw new Error(`"runbook ${action2}" needs a slug, e.g. "odla-ai runbook ${action2} release"`);
|
|
10521
11037
|
return slug;
|
|
10522
11038
|
}
|
|
10523
11039
|
var WRITES = /* @__PURE__ */ new Set(["new", "edit", "publish", "archive", "visibility", "revert", "rm", "import"]);
|
|
10524
|
-
async function buildContext3(parsed, deps,
|
|
11040
|
+
async function buildContext3(parsed, deps, action2) {
|
|
10525
11041
|
const appIdOption = stringOpt(parsed.options.app);
|
|
10526
11042
|
const context = await resolveOperatorContext(parsed, {
|
|
10527
11043
|
allowMissingConfig: true
|
|
@@ -10531,7 +11047,7 @@ async function buildContext3(parsed, deps, action) {
|
|
|
10531
11047
|
const out = deps.stdout ?? console;
|
|
10532
11048
|
const appId = appIdOption ?? (context.app.source === "environment" || context.app.source === "profile" ? context.app.value : null) ?? PLATFORM_SCOPE;
|
|
10533
11049
|
const dryRun = parsed.options["dry-run"] === true;
|
|
10534
|
-
if (
|
|
11050
|
+
if (action2 === "import" && dryRun) {
|
|
10535
11051
|
return {
|
|
10536
11052
|
platformUrl: cfg.platformUrl,
|
|
10537
11053
|
token: "",
|
|
@@ -10541,12 +11057,12 @@ async function buildContext3(parsed, deps, action) {
|
|
|
10541
11057
|
appId
|
|
10542
11058
|
};
|
|
10543
11059
|
}
|
|
10544
|
-
const needsCapability = WRITES.has(
|
|
11060
|
+
const needsCapability = WRITES.has(action2) && !dryRun && appId === PLATFORM_SCOPE && !stringOpt(parsed.options.token);
|
|
10545
11061
|
const token = needsCapability ? await getScopedPlatformToken({
|
|
10546
11062
|
platform: cfg.platformUrl,
|
|
10547
11063
|
scope: "platform:runbook:write",
|
|
10548
11064
|
email: stringOpt(parsed.options.email),
|
|
10549
|
-
label: `odla CLI (runbook ${
|
|
11065
|
+
label: `odla CLI (runbook ${action2})`,
|
|
10550
11066
|
fetch: doFetch,
|
|
10551
11067
|
stdout: out,
|
|
10552
11068
|
openApprovalUrl: deps.openUrl,
|
|
@@ -10581,11 +11097,11 @@ async function buildContext3(parsed, deps, action) {
|
|
|
10581
11097
|
};
|
|
10582
11098
|
}
|
|
10583
11099
|
async function runbookCommand(parsed, deps = {}) {
|
|
10584
|
-
const
|
|
10585
|
-
assertArgs(parsed, ALLOWED2,
|
|
10586
|
-
const ctx = await buildContext3(parsed, deps,
|
|
11100
|
+
const action2 = parsed.positionals[1] ?? "list";
|
|
11101
|
+
assertArgs(parsed, ALLOWED2, action2 === "ask" || action2 === "search" ? 64 : 4);
|
|
11102
|
+
const ctx = await buildContext3(parsed, deps, action2);
|
|
10587
11103
|
const slug = parsed.positionals[2];
|
|
10588
|
-
switch (
|
|
11104
|
+
switch (action2) {
|
|
10589
11105
|
case "list":
|
|
10590
11106
|
return runbookList(ctx, parsed.options.all === true, stringOpt(parsed.options.q));
|
|
10591
11107
|
case "ask": {
|
|
@@ -10670,7 +11186,7 @@ async function runbookCommand(parsed, deps = {}) {
|
|
|
10670
11186
|
case "rm":
|
|
10671
11187
|
return runbookRemove(ctx, requireSlug(slug, "rm"));
|
|
10672
11188
|
default:
|
|
10673
|
-
throw new Error(`unknown runbook action "${
|
|
11189
|
+
throw new Error(`unknown runbook action "${action2}". Try ${acceptedAfter(["runbook"]).join(", ")}.`);
|
|
10674
11190
|
}
|
|
10675
11191
|
}
|
|
10676
11192
|
|
|
@@ -10828,7 +11344,7 @@ function hostedSeverity(value2, flag) {
|
|
|
10828
11344
|
var import_security2 = require("@odla-ai/security");
|
|
10829
11345
|
|
|
10830
11346
|
// src/security.ts
|
|
10831
|
-
var
|
|
11347
|
+
var import_node_path17 = require("path");
|
|
10832
11348
|
var import_security = require("@odla-ai/security");
|
|
10833
11349
|
var import_node3 = require("@odla-ai/security/node");
|
|
10834
11350
|
async function runHostedSecurity(options) {
|
|
@@ -10840,9 +11356,9 @@ async function runHostedSecurity(options) {
|
|
|
10840
11356
|
const appId = selfAudit ? "odla-ai" : cfg.app.id;
|
|
10841
11357
|
const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
|
|
10842
11358
|
const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
|
|
10843
|
-
const target = (0,
|
|
10844
|
-
const output = (0,
|
|
10845
|
-
const outputRelative = (0,
|
|
11359
|
+
const target = (0, import_node_path17.resolve)(options.target ?? cfg?.rootDir ?? ".");
|
|
11360
|
+
const output = (0, import_node_path17.resolve)(options.out ?? (0, import_node_path17.resolve)(target, ".odla/security/hosted"));
|
|
11361
|
+
const outputRelative = (0, import_node_path17.relative)(target, output).split(import_node_path17.sep).join("/");
|
|
10846
11362
|
if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
|
|
10847
11363
|
const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
|
|
10848
11364
|
const tokenRequest = {
|
|
@@ -10854,7 +11370,7 @@ async function runHostedSecurity(options) {
|
|
|
10854
11370
|
};
|
|
10855
11371
|
const token = await injectedToken(options, tokenRequest);
|
|
10856
11372
|
const snapshot = await (0, import_node3.snapshotDirectory)(target, {
|
|
10857
|
-
exclude: !outputRelative.startsWith("../") && !(0,
|
|
11373
|
+
exclude: !outputRelative.startsWith("../") && !(0, import_node_path17.isAbsolute)(outputRelative) ? [outputRelative] : []
|
|
10858
11374
|
});
|
|
10859
11375
|
const hosted = await (0, import_security.createPlatformSecurityReasoners)({
|
|
10860
11376
|
platform,
|
|
@@ -10872,7 +11388,7 @@ async function runHostedSecurity(options) {
|
|
|
10872
11388
|
});
|
|
10873
11389
|
const harness = (0, import_security.createSecurityHarness)({
|
|
10874
11390
|
profile,
|
|
10875
|
-
store: new import_node3.FileRunStore((0,
|
|
11391
|
+
store: new import_node3.FileRunStore((0, import_node_path17.resolve)(output, "state")),
|
|
10876
11392
|
discoveryReasoner: hosted.discoveryReasoner,
|
|
10877
11393
|
validationReasoner: hosted.validationReasoner,
|
|
10878
11394
|
policy: {
|
|
@@ -10896,7 +11412,7 @@ async function runHostedSecurity(options) {
|
|
|
10896
11412
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
10897
11413
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
10898
11414
|
if (!env || !declared.includes(env)) {
|
|
10899
|
-
const shown = (0,
|
|
11415
|
+
const shown = (0, import_node_path17.relative)(rootDir, configPath) || configPath;
|
|
10900
11416
|
throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
|
|
10901
11417
|
}
|
|
10902
11418
|
return env;
|
|
@@ -10925,7 +11441,7 @@ function printSummary(out, appId, env, run, report4, output) {
|
|
|
10925
11441
|
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}`);
|
|
10926
11442
|
if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
|
|
10927
11443
|
out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
|
|
10928
|
-
out.log(` report: ${(0,
|
|
11444
|
+
out.log(` report: ${(0, import_node_path17.resolve)(output, "REPORT.md")}`);
|
|
10929
11445
|
}
|
|
10930
11446
|
function formatBudget(usage) {
|
|
10931
11447
|
return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
|
|
@@ -11302,8 +11818,8 @@ async function securityCommand(parsed, dependencies) {
|
|
|
11302
11818
|
else await runLocalSecurityCommand(parsed, dependencies);
|
|
11303
11819
|
}
|
|
11304
11820
|
async function githubSecurityCommand(parsed, dependencies) {
|
|
11305
|
-
const
|
|
11306
|
-
if (
|
|
11821
|
+
const action2 = parsed.positionals[2];
|
|
11822
|
+
if (action2 === "disconnect") {
|
|
11307
11823
|
assertArgs(parsed, ["config", "env", "platform", "source", "email", "open", "yes"], 3);
|
|
11308
11824
|
const context2 = await hostedSecurityContext(parsed, dependencies);
|
|
11309
11825
|
const sourceId = requiredString(parsed.options.source, "--source");
|
|
@@ -11318,7 +11834,7 @@ async function githubSecurityCommand(parsed, dependencies) {
|
|
|
11318
11834
|
context2.stdout.log(`github: disconnected ${sourceId} from ${context2.appId}/${context2.env}`);
|
|
11319
11835
|
return;
|
|
11320
11836
|
}
|
|
11321
|
-
if (
|
|
11837
|
+
if (action2 !== "connect") {
|
|
11322
11838
|
throw new Error('unknown security github command. Try "odla-ai security github connect".');
|
|
11323
11839
|
}
|
|
11324
11840
|
assertArgs(parsed, ["config", "env", "platform", "repo", "email", "open"], 3);
|
|
@@ -11538,8 +12054,11 @@ async function calendarCommand(parsed, dependencies) {
|
|
|
11538
12054
|
calendarServiceConfig,
|
|
11539
12055
|
calendarStatus,
|
|
11540
12056
|
codeConnect,
|
|
12057
|
+
configDiff,
|
|
12058
|
+
configPlan,
|
|
11541
12059
|
connectGitHubSecuritySource,
|
|
11542
12060
|
describeProblem,
|
|
12061
|
+
desiredRegistryState,
|
|
11543
12062
|
disconnectGitHubSecuritySource,
|
|
11544
12063
|
doctor,
|
|
11545
12064
|
exitCodeFor,
|
|
@@ -11559,6 +12078,7 @@ async function calendarCommand(parsed, dependencies) {
|
|
|
11559
12078
|
prepareCodeImages,
|
|
11560
12079
|
printCapabilities,
|
|
11561
12080
|
provision,
|
|
12081
|
+
reconcileConfig,
|
|
11562
12082
|
redactSecrets,
|
|
11563
12083
|
repositoryFromGitRemote,
|
|
11564
12084
|
runCli,
|