@odla-ai/cli 0.27.11 → 0.27.13
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 +7 -1
- package/dist/bin.cjs +272 -630
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-Y7WFLSTN.js → chunk-3YSCRXPF.js} +157 -449
- package/dist/chunk-3YSCRXPF.js.map +1 -0
- package/dist/index.cjs +272 -563
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/skills/odla/references/build.md +8 -0
- package/dist/chunk-Y7WFLSTN.js.map +0 -1
package/dist/bin.cjs
CHANGED
|
@@ -318,23 +318,30 @@ function handshakeWaitMs(waitSeconds, interactive = import_node_process3.default
|
|
|
318
318
|
|
|
319
319
|
// src/token.ts
|
|
320
320
|
async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {}) {
|
|
321
|
-
if (options.token) return options.token;
|
|
322
321
|
const audience = platformAudience(cfg.platformUrl);
|
|
323
|
-
if (import_node_process4.default.env.ODLA_DEV_TOKEN) {
|
|
324
|
-
const declared = import_node_process4.default.env.ODLA_DEV_TOKEN_AUDIENCE;
|
|
325
|
-
if (declared) {
|
|
326
|
-
if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
|
|
327
|
-
} else if (audience !== "https://odla.ai") {
|
|
328
|
-
throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
|
|
329
|
-
}
|
|
330
|
-
return import_node_process4.default.env.ODLA_DEV_TOKEN;
|
|
331
|
-
}
|
|
332
322
|
const optionalProjectCapabilities = grantRequest.optionalProjectCapabilities ?? [];
|
|
333
323
|
const grantIntent = { projectIds: [cfg.app.id], optionalProjectCapabilities };
|
|
334
324
|
const cached = readJsonFile(cfg.local.tokenFile);
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
|
|
325
|
+
if (!grantRequest.forceReview) {
|
|
326
|
+
if (options.token) return options.token;
|
|
327
|
+
if (import_node_process4.default.env.ODLA_DEV_TOKEN) {
|
|
328
|
+
const declared = import_node_process4.default.env.ODLA_DEV_TOKEN_AUDIENCE;
|
|
329
|
+
if (declared) {
|
|
330
|
+
if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
|
|
331
|
+
} else if (audience !== "https://odla.ai") {
|
|
332
|
+
throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
|
|
333
|
+
}
|
|
334
|
+
return import_node_process4.default.env.ODLA_DEV_TOKEN;
|
|
335
|
+
}
|
|
336
|
+
if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4 && cachedGrantCovers(cached, grantIntent)) {
|
|
337
|
+
out.error(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
338
|
+
return cached.token;
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
if (options.token) {
|
|
342
|
+
throw new Error("--request-grant cannot be combined with --token; remove --token so the approved replacement credential can be collected and cached");
|
|
343
|
+
}
|
|
344
|
+
out.error(`auth: requesting fresh owner review for app.manage on exact project "${cfg.app.id}"`);
|
|
338
345
|
}
|
|
339
346
|
const ctx = {
|
|
340
347
|
cfg,
|
|
@@ -1668,7 +1675,7 @@ async function agentCommand(parsed, deps = {}) {
|
|
|
1668
1675
|
if (action2 !== "jobs" && action2 !== "retry") {
|
|
1669
1676
|
throw new Error(`unknown agent action "${action2 ?? ""}". Try "odla-ai agent jobs --json".`);
|
|
1670
1677
|
}
|
|
1671
|
-
assertArgs(parsed, ["config", "env", "state", "limit", "json", "token"
|
|
1678
|
+
assertArgs(parsed, ["config", "env", "state", "limit", "json", "token"], action2 === "jobs" ? 2 : 3);
|
|
1672
1679
|
if (action2 === "retry" && (parsed.options.state !== void 0 || parsed.options.limit !== void 0)) {
|
|
1673
1680
|
throw new Error('--state and --limit are supported only by "agent jobs"');
|
|
1674
1681
|
}
|
|
@@ -1676,17 +1683,17 @@ async function agentCommand(parsed, deps = {}) {
|
|
|
1676
1683
|
const { env, tenant } = resolveTenant(cfg, stringOpt(parsed.options.env));
|
|
1677
1684
|
const doFetch = deps.fetch ?? fetch;
|
|
1678
1685
|
const out = deps.stdout ?? console;
|
|
1679
|
-
const credential2 =
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1686
|
+
const credential2 = stringOpt(parsed.options.token) ?? readCredentials(cfg.local.credentialsFile)?.envs[env]?.dbKey;
|
|
1687
|
+
if (!credential2) {
|
|
1688
|
+
throw new Error(
|
|
1689
|
+
`no ${env} app credential found; run \`odla-ai provision --write-dev-vars --yes\` or pass --token <ODLA_API_KEY>`
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
if (credential2.startsWith("odla_dev_")) {
|
|
1693
|
+
throw new Error(
|
|
1694
|
+
"agent job administration requires an app credential (ODLA_API_KEY / odla_sk_\u2026), not a developer device token"
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1690
1697
|
const base = `${cfg.dbEndpoint}/app/${encodeURIComponent(tenant)}/admin/agent-jobs`;
|
|
1691
1698
|
const headers = { authorization: `Bearer ${credential2}` };
|
|
1692
1699
|
if (action2 === "retry") {
|
|
@@ -1742,51 +1749,23 @@ function errorMessage(body) {
|
|
|
1742
1749
|
return "request failed";
|
|
1743
1750
|
}
|
|
1744
1751
|
|
|
1752
|
+
// src/human-session.ts
|
|
1753
|
+
async function requireStudioHuman(configPath, action2, destination = "app", env) {
|
|
1754
|
+
const cfg = await loadProjectConfig(configPath);
|
|
1755
|
+
const appEnv = env && cfg.envs.includes(env) ? env : cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
|
|
1756
|
+
const path = destination === "app" ? `/studio/apps/${encodeURIComponent(cfg.app.id)}/${encodeURIComponent(appEnv)}/settings/app` : `/studio/apps/${encodeURIComponent(cfg.app.id)}/${encodeURIComponent(appEnv)}/${destination}`;
|
|
1757
|
+
throw new Error(
|
|
1758
|
+
`human_session_required: ${action2} requires the signed-in owner in Studio. A CLI device token is an agent credential, so another approval or retry cannot work. ${new URL(path, cfg.platformUrl).href}`
|
|
1759
|
+
);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1745
1762
|
// src/app-export.ts
|
|
1746
|
-
var import_node_fs7 = require("fs");
|
|
1747
|
-
var import_node_stream = require("stream");
|
|
1748
|
-
var import_promises = require("stream/promises");
|
|
1749
1763
|
async function appExport(options) {
|
|
1750
|
-
|
|
1751
|
-
const out = options.stdout ?? console;
|
|
1752
|
-
const doFetch = options.fetch ?? fetch;
|
|
1753
|
-
const { tenant } = resolveTenant(cfg, options.env);
|
|
1754
|
-
const token = await getDeveloperToken(
|
|
1755
|
-
cfg,
|
|
1756
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
1757
|
-
doFetch,
|
|
1758
|
-
out
|
|
1759
|
-
);
|
|
1760
|
-
const auth = { authorization: `Bearer ${token}` };
|
|
1761
|
-
const base = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}`;
|
|
1762
|
-
if (options.fresh) {
|
|
1763
|
-
const res = await doFetch(`${base}/export`, { method: "POST", headers: auth });
|
|
1764
|
-
const body = await res.json().catch(() => ({}));
|
|
1765
|
-
if (!res.ok) throw new Error(`export failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? res.status}`);
|
|
1766
|
-
}
|
|
1767
|
-
const list = await doFetch(`${base}/backups`, { headers: auth });
|
|
1768
|
-
if (!list.ok) throw new Error(`couldn't list backups (${list.status})`);
|
|
1769
|
-
const { backups } = await list.json();
|
|
1770
|
-
const newest = backups[0];
|
|
1771
|
-
if (!newest) {
|
|
1772
|
-
throw new Error(
|
|
1773
|
-
`${tenant} has no backups yet \u2014 run \`odla-ai app export --fresh\` to take one now (nightly snapshots appear after the first day with writes)`
|
|
1774
|
-
);
|
|
1775
|
-
}
|
|
1776
|
-
const download = await doFetch(`${base}/backups/${newest.id}/download`, { headers: auth });
|
|
1777
|
-
if (!download.ok || !download.body) throw new Error(`download failed (${download.status})`);
|
|
1778
|
-
const file = options.out ?? `${tenant}-${new Date(newest.created_at).toISOString().slice(0, 10)}-tx${newest.max_tx}.jsonl.gz`;
|
|
1779
|
-
await (0, import_promises.pipeline)(import_node_stream.Readable.fromWeb(download.body), (0, import_node_fs7.createWriteStream)(file));
|
|
1780
|
-
if (options.json) out.log(JSON.stringify({ file, backup: newest }, null, 2));
|
|
1781
|
-
else {
|
|
1782
|
-
out.log(`${tenant}: wrote ${file} (${newest.bytes} bytes, ${newest.kind} snapshot at tx ${newest.max_tx})`);
|
|
1783
|
-
out.log(`sha256 ${download.headers.get("x-odla-sha256") ?? newest.sha256}`);
|
|
1784
|
-
}
|
|
1785
|
-
return { file, backup: newest };
|
|
1764
|
+
return requireStudioHuman(options.configPath, "database export", "database", options.env);
|
|
1786
1765
|
}
|
|
1787
1766
|
|
|
1788
1767
|
// src/app-import.ts
|
|
1789
|
-
var
|
|
1768
|
+
var import_node_fs7 = require("fs");
|
|
1790
1769
|
var import_import = require("@odla-ai/db/import");
|
|
1791
1770
|
function chooseIdMode(options, rows) {
|
|
1792
1771
|
const chosen = [options.idField && "field", options.key && "key", options.generateIds && "generate"].filter(Boolean);
|
|
@@ -1803,9 +1782,8 @@ async function appImport(options) {
|
|
|
1803
1782
|
const cfg = await loadProjectConfig(options.configPath);
|
|
1804
1783
|
const out = options.stdout ?? console;
|
|
1805
1784
|
const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
|
|
1806
|
-
const doFetch = options.fetch ?? fetch;
|
|
1807
1785
|
const { tenant } = resolveTenant(cfg, options.env);
|
|
1808
|
-
const text2 = options.file === "-" ? (options.readStdin ?? (() => (0,
|
|
1786
|
+
const text2 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs7.readFileSync)(0, "utf8")))() : (0, import_node_fs7.readFileSync)(options.file, "utf8");
|
|
1809
1787
|
const { format, sources } = (0, import_import.parseImport)(text2, options.ns);
|
|
1810
1788
|
if (format === "namespace-map" && options.ns) {
|
|
1811
1789
|
throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
|
|
@@ -1831,100 +1809,18 @@ ${detail}${more}`);
|
|
|
1831
1809
|
if (options.json) out.log(JSON.stringify(result, null, 2));
|
|
1832
1810
|
return result;
|
|
1833
1811
|
}
|
|
1834
|
-
|
|
1835
|
-
cfg,
|
|
1836
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
1837
|
-
doFetch,
|
|
1838
|
-
out
|
|
1839
|
-
);
|
|
1840
|
-
const runId = crypto.randomUUID();
|
|
1841
|
-
const url = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}/transact`;
|
|
1842
|
-
for (const chunk of chunks) {
|
|
1843
|
-
const res = await doFetch(url, {
|
|
1844
|
-
method: "POST",
|
|
1845
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
1846
|
-
body: JSON.stringify({ mutationId: (0, import_import.importMutationId)(runId, chunk.index), ops: chunk.ops })
|
|
1847
|
-
});
|
|
1848
|
-
const body = await res.json().catch(() => ({}));
|
|
1849
|
-
if (!res.ok) {
|
|
1850
|
-
const code = body.error?.code ? ` (${body.error.code})` : "";
|
|
1851
|
-
throw new Error(
|
|
1852
|
-
`chunk ${chunk.index + 1} of ${chunks.length} failed${code}: ${body.error?.message ?? res.status}. ${result.committed} row(s) already committed; fix the input and re-run to import the rest.`
|
|
1853
|
-
);
|
|
1854
|
-
}
|
|
1855
|
-
result.committed += chunk.ops.length;
|
|
1856
|
-
if (typeof body.txId === "number") result.txIds.push(body.txId);
|
|
1857
|
-
if (body.duplicate) result.duplicate++;
|
|
1858
|
-
}
|
|
1859
|
-
if (options.json) out.log(JSON.stringify(result, null, 2));
|
|
1860
|
-
else {
|
|
1861
|
-
const dup = result.duplicate > 0 ? ` (${result.duplicate} chunk(s) were already applied)` : "";
|
|
1862
|
-
out.log(`${tenant}: upserted ${result.committed} row(s) in ${chunks.length} transaction(s)${dup}`);
|
|
1863
|
-
}
|
|
1864
|
-
return result;
|
|
1812
|
+
return requireStudioHuman(options.configPath, "database import", "database", options.env);
|
|
1865
1813
|
}
|
|
1866
1814
|
|
|
1867
1815
|
// src/app-owners.ts
|
|
1868
|
-
var sink = (options) => options.stdout ?? console;
|
|
1869
|
-
async function ownersRequest(method, suffix, options, body) {
|
|
1870
|
-
const cfg = await loadProjectConfig(options.configPath);
|
|
1871
|
-
const doFetch = options.fetch ?? fetch;
|
|
1872
|
-
const token = await getDeveloperToken(
|
|
1873
|
-
cfg,
|
|
1874
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
1875
|
-
doFetch,
|
|
1876
|
-
sink(options)
|
|
1877
|
-
);
|
|
1878
|
-
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/owners${suffix}`, {
|
|
1879
|
-
method,
|
|
1880
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
1881
|
-
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
1882
|
-
});
|
|
1883
|
-
const data = await res.json().catch(() => ({}));
|
|
1884
|
-
if (!res.ok) {
|
|
1885
|
-
throw new Error(
|
|
1886
|
-
`owners ${method} failed${data.error?.code ? ` (${data.error.code})` : ""}: ` + (data.error?.message ?? `registry returned ${res.status}`)
|
|
1887
|
-
);
|
|
1888
|
-
}
|
|
1889
|
-
return data.owners ?? [];
|
|
1890
|
-
}
|
|
1891
|
-
function report(options, owners, headline) {
|
|
1892
|
-
const out = sink(options);
|
|
1893
|
-
if (options.json === true) {
|
|
1894
|
-
out.log(JSON.stringify(owners, null, 2));
|
|
1895
|
-
return;
|
|
1896
|
-
}
|
|
1897
|
-
if (headline) out.log(headline);
|
|
1898
|
-
out.log(`owners (${owners.length}):`);
|
|
1899
|
-
for (const o of owners) {
|
|
1900
|
-
const name = o.email?.trim() || "Unnamed member";
|
|
1901
|
-
out.log(
|
|
1902
|
-
` ${o.primary ? "\u2605" : "\xB7"} ${name} [${o.ownerId}]${o.primary ? " (primary)" : ""}`
|
|
1903
|
-
);
|
|
1904
|
-
}
|
|
1905
|
-
}
|
|
1906
1816
|
async function ownersList(options) {
|
|
1907
|
-
|
|
1817
|
+
await requireStudioHuman(options.configPath, "listing app owners", "app");
|
|
1908
1818
|
}
|
|
1909
1819
|
async function ownersAdd(email, options) {
|
|
1910
|
-
|
|
1911
|
-
report(
|
|
1912
|
-
options,
|
|
1913
|
-
owners,
|
|
1914
|
-
`added ${email} as a co-owner \u2014 they share full access. They can now run \`odla-ai provision\` to mint their own credentials (no secret sharing).`
|
|
1915
|
-
);
|
|
1820
|
+
await requireStudioHuman(options.configPath, `adding ${email} as an app owner`, "app");
|
|
1916
1821
|
}
|
|
1917
1822
|
async function ownersRemove(target, options) {
|
|
1918
|
-
|
|
1919
|
-
if (target.includes("@")) {
|
|
1920
|
-
const owners2 = await ownersRequest("GET", "", options);
|
|
1921
|
-
const match = owners2.find((o) => o.email?.toLowerCase() === target.toLowerCase());
|
|
1922
|
-
if (!match) throw new Error(`no co-owner with email ${target}`);
|
|
1923
|
-
if (match.primary) throw new Error("can't remove the primary owner");
|
|
1924
|
-
ownerId = match.ownerId;
|
|
1925
|
-
}
|
|
1926
|
-
const owners = await ownersRequest("DELETE", `/${encodeURIComponent(ownerId)}`, options);
|
|
1927
|
-
report(options, owners, `removed ${target}`);
|
|
1823
|
+
await requireStudioHuman(options.configPath, `removing ${target} as an app owner`, "app");
|
|
1928
1824
|
}
|
|
1929
1825
|
async function appOwnersCommand(parsed, dependencies = {}) {
|
|
1930
1826
|
const sub = parsed.positionals[2] ?? "list";
|
|
@@ -1956,35 +1852,9 @@ async function appOwnersCommand(parsed, dependencies = {}) {
|
|
|
1956
1852
|
|
|
1957
1853
|
// src/app-rename.ts
|
|
1958
1854
|
async function appRename(name, options) {
|
|
1959
|
-
const out = options.stdout ?? console;
|
|
1960
1855
|
const trimmed = name.trim();
|
|
1961
1856
|
if (!trimmed) throw new Error('"app rename" needs a name \u2014 try `odla-ai app rename "Acme Storefront"`.');
|
|
1962
|
-
|
|
1963
|
-
const doFetch = options.fetch ?? fetch;
|
|
1964
|
-
const token = await getDeveloperToken(
|
|
1965
|
-
cfg,
|
|
1966
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
1967
|
-
doFetch,
|
|
1968
|
-
out
|
|
1969
|
-
);
|
|
1970
|
-
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/name`, {
|
|
1971
|
-
method: "PUT",
|
|
1972
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
1973
|
-
body: JSON.stringify({ name: trimmed })
|
|
1974
|
-
});
|
|
1975
|
-
const data = await res.json().catch(() => ({}));
|
|
1976
|
-
if (!res.ok || !data.app) {
|
|
1977
|
-
throw new Error(
|
|
1978
|
-
`rename failed${data.error?.code ? ` (${data.error.code})` : ""}: ` + (data.error?.message ?? `registry returned ${res.status}`)
|
|
1979
|
-
);
|
|
1980
|
-
}
|
|
1981
|
-
if (options.json === true) {
|
|
1982
|
-
out.log(JSON.stringify(data.app, null, 2));
|
|
1983
|
-
return;
|
|
1984
|
-
}
|
|
1985
|
-
out.log(`renamed ${data.app.appId} \u2192 "${data.app.name}"`);
|
|
1986
|
-
out.log("The app id is unchanged, so credentials, tenants, and URLs keep working.");
|
|
1987
|
-
out.log(`Update the "name" in ${cfg.configPath} to match.`);
|
|
1857
|
+
await requireStudioHuman(options.configPath, `renaming the app to "${trimmed}"`, "app");
|
|
1988
1858
|
}
|
|
1989
1859
|
async function appRenameCommand(parsed, dependencies = {}) {
|
|
1990
1860
|
assertArgs(parsed, ["config", "token", "email", "json"], parsed.positionals.length);
|
|
@@ -2000,172 +1870,23 @@ async function appRenameCommand(parsed, dependencies = {}) {
|
|
|
2000
1870
|
}
|
|
2001
1871
|
|
|
2002
1872
|
// src/app-transfer.ts
|
|
2003
|
-
function endpointsFor(verb, tenants) {
|
|
2004
|
-
const up = { source: tenants.sandbox, target: tenants.live, from: "dev" };
|
|
2005
|
-
const down = { source: tenants.live, target: tenants.sandbox, from: "prod" };
|
|
2006
|
-
if (verb === "refresh-sandbox") return { ...down, mode: "refresh" };
|
|
2007
|
-
return { ...up, mode: "cutover" };
|
|
2008
|
-
}
|
|
2009
|
-
async function api(cfg, token, path, init, doFetch) {
|
|
2010
|
-
const res = await doFetch(`${cfg.dbEndpoint}${path}`, {
|
|
2011
|
-
...init,
|
|
2012
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json", ...init?.headers }
|
|
2013
|
-
});
|
|
2014
|
-
return { status: res.status, body: await res.json().catch(() => ({})) };
|
|
2015
|
-
}
|
|
2016
|
-
function describe(side) {
|
|
2017
|
-
if (!side.exists) return "not provisioned";
|
|
2018
|
-
if (side.maxTx === 0) return "empty \u2014 never written";
|
|
2019
|
-
const ns = side.namespaces.length === 1 ? "1 namespace" : `${side.namespaces.length} namespaces`;
|
|
2020
|
-
const identity = side.identityRows > 0 ? `, ${side.identityRows} identity row(s)` : "";
|
|
2021
|
-
return `${ns} \xB7 ${side.triples} value(s) \xB7 tx ${side.maxTx}${identity}`;
|
|
2022
|
-
}
|
|
2023
|
-
function printPlan(out, verb, pre, opts) {
|
|
2024
|
-
const arrow = verb === "refresh-sandbox" ? "live \u2192 sandbox" : "sandbox \u2192 live";
|
|
2025
|
-
out.log(`${verb} (${arrow})`);
|
|
2026
|
-
out.log(` from ${pre.source.tenant} ${describe(pre.source)}`);
|
|
2027
|
-
out.log(` to ${pre.target.tenant} ${describe(pre.target)}`);
|
|
2028
|
-
if (verb === "promote") {
|
|
2029
|
-
out.log(" moves schema, rules and gates only \u2014 no rows are copied or removed");
|
|
2030
|
-
} else {
|
|
2031
|
-
out.log(` ${pre.target.tenant} is REPLACED by ${pre.source.tenant}`);
|
|
2032
|
-
out.log(` stays put: ${pre.staysPut.join(", ")}`);
|
|
2033
|
-
const identity = opts.includeIdentity ? "INCLUDED (--include-identity)" : `left behind: ${pre.excluded.join(", ")}`;
|
|
2034
|
-
out.log(` identity: ${identity}`);
|
|
2035
|
-
out.log(` files: ${opts.includeFiles ? "copied (--include-files)" : "not copied"}`);
|
|
2036
|
-
}
|
|
2037
|
-
for (const blocker of pre.blockers) out.log(` \u2716 ${blocker.code}: ${blocker.message}`);
|
|
2038
|
-
}
|
|
2039
1873
|
async function appTransfer(options) {
|
|
2040
1874
|
const cfg = await loadProjectConfig(options.configPath);
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
const doFetch = options.fetch ?? fetch;
|
|
2044
|
-
const tenants = bothTenants(cfg);
|
|
2045
|
-
const route2 = endpointsFor(options.verb, tenants);
|
|
2046
|
-
const token = await getDeveloperToken(
|
|
2047
|
-
cfg,
|
|
2048
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
2049
|
-
doFetch,
|
|
2050
|
-
out
|
|
2051
|
-
);
|
|
2052
|
-
const pre = await api(
|
|
2053
|
-
cfg,
|
|
2054
|
-
token,
|
|
2055
|
-
`/admin/apps/${encodeURIComponent(route2.target)}/copy-preflight?from=${route2.from}`,
|
|
2056
|
-
void 0,
|
|
2057
|
-
doFetch
|
|
2058
|
-
);
|
|
2059
|
-
if (pre.status !== 200) {
|
|
2060
|
-
throw new Error(`pre-flight failed${pre.body.error?.code ? ` (${pre.body.error.code})` : ""}: ${pre.body.error?.message ?? pre.status}`);
|
|
2061
|
-
}
|
|
2062
|
-
const plan = pre.body;
|
|
2063
|
-
printPlan({ log: say }, options.verb, plan, options);
|
|
2064
|
-
if (options.verb === "go-live" && !plan.targetEmpty) {
|
|
2065
|
-
throw new Error(
|
|
2066
|
-
`${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.`
|
|
2067
|
-
);
|
|
2068
|
-
}
|
|
2069
|
-
if (plan.blockers.length > 0) throw new Error(`cannot ${options.verb}: ${plan.blockers.map((b) => b.message).join("; ")}`);
|
|
2070
|
-
if (options.dryRun || options.yes !== true) {
|
|
2071
|
-
say(`nothing written (${options.dryRun ? "--dry-run" : "no --yes"})`);
|
|
2072
|
-
if (options.json) out.log(JSON.stringify(plan, null, 2));
|
|
2073
|
-
return { ok: true, plan };
|
|
2074
|
-
}
|
|
2075
|
-
if (options.verb === "promote") {
|
|
2076
|
-
const res2 = await api(
|
|
2077
|
-
cfg,
|
|
2078
|
-
token,
|
|
2079
|
-
`/admin/apps/${encodeURIComponent(route2.target)}/promote-definitions`,
|
|
2080
|
-
{ method: "POST", body: JSON.stringify({ from: "dev" }) },
|
|
2081
|
-
doFetch
|
|
2082
|
-
);
|
|
2083
|
-
if (res2.status !== 200) throw new Error(`promote failed${res2.body.error?.code ? ` (${res2.body.error.code})` : ""}: ${res2.body.error?.message ?? res2.status}`);
|
|
2084
|
-
if (options.json) out.log(JSON.stringify(res2.body, null, 2));
|
|
2085
|
-
else say(`${route2.target}: promoted ${(res2.body.applied ?? []).join(", ")}`);
|
|
2086
|
-
return { ok: true, plan, result: res2.body };
|
|
2087
|
-
}
|
|
2088
|
-
const res = await api(
|
|
2089
|
-
cfg,
|
|
2090
|
-
token,
|
|
2091
|
-
`/admin/apps/${encodeURIComponent(route2.target)}/copy-db`,
|
|
2092
|
-
{
|
|
2093
|
-
method: "POST",
|
|
2094
|
-
body: JSON.stringify({
|
|
2095
|
-
from: route2.from,
|
|
2096
|
-
mode: route2.mode,
|
|
2097
|
-
...options.includeIdentity ? { includeUsers: true } : {},
|
|
2098
|
-
...options.includeFiles ? { includeFiles: true } : {}
|
|
2099
|
-
})
|
|
2100
|
-
},
|
|
2101
|
-
doFetch
|
|
2102
|
-
);
|
|
2103
|
-
if (res.status !== 200) throw new Error(`${options.verb} failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
|
|
2104
|
-
say(`${route2.target}: replaced from ${route2.source} (tx ${res.body.destination?.maxTx}, epoch ${res.body.destination?.epoch}) \u2014 connected clients resync automatically`);
|
|
2105
|
-
if (options.includeFiles) await copyFiles(cfg, token, route2, doFetch, out);
|
|
2106
|
-
if (options.json) out.log(JSON.stringify(res.body, null, 2));
|
|
2107
|
-
return { ok: true, plan, result: res.body };
|
|
2108
|
-
}
|
|
2109
|
-
async function copyFiles(cfg, token, route2, doFetch, out) {
|
|
2110
|
-
for (let attempt = 1; attempt <= 20; attempt++) {
|
|
2111
|
-
const res = await api(
|
|
2112
|
-
cfg,
|
|
2113
|
-
token,
|
|
2114
|
-
`/admin/apps/${encodeURIComponent(route2.target)}/copy-files`,
|
|
2115
|
-
{ method: "POST", body: JSON.stringify({ from: route2.from }) },
|
|
2116
|
-
doFetch
|
|
2117
|
-
);
|
|
2118
|
-
if (res.status === 200) {
|
|
2119
|
-
out.log(`${route2.target}: files copied`);
|
|
2120
|
-
return;
|
|
2121
|
-
}
|
|
2122
|
-
if (!res.body.error?.retry) {
|
|
2123
|
-
throw new Error(`file copy failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
|
|
2124
|
-
}
|
|
2125
|
-
out.log(` files: bounded at attempt ${attempt}, resuming\u2026`);
|
|
2126
|
-
}
|
|
2127
|
-
throw new Error("file copy did not finish within 20 rounds \u2014 re-run to continue where it left off");
|
|
1875
|
+
bothTenants(cfg);
|
|
1876
|
+
return requireStudioHuman(options.configPath, `app ${options.verb}`, "database");
|
|
2128
1877
|
}
|
|
2129
1878
|
|
|
2130
1879
|
// src/app-lifecycle.ts
|
|
2131
|
-
async function lifecycleCall(action2, options) {
|
|
2132
|
-
const cfg = await loadProjectConfig(options.configPath);
|
|
2133
|
-
const out = options.stdout ?? console;
|
|
2134
|
-
const doFetch = options.fetch ?? fetch;
|
|
2135
|
-
const token = await getDeveloperToken(
|
|
2136
|
-
cfg,
|
|
2137
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
2138
|
-
doFetch,
|
|
2139
|
-
out
|
|
2140
|
-
);
|
|
2141
|
-
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/${action2}`, {
|
|
2142
|
-
method: "POST",
|
|
2143
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }
|
|
2144
|
-
});
|
|
2145
|
-
const body = await res.json().catch(() => ({}));
|
|
2146
|
-
if (!res.ok || !body.ok) {
|
|
2147
|
-
throw new Error(`${action2} failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? `registry returned ${res.status}`}`);
|
|
2148
|
-
}
|
|
2149
|
-
return body;
|
|
2150
|
-
}
|
|
2151
1880
|
async function appArchive(options) {
|
|
2152
1881
|
if (options.yes !== true) {
|
|
2153
1882
|
throw new Error(
|
|
2154
1883
|
"app archive suspends EVERY environment: API keys stop working and all services refuse requests until restored. All data is retained. Pass --yes to proceed."
|
|
2155
1884
|
);
|
|
2156
1885
|
}
|
|
2157
|
-
|
|
2158
|
-
const body = await lifecycleCall("archive", options);
|
|
2159
|
-
if (options.json) out.log(JSON.stringify(body, null, 2));
|
|
2160
|
-
else if (body.operation?.state === "noop") out.log(`${body.app?.appId}: already archived`);
|
|
2161
|
-
else out.log(`${body.app?.appId}: archived \u2014 data retained; run \`odla-ai app restore\` (or use Studio) to bring it back`);
|
|
1886
|
+
await requireStudioHuman(options.configPath, "app archive", "app");
|
|
2162
1887
|
}
|
|
2163
1888
|
async function appRestore(options) {
|
|
2164
|
-
|
|
2165
|
-
const body = await lifecycleCall("restore", options);
|
|
2166
|
-
if (options.json) out.log(JSON.stringify(body, null, 2));
|
|
2167
|
-
else if (body.operation?.state === "noop") out.log(`${body.app?.appId}: already active`);
|
|
2168
|
-
else out.log(`${body.app?.appId}: restored \u2014 every service's data plane is live again`);
|
|
1889
|
+
await requireStudioHuman(options.configPath, "app restore", "app");
|
|
2169
1890
|
}
|
|
2170
1891
|
async function appCommand(parsed, dependencies = {}) {
|
|
2171
1892
|
const sub = parsed.positionals[1];
|
|
@@ -2251,7 +1972,7 @@ async function appCommand(parsed, dependencies = {}) {
|
|
|
2251
1972
|
}
|
|
2252
1973
|
|
|
2253
1974
|
// src/brand-command.ts
|
|
2254
|
-
var
|
|
1975
|
+
var import_promises = require("fs/promises");
|
|
2255
1976
|
var import_node_path7 = require("path");
|
|
2256
1977
|
|
|
2257
1978
|
// src/brand-design-unpack.ts
|
|
@@ -2354,7 +2075,7 @@ function describeUnpack(result, outDir) {
|
|
|
2354
2075
|
// src/brand-command.ts
|
|
2355
2076
|
var USAGE = "usage: odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]";
|
|
2356
2077
|
async function readBundle(source, deps) {
|
|
2357
|
-
if (source !== "-") return (0,
|
|
2078
|
+
if (source !== "-") return (0, import_promises.readFile)((0, import_node_path7.resolve)(source), "utf8");
|
|
2358
2079
|
const readStdin = deps.readStdin;
|
|
2359
2080
|
if (!readStdin) throw new Error("reading a bundle from stdin is not supported here");
|
|
2360
2081
|
return readStdin();
|
|
@@ -2362,8 +2083,8 @@ async function readBundle(source, deps) {
|
|
|
2362
2083
|
async function writeAll(result, outDir) {
|
|
2363
2084
|
for (const file of result.files) {
|
|
2364
2085
|
const target = (0, import_node_path7.resolve)(outDir, file.path);
|
|
2365
|
-
await (0,
|
|
2366
|
-
await (0,
|
|
2086
|
+
await (0, import_promises.mkdir)((0, import_node_path7.dirname)(target), { recursive: true });
|
|
2087
|
+
await (0, import_promises.writeFile)(target, file.bytes);
|
|
2367
2088
|
}
|
|
2368
2089
|
}
|
|
2369
2090
|
async function designUnpack(parsed, deps) {
|
|
@@ -2523,12 +2244,6 @@ async function pollCalendarConnection(ctx, attemptId) {
|
|
|
2523
2244
|
ctx.env
|
|
2524
2245
|
);
|
|
2525
2246
|
}
|
|
2526
|
-
async function requestCalendarDisconnect(ctx) {
|
|
2527
|
-
return parseCalendarStatus(
|
|
2528
|
-
await calendarJson(ctx, "/disconnect", { method: "POST", body: JSON.stringify({ purge: false }) }),
|
|
2529
|
-
ctx.env
|
|
2530
|
-
);
|
|
2531
|
-
}
|
|
2532
2247
|
function parseCalendarStatus(raw, env) {
|
|
2533
2248
|
const outer = wrapped(raw, "calendar");
|
|
2534
2249
|
const value2 = record(outer.attempt) ?? record(outer.status) ?? outer;
|
|
@@ -2712,10 +2427,7 @@ async function applyCalendarBookingPage(ctx, bookingPageUrl, out) {
|
|
|
2712
2427
|
}
|
|
2713
2428
|
async function calendarDisconnect(options) {
|
|
2714
2429
|
if (!options.yes) throw new Error("calendar disconnect requires --yes");
|
|
2715
|
-
|
|
2716
|
-
const status = await requestCalendarDisconnect(ctx);
|
|
2717
|
-
out.log(`${ctx.env}: calendar disconnected; no calendar data was stored`);
|
|
2718
|
-
return status;
|
|
2430
|
+
return requireStudioHuman(options.configPath, "calendar disconnect", "calendar", options.env);
|
|
2719
2431
|
}
|
|
2720
2432
|
async function ensureCalendarConnected(ctx, options) {
|
|
2721
2433
|
const out = options.stdout ?? console;
|
|
@@ -2929,9 +2641,9 @@ var import_apps6 = require("@odla-ai/apps");
|
|
|
2929
2641
|
var import_node_path8 = require("path");
|
|
2930
2642
|
|
|
2931
2643
|
// src/version.ts
|
|
2932
|
-
var
|
|
2644
|
+
var import_node_fs8 = require("fs");
|
|
2933
2645
|
function cliVersion() {
|
|
2934
|
-
const pkg = JSON.parse((0,
|
|
2646
|
+
const pkg = JSON.parse((0, import_node_fs8.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
|
|
2935
2647
|
return pkg.version ?? "unknown";
|
|
2936
2648
|
}
|
|
2937
2649
|
|
|
@@ -2947,7 +2659,7 @@ var ConfigOperationCommandError = class extends Error {
|
|
|
2947
2659
|
|
|
2948
2660
|
// src/config-operation-validate.ts
|
|
2949
2661
|
var import_apps3 = require("@odla-ai/apps");
|
|
2950
|
-
var
|
|
2662
|
+
var import_node_fs9 = require("fs");
|
|
2951
2663
|
|
|
2952
2664
|
// src/config-reconcile-digest.ts
|
|
2953
2665
|
var import_node_crypto2 = require("crypto");
|
|
@@ -2983,7 +2695,7 @@ var SERVICE = /^[a-z][a-z0-9-]{0,39}$/;
|
|
|
2983
2695
|
function readPlan(path) {
|
|
2984
2696
|
let value2;
|
|
2985
2697
|
try {
|
|
2986
|
-
const raw = (0,
|
|
2698
|
+
const raw = (0, import_node_fs9.readFileSync)(path, "utf8");
|
|
2987
2699
|
if (Buffer.byteLength(raw) > 128 * 1024) throw new Error("plan exceeds 128 KiB");
|
|
2988
2700
|
value2 = JSON.parse(raw);
|
|
2989
2701
|
} catch (error) {
|
|
@@ -3126,11 +2838,11 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
|
|
|
3126
2838
|
}
|
|
3127
2839
|
if (code === "provision_approval_required") {
|
|
3128
2840
|
throw new Error(
|
|
3129
|
-
`${env}: the agent credential does not carry the owner-reviewed app.manage grant required to provision "${cfg.app.id}" (tenant ${tenantId}). The human owner id on the token is accountability, not agent authority.
|
|
2841
|
+
`${env}: the agent credential does not carry the owner-reviewed app.manage grant required to provision "${cfg.app.id}" (tenant ${tenantId}). The human owner id on the token is accountability, not agent authority. Run "odla-ai provision --request-grant --email <odla-account>" to open one fresh exact-project owner review; do not change app ownership unless the human account itself is not an owner`
|
|
3130
2842
|
);
|
|
3131
2843
|
}
|
|
3132
2844
|
throw new Error(
|
|
3133
|
-
`${env}: this credential lacks live app.manage authority for "${cfg.app.id}" (tenant ${tenantId}) \u2014 nothing was minted or written;
|
|
2845
|
+
`${env}: this credential lacks live app.manage authority for "${cfg.app.id}" (tenant ${tenantId}) \u2014 nothing was minted or written; run "odla-ai provision --request-grant --email <odla-account>" to open a fresh owner review. If the human account is not an owner, an existing owner must add it in signed-in Studio; an agent token cannot repair ownership`
|
|
3134
2846
|
);
|
|
3135
2847
|
}
|
|
3136
2848
|
throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText4(res)}`);
|
|
@@ -3696,7 +3408,7 @@ async function configPlan(options) {
|
|
|
3696
3408
|
apply,
|
|
3697
3409
|
nextActions: planNextActions(reconciliation, options.configPath)
|
|
3698
3410
|
};
|
|
3699
|
-
|
|
3411
|
+
printPlan(document2, options);
|
|
3700
3412
|
return document2;
|
|
3701
3413
|
}
|
|
3702
3414
|
async function inspectConfig(options) {
|
|
@@ -3736,7 +3448,7 @@ function printDiff(document2, options) {
|
|
|
3736
3448
|
printDifferences(out, document2);
|
|
3737
3449
|
printNext(out, document2.nextActions);
|
|
3738
3450
|
}
|
|
3739
|
-
function
|
|
3451
|
+
function printPlan(document2, options) {
|
|
3740
3452
|
const out = options.stdout ?? console;
|
|
3741
3453
|
if (options.json) {
|
|
3742
3454
|
out.log(JSON.stringify(document2, null, 2));
|
|
@@ -3839,12 +3551,12 @@ function quoteArg2(value2) {
|
|
|
3839
3551
|
|
|
3840
3552
|
// src/doctor-checks.ts
|
|
3841
3553
|
var import_node_child_process3 = require("child_process");
|
|
3842
|
-
var
|
|
3554
|
+
var import_node_fs11 = require("fs");
|
|
3843
3555
|
var import_node_path11 = require("path");
|
|
3844
3556
|
|
|
3845
3557
|
// src/wrangler.ts
|
|
3846
3558
|
var import_node_child_process2 = require("child_process");
|
|
3847
|
-
var
|
|
3559
|
+
var import_node_fs10 = require("fs");
|
|
3848
3560
|
var import_node_path10 = require("path");
|
|
3849
3561
|
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
3850
3562
|
const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -3860,14 +3572,14 @@ var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]
|
|
|
3860
3572
|
function findWranglerConfig(rootDir) {
|
|
3861
3573
|
for (const name of WRANGLER_CONFIG_FILES) {
|
|
3862
3574
|
const path = (0, import_node_path10.join)(rootDir, name);
|
|
3863
|
-
if ((0,
|
|
3575
|
+
if ((0, import_node_fs10.existsSync)(path)) return path;
|
|
3864
3576
|
}
|
|
3865
3577
|
return null;
|
|
3866
3578
|
}
|
|
3867
3579
|
function readWranglerConfig(path) {
|
|
3868
3580
|
if (path.endsWith(".toml")) return null;
|
|
3869
3581
|
try {
|
|
3870
|
-
return JSON.parse(stripJsonComments((0,
|
|
3582
|
+
return JSON.parse(stripJsonComments((0, import_node_fs10.readFileSync)(path, "utf8")));
|
|
3871
3583
|
} catch {
|
|
3872
3584
|
return null;
|
|
3873
3585
|
}
|
|
@@ -3975,7 +3687,7 @@ function wranglerWarnings(rootDir) {
|
|
|
3975
3687
|
const dir = (0, import_node_path11.resolve)(rootDir, assets.directory);
|
|
3976
3688
|
if (dir === (0, import_node_path11.resolve)(rootDir)) {
|
|
3977
3689
|
warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
|
|
3978
|
-
} else if ((0,
|
|
3690
|
+
} else if ((0, import_node_fs11.existsSync)((0, import_node_path11.join)(dir, "node_modules"))) {
|
|
3979
3691
|
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
3980
3692
|
}
|
|
3981
3693
|
}
|
|
@@ -4011,12 +3723,12 @@ function o11yProjectWarnings(rootDir) {
|
|
|
4011
3723
|
return warnings;
|
|
4012
3724
|
}
|
|
4013
3725
|
const main = typeof config.main === "string" ? (0, import_node_path11.resolve)(rootDir, config.main) : null;
|
|
4014
|
-
if (!main || !(0,
|
|
3726
|
+
if (!main || !(0, import_node_fs11.existsSync)(main)) {
|
|
4015
3727
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
|
|
4016
3728
|
} else {
|
|
4017
3729
|
let source = "";
|
|
4018
3730
|
try {
|
|
4019
|
-
source = (0,
|
|
3731
|
+
source = (0, import_node_fs11.readFileSync)(main, "utf8");
|
|
4020
3732
|
} catch {
|
|
4021
3733
|
}
|
|
4022
3734
|
if (!/\bwithObservability\b/.test(source)) {
|
|
@@ -4040,7 +3752,7 @@ function calendarProjectWarnings(rootDir) {
|
|
|
4040
3752
|
}
|
|
4041
3753
|
function readPackageJson(rootDir) {
|
|
4042
3754
|
try {
|
|
4043
|
-
return JSON.parse((0,
|
|
3755
|
+
return JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path11.join)(rootDir, "package.json"), "utf8"));
|
|
4044
3756
|
} catch {
|
|
4045
3757
|
return null;
|
|
4046
3758
|
}
|
|
@@ -4269,14 +3981,14 @@ function harnessOption(value2, flag) {
|
|
|
4269
3981
|
}
|
|
4270
3982
|
|
|
4271
3983
|
// src/init.ts
|
|
4272
|
-
var
|
|
3984
|
+
var import_node_fs12 = require("fs");
|
|
4273
3985
|
var import_node_path12 = require("path");
|
|
4274
3986
|
var import_apps9 = require("@odla-ai/apps");
|
|
4275
3987
|
function initProject(options) {
|
|
4276
3988
|
const out = options.stdout ?? console;
|
|
4277
3989
|
const rootDir = (0, import_node_path12.resolve)(options.rootDir ?? process.cwd());
|
|
4278
3990
|
const configPath = (0, import_node_path12.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
|
|
4279
|
-
if ((0,
|
|
3991
|
+
if ((0, import_node_fs12.existsSync)(configPath) && !options.force) {
|
|
4280
3992
|
throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
|
|
4281
3993
|
}
|
|
4282
3994
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
|
|
@@ -4292,10 +4004,10 @@ function initProject(options) {
|
|
|
4292
4004
|
}
|
|
4293
4005
|
}
|
|
4294
4006
|
const aiProvider = options.aiProvider;
|
|
4295
|
-
(0,
|
|
4296
|
-
(0,
|
|
4297
|
-
(0,
|
|
4298
|
-
(0,
|
|
4007
|
+
(0, import_node_fs12.mkdirSync)((0, import_node_path12.dirname)(configPath), { recursive: true });
|
|
4008
|
+
(0, import_node_fs12.mkdirSync)((0, import_node_path12.resolve)(rootDir, "src/odla"), { recursive: true });
|
|
4009
|
+
(0, import_node_fs12.mkdirSync)((0, import_node_path12.resolve)(rootDir, ".odla"), { recursive: true });
|
|
4010
|
+
(0, import_node_fs12.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
4299
4011
|
writeIfMissing((0, import_node_path12.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
4300
4012
|
writeIfMissing((0, import_node_path12.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
4301
4013
|
ensureGitignore(rootDir);
|
|
@@ -4304,8 +4016,8 @@ function initProject(options) {
|
|
|
4304
4016
|
out.log("updated .gitignore for local odla credentials");
|
|
4305
4017
|
}
|
|
4306
4018
|
function writeIfMissing(path, text2) {
|
|
4307
|
-
if ((0,
|
|
4308
|
-
(0,
|
|
4019
|
+
if ((0, import_node_fs12.existsSync)(path)) return;
|
|
4020
|
+
(0, import_node_fs12.writeFileSync)(path, text2);
|
|
4309
4021
|
}
|
|
4310
4022
|
function configTemplate(input) {
|
|
4311
4023
|
const calendar = input.services.includes("calendar") ? ` calendar: {
|
|
@@ -4512,7 +4224,9 @@ async function secretsSetClerkKey(options) {
|
|
|
4512
4224
|
if (value2.startsWith("sk_live_") && !PROD_ENV_NAMES2.has(options.env) && !options.yes) {
|
|
4513
4225
|
throw new Error(`refusing to store an sk_live_ Clerk key for "${options.env}" without --yes (live users would sync into a non-prod tenant)`);
|
|
4514
4226
|
}
|
|
4515
|
-
const token = await getDeveloperToken(cfg, options, doFetch, out
|
|
4227
|
+
const token = await getDeveloperToken(cfg, options, doFetch, out, {
|
|
4228
|
+
optionalProjectCapabilities: ["app.manage"]
|
|
4229
|
+
});
|
|
4516
4230
|
const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/clerk-secret`, {
|
|
4517
4231
|
method: "POST",
|
|
4518
4232
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
@@ -4542,7 +4256,7 @@ async function resolveVaultWrite(options) {
|
|
|
4542
4256
|
}
|
|
4543
4257
|
|
|
4544
4258
|
// src/skill.ts
|
|
4545
|
-
var
|
|
4259
|
+
var import_node_fs13 = require("fs");
|
|
4546
4260
|
var import_node_os2 = require("os");
|
|
4547
4261
|
var import_node_path13 = require("path");
|
|
4548
4262
|
var import_node_url2 = require("url");
|
|
@@ -4639,7 +4353,7 @@ function installSkill(options = {}) {
|
|
|
4639
4353
|
plans.set(target, { target, content: content2, boundary, managedMerge });
|
|
4640
4354
|
};
|
|
4641
4355
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
4642
|
-
for (const rel of files) plan((0, import_node_path13.join)(targetDir2, rel), (0,
|
|
4356
|
+
for (const rel of files) plan((0, import_node_path13.join)(targetDir2, rel), (0, import_node_fs13.readFileSync)((0, import_node_path13.join)(sourceDir, rel), "utf8"), false, boundary);
|
|
4643
4357
|
};
|
|
4644
4358
|
let targetDir;
|
|
4645
4359
|
if (options.global) {
|
|
@@ -4659,7 +4373,7 @@ function installSkill(options = {}) {
|
|
|
4659
4373
|
for (const harness of harnesses) rememberTarget(harness, sharedRoot);
|
|
4660
4374
|
if (harnesses.includes("claude")) {
|
|
4661
4375
|
for (const skill of skillNames(files)) {
|
|
4662
|
-
const canonical = (0,
|
|
4376
|
+
const canonical = (0, import_node_fs13.readFileSync)((0, import_node_path13.join)(sourceDir, skill, "SKILL.md"), "utf8");
|
|
4663
4377
|
plan((0, import_node_path13.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
|
|
4664
4378
|
}
|
|
4665
4379
|
rememberTarget("claude", claudeRoot);
|
|
@@ -4694,11 +4408,11 @@ function installSkill(options = {}) {
|
|
|
4694
4408
|
conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
|
|
4695
4409
|
continue;
|
|
4696
4410
|
}
|
|
4697
|
-
if (!(0,
|
|
4411
|
+
if (!(0, import_node_fs13.existsSync)(file.target)) {
|
|
4698
4412
|
writtenPaths.add(file.target);
|
|
4699
4413
|
continue;
|
|
4700
4414
|
}
|
|
4701
|
-
const current = (0,
|
|
4415
|
+
const current = (0, import_node_fs13.readFileSync)(file.target, "utf8");
|
|
4702
4416
|
if (current === file.content) {
|
|
4703
4417
|
unchangedPaths.add(file.target);
|
|
4704
4418
|
} else if (file.managedMerge || options.force) {
|
|
@@ -4715,9 +4429,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
4715
4429
|
);
|
|
4716
4430
|
}
|
|
4717
4431
|
for (const file of plans.values()) {
|
|
4718
|
-
if (!(0,
|
|
4719
|
-
(0,
|
|
4720
|
-
(0,
|
|
4432
|
+
if (!(0, import_node_fs13.existsSync)(file.target) || (0, import_node_fs13.readFileSync)(file.target, "utf8") !== file.content) {
|
|
4433
|
+
(0, import_node_fs13.mkdirSync)((0, import_node_path13.dirname)(file.target), { recursive: true });
|
|
4434
|
+
(0, import_node_fs13.writeFileSync)(file.target, file.content);
|
|
4721
4435
|
}
|
|
4722
4436
|
}
|
|
4723
4437
|
const skills = skillNames(files);
|
|
@@ -4758,9 +4472,9 @@ function normalizeHarnesses(values, global) {
|
|
|
4758
4472
|
function managedFileContent(path, block, force, boundary) {
|
|
4759
4473
|
const symlink = symlinkedComponent(boundary, path);
|
|
4760
4474
|
if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
|
|
4761
|
-
if (!(0,
|
|
4475
|
+
if (!(0, import_node_fs13.existsSync)(path)) return `${block}
|
|
4762
4476
|
`;
|
|
4763
|
-
const current = (0,
|
|
4477
|
+
const current = (0, import_node_fs13.readFileSync)(path, "utf8");
|
|
4764
4478
|
const start = "<!-- odla-ai agent setup:start -->";
|
|
4765
4479
|
const end = "<!-- odla-ai agent setup:end -->";
|
|
4766
4480
|
const startAt = current.indexOf(start);
|
|
@@ -4789,7 +4503,7 @@ function symlinkedComponent(boundary, target) {
|
|
|
4789
4503
|
for (const part of rel.split(import_node_path13.sep).filter(Boolean)) {
|
|
4790
4504
|
current = (0, import_node_path13.join)(current, part);
|
|
4791
4505
|
try {
|
|
4792
|
-
if ((0,
|
|
4506
|
+
if ((0, import_node_fs13.lstatSync)(current).isSymbolicLink()) return current;
|
|
4793
4507
|
} catch (error) {
|
|
4794
4508
|
if (error.code !== "ENOENT") throw error;
|
|
4795
4509
|
}
|
|
@@ -4800,10 +4514,10 @@ function skillNames(files) {
|
|
|
4800
4514
|
return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
|
|
4801
4515
|
}
|
|
4802
4516
|
function listFiles(dir) {
|
|
4803
|
-
if (!(0,
|
|
4517
|
+
if (!(0, import_node_fs13.existsSync)(dir)) return [];
|
|
4804
4518
|
const results = [];
|
|
4805
4519
|
const walk = (current) => {
|
|
4806
|
-
for (const entry of (0,
|
|
4520
|
+
for (const entry of (0, import_node_fs13.readdirSync)(current, { withFileTypes: true })) {
|
|
4807
4521
|
const path = (0, import_node_path13.join)(current, entry.name);
|
|
4808
4522
|
if (entry.isDirectory()) walk(path);
|
|
4809
4523
|
else results.push((0, import_node_path13.relative)(dir, path));
|
|
@@ -5120,7 +4834,7 @@ async function projectCommand(command, parsed, deps) {
|
|
|
5120
4834
|
}
|
|
5121
4835
|
|
|
5122
4836
|
// src/code-connect.ts
|
|
5123
|
-
var
|
|
4837
|
+
var import_node_fs14 = require("fs");
|
|
5124
4838
|
var import_node_os4 = require("os");
|
|
5125
4839
|
var import_node_path15 = require("path");
|
|
5126
4840
|
|
|
@@ -5208,15 +4922,15 @@ function encodeAgentInput(message2) {
|
|
|
5208
4922
|
// ../harness/dist/chunk-PHXQH4YM.js
|
|
5209
4923
|
var import_child_process = require("child_process");
|
|
5210
4924
|
var import_fs = require("fs");
|
|
5211
|
-
var
|
|
4925
|
+
var import_promises2 = require("fs/promises");
|
|
5212
4926
|
var import_path = require("path");
|
|
5213
4927
|
var import_process = require("process");
|
|
5214
|
-
var
|
|
4928
|
+
var import_promises3 = require("fs/promises");
|
|
5215
4929
|
var import_os = require("os");
|
|
5216
4930
|
var import_path2 = require("path");
|
|
5217
4931
|
var import_child_process2 = require("child_process");
|
|
5218
4932
|
var import_path3 = require("path");
|
|
5219
|
-
var
|
|
4933
|
+
var import_promises4 = require("fs/promises");
|
|
5220
4934
|
var import_os2 = require("os");
|
|
5221
4935
|
var import_path4 = require("path");
|
|
5222
4936
|
var import_child_process3 = require("child_process");
|
|
@@ -5227,7 +4941,7 @@ function assertPinnedImage(image) {
|
|
|
5227
4941
|
async function commandAvailable(engine) {
|
|
5228
4942
|
for (const directory of (process.env.PATH ?? "").split(import_path.delimiter).filter(Boolean)) {
|
|
5229
4943
|
try {
|
|
5230
|
-
await (0,
|
|
4944
|
+
await (0, import_promises2.access)((0, import_path.join)(directory, engine), import_fs.constants.X_OK);
|
|
5231
4945
|
return true;
|
|
5232
4946
|
} catch {
|
|
5233
4947
|
}
|
|
@@ -5513,7 +5227,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
|
|
|
5513
5227
|
}
|
|
5514
5228
|
async function materializeGitTree(source, commitSha, options = {}) {
|
|
5515
5229
|
if (!/^[0-9a-f]{40}$/.test(commitSha)) throw new TypeError("Git tree requires an exact commit SHA");
|
|
5516
|
-
const sourceDir = await (0,
|
|
5230
|
+
const sourceDir = await (0, import_promises3.realpath)((0, import_path2.resolve)(source));
|
|
5517
5231
|
const maxFiles = options.maxFiles ?? 2e4;
|
|
5518
5232
|
const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
|
|
5519
5233
|
const inventory = (await gitOutput(sourceDir, ["ls-tree", "-rz", commitSha], 16 * 1024 * 1024)).toString("utf8").split("\0").filter(Boolean);
|
|
@@ -5522,9 +5236,9 @@ async function materializeGitTree(source, commitSha, options = {}) {
|
|
|
5522
5236
|
return match && allowedWorkspacePath(match[3]) ? [{ mode: match[1], hash: match[2], path: match[3] }] : [];
|
|
5523
5237
|
});
|
|
5524
5238
|
if (entries.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
|
|
5525
|
-
const root = await (0,
|
|
5239
|
+
const root = await (0, import_promises3.mkdtemp)((0, import_path2.join)(options.tempRoot ?? (0, import_os.tmpdir)(), "odla-git-tree-"));
|
|
5526
5240
|
const targetRoot = (0, import_path2.join)(root, "source");
|
|
5527
|
-
await (0,
|
|
5241
|
+
await (0, import_promises3.mkdir)(targetRoot);
|
|
5528
5242
|
let byteCount = 0;
|
|
5529
5243
|
try {
|
|
5530
5244
|
const blobs = await gitBlobs(sourceDir, entries, maxBytes);
|
|
@@ -5534,18 +5248,18 @@ async function materializeGitTree(source, commitSha, options = {}) {
|
|
|
5534
5248
|
if (byteCount > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
5535
5249
|
const target = (0, import_path2.resolve)(targetRoot, entry.path);
|
|
5536
5250
|
if (!target.startsWith(`${(0, import_path2.resolve)(targetRoot)}${import_path2.sep}`)) throw new TypeError("Git tree path escapes workspace");
|
|
5537
|
-
await (0,
|
|
5538
|
-
await (0,
|
|
5251
|
+
await (0, import_promises3.mkdir)((0, import_path2.resolve)(target, ".."), { recursive: true });
|
|
5252
|
+
await (0, import_promises3.writeFile)(target, content2, { flag: "wx", mode: entry.mode === "100755" ? 493 : 420 });
|
|
5539
5253
|
}
|
|
5540
5254
|
return {
|
|
5541
5255
|
root,
|
|
5542
5256
|
sourceDir: targetRoot,
|
|
5543
5257
|
fileCount: entries.length,
|
|
5544
5258
|
byteCount,
|
|
5545
|
-
cleanup: () => (0,
|
|
5259
|
+
cleanup: () => (0, import_promises3.rm)(root, { recursive: true, force: true })
|
|
5546
5260
|
};
|
|
5547
5261
|
} catch (error) {
|
|
5548
|
-
await (0,
|
|
5262
|
+
await (0, import_promises3.rm)(root, { recursive: true, force: true });
|
|
5549
5263
|
throw error;
|
|
5550
5264
|
}
|
|
5551
5265
|
}
|
|
@@ -5553,7 +5267,7 @@ async function sourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
|
5553
5267
|
const files = [];
|
|
5554
5268
|
let bytes = 0;
|
|
5555
5269
|
const walk = async (dir) => {
|
|
5556
|
-
for (const entry of await (0,
|
|
5270
|
+
for (const entry of await (0, import_promises4.readdir)(dir, { withFileTypes: true })) {
|
|
5557
5271
|
if (entry.isDirectory() && SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
|
|
5558
5272
|
if (!entry.isDirectory() && SECRET_WORKSPACE_FILE.test(entry.name)) continue;
|
|
5559
5273
|
const path = (0, import_path4.join)(dir, entry.name);
|
|
@@ -5563,7 +5277,7 @@ async function sourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
|
5563
5277
|
continue;
|
|
5564
5278
|
}
|
|
5565
5279
|
if (!entry.isFile()) continue;
|
|
5566
|
-
const metadata2 = await (0,
|
|
5280
|
+
const metadata2 = await (0, import_promises4.stat)(path);
|
|
5567
5281
|
bytes += metadata2.size;
|
|
5568
5282
|
if (files.length + 1 > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
|
|
5569
5283
|
if (bytes > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
@@ -5612,7 +5326,7 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
|
5612
5326
|
if (!source.startsWith(`${root}${import_path4.sep}`)) throw new TypeError("git file path escapes workspace");
|
|
5613
5327
|
let metadata2;
|
|
5614
5328
|
try {
|
|
5615
|
-
metadata2 = await (0,
|
|
5329
|
+
metadata2 = await (0, import_promises4.lstat)(source);
|
|
5616
5330
|
} catch (error) {
|
|
5617
5331
|
if (error.code === "ENOENT") continue;
|
|
5618
5332
|
throw error;
|
|
@@ -5627,9 +5341,9 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
|
5627
5341
|
async function copyTree(files, destination) {
|
|
5628
5342
|
for (const file of files) {
|
|
5629
5343
|
const target = (0, import_path4.join)(destination, file.relativePath);
|
|
5630
|
-
await (0,
|
|
5631
|
-
await (0,
|
|
5632
|
-
await (0,
|
|
5344
|
+
await (0, import_promises4.mkdir)((0, import_path4.resolve)(target, ".."), { recursive: true });
|
|
5345
|
+
await (0, import_promises4.copyFile)(file.source, target);
|
|
5346
|
+
await (0, import_promises4.chmod)(target, file.mode);
|
|
5633
5347
|
}
|
|
5634
5348
|
}
|
|
5635
5349
|
async function captureGitDiff(root, maxBytes) {
|
|
@@ -5666,13 +5380,13 @@ async function captureGitDiff(root, maxBytes) {
|
|
|
5666
5380
|
return Buffer.concat(stdout).toString("utf8").replaceAll("a/baseline/", "a/").replaceAll("a/workspace/", "a/").replaceAll("b/baseline/", "b/").replaceAll("b/workspace/", "b/").replaceAll("--- a/baseline", "--- a").replaceAll("+++ b/workspace", "+++ b");
|
|
5667
5381
|
}
|
|
5668
5382
|
async function stageWorkspace(source, options = {}) {
|
|
5669
|
-
const sourceDir = await (0,
|
|
5670
|
-
const sourceStat = await (0,
|
|
5383
|
+
const sourceDir = await (0, import_promises4.realpath)((0, import_path4.resolve)(source));
|
|
5384
|
+
const sourceStat = await (0, import_promises4.stat)(sourceDir);
|
|
5671
5385
|
if (!sourceStat.isDirectory()) throw new TypeError("workspace source must be a directory");
|
|
5672
|
-
const root = await (0,
|
|
5386
|
+
const root = await (0, import_promises4.mkdtemp)((0, import_path4.join)(options.tempRoot ?? (0, import_os2.tmpdir)(), "odla-harness-"));
|
|
5673
5387
|
const baselineDir = (0, import_path4.join)(root, "baseline");
|
|
5674
5388
|
const workspaceDir = (0, import_path4.join)(root, "workspace");
|
|
5675
|
-
await Promise.all([(0,
|
|
5389
|
+
await Promise.all([(0, import_promises4.mkdir)(baselineDir), (0, import_promises4.mkdir)(workspaceDir)]);
|
|
5676
5390
|
try {
|
|
5677
5391
|
const maxFiles = options.maxFiles ?? 2e4;
|
|
5678
5392
|
const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
|
|
@@ -5685,26 +5399,26 @@ async function stageWorkspace(source, options = {}) {
|
|
|
5685
5399
|
fileCount: files.length,
|
|
5686
5400
|
byteCount: files.reduce((sum, file) => sum + file.bytes, 0),
|
|
5687
5401
|
patch: (maxBytes2) => captureGitDiff(root, maxBytes2),
|
|
5688
|
-
cleanup: () => (0,
|
|
5402
|
+
cleanup: () => (0, import_promises4.rm)(root, { recursive: true, force: true })
|
|
5689
5403
|
};
|
|
5690
5404
|
} catch (error) {
|
|
5691
|
-
await (0,
|
|
5405
|
+
await (0, import_promises4.rm)(root, { recursive: true, force: true });
|
|
5692
5406
|
throw error;
|
|
5693
5407
|
}
|
|
5694
5408
|
}
|
|
5695
5409
|
async function stageWorkspacePair(baselineSource, workspaceSource, options = {}) {
|
|
5696
|
-
const baselineDirSource = await (0,
|
|
5697
|
-
const workspaceDirSource = await (0,
|
|
5410
|
+
const baselineDirSource = await (0, import_promises4.realpath)((0, import_path4.resolve)(baselineSource));
|
|
5411
|
+
const workspaceDirSource = await (0, import_promises4.realpath)((0, import_path4.resolve)(workspaceSource));
|
|
5698
5412
|
const maxFiles = options.maxFiles ?? 2e4;
|
|
5699
5413
|
const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
|
|
5700
5414
|
const [baselineFiles, workspaceFiles] = await Promise.all([
|
|
5701
5415
|
sourceFiles(baselineDirSource, maxFiles, maxBytes),
|
|
5702
5416
|
sourceFiles(workspaceDirSource, maxFiles, maxBytes)
|
|
5703
5417
|
]);
|
|
5704
|
-
const root = await (0,
|
|
5418
|
+
const root = await (0, import_promises4.mkdtemp)((0, import_path4.join)(options.tempRoot ?? (0, import_os2.tmpdir)(), "odla-harness-"));
|
|
5705
5419
|
const baselineDir = (0, import_path4.join)(root, "baseline");
|
|
5706
5420
|
const workspaceDir = (0, import_path4.join)(root, "workspace");
|
|
5707
|
-
await Promise.all([(0,
|
|
5421
|
+
await Promise.all([(0, import_promises4.mkdir)(baselineDir), (0, import_promises4.mkdir)(workspaceDir)]);
|
|
5708
5422
|
try {
|
|
5709
5423
|
await Promise.all([copyTree(baselineFiles, baselineDir), copyTree(workspaceFiles, workspaceDir)]);
|
|
5710
5424
|
return {
|
|
@@ -5714,17 +5428,17 @@ async function stageWorkspacePair(baselineSource, workspaceSource, options = {})
|
|
|
5714
5428
|
fileCount: workspaceFiles.length,
|
|
5715
5429
|
byteCount: workspaceFiles.reduce((sum, file) => sum + file.bytes, 0),
|
|
5716
5430
|
patch: (maxPatchBytes) => captureGitDiff(root, maxPatchBytes),
|
|
5717
|
-
cleanup: () => (0,
|
|
5431
|
+
cleanup: () => (0, import_promises4.rm)(root, { recursive: true, force: true })
|
|
5718
5432
|
};
|
|
5719
5433
|
} catch (error) {
|
|
5720
|
-
await (0,
|
|
5434
|
+
await (0, import_promises4.rm)(root, { recursive: true, force: true });
|
|
5721
5435
|
throw error;
|
|
5722
5436
|
}
|
|
5723
5437
|
}
|
|
5724
5438
|
|
|
5725
5439
|
// ../harness/dist/chunk-GMVZ4LZH.js
|
|
5726
5440
|
var import_crypto = require("crypto");
|
|
5727
|
-
var
|
|
5441
|
+
var import_promises5 = require("fs/promises");
|
|
5728
5442
|
var import_path5 = require("path");
|
|
5729
5443
|
|
|
5730
5444
|
// ../camel/dist/chunk-7FHPOQVP.js
|
|
@@ -6061,19 +5775,19 @@ function validateSnapshot(snapshot, limits) {
|
|
|
6061
5775
|
|
|
6062
5776
|
// ../harness/dist/chunk-GMVZ4LZH.js
|
|
6063
5777
|
var import_child_process4 = require("child_process");
|
|
6064
|
-
var
|
|
5778
|
+
var import_promises6 = require("fs/promises");
|
|
6065
5779
|
var import_path6 = require("path");
|
|
6066
5780
|
var import_child_process5 = require("child_process");
|
|
6067
5781
|
var import_process2 = require("process");
|
|
6068
5782
|
var import_crypto2 = require("crypto");
|
|
6069
5783
|
var import_crypto3 = require("crypto");
|
|
6070
5784
|
var import_fs2 = require("fs");
|
|
6071
|
-
var
|
|
5785
|
+
var import_promises7 = require("fs/promises");
|
|
6072
5786
|
var import_path7 = require("path");
|
|
6073
|
-
var
|
|
5787
|
+
var import_promises8 = require("fs/promises");
|
|
6074
5788
|
var import_os3 = require("os");
|
|
6075
5789
|
var import_path8 = require("path");
|
|
6076
|
-
var
|
|
5790
|
+
var import_promises9 = require("fs/promises");
|
|
6077
5791
|
var import_path9 = require("path");
|
|
6078
5792
|
|
|
6079
5793
|
// ../camel/dist/chunk-4EIRFS3A.js
|
|
@@ -6365,7 +6079,7 @@ var import_crypto4 = require("crypto");
|
|
|
6365
6079
|
async function digestStagedWorkspace(root, limits) {
|
|
6366
6080
|
const files = [];
|
|
6367
6081
|
const walk = async (directory) => {
|
|
6368
|
-
const entries = await (0,
|
|
6082
|
+
const entries = await (0, import_promises5.readdir)(directory, { withFileTypes: true });
|
|
6369
6083
|
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
6370
6084
|
if (entry.isSymbolicLink()) throw new TypeError("workspace digest refuses symbolic links");
|
|
6371
6085
|
const target = (0, import_path5.resolve)(directory, entry.name);
|
|
@@ -6380,7 +6094,7 @@ async function digestStagedWorkspace(root, limits) {
|
|
|
6380
6094
|
const hash = (0, import_crypto.createHash)("sha256");
|
|
6381
6095
|
let bytes = 0;
|
|
6382
6096
|
for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
|
|
6383
|
-
const content2 = await (0,
|
|
6097
|
+
const content2 = await (0, import_promises5.readFile)(file.target);
|
|
6384
6098
|
bytes += Buffer.byteLength(file.path) + content2.byteLength;
|
|
6385
6099
|
if (bytes > limits.maxBytes) throw new TypeError("workspace digest exceeds its byte bound");
|
|
6386
6100
|
hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content2.byteLength}:`);
|
|
@@ -6706,7 +6420,7 @@ async function applyCodePatch(workspaceDir, patch2, paths) {
|
|
|
6706
6420
|
await gitApply(workspaceDir, patch2, false);
|
|
6707
6421
|
for (const path of paths) {
|
|
6708
6422
|
try {
|
|
6709
|
-
const info = await (0,
|
|
6423
|
+
const info = await (0, import_promises6.lstat)(resolveCodePath(workspaceDir, path));
|
|
6710
6424
|
if (info.isSymbolicLink() || !info.isFile() && !info.isDirectory()) {
|
|
6711
6425
|
throw new TypeError("patch created a non-regular workspace entry");
|
|
6712
6426
|
}
|
|
@@ -6997,7 +6711,7 @@ async function inspectArtifacts(workspaceDir, recipe2) {
|
|
|
6997
6711
|
for (const artifact of recipe2.expectedArtifacts ?? []) {
|
|
6998
6712
|
try {
|
|
6999
6713
|
const path = (0, import_path7.join)(workspaceDir, artifact.path);
|
|
7000
|
-
const info = await (0,
|
|
6714
|
+
const info = await (0, import_promises7.lstat)(path);
|
|
7001
6715
|
if (!info.isFile() || info.isSymbolicLink()) {
|
|
7002
6716
|
receipts.push({ artifactId: artifact.id, status: "invalid", bytes: null, digest: null });
|
|
7003
6717
|
} else if (info.size > artifact.maximumBytes) {
|
|
@@ -7178,9 +6892,9 @@ var RESERVED2 = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_mod
|
|
|
7178
6892
|
var SECRET2 = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
|
|
7179
6893
|
async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os3.tmpdir)()) {
|
|
7180
6894
|
if (!snapshot.files.length || snapshot.files.length > 1e4) throw new TypeError("Code source file count is invalid");
|
|
7181
|
-
const root = await (0,
|
|
6895
|
+
const root = await (0, import_promises8.mkdtemp)((0, import_path8.join)(tempRoot, "odla-code-source-"));
|
|
7182
6896
|
const sourceDir = (0, import_path8.join)(root, "source");
|
|
7183
|
-
await (0,
|
|
6897
|
+
await (0, import_promises8.mkdir)(sourceDir);
|
|
7184
6898
|
const seen = /* @__PURE__ */ new Set();
|
|
7185
6899
|
let bytes = 0;
|
|
7186
6900
|
try {
|
|
@@ -7192,8 +6906,8 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os3.
|
|
|
7192
6906
|
if (bytes > 16 * 1024 * 1024) throw new TypeError("Code source exceeds its byte bound");
|
|
7193
6907
|
const target = (0, import_path8.resolve)(sourceDir, file.path);
|
|
7194
6908
|
if (!target.startsWith(`${(0, import_path8.resolve)(sourceDir)}${import_path8.sep}`)) throw new TypeError("Code source path escapes its root");
|
|
7195
|
-
await (0,
|
|
7196
|
-
await (0,
|
|
6909
|
+
await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
|
|
6910
|
+
await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 420 });
|
|
7197
6911
|
}
|
|
7198
6912
|
for (const reference of snapshot.references ?? []) {
|
|
7199
6913
|
validateAlias(reference.alias);
|
|
@@ -7207,13 +6921,13 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os3.
|
|
|
7207
6921
|
if (bytes > 80 * 1024 * 1024) throw new TypeError("Code source set exceeds its byte bound");
|
|
7208
6922
|
const target = (0, import_path8.resolve)(sourceDir, path);
|
|
7209
6923
|
if (!target.startsWith(`${(0, import_path8.resolve)(sourceDir)}${import_path8.sep}`)) throw new TypeError("Code reference path escapes its root");
|
|
7210
|
-
await (0,
|
|
7211
|
-
await (0,
|
|
6924
|
+
await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
|
|
6925
|
+
await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 292 });
|
|
7212
6926
|
}
|
|
7213
6927
|
}
|
|
7214
|
-
return { sourceDir, cleanup: () => (0,
|
|
6928
|
+
return { sourceDir, cleanup: () => (0, import_promises8.rm)(root, { recursive: true, force: true }) };
|
|
7215
6929
|
} catch (cause) {
|
|
7216
|
-
await (0,
|
|
6930
|
+
await (0, import_promises8.rm)(root, { recursive: true, force: true });
|
|
7217
6931
|
throw cause;
|
|
7218
6932
|
}
|
|
7219
6933
|
}
|
|
@@ -7234,8 +6948,8 @@ async function attachCodeRuntimeReferences(workspace, references) {
|
|
|
7234
6948
|
for (const root of [workspace.baselineDir, workspace.workspaceDir]) {
|
|
7235
6949
|
const target = (0, import_path8.resolve)(root, path);
|
|
7236
6950
|
if (!target.startsWith(`${(0, import_path8.resolve)(root)}${import_path8.sep}`)) throw new TypeError("Code reference path escapes its root");
|
|
7237
|
-
await (0,
|
|
7238
|
-
await (0,
|
|
6951
|
+
await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
|
|
6952
|
+
await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 292 });
|
|
7239
6953
|
}
|
|
7240
6954
|
}
|
|
7241
6955
|
}
|
|
@@ -7421,11 +7135,11 @@ async function read(context, request2, options, policy) {
|
|
|
7421
7135
|
const allowed = await policy.read(policyContext(context, request2, options, { paths, path, startLine, endLine }));
|
|
7422
7136
|
if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
|
|
7423
7137
|
const target = resolveCodePath(context.workspaceDir, path);
|
|
7424
|
-
const info = await (0,
|
|
7138
|
+
const info = await (0, import_promises9.stat)(target);
|
|
7425
7139
|
if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
|
|
7426
7140
|
throw new TypeError("file is not a bounded regular source file");
|
|
7427
7141
|
}
|
|
7428
|
-
const source = await (0,
|
|
7142
|
+
const source = await (0, import_promises9.readFile)(target);
|
|
7429
7143
|
if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
|
|
7430
7144
|
const lines = source.toString("utf8").split("\n");
|
|
7431
7145
|
const content2 = lines.slice(startLine - 1, endLine).join("\n");
|
|
@@ -7502,7 +7216,7 @@ function policyContext(context, request2, options, extra) {
|
|
|
7502
7216
|
async function registeredFiles(root, limit) {
|
|
7503
7217
|
const paths = [];
|
|
7504
7218
|
const walk = async (directory) => {
|
|
7505
|
-
for (const entry of await (0,
|
|
7219
|
+
for (const entry of await (0, import_promises9.readdir)(directory, { withFileTypes: true })) {
|
|
7506
7220
|
if (entry.isSymbolicLink()) throw new TypeError("workspace contains a symbolic link");
|
|
7507
7221
|
const target = (0, import_path9.resolve)(directory, entry.name);
|
|
7508
7222
|
if (entry.isDirectory()) await walk(target);
|
|
@@ -8075,18 +7789,6 @@ function githubRepositoryName(value2) {
|
|
|
8075
7789
|
}
|
|
8076
7790
|
return `${owner}/${name}`;
|
|
8077
7791
|
}
|
|
8078
|
-
function trustedGitHubInstallUrl(value2) {
|
|
8079
|
-
let url;
|
|
8080
|
-
try {
|
|
8081
|
-
url = new URL(value2);
|
|
8082
|
-
} catch {
|
|
8083
|
-
throw new Error("odla.ai returned an invalid GitHub installation URL");
|
|
8084
|
-
}
|
|
8085
|
-
if (url.protocol !== "https:" || url.hostname !== "github.com" || url.username || url.password) {
|
|
8086
|
-
throw new Error("odla.ai returned an untrusted GitHub installation URL");
|
|
8087
|
-
}
|
|
8088
|
-
return url.toString();
|
|
8089
|
-
}
|
|
8090
7792
|
function hostedPollInterval(value2 = 2e3) {
|
|
8091
7793
|
if (!Number.isSafeInteger(value2) || value2 < 100 || value2 > 3e4) {
|
|
8092
7794
|
throw new Error("poll interval must be 100-30000ms");
|
|
@@ -8122,51 +7824,6 @@ function hostedSecurityCredential(value2) {
|
|
|
8122
7824
|
}
|
|
8123
7825
|
|
|
8124
7826
|
// src/security-hosted-github.ts
|
|
8125
|
-
async function connectGitHubSecuritySource(options) {
|
|
8126
|
-
const out = options.stdout ?? console;
|
|
8127
|
-
const repository = options.repository === void 0 ? void 0 : githubRepositoryName(options.repository);
|
|
8128
|
-
const attempt = await requestHostedSecurityJson(
|
|
8129
|
-
options,
|
|
8130
|
-
"/registry/github/connect",
|
|
8131
|
-
{
|
|
8132
|
-
method: "POST",
|
|
8133
|
-
body: JSON.stringify({
|
|
8134
|
-
appId: hostedIdentifier(options.appId, "appId"),
|
|
8135
|
-
env: hostedIdentifier(options.env, "env"),
|
|
8136
|
-
...repository === void 0 ? {} : { repository }
|
|
8137
|
-
})
|
|
8138
|
-
},
|
|
8139
|
-
"start GitHub connection"
|
|
8140
|
-
);
|
|
8141
|
-
const serverExpiry = Date.parse(attempt?.expiresAt ?? "");
|
|
8142
|
-
if (!attempt?.attemptId || !attempt.installUrl || !Number.isFinite(serverExpiry)) {
|
|
8143
|
-
throw new Error("odla.ai returned an invalid GitHub connection attempt");
|
|
8144
|
-
}
|
|
8145
|
-
const installUrl = trustedGitHubInstallUrl(attempt.installUrl);
|
|
8146
|
-
out.log(`GitHub approval: ${installUrl}`);
|
|
8147
|
-
if (options.open !== false) {
|
|
8148
|
-
await (options.openInstallUrl ?? openUrl)(installUrl);
|
|
8149
|
-
out.log("github: opened installation approval in your browser");
|
|
8150
|
-
}
|
|
8151
|
-
const interval = hostedPollInterval(options.pollIntervalMs);
|
|
8152
|
-
const now = options.now ?? Date.now;
|
|
8153
|
-
const deadline = Math.min(serverExpiry, now() + hostedPollTimeout(options.pollTimeoutMs));
|
|
8154
|
-
const wait2 = options.wait ?? waitForHostedPoll;
|
|
8155
|
-
while (true) {
|
|
8156
|
-
const state2 = await requestHostedSecurityJson(
|
|
8157
|
-
options,
|
|
8158
|
-
`/registry/github/connect/${encodeURIComponent(attempt.attemptId)}`,
|
|
8159
|
-
{},
|
|
8160
|
-
"check GitHub connection"
|
|
8161
|
-
);
|
|
8162
|
-
if (state2.status !== "pending") {
|
|
8163
|
-
if (state2.status === "connected") return state2;
|
|
8164
|
-
throw new Error(`GitHub connection ${state2.status}${state2.failureCode ? `: ${state2.failureCode}` : ""}`);
|
|
8165
|
-
}
|
|
8166
|
-
if (now() >= deadline) throw new Error("GitHub connection approval timed out");
|
|
8167
|
-
await wait2(Math.min(interval, Math.max(0, deadline - now())), options.signal);
|
|
8168
|
-
}
|
|
8169
|
-
}
|
|
8170
7827
|
async function listGitHubSecuritySources(options) {
|
|
8171
7828
|
const appId = hostedIdentifier(options.appId, "appId");
|
|
8172
7829
|
const env = hostedIdentifier(options.env, "env");
|
|
@@ -8178,16 +7835,6 @@ async function listGitHubSecuritySources(options) {
|
|
|
8178
7835
|
);
|
|
8179
7836
|
return Array.isArray(body.sources) ? body.sources : [];
|
|
8180
7837
|
}
|
|
8181
|
-
async function disconnectGitHubSecuritySource(options) {
|
|
8182
|
-
const appId = hostedIdentifier(options.appId, "appId");
|
|
8183
|
-
const sourceId = hostedIdentifier(options.sourceId, "sourceId");
|
|
8184
|
-
await requestHostedSecurityJson(
|
|
8185
|
-
options,
|
|
8186
|
-
`/registry/apps/${encodeURIComponent(appId)}/github/sources/${encodeURIComponent(sourceId)}`,
|
|
8187
|
-
{ method: "DELETE" },
|
|
8188
|
-
"disconnect GitHub security source"
|
|
8189
|
-
);
|
|
8190
|
-
}
|
|
8191
7838
|
function repositoryFromGitRemote(remoteInput) {
|
|
8192
7839
|
const remote = remoteInput.trim();
|
|
8193
7840
|
const scp = /^git@github\.com:([^/\s]+)\/([^/\s]+?)\/?$/.exec(remote);
|
|
@@ -8287,7 +7934,7 @@ function digestText(value2) {
|
|
|
8287
7934
|
// src/code-images.ts
|
|
8288
7935
|
var import_node_child_process6 = require("child_process");
|
|
8289
7936
|
var import_node_crypto4 = require("crypto");
|
|
8290
|
-
var
|
|
7937
|
+
var import_promises10 = require("fs/promises");
|
|
8291
7938
|
var import_node_os3 = require("os");
|
|
8292
7939
|
var import_node_path14 = require("path");
|
|
8293
7940
|
var import_node_url3 = require("url");
|
|
@@ -8369,16 +8016,16 @@ function embeddedPiAssetPath() {
|
|
|
8369
8016
|
return (0, import_node_url3.fileURLToPath)(new URL("./runtime/pi-agent.js", importMetaUrl));
|
|
8370
8017
|
}
|
|
8371
8018
|
async function embeddedPiImageName() {
|
|
8372
|
-
const bundle = await (0,
|
|
8019
|
+
const bundle = await (0, import_promises10.readFile)(embeddedPiAssetPath()).catch(() => {
|
|
8373
8020
|
throw new Error("CLI-embedded Pi runtime is missing; reinstall this exact @odla-ai/cli version");
|
|
8374
8021
|
});
|
|
8375
8022
|
return `odla-ai/pi-agent:embedded-sha256-${(0, import_node_crypto4.createHash)("sha256").update(bundle).digest("hex")}`;
|
|
8376
8023
|
}
|
|
8377
8024
|
async function buildEmbeddedPiImage(engine, image, run) {
|
|
8378
|
-
const context = await (0,
|
|
8025
|
+
const context = await (0, import_promises10.mkdtemp)((0, import_node_path14.join)((0, import_node_os3.tmpdir)(), "odla-code-pi-"));
|
|
8379
8026
|
try {
|
|
8380
|
-
await (0,
|
|
8381
|
-
await (0,
|
|
8027
|
+
await (0, import_promises10.copyFile)(embeddedPiAssetPath(), (0, import_node_path14.join)(context, "pi-agent.js"));
|
|
8028
|
+
await (0, import_promises10.writeFile)((0, import_node_path14.join)(context, "Dockerfile"), [
|
|
8382
8029
|
`FROM ${CODE_NODE_IMAGE}`,
|
|
8383
8030
|
"COPY pi-agent.js /opt/odla/pi-agent.js",
|
|
8384
8031
|
"WORKDIR /workspace",
|
|
@@ -8387,7 +8034,7 @@ async function buildEmbeddedPiImage(engine, image, run) {
|
|
|
8387
8034
|
].join("\n"), { mode: 384 });
|
|
8388
8035
|
await run(engine, ["build", "--tag", image, context], "inherit");
|
|
8389
8036
|
} finally {
|
|
8390
|
-
await (0,
|
|
8037
|
+
await (0, import_promises10.rm)(context, { recursive: true, force: true });
|
|
8391
8038
|
}
|
|
8392
8039
|
}
|
|
8393
8040
|
|
|
@@ -8395,7 +8042,7 @@ async function buildEmbeddedPiImage(engine, image, run) {
|
|
|
8395
8042
|
async function codeConnect(options) {
|
|
8396
8043
|
const cwd = options.cwd ?? process.cwd();
|
|
8397
8044
|
const configPath = (0, import_node_path15.resolve)(cwd, options.configPath);
|
|
8398
|
-
const cfg = (0,
|
|
8045
|
+
const cfg = (0, import_node_fs14.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
|
|
8399
8046
|
const requestedAppId = options.appId?.trim();
|
|
8400
8047
|
if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
|
|
8401
8048
|
throw new Error("--app-id must be a valid odla app id");
|
|
@@ -8786,18 +8433,17 @@ Usage:
|
|
|
8786
8433
|
odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
|
|
8787
8434
|
odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
|
|
8788
8435
|
odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
|
|
8789
|
-
odla-ai calendar disconnect [--env dev] [
|
|
8790
|
-
odla-ai app archive [--config odla.config.mjs] [
|
|
8791
|
-
odla-ai app restore [--config odla.config.mjs]
|
|
8792
|
-
odla-ai app export [--env dev]
|
|
8436
|
+
odla-ai calendar disconnect [--env dev] --yes [continue in Studio; human session required]
|
|
8437
|
+
odla-ai app archive [--config odla.config.mjs] --yes [continue in Studio; human session required]
|
|
8438
|
+
odla-ai app restore [--config odla.config.mjs] [continue in Studio; human session required]
|
|
8439
|
+
odla-ai app export [--env dev] [continue in Studio; human session required]
|
|
8793
8440
|
odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
|
|
8794
|
-
|
|
8795
|
-
odla-ai app
|
|
8796
|
-
odla-ai app
|
|
8797
|
-
odla-ai app
|
|
8798
|
-
odla-ai app
|
|
8799
|
-
odla-ai app owners add
|
|
8800
|
-
odla-ai app owners remove <email> [--email <odla-account>] [--json]
|
|
8441
|
+
[dry-run is local; writes continue in Studio]
|
|
8442
|
+
odla-ai app refresh-sandbox [continue in Studio; human session required]
|
|
8443
|
+
odla-ai app go-live [continue in Studio; human session required]
|
|
8444
|
+
odla-ai app promote [continue in Studio; human session required]
|
|
8445
|
+
odla-ai app rename <name> [continue in Studio; human session required]
|
|
8446
|
+
odla-ai app owners <list|add|remove> [...] [continue in Studio; human session required]
|
|
8801
8447
|
odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
|
|
8802
8448
|
odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
8803
8449
|
odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
@@ -8832,8 +8478,8 @@ Usage:
|
|
|
8832
8478
|
odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
|
|
8833
8479
|
odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
|
|
8834
8480
|
odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
|
|
8835
|
-
odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--
|
|
8836
|
-
odla-ai agent retry <job-id> [--env dev] [--
|
|
8481
|
+
odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--token <ODLA_API_KEY>] [--json]
|
|
8482
|
+
odla-ai agent retry <job-id> [--env dev] [--token <ODLA_API_KEY>] [--json]
|
|
8837
8483
|
odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
|
|
8838
8484
|
odla-ai context list [--json]
|
|
8839
8485
|
odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
|
|
@@ -8869,8 +8515,8 @@ Usage:
|
|
|
8869
8515
|
odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
|
|
8870
8516
|
odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
|
|
8871
8517
|
odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
|
|
8872
|
-
odla-ai security github connect [--repo owner/name] [--env dev]
|
|
8873
|
-
odla-ai security github disconnect --source <id> [--env dev]
|
|
8518
|
+
odla-ai security github connect [--repo owner/name] [--env dev] [continue in Studio; human session required]
|
|
8519
|
+
odla-ai security github disconnect --source <id> [--env dev] [continue in Studio; human session required]
|
|
8874
8520
|
odla-ai security plan [--env dev] [--json]
|
|
8875
8521
|
odla-ai security sources [--env dev] [--json]
|
|
8876
8522
|
odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
|
|
@@ -8878,7 +8524,7 @@ Usage:
|
|
|
8878
8524
|
odla-ai security report <job-id> [--json]
|
|
8879
8525
|
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
8880
8526
|
odla-ai security run [target] --self --ack-redacted-source
|
|
8881
|
-
odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
8527
|
+
odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--request-grant] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
8882
8528
|
odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
|
|
8883
8529
|
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
8884
8530
|
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
@@ -9013,6 +8659,10 @@ Safety:
|
|
|
9013
8659
|
The email is a non-secret identity hint: never provide a password or session
|
|
9014
8660
|
token. The matching account must already exist, be signed in, explicitly
|
|
9015
8661
|
review the exact code, and finish any current request before claiming another.
|
|
8662
|
+
If provision reports that the current agent principal has no live app.manage
|
|
8663
|
+
grant, run it once with --request-grant. That flag ignores ODLA_DEV_TOKEN and
|
|
8664
|
+
the local cache, prints and opens a fresh exact-project owner-review URL, then
|
|
8665
|
+
continues provisioning with the approved replacement credential.
|
|
9016
8666
|
Run Code from a GitHub checkout already connected to an app in Studio; an
|
|
9017
8667
|
odla.config.mjs may select the app explicitly but is not required. Code host
|
|
9018
8668
|
approval and credential hashes live in odla-ai/db. The host
|
|
@@ -9381,7 +9031,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
9381
9031
|
throw new WatchRemoteError(cursor, error);
|
|
9382
9032
|
}
|
|
9383
9033
|
if (deadline !== void 0 && now() >= deadline) {
|
|
9384
|
-
const result =
|
|
9034
|
+
const result = report(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
9385
9035
|
throw new WatchTimeoutError(result.cursor);
|
|
9386
9036
|
}
|
|
9387
9037
|
const base = Math.min(intervalMs, 1e3);
|
|
@@ -9422,7 +9072,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
9422
9072
|
});
|
|
9423
9073
|
const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
|
|
9424
9074
|
const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
|
|
9425
|
-
return
|
|
9075
|
+
return report(ctx, parsed, {
|
|
9426
9076
|
found: true,
|
|
9427
9077
|
cursor,
|
|
9428
9078
|
events: matching,
|
|
@@ -9449,13 +9099,13 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
9449
9099
|
}
|
|
9450
9100
|
if (page2.hasMore) continue;
|
|
9451
9101
|
if (deadline !== void 0 && now() >= deadline) {
|
|
9452
|
-
return
|
|
9102
|
+
return report(ctx, parsed, { found: false, cursor });
|
|
9453
9103
|
}
|
|
9454
9104
|
const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
|
|
9455
9105
|
await sleep(wait2);
|
|
9456
9106
|
}
|
|
9457
9107
|
}
|
|
9458
|
-
function
|
|
9108
|
+
function report(ctx, parsed, result) {
|
|
9459
9109
|
if (ctx.json) {
|
|
9460
9110
|
ctx.out.log(JSON.stringify(result, null, 2));
|
|
9461
9111
|
} else if (parsed.options.jsonl !== true && result.found) {
|
|
@@ -9956,7 +9606,7 @@ function eventLabel(event) {
|
|
|
9956
9606
|
const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
|
|
9957
9607
|
return body || event.payload.entityId;
|
|
9958
9608
|
}
|
|
9959
|
-
function
|
|
9609
|
+
function report2(ctx, parsed, result) {
|
|
9960
9610
|
if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
|
|
9961
9611
|
else if (parsed.options.jsonl !== true && result.found) {
|
|
9962
9612
|
for (const event of result.events ?? []) {
|
|
@@ -10016,7 +9666,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
10016
9666
|
});
|
|
10017
9667
|
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
|
|
10018
9668
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10019
|
-
return
|
|
9669
|
+
return report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
10020
9670
|
}
|
|
10021
9671
|
const backoff = Math.min(
|
|
10022
9672
|
MAX_BACKOFF_MS2,
|
|
@@ -10057,7 +9707,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
10057
9707
|
cursor,
|
|
10058
9708
|
serverTime: current.serverTime
|
|
10059
9709
|
});
|
|
10060
|
-
return
|
|
9710
|
+
return report2(ctx, parsed, { found: true, cursor, events: matching });
|
|
10061
9711
|
}
|
|
10062
9712
|
if (current.events.length > 0) {
|
|
10063
9713
|
jsonl2(ctx, parsed, {
|
|
@@ -10076,7 +9726,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
10076
9726
|
}
|
|
10077
9727
|
if (current.hasMore) continue;
|
|
10078
9728
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10079
|
-
return
|
|
9729
|
+
return report2(ctx, parsed, { found: false, cursor });
|
|
10080
9730
|
}
|
|
10081
9731
|
await sleep(
|
|
10082
9732
|
deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
|
|
@@ -10978,14 +10628,25 @@ async function provision(options) {
|
|
|
10978
10628
|
}
|
|
10979
10629
|
const doFetch = options.fetch ?? fetch;
|
|
10980
10630
|
const token = await getDeveloperToken(cfg, options, doFetch, out, {
|
|
10981
|
-
optionalProjectCapabilities: ["app.manage"]
|
|
10631
|
+
optionalProjectCapabilities: ["app.manage"],
|
|
10632
|
+
forceReview: options.requestGrant
|
|
10982
10633
|
});
|
|
10983
10634
|
const apps = (0, import_apps12.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
|
|
10984
10635
|
const existing = await apps.resolveApp(cfg.app.id);
|
|
10985
10636
|
if (existing) {
|
|
10986
10637
|
out.log(`app: ${cfg.app.id} already exists`);
|
|
10987
10638
|
} else {
|
|
10988
|
-
|
|
10639
|
+
try {
|
|
10640
|
+
await apps.createApp({ name: cfg.app.name, appId: cfg.app.id });
|
|
10641
|
+
} catch (error) {
|
|
10642
|
+
if (error instanceof import_apps12.AppsError && error.status === 403) {
|
|
10643
|
+
throw new Error(
|
|
10644
|
+
`app "${cfg.app.id}" does not exist, and this authenticated agent credential has no owner-reviewed app.manage bootstrap grant for that exact id. Run "odla-ai provision --request-grant --email <odla-account>" to open the review URL and continue; developer ownership alone is not agent authority`,
|
|
10645
|
+
{ cause: error }
|
|
10646
|
+
);
|
|
10647
|
+
}
|
|
10648
|
+
throw error;
|
|
10649
|
+
}
|
|
10989
10650
|
out.log(`app: created ${cfg.app.id}`);
|
|
10990
10651
|
}
|
|
10991
10652
|
for (const env of cfg.envs) {
|
|
@@ -11109,7 +10770,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
11109
10770
|
}
|
|
11110
10771
|
|
|
11111
10772
|
// src/record.ts
|
|
11112
|
-
var
|
|
10773
|
+
var import_node_fs15 = require("fs");
|
|
11113
10774
|
var import_node_process12 = __toESM(require("process"), 1);
|
|
11114
10775
|
|
|
11115
10776
|
// src/surface.ts
|
|
@@ -11280,14 +10941,14 @@ function recordInvocation(parsed) {
|
|
|
11280
10941
|
options: Object.entries(parsed.options).map(([name, value2]) => value2 === false ? `no-${name}` : name).sort()
|
|
11281
10942
|
};
|
|
11282
10943
|
if (!entry.path.length) return;
|
|
11283
|
-
(0,
|
|
10944
|
+
(0, import_node_fs15.appendFileSync)(file, `${JSON.stringify(entry)}
|
|
11284
10945
|
`);
|
|
11285
10946
|
} catch {
|
|
11286
10947
|
}
|
|
11287
10948
|
}
|
|
11288
10949
|
|
|
11289
10950
|
// src/runbook-actions.ts
|
|
11290
|
-
var
|
|
10951
|
+
var import_node_fs16 = require("fs");
|
|
11291
10952
|
|
|
11292
10953
|
// src/runbook-requires.ts
|
|
11293
10954
|
var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
|
|
@@ -11372,7 +11033,7 @@ async function bySlug(ctx, slug) {
|
|
|
11372
11033
|
function readBody(file, inline) {
|
|
11373
11034
|
if (inline !== void 0) return inline;
|
|
11374
11035
|
if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
|
|
11375
|
-
return (0,
|
|
11036
|
+
return (0, import_node_fs16.readFileSync)(file === "-" ? 0 : file, "utf8");
|
|
11376
11037
|
}
|
|
11377
11038
|
var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
|
|
11378
11039
|
async function runbookList(ctx, all, query) {
|
|
@@ -11464,7 +11125,7 @@ async function runbookRemove(ctx, slug) {
|
|
|
11464
11125
|
}
|
|
11465
11126
|
|
|
11466
11127
|
// src/runbook-import.ts
|
|
11467
|
-
var
|
|
11128
|
+
var import_node_fs17 = require("fs");
|
|
11468
11129
|
var import_node_path16 = require("path");
|
|
11469
11130
|
function parseRunbook(text2, slug) {
|
|
11470
11131
|
let rest = text2;
|
|
@@ -11490,12 +11151,12 @@ function parseRunbook(text2, slug) {
|
|
|
11490
11151
|
};
|
|
11491
11152
|
}
|
|
11492
11153
|
function readRunbookDir(dir) {
|
|
11493
|
-
if (!(0,
|
|
11494
|
-
const files = (0,
|
|
11154
|
+
if (!(0, import_node_fs17.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
|
|
11155
|
+
const files = (0, import_node_fs17.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
|
|
11495
11156
|
if (!files.length) throw new Error(`no .md files in ${dir}`);
|
|
11496
11157
|
return files.map((file) => {
|
|
11497
11158
|
const slug = (0, import_node_path16.basename)(file, ".md");
|
|
11498
|
-
const parsed = parseRunbook((0,
|
|
11159
|
+
const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0, import_node_path16.join)(dir, file), "utf8"), slug);
|
|
11499
11160
|
return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
|
|
11500
11161
|
});
|
|
11501
11162
|
}
|
|
@@ -11568,7 +11229,7 @@ async function upsert(ctx, r, visibility) {
|
|
|
11568
11229
|
|
|
11569
11230
|
// src/runbook-impact.ts
|
|
11570
11231
|
var import_node_child_process7 = require("child_process");
|
|
11571
|
-
var
|
|
11232
|
+
var import_node_fs18 = require("fs");
|
|
11572
11233
|
var import_node_path17 = require("path");
|
|
11573
11234
|
|
|
11574
11235
|
// src/runbook-impact-scan.ts
|
|
@@ -11739,9 +11400,9 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
|
|
|
11739
11400
|
function manifestLabeller(root) {
|
|
11740
11401
|
return (workspace) => {
|
|
11741
11402
|
const manifest = (0, import_node_path17.join)(root, workspace, "package.json");
|
|
11742
|
-
if (!(0,
|
|
11403
|
+
if (!(0, import_node_fs18.existsSync)(manifest)) return void 0;
|
|
11743
11404
|
try {
|
|
11744
|
-
const name = JSON.parse((0,
|
|
11405
|
+
const name = JSON.parse((0, import_node_fs18.readFileSync)(manifest, "utf8")).name;
|
|
11745
11406
|
return typeof name === "string" ? name : void 0;
|
|
11746
11407
|
} catch {
|
|
11747
11408
|
return void 0;
|
|
@@ -11779,7 +11440,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
|
|
|
11779
11440
|
return out;
|
|
11780
11441
|
}
|
|
11781
11442
|
var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
|
|
11782
|
-
function
|
|
11443
|
+
function report3(ctx, impacts) {
|
|
11783
11444
|
const covered = impacts.filter((i) => i.runbooks.length);
|
|
11784
11445
|
ctx.out.log(
|
|
11785
11446
|
`${impacts.length} changed surface${impacts.length === 1 ? "" : "s"}; ${covered.length} covered by a runbook. Reread each one and fix any step this change made wrong.`
|
|
@@ -11808,7 +11469,7 @@ function report4(ctx, impacts) {
|
|
|
11808
11469
|
async function runbookImpact(ctx, options, deps = {}) {
|
|
11809
11470
|
const cwd = deps.cwd ?? process.cwd();
|
|
11810
11471
|
const runGit = deps.runGit ?? gitRunner(cwd);
|
|
11811
|
-
const read3 = deps.readRepoFile ?? ((path) => (0,
|
|
11472
|
+
const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0, import_node_path17.join)(cwd, path), "utf8"));
|
|
11812
11473
|
const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
|
|
11813
11474
|
if (!surfaces.length) {
|
|
11814
11475
|
return ctx.out.log(
|
|
@@ -11817,7 +11478,7 @@ async function runbookImpact(ctx, options, deps = {}) {
|
|
|
11817
11478
|
}
|
|
11818
11479
|
const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
|
|
11819
11480
|
if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
|
|
11820
|
-
|
|
11481
|
+
report3(ctx, impacts);
|
|
11821
11482
|
}
|
|
11822
11483
|
|
|
11823
11484
|
// src/runbook-lint.ts
|
|
@@ -11941,7 +11602,7 @@ async function runbookComment(ctx, slug, body) {
|
|
|
11941
11602
|
|
|
11942
11603
|
// src/runbook-editor.ts
|
|
11943
11604
|
var import_node_child_process8 = require("child_process");
|
|
11944
|
-
var
|
|
11605
|
+
var import_node_fs19 = require("fs");
|
|
11945
11606
|
var import_node_os5 = require("os");
|
|
11946
11607
|
var import_node_path18 = require("path");
|
|
11947
11608
|
var import_node_process13 = __toESM(require("process"), 1);
|
|
@@ -11969,16 +11630,16 @@ function editText(initial, slug, deps = {}) {
|
|
|
11969
11630
|
);
|
|
11970
11631
|
if (!interactive())
|
|
11971
11632
|
throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
|
|
11972
|
-
const dir = (0,
|
|
11633
|
+
const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path18.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
|
|
11973
11634
|
const file = (0, import_node_path18.join)(dir, `${slug}.md`);
|
|
11974
11635
|
try {
|
|
11975
|
-
(0,
|
|
11636
|
+
(0, import_node_fs19.writeFileSync)(file, initial, { mode: 384 });
|
|
11976
11637
|
const code = defaultRunOrInjected(deps)(editor, file);
|
|
11977
11638
|
if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
|
|
11978
|
-
const edited = (0,
|
|
11639
|
+
const edited = (0, import_node_fs19.readFileSync)(file, "utf8");
|
|
11979
11640
|
return edited === initial ? null : edited;
|
|
11980
11641
|
} finally {
|
|
11981
|
-
(0,
|
|
11642
|
+
(0, import_node_fs19.rmSync)(dir, { recursive: true, force: true });
|
|
11982
11643
|
}
|
|
11983
11644
|
}
|
|
11984
11645
|
var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
|
|
@@ -12317,7 +11978,6 @@ async function runbookCommand(parsed, deps = {}) {
|
|
|
12317
11978
|
}
|
|
12318
11979
|
|
|
12319
11980
|
// src/security-command-context.ts
|
|
12320
|
-
var import_promises12 = require("readline/promises");
|
|
12321
11981
|
async function hostedSecurityContext(parsed, dependencies) {
|
|
12322
11982
|
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
12323
11983
|
const cfg = await loadProjectConfig(configPath);
|
|
@@ -12336,21 +11996,11 @@ async function hostedSecurityContext(parsed, dependencies) {
|
|
|
12336
11996
|
cfg,
|
|
12337
11997
|
{ configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
|
|
12338
11998
|
doFetch,
|
|
12339
|
-
stdout
|
|
11999
|
+
stdout,
|
|
12000
|
+
{ optionalProjectCapabilities: ["app.manage"] }
|
|
12340
12001
|
);
|
|
12341
12002
|
return { platform, token, appId: cfg.app.id, env, fetch: doFetch, stdout };
|
|
12342
12003
|
}
|
|
12343
|
-
async function interactiveConfirmation(message2, dependencies) {
|
|
12344
|
-
if (dependencies.confirm) return dependencies.confirm(message2);
|
|
12345
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
12346
|
-
const prompt = (0, import_promises12.createInterface)({ input: process.stdin, output: process.stdout });
|
|
12347
|
-
try {
|
|
12348
|
-
const answer = await prompt.question(`${message2} [y/N] `);
|
|
12349
|
-
return /^y(?:es)?$/i.test(answer.trim());
|
|
12350
|
-
} finally {
|
|
12351
|
-
prompt.close();
|
|
12352
|
-
}
|
|
12353
|
-
}
|
|
12354
12004
|
function requiredSecurityPositional(parsed, index, label) {
|
|
12355
12005
|
const value2 = parsed.positionals[index];
|
|
12356
12006
|
if (!value2) throw new Error(`${label} is required`);
|
|
@@ -12416,31 +12066,31 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
12416
12066
|
url.searchParams.set("job", job.jobId);
|
|
12417
12067
|
out.log(` Studio: ${url.toString()}`);
|
|
12418
12068
|
}
|
|
12419
|
-
function printHostedReport(out,
|
|
12420
|
-
out.log(`security report ${
|
|
12421
|
-
out.log(` coverage: ${
|
|
12422
|
-
out.log(` findings: confirmed=${
|
|
12423
|
-
out.log(` discovery: ${
|
|
12424
|
-
out.log(` validation: ${
|
|
12425
|
-
for (const finding of
|
|
12069
|
+
function printHostedReport(out, report4) {
|
|
12070
|
+
out.log(`security report ${report4.jobId}: ${report4.repository}@${report4.revision}`);
|
|
12071
|
+
out.log(` coverage: ${report4.coverageStatus} cells=${report4.metrics.coverageCells} shallow=${report4.metrics.shallowCells} blocked=${report4.metrics.blockedCells} unscheduled=${report4.metrics.unscheduledCells} budget_exhausted=${report4.metrics.budgetExhaustedCells}`);
|
|
12072
|
+
out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates} rejected=${report4.metrics.rejected}`);
|
|
12073
|
+
out.log(` discovery: ${report4.provenance.discovery?.provider ?? "unknown"}/${report4.provenance.discovery?.model ?? "unknown"}`);
|
|
12074
|
+
out.log(` validation: ${report4.provenance.validation?.provider ?? "unknown"}/${report4.provenance.validation?.model ?? "unknown"} independent=${String(report4.provenance.independentValidation)}`);
|
|
12075
|
+
for (const finding of report4.findings) {
|
|
12426
12076
|
const location = finding.locations[0];
|
|
12427
12077
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
12428
12078
|
}
|
|
12429
|
-
for (const limitation of
|
|
12079
|
+
for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
|
|
12430
12080
|
}
|
|
12431
|
-
function enforceHostedReportGate(
|
|
12081
|
+
function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
|
|
12432
12082
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
12433
12083
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
12434
12084
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
12435
12085
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
12436
|
-
const confirmed =
|
|
12437
|
-
const leads = failOnCandidates ?
|
|
12438
|
-
const incomplete =
|
|
12086
|
+
const confirmed = report4.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
|
|
12087
|
+
const leads = failOnCandidates ? report4.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
|
|
12088
|
+
const incomplete = report4.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
|
|
12439
12089
|
if (confirmed.length || leads.length || incomplete) {
|
|
12440
|
-
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${
|
|
12090
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report4.coverageStatus}` : ""}`);
|
|
12441
12091
|
}
|
|
12442
12092
|
if (emitSuccess) {
|
|
12443
|
-
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${
|
|
12093
|
+
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report4.coverageStatus}. This is not proof that the application is secure.`);
|
|
12444
12094
|
}
|
|
12445
12095
|
}
|
|
12446
12096
|
function printHostedSecurityPlanRoute(out, label, route2) {
|
|
@@ -12523,17 +12173,17 @@ async function runHostedSecurity(options) {
|
|
|
12523
12173
|
allowNetwork: false
|
|
12524
12174
|
}
|
|
12525
12175
|
});
|
|
12526
|
-
const
|
|
12527
|
-
await (0, import_node3.writeSecurityArtifacts)(output,
|
|
12528
|
-
const reportDigest = await (0, import_security.securityFingerprint)(
|
|
12176
|
+
const report4 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
12177
|
+
await (0, import_node3.writeSecurityArtifacts)(output, report4);
|
|
12178
|
+
const reportDigest = await (0, import_security.securityFingerprint)(report4);
|
|
12529
12179
|
await hosted.complete({
|
|
12530
12180
|
reportDigest,
|
|
12531
|
-
coverageStatus:
|
|
12532
|
-
confirmed:
|
|
12533
|
-
candidates:
|
|
12181
|
+
coverageStatus: report4.coverageStatus,
|
|
12182
|
+
confirmed: report4.metrics.confirmed,
|
|
12183
|
+
candidates: report4.metrics.candidates
|
|
12534
12184
|
}, { signal: options.signal });
|
|
12535
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run,
|
|
12536
|
-
return Object.freeze({ report:
|
|
12185
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report4, output);
|
|
12186
|
+
return Object.freeze({ report: report4, run: hosted.run, output });
|
|
12537
12187
|
}
|
|
12538
12188
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
12539
12189
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
@@ -12559,14 +12209,14 @@ function profileFor(name, maxHuntTasks) {
|
|
|
12559
12209
|
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
12560
12210
|
return { ...profile, maxHuntTasks };
|
|
12561
12211
|
}
|
|
12562
|
-
function printSummary(out, appId, env, run,
|
|
12563
|
-
const complete =
|
|
12212
|
+
function printSummary(out, appId, env, run, report4, output) {
|
|
12213
|
+
const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
|
|
12564
12214
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
12565
12215
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
12566
12216
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
12567
|
-
out.log(` coverage: ${
|
|
12568
|
-
if (
|
|
12569
|
-
out.log(` findings: confirmed=${
|
|
12217
|
+
out.log(` coverage: ${report4.coverageStatus} ${complete}/${report4.coverage.length} blocked=${report4.metrics.blockedCells} shallow=${report4.metrics.shallowCells} unscheduled=${report4.metrics.unscheduledCells} budget_exhausted=${report4.metrics.budgetExhaustedCells}`);
|
|
12218
|
+
if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
|
|
12219
|
+
out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
|
|
12570
12220
|
out.log(` report: ${(0, import_node_path19.resolve)(output, "REPORT.md")}`);
|
|
12571
12221
|
}
|
|
12572
12222
|
function formatBudget(usage) {
|
|
@@ -12802,13 +12452,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
12802
12452
|
}
|
|
12803
12453
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
12804
12454
|
}
|
|
12805
|
-
const
|
|
12455
|
+
const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
12806
12456
|
if (parsed.options.json === true) {
|
|
12807
|
-
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report:
|
|
12457
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report4 }, null, 2));
|
|
12808
12458
|
} else {
|
|
12809
|
-
printHostedReport(context.stdout,
|
|
12459
|
+
printHostedReport(context.stdout, report4);
|
|
12810
12460
|
}
|
|
12811
|
-
enforceHostedReportGate(
|
|
12461
|
+
enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
|
|
12812
12462
|
}
|
|
12813
12463
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
12814
12464
|
if (parsed.options.source === true) {
|
|
@@ -12869,19 +12519,20 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
12869
12519
|
cfg,
|
|
12870
12520
|
{ configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
|
|
12871
12521
|
doFetch,
|
|
12872
|
-
out
|
|
12522
|
+
out,
|
|
12523
|
+
{ optionalProjectCapabilities: ["app.manage"] }
|
|
12873
12524
|
);
|
|
12874
12525
|
}
|
|
12875
12526
|
});
|
|
12876
12527
|
enforceLocalGate(result.report, parsed);
|
|
12877
12528
|
}
|
|
12878
|
-
function enforceLocalGate(
|
|
12529
|
+
function enforceLocalGate(report4, parsed) {
|
|
12879
12530
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
12880
12531
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
12881
12532
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
12882
|
-
const confirmed = (0, import_security2.findingsAtOrAbove)(
|
|
12883
|
-
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(
|
|
12884
|
-
const incomplete =
|
|
12533
|
+
const confirmed = (0, import_security2.findingsAtOrAbove)(report4, failOn);
|
|
12534
|
+
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report4, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
12535
|
+
const incomplete = report4.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
12885
12536
|
if (confirmed.length || leads.length || incomplete) {
|
|
12886
12537
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
12887
12538
|
}
|
|
@@ -12920,9 +12571,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
12920
12571
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
12921
12572
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
12922
12573
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
12923
|
-
const
|
|
12924
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
12925
|
-
else printHostedReport(context.stdout,
|
|
12574
|
+
const report4 = await getHostedSecurityReport({ ...context, jobId });
|
|
12575
|
+
if (parsed.options.json === true) context.stdout.log(JSON.stringify(report4, null, 2));
|
|
12576
|
+
else printHostedReport(context.stdout, report4);
|
|
12926
12577
|
return;
|
|
12927
12578
|
}
|
|
12928
12579
|
if (sub !== "run") {
|
|
@@ -12936,35 +12587,24 @@ async function githubSecurityCommand(parsed, dependencies) {
|
|
|
12936
12587
|
const action2 = parsed.positionals[2];
|
|
12937
12588
|
if (action2 === "disconnect") {
|
|
12938
12589
|
assertArgs(parsed, ["config", "env", "platform", "source", "email", "open", "yes"], 3);
|
|
12939
|
-
const context2 = await hostedSecurityContext(parsed, dependencies);
|
|
12940
12590
|
const sourceId = requiredString(parsed.options.source, "--source");
|
|
12941
|
-
|
|
12942
|
-
|
|
12943
|
-
|
|
12591
|
+
return requireStudioHuman(
|
|
12592
|
+
stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
12593
|
+
`disconnecting GitHub security source ${sourceId}`,
|
|
12594
|
+
"security",
|
|
12595
|
+
stringOpt(parsed.options.env)
|
|
12944
12596
|
);
|
|
12945
|
-
if (!confirmed) {
|
|
12946
|
-
throw new Error("GitHub source disconnect cancelled; pass --yes in a non-interactive shell");
|
|
12947
|
-
}
|
|
12948
|
-
await disconnectGitHubSecuritySource({ ...context2, sourceId });
|
|
12949
|
-
context2.stdout.log(`github: disconnected ${sourceId} from ${context2.appId}/${context2.env}`);
|
|
12950
|
-
return;
|
|
12951
12597
|
}
|
|
12952
12598
|
if (action2 !== "connect") {
|
|
12953
12599
|
throw new Error('unknown security github command. Try "odla-ai security github connect".');
|
|
12954
12600
|
}
|
|
12955
12601
|
assertArgs(parsed, ["config", "env", "platform", "repo", "email", "open"], 3);
|
|
12956
|
-
|
|
12957
|
-
|
|
12958
|
-
|
|
12959
|
-
|
|
12960
|
-
|
|
12961
|
-
|
|
12962
|
-
openInstallUrl: dependencies.openUrl ?? openUrl,
|
|
12963
|
-
wait: dependencies.pollWait,
|
|
12964
|
-
stdout: context.stdout
|
|
12965
|
-
});
|
|
12966
|
-
context.stdout.log(`github: connected ${connection.repository ?? repository ?? "app repository"} (${connection.sourceId ?? "source pending"})`);
|
|
12967
|
-
context.stdout.log("github: odla.ai stores the installation; no PAT or GitHub token is written locally");
|
|
12602
|
+
await requireStudioHuman(
|
|
12603
|
+
stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
12604
|
+
"connecting the GitHub security repository",
|
|
12605
|
+
"security",
|
|
12606
|
+
stringOpt(parsed.options.env)
|
|
12607
|
+
);
|
|
12968
12608
|
}
|
|
12969
12609
|
async function listSecuritySources(parsed, dependencies) {
|
|
12970
12610
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 2);
|
|
@@ -13105,6 +12745,7 @@ async function provisionCommand(parsed, dependencies) {
|
|
|
13105
12745
|
"write-credentials",
|
|
13106
12746
|
"write-dev-vars",
|
|
13107
12747
|
"token",
|
|
12748
|
+
"request-grant",
|
|
13108
12749
|
"email",
|
|
13109
12750
|
"open",
|
|
13110
12751
|
"wait",
|
|
@@ -13120,6 +12761,7 @@ async function provisionCommand(parsed, dependencies) {
|
|
|
13120
12761
|
writeCredentials: parsed.options["write-credentials"] !== false,
|
|
13121
12762
|
writeDevVars: typeof writeDevVars2 === "string" ? writeDevVars2 : writeDevVars2 === true,
|
|
13122
12763
|
token: stringOpt(parsed.options.token),
|
|
12764
|
+
requestGrant: parsed.options["request-grant"] === true,
|
|
13123
12765
|
email: stringOpt(parsed.options.email),
|
|
13124
12766
|
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
13125
12767
|
wait: numberOpt(parsed.options.wait, "--wait"),
|