@odla-ai/cli 0.25.21 → 0.25.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -2
- package/REQUIREMENTS.md +6 -0
- package/dist/bin.cjs +1130 -618
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-L2NG4UR4.js → chunk-2AFAXLKE.js} +1043 -529
- package/dist/chunk-2AFAXLKE.js.map +1 -0
- package/dist/index.cjs +1138 -618
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +169 -6
- package/dist/index.d.ts +169 -6
- package/dist/index.js +9 -1
- package/package.json +1 -1
- package/dist/chunk-L2NG4UR4.js.map +0 -1
|
@@ -165,7 +165,9 @@ function approvalLines(prompt) {
|
|
|
165
165
|
lines.push("");
|
|
166
166
|
lines.push(` ${prompt.approvalUrl}`);
|
|
167
167
|
lines.push("");
|
|
168
|
-
lines.push(
|
|
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(
|
|
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 `${
|
|
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(
|
|
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 `${
|
|
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,
|
|
1607
|
-
if ((env === "prod" || env === "production") && !yes) throw new Error(`refusing to ${
|
|
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
|
|
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
|
|
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 =
|
|
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 [
|
|
2301
|
+
for (const [action2, expr] of Object.entries(actions)) {
|
|
1772
2302
|
if (typeof expr !== "string" || expr.trim() !== "true") continue;
|
|
1773
|
-
if (
|
|
2303
|
+
if (action2 === "view" && publicRead.includes(ns)) continue;
|
|
1774
2304
|
warnings.push(
|
|
1775
|
-
|
|
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(
|
|
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(
|
|
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
|
|
1936
|
-
if (rule[
|
|
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
|
|
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: ${
|
|
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:
|
|
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
|
|
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(
|
|
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 =
|
|
2455
|
-
const codexRoot = resolve5(options.codexHomeDir ?? process.env.CODEX_HOME ??
|
|
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 =
|
|
2993
|
+
const sharedRoot = join6(root, ".agents", "skills");
|
|
2464
2994
|
planSkillTree(sharedRoot);
|
|
2465
|
-
const claudeRoot =
|
|
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(
|
|
2471
|
-
plan(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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(`${
|
|
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
|
|
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-${
|
|
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(
|
|
3595
|
+
const context = await mkdtemp(join7(tmpdir(), "odla-code-pi-"));
|
|
3066
3596
|
try {
|
|
3067
|
-
await copyFile(embeddedPiAssetPath(),
|
|
3068
|
-
await writeFile(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
4570
|
+
return sha256Hex(canonicalJson2(policy));
|
|
4041
4571
|
}
|
|
4042
4572
|
function registeredIdRegistryDigest(values) {
|
|
4043
|
-
return sha256Hex(
|
|
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(
|
|
4774
|
+
return sha256Hex(canonicalJson2([...values].sort()));
|
|
4245
4775
|
}
|
|
4246
4776
|
async function evaluate(input, registries, approvals) {
|
|
4247
|
-
const actionDigest = await sha256Hex(
|
|
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
|
|
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 =
|
|
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 =
|
|
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:${
|
|
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/
|
|
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
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
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(
|
|
6732
|
+
function apiFailure(action2, status, value2) {
|
|
6452
6733
|
const message2 = record4(record4(value2)?.error)?.message;
|
|
6453
|
-
return `${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
6798
|
+
import { tenantIdFor as tenantIdFor3 } from "@odla-ai/apps";
|
|
6518
6799
|
async function provisionEnvCredentials(opts) {
|
|
6519
|
-
const tenantId =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) ??
|
|
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 ?
|
|
7646
|
+
const base = selection.name ? join10(dirname7(selection.file), "profiles", selection.name) : join10(homedir2(), ".odla");
|
|
7412
7647
|
return {
|
|
7413
|
-
developer:
|
|
7414
|
-
scoped:
|
|
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 ?
|
|
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:
|
|
7548
|
-
devVarsFile:
|
|
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
|
|
7600
|
-
const credentialSet =
|
|
7601
|
-
const credentials =
|
|
7602
|
-
const models =
|
|
7603
|
-
const usage =
|
|
7604
|
-
const audit =
|
|
7605
|
-
if (area !== "ai" ||
|
|
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"] :
|
|
7609
|
-
assertArgs(parsed, allowed, credentialSet ? 5 :
|
|
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" :
|
|
7613
|
-
purpose:
|
|
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
|
|
7671
|
-
if (
|
|
7672
|
-
throw new Error(`unknown agent action "${
|
|
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"],
|
|
7675
|
-
if (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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(
|
|
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)}/${
|
|
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(`${
|
|
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
|
|
8714
|
+
const action2 = parsed.positionals[1];
|
|
8460
8715
|
const out = deps.stdout ?? console;
|
|
8461
|
-
if (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
8777
|
+
if (action2 !== "show") {
|
|
8523
8778
|
throw new Error(
|
|
8524
|
-
`unknown context action "${
|
|
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,
|
|
8997
|
-
if (!id) throw new Error(`"discuss ${
|
|
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
|
|
9541
|
+
const action2 = parsed.positionals[1];
|
|
9032
9542
|
const id = parsed.positionals[2];
|
|
9033
|
-
if (!
|
|
9543
|
+
if (!action2) throw new Error('"discuss" needs an action. Run "odla-ai help".');
|
|
9034
9544
|
const ctx = await buildContext(parsed, deps);
|
|
9035
|
-
switch (
|
|
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 "${
|
|
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(
|
|
9319
|
-
if (
|
|
9320
|
-
if (
|
|
9321
|
-
if (
|
|
9322
|
-
return
|
|
9323
|
-
}
|
|
9324
|
-
function allowedOptions(entity,
|
|
9325
|
-
const entityOptions =
|
|
9326
|
-
return [...COMMON_OPTIONS, ...ACTION_OPTIONS[
|
|
9327
|
-
}
|
|
9328
|
-
function requireId2(id,
|
|
9329
|
-
if (!id) throw new Error(`"pm ... ${
|
|
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
|
|
9366
|
-
if (!
|
|
9367
|
-
assertArgs(parsed, allowedOptions(entity,
|
|
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 (
|
|
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,
|
|
9886
|
+
return pmGet(ctx, entity, requireId2(id, action2));
|
|
9377
9887
|
case "set":
|
|
9378
|
-
return pmSet(ctx, entity, requireId2(id,
|
|
9888
|
+
return pmSet(ctx, entity, requireId2(id, action2), parsed);
|
|
9379
9889
|
case "done":
|
|
9380
|
-
return pmDone(ctx, entity, requireId2(id,
|
|
9890
|
+
return pmDone(ctx, entity, requireId2(id, action2), parsed);
|
|
9381
9891
|
case "comment":
|
|
9382
|
-
return pmComment(ctx, entity, requireId2(id,
|
|
9892
|
+
return pmComment(ctx, entity, requireId2(id, action2), parsed);
|
|
9383
9893
|
case "comments":
|
|
9384
|
-
return pmComments(ctx, entity, requireId2(id,
|
|
9894
|
+
return pmComments(ctx, entity, requireId2(id, action2));
|
|
9385
9895
|
case "rm":
|
|
9386
|
-
return pmRemove(ctx, entity, requireId2(id,
|
|
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
|
|
9420
|
-
out.log(`next ${
|
|
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
|
|
9441
|
-
if (
|
|
9950
|
+
const action2 = parsed.positionals[1];
|
|
9951
|
+
if (action2 !== "status") {
|
|
9442
9952
|
throw new Error(
|
|
9443
|
-
`unknown platform action "${
|
|
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
|
|
9773
|
-
if (
|
|
10282
|
+
const action2 = parsed.positionals[1];
|
|
10283
|
+
if (action2 !== "status") {
|
|
9774
10284
|
throw new Error(
|
|
9775
|
-
`unknown o11y action "${
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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(
|
|
10599
|
-
const file =
|
|
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,
|
|
10708
|
-
if (!slug) throw new Error(`"runbook ${
|
|
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,
|
|
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 (
|
|
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(
|
|
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 ${
|
|
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
|
|
10773
|
-
assertArgs(parsed, ALLOWED2,
|
|
10774
|
-
const ctx = await buildContext3(parsed, deps,
|
|
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 (
|
|
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 "${
|
|
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
|
|
11225
|
-
if (
|
|
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 (
|
|
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-
|
|
12008
|
+
//# sourceMappingURL=chunk-2AFAXLKE.js.map
|