@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.
@@ -165,7 +165,9 @@ function approvalLines(prompt) {
165
165
  lines.push("");
166
166
  lines.push(` ${prompt.approvalUrl}`);
167
167
  lines.push("");
168
- lines.push(" A signed-in odla admin opens that URL, checks the code matches, and approves.");
168
+ lines.push(
169
+ ` ${prompt.approver ?? "The matching signed-in odla account"} opens that URL, checks the code matches, and approves.`
170
+ );
169
171
  lines.push(" Opening the link without approving does nothing; this command waits until they do.");
170
172
  if (prompt.browserAttempted) {
171
173
  lines.push(" A browser launch was attempted, but it is best-effort and may have shown no tab.");
@@ -437,6 +439,7 @@ function audienceBoundEnvToken(token, platform) {
437
439
  }
438
440
  var SCOPE_PURPOSE = {
439
441
  "platform:status:read": "read the platform fleet health and deployment snapshot",
442
+ "app:config:read": "compare checked-in intent with an exact-id app Registry configuration",
440
443
  "platform:runbook:write": "add or edit odla's operational runbooks",
441
444
  "platform:ai:policy:write": "change System AI model routing",
442
445
  "platform:ai:policy:read": "read System AI model routing",
@@ -473,6 +476,7 @@ async function scopedToken(platform, scope, options, doFetch, out) {
473
476
  approvalUrl,
474
477
  minutesLeft: Math.floor(expiresIn / 60),
475
478
  purpose: SCOPE_PURPOSE[scope] ?? `use ${scope}`,
479
+ approver: scope.startsWith("app:") ? "A signed-in app owner" : "A signed-in odla platform admin",
476
480
  browserAttempted: browser.open,
477
481
  browserSkipped: browser.reason
478
482
  });
@@ -668,10 +672,10 @@ async function responseBody2(res) {
668
672
  return { message: text.slice(0, 300) };
669
673
  }
670
674
  }
671
- function apiError2(action, status, body) {
675
+ function apiError2(action2, status, body) {
672
676
  const error = isRecord2(body) && isRecord2(body.error) ? body.error : void 0;
673
677
  const message2 = error && typeof error.message === "string" ? error.message : isRecord2(body) && typeof body.message === "string" ? body.message : "request failed";
674
- return `${action} failed (${status}): ${message2}`;
678
+ return `${action2} failed (${status}): ${message2}`;
675
679
  }
676
680
  function isRecord2(value2) {
677
681
  return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
@@ -849,10 +853,10 @@ async function responseBody3(res) {
849
853
  return { message: text.slice(0, 300) };
850
854
  }
851
855
  }
852
- function apiError3(action, status, body) {
856
+ function apiError3(action2, status, body) {
853
857
  const error = isRecord3(body) && isRecord3(body.error) ? body.error : void 0;
854
858
  const message2 = error && typeof error.message === "string" ? error.message : isRecord3(body) && typeof body.message === "string" ? body.message : "request failed";
855
- return `${action} failed (${status}): ${message2}`;
859
+ return `${action2} failed (${status}): ${message2}`;
856
860
  }
857
861
  function isRecord3(value2) {
858
862
  return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
@@ -1603,8 +1607,8 @@ function pollTimeout(value2 = 10 * 6e4) {
1603
1607
  if (!Number.isSafeInteger(value2) || value2 < 1e3 || value2 > 60 * 6e4) throw new Error("calendar poll timeout must be 1000-3600000ms");
1604
1608
  return value2;
1605
1609
  }
1606
- function productionConsent(env, yes, action) {
1607
- if ((env === "prod" || env === "production") && !yes) throw new Error(`refusing to ${action} for "${env}" without --yes`);
1610
+ function productionConsent(env, yes, action2) {
1611
+ if ((env === "prod" || env === "production") && !yes) throw new Error(`refusing to ${action2} for "${env}" without --yes`);
1608
1612
  }
1609
1613
  function printStatus(status, json, out) {
1610
1614
  if (json) {
@@ -1634,6 +1638,7 @@ var CAPABILITIES = {
1634
1638
  "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",
1635
1639
  "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",
1636
1640
  "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",
1641
+ "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",
1637
1642
  "inspect durable agent wakeups as a versioned JSON envelope and explicitly requeue one dead-lettered job with a scoped environment credential",
1638
1643
  "run app-attributed hosted security discovery and independent validation without provider keys",
1639
1644
  "connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
@@ -1680,15 +1685,540 @@ function printGroup(out, heading, items) {
1680
1685
  out.log("");
1681
1686
  }
1682
1687
 
1688
+ // src/config-reconcile-desired.ts
1689
+ import { orderAppServices } from "@odla-ai/apps";
1690
+
1691
+ // src/provision-helpers.ts
1692
+ import { DEFAULT_SECRET_NAMES } from "@odla-ai/ai";
1693
+ import { tenantIdFor } from "@odla-ai/apps";
1694
+ function defaultSecretName(provider) {
1695
+ const names = DEFAULT_SECRET_NAMES;
1696
+ return names[provider] ?? `${provider}_api_key`;
1697
+ }
1698
+ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
1699
+ const tenantId = tenantIdFor(cfg.app.id, env);
1700
+ const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/entitlements`, {
1701
+ headers: { authorization: `Bearer ${token}` }
1702
+ });
1703
+ if (res.ok || res.status === 404) return;
1704
+ if (res.status === 403) {
1705
+ throw new Error(
1706
+ `${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`
1707
+ );
1708
+ }
1709
+ throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText3(res)}`);
1710
+ }
1711
+ async function postJson(doFetch, url, bearer, body) {
1712
+ const res = await doFetch(url, {
1713
+ method: "POST",
1714
+ headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
1715
+ body: JSON.stringify(body)
1716
+ });
1717
+ if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText3(res)}`);
1718
+ }
1719
+ function normalizeClerkConfig(value2) {
1720
+ if (!value2) return null;
1721
+ if (typeof value2 === "string") {
1722
+ const publishableKey2 = envValue(value2);
1723
+ return publishableKey2 ? { publishableKey: publishableKey2 } : null;
1724
+ }
1725
+ if (typeof value2 !== "object") return null;
1726
+ const cfg = value2;
1727
+ const publishableKey = envValue(cfg.publishableKey);
1728
+ return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
1729
+ }
1730
+ async function safeText3(res) {
1731
+ try {
1732
+ return redactSecrets((await res.text()).slice(0, 500));
1733
+ } catch {
1734
+ return "";
1735
+ }
1736
+ }
1737
+
1738
+ // src/config-reconcile-desired.ts
1739
+ function desiredRegistryState(cfg) {
1740
+ const environments = {};
1741
+ const services = orderAppServices(cfg.services);
1742
+ for (const env of cfg.envs) {
1743
+ const desiredServices = {};
1744
+ for (const service of services) {
1745
+ desiredServices[service] = {
1746
+ enabled: true,
1747
+ ...managedServiceConfig(cfg, env, service)
1748
+ };
1749
+ }
1750
+ const authoredAuth = cfg.auth?.clerk && Object.hasOwn(cfg.auth.clerk, env) ? cfg.auth.clerk[env] : void 0;
1751
+ const auth = normalizeClerkConfig(authoredAuth);
1752
+ if (authoredAuth && !auth) {
1753
+ throw new Error(`auth.clerk.${env} could not resolve its publishable key`);
1754
+ }
1755
+ const linkManaged = !!cfg.links && Object.hasOwn(cfg.links, env);
1756
+ environments[env] = {
1757
+ services: desiredServices,
1758
+ ...auth ? { auth } : {},
1759
+ ...linkManaged ? { link: cfg.links?.[env] ?? null } : {}
1760
+ };
1761
+ }
1762
+ return {
1763
+ appId: cfg.app.id,
1764
+ name: cfg.app.name,
1765
+ environments
1766
+ };
1767
+ }
1768
+ function managedServiceConfig(cfg, env, service) {
1769
+ if (service === "ai" && cfg.ai?.provider) {
1770
+ return {
1771
+ config: {
1772
+ provider: cfg.ai.provider,
1773
+ ...cfg.ai.model ? { model: cfg.ai.model } : {}
1774
+ }
1775
+ };
1776
+ }
1777
+ if (service === "calendar") return { config: calendarServiceConfig(cfg, env) };
1778
+ return {};
1779
+ }
1780
+
1781
+ // src/config-reconcile.ts
1782
+ import { appServiceIds as appServiceIds2, orderAppServices as orderAppServices2 } from "@odla-ai/apps";
1783
+
1784
+ // src/config-reconcile-digest.ts
1785
+ import { createHash } from "crypto";
1786
+ function canonicalJson(value2) {
1787
+ return JSON.stringify(canonicalValue(value2));
1788
+ }
1789
+ function configDigest(value2) {
1790
+ return `sha256:${createHash("sha256").update(canonicalJson(value2)).digest("hex")}`;
1791
+ }
1792
+ function canonicalValue(value2) {
1793
+ if (Array.isArray(value2)) return value2.map(canonicalValue);
1794
+ if (value2 && typeof value2 === "object") {
1795
+ return Object.fromEntries(
1796
+ Object.entries(value2).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, canonicalValue(entry)])
1797
+ );
1798
+ }
1799
+ return value2;
1800
+ }
1801
+
1802
+ // src/config-reconcile-values.ts
1803
+ function difference(path, desired, observed, status, reason, desiredSource, observedSource, env, service) {
1804
+ return { path, env, service, status, desired, observed, desiredSource, observedSource, reason };
1805
+ }
1806
+ function action(kind, path, before, after, risk, applySupport, reason) {
1807
+ return { kind, path, before, after, risk, requiresApproval: false, applySupport, reason };
1808
+ }
1809
+ function scopedAction(kind, path, env, service, before, after, reason, destructive = false) {
1810
+ const production = env === "prod" || env === "production";
1811
+ return {
1812
+ ...action(
1813
+ kind,
1814
+ path,
1815
+ before,
1816
+ after,
1817
+ destructive ? "high" : production ? "medium" : "low",
1818
+ destructive ? "studio" : "provision",
1819
+ reason
1820
+ ),
1821
+ env,
1822
+ ...service ? { service } : {},
1823
+ requiresApproval: production || destructive
1824
+ };
1825
+ }
1826
+ function observedProjection(app) {
1827
+ return {
1828
+ appId: app.appId,
1829
+ name: app.name,
1830
+ archivedAt: app.archivedAt,
1831
+ environments: Object.fromEntries(
1832
+ Object.entries(app.environments).sort(([left], [right]) => left.localeCompare(right)).map(([env, services]) => [
1833
+ env,
1834
+ Object.fromEntries(Object.entries(services).sort(([left], [right]) => left.localeCompare(right)))
1835
+ ])
1836
+ ),
1837
+ auth: app.auth,
1838
+ links: app.links
1839
+ };
1840
+ }
1841
+ function projectConfig(value2, keys) {
1842
+ return Object.fromEntries(keys.filter((key) => value2[key] !== void 0).map((key) => [key, value2[key]]));
1843
+ }
1844
+ function same(left, right) {
1845
+ return canonicalJson(left) === canonicalJson(right);
1846
+ }
1847
+ function actionId(actionValue) {
1848
+ return `action-${configDigest(actionValue).slice("sha256:".length, "sha256:".length + 16)}`;
1849
+ }
1850
+ function quoteArg(value2) {
1851
+ return `'${value2.replace(/'/g, `'\\''`)}'`;
1852
+ }
1853
+
1854
+ // src/config-reconcile.ts
1855
+ var COVERAGE = {
1856
+ included: [
1857
+ "app display name",
1858
+ "environment service enablement",
1859
+ "AI provider/model and calendar booking service configuration",
1860
+ "auth configuration when explicitly declared",
1861
+ "deployment link when explicitly declared"
1862
+ ],
1863
+ excluded: [
1864
+ { path: "db.schema", reason: "owned by the database data plane" },
1865
+ { path: "db.rules", reason: "owned by the database data plane" },
1866
+ { path: "integrations.seeds", reason: "guarded data-plane writes are not Registry state" },
1867
+ { path: "credentials", reason: "shown-once credentials are intentionally local" },
1868
+ { path: "secrets", reason: "vault and Worker secrets are write-only" },
1869
+ { path: "calendar.connection", reason: "OAuth connection state is provider-owned" },
1870
+ { path: "service.config.opaque", reason: "controller-assigned fields remain service-owned" }
1871
+ ]
1872
+ };
1873
+ function reconcileConfig(input) {
1874
+ const desiredSource = { kind: "project_config", location: input.configPath };
1875
+ const observedSource = {
1876
+ kind: "registry",
1877
+ location: `${input.platformUrl}/registry/apps/${encodeURIComponent(input.desired.appId)}`
1878
+ };
1879
+ const differences = [];
1880
+ const actions = [];
1881
+ const add = (difference2, action2) => {
1882
+ differences.push(difference2);
1883
+ if (action2) actions.push({ ...action2, id: actionId(action2) });
1884
+ };
1885
+ compareApp(input.desired, input.observed, desiredSource, observedSource, add);
1886
+ compareEnvironments(input.desired, input.observed, desiredSource, observedSource, add);
1887
+ const desiredRevision = configDigest(input.desired);
1888
+ const observedRevision = input.observed ? configDigest(observedProjection(input.observed)) : null;
1889
+ const different = differences.filter((entry) => entry.status === "different").length;
1890
+ const unmanaged = differences.length - different;
1891
+ return {
1892
+ generatedAt: input.generatedAt,
1893
+ scope: {
1894
+ appId: input.desired.appId,
1895
+ platformUrl: input.platformUrl,
1896
+ environments: Object.keys(input.desired.environments).sort()
1897
+ },
1898
+ sources: { desired: desiredSource, observed: observedSource },
1899
+ desiredRevision,
1900
+ observedRevision,
1901
+ status: different ? "different" : unmanaged ? "unmanaged" : "in_sync",
1902
+ summary: {
1903
+ differences: different,
1904
+ unmanaged,
1905
+ actions: actions.length,
1906
+ productionActions: actions.filter((action2) => action2.env === "prod" || action2.env === "production").length,
1907
+ highRiskActions: actions.filter((action2) => action2.risk === "high").length
1908
+ },
1909
+ coverage: COVERAGE,
1910
+ differences,
1911
+ actions
1912
+ };
1913
+ }
1914
+ function compareApp(desired, observed, desiredSource, observedSource, add) {
1915
+ if (!observed) {
1916
+ const path = "app";
1917
+ add(
1918
+ difference(path, desired, null, "different", "the app is absent from Registry", desiredSource, observedSource),
1919
+ action(
1920
+ "create_app",
1921
+ path,
1922
+ null,
1923
+ { appId: desired.appId, name: desired.name },
1924
+ "low",
1925
+ "provision",
1926
+ "create the repository-declared app"
1927
+ )
1928
+ );
1929
+ return;
1930
+ }
1931
+ if (observed.name !== desired.name) {
1932
+ const path = "app.name";
1933
+ add(
1934
+ difference(path, desired.name, observed.name, "different", "the checked-in display name differs", desiredSource, observedSource),
1935
+ {
1936
+ ...action("rename_app", path, observed.name, desired.name, "low", "command", "make Registry match the checked-in display name"),
1937
+ command: `odla-ai app rename ${quoteArg(desired.name)} --config ${quoteArg(desiredSource.location)}`
1938
+ }
1939
+ );
1940
+ }
1941
+ }
1942
+ function compareEnvironments(desired, observed, desiredSource, observedSource, add) {
1943
+ const envs = /* @__PURE__ */ new Set([
1944
+ ...Object.keys(desired.environments),
1945
+ ...Object.keys(observed?.environments ?? {}).filter((env) => Object.values(observed?.environments[env] ?? {}).some((service) => service.enabled))
1946
+ ]);
1947
+ for (const env of [...envs].sort()) {
1948
+ compareServices(env, desired, observed, desiredSource, observedSource, add);
1949
+ compareOptionalEnvironmentState(env, desired, observed, desiredSource, observedSource, add);
1950
+ }
1951
+ }
1952
+ function compareServices(env, desired, observed, desiredSource, observedSource, add) {
1953
+ const wanted = desired.environments[env]?.services ?? {};
1954
+ const live = observed?.environments[env] ?? {};
1955
+ const knownOrder = orderAppServices2(appServiceIds2());
1956
+ const services = [.../* @__PURE__ */ new Set([...knownOrder, ...Object.keys(wanted), ...Object.keys(live)])];
1957
+ for (const service of services) {
1958
+ const next = wanted[service];
1959
+ const current = live[service];
1960
+ const path = `environments.${env}.services.${service}`;
1961
+ if (next?.enabled && !current?.enabled) {
1962
+ add(
1963
+ difference(path, next, current ?? null, "different", "the repository enables a service Registry does not", desiredSource, observedSource, env, service),
1964
+ scopedAction("enable_service", path, env, service, current ?? null, next, "enable the repository-declared service")
1965
+ );
1966
+ continue;
1967
+ }
1968
+ if (next?.enabled && current?.enabled && next.config) {
1969
+ const projected = projectConfig(current.config, Object.keys(next.config));
1970
+ if (!same(next.config, projected)) {
1971
+ add(
1972
+ difference(`${path}.config`, next.config, projected, "different", "repository-managed service settings differ", desiredSource, observedSource, env, service),
1973
+ scopedAction(
1974
+ "configure_service",
1975
+ `${path}.config`,
1976
+ env,
1977
+ service,
1978
+ projected,
1979
+ next.config,
1980
+ "apply the repository-managed service settings"
1981
+ )
1982
+ );
1983
+ }
1984
+ }
1985
+ }
1986
+ for (const service of [...services].reverse()) {
1987
+ const next = wanted[service];
1988
+ const current = live[service];
1989
+ if (next?.enabled || !current?.enabled) continue;
1990
+ const path = `environments.${env}.services.${service}`;
1991
+ add(
1992
+ difference(path, null, { enabled: true }, "different", "Registry enables a service absent from repository intent", desiredSource, observedSource, env, service),
1993
+ scopedAction(
1994
+ "disable_service",
1995
+ path,
1996
+ env,
1997
+ service,
1998
+ { enabled: true },
1999
+ { enabled: false },
2000
+ "disablement is destructive to availability and requires an explicit Studio review",
2001
+ true
2002
+ )
2003
+ );
2004
+ }
2005
+ }
2006
+ function compareOptionalEnvironmentState(env, desired, observed, desiredSource, observedSource, add) {
2007
+ const wanted = desired.environments[env];
2008
+ const liveAuth = observed?.auth[env] ?? null;
2009
+ if (wanted?.auth) {
2010
+ const projected = liveAuth ? projectConfig(liveAuth, Object.keys(wanted.auth)) : null;
2011
+ if (!same(wanted.auth, projected)) {
2012
+ const path = `environments.${env}.auth`;
2013
+ add(
2014
+ difference(path, wanted.auth, projected, "different", "explicit repository auth settings differ", desiredSource, observedSource, env),
2015
+ scopedAction("set_auth", path, env, void 0, projected, wanted.auth, "apply explicit repository auth settings")
2016
+ );
2017
+ }
2018
+ } else if (liveAuth) {
2019
+ add(difference(
2020
+ `environments.${env}.auth`,
2021
+ null,
2022
+ projectConfig(liveAuth, ["publishableKey", "audience", "mode"]),
2023
+ "unmanaged",
2024
+ "live auth exists but this project config does not manage it",
2025
+ desiredSource,
2026
+ observedSource,
2027
+ env
2028
+ ));
2029
+ }
2030
+ const managesLink = wanted && Object.hasOwn(wanted, "link");
2031
+ const liveLink = observed?.links[env] ?? null;
2032
+ if (managesLink && wanted.link !== liveLink) {
2033
+ const path = `environments.${env}.link`;
2034
+ add(
2035
+ difference(path, wanted.link ?? null, liveLink, "different", "explicit repository deployment link differs", desiredSource, observedSource, env),
2036
+ scopedAction("set_link", path, env, void 0, liveLink, wanted.link ?? null, "apply the repository deployment link")
2037
+ );
2038
+ } else if (!managesLink && liveLink) {
2039
+ add(difference(
2040
+ `environments.${env}.link`,
2041
+ null,
2042
+ liveLink,
2043
+ "unmanaged",
2044
+ "a live deployment link exists but this project config does not manage it",
2045
+ desiredSource,
2046
+ observedSource,
2047
+ env
2048
+ ));
2049
+ }
2050
+ }
2051
+
2052
+ // src/config-reconcile-command.ts
2053
+ import { createAppsClient } from "@odla-ai/apps";
2054
+ import { join as join3 } from "path";
2055
+ async function configDiff(options) {
2056
+ const reconciliation = await inspectConfig(options);
2057
+ const { actions: _actions, ...read3 } = reconciliation;
2058
+ const document = {
2059
+ schemaVersion: "odla.config-diff/v1",
2060
+ ...read3,
2061
+ nextActions: diffNextActions(reconciliation, options.configPath)
2062
+ };
2063
+ printDiff(document, options);
2064
+ return document;
2065
+ }
2066
+ async function configPlan(options) {
2067
+ const reconciliation = await inspectConfig(options);
2068
+ const planDigest = configDigest({
2069
+ schemaVersion: "odla.config-plan/v1",
2070
+ desiredRevision: reconciliation.desiredRevision,
2071
+ observedRevision: reconciliation.observedRevision,
2072
+ actions: reconciliation.actions
2073
+ });
2074
+ const document = {
2075
+ schemaVersion: "odla.config-plan/v1",
2076
+ ...reconciliation,
2077
+ planDigest,
2078
+ apply: {
2079
+ supported: false,
2080
+ reason: "conditional, resumable config apply is not available yet; use the exact reviewed commands below"
2081
+ },
2082
+ nextActions: planNextActions(reconciliation, options.configPath)
2083
+ };
2084
+ printPlan(document, options);
2085
+ return document;
2086
+ }
2087
+ async function inspectConfig(options) {
2088
+ const out = options.stdout ?? console;
2089
+ const cfg = await loadProjectConfig(options.configPath);
2090
+ const doFetch = options.fetch ?? fetch;
2091
+ const token = await resolveAdminPlatformToken({
2092
+ platform: cfg.platformUrl,
2093
+ scope: "app:config:read",
2094
+ token: options.token,
2095
+ tokenFile: join3(cfg.rootDir, ".odla", "admin-token.local.json"),
2096
+ rootDir: cfg.rootDir,
2097
+ email: options.email,
2098
+ open: options.open,
2099
+ fetch: doFetch,
2100
+ stdout: out,
2101
+ openApprovalUrl: options.openApprovalUrl,
2102
+ label: `odla CLI (${cfg.app.id} config read)`
2103
+ });
2104
+ const client = createAppsClient({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
2105
+ const observed = await client.resolveApp(cfg.app.id);
2106
+ return reconcileConfig({
2107
+ desired: desiredRegistryState(cfg),
2108
+ observed,
2109
+ configPath: cfg.configPath,
2110
+ platformUrl: cfg.platformUrl,
2111
+ generatedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
2112
+ });
2113
+ }
2114
+ function printDiff(document, options) {
2115
+ const out = options.stdout ?? console;
2116
+ if (options.json) {
2117
+ out.log(JSON.stringify(document, null, 2));
2118
+ return;
2119
+ }
2120
+ printHeader(out, "diff", document);
2121
+ printDifferences(out, document);
2122
+ printNext(out, document.nextActions);
2123
+ }
2124
+ function printPlan(document, options) {
2125
+ const out = options.stdout ?? console;
2126
+ if (options.json) {
2127
+ out.log(JSON.stringify(document, null, 2));
2128
+ return;
2129
+ }
2130
+ printHeader(out, "plan", document);
2131
+ for (const entry of document.actions) {
2132
+ const scope = [entry.env, entry.service].filter(Boolean).join("/");
2133
+ out.log(
2134
+ ` ${entry.id} ${entry.kind}${scope ? ` (${scope})` : ""} ${entry.risk}${entry.requiresApproval ? ", approval required" : ""}`
2135
+ );
2136
+ out.log(` ${entry.reason}`);
2137
+ }
2138
+ if (!document.actions.length) out.log(" no managed changes");
2139
+ out.log(`plan digest: ${document.planDigest}`);
2140
+ out.log(`apply: unsupported \u2014 ${document.apply.reason}`);
2141
+ printNext(out, document.nextActions);
2142
+ }
2143
+ function printHeader(out, kind, document) {
2144
+ out.log(`config ${kind}: ${document.scope.appId} \u2014 ${document.status}`);
2145
+ out.log(`platform: ${document.scope.platformUrl}`);
2146
+ out.log(`desired: ${document.desiredRevision}`);
2147
+ out.log(`observed: ${document.observedRevision ?? "absent"}`);
2148
+ out.log(
2149
+ `summary: ${document.summary.differences} different, ${document.summary.unmanaged} unmanaged, ${document.summary.actions} planned actions`
2150
+ );
2151
+ }
2152
+ function printDifferences(out, document) {
2153
+ for (const entry of document.differences) {
2154
+ out.log(` ${entry.status} ${entry.path} ${entry.reason}`);
2155
+ }
2156
+ if (!document.differences.length) out.log(" managed Registry configuration is in sync");
2157
+ }
2158
+ function printNext(out, next) {
2159
+ if (!next.length) return;
2160
+ out.log("next:");
2161
+ for (const item of next) out.log(` ${item.command}
2162
+ ${item.description}`);
2163
+ }
2164
+ function diffNextActions(reconciliation, configPath) {
2165
+ if (reconciliation.status === "in_sync") {
2166
+ return [{
2167
+ code: "verify_runtime",
2168
+ command: `odla-ai smoke --config ${quoteArg2(configPath)}`,
2169
+ description: "Registry intent matches; verify the service-owned data planes separately."
2170
+ }];
2171
+ }
2172
+ return [{
2173
+ code: "freeze_plan",
2174
+ command: `odla-ai config plan --config ${quoteArg2(configPath)} --json`,
2175
+ description: "Freeze the current desired and observed revisions into a reviewable action plan."
2176
+ }];
2177
+ }
2178
+ function planNextActions(reconciliation, configPath) {
2179
+ const next = [];
2180
+ if (reconciliation.actions.some((action2) => action2.applySupport === "provision")) {
2181
+ next.push({
2182
+ code: "review_provision",
2183
+ command: `odla-ai provision --config ${quoteArg2(configPath)} --dry-run`,
2184
+ description: "Review the existing provisioner's service/auth/link work before applying it."
2185
+ });
2186
+ }
2187
+ for (const action2 of reconciliation.actions.filter((entry) => entry.command)) {
2188
+ if (!next.some((entry) => entry.command === action2.command)) {
2189
+ next.push({ code: action2.kind, command: action2.command, description: action2.reason });
2190
+ }
2191
+ }
2192
+ if (reconciliation.actions.some((action2) => action2.applySupport === "studio")) {
2193
+ const { appId, platformUrl } = reconciliation.scope;
2194
+ next.push({
2195
+ code: "review_destructive",
2196
+ command: `${platformUrl}/studio/apps/${encodeURIComponent(appId)}/settings`,
2197
+ description: "Review service disablement in the owning Studio scope; this plan will not apply it."
2198
+ });
2199
+ }
2200
+ if (!reconciliation.actions.length && reconciliation.summary.unmanaged) {
2201
+ next.push({
2202
+ code: "declare_or_accept_runtime_state",
2203
+ command: `${reconciliation.scope.platformUrl}/studio/apps/${encodeURIComponent(reconciliation.scope.appId)}/settings`,
2204
+ description: "Declare the live value in project config or keep it as an explicit runtime-managed setting."
2205
+ });
2206
+ }
2207
+ return next;
2208
+ }
2209
+ function quoteArg2(value2) {
2210
+ return `'${value2.replace(/'/g, `'\\''`)}'`;
2211
+ }
2212
+
1683
2213
  // src/doctor-checks.ts
1684
2214
  import { execFileSync } from "child_process";
1685
2215
  import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1686
- import { join as join4, resolve as resolve3 } from "path";
2216
+ import { join as join5, resolve as resolve3 } from "path";
1687
2217
 
1688
2218
  // src/wrangler.ts
1689
2219
  import { spawn as spawn2 } from "child_process";
1690
2220
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1691
- import { join as join3 } from "path";
2221
+ import { join as join4 } from "path";
1692
2222
  var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
1693
2223
  const child = spawn2(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
1694
2224
  let stdout = "";
@@ -1702,7 +2232,7 @@ var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) =>
1702
2232
  var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
1703
2233
  function findWranglerConfig(rootDir) {
1704
2234
  for (const name of WRANGLER_CONFIG_FILES) {
1705
- const path = join3(rootDir, name);
2235
+ const path = join4(rootDir, name);
1706
2236
  if (existsSync4(path)) return path;
1707
2237
  }
1708
2238
  return null;
@@ -1768,11 +2298,11 @@ function lintRules(rules, entities, publicRead) {
1768
2298
  const warnings = [];
1769
2299
  if (!rules) return warnings;
1770
2300
  for (const [ns, actions] of Object.entries(rules)) {
1771
- for (const [action, expr] of Object.entries(actions)) {
2301
+ for (const [action2, expr] of Object.entries(actions)) {
1772
2302
  if (typeof expr !== "string" || expr.trim() !== "true") continue;
1773
- if (action === "view" && publicRead.includes(ns)) continue;
2303
+ if (action2 === "view" && publicRead.includes(ns)) continue;
1774
2304
  warnings.push(
1775
- action === "view" ? `rules.${ns}.view is "true" \u2014 public read; add "${ns}" to db.publicRead if intended` : `rules.${ns}.${action} is "true" \u2014 any client can ${action} ${ns} rows`
2305
+ 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`
1776
2306
  );
1777
2307
  }
1778
2308
  }
@@ -1811,7 +2341,7 @@ function wranglerWarnings(rootDir) {
1811
2341
  const dir = resolve3(rootDir, assets.directory);
1812
2342
  if (dir === resolve3(rootDir)) {
1813
2343
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
1814
- } else if (existsSync5(join4(dir, "node_modules"))) {
2344
+ } else if (existsSync5(join5(dir, "node_modules"))) {
1815
2345
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
1816
2346
  }
1817
2347
  }
@@ -1876,7 +2406,7 @@ function calendarProjectWarnings(rootDir) {
1876
2406
  }
1877
2407
  function readPackageJson(rootDir) {
1878
2408
  try {
1879
- return JSON.parse(readFileSync4(join4(rootDir, "package.json"), "utf8"));
2409
+ return JSON.parse(readFileSync4(join5(rootDir, "package.json"), "utf8"));
1880
2410
  } catch {
1881
2411
  return null;
1882
2412
  }
@@ -1932,8 +2462,8 @@ function integrationWarnings(integrations, schema, rules) {
1932
2462
  warnings.push(`integration "${integration.id}" has no rules for "${ns}"`);
1933
2463
  continue;
1934
2464
  }
1935
- for (const action of ["view", "create", "update", "delete"]) {
1936
- if (rule[action] === void 0) warnings.push(`integration "${integration.id}" rule "${ns}.${action}" is missing`);
2465
+ for (const action2 of ["view", "create", "update", "delete"]) {
2466
+ if (rule[action2] === void 0) warnings.push(`integration "${integration.id}" rule "${ns}.${action2}" is missing`);
1937
2467
  }
1938
2468
  }
1939
2469
  for (const seed of integration.seeds ?? []) {
@@ -2086,7 +2616,7 @@ async function doctor(options) {
2086
2616
  // src/init.ts
2087
2617
  import { existsSync as existsSync6, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
2088
2618
  import { dirname as dirname4, resolve as resolve4 } from "path";
2089
- import { appServiceDefinition as appServiceDefinition2, appServiceIds as appServiceIds2 } from "@odla-ai/apps";
2619
+ import { appServiceDefinition as appServiceDefinition2, appServiceIds as appServiceIds3 } from "@odla-ai/apps";
2090
2620
  function initProject(options) {
2091
2621
  const out = options.stdout ?? console;
2092
2622
  const rootDir = resolve4(options.rootDir ?? process.cwd());
@@ -2101,7 +2631,7 @@ function initProject(options) {
2101
2631
  const services = options.services?.length ? options.services : ["db", "ai"];
2102
2632
  for (const service of services) {
2103
2633
  const definition = appServiceDefinition2(service);
2104
- if (!definition) throw new Error(`--services contains unknown service "${service}" (known: ${appServiceIds2().join(", ")})`);
2634
+ if (!definition) throw new Error(`--services contains unknown service "${service}" (known: ${appServiceIds3().join(", ")})`);
2105
2635
  for (const dependency of definition.requires) {
2106
2636
  if (!services.includes(dependency)) throw new Error(`--services ${service} requires ${dependency}`);
2107
2637
  }
@@ -2295,7 +2825,7 @@ function assertWranglerConfig(cfg) {
2295
2825
 
2296
2826
  // src/secrets-set.ts
2297
2827
  import { putSecret } from "@odla-ai/ai";
2298
- import { tenantIdFor } from "@odla-ai/apps";
2828
+ import { tenantIdFor as tenantIdFor2 } from "@odla-ai/apps";
2299
2829
  var PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
2300
2830
  async function secretsSet(options) {
2301
2831
  const name = (options.name ?? "").trim();
@@ -2347,13 +2877,13 @@ async function resolveVaultWrite(options) {
2347
2877
  throw new Error(`refusing to store a secret for "${options.env}" without --yes`);
2348
2878
  }
2349
2879
  const value2 = await secretInputValue(options, "secret");
2350
- return { cfg, tenantId: tenantIdFor(cfg.app.id, options.env), value: value2, doFetch, out };
2880
+ return { cfg, tenantId: tenantIdFor2(cfg.app.id, options.env), value: value2, doFetch, out };
2351
2881
  }
2352
2882
 
2353
2883
  // src/skill.ts
2354
2884
  import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync5, readdirSync, writeFileSync as writeFileSync3 } from "fs";
2355
2885
  import { homedir } from "os";
2356
- import { dirname as dirname5, isAbsolute as isAbsolute3, join as join5, relative as relative2, resolve as resolve5, sep } from "path";
2886
+ import { dirname as dirname5, isAbsolute as isAbsolute3, join as join6, relative as relative2, resolve as resolve5, sep } from "path";
2357
2887
  import { fileURLToPath } from "url";
2358
2888
 
2359
2889
  // src/skill-adapters.ts
@@ -2447,12 +2977,12 @@ function installSkill(options = {}) {
2447
2977
  plans.set(target, { target, content: content2, boundary, managedMerge });
2448
2978
  };
2449
2979
  const planSkillTree = (targetDir2, boundary = root) => {
2450
- for (const rel of files) plan(join5(targetDir2, rel), readFileSync5(join5(sourceDir, rel), "utf8"), false, boundary);
2980
+ for (const rel of files) plan(join6(targetDir2, rel), readFileSync5(join6(sourceDir, rel), "utf8"), false, boundary);
2451
2981
  };
2452
2982
  let targetDir;
2453
2983
  if (options.global) {
2454
- const claudeRoot = join5(home, ".claude", "skills");
2455
- const codexRoot = resolve5(options.codexHomeDir ?? process.env.CODEX_HOME ?? join5(home, ".codex"), "skills");
2984
+ const claudeRoot = join6(home, ".claude", "skills");
2985
+ const codexRoot = resolve5(options.codexHomeDir ?? process.env.CODEX_HOME ?? join6(home, ".codex"), "skills");
2456
2986
  targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
2457
2987
  for (const harness of harnesses) {
2458
2988
  const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
@@ -2460,35 +2990,35 @@ function installSkill(options = {}) {
2460
2990
  rememberTarget(harness, skillRoot);
2461
2991
  }
2462
2992
  } else {
2463
- const sharedRoot = join5(root, ".agents", "skills");
2993
+ const sharedRoot = join6(root, ".agents", "skills");
2464
2994
  planSkillTree(sharedRoot);
2465
- const claudeRoot = join5(root, ".claude", "skills");
2995
+ const claudeRoot = join6(root, ".claude", "skills");
2466
2996
  targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
2467
2997
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
2468
2998
  if (harnesses.includes("claude")) {
2469
2999
  for (const skill of skillNames(files)) {
2470
- const canonical = readFileSync5(join5(sourceDir, skill, "SKILL.md"), "utf8");
2471
- plan(join5(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
3000
+ const canonical = readFileSync5(join6(sourceDir, skill, "SKILL.md"), "utf8");
3001
+ plan(join6(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
2472
3002
  }
2473
3003
  rememberTarget("claude", claudeRoot);
2474
3004
  }
2475
3005
  if (harnesses.includes("cursor")) {
2476
- const cursorRule = join5(root, ".cursor", "rules", "odla.mdc");
3006
+ const cursorRule = join6(root, ".cursor", "rules", "odla.mdc");
2477
3007
  plan(cursorRule, CURSOR_RULE);
2478
3008
  rememberTarget("cursor", cursorRule);
2479
3009
  }
2480
3010
  if (harnesses.includes("agents")) {
2481
- const agentsFile = join5(root, "AGENTS.md");
3011
+ const agentsFile = join6(root, "AGENTS.md");
2482
3012
  plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
2483
3013
  rememberTarget("agents", agentsFile);
2484
3014
  }
2485
3015
  if (harnesses.includes("copilot")) {
2486
- const copilotFile = join5(root, ".github", "copilot-instructions.md");
3016
+ const copilotFile = join6(root, ".github", "copilot-instructions.md");
2487
3017
  plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
2488
3018
  rememberTarget("copilot", copilotFile);
2489
3019
  }
2490
3020
  if (harnesses.includes("gemini")) {
2491
- const geminiFile = join5(root, "GEMINI.md");
3021
+ const geminiFile = join6(root, "GEMINI.md");
2492
3022
  plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
2493
3023
  rememberTarget("gemini", geminiFile);
2494
3024
  }
@@ -2595,7 +3125,7 @@ function symlinkedComponent(boundary, target) {
2595
3125
  }
2596
3126
  let current = boundary;
2597
3127
  for (const part of rel.split(sep).filter(Boolean)) {
2598
- current = join5(current, part);
3128
+ current = join6(current, part);
2599
3129
  try {
2600
3130
  if (lstatSync(current).isSymbolicLink()) return current;
2601
3131
  } catch (error) {
@@ -2612,7 +3142,7 @@ function listFiles(dir) {
2612
3142
  const results = [];
2613
3143
  const walk = (current) => {
2614
3144
  for (const entry of readdirSync(current, { withFileTypes: true })) {
2615
- const path = join5(current, entry.name);
3145
+ const path = join6(current, entry.name);
2616
3146
  if (entry.isDirectory()) walk(path);
2617
3147
  else results.push(relative2(dir, path));
2618
3148
  }
@@ -2690,7 +3220,7 @@ async function smoke(options) {
2690
3220
  out.log(` schema: ${liveEntities.length} entities`);
2691
3221
  const aggregateEntity = expectedEntities[0] ?? liveEntities[0];
2692
3222
  if (aggregateEntity) {
2693
- const aggregate = await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
3223
+ const aggregate = await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
2694
3224
  ns: aggregateEntity,
2695
3225
  aggregate: { count: true }
2696
3226
  });
@@ -2734,16 +3264,16 @@ async function getJson(doFetch, url, bearer) {
2734
3264
  const res = await doFetch(url, {
2735
3265
  headers: bearer ? { authorization: `Bearer ${bearer}` } : void 0
2736
3266
  });
2737
- if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText3(res)}`);
3267
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText4(res)}`);
2738
3268
  return res.json();
2739
3269
  }
2740
- async function postJson(doFetch, url, bearer, body) {
3270
+ async function postJson2(doFetch, url, bearer, body) {
2741
3271
  const res = await doFetch(url, {
2742
3272
  method: "POST",
2743
3273
  headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
2744
3274
  body: JSON.stringify(body)
2745
3275
  });
2746
- if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText3(res)}`);
3276
+ if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText4(res)}`);
2747
3277
  return res.json();
2748
3278
  }
2749
3279
  function publicConfigUrl(platformUrl, appId, env) {
@@ -2751,7 +3281,7 @@ function publicConfigUrl(platformUrl, appId, env) {
2751
3281
  url.searchParams.set("env", env);
2752
3282
  return url.toString();
2753
3283
  }
2754
- async function safeText3(res) {
3284
+ async function safeText4(res) {
2755
3285
  try {
2756
3286
  return redactSecrets((await res.text()).slice(0, 500));
2757
3287
  } catch {
@@ -2764,7 +3294,7 @@ import { execFile } from "child_process";
2764
3294
  import { promisify } from "util";
2765
3295
 
2766
3296
  // src/security-hosted-request.ts
2767
- async function requestHostedSecurityJson(options, path, init, action) {
3297
+ async function requestHostedSecurityJson(options, path, init, action2) {
2768
3298
  const platform = platformAudience(options.platform);
2769
3299
  const token = hostedSecurityCredential(options.token);
2770
3300
  const requestInit = {
@@ -2784,7 +3314,7 @@ async function requestHostedSecurityJson(options, path, init, action) {
2784
3314
  if (!response2.ok) {
2785
3315
  const code = optionalHostedText(body.error?.code, "error code", 128);
2786
3316
  const message2 = optionalHostedText(body.error?.message, "error message", 300);
2787
- throw new Error(`${action} failed (${response2.status})${code ? ` ${code}` : ""}${message2 ? `: ${message2}` : ""}`);
3317
+ throw new Error(`${action2} failed (${response2.status})${code ? ` ${code}` : ""}${message2 ? `: ${message2}` : ""}`);
2788
3318
  }
2789
3319
  return body;
2790
3320
  }
@@ -3000,10 +3530,10 @@ var CODE_BUILD_RECIPES = Object.freeze([{
3000
3530
 
3001
3531
  // src/code-images.ts
3002
3532
  import { spawn as spawn3 } from "child_process";
3003
- import { createHash } from "crypto";
3533
+ import { createHash as createHash2 } from "crypto";
3004
3534
  import { copyFile, mkdtemp, readFile, rm, writeFile } from "fs/promises";
3005
3535
  import { tmpdir } from "os";
3006
- import { join as join6 } from "path";
3536
+ import { join as join7 } from "path";
3007
3537
  import { fileURLToPath as fileURLToPath2 } from "url";
3008
3538
  var runCodeImageCommand = (command, args, stdio) => new Promise((accept, reject) => {
3009
3539
  const child = spawn3(command, [...args], { shell: false, stdio });
@@ -3059,13 +3589,13 @@ async function embeddedPiImageName() {
3059
3589
  const bundle = await readFile(embeddedPiAssetPath()).catch(() => {
3060
3590
  throw new Error("CLI-embedded Pi runtime is missing; reinstall this exact @odla-ai/cli version");
3061
3591
  });
3062
- return `odla-ai/pi-agent:embedded-sha256-${createHash("sha256").update(bundle).digest("hex")}`;
3592
+ return `odla-ai/pi-agent:embedded-sha256-${createHash2("sha256").update(bundle).digest("hex")}`;
3063
3593
  }
3064
3594
  async function buildEmbeddedPiImage(engine, image, run) {
3065
- const context = await mkdtemp(join6(tmpdir(), "odla-code-pi-"));
3595
+ const context = await mkdtemp(join7(tmpdir(), "odla-code-pi-"));
3066
3596
  try {
3067
- await copyFile(embeddedPiAssetPath(), join6(context, "pi-agent.js"));
3068
- await writeFile(join6(context, "Dockerfile"), [
3597
+ await copyFile(embeddedPiAssetPath(), join7(context, "pi-agent.js"));
3598
+ await writeFile(join7(context, "Dockerfile"), [
3069
3599
  `FROM ${CODE_NODE_IMAGE}`,
3070
3600
  "COPY pi-agent.js /opt/odla/pi-agent.js",
3071
3601
  "WORKDIR /workspace",
@@ -3168,7 +3698,7 @@ function encodeAgentInput(message2) {
3168
3698
  import { execFile as execFile2, spawn as spawn4 } from "child_process";
3169
3699
  import { constants } from "fs";
3170
3700
  import { access } from "fs/promises";
3171
- import { delimiter, join as join7 } from "path";
3701
+ import { delimiter, join as join8 } from "path";
3172
3702
  import { getgid, getuid } from "process";
3173
3703
  import { mkdir, mkdtemp as mkdtemp2, realpath, rm as rm2, writeFile as writeFile2 } from "fs/promises";
3174
3704
  import { tmpdir as tmpdir2 } from "os";
@@ -3186,7 +3716,7 @@ function assertPinnedImage(image) {
3186
3716
  async function commandAvailable(engine) {
3187
3717
  for (const directory of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
3188
3718
  try {
3189
- await access(join7(directory, engine), constants.X_OK);
3719
+ await access(join8(directory, engine), constants.X_OK);
3190
3720
  return true;
3191
3721
  } catch {
3192
3722
  }
@@ -3682,7 +4212,7 @@ async function stageWorkspacePair(baselineSource, workspaceSource, options = {})
3682
4212
  }
3683
4213
 
3684
4214
  // ../harness/dist/chunk-GMVZ4LZH.js
3685
- import { createHash as createHash2 } from "crypto";
4215
+ import { createHash as createHash3 } from "crypto";
3686
4216
  import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
3687
4217
  import { relative as relative4, resolve as resolve7 } from "path";
3688
4218
 
@@ -3699,7 +4229,7 @@ var CamelError = class extends Error {
3699
4229
  };
3700
4230
 
3701
4231
  // ../camel/dist/chunk-L5DYU2E2.js
3702
- function canonicalJson(value2) {
4232
+ function canonicalJson2(value2) {
3703
4233
  return JSON.stringify(normalize(value2));
3704
4234
  }
3705
4235
  async function sha256Hex(value2) {
@@ -3711,7 +4241,7 @@ function utf8Length(value2) {
3711
4241
  if (typeof value2 === "string") return new TextEncoder().encode(value2).byteLength;
3712
4242
  if (value2 instanceof Uint8Array) return value2.byteLength;
3713
4243
  try {
3714
- return new TextEncoder().encode(canonicalJson(value2)).byteLength;
4244
+ return new TextEncoder().encode(canonicalJson2(value2)).byteLength;
3715
4245
  } catch {
3716
4246
  throw new CamelError("conversion_rejected", "Unsafe input could not be canonically measured.");
3717
4247
  }
@@ -3837,7 +4367,7 @@ async function digestCodeVerificationReceipt(fields) {
3837
4367
  changedTestsRequireReview: fields.changedTestsRequireReview,
3838
4368
  outcome: fields.outcome
3839
4369
  };
3840
- return `sha256:${await sha256Hex(canonicalJson(canonical))}`;
4370
+ return `sha256:${await sha256Hex(canonicalJson2(canonical))}`;
3841
4371
  }
3842
4372
  function validate(fields) {
3843
4373
  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) {
@@ -3875,9 +4405,9 @@ async function createCodePortableCheckpoint(input) {
3875
4405
  const state2 = normalizeState(input.state);
3876
4406
  validateBaseAndPatch(input.baseCommitSha, input.patch);
3877
4407
  const patchDigest = `sha256:${await sha256Hex(input.patch)}`;
3878
- const stateDigest = `sha256:${await sha256Hex(canonicalJson(state2))}`;
4408
+ const stateDigest = `sha256:${await sha256Hex(canonicalJson2(state2))}`;
3879
4409
  const fields = { schemaVersion: 1, baseCommitSha: input.baseCommitSha, patchDigest, state: state2, stateDigest };
3880
- const checkpointDigest = `sha256:${await sha256Hex(canonicalJson(fields))}`;
4410
+ const checkpointDigest = `sha256:${await sha256Hex(canonicalJson2(fields))}`;
3881
4411
  return freeze({ ...fields, patch: input.patch, checkpointDigest });
3882
4412
  }
3883
4413
  async function verifyCodePortableCheckpoint(value2) {
@@ -3995,7 +4525,7 @@ async function digestCodeRepositorySnapshot(snapshot, limits = DEFAULT_LIMITS) {
3995
4525
  commitSha: snapshot.commitSha,
3996
4526
  files: [...snapshot.files].map((file) => ({ path: file.path, content: file.content })).sort((left, right) => left.path.localeCompare(right.path))
3997
4527
  };
3998
- return `sha256:${await sha256Hex(canonicalJson(normalized))}`;
4528
+ return `sha256:${await sha256Hex(canonicalJson2(normalized))}`;
3999
4529
  }
4000
4530
  function validateSnapshot(snapshot, limits) {
4001
4531
  if (!REPOSITORY.test(snapshot.repository) || !SHA4.test(snapshot.commitSha)) {
@@ -4028,7 +4558,7 @@ import { randomUUID } from "crypto";
4028
4558
  import { createHash as createHash22, randomUUID as randomUUID2 } from "crypto";
4029
4559
  import { createReadStream } from "fs";
4030
4560
  import { lstat as lstat22 } from "fs/promises";
4031
- import { join as join8 } from "path";
4561
+ import { join as join9 } from "path";
4032
4562
  import { mkdir as mkdir3, mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
4033
4563
  import { tmpdir as tmpdir3 } from "os";
4034
4564
  import { dirname as dirname6, join as join23, resolve as resolve32, sep as sep23 } from "path";
@@ -4037,10 +4567,10 @@ import { relative as relative22, resolve as resolve42 } from "path";
4037
4567
 
4038
4568
  // ../camel/dist/chunk-LAXU2AVK.js
4039
4569
  function conversionPolicyDigest(policy) {
4040
- return sha256Hex(canonicalJson(policy));
4570
+ return sha256Hex(canonicalJson2(policy));
4041
4571
  }
4042
4572
  function registeredIdRegistryDigest(values) {
4043
- return sha256Hex(canonicalJson(values));
4573
+ return sha256Hex(canonicalJson2(values));
4044
4574
  }
4045
4575
  async function createConversionRegistry(config) {
4046
4576
  const policies = /* @__PURE__ */ new Map();
@@ -4241,10 +4771,10 @@ function createEffectPolicy(config = {}) {
4241
4771
  return Object.freeze({ evaluate: (input) => evaluate(input, registries, approvalEffects) });
4242
4772
  }
4243
4773
  function destinationRegistryDigest(values) {
4244
- return sha256Hex(canonicalJson([...values].sort()));
4774
+ return sha256Hex(canonicalJson2([...values].sort()));
4245
4775
  }
4246
4776
  async function evaluate(input, registries, approvals) {
4247
- const actionDigest = await sha256Hex(canonicalJson(input));
4777
+ const actionDigest = await sha256Hex(canonicalJson2(input));
4248
4778
  const decisionId = `decision_${actionDigest.slice(0, 24)}`;
4249
4779
  const deny = (reasonCode) => ({ outcome: "deny", decisionId, reasonCode });
4250
4780
  if (!input.planId || !input.tool.name || !input.tool.policyId || !Number.isSafeInteger(input.tool.version)) return deny("invalid_descriptor");
@@ -4312,7 +4842,7 @@ function looksLikeDestination(value2) {
4312
4842
  }
4313
4843
 
4314
4844
  // ../harness/dist/chunk-GMVZ4LZH.js
4315
- import { createHash as createHash3 } from "crypto";
4845
+ import { createHash as createHash32 } from "crypto";
4316
4846
  async function digestStagedWorkspace(root, limits) {
4317
4847
  const files = [];
4318
4848
  const walk = async (directory) => {
@@ -4328,7 +4858,7 @@ async function digestStagedWorkspace(root, limits) {
4328
4858
  }
4329
4859
  };
4330
4860
  await walk(resolve7(root));
4331
- const hash = createHash2("sha256");
4861
+ const hash = createHash3("sha256");
4332
4862
  let bytes = 0;
4333
4863
  for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
4334
4864
  const content2 = await readFile2(file.target);
@@ -4947,7 +5477,7 @@ async function inspectArtifacts(workspaceDir, recipe2) {
4947
5477
  const receipts = [];
4948
5478
  for (const artifact of recipe2.expectedArtifacts ?? []) {
4949
5479
  try {
4950
- const path = join8(workspaceDir, artifact.path);
5480
+ const path = join9(workspaceDir, artifact.path);
4951
5481
  const info = await lstat22(path);
4952
5482
  if (!info.isFile() || info.isSymbolicLink()) {
4953
5483
  receipts.push({ artifactId: artifact.id, status: "invalid", bytes: null, digest: null });
@@ -5654,7 +6184,7 @@ async function appendCodeRuntimeEvent(control, command, event, refs) {
5654
6184
  const bounded = event.type === "message" ? { ...event, body: event.body.trim().slice(0, 2e4) || `${event.actor} event` } : event;
5655
6185
  await control.appendSessionEvent(command.sessionId, eventId, bounded);
5656
6186
  }
5657
- var digestRuntimeValue = (value2) => `sha256:${createHash3("sha256").update(value2).digest("hex")}`;
6187
+ var digestRuntimeValue = (value2) => `sha256:${createHash32("sha256").update(value2).digest("hex")}`;
5658
6188
  var runtimeErrorMessage = (value2) => value2 instanceof Error ? value2.message : String(value2);
5659
6189
  var runtimeRecord = (value2) => value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
5660
6190
  var safeRuntimeJson = (value2) => {
@@ -5965,317 +6495,68 @@ var CodePiRuntimeEngine = class {
5965
6495
  }
5966
6496
  };
5967
6497
 
5968
- // src/help.ts
6498
+ // src/version.ts
5969
6499
  import { readFileSync as readFileSync6 } from "fs";
5970
6500
  function cliVersion() {
5971
6501
  const pkg = JSON.parse(readFileSync6(new URL("../package.json", import.meta.url), "utf8"));
5972
6502
  return pkg.version ?? "unknown";
5973
6503
  }
5974
- function printHelp(output = console) {
5975
- output.log(`odla-ai
5976
6504
 
5977
- Start here:
5978
- odla-ai runbook ask "<question>" The current procedure, from odla's own
5979
- runbooks. Ask BEFORE searching the web or
5980
- working from memory: runbooks are edited
5981
- live, so your training data is out of date
5982
- and this is not.
5983
- odla-ai runbook impact After a change: which runbooks describe the
5984
- code you just touched, so you can fix any
5985
- step it made wrong.
5986
-
5987
- Usage:
5988
- odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
5989
- odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
5990
- odla-ai doctor [--config odla.config.mjs]
5991
- odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
5992
- odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
5993
- odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
5994
- odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
5995
- odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
5996
- odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
5997
- odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
5998
- odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
5999
- odla-ai app refresh-sandbox [--include-identity] [--include-files] [--dry-run] [--json] --yes
6000
- odla-ai app go-live [--include-identity] [--include-files] [--dry-run] [--json] --yes
6001
- odla-ai app promote [--dry-run] [--json] --yes
6002
- odla-ai app rename <name> [--config odla.config.mjs] [--email <odla-account>] [--json]
6003
- odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
6004
- odla-ai app owners add <email> [--email <odla-account>] [--json]
6005
- odla-ai app owners remove <email> [--email <odla-account>] [--json]
6006
- odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
6007
- odla-ai pm task list [--app <id>] [--column <c>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
6008
- odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
6009
- odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
6010
- odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
6011
- 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]
6012
- odla-ai pm decision add --app <id> --title <t> --body <text> [--status <s>] [--mutation-id <id>] [--json]
6013
- 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]
6014
- odla-ai pm <goal|task|decision|bug> get <id> [--json]
6015
- odla-ai pm goal set <id> [--title <t>|--status <s>|--proof <text>|--no-proof|--target <pct>|--no-target] [--mutation-id <id>] [--json]
6016
- 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]
6017
- odla-ai pm decision set <id> [--title <t>|--status <s>|--body <text>] [--mutation-id <id>] [--json]
6018
- 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]
6019
- odla-ai pm <goal|task|decision> done <id> [--mutation-id <id>]
6020
- odla-ai pm bug done <id> [--decision <accepted-decision-id>] [--mutation-id <id>]
6021
- odla-ai pm <goal|task|decision|bug> comment <id> --body "..." [--mutation-id <id>]
6022
- odla-ai pm <goal|task|decision|bug> comments <id> [--json]
6023
- odla-ai pm <goal|task|decision|bug> rm <id>
6024
- odla-ai pm handoff --app <id> [--json]
6025
- odla-ai discuss groups [--json]
6026
- odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
6027
- odla-ai discuss read <topic> [--limit <n> --offset <n>] [--json]
6028
- odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"] [--mutation-id <id>]
6029
- odla-ai discuss reply <topic> --body "..." [--markup "..."] [--mutation-id <id>]
6030
- odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
6031
- odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
6032
- odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
6033
- odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
6034
- odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
6035
- odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
6036
- odla-ai context list [--json]
6037
- odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
6038
- odla-ai context remove <name> --yes [--json]
6039
- odla-ai o11y status [--app <id>] [--context <name>] [--platform https://odla.ai] [--env prod] [--minutes 60] [--json]
6040
- odla-ai platform status [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
6041
- odla-ai whoami [--context <name>] [--platform https://odla.ai] [--json]
6042
- odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
6043
- odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
6044
- odla-ai runbook impact [--base origin/main] [--app <id>] [--all] [--limit <n>] [--json]
6045
- odla-ai runbook lint [--app <id>] [--all] [--json]
6046
- odla-ai runbook list [--app <id>] [--all] [--q <text>] [--json]
6047
- odla-ai runbook comment <slug> --body "..." [--app <id>]
6048
- odla-ai runbook get <slug> [--app <id>]
6049
- odla-ai runbook new <slug> --title <t> --file <path|-> [--summary <s>] [--requires <specs>] [--app <id>]
6050
- odla-ai runbook edit <slug> [--file <path|->|--body "..."] [--note <why>] [--requires <specs>] [--app <id>]
6051
- odla-ai runbook import <dir> [--visibility operator|admin] [--dry-run] [--app <id>]
6052
- odla-ai runbook publish <slug> [--app <id>]
6053
- odla-ai runbook visibility <slug> <operator|admin> [--app <id>]
6054
- odla-ai runbook archive <slug> [--app <id>]
6055
- odla-ai runbook history <slug> [--app <id>] [--json]
6056
- odla-ai runbook revert <slug> --version <n> [--app <id>]
6057
- odla-ai runbook rm <slug> [--app <id>]
6058
- odla-ai capabilities [--json]
6059
- odla-ai code connect [--env dev|prod] [--email <odla-account>] [--engine auto|container|podman|docker] [--slots <1-64>] [--once]
6060
- odla-ai admin ai show [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
6061
- odla-ai admin ai models [--context <name>] [--provider <id>] [--json]
6062
- odla-ai admin ai set <purpose> [--context <name>] [--provider <id>] [--model <id>] [--enabled|--no-enabled]
6063
- [--max-input-bytes <n>] [--max-output-tokens <n>] [--max-calls-per-run <n>] [--json]
6064
- odla-ai admin ai set security [--context <name>] --discovery-provider <id> --discovery-model <id>
6065
- --validation-provider <id> --validation-model <id> [--enabled|--no-enabled] [--json]
6066
- odla-ai admin ai credentials [--context <name>] [--json]
6067
- odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
6068
- odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
6069
- odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
6070
- odla-ai security github connect [--repo owner/name] [--env dev] [--email <odla-account>] [--no-open]
6071
- odla-ai security github disconnect --source <id> [--env dev] [--yes]
6072
- odla-ai security plan [--env dev] [--json]
6073
- odla-ai security sources [--env dev] [--json]
6074
- odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
6075
- odla-ai security status <job-id> [--follow] [--json]
6076
- odla-ai security report <job-id> [--json]
6077
- odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
6078
- odla-ai security run [target] --self --ack-redacted-source
6079
- odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
6080
- odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
6081
- odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
6082
- odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
6083
- odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
6084
- odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
6085
- odla-ai version
6086
-
6087
- Commands:
6088
- agent Inspect durable agent wakeups and explicitly requeue a
6089
- dead-lettered job; JSON output is stable for remote operators.
6090
- runbook odla's operational procedures, stored in the database and read at
6091
- the moment they are followed. "ask" gives a written, cited answer;
6092
- "search" the passages behind it; "get" the whole document.
6093
- "impact" diffs your working tree against a base ref and names the
6094
- runbooks that describe what you changed \u2014 run it after touching a
6095
- package's exported API or JSDoc, or a Studio surface. "lint"
6096
- checks the corpus the other way: every odla-ai command the
6097
- runbooks name is held against this CLI's real command surface,
6098
- and against the minimum versions each runbook declares. A wrong step
6099
- is fixed with "edit", which takes effect immediately: a runbook is
6100
- a row, not a release. Runbooks describe PROCEDURE; what an export
6101
- does and what it guarantees is JSDoc, rendered per package at
6102
- https://odla.ai/docs and shipped in the installed .d.ts. Answering
6103
- a question usually needs both.
6104
- whoami Report who this terminal is authenticated as, and whether it holds
6105
- platform admin. A handshake-minted device token is never admin,
6106
- however the human who approved it is configured.
6107
- context Explain selected config, platform, app, environment, and
6108
- developer-token provenance without printing credentials or
6109
- starting a device handshake. Operator work can run outside a
6110
- project checkout with explicit flags or ODLA_* environment
6111
- variables.
6112
- setup Install offline odla runbooks for common coding-agent harnesses.
6113
- init Create a generic odla.config.mjs plus starter schema/rules files.
6114
- doctor Validate and summarize the project config without network calls.
6115
- calendar Inspect, connect, or disconnect the live Google booking connection.
6116
- app Archive (suspend, data retained), restore, export, import, or
6117
- manage the co-owners of the app. Archiving takes every
6118
- environment's data plane down until restored; permanent deletion
6119
- has NO CLI \u2014 it requires a signed-in owner in Studio, and no
6120
- machine or agent credential can purge. "app export" downloads a
6121
- portable gzipped-JSONL snapshot of one database (--fresh takes a
6122
- new snapshot first) \u2014 your data is always yours to take.
6123
- "app import" upserts rows back in from JSON, JSONL, or a
6124
- {namespace: rows} map (the shape a query returns); it writes only
6125
- with --yes, so the bare command is a dry run, and it refuses to
6126
- guess an id mode that would duplicate rows on a re-run.
6127
- "app refresh-sandbox" replaces the sandbox with a copy of live
6128
- (the routine dev loop); "app go-live" copies the sandbox into an
6129
- EMPTY live database, once, at launch; "app promote" pushes only
6130
- schema, rules and gates up afterwards, leaving live's rows alone.
6131
- Each prints its plan and writes nothing without --yes, so the
6132
- bare command is the dry run. Clerk config, triggers, allowlists,
6133
- secrets and API keys never travel; identity rows stay behind
6134
- unless --include-identity.
6135
- "app rename <name>" changes only the display name humans read;
6136
- the app id is permanent (tenants, URLs and keys embed it), so a
6137
- rename never re-provisions anything or invalidates a credential.
6138
- "app owners add <email>" grants a signed-up odla member the SAME
6139
- full access as you; they then run their own "provision" to mint
6140
- their own credentials for the shared db (the live one included)
6141
- \u2014 no secret is ever copied between people.
6142
- capabilities Show what the CLI automates vs agent edits and human checkpoints.
6143
- code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
6144
- admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
6145
- security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
6146
- pm Project management (via @odla-ai/pm) shared across the apps you
6147
- co-own: conformance goals, kanban tasks, decisions, and bugs. With
6148
- no --app, "list" spans every co-owned project (the cross-project
6149
- view); with --app it scopes to one. Same device-grant auth as
6150
- "app". Entities: goal (alias conformance), task (alias kanban),
6151
- decision, bug. Status changes and comments post to each item's
6152
- @odla-ai/chat discussion thread.
6153
- discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
6154
- one group per project, topics with replies, @-mentions of people,
6155
- agents, PM items, and projects. Built for unattended use \u2014 post a
6156
- question, then "watch" it until someone answers. "watch" exits 75
6157
- (not 1) when it times out with nothing new, so a script can tell
6158
- "no answer yet" from a failure and just rerun. Use "who" to look
6159
- up the exact @[Label](kind/id) markup for a mention.
6160
- o11y Read one stable status envelope for application RED, current
6161
- live-sync load/freshness, the protected commit-to-visible
6162
- canary, collector ingest/scheduler trust, Cloudflare-owned
6163
- runtime metrics, and a machine verdict.
6164
- --json keeps auth progress on stderr for unattended agents.
6165
- platform Read canonical fleet health, releases, provider load/freshness,
6166
- explicit unknowns, and next actions through a read-only grant.
6167
- provision Register services, compose integrations, persist credentials, optionally push secrets.
6168
- smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
6169
- skill Same installer; --agent accepts all, claude, codex, cursor,
6170
- copilot, gemini, or agents (repeatable or comma-separated).
6171
- secrets Push configured db/o11y secrets into the Worker via wrangler
6172
- stdin; set stores a tenant-vault secret and set-clerk-key the
6173
- reserved Clerk secret key, write-only from stdin or an env var.
6174
- version Print the CLI version.
6175
-
6176
- Safety:
6177
- Every app has two databases on odla.ai: a sandbox (env "dev", tenant
6178
- <appId>--dev) and a live one (env "prod", tenant <appId>). Both are served by
6179
- production odla.ai \u2014 there is no separate odla to point at. New projects get
6180
- the sandbox only. Add "prod" explicitly to odla.config.mjs and pass --yes to
6181
- provision the live database; use --dry-run first to inspect the resolved plan.
6182
- Provision caches the approved developer token and service credentials under
6183
- .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
6184
- preflights Wrangler before any shown-once issuance or destructive rotation.
6185
- Projectless PM, Discussions, o11y, runbook, and identity commands use
6186
- --platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
6187
- ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
6188
- select it explicitly with --context or ODLA_CONTEXT. Each named context gets
6189
- isolated developer and scoped-token caches. Override their locations with
6190
- ODLA_DEV_TOKEN_FILE and ODLA_ADMIN_TOKEN_FILE; ODLA_CONTEXT_FILE relocates
6191
- the metadata file. Flags and specific ODLA_* scope variables beat a selected
6192
- context, which beats project config. There is no ambient current context.
6193
- "context show" reports only provenance and cache state and never authenticates.
6194
- Provision opens the approval page in your browser automatically whenever the
6195
- machine can show one, including agent-driven runs; only CI, SSH, and
6196
- display-less hosts skip it. Use --open to force or --no-open to suppress.
6197
- Browser launch is best-effort: the printed approval URL is authoritative and
6198
- agents must relay it to the human verbatim. A started handshake is persisted
6199
- under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
6200
- the same code. Outside an interactive terminal the wait is capped (90s by
6201
- default, --wait <seconds> to change); a still-pending handshake then exits
6202
- with code 75: relay the URL, wait for approval, and re-run to collect.
6203
- A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
6204
- The email is a non-secret identity hint: never provide a password or session
6205
- token. The matching account must already exist, be signed in, explicitly
6206
- review the exact code, and finish any current request before claiming another.
6207
- Run Code from a GitHub checkout already connected to an app in Studio; an
6208
- odla.config.mjs may select the app explicitly but is not required. Code host
6209
- approval and credential hashes live in odla-ai/db. The host
6210
- credential is never written under .odla/; it exists only in the foreground
6211
- "code connect" process and is rotated by the next approved connection.
6212
- Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
6213
- OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
6214
- GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
6215
- for a PAT or provider key. GitHub read access is separate from the explicit
6216
- --ack-redacted-source consent and the exact --plan-digest printed by security
6217
- plan are required before bounded snippets reach System AI.
6218
- Run security plan first to inspect the admin-selected providers, models,
6219
- per-route bounds, credential readiness, retention, no-execution boundary,
6220
- and digest that binds consent to that exact plan.
6221
- `);
6222
- }
6223
-
6224
- // src/code-local-source.ts
6225
- import { execFile as execFile3 } from "child_process";
6226
- import { createHash as createHash4 } from "crypto";
6227
- var SOURCE_LIMITS2 = { maxFiles: 2e4, maxBytes: 512 * 1024 * 1024 };
6228
- async function prepareCodeLocalSource(cwd, repository, readHead = readGitHead) {
6229
- const headCommitSha = await readHead(cwd);
6230
- const staged = await stageWorkspace(cwd, { ...SOURCE_LIMITS2, gitTrackedAndUnignored: true });
6231
- let trusted;
6232
- try {
6233
- trusted = await materializeGitTree(cwd, headCommitSha, SOURCE_LIMITS2);
6234
- const comparison = await stageWorkspacePair(trusted.sourceDir, staged.baselineDir, SOURCE_LIMITS2);
6235
- let developerPatch;
6236
- try {
6237
- developerPatch = await comparison.patch(4 * 1024 * 1024);
6238
- } finally {
6239
- await comparison.cleanup();
6240
- }
6241
- const descriptor2 = {
6242
- kind: "local_checkout",
6243
- repository,
6244
- headCommitSha,
6245
- trustedBaseDigest: await digestStagedWorkspace(trusted.sourceDir, SOURCE_LIMITS2),
6246
- developerPatchDigest: digestText(developerPatch),
6247
- snapshotDigest: await digestStagedWorkspace(staged.baselineDir, SOURCE_LIMITS2),
6248
- modified: developerPatch.length > 0,
6249
- fileCount: staged.fileCount,
6250
- byteCount: staged.byteCount,
6251
- capturedAt: Date.now()
6252
- };
6253
- return {
6254
- descriptor: descriptor2,
6255
- trustedBaseDir: trusted.sourceDir,
6256
- sourceDir: staged.baselineDir,
6257
- cleanup: async () => {
6258
- await Promise.all([staged.cleanup(), trusted.cleanup()]);
6259
- }
6260
- };
6261
- } catch (error) {
6262
- await Promise.all([staged.cleanup(), trusted?.cleanup()]);
6263
- throw error;
6264
- }
6265
- }
6266
- async function readGitHead(cwd) {
6267
- const value2 = await new Promise((accept, reject) => {
6268
- execFile3("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8", maxBuffer: 16384 }, (error, stdout) => {
6269
- if (error) reject(new Error("code connect requires a Git checkout with an initial commit"));
6270
- else accept(stdout.trim());
6271
- });
6272
- });
6273
- if (!/^[0-9a-f]{40}$/.test(value2)) throw new Error("code connect could not resolve the checkout HEAD commit");
6274
- return value2;
6275
- }
6276
- function digestText(value2) {
6277
- return `sha256:${createHash4("sha256").update(value2).digest("hex")}`;
6278
- }
6505
+ // src/code-local-source.ts
6506
+ import { execFile as execFile3 } from "child_process";
6507
+ import { createHash as createHash4 } from "crypto";
6508
+ var SOURCE_LIMITS2 = { maxFiles: 2e4, maxBytes: 512 * 1024 * 1024 };
6509
+ async function prepareCodeLocalSource(cwd, repository, readHead = readGitHead) {
6510
+ const headCommitSha = await readHead(cwd);
6511
+ const staged = await stageWorkspace(cwd, { ...SOURCE_LIMITS2, gitTrackedAndUnignored: true });
6512
+ let trusted;
6513
+ try {
6514
+ trusted = await materializeGitTree(cwd, headCommitSha, SOURCE_LIMITS2);
6515
+ const comparison = await stageWorkspacePair(trusted.sourceDir, staged.baselineDir, SOURCE_LIMITS2);
6516
+ let developerPatch;
6517
+ try {
6518
+ developerPatch = await comparison.patch(4 * 1024 * 1024);
6519
+ } finally {
6520
+ await comparison.cleanup();
6521
+ }
6522
+ const descriptor2 = {
6523
+ kind: "local_checkout",
6524
+ repository,
6525
+ headCommitSha,
6526
+ trustedBaseDigest: await digestStagedWorkspace(trusted.sourceDir, SOURCE_LIMITS2),
6527
+ developerPatchDigest: digestText(developerPatch),
6528
+ snapshotDigest: await digestStagedWorkspace(staged.baselineDir, SOURCE_LIMITS2),
6529
+ modified: developerPatch.length > 0,
6530
+ fileCount: staged.fileCount,
6531
+ byteCount: staged.byteCount,
6532
+ capturedAt: Date.now()
6533
+ };
6534
+ return {
6535
+ descriptor: descriptor2,
6536
+ trustedBaseDir: trusted.sourceDir,
6537
+ sourceDir: staged.baselineDir,
6538
+ cleanup: async () => {
6539
+ await Promise.all([staged.cleanup(), trusted.cleanup()]);
6540
+ }
6541
+ };
6542
+ } catch (error) {
6543
+ await Promise.all([staged.cleanup(), trusted?.cleanup()]);
6544
+ throw error;
6545
+ }
6546
+ }
6547
+ async function readGitHead(cwd) {
6548
+ const value2 = await new Promise((accept, reject) => {
6549
+ execFile3("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8", maxBuffer: 16384 }, (error, stdout) => {
6550
+ if (error) reject(new Error("code connect requires a Git checkout with an initial commit"));
6551
+ else accept(stdout.trim());
6552
+ });
6553
+ });
6554
+ if (!/^[0-9a-f]{40}$/.test(value2)) throw new Error("code connect could not resolve the checkout HEAD commit");
6555
+ return value2;
6556
+ }
6557
+ function digestText(value2) {
6558
+ return `sha256:${createHash4("sha256").update(value2).digest("hex")}`;
6559
+ }
6279
6560
 
6280
6561
  // src/code-connect.ts
6281
6562
  async function codeConnect(options) {
@@ -6448,16 +6729,16 @@ function parseConnection(value2, appId, appEnv) {
6448
6729
  }
6449
6730
  return root;
6450
6731
  }
6451
- function apiFailure(action, status, value2) {
6732
+ function apiFailure(action2, status, value2) {
6452
6733
  const message2 = record4(record4(value2)?.error)?.message;
6453
- return `${action} failed (${status})${typeof message2 === "string" ? `: ${message2}` : ""}`;
6734
+ return `${action2} failed (${status})${typeof message2 === "string" ? `: ${message2}` : ""}`;
6454
6735
  }
6455
6736
  function record4(value2) {
6456
6737
  return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
6457
6738
  }
6458
6739
 
6459
6740
  // src/provision.ts
6460
- import { createAppsClient, orderAppServices, tenantIdFor as tenantIdFor4 } from "@odla-ai/apps";
6741
+ import { createAppsClient as createAppsClient2, orderAppServices as orderAppServices3, tenantIdFor as tenantIdFor4 } from "@odla-ai/apps";
6461
6742
  import { putSecret as putSecret2 } from "@odla-ai/ai";
6462
6743
  import process8 from "process";
6463
6744
 
@@ -6467,7 +6748,7 @@ async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, int
6467
6748
  const base = `${endpoint}/app/${encodeURIComponent(tenantId)}`;
6468
6749
  for (const integration of integrations) {
6469
6750
  for (const seed of integration.seeds ?? []) {
6470
- const payload = await postJson2(doFetch, `${base}/query`, dbKey, {
6751
+ const payload = await postJson3(doFetch, `${base}/query`, dbKey, {
6471
6752
  query: { [seed.ns]: { $: { where: { [seed.key.attr]: seed.key.value }, limit: 1 } } }
6472
6753
  });
6473
6754
  const rows = isRecord7(payload) && isRecord7(payload.result) ? payload.result[seed.ns] : void 0;
@@ -6478,7 +6759,7 @@ async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, int
6478
6759
  out.log(`${env}: integration ${integration.id} seed ${seed.id} already exists`);
6479
6760
  continue;
6480
6761
  }
6481
- await postJson2(doFetch, `${base}/transact`, dbKey, {
6762
+ await postJson3(doFetch, `${base}/transact`, dbKey, {
6482
6763
  mutationId: `integration:${integration.id}:seed:${seed.id}`,
6483
6764
  ops: [{
6484
6765
  t: "update",
@@ -6493,7 +6774,7 @@ async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, int
6493
6774
  }
6494
6775
  }
6495
6776
  }
6496
- async function postJson2(doFetch, url, bearer, body) {
6777
+ async function postJson3(doFetch, url, bearer, body) {
6497
6778
  const res = await doFetch(url, {
6498
6779
  method: "POST",
6499
6780
  headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
@@ -6514,9 +6795,9 @@ async function responseText(res) {
6514
6795
  }
6515
6796
 
6516
6797
  // src/provision-credentials.ts
6517
- import { tenantIdFor as tenantIdFor2 } from "@odla-ai/apps";
6798
+ import { tenantIdFor as tenantIdFor3 } from "@odla-ai/apps";
6518
6799
  async function provisionEnvCredentials(opts) {
6519
- const tenantId = tenantIdFor2(opts.cfg.app.id, opts.env);
6800
+ const tenantId = tenantIdFor3(opts.cfg.app.id, opts.env);
6520
6801
  const prior = opts.credentials?.envs[opts.env];
6521
6802
  let credentials = opts.credentials;
6522
6803
  let dbKey = opts.cfg.services.includes("db") && !opts.rotateDb ? prior?.dbKey : void 0;
@@ -6572,14 +6853,14 @@ async function mintDbKey(opts, tenantId) {
6572
6853
  appId: tenantId
6573
6854
  })
6574
6855
  });
6575
- if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText4(created)}`);
6856
+ if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText5(created)}`);
6576
6857
  res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/keys`, {
6577
6858
  method: "POST",
6578
6859
  headers,
6579
6860
  body: "{}"
6580
6861
  });
6581
6862
  }
6582
- if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText4(res)}`);
6863
+ if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText5(res)}`);
6583
6864
  const body = await res.json();
6584
6865
  if (!body.key) throw new Error(`db key mint (${tenantId}) returned no key`);
6585
6866
  return body.key;
@@ -6595,58 +6876,11 @@ async function issueO11yToken(opts) {
6595
6876
  `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`
6596
6877
  );
6597
6878
  }
6598
- if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText4(res)}`);
6879
+ if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText5(res)}`);
6599
6880
  const body = await res.json();
6600
6881
  if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
6601
6882
  return body.token;
6602
6883
  }
6603
- async function safeText4(res) {
6604
- try {
6605
- return redactSecrets((await res.text()).slice(0, 500));
6606
- } catch {
6607
- return "";
6608
- }
6609
- }
6610
-
6611
- // src/provision-helpers.ts
6612
- import { DEFAULT_SECRET_NAMES } from "@odla-ai/ai";
6613
- import { tenantIdFor as tenantIdFor3 } from "@odla-ai/apps";
6614
- function defaultSecretName(provider) {
6615
- const names = DEFAULT_SECRET_NAMES;
6616
- return names[provider] ?? `${provider}_api_key`;
6617
- }
6618
- async function assertTenantAdminAccess(doFetch, cfg, env, token) {
6619
- const tenantId = tenantIdFor3(cfg.app.id, env);
6620
- const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/entitlements`, {
6621
- headers: { authorization: `Bearer ${token}` }
6622
- });
6623
- if (res.ok || res.status === 404) return;
6624
- if (res.status === 403) {
6625
- throw new Error(
6626
- `${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`
6627
- );
6628
- }
6629
- throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText5(res)}`);
6630
- }
6631
- async function postJson3(doFetch, url, bearer, body) {
6632
- const res = await doFetch(url, {
6633
- method: "POST",
6634
- headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
6635
- body: JSON.stringify(body)
6636
- });
6637
- if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText5(res)}`);
6638
- }
6639
- function normalizeClerkConfig(value2) {
6640
- if (!value2) return null;
6641
- if (typeof value2 === "string") {
6642
- const publishableKey2 = envValue(value2);
6643
- return publishableKey2 ? { publishableKey: publishableKey2 } : null;
6644
- }
6645
- if (typeof value2 !== "object") return null;
6646
- const cfg = value2;
6647
- const publishableKey = envValue(cfg.publishableKey);
6648
- return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
6649
- }
6650
6884
  async function safeText5(res) {
6651
6885
  try {
6652
6886
  return redactSecrets((await res.text()).slice(0, 500));
@@ -6725,7 +6959,7 @@ async function provision(options) {
6725
6959
  }
6726
6960
  const doFetch = options.fetch ?? fetch;
6727
6961
  const token = await getDeveloperToken(cfg, options, doFetch, out);
6728
- const apps = createAppsClient({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
6962
+ const apps = createAppsClient2({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
6729
6963
  const existing = await apps.resolveApp(cfg.app.id);
6730
6964
  if (existing) {
6731
6965
  out.log(`app: ${cfg.app.id} already exists`);
@@ -6736,7 +6970,7 @@ async function provision(options) {
6736
6970
  for (const env of cfg.envs) {
6737
6971
  await assertTenantAdminAccess(doFetch, cfg, env, token);
6738
6972
  }
6739
- const serviceOrder = orderAppServices(cfg.services);
6973
+ const serviceOrder = orderAppServices3(cfg.services);
6740
6974
  for (const env of cfg.envs) {
6741
6975
  for (const service of serviceOrder) {
6742
6976
  if (service === "ai") {
@@ -6802,11 +7036,11 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
6802
7036
  }
6803
7037
  }
6804
7038
  if (schema && dbKey) {
6805
- await postJson3(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
7039
+ await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
6806
7040
  out.log(`${env}: schema pushed`);
6807
7041
  }
6808
7042
  if (rules) {
6809
- await postJson3(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/admin/rules`, token, rules);
7043
+ await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/admin/rules`, token, rules);
6810
7044
  out.log(`${env}: rules pushed (${Object.keys(rules).length} namespaces)`);
6811
7045
  }
6812
7046
  if (dbKey) {
@@ -6899,6 +7133,7 @@ var COMMAND_SURFACE = {
6899
7133
  calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
6900
7134
  capabilities: {},
6901
7135
  code: { connect: {} },
7136
+ config: { diff: {}, plan: {} },
6902
7137
  context: { show: {}, list: {}, save: {}, remove: {} },
6903
7138
  // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
6904
7139
  discuss: {
@@ -7353,17 +7588,17 @@ function addOption(options, name, value2) {
7353
7588
 
7354
7589
  // src/operator-context.ts
7355
7590
  import { existsSync as existsSync10 } from "fs";
7356
- import { join as join10, resolve as resolve11 } from "path";
7591
+ import { join as join11, resolve as resolve11 } from "path";
7357
7592
  import process10 from "process";
7358
7593
 
7359
7594
  // src/operator-profiles.ts
7360
7595
  import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
7361
7596
  import { homedir as homedir2 } from "os";
7362
- import { dirname as dirname7, join as join9, resolve as resolve10 } from "path";
7597
+ import { dirname as dirname7, join as join10, resolve as resolve10 } from "path";
7363
7598
  import process9 from "process";
7364
7599
  function operatorProfileFile() {
7365
7600
  return resolve10(
7366
- clean(process9.env.ODLA_CONTEXT_FILE) ?? join9(homedir2(), ".odla", "contexts.json")
7601
+ clean(process9.env.ODLA_CONTEXT_FILE) ?? join10(homedir2(), ".odla", "contexts.json")
7367
7602
  );
7368
7603
  }
7369
7604
  function resolveOperatorProfile(parsed) {
@@ -7408,10 +7643,10 @@ function removeOperatorProfile(name, file = operatorProfileFile()) {
7408
7643
  return true;
7409
7644
  }
7410
7645
  function operatorCredentialFiles(selection) {
7411
- const base = selection.name ? join9(dirname7(selection.file), "profiles", selection.name) : join9(homedir2(), ".odla");
7646
+ const base = selection.name ? join10(dirname7(selection.file), "profiles", selection.name) : join10(homedir2(), ".odla");
7412
7647
  return {
7413
- developer: join9(base, "dev-token.json"),
7414
- scoped: join9(base, "admin-token.local.json")
7648
+ developer: join10(base, "dev-token.json"),
7649
+ scoped: join10(base, "admin-token.local.json")
7415
7650
  };
7416
7651
  }
7417
7652
  function assertOperatorName(value2, label) {
@@ -7522,7 +7757,7 @@ async function resolveOperatorContext(parsed, options = {}) {
7522
7757
  const rootDir = loaded?.rootDir ?? process10.cwd();
7523
7758
  const profileCredentials = operatorCredentialFiles(profile);
7524
7759
  const tokenFile = clean2(process10.env.ODLA_DEV_TOKEN_FILE) ? resolve11(process10.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
7525
- const scopedTokenFile = clean2(process10.env.ODLA_ADMIN_TOKEN_FILE) ? resolve11(process10.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? join10(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
7760
+ const scopedTokenFile = clean2(process10.env.ODLA_ADMIN_TOKEN_FILE) ? resolve11(process10.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? join11(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
7526
7761
  const cfg = loaded ? {
7527
7762
  ...loaded,
7528
7763
  platformUrl: platformValue,
@@ -7544,8 +7779,8 @@ async function resolveOperatorContext(parsed, options = {}) {
7544
7779
  services: [],
7545
7780
  local: {
7546
7781
  tokenFile,
7547
- credentialsFile: join10(rootDir, ".odla", "credentials.local.json"),
7548
- devVarsFile: join10(rootDir, ".dev.vars"),
7782
+ credentialsFile: join11(rootDir, ".odla", "credentials.local.json"),
7783
+ devVarsFile: join11(rootDir, ".dev.vars"),
7549
7784
  gitignore: true
7550
7785
  }
7551
7786
  };
@@ -7596,21 +7831,21 @@ var SET_OPTIONS = [
7596
7831
  ];
7597
7832
  async function adminCommand(parsed, deps = {}) {
7598
7833
  const area = parsed.positionals[1];
7599
- const action = parsed.positionals[2];
7600
- const credentialSet = action === "credential" && parsed.positionals[3] === "set";
7601
- const credentials = action === "credentials";
7602
- const models = action === "models";
7603
- const usage = action === "usage";
7604
- const audit = action === "audit";
7605
- if (area !== "ai" || action !== "show" && action !== "set" && !credentialSet && !credentials && !models && !usage && !audit) {
7834
+ const action2 = parsed.positionals[2];
7835
+ const credentialSet = action2 === "credential" && parsed.positionals[3] === "set";
7836
+ const credentials = action2 === "credentials";
7837
+ const models = action2 === "models";
7838
+ const usage = action2 === "usage";
7839
+ const audit = action2 === "audit";
7840
+ if (area !== "ai" || action2 !== "show" && action2 !== "set" && !credentialSet && !credentials && !models && !usage && !audit) {
7606
7841
  throw new Error('unknown admin command. Try "odla-ai admin ai show".');
7607
7842
  }
7608
- const allowed = credentialSet ? [...CONTEXT_OPTIONS, "from-env", "stdin"] : action === "set" ? SET_OPTIONS : models ? [...JSON_OPTIONS, "provider"] : usage ? [...JSON_OPTIONS, "app-id", "env", "run-id", "limit"] : audit ? [...JSON_OPTIONS, "limit"] : JSON_OPTIONS;
7609
- assertArgs(parsed, allowed, credentialSet ? 5 : action === "set" ? 4 : 3);
7843
+ 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;
7844
+ assertArgs(parsed, allowed, credentialSet ? 5 : action2 === "set" ? 4 : 3);
7610
7845
  const context = await resolveOperatorContext(parsed, { allowMissingConfig: true });
7611
7846
  await adminAi({
7612
- action: credentialSet ? "credential-set" : credentials ? "credentials" : models ? "models" : usage ? "usage" : audit ? "audit" : action,
7613
- purpose: action === "set" ? parsed.positionals[3] : void 0,
7847
+ action: credentialSet ? "credential-set" : credentials ? "credentials" : models ? "models" : usage ? "usage" : audit ? "audit" : action2,
7848
+ purpose: action2 === "set" ? parsed.positionals[3] : void 0,
7614
7849
  provider: stringOpt(parsed.options.provider),
7615
7850
  model: stringOpt(parsed.options.model),
7616
7851
  enabled: boolOpt(parsed.options.enabled),
@@ -7667,12 +7902,12 @@ function bothTenants(cfg) {
7667
7902
 
7668
7903
  // src/agent-command.ts
7669
7904
  async function agentCommand(parsed, deps = {}) {
7670
- const action = parsed.positionals[1];
7671
- if (action !== "jobs" && action !== "retry") {
7672
- throw new Error(`unknown agent action "${action ?? ""}". Try "odla-ai agent jobs --json".`);
7905
+ const action2 = parsed.positionals[1];
7906
+ if (action2 !== "jobs" && action2 !== "retry") {
7907
+ throw new Error(`unknown agent action "${action2 ?? ""}". Try "odla-ai agent jobs --json".`);
7673
7908
  }
7674
- assertArgs(parsed, ["config", "env", "state", "limit", "json", "token", "email"], action === "jobs" ? 2 : 3);
7675
- if (action === "retry" && (parsed.options.state !== void 0 || parsed.options.limit !== void 0)) {
7909
+ assertArgs(parsed, ["config", "env", "state", "limit", "json", "token", "email"], action2 === "jobs" ? 2 : 3);
7910
+ if (action2 === "retry" && (parsed.options.state !== void 0 || parsed.options.limit !== void 0)) {
7676
7911
  throw new Error('--state and --limit are supported only by "agent jobs"');
7677
7912
  }
7678
7913
  const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
@@ -7692,7 +7927,7 @@ async function agentCommand(parsed, deps = {}) {
7692
7927
  );
7693
7928
  const base = `${cfg.dbEndpoint}/app/${encodeURIComponent(tenant)}/admin/agent-jobs`;
7694
7929
  const headers = { authorization: `Bearer ${credential2}` };
7695
- if (action === "retry") {
7930
+ if (action2 === "retry") {
7696
7931
  const id = parsed.positionals[2];
7697
7932
  const res2 = await doFetch(`${base}/${encodeURIComponent(id)}/retry`, { method: "POST", headers });
7698
7933
  const body2 = await readJson(res2);
@@ -8025,7 +8260,7 @@ function describe(side) {
8025
8260
  const identity = side.identityRows > 0 ? `, ${side.identityRows} identity row(s)` : "";
8026
8261
  return `${ns} \xB7 ${side.triples} value(s) \xB7 tx ${side.maxTx}${identity}`;
8027
8262
  }
8028
- function printPlan(out, verb, pre, opts) {
8263
+ function printPlan2(out, verb, pre, opts) {
8029
8264
  const arrow = verb === "refresh-sandbox" ? "live \u2192 sandbox" : "sandbox \u2192 live";
8030
8265
  out.log(`${verb} (${arrow})`);
8031
8266
  out.log(` from ${pre.source.tenant} ${describe(pre.source)}`);
@@ -8065,7 +8300,7 @@ async function appTransfer(options) {
8065
8300
  throw new Error(`pre-flight failed${pre.body.error?.code ? ` (${pre.body.error.code})` : ""}: ${pre.body.error?.message ?? pre.status}`);
8066
8301
  }
8067
8302
  const plan = pre.body;
8068
- printPlan({ log: say }, options.verb, plan, options);
8303
+ printPlan2({ log: say }, options.verb, plan, options);
8069
8304
  if (options.verb === "go-live" && !plan.targetEmpty) {
8070
8305
  throw new Error(
8071
8306
  `${route2.target} already has data \u2014 go-live has already happened. Use \`odla-ai app promote --yes\` to push schema and rules, or \`odla-ai app refresh-sandbox --yes\` to pull live back down.`
@@ -8133,7 +8368,7 @@ async function copyFiles(cfg, token, route2, doFetch, out) {
8133
8368
  }
8134
8369
 
8135
8370
  // src/app-lifecycle.ts
8136
- async function lifecycleCall(action, options) {
8371
+ async function lifecycleCall(action2, options) {
8137
8372
  const cfg = await loadProjectConfig(options.configPath);
8138
8373
  const out = options.stdout ?? console;
8139
8374
  const doFetch = options.fetch ?? fetch;
@@ -8143,13 +8378,13 @@ async function lifecycleCall(action, options) {
8143
8378
  doFetch,
8144
8379
  out
8145
8380
  );
8146
- const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/${action}`, {
8381
+ const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/${action2}`, {
8147
8382
  method: "POST",
8148
8383
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }
8149
8384
  });
8150
8385
  const body = await res.json().catch(() => ({}));
8151
8386
  if (!res.ok || !body.ok) {
8152
- throw new Error(`${action} failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? `registry returned ${res.status}`}`);
8387
+ throw new Error(`${action2} failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? `registry returned ${res.status}`}`);
8153
8388
  }
8154
8389
  return body;
8155
8390
  }
@@ -8322,6 +8557,26 @@ async function secretsCommand(parsed, deps) {
8322
8557
  });
8323
8558
  }
8324
8559
  async function projectCommand(command, parsed, deps) {
8560
+ if (command === "config") {
8561
+ const sub = parsed.positionals[1];
8562
+ if (sub !== "diff" && sub !== "plan") {
8563
+ throw new Error(`unknown config subcommand "${sub ?? ""}". Try "odla-ai config diff --json".`);
8564
+ }
8565
+ assertArgs(parsed, ["config", "token", "email", "open", "json"], 2);
8566
+ const options = {
8567
+ configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
8568
+ token: stringOpt(parsed.options.token),
8569
+ email: stringOpt(parsed.options.email),
8570
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
8571
+ json: parsed.options.json === true,
8572
+ fetch: deps.fetch,
8573
+ openApprovalUrl: deps.openUrl,
8574
+ stdout: deps.stdout
8575
+ };
8576
+ if (sub === "diff") await configDiff(options);
8577
+ else await configPlan(options);
8578
+ return true;
8579
+ }
8325
8580
  if (command === "init") {
8326
8581
  assertArgs(parsed, ["app-id", "name", "config", "env", "services", "ai-provider", "force"], 1);
8327
8582
  initProject({
@@ -8456,9 +8711,9 @@ async function contextCommand(parsed, deps = {}) {
8456
8711
  ],
8457
8712
  3
8458
8713
  );
8459
- const action = parsed.positionals[1];
8714
+ const action2 = parsed.positionals[1];
8460
8715
  const out = deps.stdout ?? console;
8461
- if (action === "list") {
8716
+ if (action2 === "list") {
8462
8717
  const profiles = listOperatorProfiles();
8463
8718
  if (parsed.options.json === true) {
8464
8719
  out.log(JSON.stringify({ schemaVersion: 1, profiles }, null, 2));
@@ -8476,7 +8731,7 @@ async function contextCommand(parsed, deps = {}) {
8476
8731
  }
8477
8732
  return;
8478
8733
  }
8479
- if (action === "save") {
8734
+ if (action2 === "save") {
8480
8735
  if (parsed.options.token !== void 0) {
8481
8736
  throw new Error(
8482
8737
  "context save does not accept --token; contexts store scope metadata only"
@@ -8499,7 +8754,7 @@ async function contextCommand(parsed, deps = {}) {
8499
8754
  }
8500
8755
  return;
8501
8756
  }
8502
- if (action === "remove") {
8757
+ if (action2 === "remove") {
8503
8758
  const name = requireName(parsed);
8504
8759
  if (parsed.options.yes !== true) {
8505
8760
  throw new Error(`context remove "${name}" requires --yes`);
@@ -8519,9 +8774,9 @@ async function contextCommand(parsed, deps = {}) {
8519
8774
  }
8520
8775
  return;
8521
8776
  }
8522
- if (action !== "show") {
8777
+ if (action2 !== "show") {
8523
8778
  throw new Error(
8524
- `unknown context action "${action ?? ""}". Try show|list|save|remove.`
8779
+ `unknown context action "${action2 ?? ""}". Try show|list|save|remove.`
8525
8780
  );
8526
8781
  }
8527
8782
  const context = await resolveOperatorContext(parsed, {
@@ -8572,6 +8827,261 @@ function requireName(parsed) {
8572
8827
  return name;
8573
8828
  }
8574
8829
 
8830
+ // src/help.ts
8831
+ function printHelp(output = console) {
8832
+ output.log(`odla-ai
8833
+
8834
+ Start here:
8835
+ odla-ai runbook ask "<question>" The current procedure, from odla's own
8836
+ runbooks. Ask BEFORE searching the web or
8837
+ working from memory: runbooks are edited
8838
+ live, so your training data is out of date
8839
+ and this is not.
8840
+ odla-ai runbook impact After a change: which runbooks describe the
8841
+ code you just touched, so you can fix any
8842
+ step it made wrong.
8843
+
8844
+ Usage:
8845
+ odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
8846
+ odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
8847
+ odla-ai doctor [--config odla.config.mjs]
8848
+ odla-ai config diff [--config odla.config.mjs] [--email <odla-account>] [--json]
8849
+ odla-ai config plan [--config odla.config.mjs] [--email <odla-account>] [--json]
8850
+ odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
8851
+ odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
8852
+ odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
8853
+ odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
8854
+ odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
8855
+ odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
8856
+ odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
8857
+ odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
8858
+ odla-ai app refresh-sandbox [--include-identity] [--include-files] [--dry-run] [--json] --yes
8859
+ odla-ai app go-live [--include-identity] [--include-files] [--dry-run] [--json] --yes
8860
+ odla-ai app promote [--dry-run] [--json] --yes
8861
+ odla-ai app rename <name> [--config odla.config.mjs] [--email <odla-account>] [--json]
8862
+ odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
8863
+ odla-ai app owners add <email> [--email <odla-account>] [--json]
8864
+ odla-ai app owners remove <email> [--email <odla-account>] [--json]
8865
+ odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8866
+ odla-ai pm task list [--app <id>] [--column <c>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8867
+ odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8868
+ odla-ai pm bug list [--app <id>] [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8869
+ odla-ai pm goal add --app <id> --title <t> [--status <s>] [--proof <text>] [--target <pct>] [--mutation-id <id>] [--json]
8870
+ 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]
8871
+ odla-ai pm decision add --app <id> --title <t> --body <text> [--status <s>] [--mutation-id <id>] [--json]
8872
+ 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]
8873
+ odla-ai pm <goal|task|decision|bug> get <id> [--json]
8874
+ odla-ai pm goal set <id> [--title <t>|--status <s>|--proof <text>|--no-proof|--target <pct>|--no-target] [--mutation-id <id>] [--json]
8875
+ 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]
8876
+ odla-ai pm decision set <id> [--title <t>|--status <s>|--body <text>] [--mutation-id <id>] [--json]
8877
+ 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]
8878
+ odla-ai pm <goal|task|decision> done <id> [--mutation-id <id>]
8879
+ odla-ai pm bug done <id> [--decision <accepted-decision-id>] [--mutation-id <id>]
8880
+ odla-ai pm <goal|task|decision|bug> comment <id> --body "..." [--mutation-id <id>]
8881
+ odla-ai pm <goal|task|decision|bug> comments <id> [--json]
8882
+ odla-ai pm <goal|task|decision|bug> rm <id>
8883
+ odla-ai pm handoff --app <id> [--json]
8884
+ odla-ai discuss groups [--json]
8885
+ odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
8886
+ odla-ai discuss read <topic> [--limit <n> --offset <n>] [--json]
8887
+ odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"] [--mutation-id <id>]
8888
+ odla-ai discuss reply <topic> --body "..." [--markup "..."] [--mutation-id <id>]
8889
+ odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
8890
+ odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
8891
+ odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
8892
+ odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
8893
+ odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
8894
+ odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
8895
+ odla-ai context list [--json]
8896
+ odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
8897
+ odla-ai context remove <name> --yes [--json]
8898
+ odla-ai o11y status [--app <id>] [--context <name>] [--platform https://odla.ai] [--env prod] [--minutes 60] [--json]
8899
+ odla-ai platform status [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
8900
+ odla-ai whoami [--context <name>] [--platform https://odla.ai] [--json]
8901
+ odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
8902
+ odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
8903
+ odla-ai runbook impact [--base origin/main] [--app <id>] [--all] [--limit <n>] [--json]
8904
+ odla-ai runbook lint [--app <id>] [--all] [--json]
8905
+ odla-ai runbook list [--app <id>] [--all] [--q <text>] [--json]
8906
+ odla-ai runbook comment <slug> --body "..." [--app <id>]
8907
+ odla-ai runbook get <slug> [--app <id>]
8908
+ odla-ai runbook new <slug> --title <t> --file <path|-> [--summary <s>] [--requires <specs>] [--app <id>]
8909
+ odla-ai runbook edit <slug> [--file <path|->|--body "..."] [--note <why>] [--requires <specs>] [--app <id>]
8910
+ odla-ai runbook import <dir> [--visibility operator|admin] [--dry-run] [--app <id>]
8911
+ odla-ai runbook publish <slug> [--app <id>]
8912
+ odla-ai runbook visibility <slug> <operator|admin> [--app <id>]
8913
+ odla-ai runbook archive <slug> [--app <id>]
8914
+ odla-ai runbook history <slug> [--app <id>] [--json]
8915
+ odla-ai runbook revert <slug> --version <n> [--app <id>]
8916
+ odla-ai runbook rm <slug> [--app <id>]
8917
+ odla-ai capabilities [--json]
8918
+ odla-ai code connect [--env dev|prod] [--email <odla-account>] [--engine auto|container|podman|docker] [--slots <1-64>] [--once]
8919
+ odla-ai admin ai show [--context <name>] [--platform https://odla.ai] [--email <odla-account>] [--json]
8920
+ odla-ai admin ai models [--context <name>] [--provider <id>] [--json]
8921
+ odla-ai admin ai set <purpose> [--context <name>] [--provider <id>] [--model <id>] [--enabled|--no-enabled]
8922
+ [--max-input-bytes <n>] [--max-output-tokens <n>] [--max-calls-per-run <n>] [--json]
8923
+ odla-ai admin ai set security [--context <name>] --discovery-provider <id> --discovery-model <id>
8924
+ --validation-provider <id> --validation-model <id> [--enabled|--no-enabled] [--json]
8925
+ odla-ai admin ai credentials [--context <name>] [--json]
8926
+ odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
8927
+ odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
8928
+ odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
8929
+ odla-ai security github connect [--repo owner/name] [--env dev] [--email <odla-account>] [--no-open]
8930
+ odla-ai security github disconnect --source <id> [--env dev] [--yes]
8931
+ odla-ai security plan [--env dev] [--json]
8932
+ odla-ai security sources [--env dev] [--json]
8933
+ odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
8934
+ odla-ai security status <job-id> [--follow] [--json]
8935
+ odla-ai security report <job-id> [--json]
8936
+ odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
8937
+ odla-ai security run [target] --self --ack-redacted-source
8938
+ odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
8939
+ odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
8940
+ odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
8941
+ odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
8942
+ odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
8943
+ odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
8944
+ odla-ai version
8945
+
8946
+ Commands:
8947
+ agent Inspect durable agent wakeups and explicitly requeue a
8948
+ dead-lettered job; JSON output is stable for remote operators.
8949
+ runbook odla's operational procedures, stored in the database and read at
8950
+ the moment they are followed. "ask" gives a written, cited answer;
8951
+ "search" the passages behind it; "get" the whole document.
8952
+ "impact" diffs your working tree against a base ref and names the
8953
+ runbooks that describe what you changed \u2014 run it after touching a
8954
+ package's exported API or JSDoc, or a Studio surface. "lint"
8955
+ checks the corpus the other way: every odla-ai command the
8956
+ runbooks name is held against this CLI's real command surface,
8957
+ and against the minimum versions each runbook declares. A wrong step
8958
+ is fixed with "edit", which takes effect immediately: a runbook is
8959
+ a row, not a release. Runbooks describe PROCEDURE; what an export
8960
+ does and what it guarantees is JSDoc, rendered per package at
8961
+ https://odla.ai/docs and shipped in the installed .d.ts. Answering
8962
+ a question usually needs both.
8963
+ whoami Report who this terminal is authenticated as, and whether it holds
8964
+ platform admin. A handshake-minted device token is never admin,
8965
+ however the human who approved it is configured.
8966
+ context Explain selected config, platform, app, environment, and
8967
+ developer-token provenance without printing credentials or
8968
+ starting a device handshake. Operator work can run outside a
8969
+ project checkout with explicit flags or ODLA_* environment
8970
+ variables.
8971
+ setup Install offline odla runbooks for common coding-agent harnesses.
8972
+ init Create a generic odla.config.mjs plus starter schema/rules files.
8973
+ doctor Validate and summarize the project config without network calls.
8974
+ config Read-only diff and revision-bound plan for checked-in Registry
8975
+ intent; runtime-owned fields and excluded coverage stay explicit.
8976
+ calendar Inspect, connect, or disconnect the live Google booking connection.
8977
+ app Archive (suspend, data retained), restore, export, import, or
8978
+ manage the co-owners of the app. Archiving takes every
8979
+ environment's data plane down until restored; permanent deletion
8980
+ has NO CLI \u2014 it requires a signed-in owner in Studio, and no
8981
+ machine or agent credential can purge. "app export" downloads a
8982
+ portable gzipped-JSONL snapshot of one database (--fresh takes a
8983
+ new snapshot first) \u2014 your data is always yours to take.
8984
+ "app import" upserts rows back in from JSON, JSONL, or a
8985
+ {namespace: rows} map (the shape a query returns); it writes only
8986
+ with --yes, so the bare command is a dry run, and it refuses to
8987
+ guess an id mode that would duplicate rows on a re-run.
8988
+ "app refresh-sandbox" replaces the sandbox with a copy of live
8989
+ (the routine dev loop); "app go-live" copies the sandbox into an
8990
+ EMPTY live database, once, at launch; "app promote" pushes only
8991
+ schema, rules and gates up afterwards, leaving live's rows alone.
8992
+ Each prints its plan and writes nothing without --yes, so the
8993
+ bare command is the dry run. Clerk config, triggers, allowlists,
8994
+ secrets and API keys never travel; identity rows stay behind
8995
+ unless --include-identity.
8996
+ "app rename <name>" changes only the display name humans read;
8997
+ the app id is permanent (tenants, URLs and keys embed it), so a
8998
+ rename never re-provisions anything or invalidates a credential.
8999
+ "app owners add <email>" grants a signed-up odla member the SAME
9000
+ full access as you; they then run their own "provision" to mint
9001
+ their own credentials for the shared db (the live one included)
9002
+ \u2014 no secret is ever copied between people.
9003
+ capabilities Show what the CLI automates vs agent edits and human checkpoints.
9004
+ code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
9005
+ admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
9006
+ security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
9007
+ pm Project management (via @odla-ai/pm) shared across the apps you
9008
+ co-own: conformance goals, kanban tasks, decisions, and bugs. With
9009
+ no --app, "list" spans every co-owned project (the cross-project
9010
+ view); with --app it scopes to one. Same device-grant auth as
9011
+ "app". Entities: goal (alias conformance), task (alias kanban),
9012
+ decision, bug. Status changes and comments post to each item's
9013
+ @odla-ai/chat discussion thread.
9014
+ discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
9015
+ one group per project, topics with replies, @-mentions of people,
9016
+ agents, PM items, and projects. Built for unattended use \u2014 post a
9017
+ question, then "watch" it until someone answers. "watch" exits 75
9018
+ (not 1) when it times out with nothing new, so a script can tell
9019
+ "no answer yet" from a failure and just rerun. Use "who" to look
9020
+ up the exact @[Label](kind/id) markup for a mention.
9021
+ o11y Read one stable status envelope for application RED, current
9022
+ live-sync load/freshness, the protected commit-to-visible
9023
+ canary, collector ingest/scheduler trust, Cloudflare-owned
9024
+ runtime metrics, and a machine verdict.
9025
+ --json keeps auth progress on stderr for unattended agents.
9026
+ platform Read canonical fleet health, releases, provider load/freshness,
9027
+ explicit unknowns, and next actions through a read-only grant.
9028
+ provision Register services, compose integrations, persist credentials, optionally push secrets.
9029
+ smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
9030
+ skill Same installer; --agent accepts all, claude, codex, cursor,
9031
+ copilot, gemini, or agents (repeatable or comma-separated).
9032
+ secrets Push configured db/o11y secrets into the Worker via wrangler
9033
+ stdin; set stores a tenant-vault secret and set-clerk-key the
9034
+ reserved Clerk secret key, write-only from stdin or an env var.
9035
+ version Print the CLI version.
9036
+
9037
+ Safety:
9038
+ Every app has two databases on odla.ai: a sandbox (env "dev", tenant
9039
+ <appId>--dev) and a live one (env "prod", tenant <appId>). Both are served by
9040
+ production odla.ai \u2014 there is no separate odla to point at. New projects get
9041
+ the sandbox only. Add "prod" explicitly to odla.config.mjs and pass --yes to
9042
+ provision the live database; use --dry-run first to inspect the resolved plan.
9043
+ Provision caches the approved developer token and service credentials under
9044
+ .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
9045
+ preflights Wrangler before any shown-once issuance or destructive rotation.
9046
+ Projectless PM, Discussions, o11y, runbook, and identity commands use
9047
+ --platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
9048
+ ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
9049
+ select it explicitly with --context or ODLA_CONTEXT. Each named context gets
9050
+ isolated developer and scoped-token caches. Override their locations with
9051
+ ODLA_DEV_TOKEN_FILE and ODLA_ADMIN_TOKEN_FILE; ODLA_CONTEXT_FILE relocates
9052
+ the metadata file. Flags and specific ODLA_* scope variables beat a selected
9053
+ context, which beats project config. There is no ambient current context.
9054
+ "context show" reports only provenance and cache state and never authenticates.
9055
+ Provision opens the approval page in your browser automatically whenever the
9056
+ machine can show one, including agent-driven runs; only CI, SSH, and
9057
+ display-less hosts skip it. Use --open to force or --no-open to suppress.
9058
+ Browser launch is best-effort: the printed approval URL is authoritative and
9059
+ agents must relay it to the human verbatim. A started handshake is persisted
9060
+ under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
9061
+ the same code. Outside an interactive terminal the wait is capped (90s by
9062
+ default, --wait <seconds> to change); a still-pending handshake then exits
9063
+ with code 75: relay the URL, wait for approval, and re-run to collect.
9064
+ A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
9065
+ The email is a non-secret identity hint: never provide a password or session
9066
+ token. The matching account must already exist, be signed in, explicitly
9067
+ review the exact code, and finish any current request before claiming another.
9068
+ Run Code from a GitHub checkout already connected to an app in Studio; an
9069
+ odla.config.mjs may select the app explicitly but is not required. Code host
9070
+ approval and credential hashes live in odla-ai/db. The host
9071
+ credential is never written under .odla/; it exists only in the foreground
9072
+ "code connect" process and is rotated by the next approved connection.
9073
+ Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
9074
+ OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
9075
+ GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
9076
+ for a PAT or provider key. GitHub read access is separate from the explicit
9077
+ --ack-redacted-source consent and the exact --plan-digest printed by security
9078
+ plan are required before bounded snippets reach System AI.
9079
+ Run security plan first to inspect the admin-selected providers, models,
9080
+ per-route bounds, credential readiness, retention, no-execution boundary,
9081
+ and digest that binds consent to that exact plan.
9082
+ `);
9083
+ }
9084
+
8575
9085
  // src/discuss-actions.ts
8576
9086
  var writeMutationId = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
8577
9087
  async function request(ctx, method, path, body) {
@@ -8993,8 +9503,8 @@ var ALLOWED = [
8993
9503
  "platform",
8994
9504
  "context"
8995
9505
  ];
8996
- function requireId(id, action) {
8997
- if (!id) throw new Error(`"discuss ${action}" needs a topic id`);
9506
+ function requireId(id, action2) {
9507
+ if (!id) throw new Error(`"discuss ${action2}" needs a topic id`);
8998
9508
  return id;
8999
9509
  }
9000
9510
  async function buildContext(parsed, deps) {
@@ -9028,11 +9538,11 @@ async function buildContext(parsed, deps) {
9028
9538
  }
9029
9539
  async function discussCommand(parsed, deps = {}) {
9030
9540
  assertArgs(parsed, ALLOWED, 3);
9031
- const action = parsed.positionals[1];
9541
+ const action2 = parsed.positionals[1];
9032
9542
  const id = parsed.positionals[2];
9033
- if (!action) throw new Error('"discuss" needs an action. Run "odla-ai help".');
9543
+ if (!action2) throw new Error('"discuss" needs an action. Run "odla-ai help".');
9034
9544
  const ctx = await buildContext(parsed, deps);
9035
- switch (action) {
9545
+ switch (action2) {
9036
9546
  case "groups":
9037
9547
  return discussGroups(ctx);
9038
9548
  case "list":
@@ -9054,7 +9564,7 @@ async function discussCommand(parsed, deps = {}) {
9054
9564
  return;
9055
9565
  }
9056
9566
  default:
9057
- throw new Error(`unknown discuss action "${action}". Run "odla-ai help".`);
9567
+ throw new Error(`unknown discuss action "${action2}". Run "odla-ai help".`);
9058
9568
  }
9059
9569
  }
9060
9570
 
@@ -9315,18 +9825,18 @@ var ENTITY_OPTIONS = {
9315
9825
  done: ["decision"]
9316
9826
  }
9317
9827
  };
9318
- function canonicalAction(action) {
9319
- if (action === "create") return "add";
9320
- if (action === "update" || action === "status" || action === "move") return "set";
9321
- if (action === "delete") return "rm";
9322
- return action in ACTION_OPTIONS ? action : null;
9323
- }
9324
- function allowedOptions(entity, action) {
9325
- const entityOptions = action === "list" || action === "add" || action === "set" || action === "done" ? ENTITY_OPTIONS[entity][action] : [];
9326
- return [...COMMON_OPTIONS, ...ACTION_OPTIONS[action], ...entityOptions];
9327
- }
9328
- function requireId2(id, action) {
9329
- if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
9828
+ function canonicalAction(action2) {
9829
+ if (action2 === "create") return "add";
9830
+ if (action2 === "update" || action2 === "status" || action2 === "move") return "set";
9831
+ if (action2 === "delete") return "rm";
9832
+ return action2 in ACTION_OPTIONS ? action2 : null;
9833
+ }
9834
+ function allowedOptions(entity, action2) {
9835
+ const entityOptions = action2 === "list" || action2 === "add" || action2 === "set" || action2 === "done" ? ENTITY_OPTIONS[entity][action2] : [];
9836
+ return [...COMMON_OPTIONS, ...ACTION_OPTIONS[action2], ...entityOptions];
9837
+ }
9838
+ function requireId2(id, action2) {
9839
+ if (!id) throw new Error(`"pm ... ${action2}" needs an item id`);
9330
9840
  return id;
9331
9841
  }
9332
9842
  async function buildContext2(parsed, deps) {
@@ -9362,28 +9872,28 @@ async function pmCommand(parsed, deps = {}) {
9362
9872
  const entity = ALIASES[word];
9363
9873
  if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
9364
9874
  const requestedAction = parsed.positionals[2] ?? "list";
9365
- const action = canonicalAction(requestedAction);
9366
- if (!action) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|comment|comments|rm.`);
9367
- assertArgs(parsed, allowedOptions(entity, action), 4);
9875
+ const action2 = canonicalAction(requestedAction);
9876
+ if (!action2) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|comment|comments|rm.`);
9877
+ assertArgs(parsed, allowedOptions(entity, action2), 4);
9368
9878
  const ctx = await buildContext2(parsed, deps);
9369
9879
  const id = parsed.positionals[3];
9370
- switch (action) {
9880
+ switch (action2) {
9371
9881
  case "list":
9372
9882
  return pmList(ctx, entity, parsed);
9373
9883
  case "add":
9374
9884
  return pmAdd(ctx, entity, parsed);
9375
9885
  case "get":
9376
- return pmGet(ctx, entity, requireId2(id, action));
9886
+ return pmGet(ctx, entity, requireId2(id, action2));
9377
9887
  case "set":
9378
- return pmSet(ctx, entity, requireId2(id, action), parsed);
9888
+ return pmSet(ctx, entity, requireId2(id, action2), parsed);
9379
9889
  case "done":
9380
- return pmDone(ctx, entity, requireId2(id, action), parsed);
9890
+ return pmDone(ctx, entity, requireId2(id, action2), parsed);
9381
9891
  case "comment":
9382
- return pmComment(ctx, entity, requireId2(id, action), parsed);
9892
+ return pmComment(ctx, entity, requireId2(id, action2), parsed);
9383
9893
  case "comments":
9384
- return pmComments(ctx, entity, requireId2(id, action));
9894
+ return pmComments(ctx, entity, requireId2(id, action2));
9385
9895
  case "rm":
9386
- return pmRemove(ctx, entity, requireId2(id, action));
9896
+ return pmRemove(ctx, entity, requireId2(id, action2));
9387
9897
  }
9388
9898
  }
9389
9899
 
@@ -9416,8 +9926,8 @@ function printPlatformStatus(status, out) {
9416
9926
  if (status.verdict.reasons.length) {
9417
9927
  out.log(`reasons ${status.verdict.reasons.join(", ")}`);
9418
9928
  }
9419
- for (const action of status.nextActions) {
9420
- out.log(`next ${action.service} ${action.code} ${action.message}`);
9929
+ for (const action2 of status.nextActions) {
9930
+ out.log(`next ${action2.service} ${action2.code} ${action2.message}`);
9421
9931
  }
9422
9932
  }
9423
9933
  function value(input) {
@@ -9437,10 +9947,10 @@ async function platformCommand(parsed, deps = {}) {
9437
9947
  ["config", "context", "platform", "token", "email", "open", "json"],
9438
9948
  2
9439
9949
  );
9440
- const action = parsed.positionals[1];
9441
- if (action !== "status") {
9950
+ const action2 = parsed.positionals[1];
9951
+ if (action2 !== "status") {
9442
9952
  throw new Error(
9443
- `unknown platform action "${action ?? ""}". Try "odla-ai platform status --json".`
9953
+ `unknown platform action "${action2 ?? ""}". Try "odla-ai platform status --json".`
9444
9954
  );
9445
9955
  }
9446
9956
  const context = await resolveOperatorContext(parsed, {
@@ -9769,10 +10279,10 @@ async function o11yCommand(parsed, deps = {}) {
9769
10279
  ],
9770
10280
  2
9771
10281
  );
9772
- const action = parsed.positionals[1];
9773
- if (action !== "status") {
10282
+ const action2 = parsed.positionals[1];
10283
+ if (action2 !== "status") {
9774
10284
  throw new Error(
9775
- `unknown o11y action "${action ?? ""}". Try "odla-ai o11y status --json".`
10285
+ `unknown o11y action "${action2 ?? ""}". Try "odla-ai o11y status --json".`
9776
10286
  );
9777
10287
  }
9778
10288
  const minutes = statusMinutes(
@@ -10092,7 +10602,7 @@ async function runbookRemove(ctx, slug) {
10092
10602
 
10093
10603
  // src/runbook-import.ts
10094
10604
  import { readFileSync as readFileSync10, readdirSync as readdirSync2, statSync } from "fs";
10095
- import { basename as basename2, join as join11 } from "path";
10605
+ import { basename as basename2, join as join12 } from "path";
10096
10606
  function parseRunbook(text, slug) {
10097
10607
  let rest = text;
10098
10608
  const meta = {};
@@ -10122,7 +10632,7 @@ function readRunbookDir(dir) {
10122
10632
  if (!files.length) throw new Error(`no .md files in ${dir}`);
10123
10633
  return files.map((file) => {
10124
10634
  const slug = basename2(file, ".md");
10125
- const parsed = parseRunbook(readFileSync10(join11(dir, file), "utf8"), slug);
10635
+ const parsed = parseRunbook(readFileSync10(join12(dir, file), "utf8"), slug);
10126
10636
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
10127
10637
  });
10128
10638
  }
@@ -10195,7 +10705,7 @@ async function upsert(ctx, r, visibility) {
10195
10705
  // src/runbook-impact.ts
10196
10706
  import { execFileSync as execFileSync2 } from "child_process";
10197
10707
  import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
10198
- import { join as join12 } from "path";
10708
+ import { join as join13 } from "path";
10199
10709
 
10200
10710
  // src/runbook-impact-scan.ts
10201
10711
  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$]*)/;
@@ -10364,7 +10874,7 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
10364
10874
  }
10365
10875
  function manifestLabeller(root) {
10366
10876
  return (workspace) => {
10367
- const manifest = join12(root, workspace, "package.json");
10877
+ const manifest = join13(root, workspace, "package.json");
10368
10878
  if (!existsSync11(manifest)) return void 0;
10369
10879
  try {
10370
10880
  const name = JSON.parse(readFileSync11(manifest, "utf8")).name;
@@ -10434,7 +10944,7 @@ function report3(ctx, impacts) {
10434
10944
  async function runbookImpact(ctx, options, deps = {}) {
10435
10945
  const cwd = deps.cwd ?? process.cwd();
10436
10946
  const runGit = deps.runGit ?? gitRunner(cwd);
10437
- const read3 = deps.readRepoFile ?? ((path) => readFileSync11(join12(cwd, path), "utf8"));
10947
+ const read3 = deps.readRepoFile ?? ((path) => readFileSync11(join13(cwd, path), "utf8"));
10438
10948
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
10439
10949
  if (!surfaces.length) {
10440
10950
  return ctx.out.log(
@@ -10569,7 +11079,7 @@ async function runbookComment(ctx, slug, body) {
10569
11079
  import { spawnSync } from "child_process";
10570
11080
  import { mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
10571
11081
  import { tmpdir as tmpdir4 } from "os";
10572
- import { join as join13 } from "path";
11082
+ import { join as join14 } from "path";
10573
11083
  import process13 from "process";
10574
11084
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
10575
11085
  function resolveEditor(env = process13.env) {
@@ -10595,8 +11105,8 @@ function editText(initial, slug, deps = {}) {
10595
11105
  );
10596
11106
  if (!interactive())
10597
11107
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
10598
- const dir = mkdtempSync(join13(tmpdir4(), "odla-runbook-"));
10599
- const file = join13(dir, `${slug}.md`);
11108
+ const dir = mkdtempSync(join14(tmpdir4(), "odla-runbook-"));
11109
+ const file = join14(dir, `${slug}.md`);
10600
11110
  try {
10601
11111
  writeFileSync4(file, initial, { mode: 384 });
10602
11112
  const code = defaultRunOrInjected(deps)(editor, file);
@@ -10704,12 +11214,12 @@ var ALLOWED2 = [
10704
11214
  "platform",
10705
11215
  "context"
10706
11216
  ];
10707
- function requireSlug(slug, action) {
10708
- if (!slug) throw new Error(`"runbook ${action}" needs a slug, e.g. "odla-ai runbook ${action} release"`);
11217
+ function requireSlug(slug, action2) {
11218
+ if (!slug) throw new Error(`"runbook ${action2}" needs a slug, e.g. "odla-ai runbook ${action2} release"`);
10709
11219
  return slug;
10710
11220
  }
10711
11221
  var WRITES = /* @__PURE__ */ new Set(["new", "edit", "publish", "archive", "visibility", "revert", "rm", "import"]);
10712
- async function buildContext3(parsed, deps, action) {
11222
+ async function buildContext3(parsed, deps, action2) {
10713
11223
  const appIdOption = stringOpt(parsed.options.app);
10714
11224
  const context = await resolveOperatorContext(parsed, {
10715
11225
  allowMissingConfig: true
@@ -10719,7 +11229,7 @@ async function buildContext3(parsed, deps, action) {
10719
11229
  const out = deps.stdout ?? console;
10720
11230
  const appId = appIdOption ?? (context.app.source === "environment" || context.app.source === "profile" ? context.app.value : null) ?? PLATFORM_SCOPE;
10721
11231
  const dryRun = parsed.options["dry-run"] === true;
10722
- if (action === "import" && dryRun) {
11232
+ if (action2 === "import" && dryRun) {
10723
11233
  return {
10724
11234
  platformUrl: cfg.platformUrl,
10725
11235
  token: "",
@@ -10729,12 +11239,12 @@ async function buildContext3(parsed, deps, action) {
10729
11239
  appId
10730
11240
  };
10731
11241
  }
10732
- const needsCapability = WRITES.has(action) && !dryRun && appId === PLATFORM_SCOPE && !stringOpt(parsed.options.token);
11242
+ const needsCapability = WRITES.has(action2) && !dryRun && appId === PLATFORM_SCOPE && !stringOpt(parsed.options.token);
10733
11243
  const token = needsCapability ? await getScopedPlatformToken({
10734
11244
  platform: cfg.platformUrl,
10735
11245
  scope: "platform:runbook:write",
10736
11246
  email: stringOpt(parsed.options.email),
10737
- label: `odla CLI (runbook ${action})`,
11247
+ label: `odla CLI (runbook ${action2})`,
10738
11248
  fetch: doFetch,
10739
11249
  stdout: out,
10740
11250
  openApprovalUrl: deps.openUrl,
@@ -10769,11 +11279,11 @@ async function buildContext3(parsed, deps, action) {
10769
11279
  };
10770
11280
  }
10771
11281
  async function runbookCommand(parsed, deps = {}) {
10772
- const action = parsed.positionals[1] ?? "list";
10773
- assertArgs(parsed, ALLOWED2, action === "ask" || action === "search" ? 64 : 4);
10774
- const ctx = await buildContext3(parsed, deps, action);
11282
+ const action2 = parsed.positionals[1] ?? "list";
11283
+ assertArgs(parsed, ALLOWED2, action2 === "ask" || action2 === "search" ? 64 : 4);
11284
+ const ctx = await buildContext3(parsed, deps, action2);
10775
11285
  const slug = parsed.positionals[2];
10776
- switch (action) {
11286
+ switch (action2) {
10777
11287
  case "list":
10778
11288
  return runbookList(ctx, parsed.options.all === true, stringOpt(parsed.options.q));
10779
11289
  case "ask": {
@@ -10858,7 +11368,7 @@ async function runbookCommand(parsed, deps = {}) {
10858
11368
  case "rm":
10859
11369
  return runbookRemove(ctx, requireSlug(slug, "rm"));
10860
11370
  default:
10861
- throw new Error(`unknown runbook action "${action}". Try ${acceptedAfter(["runbook"]).join(", ")}.`);
11371
+ throw new Error(`unknown runbook action "${action2}". Try ${acceptedAfter(["runbook"]).join(", ")}.`);
10862
11372
  }
10863
11373
  }
10864
11374
 
@@ -11221,8 +11731,8 @@ async function securityCommand(parsed, dependencies) {
11221
11731
  else await runLocalSecurityCommand(parsed, dependencies);
11222
11732
  }
11223
11733
  async function githubSecurityCommand(parsed, dependencies) {
11224
- const action = parsed.positionals[2];
11225
- if (action === "disconnect") {
11734
+ const action2 = parsed.positionals[2];
11735
+ if (action2 === "disconnect") {
11226
11736
  assertArgs(parsed, ["config", "env", "platform", "source", "email", "open", "yes"], 3);
11227
11737
  const context2 = await hostedSecurityContext(parsed, dependencies);
11228
11738
  const sourceId = requiredString(parsed.options.source, "--source");
@@ -11237,7 +11747,7 @@ async function githubSecurityCommand(parsed, dependencies) {
11237
11747
  context2.stdout.log(`github: disconnected ${sourceId} from ${context2.appId}/${context2.env}`);
11238
11748
  return;
11239
11749
  }
11240
- if (action !== "connect") {
11750
+ if (action2 !== "connect") {
11241
11751
  throw new Error('unknown security github command. Try "odla-ai security github connect".');
11242
11752
  }
11243
11753
  assertArgs(parsed, ["config", "env", "platform", "repo", "email", "open"], 3);
@@ -11454,6 +11964,10 @@ export {
11454
11964
  calendarDisconnect,
11455
11965
  CAPABILITIES,
11456
11966
  printCapabilities,
11967
+ desiredRegistryState,
11968
+ reconcileConfig,
11969
+ configDiff,
11970
+ configPlan,
11457
11971
  doctor,
11458
11972
  initProject,
11459
11973
  secretsPush,
@@ -11491,4 +12005,4 @@ export {
11491
12005
  exitCodeFor,
11492
12006
  runCli
11493
12007
  };
11494
- //# sourceMappingURL=chunk-L2NG4UR4.js.map
12008
+ //# sourceMappingURL=chunk-2AFAXLKE.js.map