@odla-ai/cli 0.27.10 → 0.27.12
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/dist/bin.cjs +244 -617
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-OGZELVS5.js → chunk-ECRBOR66.js} +128 -435
- package/dist/chunk-ECRBOR66.js.map +1 -0
- package/dist/index.cjs +244 -550
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-OGZELVS5.js.map +0 -1
package/dist/bin.cjs
CHANGED
|
@@ -1668,7 +1668,7 @@ async function agentCommand(parsed, deps = {}) {
|
|
|
1668
1668
|
if (action2 !== "jobs" && action2 !== "retry") {
|
|
1669
1669
|
throw new Error(`unknown agent action "${action2 ?? ""}". Try "odla-ai agent jobs --json".`);
|
|
1670
1670
|
}
|
|
1671
|
-
assertArgs(parsed, ["config", "env", "state", "limit", "json", "token"
|
|
1671
|
+
assertArgs(parsed, ["config", "env", "state", "limit", "json", "token"], action2 === "jobs" ? 2 : 3);
|
|
1672
1672
|
if (action2 === "retry" && (parsed.options.state !== void 0 || parsed.options.limit !== void 0)) {
|
|
1673
1673
|
throw new Error('--state and --limit are supported only by "agent jobs"');
|
|
1674
1674
|
}
|
|
@@ -1676,17 +1676,17 @@ async function agentCommand(parsed, deps = {}) {
|
|
|
1676
1676
|
const { env, tenant } = resolveTenant(cfg, stringOpt(parsed.options.env));
|
|
1677
1677
|
const doFetch = deps.fetch ?? fetch;
|
|
1678
1678
|
const out = deps.stdout ?? console;
|
|
1679
|
-
const credential2 =
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1679
|
+
const credential2 = stringOpt(parsed.options.token) ?? readCredentials(cfg.local.credentialsFile)?.envs[env]?.dbKey;
|
|
1680
|
+
if (!credential2) {
|
|
1681
|
+
throw new Error(
|
|
1682
|
+
`no ${env} app credential found; run \`odla-ai provision --write-dev-vars --yes\` or pass --token <ODLA_API_KEY>`
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
if (credential2.startsWith("odla_dev_")) {
|
|
1686
|
+
throw new Error(
|
|
1687
|
+
"agent job administration requires an app credential (ODLA_API_KEY / odla_sk_\u2026), not a developer device token"
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
1690
|
const base = `${cfg.dbEndpoint}/app/${encodeURIComponent(tenant)}/admin/agent-jobs`;
|
|
1691
1691
|
const headers = { authorization: `Bearer ${credential2}` };
|
|
1692
1692
|
if (action2 === "retry") {
|
|
@@ -1742,51 +1742,23 @@ function errorMessage(body) {
|
|
|
1742
1742
|
return "request failed";
|
|
1743
1743
|
}
|
|
1744
1744
|
|
|
1745
|
+
// src/human-session.ts
|
|
1746
|
+
async function requireStudioHuman(configPath, action2, destination = "app", env) {
|
|
1747
|
+
const cfg = await loadProjectConfig(configPath);
|
|
1748
|
+
const appEnv = env && cfg.envs.includes(env) ? env : cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
|
|
1749
|
+
const path = destination === "app" ? `/studio/apps/${encodeURIComponent(cfg.app.id)}/${encodeURIComponent(appEnv)}/settings/app` : `/studio/apps/${encodeURIComponent(cfg.app.id)}/${encodeURIComponent(appEnv)}/${destination}`;
|
|
1750
|
+
throw new Error(
|
|
1751
|
+
`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}`
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1745
1755
|
// 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
1756
|
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 };
|
|
1757
|
+
return requireStudioHuman(options.configPath, "database export", "database", options.env);
|
|
1786
1758
|
}
|
|
1787
1759
|
|
|
1788
1760
|
// src/app-import.ts
|
|
1789
|
-
var
|
|
1761
|
+
var import_node_fs7 = require("fs");
|
|
1790
1762
|
var import_import = require("@odla-ai/db/import");
|
|
1791
1763
|
function chooseIdMode(options, rows) {
|
|
1792
1764
|
const chosen = [options.idField && "field", options.key && "key", options.generateIds && "generate"].filter(Boolean);
|
|
@@ -1803,9 +1775,8 @@ async function appImport(options) {
|
|
|
1803
1775
|
const cfg = await loadProjectConfig(options.configPath);
|
|
1804
1776
|
const out = options.stdout ?? console;
|
|
1805
1777
|
const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
|
|
1806
|
-
const doFetch = options.fetch ?? fetch;
|
|
1807
1778
|
const { tenant } = resolveTenant(cfg, options.env);
|
|
1808
|
-
const text2 = options.file === "-" ? (options.readStdin ?? (() => (0,
|
|
1779
|
+
const text2 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs7.readFileSync)(0, "utf8")))() : (0, import_node_fs7.readFileSync)(options.file, "utf8");
|
|
1809
1780
|
const { format, sources } = (0, import_import.parseImport)(text2, options.ns);
|
|
1810
1781
|
if (format === "namespace-map" && options.ns) {
|
|
1811
1782
|
throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
|
|
@@ -1831,100 +1802,18 @@ ${detail}${more}`);
|
|
|
1831
1802
|
if (options.json) out.log(JSON.stringify(result, null, 2));
|
|
1832
1803
|
return result;
|
|
1833
1804
|
}
|
|
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;
|
|
1805
|
+
return requireStudioHuman(options.configPath, "database import", "database", options.env);
|
|
1865
1806
|
}
|
|
1866
1807
|
|
|
1867
1808
|
// 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
1809
|
async function ownersList(options) {
|
|
1907
|
-
|
|
1810
|
+
await requireStudioHuman(options.configPath, "listing app owners", "app");
|
|
1908
1811
|
}
|
|
1909
1812
|
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
|
-
);
|
|
1813
|
+
await requireStudioHuman(options.configPath, `adding ${email} as an app owner`, "app");
|
|
1916
1814
|
}
|
|
1917
1815
|
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}`);
|
|
1816
|
+
await requireStudioHuman(options.configPath, `removing ${target} as an app owner`, "app");
|
|
1928
1817
|
}
|
|
1929
1818
|
async function appOwnersCommand(parsed, dependencies = {}) {
|
|
1930
1819
|
const sub = parsed.positionals[2] ?? "list";
|
|
@@ -1956,35 +1845,9 @@ async function appOwnersCommand(parsed, dependencies = {}) {
|
|
|
1956
1845
|
|
|
1957
1846
|
// src/app-rename.ts
|
|
1958
1847
|
async function appRename(name, options) {
|
|
1959
|
-
const out = options.stdout ?? console;
|
|
1960
1848
|
const trimmed = name.trim();
|
|
1961
1849
|
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.`);
|
|
1850
|
+
await requireStudioHuman(options.configPath, `renaming the app to "${trimmed}"`, "app");
|
|
1988
1851
|
}
|
|
1989
1852
|
async function appRenameCommand(parsed, dependencies = {}) {
|
|
1990
1853
|
assertArgs(parsed, ["config", "token", "email", "json"], parsed.positionals.length);
|
|
@@ -2000,172 +1863,23 @@ async function appRenameCommand(parsed, dependencies = {}) {
|
|
|
2000
1863
|
}
|
|
2001
1864
|
|
|
2002
1865
|
// 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
1866
|
async function appTransfer(options) {
|
|
2040
1867
|
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");
|
|
1868
|
+
bothTenants(cfg);
|
|
1869
|
+
return requireStudioHuman(options.configPath, `app ${options.verb}`, "database");
|
|
2128
1870
|
}
|
|
2129
1871
|
|
|
2130
1872
|
// 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
1873
|
async function appArchive(options) {
|
|
2152
1874
|
if (options.yes !== true) {
|
|
2153
1875
|
throw new Error(
|
|
2154
1876
|
"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
1877
|
);
|
|
2156
1878
|
}
|
|
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`);
|
|
1879
|
+
await requireStudioHuman(options.configPath, "app archive", "app");
|
|
2162
1880
|
}
|
|
2163
1881
|
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`);
|
|
1882
|
+
await requireStudioHuman(options.configPath, "app restore", "app");
|
|
2169
1883
|
}
|
|
2170
1884
|
async function appCommand(parsed, dependencies = {}) {
|
|
2171
1885
|
const sub = parsed.positionals[1];
|
|
@@ -2251,7 +1965,7 @@ async function appCommand(parsed, dependencies = {}) {
|
|
|
2251
1965
|
}
|
|
2252
1966
|
|
|
2253
1967
|
// src/brand-command.ts
|
|
2254
|
-
var
|
|
1968
|
+
var import_promises = require("fs/promises");
|
|
2255
1969
|
var import_node_path7 = require("path");
|
|
2256
1970
|
|
|
2257
1971
|
// src/brand-design-unpack.ts
|
|
@@ -2354,7 +2068,7 @@ function describeUnpack(result, outDir) {
|
|
|
2354
2068
|
// src/brand-command.ts
|
|
2355
2069
|
var USAGE = "usage: odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]";
|
|
2356
2070
|
async function readBundle(source, deps) {
|
|
2357
|
-
if (source !== "-") return (0,
|
|
2071
|
+
if (source !== "-") return (0, import_promises.readFile)((0, import_node_path7.resolve)(source), "utf8");
|
|
2358
2072
|
const readStdin = deps.readStdin;
|
|
2359
2073
|
if (!readStdin) throw new Error("reading a bundle from stdin is not supported here");
|
|
2360
2074
|
return readStdin();
|
|
@@ -2362,8 +2076,8 @@ async function readBundle(source, deps) {
|
|
|
2362
2076
|
async function writeAll(result, outDir) {
|
|
2363
2077
|
for (const file of result.files) {
|
|
2364
2078
|
const target = (0, import_node_path7.resolve)(outDir, file.path);
|
|
2365
|
-
await (0,
|
|
2366
|
-
await (0,
|
|
2079
|
+
await (0, import_promises.mkdir)((0, import_node_path7.dirname)(target), { recursive: true });
|
|
2080
|
+
await (0, import_promises.writeFile)(target, file.bytes);
|
|
2367
2081
|
}
|
|
2368
2082
|
}
|
|
2369
2083
|
async function designUnpack(parsed, deps) {
|
|
@@ -2523,12 +2237,6 @@ async function pollCalendarConnection(ctx, attemptId) {
|
|
|
2523
2237
|
ctx.env
|
|
2524
2238
|
);
|
|
2525
2239
|
}
|
|
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
2240
|
function parseCalendarStatus(raw, env) {
|
|
2533
2241
|
const outer = wrapped(raw, "calendar");
|
|
2534
2242
|
const value2 = record(outer.attempt) ?? record(outer.status) ?? outer;
|
|
@@ -2697,7 +2405,7 @@ async function calendarCalendars(options) {
|
|
|
2697
2405
|
return calendars;
|
|
2698
2406
|
}
|
|
2699
2407
|
async function calendarConnect(options) {
|
|
2700
|
-
const { cfg, ctx, out } = await lifecycleContext(options);
|
|
2408
|
+
const { cfg, ctx, out } = await lifecycleContext(options, ["app.manage"]);
|
|
2701
2409
|
productionConsent(ctx.env, options.yes, "connect calendar");
|
|
2702
2410
|
const page2 = calendarBookingPageUrl(cfg, ctx.env);
|
|
2703
2411
|
const applied = page2 === void 0 ? await readCalendarStatus(ctx) : await applyCalendarSettings(ctx, page2);
|
|
@@ -2712,10 +2420,7 @@ async function applyCalendarBookingPage(ctx, bookingPageUrl, out) {
|
|
|
2712
2420
|
}
|
|
2713
2421
|
async function calendarDisconnect(options) {
|
|
2714
2422
|
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;
|
|
2423
|
+
return requireStudioHuman(options.configPath, "calendar disconnect", "calendar", options.env);
|
|
2719
2424
|
}
|
|
2720
2425
|
async function ensureCalendarConnected(ctx, options) {
|
|
2721
2426
|
const out = options.stdout ?? console;
|
|
@@ -2723,7 +2428,7 @@ async function ensureCalendarConnected(ctx, options) {
|
|
|
2723
2428
|
const connectOptions = connectionOptions(options, out);
|
|
2724
2429
|
return await continueConnectedCalendar(ctx, current, connectOptions) ?? connectWithContext(ctx, connectOptions);
|
|
2725
2430
|
}
|
|
2726
|
-
async function lifecycleContext(options) {
|
|
2431
|
+
async function lifecycleContext(options, optionalProjectCapabilities = []) {
|
|
2727
2432
|
const cfg = await loadProjectConfig(options.configPath);
|
|
2728
2433
|
if (!cfg.services.includes("calendar")) throw new Error("calendar service is not enabled in config services");
|
|
2729
2434
|
const env = options.env ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
|
|
@@ -2741,7 +2446,8 @@ async function lifecycleContext(options) {
|
|
|
2741
2446
|
openApprovalUrl: options.openConsentUrl
|
|
2742
2447
|
},
|
|
2743
2448
|
doFetch,
|
|
2744
|
-
out
|
|
2449
|
+
out,
|
|
2450
|
+
{ optionalProjectCapabilities }
|
|
2745
2451
|
);
|
|
2746
2452
|
return { cfg, ctx: { platform: cfg.platformUrl, appId: cfg.app.id, env, token, fetch: doFetch }, out };
|
|
2747
2453
|
}
|
|
@@ -2928,9 +2634,9 @@ var import_apps6 = require("@odla-ai/apps");
|
|
|
2928
2634
|
var import_node_path8 = require("path");
|
|
2929
2635
|
|
|
2930
2636
|
// src/version.ts
|
|
2931
|
-
var
|
|
2637
|
+
var import_node_fs8 = require("fs");
|
|
2932
2638
|
function cliVersion() {
|
|
2933
|
-
const pkg = JSON.parse((0,
|
|
2639
|
+
const pkg = JSON.parse((0, import_node_fs8.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
|
|
2934
2640
|
return pkg.version ?? "unknown";
|
|
2935
2641
|
}
|
|
2936
2642
|
|
|
@@ -2946,7 +2652,7 @@ var ConfigOperationCommandError = class extends Error {
|
|
|
2946
2652
|
|
|
2947
2653
|
// src/config-operation-validate.ts
|
|
2948
2654
|
var import_apps3 = require("@odla-ai/apps");
|
|
2949
|
-
var
|
|
2655
|
+
var import_node_fs9 = require("fs");
|
|
2950
2656
|
|
|
2951
2657
|
// src/config-reconcile-digest.ts
|
|
2952
2658
|
var import_node_crypto2 = require("crypto");
|
|
@@ -2982,7 +2688,7 @@ var SERVICE = /^[a-z][a-z0-9-]{0,39}$/;
|
|
|
2982
2688
|
function readPlan(path) {
|
|
2983
2689
|
let value2;
|
|
2984
2690
|
try {
|
|
2985
|
-
const raw = (0,
|
|
2691
|
+
const raw = (0, import_node_fs9.readFileSync)(path, "utf8");
|
|
2986
2692
|
if (Buffer.byteLength(raw) > 128 * 1024) throw new Error("plan exceeds 128 KiB");
|
|
2987
2693
|
value2 = JSON.parse(raw);
|
|
2988
2694
|
} catch (error) {
|
|
@@ -3117,11 +2823,17 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
|
|
|
3117
2823
|
if (res.ok || res.status === 404) return;
|
|
3118
2824
|
if (res.status === 403) {
|
|
3119
2825
|
const detail = await safeText4(res);
|
|
3120
|
-
|
|
2826
|
+
const code = errorCode(detail);
|
|
2827
|
+
if (code === "human_session_required") {
|
|
3121
2828
|
throw new Error(
|
|
3122
2829
|
`${env}: odla-db rejected the provision credential before checking ownership for "${cfg.app.id}" (tenant ${tenantId}): human_session_required. Retrying or changing app owners will not help; the deployed odla-db must accept owner-approved app.manage credentials on provisioning routes`
|
|
3123
2830
|
);
|
|
3124
2831
|
}
|
|
2832
|
+
if (code === "provision_approval_required") {
|
|
2833
|
+
throw new Error(
|
|
2834
|
+
`${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. Discard the cached or supplied token and run this command with the current CLI to approve one fresh exact-project provisioning handshake`
|
|
2835
|
+
);
|
|
2836
|
+
}
|
|
3125
2837
|
throw new Error(
|
|
3126
2838
|
`${env}: this credential lacks live app.manage authority for "${cfg.app.id}" (tenant ${tenantId}) \u2014 nothing was minted or written; re-run provision with a fresh owner-approved provision handshake. If the human account is not an owner, an existing owner must add it in signed-in Studio; an agent token cannot repair ownership`
|
|
3127
2839
|
);
|
|
@@ -3689,7 +3401,7 @@ async function configPlan(options) {
|
|
|
3689
3401
|
apply,
|
|
3690
3402
|
nextActions: planNextActions(reconciliation, options.configPath)
|
|
3691
3403
|
};
|
|
3692
|
-
|
|
3404
|
+
printPlan(document2, options);
|
|
3693
3405
|
return document2;
|
|
3694
3406
|
}
|
|
3695
3407
|
async function inspectConfig(options) {
|
|
@@ -3729,7 +3441,7 @@ function printDiff(document2, options) {
|
|
|
3729
3441
|
printDifferences(out, document2);
|
|
3730
3442
|
printNext(out, document2.nextActions);
|
|
3731
3443
|
}
|
|
3732
|
-
function
|
|
3444
|
+
function printPlan(document2, options) {
|
|
3733
3445
|
const out = options.stdout ?? console;
|
|
3734
3446
|
if (options.json) {
|
|
3735
3447
|
out.log(JSON.stringify(document2, null, 2));
|
|
@@ -3832,12 +3544,12 @@ function quoteArg2(value2) {
|
|
|
3832
3544
|
|
|
3833
3545
|
// src/doctor-checks.ts
|
|
3834
3546
|
var import_node_child_process3 = require("child_process");
|
|
3835
|
-
var
|
|
3547
|
+
var import_node_fs11 = require("fs");
|
|
3836
3548
|
var import_node_path11 = require("path");
|
|
3837
3549
|
|
|
3838
3550
|
// src/wrangler.ts
|
|
3839
3551
|
var import_node_child_process2 = require("child_process");
|
|
3840
|
-
var
|
|
3552
|
+
var import_node_fs10 = require("fs");
|
|
3841
3553
|
var import_node_path10 = require("path");
|
|
3842
3554
|
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
3843
3555
|
const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -3853,14 +3565,14 @@ var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]
|
|
|
3853
3565
|
function findWranglerConfig(rootDir) {
|
|
3854
3566
|
for (const name of WRANGLER_CONFIG_FILES) {
|
|
3855
3567
|
const path = (0, import_node_path10.join)(rootDir, name);
|
|
3856
|
-
if ((0,
|
|
3568
|
+
if ((0, import_node_fs10.existsSync)(path)) return path;
|
|
3857
3569
|
}
|
|
3858
3570
|
return null;
|
|
3859
3571
|
}
|
|
3860
3572
|
function readWranglerConfig(path) {
|
|
3861
3573
|
if (path.endsWith(".toml")) return null;
|
|
3862
3574
|
try {
|
|
3863
|
-
return JSON.parse(stripJsonComments((0,
|
|
3575
|
+
return JSON.parse(stripJsonComments((0, import_node_fs10.readFileSync)(path, "utf8")));
|
|
3864
3576
|
} catch {
|
|
3865
3577
|
return null;
|
|
3866
3578
|
}
|
|
@@ -3968,7 +3680,7 @@ function wranglerWarnings(rootDir) {
|
|
|
3968
3680
|
const dir = (0, import_node_path11.resolve)(rootDir, assets.directory);
|
|
3969
3681
|
if (dir === (0, import_node_path11.resolve)(rootDir)) {
|
|
3970
3682
|
warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
|
|
3971
|
-
} else if ((0,
|
|
3683
|
+
} else if ((0, import_node_fs11.existsSync)((0, import_node_path11.join)(dir, "node_modules"))) {
|
|
3972
3684
|
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
3973
3685
|
}
|
|
3974
3686
|
}
|
|
@@ -4004,12 +3716,12 @@ function o11yProjectWarnings(rootDir) {
|
|
|
4004
3716
|
return warnings;
|
|
4005
3717
|
}
|
|
4006
3718
|
const main = typeof config.main === "string" ? (0, import_node_path11.resolve)(rootDir, config.main) : null;
|
|
4007
|
-
if (!main || !(0,
|
|
3719
|
+
if (!main || !(0, import_node_fs11.existsSync)(main)) {
|
|
4008
3720
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
|
|
4009
3721
|
} else {
|
|
4010
3722
|
let source = "";
|
|
4011
3723
|
try {
|
|
4012
|
-
source = (0,
|
|
3724
|
+
source = (0, import_node_fs11.readFileSync)(main, "utf8");
|
|
4013
3725
|
} catch {
|
|
4014
3726
|
}
|
|
4015
3727
|
if (!/\bwithObservability\b/.test(source)) {
|
|
@@ -4033,7 +3745,7 @@ function calendarProjectWarnings(rootDir) {
|
|
|
4033
3745
|
}
|
|
4034
3746
|
function readPackageJson(rootDir) {
|
|
4035
3747
|
try {
|
|
4036
|
-
return JSON.parse((0,
|
|
3748
|
+
return JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path11.join)(rootDir, "package.json"), "utf8"));
|
|
4037
3749
|
} catch {
|
|
4038
3750
|
return null;
|
|
4039
3751
|
}
|
|
@@ -4262,14 +3974,14 @@ function harnessOption(value2, flag) {
|
|
|
4262
3974
|
}
|
|
4263
3975
|
|
|
4264
3976
|
// src/init.ts
|
|
4265
|
-
var
|
|
3977
|
+
var import_node_fs12 = require("fs");
|
|
4266
3978
|
var import_node_path12 = require("path");
|
|
4267
3979
|
var import_apps9 = require("@odla-ai/apps");
|
|
4268
3980
|
function initProject(options) {
|
|
4269
3981
|
const out = options.stdout ?? console;
|
|
4270
3982
|
const rootDir = (0, import_node_path12.resolve)(options.rootDir ?? process.cwd());
|
|
4271
3983
|
const configPath = (0, import_node_path12.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
|
|
4272
|
-
if ((0,
|
|
3984
|
+
if ((0, import_node_fs12.existsSync)(configPath) && !options.force) {
|
|
4273
3985
|
throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
|
|
4274
3986
|
}
|
|
4275
3987
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
|
|
@@ -4285,10 +3997,10 @@ function initProject(options) {
|
|
|
4285
3997
|
}
|
|
4286
3998
|
}
|
|
4287
3999
|
const aiProvider = options.aiProvider;
|
|
4288
|
-
(0,
|
|
4289
|
-
(0,
|
|
4290
|
-
(0,
|
|
4291
|
-
(0,
|
|
4000
|
+
(0, import_node_fs12.mkdirSync)((0, import_node_path12.dirname)(configPath), { recursive: true });
|
|
4001
|
+
(0, import_node_fs12.mkdirSync)((0, import_node_path12.resolve)(rootDir, "src/odla"), { recursive: true });
|
|
4002
|
+
(0, import_node_fs12.mkdirSync)((0, import_node_path12.resolve)(rootDir, ".odla"), { recursive: true });
|
|
4003
|
+
(0, import_node_fs12.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
4292
4004
|
writeIfMissing((0, import_node_path12.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
4293
4005
|
writeIfMissing((0, import_node_path12.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
4294
4006
|
ensureGitignore(rootDir);
|
|
@@ -4297,8 +4009,8 @@ function initProject(options) {
|
|
|
4297
4009
|
out.log("updated .gitignore for local odla credentials");
|
|
4298
4010
|
}
|
|
4299
4011
|
function writeIfMissing(path, text2) {
|
|
4300
|
-
if ((0,
|
|
4301
|
-
(0,
|
|
4012
|
+
if ((0, import_node_fs12.existsSync)(path)) return;
|
|
4013
|
+
(0, import_node_fs12.writeFileSync)(path, text2);
|
|
4302
4014
|
}
|
|
4303
4015
|
function configTemplate(input) {
|
|
4304
4016
|
const calendar = input.services.includes("calendar") ? ` calendar: {
|
|
@@ -4486,7 +4198,9 @@ async function secretsSet(options) {
|
|
|
4486
4198
|
throw new Error('"$"-prefixed vault names are platform-reserved; for the Clerk secret key use "odla-ai secrets set-clerk-key"');
|
|
4487
4199
|
}
|
|
4488
4200
|
const { cfg, tenantId, value: value2, doFetch, out } = await resolveVaultWrite(options);
|
|
4489
|
-
const token = await getDeveloperToken(cfg, options, doFetch, out
|
|
4201
|
+
const token = await getDeveloperToken(cfg, options, doFetch, out, {
|
|
4202
|
+
optionalProjectCapabilities: ["app.manage"]
|
|
4203
|
+
});
|
|
4490
4204
|
try {
|
|
4491
4205
|
await (0, import_ai2.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, name, value2);
|
|
4492
4206
|
} catch (err) {
|
|
@@ -4503,7 +4217,9 @@ async function secretsSetClerkKey(options) {
|
|
|
4503
4217
|
if (value2.startsWith("sk_live_") && !PROD_ENV_NAMES2.has(options.env) && !options.yes) {
|
|
4504
4218
|
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)`);
|
|
4505
4219
|
}
|
|
4506
|
-
const token = await getDeveloperToken(cfg, options, doFetch, out
|
|
4220
|
+
const token = await getDeveloperToken(cfg, options, doFetch, out, {
|
|
4221
|
+
optionalProjectCapabilities: ["app.manage"]
|
|
4222
|
+
});
|
|
4507
4223
|
const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/clerk-secret`, {
|
|
4508
4224
|
method: "POST",
|
|
4509
4225
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
@@ -4533,7 +4249,7 @@ async function resolveVaultWrite(options) {
|
|
|
4533
4249
|
}
|
|
4534
4250
|
|
|
4535
4251
|
// src/skill.ts
|
|
4536
|
-
var
|
|
4252
|
+
var import_node_fs13 = require("fs");
|
|
4537
4253
|
var import_node_os2 = require("os");
|
|
4538
4254
|
var import_node_path13 = require("path");
|
|
4539
4255
|
var import_node_url2 = require("url");
|
|
@@ -4630,7 +4346,7 @@ function installSkill(options = {}) {
|
|
|
4630
4346
|
plans.set(target, { target, content: content2, boundary, managedMerge });
|
|
4631
4347
|
};
|
|
4632
4348
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
4633
|
-
for (const rel of files) plan((0, import_node_path13.join)(targetDir2, rel), (0,
|
|
4349
|
+
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);
|
|
4634
4350
|
};
|
|
4635
4351
|
let targetDir;
|
|
4636
4352
|
if (options.global) {
|
|
@@ -4650,7 +4366,7 @@ function installSkill(options = {}) {
|
|
|
4650
4366
|
for (const harness of harnesses) rememberTarget(harness, sharedRoot);
|
|
4651
4367
|
if (harnesses.includes("claude")) {
|
|
4652
4368
|
for (const skill of skillNames(files)) {
|
|
4653
|
-
const canonical = (0,
|
|
4369
|
+
const canonical = (0, import_node_fs13.readFileSync)((0, import_node_path13.join)(sourceDir, skill, "SKILL.md"), "utf8");
|
|
4654
4370
|
plan((0, import_node_path13.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
|
|
4655
4371
|
}
|
|
4656
4372
|
rememberTarget("claude", claudeRoot);
|
|
@@ -4685,11 +4401,11 @@ function installSkill(options = {}) {
|
|
|
4685
4401
|
conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
|
|
4686
4402
|
continue;
|
|
4687
4403
|
}
|
|
4688
|
-
if (!(0,
|
|
4404
|
+
if (!(0, import_node_fs13.existsSync)(file.target)) {
|
|
4689
4405
|
writtenPaths.add(file.target);
|
|
4690
4406
|
continue;
|
|
4691
4407
|
}
|
|
4692
|
-
const current = (0,
|
|
4408
|
+
const current = (0, import_node_fs13.readFileSync)(file.target, "utf8");
|
|
4693
4409
|
if (current === file.content) {
|
|
4694
4410
|
unchangedPaths.add(file.target);
|
|
4695
4411
|
} else if (file.managedMerge || options.force) {
|
|
@@ -4706,9 +4422,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
4706
4422
|
);
|
|
4707
4423
|
}
|
|
4708
4424
|
for (const file of plans.values()) {
|
|
4709
|
-
if (!(0,
|
|
4710
|
-
(0,
|
|
4711
|
-
(0,
|
|
4425
|
+
if (!(0, import_node_fs13.existsSync)(file.target) || (0, import_node_fs13.readFileSync)(file.target, "utf8") !== file.content) {
|
|
4426
|
+
(0, import_node_fs13.mkdirSync)((0, import_node_path13.dirname)(file.target), { recursive: true });
|
|
4427
|
+
(0, import_node_fs13.writeFileSync)(file.target, file.content);
|
|
4712
4428
|
}
|
|
4713
4429
|
}
|
|
4714
4430
|
const skills = skillNames(files);
|
|
@@ -4749,9 +4465,9 @@ function normalizeHarnesses(values, global) {
|
|
|
4749
4465
|
function managedFileContent(path, block, force, boundary) {
|
|
4750
4466
|
const symlink = symlinkedComponent(boundary, path);
|
|
4751
4467
|
if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
|
|
4752
|
-
if (!(0,
|
|
4468
|
+
if (!(0, import_node_fs13.existsSync)(path)) return `${block}
|
|
4753
4469
|
`;
|
|
4754
|
-
const current = (0,
|
|
4470
|
+
const current = (0, import_node_fs13.readFileSync)(path, "utf8");
|
|
4755
4471
|
const start = "<!-- odla-ai agent setup:start -->";
|
|
4756
4472
|
const end = "<!-- odla-ai agent setup:end -->";
|
|
4757
4473
|
const startAt = current.indexOf(start);
|
|
@@ -4780,7 +4496,7 @@ function symlinkedComponent(boundary, target) {
|
|
|
4780
4496
|
for (const part of rel.split(import_node_path13.sep).filter(Boolean)) {
|
|
4781
4497
|
current = (0, import_node_path13.join)(current, part);
|
|
4782
4498
|
try {
|
|
4783
|
-
if ((0,
|
|
4499
|
+
if ((0, import_node_fs13.lstatSync)(current).isSymbolicLink()) return current;
|
|
4784
4500
|
} catch (error) {
|
|
4785
4501
|
if (error.code !== "ENOENT") throw error;
|
|
4786
4502
|
}
|
|
@@ -4791,10 +4507,10 @@ function skillNames(files) {
|
|
|
4791
4507
|
return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
|
|
4792
4508
|
}
|
|
4793
4509
|
function listFiles(dir) {
|
|
4794
|
-
if (!(0,
|
|
4510
|
+
if (!(0, import_node_fs13.existsSync)(dir)) return [];
|
|
4795
4511
|
const results = [];
|
|
4796
4512
|
const walk = (current) => {
|
|
4797
|
-
for (const entry of (0,
|
|
4513
|
+
for (const entry of (0, import_node_fs13.readdirSync)(current, { withFileTypes: true })) {
|
|
4798
4514
|
const path = (0, import_node_path13.join)(current, entry.name);
|
|
4799
4515
|
if (entry.isDirectory()) walk(path);
|
|
4800
4516
|
else results.push((0, import_node_path13.relative)(dir, path));
|
|
@@ -5111,7 +4827,7 @@ async function projectCommand(command, parsed, deps) {
|
|
|
5111
4827
|
}
|
|
5112
4828
|
|
|
5113
4829
|
// src/code-connect.ts
|
|
5114
|
-
var
|
|
4830
|
+
var import_node_fs14 = require("fs");
|
|
5115
4831
|
var import_node_os4 = require("os");
|
|
5116
4832
|
var import_node_path15 = require("path");
|
|
5117
4833
|
|
|
@@ -5199,15 +4915,15 @@ function encodeAgentInput(message2) {
|
|
|
5199
4915
|
// ../harness/dist/chunk-PHXQH4YM.js
|
|
5200
4916
|
var import_child_process = require("child_process");
|
|
5201
4917
|
var import_fs = require("fs");
|
|
5202
|
-
var
|
|
4918
|
+
var import_promises2 = require("fs/promises");
|
|
5203
4919
|
var import_path = require("path");
|
|
5204
4920
|
var import_process = require("process");
|
|
5205
|
-
var
|
|
4921
|
+
var import_promises3 = require("fs/promises");
|
|
5206
4922
|
var import_os = require("os");
|
|
5207
4923
|
var import_path2 = require("path");
|
|
5208
4924
|
var import_child_process2 = require("child_process");
|
|
5209
4925
|
var import_path3 = require("path");
|
|
5210
|
-
var
|
|
4926
|
+
var import_promises4 = require("fs/promises");
|
|
5211
4927
|
var import_os2 = require("os");
|
|
5212
4928
|
var import_path4 = require("path");
|
|
5213
4929
|
var import_child_process3 = require("child_process");
|
|
@@ -5218,7 +4934,7 @@ function assertPinnedImage(image) {
|
|
|
5218
4934
|
async function commandAvailable(engine) {
|
|
5219
4935
|
for (const directory of (process.env.PATH ?? "").split(import_path.delimiter).filter(Boolean)) {
|
|
5220
4936
|
try {
|
|
5221
|
-
await (0,
|
|
4937
|
+
await (0, import_promises2.access)((0, import_path.join)(directory, engine), import_fs.constants.X_OK);
|
|
5222
4938
|
return true;
|
|
5223
4939
|
} catch {
|
|
5224
4940
|
}
|
|
@@ -5504,7 +5220,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
|
|
|
5504
5220
|
}
|
|
5505
5221
|
async function materializeGitTree(source, commitSha, options = {}) {
|
|
5506
5222
|
if (!/^[0-9a-f]{40}$/.test(commitSha)) throw new TypeError("Git tree requires an exact commit SHA");
|
|
5507
|
-
const sourceDir = await (0,
|
|
5223
|
+
const sourceDir = await (0, import_promises3.realpath)((0, import_path2.resolve)(source));
|
|
5508
5224
|
const maxFiles = options.maxFiles ?? 2e4;
|
|
5509
5225
|
const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
|
|
5510
5226
|
const inventory = (await gitOutput(sourceDir, ["ls-tree", "-rz", commitSha], 16 * 1024 * 1024)).toString("utf8").split("\0").filter(Boolean);
|
|
@@ -5513,9 +5229,9 @@ async function materializeGitTree(source, commitSha, options = {}) {
|
|
|
5513
5229
|
return match && allowedWorkspacePath(match[3]) ? [{ mode: match[1], hash: match[2], path: match[3] }] : [];
|
|
5514
5230
|
});
|
|
5515
5231
|
if (entries.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
|
|
5516
|
-
const root = await (0,
|
|
5232
|
+
const root = await (0, import_promises3.mkdtemp)((0, import_path2.join)(options.tempRoot ?? (0, import_os.tmpdir)(), "odla-git-tree-"));
|
|
5517
5233
|
const targetRoot = (0, import_path2.join)(root, "source");
|
|
5518
|
-
await (0,
|
|
5234
|
+
await (0, import_promises3.mkdir)(targetRoot);
|
|
5519
5235
|
let byteCount = 0;
|
|
5520
5236
|
try {
|
|
5521
5237
|
const blobs = await gitBlobs(sourceDir, entries, maxBytes);
|
|
@@ -5525,18 +5241,18 @@ async function materializeGitTree(source, commitSha, options = {}) {
|
|
|
5525
5241
|
if (byteCount > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
5526
5242
|
const target = (0, import_path2.resolve)(targetRoot, entry.path);
|
|
5527
5243
|
if (!target.startsWith(`${(0, import_path2.resolve)(targetRoot)}${import_path2.sep}`)) throw new TypeError("Git tree path escapes workspace");
|
|
5528
|
-
await (0,
|
|
5529
|
-
await (0,
|
|
5244
|
+
await (0, import_promises3.mkdir)((0, import_path2.resolve)(target, ".."), { recursive: true });
|
|
5245
|
+
await (0, import_promises3.writeFile)(target, content2, { flag: "wx", mode: entry.mode === "100755" ? 493 : 420 });
|
|
5530
5246
|
}
|
|
5531
5247
|
return {
|
|
5532
5248
|
root,
|
|
5533
5249
|
sourceDir: targetRoot,
|
|
5534
5250
|
fileCount: entries.length,
|
|
5535
5251
|
byteCount,
|
|
5536
|
-
cleanup: () => (0,
|
|
5252
|
+
cleanup: () => (0, import_promises3.rm)(root, { recursive: true, force: true })
|
|
5537
5253
|
};
|
|
5538
5254
|
} catch (error) {
|
|
5539
|
-
await (0,
|
|
5255
|
+
await (0, import_promises3.rm)(root, { recursive: true, force: true });
|
|
5540
5256
|
throw error;
|
|
5541
5257
|
}
|
|
5542
5258
|
}
|
|
@@ -5544,7 +5260,7 @@ async function sourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
|
5544
5260
|
const files = [];
|
|
5545
5261
|
let bytes = 0;
|
|
5546
5262
|
const walk = async (dir) => {
|
|
5547
|
-
for (const entry of await (0,
|
|
5263
|
+
for (const entry of await (0, import_promises4.readdir)(dir, { withFileTypes: true })) {
|
|
5548
5264
|
if (entry.isDirectory() && SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
|
|
5549
5265
|
if (!entry.isDirectory() && SECRET_WORKSPACE_FILE.test(entry.name)) continue;
|
|
5550
5266
|
const path = (0, import_path4.join)(dir, entry.name);
|
|
@@ -5554,7 +5270,7 @@ async function sourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
|
5554
5270
|
continue;
|
|
5555
5271
|
}
|
|
5556
5272
|
if (!entry.isFile()) continue;
|
|
5557
|
-
const metadata2 = await (0,
|
|
5273
|
+
const metadata2 = await (0, import_promises4.stat)(path);
|
|
5558
5274
|
bytes += metadata2.size;
|
|
5559
5275
|
if (files.length + 1 > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
|
|
5560
5276
|
if (bytes > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
@@ -5603,7 +5319,7 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
|
5603
5319
|
if (!source.startsWith(`${root}${import_path4.sep}`)) throw new TypeError("git file path escapes workspace");
|
|
5604
5320
|
let metadata2;
|
|
5605
5321
|
try {
|
|
5606
|
-
metadata2 = await (0,
|
|
5322
|
+
metadata2 = await (0, import_promises4.lstat)(source);
|
|
5607
5323
|
} catch (error) {
|
|
5608
5324
|
if (error.code === "ENOENT") continue;
|
|
5609
5325
|
throw error;
|
|
@@ -5618,9 +5334,9 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
|
5618
5334
|
async function copyTree(files, destination) {
|
|
5619
5335
|
for (const file of files) {
|
|
5620
5336
|
const target = (0, import_path4.join)(destination, file.relativePath);
|
|
5621
|
-
await (0,
|
|
5622
|
-
await (0,
|
|
5623
|
-
await (0,
|
|
5337
|
+
await (0, import_promises4.mkdir)((0, import_path4.resolve)(target, ".."), { recursive: true });
|
|
5338
|
+
await (0, import_promises4.copyFile)(file.source, target);
|
|
5339
|
+
await (0, import_promises4.chmod)(target, file.mode);
|
|
5624
5340
|
}
|
|
5625
5341
|
}
|
|
5626
5342
|
async function captureGitDiff(root, maxBytes) {
|
|
@@ -5657,13 +5373,13 @@ async function captureGitDiff(root, maxBytes) {
|
|
|
5657
5373
|
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");
|
|
5658
5374
|
}
|
|
5659
5375
|
async function stageWorkspace(source, options = {}) {
|
|
5660
|
-
const sourceDir = await (0,
|
|
5661
|
-
const sourceStat = await (0,
|
|
5376
|
+
const sourceDir = await (0, import_promises4.realpath)((0, import_path4.resolve)(source));
|
|
5377
|
+
const sourceStat = await (0, import_promises4.stat)(sourceDir);
|
|
5662
5378
|
if (!sourceStat.isDirectory()) throw new TypeError("workspace source must be a directory");
|
|
5663
|
-
const root = await (0,
|
|
5379
|
+
const root = await (0, import_promises4.mkdtemp)((0, import_path4.join)(options.tempRoot ?? (0, import_os2.tmpdir)(), "odla-harness-"));
|
|
5664
5380
|
const baselineDir = (0, import_path4.join)(root, "baseline");
|
|
5665
5381
|
const workspaceDir = (0, import_path4.join)(root, "workspace");
|
|
5666
|
-
await Promise.all([(0,
|
|
5382
|
+
await Promise.all([(0, import_promises4.mkdir)(baselineDir), (0, import_promises4.mkdir)(workspaceDir)]);
|
|
5667
5383
|
try {
|
|
5668
5384
|
const maxFiles = options.maxFiles ?? 2e4;
|
|
5669
5385
|
const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
|
|
@@ -5676,26 +5392,26 @@ async function stageWorkspace(source, options = {}) {
|
|
|
5676
5392
|
fileCount: files.length,
|
|
5677
5393
|
byteCount: files.reduce((sum, file) => sum + file.bytes, 0),
|
|
5678
5394
|
patch: (maxBytes2) => captureGitDiff(root, maxBytes2),
|
|
5679
|
-
cleanup: () => (0,
|
|
5395
|
+
cleanup: () => (0, import_promises4.rm)(root, { recursive: true, force: true })
|
|
5680
5396
|
};
|
|
5681
5397
|
} catch (error) {
|
|
5682
|
-
await (0,
|
|
5398
|
+
await (0, import_promises4.rm)(root, { recursive: true, force: true });
|
|
5683
5399
|
throw error;
|
|
5684
5400
|
}
|
|
5685
5401
|
}
|
|
5686
5402
|
async function stageWorkspacePair(baselineSource, workspaceSource, options = {}) {
|
|
5687
|
-
const baselineDirSource = await (0,
|
|
5688
|
-
const workspaceDirSource = await (0,
|
|
5403
|
+
const baselineDirSource = await (0, import_promises4.realpath)((0, import_path4.resolve)(baselineSource));
|
|
5404
|
+
const workspaceDirSource = await (0, import_promises4.realpath)((0, import_path4.resolve)(workspaceSource));
|
|
5689
5405
|
const maxFiles = options.maxFiles ?? 2e4;
|
|
5690
5406
|
const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
|
|
5691
5407
|
const [baselineFiles, workspaceFiles] = await Promise.all([
|
|
5692
5408
|
sourceFiles(baselineDirSource, maxFiles, maxBytes),
|
|
5693
5409
|
sourceFiles(workspaceDirSource, maxFiles, maxBytes)
|
|
5694
5410
|
]);
|
|
5695
|
-
const root = await (0,
|
|
5411
|
+
const root = await (0, import_promises4.mkdtemp)((0, import_path4.join)(options.tempRoot ?? (0, import_os2.tmpdir)(), "odla-harness-"));
|
|
5696
5412
|
const baselineDir = (0, import_path4.join)(root, "baseline");
|
|
5697
5413
|
const workspaceDir = (0, import_path4.join)(root, "workspace");
|
|
5698
|
-
await Promise.all([(0,
|
|
5414
|
+
await Promise.all([(0, import_promises4.mkdir)(baselineDir), (0, import_promises4.mkdir)(workspaceDir)]);
|
|
5699
5415
|
try {
|
|
5700
5416
|
await Promise.all([copyTree(baselineFiles, baselineDir), copyTree(workspaceFiles, workspaceDir)]);
|
|
5701
5417
|
return {
|
|
@@ -5705,17 +5421,17 @@ async function stageWorkspacePair(baselineSource, workspaceSource, options = {})
|
|
|
5705
5421
|
fileCount: workspaceFiles.length,
|
|
5706
5422
|
byteCount: workspaceFiles.reduce((sum, file) => sum + file.bytes, 0),
|
|
5707
5423
|
patch: (maxPatchBytes) => captureGitDiff(root, maxPatchBytes),
|
|
5708
|
-
cleanup: () => (0,
|
|
5424
|
+
cleanup: () => (0, import_promises4.rm)(root, { recursive: true, force: true })
|
|
5709
5425
|
};
|
|
5710
5426
|
} catch (error) {
|
|
5711
|
-
await (0,
|
|
5427
|
+
await (0, import_promises4.rm)(root, { recursive: true, force: true });
|
|
5712
5428
|
throw error;
|
|
5713
5429
|
}
|
|
5714
5430
|
}
|
|
5715
5431
|
|
|
5716
5432
|
// ../harness/dist/chunk-GMVZ4LZH.js
|
|
5717
5433
|
var import_crypto = require("crypto");
|
|
5718
|
-
var
|
|
5434
|
+
var import_promises5 = require("fs/promises");
|
|
5719
5435
|
var import_path5 = require("path");
|
|
5720
5436
|
|
|
5721
5437
|
// ../camel/dist/chunk-7FHPOQVP.js
|
|
@@ -6052,19 +5768,19 @@ function validateSnapshot(snapshot, limits) {
|
|
|
6052
5768
|
|
|
6053
5769
|
// ../harness/dist/chunk-GMVZ4LZH.js
|
|
6054
5770
|
var import_child_process4 = require("child_process");
|
|
6055
|
-
var
|
|
5771
|
+
var import_promises6 = require("fs/promises");
|
|
6056
5772
|
var import_path6 = require("path");
|
|
6057
5773
|
var import_child_process5 = require("child_process");
|
|
6058
5774
|
var import_process2 = require("process");
|
|
6059
5775
|
var import_crypto2 = require("crypto");
|
|
6060
5776
|
var import_crypto3 = require("crypto");
|
|
6061
5777
|
var import_fs2 = require("fs");
|
|
6062
|
-
var
|
|
5778
|
+
var import_promises7 = require("fs/promises");
|
|
6063
5779
|
var import_path7 = require("path");
|
|
6064
|
-
var
|
|
5780
|
+
var import_promises8 = require("fs/promises");
|
|
6065
5781
|
var import_os3 = require("os");
|
|
6066
5782
|
var import_path8 = require("path");
|
|
6067
|
-
var
|
|
5783
|
+
var import_promises9 = require("fs/promises");
|
|
6068
5784
|
var import_path9 = require("path");
|
|
6069
5785
|
|
|
6070
5786
|
// ../camel/dist/chunk-4EIRFS3A.js
|
|
@@ -6356,7 +6072,7 @@ var import_crypto4 = require("crypto");
|
|
|
6356
6072
|
async function digestStagedWorkspace(root, limits) {
|
|
6357
6073
|
const files = [];
|
|
6358
6074
|
const walk = async (directory) => {
|
|
6359
|
-
const entries = await (0,
|
|
6075
|
+
const entries = await (0, import_promises5.readdir)(directory, { withFileTypes: true });
|
|
6360
6076
|
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
6361
6077
|
if (entry.isSymbolicLink()) throw new TypeError("workspace digest refuses symbolic links");
|
|
6362
6078
|
const target = (0, import_path5.resolve)(directory, entry.name);
|
|
@@ -6371,7 +6087,7 @@ async function digestStagedWorkspace(root, limits) {
|
|
|
6371
6087
|
const hash = (0, import_crypto.createHash)("sha256");
|
|
6372
6088
|
let bytes = 0;
|
|
6373
6089
|
for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
|
|
6374
|
-
const content2 = await (0,
|
|
6090
|
+
const content2 = await (0, import_promises5.readFile)(file.target);
|
|
6375
6091
|
bytes += Buffer.byteLength(file.path) + content2.byteLength;
|
|
6376
6092
|
if (bytes > limits.maxBytes) throw new TypeError("workspace digest exceeds its byte bound");
|
|
6377
6093
|
hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content2.byteLength}:`);
|
|
@@ -6697,7 +6413,7 @@ async function applyCodePatch(workspaceDir, patch2, paths) {
|
|
|
6697
6413
|
await gitApply(workspaceDir, patch2, false);
|
|
6698
6414
|
for (const path of paths) {
|
|
6699
6415
|
try {
|
|
6700
|
-
const info = await (0,
|
|
6416
|
+
const info = await (0, import_promises6.lstat)(resolveCodePath(workspaceDir, path));
|
|
6701
6417
|
if (info.isSymbolicLink() || !info.isFile() && !info.isDirectory()) {
|
|
6702
6418
|
throw new TypeError("patch created a non-regular workspace entry");
|
|
6703
6419
|
}
|
|
@@ -6988,7 +6704,7 @@ async function inspectArtifacts(workspaceDir, recipe2) {
|
|
|
6988
6704
|
for (const artifact of recipe2.expectedArtifacts ?? []) {
|
|
6989
6705
|
try {
|
|
6990
6706
|
const path = (0, import_path7.join)(workspaceDir, artifact.path);
|
|
6991
|
-
const info = await (0,
|
|
6707
|
+
const info = await (0, import_promises7.lstat)(path);
|
|
6992
6708
|
if (!info.isFile() || info.isSymbolicLink()) {
|
|
6993
6709
|
receipts.push({ artifactId: artifact.id, status: "invalid", bytes: null, digest: null });
|
|
6994
6710
|
} else if (info.size > artifact.maximumBytes) {
|
|
@@ -7169,9 +6885,9 @@ var RESERVED2 = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_mod
|
|
|
7169
6885
|
var SECRET2 = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
|
|
7170
6886
|
async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os3.tmpdir)()) {
|
|
7171
6887
|
if (!snapshot.files.length || snapshot.files.length > 1e4) throw new TypeError("Code source file count is invalid");
|
|
7172
|
-
const root = await (0,
|
|
6888
|
+
const root = await (0, import_promises8.mkdtemp)((0, import_path8.join)(tempRoot, "odla-code-source-"));
|
|
7173
6889
|
const sourceDir = (0, import_path8.join)(root, "source");
|
|
7174
|
-
await (0,
|
|
6890
|
+
await (0, import_promises8.mkdir)(sourceDir);
|
|
7175
6891
|
const seen = /* @__PURE__ */ new Set();
|
|
7176
6892
|
let bytes = 0;
|
|
7177
6893
|
try {
|
|
@@ -7183,8 +6899,8 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os3.
|
|
|
7183
6899
|
if (bytes > 16 * 1024 * 1024) throw new TypeError("Code source exceeds its byte bound");
|
|
7184
6900
|
const target = (0, import_path8.resolve)(sourceDir, file.path);
|
|
7185
6901
|
if (!target.startsWith(`${(0, import_path8.resolve)(sourceDir)}${import_path8.sep}`)) throw new TypeError("Code source path escapes its root");
|
|
7186
|
-
await (0,
|
|
7187
|
-
await (0,
|
|
6902
|
+
await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
|
|
6903
|
+
await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 420 });
|
|
7188
6904
|
}
|
|
7189
6905
|
for (const reference of snapshot.references ?? []) {
|
|
7190
6906
|
validateAlias(reference.alias);
|
|
@@ -7198,13 +6914,13 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os3.
|
|
|
7198
6914
|
if (bytes > 80 * 1024 * 1024) throw new TypeError("Code source set exceeds its byte bound");
|
|
7199
6915
|
const target = (0, import_path8.resolve)(sourceDir, path);
|
|
7200
6916
|
if (!target.startsWith(`${(0, import_path8.resolve)(sourceDir)}${import_path8.sep}`)) throw new TypeError("Code reference path escapes its root");
|
|
7201
|
-
await (0,
|
|
7202
|
-
await (0,
|
|
6917
|
+
await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
|
|
6918
|
+
await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 292 });
|
|
7203
6919
|
}
|
|
7204
6920
|
}
|
|
7205
|
-
return { sourceDir, cleanup: () => (0,
|
|
6921
|
+
return { sourceDir, cleanup: () => (0, import_promises8.rm)(root, { recursive: true, force: true }) };
|
|
7206
6922
|
} catch (cause) {
|
|
7207
|
-
await (0,
|
|
6923
|
+
await (0, import_promises8.rm)(root, { recursive: true, force: true });
|
|
7208
6924
|
throw cause;
|
|
7209
6925
|
}
|
|
7210
6926
|
}
|
|
@@ -7225,8 +6941,8 @@ async function attachCodeRuntimeReferences(workspace, references) {
|
|
|
7225
6941
|
for (const root of [workspace.baselineDir, workspace.workspaceDir]) {
|
|
7226
6942
|
const target = (0, import_path8.resolve)(root, path);
|
|
7227
6943
|
if (!target.startsWith(`${(0, import_path8.resolve)(root)}${import_path8.sep}`)) throw new TypeError("Code reference path escapes its root");
|
|
7228
|
-
await (0,
|
|
7229
|
-
await (0,
|
|
6944
|
+
await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
|
|
6945
|
+
await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 292 });
|
|
7230
6946
|
}
|
|
7231
6947
|
}
|
|
7232
6948
|
}
|
|
@@ -7412,11 +7128,11 @@ async function read(context, request2, options, policy) {
|
|
|
7412
7128
|
const allowed = await policy.read(policyContext(context, request2, options, { paths, path, startLine, endLine }));
|
|
7413
7129
|
if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
|
|
7414
7130
|
const target = resolveCodePath(context.workspaceDir, path);
|
|
7415
|
-
const info = await (0,
|
|
7131
|
+
const info = await (0, import_promises9.stat)(target);
|
|
7416
7132
|
if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
|
|
7417
7133
|
throw new TypeError("file is not a bounded regular source file");
|
|
7418
7134
|
}
|
|
7419
|
-
const source = await (0,
|
|
7135
|
+
const source = await (0, import_promises9.readFile)(target);
|
|
7420
7136
|
if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
|
|
7421
7137
|
const lines = source.toString("utf8").split("\n");
|
|
7422
7138
|
const content2 = lines.slice(startLine - 1, endLine).join("\n");
|
|
@@ -7493,7 +7209,7 @@ function policyContext(context, request2, options, extra) {
|
|
|
7493
7209
|
async function registeredFiles(root, limit) {
|
|
7494
7210
|
const paths = [];
|
|
7495
7211
|
const walk = async (directory) => {
|
|
7496
|
-
for (const entry of await (0,
|
|
7212
|
+
for (const entry of await (0, import_promises9.readdir)(directory, { withFileTypes: true })) {
|
|
7497
7213
|
if (entry.isSymbolicLink()) throw new TypeError("workspace contains a symbolic link");
|
|
7498
7214
|
const target = (0, import_path9.resolve)(directory, entry.name);
|
|
7499
7215
|
if (entry.isDirectory()) await walk(target);
|
|
@@ -8066,18 +7782,6 @@ function githubRepositoryName(value2) {
|
|
|
8066
7782
|
}
|
|
8067
7783
|
return `${owner}/${name}`;
|
|
8068
7784
|
}
|
|
8069
|
-
function trustedGitHubInstallUrl(value2) {
|
|
8070
|
-
let url;
|
|
8071
|
-
try {
|
|
8072
|
-
url = new URL(value2);
|
|
8073
|
-
} catch {
|
|
8074
|
-
throw new Error("odla.ai returned an invalid GitHub installation URL");
|
|
8075
|
-
}
|
|
8076
|
-
if (url.protocol !== "https:" || url.hostname !== "github.com" || url.username || url.password) {
|
|
8077
|
-
throw new Error("odla.ai returned an untrusted GitHub installation URL");
|
|
8078
|
-
}
|
|
8079
|
-
return url.toString();
|
|
8080
|
-
}
|
|
8081
7785
|
function hostedPollInterval(value2 = 2e3) {
|
|
8082
7786
|
if (!Number.isSafeInteger(value2) || value2 < 100 || value2 > 3e4) {
|
|
8083
7787
|
throw new Error("poll interval must be 100-30000ms");
|
|
@@ -8113,51 +7817,6 @@ function hostedSecurityCredential(value2) {
|
|
|
8113
7817
|
}
|
|
8114
7818
|
|
|
8115
7819
|
// src/security-hosted-github.ts
|
|
8116
|
-
async function connectGitHubSecuritySource(options) {
|
|
8117
|
-
const out = options.stdout ?? console;
|
|
8118
|
-
const repository = options.repository === void 0 ? void 0 : githubRepositoryName(options.repository);
|
|
8119
|
-
const attempt = await requestHostedSecurityJson(
|
|
8120
|
-
options,
|
|
8121
|
-
"/registry/github/connect",
|
|
8122
|
-
{
|
|
8123
|
-
method: "POST",
|
|
8124
|
-
body: JSON.stringify({
|
|
8125
|
-
appId: hostedIdentifier(options.appId, "appId"),
|
|
8126
|
-
env: hostedIdentifier(options.env, "env"),
|
|
8127
|
-
...repository === void 0 ? {} : { repository }
|
|
8128
|
-
})
|
|
8129
|
-
},
|
|
8130
|
-
"start GitHub connection"
|
|
8131
|
-
);
|
|
8132
|
-
const serverExpiry = Date.parse(attempt?.expiresAt ?? "");
|
|
8133
|
-
if (!attempt?.attemptId || !attempt.installUrl || !Number.isFinite(serverExpiry)) {
|
|
8134
|
-
throw new Error("odla.ai returned an invalid GitHub connection attempt");
|
|
8135
|
-
}
|
|
8136
|
-
const installUrl = trustedGitHubInstallUrl(attempt.installUrl);
|
|
8137
|
-
out.log(`GitHub approval: ${installUrl}`);
|
|
8138
|
-
if (options.open !== false) {
|
|
8139
|
-
await (options.openInstallUrl ?? openUrl)(installUrl);
|
|
8140
|
-
out.log("github: opened installation approval in your browser");
|
|
8141
|
-
}
|
|
8142
|
-
const interval = hostedPollInterval(options.pollIntervalMs);
|
|
8143
|
-
const now = options.now ?? Date.now;
|
|
8144
|
-
const deadline = Math.min(serverExpiry, now() + hostedPollTimeout(options.pollTimeoutMs));
|
|
8145
|
-
const wait2 = options.wait ?? waitForHostedPoll;
|
|
8146
|
-
while (true) {
|
|
8147
|
-
const state2 = await requestHostedSecurityJson(
|
|
8148
|
-
options,
|
|
8149
|
-
`/registry/github/connect/${encodeURIComponent(attempt.attemptId)}`,
|
|
8150
|
-
{},
|
|
8151
|
-
"check GitHub connection"
|
|
8152
|
-
);
|
|
8153
|
-
if (state2.status !== "pending") {
|
|
8154
|
-
if (state2.status === "connected") return state2;
|
|
8155
|
-
throw new Error(`GitHub connection ${state2.status}${state2.failureCode ? `: ${state2.failureCode}` : ""}`);
|
|
8156
|
-
}
|
|
8157
|
-
if (now() >= deadline) throw new Error("GitHub connection approval timed out");
|
|
8158
|
-
await wait2(Math.min(interval, Math.max(0, deadline - now())), options.signal);
|
|
8159
|
-
}
|
|
8160
|
-
}
|
|
8161
7820
|
async function listGitHubSecuritySources(options) {
|
|
8162
7821
|
const appId = hostedIdentifier(options.appId, "appId");
|
|
8163
7822
|
const env = hostedIdentifier(options.env, "env");
|
|
@@ -8169,16 +7828,6 @@ async function listGitHubSecuritySources(options) {
|
|
|
8169
7828
|
);
|
|
8170
7829
|
return Array.isArray(body.sources) ? body.sources : [];
|
|
8171
7830
|
}
|
|
8172
|
-
async function disconnectGitHubSecuritySource(options) {
|
|
8173
|
-
const appId = hostedIdentifier(options.appId, "appId");
|
|
8174
|
-
const sourceId = hostedIdentifier(options.sourceId, "sourceId");
|
|
8175
|
-
await requestHostedSecurityJson(
|
|
8176
|
-
options,
|
|
8177
|
-
`/registry/apps/${encodeURIComponent(appId)}/github/sources/${encodeURIComponent(sourceId)}`,
|
|
8178
|
-
{ method: "DELETE" },
|
|
8179
|
-
"disconnect GitHub security source"
|
|
8180
|
-
);
|
|
8181
|
-
}
|
|
8182
7831
|
function repositoryFromGitRemote(remoteInput) {
|
|
8183
7832
|
const remote = remoteInput.trim();
|
|
8184
7833
|
const scp = /^git@github\.com:([^/\s]+)\/([^/\s]+?)\/?$/.exec(remote);
|
|
@@ -8278,7 +7927,7 @@ function digestText(value2) {
|
|
|
8278
7927
|
// src/code-images.ts
|
|
8279
7928
|
var import_node_child_process6 = require("child_process");
|
|
8280
7929
|
var import_node_crypto4 = require("crypto");
|
|
8281
|
-
var
|
|
7930
|
+
var import_promises10 = require("fs/promises");
|
|
8282
7931
|
var import_node_os3 = require("os");
|
|
8283
7932
|
var import_node_path14 = require("path");
|
|
8284
7933
|
var import_node_url3 = require("url");
|
|
@@ -8360,16 +8009,16 @@ function embeddedPiAssetPath() {
|
|
|
8360
8009
|
return (0, import_node_url3.fileURLToPath)(new URL("./runtime/pi-agent.js", importMetaUrl));
|
|
8361
8010
|
}
|
|
8362
8011
|
async function embeddedPiImageName() {
|
|
8363
|
-
const bundle = await (0,
|
|
8012
|
+
const bundle = await (0, import_promises10.readFile)(embeddedPiAssetPath()).catch(() => {
|
|
8364
8013
|
throw new Error("CLI-embedded Pi runtime is missing; reinstall this exact @odla-ai/cli version");
|
|
8365
8014
|
});
|
|
8366
8015
|
return `odla-ai/pi-agent:embedded-sha256-${(0, import_node_crypto4.createHash)("sha256").update(bundle).digest("hex")}`;
|
|
8367
8016
|
}
|
|
8368
8017
|
async function buildEmbeddedPiImage(engine, image, run) {
|
|
8369
|
-
const context = await (0,
|
|
8018
|
+
const context = await (0, import_promises10.mkdtemp)((0, import_node_path14.join)((0, import_node_os3.tmpdir)(), "odla-code-pi-"));
|
|
8370
8019
|
try {
|
|
8371
|
-
await (0,
|
|
8372
|
-
await (0,
|
|
8020
|
+
await (0, import_promises10.copyFile)(embeddedPiAssetPath(), (0, import_node_path14.join)(context, "pi-agent.js"));
|
|
8021
|
+
await (0, import_promises10.writeFile)((0, import_node_path14.join)(context, "Dockerfile"), [
|
|
8373
8022
|
`FROM ${CODE_NODE_IMAGE}`,
|
|
8374
8023
|
"COPY pi-agent.js /opt/odla/pi-agent.js",
|
|
8375
8024
|
"WORKDIR /workspace",
|
|
@@ -8378,7 +8027,7 @@ async function buildEmbeddedPiImage(engine, image, run) {
|
|
|
8378
8027
|
].join("\n"), { mode: 384 });
|
|
8379
8028
|
await run(engine, ["build", "--tag", image, context], "inherit");
|
|
8380
8029
|
} finally {
|
|
8381
|
-
await (0,
|
|
8030
|
+
await (0, import_promises10.rm)(context, { recursive: true, force: true });
|
|
8382
8031
|
}
|
|
8383
8032
|
}
|
|
8384
8033
|
|
|
@@ -8386,7 +8035,7 @@ async function buildEmbeddedPiImage(engine, image, run) {
|
|
|
8386
8035
|
async function codeConnect(options) {
|
|
8387
8036
|
const cwd = options.cwd ?? process.cwd();
|
|
8388
8037
|
const configPath = (0, import_node_path15.resolve)(cwd, options.configPath);
|
|
8389
|
-
const cfg = (0,
|
|
8038
|
+
const cfg = (0, import_node_fs14.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
|
|
8390
8039
|
const requestedAppId = options.appId?.trim();
|
|
8391
8040
|
if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
|
|
8392
8041
|
throw new Error("--app-id must be a valid odla app id");
|
|
@@ -8777,18 +8426,17 @@ Usage:
|
|
|
8777
8426
|
odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
|
|
8778
8427
|
odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
|
|
8779
8428
|
odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
|
|
8780
|
-
odla-ai calendar disconnect [--env dev] [
|
|
8781
|
-
odla-ai app archive [--config odla.config.mjs] [
|
|
8782
|
-
odla-ai app restore [--config odla.config.mjs]
|
|
8783
|
-
odla-ai app export [--env dev]
|
|
8429
|
+
odla-ai calendar disconnect [--env dev] --yes [continue in Studio; human session required]
|
|
8430
|
+
odla-ai app archive [--config odla.config.mjs] --yes [continue in Studio; human session required]
|
|
8431
|
+
odla-ai app restore [--config odla.config.mjs] [continue in Studio; human session required]
|
|
8432
|
+
odla-ai app export [--env dev] [continue in Studio; human session required]
|
|
8784
8433
|
odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
|
|
8785
|
-
|
|
8786
|
-
odla-ai app
|
|
8787
|
-
odla-ai app
|
|
8788
|
-
odla-ai app
|
|
8789
|
-
odla-ai app
|
|
8790
|
-
odla-ai app owners add
|
|
8791
|
-
odla-ai app owners remove <email> [--email <odla-account>] [--json]
|
|
8434
|
+
[dry-run is local; writes continue in Studio]
|
|
8435
|
+
odla-ai app refresh-sandbox [continue in Studio; human session required]
|
|
8436
|
+
odla-ai app go-live [continue in Studio; human session required]
|
|
8437
|
+
odla-ai app promote [continue in Studio; human session required]
|
|
8438
|
+
odla-ai app rename <name> [continue in Studio; human session required]
|
|
8439
|
+
odla-ai app owners <list|add|remove> [...] [continue in Studio; human session required]
|
|
8792
8440
|
odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
|
|
8793
8441
|
odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
8794
8442
|
odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
@@ -8823,8 +8471,8 @@ Usage:
|
|
|
8823
8471
|
odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
|
|
8824
8472
|
odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
|
|
8825
8473
|
odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
|
|
8826
|
-
odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--
|
|
8827
|
-
odla-ai agent retry <job-id> [--env dev] [--
|
|
8474
|
+
odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--token <ODLA_API_KEY>] [--json]
|
|
8475
|
+
odla-ai agent retry <job-id> [--env dev] [--token <ODLA_API_KEY>] [--json]
|
|
8828
8476
|
odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
|
|
8829
8477
|
odla-ai context list [--json]
|
|
8830
8478
|
odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
|
|
@@ -8860,8 +8508,8 @@ Usage:
|
|
|
8860
8508
|
odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
|
|
8861
8509
|
odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
|
|
8862
8510
|
odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
|
|
8863
|
-
odla-ai security github connect [--repo owner/name] [--env dev]
|
|
8864
|
-
odla-ai security github disconnect --source <id> [--env dev]
|
|
8511
|
+
odla-ai security github connect [--repo owner/name] [--env dev] [continue in Studio; human session required]
|
|
8512
|
+
odla-ai security github disconnect --source <id> [--env dev] [continue in Studio; human session required]
|
|
8865
8513
|
odla-ai security plan [--env dev] [--json]
|
|
8866
8514
|
odla-ai security sources [--env dev] [--json]
|
|
8867
8515
|
odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
|
|
@@ -9372,7 +9020,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
9372
9020
|
throw new WatchRemoteError(cursor, error);
|
|
9373
9021
|
}
|
|
9374
9022
|
if (deadline !== void 0 && now() >= deadline) {
|
|
9375
|
-
const result =
|
|
9023
|
+
const result = report(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
9376
9024
|
throw new WatchTimeoutError(result.cursor);
|
|
9377
9025
|
}
|
|
9378
9026
|
const base = Math.min(intervalMs, 1e3);
|
|
@@ -9413,7 +9061,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
9413
9061
|
});
|
|
9414
9062
|
const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
|
|
9415
9063
|
const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
|
|
9416
|
-
return
|
|
9064
|
+
return report(ctx, parsed, {
|
|
9417
9065
|
found: true,
|
|
9418
9066
|
cursor,
|
|
9419
9067
|
events: matching,
|
|
@@ -9440,13 +9088,13 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
9440
9088
|
}
|
|
9441
9089
|
if (page2.hasMore) continue;
|
|
9442
9090
|
if (deadline !== void 0 && now() >= deadline) {
|
|
9443
|
-
return
|
|
9091
|
+
return report(ctx, parsed, { found: false, cursor });
|
|
9444
9092
|
}
|
|
9445
9093
|
const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
|
|
9446
9094
|
await sleep(wait2);
|
|
9447
9095
|
}
|
|
9448
9096
|
}
|
|
9449
|
-
function
|
|
9097
|
+
function report(ctx, parsed, result) {
|
|
9450
9098
|
if (ctx.json) {
|
|
9451
9099
|
ctx.out.log(JSON.stringify(result, null, 2));
|
|
9452
9100
|
} else if (parsed.options.jsonl !== true && result.found) {
|
|
@@ -9947,7 +9595,7 @@ function eventLabel(event) {
|
|
|
9947
9595
|
const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
|
|
9948
9596
|
return body || event.payload.entityId;
|
|
9949
9597
|
}
|
|
9950
|
-
function
|
|
9598
|
+
function report2(ctx, parsed, result) {
|
|
9951
9599
|
if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
|
|
9952
9600
|
else if (parsed.options.jsonl !== true && result.found) {
|
|
9953
9601
|
for (const event of result.events ?? []) {
|
|
@@ -10007,7 +9655,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
10007
9655
|
});
|
|
10008
9656
|
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
|
|
10009
9657
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10010
|
-
return
|
|
9658
|
+
return report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
10011
9659
|
}
|
|
10012
9660
|
const backoff = Math.min(
|
|
10013
9661
|
MAX_BACKOFF_MS2,
|
|
@@ -10048,7 +9696,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
10048
9696
|
cursor,
|
|
10049
9697
|
serverTime: current.serverTime
|
|
10050
9698
|
});
|
|
10051
|
-
return
|
|
9699
|
+
return report2(ctx, parsed, { found: true, cursor, events: matching });
|
|
10052
9700
|
}
|
|
10053
9701
|
if (current.events.length > 0) {
|
|
10054
9702
|
jsonl2(ctx, parsed, {
|
|
@@ -10067,7 +9715,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
10067
9715
|
}
|
|
10068
9716
|
if (current.hasMore) continue;
|
|
10069
9717
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10070
|
-
return
|
|
9718
|
+
return report2(ctx, parsed, { found: false, cursor });
|
|
10071
9719
|
}
|
|
10072
9720
|
await sleep(
|
|
10073
9721
|
deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
|
|
@@ -11100,7 +10748,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
11100
10748
|
}
|
|
11101
10749
|
|
|
11102
10750
|
// src/record.ts
|
|
11103
|
-
var
|
|
10751
|
+
var import_node_fs15 = require("fs");
|
|
11104
10752
|
var import_node_process12 = __toESM(require("process"), 1);
|
|
11105
10753
|
|
|
11106
10754
|
// src/surface.ts
|
|
@@ -11271,14 +10919,14 @@ function recordInvocation(parsed) {
|
|
|
11271
10919
|
options: Object.entries(parsed.options).map(([name, value2]) => value2 === false ? `no-${name}` : name).sort()
|
|
11272
10920
|
};
|
|
11273
10921
|
if (!entry.path.length) return;
|
|
11274
|
-
(0,
|
|
10922
|
+
(0, import_node_fs15.appendFileSync)(file, `${JSON.stringify(entry)}
|
|
11275
10923
|
`);
|
|
11276
10924
|
} catch {
|
|
11277
10925
|
}
|
|
11278
10926
|
}
|
|
11279
10927
|
|
|
11280
10928
|
// src/runbook-actions.ts
|
|
11281
|
-
var
|
|
10929
|
+
var import_node_fs16 = require("fs");
|
|
11282
10930
|
|
|
11283
10931
|
// src/runbook-requires.ts
|
|
11284
10932
|
var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
|
|
@@ -11363,7 +11011,7 @@ async function bySlug(ctx, slug) {
|
|
|
11363
11011
|
function readBody(file, inline) {
|
|
11364
11012
|
if (inline !== void 0) return inline;
|
|
11365
11013
|
if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
|
|
11366
|
-
return (0,
|
|
11014
|
+
return (0, import_node_fs16.readFileSync)(file === "-" ? 0 : file, "utf8");
|
|
11367
11015
|
}
|
|
11368
11016
|
var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
|
|
11369
11017
|
async function runbookList(ctx, all, query) {
|
|
@@ -11455,7 +11103,7 @@ async function runbookRemove(ctx, slug) {
|
|
|
11455
11103
|
}
|
|
11456
11104
|
|
|
11457
11105
|
// src/runbook-import.ts
|
|
11458
|
-
var
|
|
11106
|
+
var import_node_fs17 = require("fs");
|
|
11459
11107
|
var import_node_path16 = require("path");
|
|
11460
11108
|
function parseRunbook(text2, slug) {
|
|
11461
11109
|
let rest = text2;
|
|
@@ -11481,12 +11129,12 @@ function parseRunbook(text2, slug) {
|
|
|
11481
11129
|
};
|
|
11482
11130
|
}
|
|
11483
11131
|
function readRunbookDir(dir) {
|
|
11484
|
-
if (!(0,
|
|
11485
|
-
const files = (0,
|
|
11132
|
+
if (!(0, import_node_fs17.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
|
|
11133
|
+
const files = (0, import_node_fs17.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
|
|
11486
11134
|
if (!files.length) throw new Error(`no .md files in ${dir}`);
|
|
11487
11135
|
return files.map((file) => {
|
|
11488
11136
|
const slug = (0, import_node_path16.basename)(file, ".md");
|
|
11489
|
-
const parsed = parseRunbook((0,
|
|
11137
|
+
const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0, import_node_path16.join)(dir, file), "utf8"), slug);
|
|
11490
11138
|
return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
|
|
11491
11139
|
});
|
|
11492
11140
|
}
|
|
@@ -11559,7 +11207,7 @@ async function upsert(ctx, r, visibility) {
|
|
|
11559
11207
|
|
|
11560
11208
|
// src/runbook-impact.ts
|
|
11561
11209
|
var import_node_child_process7 = require("child_process");
|
|
11562
|
-
var
|
|
11210
|
+
var import_node_fs18 = require("fs");
|
|
11563
11211
|
var import_node_path17 = require("path");
|
|
11564
11212
|
|
|
11565
11213
|
// src/runbook-impact-scan.ts
|
|
@@ -11730,9 +11378,9 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
|
|
|
11730
11378
|
function manifestLabeller(root) {
|
|
11731
11379
|
return (workspace) => {
|
|
11732
11380
|
const manifest = (0, import_node_path17.join)(root, workspace, "package.json");
|
|
11733
|
-
if (!(0,
|
|
11381
|
+
if (!(0, import_node_fs18.existsSync)(manifest)) return void 0;
|
|
11734
11382
|
try {
|
|
11735
|
-
const name = JSON.parse((0,
|
|
11383
|
+
const name = JSON.parse((0, import_node_fs18.readFileSync)(manifest, "utf8")).name;
|
|
11736
11384
|
return typeof name === "string" ? name : void 0;
|
|
11737
11385
|
} catch {
|
|
11738
11386
|
return void 0;
|
|
@@ -11770,7 +11418,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
|
|
|
11770
11418
|
return out;
|
|
11771
11419
|
}
|
|
11772
11420
|
var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
|
|
11773
|
-
function
|
|
11421
|
+
function report3(ctx, impacts) {
|
|
11774
11422
|
const covered = impacts.filter((i) => i.runbooks.length);
|
|
11775
11423
|
ctx.out.log(
|
|
11776
11424
|
`${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.`
|
|
@@ -11799,7 +11447,7 @@ function report4(ctx, impacts) {
|
|
|
11799
11447
|
async function runbookImpact(ctx, options, deps = {}) {
|
|
11800
11448
|
const cwd = deps.cwd ?? process.cwd();
|
|
11801
11449
|
const runGit = deps.runGit ?? gitRunner(cwd);
|
|
11802
|
-
const read3 = deps.readRepoFile ?? ((path) => (0,
|
|
11450
|
+
const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0, import_node_path17.join)(cwd, path), "utf8"));
|
|
11803
11451
|
const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
|
|
11804
11452
|
if (!surfaces.length) {
|
|
11805
11453
|
return ctx.out.log(
|
|
@@ -11808,7 +11456,7 @@ async function runbookImpact(ctx, options, deps = {}) {
|
|
|
11808
11456
|
}
|
|
11809
11457
|
const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
|
|
11810
11458
|
if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
|
|
11811
|
-
|
|
11459
|
+
report3(ctx, impacts);
|
|
11812
11460
|
}
|
|
11813
11461
|
|
|
11814
11462
|
// src/runbook-lint.ts
|
|
@@ -11932,7 +11580,7 @@ async function runbookComment(ctx, slug, body) {
|
|
|
11932
11580
|
|
|
11933
11581
|
// src/runbook-editor.ts
|
|
11934
11582
|
var import_node_child_process8 = require("child_process");
|
|
11935
|
-
var
|
|
11583
|
+
var import_node_fs19 = require("fs");
|
|
11936
11584
|
var import_node_os5 = require("os");
|
|
11937
11585
|
var import_node_path18 = require("path");
|
|
11938
11586
|
var import_node_process13 = __toESM(require("process"), 1);
|
|
@@ -11960,16 +11608,16 @@ function editText(initial, slug, deps = {}) {
|
|
|
11960
11608
|
);
|
|
11961
11609
|
if (!interactive())
|
|
11962
11610
|
throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
|
|
11963
|
-
const dir = (0,
|
|
11611
|
+
const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path18.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
|
|
11964
11612
|
const file = (0, import_node_path18.join)(dir, `${slug}.md`);
|
|
11965
11613
|
try {
|
|
11966
|
-
(0,
|
|
11614
|
+
(0, import_node_fs19.writeFileSync)(file, initial, { mode: 384 });
|
|
11967
11615
|
const code = defaultRunOrInjected(deps)(editor, file);
|
|
11968
11616
|
if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
|
|
11969
|
-
const edited = (0,
|
|
11617
|
+
const edited = (0, import_node_fs19.readFileSync)(file, "utf8");
|
|
11970
11618
|
return edited === initial ? null : edited;
|
|
11971
11619
|
} finally {
|
|
11972
|
-
(0,
|
|
11620
|
+
(0, import_node_fs19.rmSync)(dir, { recursive: true, force: true });
|
|
11973
11621
|
}
|
|
11974
11622
|
}
|
|
11975
11623
|
var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
|
|
@@ -12308,7 +11956,6 @@ async function runbookCommand(parsed, deps = {}) {
|
|
|
12308
11956
|
}
|
|
12309
11957
|
|
|
12310
11958
|
// src/security-command-context.ts
|
|
12311
|
-
var import_promises12 = require("readline/promises");
|
|
12312
11959
|
async function hostedSecurityContext(parsed, dependencies) {
|
|
12313
11960
|
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
12314
11961
|
const cfg = await loadProjectConfig(configPath);
|
|
@@ -12327,21 +11974,11 @@ async function hostedSecurityContext(parsed, dependencies) {
|
|
|
12327
11974
|
cfg,
|
|
12328
11975
|
{ configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
|
|
12329
11976
|
doFetch,
|
|
12330
|
-
stdout
|
|
11977
|
+
stdout,
|
|
11978
|
+
{ optionalProjectCapabilities: ["app.manage"] }
|
|
12331
11979
|
);
|
|
12332
11980
|
return { platform, token, appId: cfg.app.id, env, fetch: doFetch, stdout };
|
|
12333
11981
|
}
|
|
12334
|
-
async function interactiveConfirmation(message2, dependencies) {
|
|
12335
|
-
if (dependencies.confirm) return dependencies.confirm(message2);
|
|
12336
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
12337
|
-
const prompt = (0, import_promises12.createInterface)({ input: process.stdin, output: process.stdout });
|
|
12338
|
-
try {
|
|
12339
|
-
const answer = await prompt.question(`${message2} [y/N] `);
|
|
12340
|
-
return /^y(?:es)?$/i.test(answer.trim());
|
|
12341
|
-
} finally {
|
|
12342
|
-
prompt.close();
|
|
12343
|
-
}
|
|
12344
|
-
}
|
|
12345
11982
|
function requiredSecurityPositional(parsed, index, label) {
|
|
12346
11983
|
const value2 = parsed.positionals[index];
|
|
12347
11984
|
if (!value2) throw new Error(`${label} is required`);
|
|
@@ -12407,31 +12044,31 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
12407
12044
|
url.searchParams.set("job", job.jobId);
|
|
12408
12045
|
out.log(` Studio: ${url.toString()}`);
|
|
12409
12046
|
}
|
|
12410
|
-
function printHostedReport(out,
|
|
12411
|
-
out.log(`security report ${
|
|
12412
|
-
out.log(` coverage: ${
|
|
12413
|
-
out.log(` findings: confirmed=${
|
|
12414
|
-
out.log(` discovery: ${
|
|
12415
|
-
out.log(` validation: ${
|
|
12416
|
-
for (const finding of
|
|
12047
|
+
function printHostedReport(out, report4) {
|
|
12048
|
+
out.log(`security report ${report4.jobId}: ${report4.repository}@${report4.revision}`);
|
|
12049
|
+
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}`);
|
|
12050
|
+
out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates} rejected=${report4.metrics.rejected}`);
|
|
12051
|
+
out.log(` discovery: ${report4.provenance.discovery?.provider ?? "unknown"}/${report4.provenance.discovery?.model ?? "unknown"}`);
|
|
12052
|
+
out.log(` validation: ${report4.provenance.validation?.provider ?? "unknown"}/${report4.provenance.validation?.model ?? "unknown"} independent=${String(report4.provenance.independentValidation)}`);
|
|
12053
|
+
for (const finding of report4.findings) {
|
|
12417
12054
|
const location = finding.locations[0];
|
|
12418
12055
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
12419
12056
|
}
|
|
12420
|
-
for (const limitation of
|
|
12057
|
+
for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
|
|
12421
12058
|
}
|
|
12422
|
-
function enforceHostedReportGate(
|
|
12059
|
+
function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
|
|
12423
12060
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
12424
12061
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
12425
12062
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
12426
12063
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
12427
|
-
const confirmed =
|
|
12428
|
-
const leads = failOnCandidates ?
|
|
12429
|
-
const incomplete =
|
|
12064
|
+
const confirmed = report4.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
|
|
12065
|
+
const leads = failOnCandidates ? report4.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
|
|
12066
|
+
const incomplete = report4.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
|
|
12430
12067
|
if (confirmed.length || leads.length || incomplete) {
|
|
12431
|
-
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${
|
|
12068
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report4.coverageStatus}` : ""}`);
|
|
12432
12069
|
}
|
|
12433
12070
|
if (emitSuccess) {
|
|
12434
|
-
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${
|
|
12071
|
+
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report4.coverageStatus}. This is not proof that the application is secure.`);
|
|
12435
12072
|
}
|
|
12436
12073
|
}
|
|
12437
12074
|
function printHostedSecurityPlanRoute(out, label, route2) {
|
|
@@ -12514,17 +12151,17 @@ async function runHostedSecurity(options) {
|
|
|
12514
12151
|
allowNetwork: false
|
|
12515
12152
|
}
|
|
12516
12153
|
});
|
|
12517
|
-
const
|
|
12518
|
-
await (0, import_node3.writeSecurityArtifacts)(output,
|
|
12519
|
-
const reportDigest = await (0, import_security.securityFingerprint)(
|
|
12154
|
+
const report4 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
12155
|
+
await (0, import_node3.writeSecurityArtifacts)(output, report4);
|
|
12156
|
+
const reportDigest = await (0, import_security.securityFingerprint)(report4);
|
|
12520
12157
|
await hosted.complete({
|
|
12521
12158
|
reportDigest,
|
|
12522
|
-
coverageStatus:
|
|
12523
|
-
confirmed:
|
|
12524
|
-
candidates:
|
|
12159
|
+
coverageStatus: report4.coverageStatus,
|
|
12160
|
+
confirmed: report4.metrics.confirmed,
|
|
12161
|
+
candidates: report4.metrics.candidates
|
|
12525
12162
|
}, { signal: options.signal });
|
|
12526
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run,
|
|
12527
|
-
return Object.freeze({ report:
|
|
12163
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report4, output);
|
|
12164
|
+
return Object.freeze({ report: report4, run: hosted.run, output });
|
|
12528
12165
|
}
|
|
12529
12166
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
12530
12167
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
@@ -12550,14 +12187,14 @@ function profileFor(name, maxHuntTasks) {
|
|
|
12550
12187
|
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
12551
12188
|
return { ...profile, maxHuntTasks };
|
|
12552
12189
|
}
|
|
12553
|
-
function printSummary(out, appId, env, run,
|
|
12554
|
-
const complete =
|
|
12190
|
+
function printSummary(out, appId, env, run, report4, output) {
|
|
12191
|
+
const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
|
|
12555
12192
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
12556
12193
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
12557
12194
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
12558
|
-
out.log(` coverage: ${
|
|
12559
|
-
if (
|
|
12560
|
-
out.log(` findings: confirmed=${
|
|
12195
|
+
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}`);
|
|
12196
|
+
if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
|
|
12197
|
+
out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
|
|
12561
12198
|
out.log(` report: ${(0, import_node_path19.resolve)(output, "REPORT.md")}`);
|
|
12562
12199
|
}
|
|
12563
12200
|
function formatBudget(usage) {
|
|
@@ -12793,13 +12430,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
12793
12430
|
}
|
|
12794
12431
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
12795
12432
|
}
|
|
12796
|
-
const
|
|
12433
|
+
const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
12797
12434
|
if (parsed.options.json === true) {
|
|
12798
|
-
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report:
|
|
12435
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report4 }, null, 2));
|
|
12799
12436
|
} else {
|
|
12800
|
-
printHostedReport(context.stdout,
|
|
12437
|
+
printHostedReport(context.stdout, report4);
|
|
12801
12438
|
}
|
|
12802
|
-
enforceHostedReportGate(
|
|
12439
|
+
enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
|
|
12803
12440
|
}
|
|
12804
12441
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
12805
12442
|
if (parsed.options.source === true) {
|
|
@@ -12860,19 +12497,20 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
12860
12497
|
cfg,
|
|
12861
12498
|
{ configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
|
|
12862
12499
|
doFetch,
|
|
12863
|
-
out
|
|
12500
|
+
out,
|
|
12501
|
+
{ optionalProjectCapabilities: ["app.manage"] }
|
|
12864
12502
|
);
|
|
12865
12503
|
}
|
|
12866
12504
|
});
|
|
12867
12505
|
enforceLocalGate(result.report, parsed);
|
|
12868
12506
|
}
|
|
12869
|
-
function enforceLocalGate(
|
|
12507
|
+
function enforceLocalGate(report4, parsed) {
|
|
12870
12508
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
12871
12509
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
12872
12510
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
12873
|
-
const confirmed = (0, import_security2.findingsAtOrAbove)(
|
|
12874
|
-
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(
|
|
12875
|
-
const incomplete =
|
|
12511
|
+
const confirmed = (0, import_security2.findingsAtOrAbove)(report4, failOn);
|
|
12512
|
+
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report4, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
12513
|
+
const incomplete = report4.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
12876
12514
|
if (confirmed.length || leads.length || incomplete) {
|
|
12877
12515
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
12878
12516
|
}
|
|
@@ -12911,9 +12549,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
12911
12549
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
12912
12550
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
12913
12551
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
12914
|
-
const
|
|
12915
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
12916
|
-
else printHostedReport(context.stdout,
|
|
12552
|
+
const report4 = await getHostedSecurityReport({ ...context, jobId });
|
|
12553
|
+
if (parsed.options.json === true) context.stdout.log(JSON.stringify(report4, null, 2));
|
|
12554
|
+
else printHostedReport(context.stdout, report4);
|
|
12917
12555
|
return;
|
|
12918
12556
|
}
|
|
12919
12557
|
if (sub !== "run") {
|
|
@@ -12927,35 +12565,24 @@ async function githubSecurityCommand(parsed, dependencies) {
|
|
|
12927
12565
|
const action2 = parsed.positionals[2];
|
|
12928
12566
|
if (action2 === "disconnect") {
|
|
12929
12567
|
assertArgs(parsed, ["config", "env", "platform", "source", "email", "open", "yes"], 3);
|
|
12930
|
-
const context2 = await hostedSecurityContext(parsed, dependencies);
|
|
12931
12568
|
const sourceId = requiredString(parsed.options.source, "--source");
|
|
12932
|
-
|
|
12933
|
-
|
|
12934
|
-
|
|
12569
|
+
return requireStudioHuman(
|
|
12570
|
+
stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
12571
|
+
`disconnecting GitHub security source ${sourceId}`,
|
|
12572
|
+
"security",
|
|
12573
|
+
stringOpt(parsed.options.env)
|
|
12935
12574
|
);
|
|
12936
|
-
if (!confirmed) {
|
|
12937
|
-
throw new Error("GitHub source disconnect cancelled; pass --yes in a non-interactive shell");
|
|
12938
|
-
}
|
|
12939
|
-
await disconnectGitHubSecuritySource({ ...context2, sourceId });
|
|
12940
|
-
context2.stdout.log(`github: disconnected ${sourceId} from ${context2.appId}/${context2.env}`);
|
|
12941
|
-
return;
|
|
12942
12575
|
}
|
|
12943
12576
|
if (action2 !== "connect") {
|
|
12944
12577
|
throw new Error('unknown security github command. Try "odla-ai security github connect".');
|
|
12945
12578
|
}
|
|
12946
12579
|
assertArgs(parsed, ["config", "env", "platform", "repo", "email", "open"], 3);
|
|
12947
|
-
|
|
12948
|
-
|
|
12949
|
-
|
|
12950
|
-
|
|
12951
|
-
|
|
12952
|
-
|
|
12953
|
-
openInstallUrl: dependencies.openUrl ?? openUrl,
|
|
12954
|
-
wait: dependencies.pollWait,
|
|
12955
|
-
stdout: context.stdout
|
|
12956
|
-
});
|
|
12957
|
-
context.stdout.log(`github: connected ${connection.repository ?? repository ?? "app repository"} (${connection.sourceId ?? "source pending"})`);
|
|
12958
|
-
context.stdout.log("github: odla.ai stores the installation; no PAT or GitHub token is written locally");
|
|
12580
|
+
await requireStudioHuman(
|
|
12581
|
+
stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
12582
|
+
"connecting the GitHub security repository",
|
|
12583
|
+
"security",
|
|
12584
|
+
stringOpt(parsed.options.env)
|
|
12585
|
+
);
|
|
12959
12586
|
}
|
|
12960
12587
|
async function listSecuritySources(parsed, dependencies) {
|
|
12961
12588
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 2);
|