@odla-ai/cli 0.27.11 → 0.27.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/dist/bin.cjs +272 -630
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-Y7WFLSTN.js → chunk-3YSCRXPF.js} +157 -449
- package/dist/chunk-3YSCRXPF.js.map +1 -0
- package/dist/index.cjs +272 -563
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/skills/odla/references/build.md +8 -0
- package/dist/chunk-Y7WFLSTN.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -386,23 +386,30 @@ function handshakeWaitMs(waitSeconds, interactive = import_node_process3.default
|
|
|
386
386
|
|
|
387
387
|
// src/token.ts
|
|
388
388
|
async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {}) {
|
|
389
|
-
if (options.token) return options.token;
|
|
390
389
|
const audience = platformAudience(cfg.platformUrl);
|
|
391
|
-
if (import_node_process4.default.env.ODLA_DEV_TOKEN) {
|
|
392
|
-
const declared = import_node_process4.default.env.ODLA_DEV_TOKEN_AUDIENCE;
|
|
393
|
-
if (declared) {
|
|
394
|
-
if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
|
|
395
|
-
} else if (audience !== "https://odla.ai") {
|
|
396
|
-
throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
|
|
397
|
-
}
|
|
398
|
-
return import_node_process4.default.env.ODLA_DEV_TOKEN;
|
|
399
|
-
}
|
|
400
390
|
const optionalProjectCapabilities = grantRequest.optionalProjectCapabilities ?? [];
|
|
401
391
|
const grantIntent = { projectIds: [cfg.app.id], optionalProjectCapabilities };
|
|
402
392
|
const cached = readJsonFile(cfg.local.tokenFile);
|
|
403
|
-
if (
|
|
404
|
-
|
|
405
|
-
|
|
393
|
+
if (!grantRequest.forceReview) {
|
|
394
|
+
if (options.token) return options.token;
|
|
395
|
+
if (import_node_process4.default.env.ODLA_DEV_TOKEN) {
|
|
396
|
+
const declared = import_node_process4.default.env.ODLA_DEV_TOKEN_AUDIENCE;
|
|
397
|
+
if (declared) {
|
|
398
|
+
if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
|
|
399
|
+
} else if (audience !== "https://odla.ai") {
|
|
400
|
+
throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
|
|
401
|
+
}
|
|
402
|
+
return import_node_process4.default.env.ODLA_DEV_TOKEN;
|
|
403
|
+
}
|
|
404
|
+
if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4 && cachedGrantCovers(cached, grantIntent)) {
|
|
405
|
+
out.error(`auth: using cached developer token (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
406
|
+
return cached.token;
|
|
407
|
+
}
|
|
408
|
+
} else {
|
|
409
|
+
if (options.token) {
|
|
410
|
+
throw new Error("--request-grant cannot be combined with --token; remove --token so the approved replacement credential can be collected and cached");
|
|
411
|
+
}
|
|
412
|
+
out.error(`auth: requesting fresh owner review for app.manage on exact project "${cfg.app.id}"`);
|
|
406
413
|
}
|
|
407
414
|
const ctx = {
|
|
408
415
|
cfg,
|
|
@@ -1736,7 +1743,7 @@ async function agentCommand(parsed, deps = {}) {
|
|
|
1736
1743
|
if (action2 !== "jobs" && action2 !== "retry") {
|
|
1737
1744
|
throw new Error(`unknown agent action "${action2 ?? ""}". Try "odla-ai agent jobs --json".`);
|
|
1738
1745
|
}
|
|
1739
|
-
assertArgs(parsed, ["config", "env", "state", "limit", "json", "token"
|
|
1746
|
+
assertArgs(parsed, ["config", "env", "state", "limit", "json", "token"], action2 === "jobs" ? 2 : 3);
|
|
1740
1747
|
if (action2 === "retry" && (parsed.options.state !== void 0 || parsed.options.limit !== void 0)) {
|
|
1741
1748
|
throw new Error('--state and --limit are supported only by "agent jobs"');
|
|
1742
1749
|
}
|
|
@@ -1744,17 +1751,17 @@ async function agentCommand(parsed, deps = {}) {
|
|
|
1744
1751
|
const { env, tenant } = resolveTenant(cfg, stringOpt(parsed.options.env));
|
|
1745
1752
|
const doFetch = deps.fetch ?? fetch;
|
|
1746
1753
|
const out = deps.stdout ?? console;
|
|
1747
|
-
const credential2 =
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1754
|
+
const credential2 = stringOpt(parsed.options.token) ?? readCredentials(cfg.local.credentialsFile)?.envs[env]?.dbKey;
|
|
1755
|
+
if (!credential2) {
|
|
1756
|
+
throw new Error(
|
|
1757
|
+
`no ${env} app credential found; run \`odla-ai provision --write-dev-vars --yes\` or pass --token <ODLA_API_KEY>`
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
if (credential2.startsWith("odla_dev_")) {
|
|
1761
|
+
throw new Error(
|
|
1762
|
+
"agent job administration requires an app credential (ODLA_API_KEY / odla_sk_\u2026), not a developer device token"
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1758
1765
|
const base = `${cfg.dbEndpoint}/app/${encodeURIComponent(tenant)}/admin/agent-jobs`;
|
|
1759
1766
|
const headers = { authorization: `Bearer ${credential2}` };
|
|
1760
1767
|
if (action2 === "retry") {
|
|
@@ -1810,51 +1817,23 @@ function errorMessage(body) {
|
|
|
1810
1817
|
return "request failed";
|
|
1811
1818
|
}
|
|
1812
1819
|
|
|
1820
|
+
// src/human-session.ts
|
|
1821
|
+
async function requireStudioHuman(configPath, action2, destination = "app", env) {
|
|
1822
|
+
const cfg = await loadProjectConfig(configPath);
|
|
1823
|
+
const appEnv = env && cfg.envs.includes(env) ? env : cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
|
|
1824
|
+
const path = destination === "app" ? `/studio/apps/${encodeURIComponent(cfg.app.id)}/${encodeURIComponent(appEnv)}/settings/app` : `/studio/apps/${encodeURIComponent(cfg.app.id)}/${encodeURIComponent(appEnv)}/${destination}`;
|
|
1825
|
+
throw new Error(
|
|
1826
|
+
`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}`
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1813
1830
|
// src/app-export.ts
|
|
1814
|
-
var import_node_fs7 = require("fs");
|
|
1815
|
-
var import_node_stream = require("stream");
|
|
1816
|
-
var import_promises = require("stream/promises");
|
|
1817
1831
|
async function appExport(options) {
|
|
1818
|
-
|
|
1819
|
-
const out = options.stdout ?? console;
|
|
1820
|
-
const doFetch = options.fetch ?? fetch;
|
|
1821
|
-
const { tenant } = resolveTenant(cfg, options.env);
|
|
1822
|
-
const token = await getDeveloperToken(
|
|
1823
|
-
cfg,
|
|
1824
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
1825
|
-
doFetch,
|
|
1826
|
-
out
|
|
1827
|
-
);
|
|
1828
|
-
const auth = { authorization: `Bearer ${token}` };
|
|
1829
|
-
const base = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}`;
|
|
1830
|
-
if (options.fresh) {
|
|
1831
|
-
const res = await doFetch(`${base}/export`, { method: "POST", headers: auth });
|
|
1832
|
-
const body = await res.json().catch(() => ({}));
|
|
1833
|
-
if (!res.ok) throw new Error(`export failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? res.status}`);
|
|
1834
|
-
}
|
|
1835
|
-
const list = await doFetch(`${base}/backups`, { headers: auth });
|
|
1836
|
-
if (!list.ok) throw new Error(`couldn't list backups (${list.status})`);
|
|
1837
|
-
const { backups } = await list.json();
|
|
1838
|
-
const newest = backups[0];
|
|
1839
|
-
if (!newest) {
|
|
1840
|
-
throw new Error(
|
|
1841
|
-
`${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)`
|
|
1842
|
-
);
|
|
1843
|
-
}
|
|
1844
|
-
const download = await doFetch(`${base}/backups/${newest.id}/download`, { headers: auth });
|
|
1845
|
-
if (!download.ok || !download.body) throw new Error(`download failed (${download.status})`);
|
|
1846
|
-
const file = options.out ?? `${tenant}-${new Date(newest.created_at).toISOString().slice(0, 10)}-tx${newest.max_tx}.jsonl.gz`;
|
|
1847
|
-
await (0, import_promises.pipeline)(import_node_stream.Readable.fromWeb(download.body), (0, import_node_fs7.createWriteStream)(file));
|
|
1848
|
-
if (options.json) out.log(JSON.stringify({ file, backup: newest }, null, 2));
|
|
1849
|
-
else {
|
|
1850
|
-
out.log(`${tenant}: wrote ${file} (${newest.bytes} bytes, ${newest.kind} snapshot at tx ${newest.max_tx})`);
|
|
1851
|
-
out.log(`sha256 ${download.headers.get("x-odla-sha256") ?? newest.sha256}`);
|
|
1852
|
-
}
|
|
1853
|
-
return { file, backup: newest };
|
|
1832
|
+
return requireStudioHuman(options.configPath, "database export", "database", options.env);
|
|
1854
1833
|
}
|
|
1855
1834
|
|
|
1856
1835
|
// src/app-import.ts
|
|
1857
|
-
var
|
|
1836
|
+
var import_node_fs7 = require("fs");
|
|
1858
1837
|
var import_import = require("@odla-ai/db/import");
|
|
1859
1838
|
function chooseIdMode(options, rows) {
|
|
1860
1839
|
const chosen = [options.idField && "field", options.key && "key", options.generateIds && "generate"].filter(Boolean);
|
|
@@ -1871,9 +1850,8 @@ async function appImport(options) {
|
|
|
1871
1850
|
const cfg = await loadProjectConfig(options.configPath);
|
|
1872
1851
|
const out = options.stdout ?? console;
|
|
1873
1852
|
const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
|
|
1874
|
-
const doFetch = options.fetch ?? fetch;
|
|
1875
1853
|
const { tenant } = resolveTenant(cfg, options.env);
|
|
1876
|
-
const text2 = options.file === "-" ? (options.readStdin ?? (() => (0,
|
|
1854
|
+
const text2 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs7.readFileSync)(0, "utf8")))() : (0, import_node_fs7.readFileSync)(options.file, "utf8");
|
|
1877
1855
|
const { format, sources } = (0, import_import.parseImport)(text2, options.ns);
|
|
1878
1856
|
if (format === "namespace-map" && options.ns) {
|
|
1879
1857
|
throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
|
|
@@ -1899,100 +1877,18 @@ ${detail}${more}`);
|
|
|
1899
1877
|
if (options.json) out.log(JSON.stringify(result, null, 2));
|
|
1900
1878
|
return result;
|
|
1901
1879
|
}
|
|
1902
|
-
|
|
1903
|
-
cfg,
|
|
1904
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
1905
|
-
doFetch,
|
|
1906
|
-
out
|
|
1907
|
-
);
|
|
1908
|
-
const runId = crypto.randomUUID();
|
|
1909
|
-
const url = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}/transact`;
|
|
1910
|
-
for (const chunk of chunks) {
|
|
1911
|
-
const res = await doFetch(url, {
|
|
1912
|
-
method: "POST",
|
|
1913
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
1914
|
-
body: JSON.stringify({ mutationId: (0, import_import.importMutationId)(runId, chunk.index), ops: chunk.ops })
|
|
1915
|
-
});
|
|
1916
|
-
const body = await res.json().catch(() => ({}));
|
|
1917
|
-
if (!res.ok) {
|
|
1918
|
-
const code = body.error?.code ? ` (${body.error.code})` : "";
|
|
1919
|
-
throw new Error(
|
|
1920
|
-
`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.`
|
|
1921
|
-
);
|
|
1922
|
-
}
|
|
1923
|
-
result.committed += chunk.ops.length;
|
|
1924
|
-
if (typeof body.txId === "number") result.txIds.push(body.txId);
|
|
1925
|
-
if (body.duplicate) result.duplicate++;
|
|
1926
|
-
}
|
|
1927
|
-
if (options.json) out.log(JSON.stringify(result, null, 2));
|
|
1928
|
-
else {
|
|
1929
|
-
const dup = result.duplicate > 0 ? ` (${result.duplicate} chunk(s) were already applied)` : "";
|
|
1930
|
-
out.log(`${tenant}: upserted ${result.committed} row(s) in ${chunks.length} transaction(s)${dup}`);
|
|
1931
|
-
}
|
|
1932
|
-
return result;
|
|
1880
|
+
return requireStudioHuman(options.configPath, "database import", "database", options.env);
|
|
1933
1881
|
}
|
|
1934
1882
|
|
|
1935
1883
|
// src/app-owners.ts
|
|
1936
|
-
var sink = (options) => options.stdout ?? console;
|
|
1937
|
-
async function ownersRequest(method, suffix, options, body) {
|
|
1938
|
-
const cfg = await loadProjectConfig(options.configPath);
|
|
1939
|
-
const doFetch = options.fetch ?? fetch;
|
|
1940
|
-
const token = await getDeveloperToken(
|
|
1941
|
-
cfg,
|
|
1942
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
1943
|
-
doFetch,
|
|
1944
|
-
sink(options)
|
|
1945
|
-
);
|
|
1946
|
-
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/owners${suffix}`, {
|
|
1947
|
-
method,
|
|
1948
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
1949
|
-
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
1950
|
-
});
|
|
1951
|
-
const data = await res.json().catch(() => ({}));
|
|
1952
|
-
if (!res.ok) {
|
|
1953
|
-
throw new Error(
|
|
1954
|
-
`owners ${method} failed${data.error?.code ? ` (${data.error.code})` : ""}: ` + (data.error?.message ?? `registry returned ${res.status}`)
|
|
1955
|
-
);
|
|
1956
|
-
}
|
|
1957
|
-
return data.owners ?? [];
|
|
1958
|
-
}
|
|
1959
|
-
function report(options, owners, headline) {
|
|
1960
|
-
const out = sink(options);
|
|
1961
|
-
if (options.json === true) {
|
|
1962
|
-
out.log(JSON.stringify(owners, null, 2));
|
|
1963
|
-
return;
|
|
1964
|
-
}
|
|
1965
|
-
if (headline) out.log(headline);
|
|
1966
|
-
out.log(`owners (${owners.length}):`);
|
|
1967
|
-
for (const o of owners) {
|
|
1968
|
-
const name = o.email?.trim() || "Unnamed member";
|
|
1969
|
-
out.log(
|
|
1970
|
-
` ${o.primary ? "\u2605" : "\xB7"} ${name} [${o.ownerId}]${o.primary ? " (primary)" : ""}`
|
|
1971
|
-
);
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
1884
|
async function ownersList(options) {
|
|
1975
|
-
|
|
1885
|
+
await requireStudioHuman(options.configPath, "listing app owners", "app");
|
|
1976
1886
|
}
|
|
1977
1887
|
async function ownersAdd(email, options) {
|
|
1978
|
-
|
|
1979
|
-
report(
|
|
1980
|
-
options,
|
|
1981
|
-
owners,
|
|
1982
|
-
`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).`
|
|
1983
|
-
);
|
|
1888
|
+
await requireStudioHuman(options.configPath, `adding ${email} as an app owner`, "app");
|
|
1984
1889
|
}
|
|
1985
1890
|
async function ownersRemove(target, options) {
|
|
1986
|
-
|
|
1987
|
-
if (target.includes("@")) {
|
|
1988
|
-
const owners2 = await ownersRequest("GET", "", options);
|
|
1989
|
-
const match = owners2.find((o) => o.email?.toLowerCase() === target.toLowerCase());
|
|
1990
|
-
if (!match) throw new Error(`no co-owner with email ${target}`);
|
|
1991
|
-
if (match.primary) throw new Error("can't remove the primary owner");
|
|
1992
|
-
ownerId = match.ownerId;
|
|
1993
|
-
}
|
|
1994
|
-
const owners = await ownersRequest("DELETE", `/${encodeURIComponent(ownerId)}`, options);
|
|
1995
|
-
report(options, owners, `removed ${target}`);
|
|
1891
|
+
await requireStudioHuman(options.configPath, `removing ${target} as an app owner`, "app");
|
|
1996
1892
|
}
|
|
1997
1893
|
async function appOwnersCommand(parsed, dependencies = {}) {
|
|
1998
1894
|
const sub = parsed.positionals[2] ?? "list";
|
|
@@ -2024,35 +1920,9 @@ async function appOwnersCommand(parsed, dependencies = {}) {
|
|
|
2024
1920
|
|
|
2025
1921
|
// src/app-rename.ts
|
|
2026
1922
|
async function appRename(name, options) {
|
|
2027
|
-
const out = options.stdout ?? console;
|
|
2028
1923
|
const trimmed = name.trim();
|
|
2029
1924
|
if (!trimmed) throw new Error('"app rename" needs a name \u2014 try `odla-ai app rename "Acme Storefront"`.');
|
|
2030
|
-
|
|
2031
|
-
const doFetch = options.fetch ?? fetch;
|
|
2032
|
-
const token = await getDeveloperToken(
|
|
2033
|
-
cfg,
|
|
2034
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
2035
|
-
doFetch,
|
|
2036
|
-
out
|
|
2037
|
-
);
|
|
2038
|
-
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/name`, {
|
|
2039
|
-
method: "PUT",
|
|
2040
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
2041
|
-
body: JSON.stringify({ name: trimmed })
|
|
2042
|
-
});
|
|
2043
|
-
const data = await res.json().catch(() => ({}));
|
|
2044
|
-
if (!res.ok || !data.app) {
|
|
2045
|
-
throw new Error(
|
|
2046
|
-
`rename failed${data.error?.code ? ` (${data.error.code})` : ""}: ` + (data.error?.message ?? `registry returned ${res.status}`)
|
|
2047
|
-
);
|
|
2048
|
-
}
|
|
2049
|
-
if (options.json === true) {
|
|
2050
|
-
out.log(JSON.stringify(data.app, null, 2));
|
|
2051
|
-
return;
|
|
2052
|
-
}
|
|
2053
|
-
out.log(`renamed ${data.app.appId} \u2192 "${data.app.name}"`);
|
|
2054
|
-
out.log("The app id is unchanged, so credentials, tenants, and URLs keep working.");
|
|
2055
|
-
out.log(`Update the "name" in ${cfg.configPath} to match.`);
|
|
1925
|
+
await requireStudioHuman(options.configPath, `renaming the app to "${trimmed}"`, "app");
|
|
2056
1926
|
}
|
|
2057
1927
|
async function appRenameCommand(parsed, dependencies = {}) {
|
|
2058
1928
|
assertArgs(parsed, ["config", "token", "email", "json"], parsed.positionals.length);
|
|
@@ -2068,172 +1938,23 @@ async function appRenameCommand(parsed, dependencies = {}) {
|
|
|
2068
1938
|
}
|
|
2069
1939
|
|
|
2070
1940
|
// src/app-transfer.ts
|
|
2071
|
-
function endpointsFor(verb, tenants) {
|
|
2072
|
-
const up = { source: tenants.sandbox, target: tenants.live, from: "dev" };
|
|
2073
|
-
const down = { source: tenants.live, target: tenants.sandbox, from: "prod" };
|
|
2074
|
-
if (verb === "refresh-sandbox") return { ...down, mode: "refresh" };
|
|
2075
|
-
return { ...up, mode: "cutover" };
|
|
2076
|
-
}
|
|
2077
|
-
async function api(cfg, token, path, init, doFetch) {
|
|
2078
|
-
const res = await doFetch(`${cfg.dbEndpoint}${path}`, {
|
|
2079
|
-
...init,
|
|
2080
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json", ...init?.headers }
|
|
2081
|
-
});
|
|
2082
|
-
return { status: res.status, body: await res.json().catch(() => ({})) };
|
|
2083
|
-
}
|
|
2084
|
-
function describe(side) {
|
|
2085
|
-
if (!side.exists) return "not provisioned";
|
|
2086
|
-
if (side.maxTx === 0) return "empty \u2014 never written";
|
|
2087
|
-
const ns = side.namespaces.length === 1 ? "1 namespace" : `${side.namespaces.length} namespaces`;
|
|
2088
|
-
const identity = side.identityRows > 0 ? `, ${side.identityRows} identity row(s)` : "";
|
|
2089
|
-
return `${ns} \xB7 ${side.triples} value(s) \xB7 tx ${side.maxTx}${identity}`;
|
|
2090
|
-
}
|
|
2091
|
-
function printPlan(out, verb, pre, opts) {
|
|
2092
|
-
const arrow = verb === "refresh-sandbox" ? "live \u2192 sandbox" : "sandbox \u2192 live";
|
|
2093
|
-
out.log(`${verb} (${arrow})`);
|
|
2094
|
-
out.log(` from ${pre.source.tenant} ${describe(pre.source)}`);
|
|
2095
|
-
out.log(` to ${pre.target.tenant} ${describe(pre.target)}`);
|
|
2096
|
-
if (verb === "promote") {
|
|
2097
|
-
out.log(" moves schema, rules and gates only \u2014 no rows are copied or removed");
|
|
2098
|
-
} else {
|
|
2099
|
-
out.log(` ${pre.target.tenant} is REPLACED by ${pre.source.tenant}`);
|
|
2100
|
-
out.log(` stays put: ${pre.staysPut.join(", ")}`);
|
|
2101
|
-
const identity = opts.includeIdentity ? "INCLUDED (--include-identity)" : `left behind: ${pre.excluded.join(", ")}`;
|
|
2102
|
-
out.log(` identity: ${identity}`);
|
|
2103
|
-
out.log(` files: ${opts.includeFiles ? "copied (--include-files)" : "not copied"}`);
|
|
2104
|
-
}
|
|
2105
|
-
for (const blocker of pre.blockers) out.log(` \u2716 ${blocker.code}: ${blocker.message}`);
|
|
2106
|
-
}
|
|
2107
1941
|
async function appTransfer(options) {
|
|
2108
1942
|
const cfg = await loadProjectConfig(options.configPath);
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
const doFetch = options.fetch ?? fetch;
|
|
2112
|
-
const tenants = bothTenants(cfg);
|
|
2113
|
-
const route2 = endpointsFor(options.verb, tenants);
|
|
2114
|
-
const token = await getDeveloperToken(
|
|
2115
|
-
cfg,
|
|
2116
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
2117
|
-
doFetch,
|
|
2118
|
-
out
|
|
2119
|
-
);
|
|
2120
|
-
const pre = await api(
|
|
2121
|
-
cfg,
|
|
2122
|
-
token,
|
|
2123
|
-
`/admin/apps/${encodeURIComponent(route2.target)}/copy-preflight?from=${route2.from}`,
|
|
2124
|
-
void 0,
|
|
2125
|
-
doFetch
|
|
2126
|
-
);
|
|
2127
|
-
if (pre.status !== 200) {
|
|
2128
|
-
throw new Error(`pre-flight failed${pre.body.error?.code ? ` (${pre.body.error.code})` : ""}: ${pre.body.error?.message ?? pre.status}`);
|
|
2129
|
-
}
|
|
2130
|
-
const plan = pre.body;
|
|
2131
|
-
printPlan({ log: say }, options.verb, plan, options);
|
|
2132
|
-
if (options.verb === "go-live" && !plan.targetEmpty) {
|
|
2133
|
-
throw new Error(
|
|
2134
|
-
`${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.`
|
|
2135
|
-
);
|
|
2136
|
-
}
|
|
2137
|
-
if (plan.blockers.length > 0) throw new Error(`cannot ${options.verb}: ${plan.blockers.map((b) => b.message).join("; ")}`);
|
|
2138
|
-
if (options.dryRun || options.yes !== true) {
|
|
2139
|
-
say(`nothing written (${options.dryRun ? "--dry-run" : "no --yes"})`);
|
|
2140
|
-
if (options.json) out.log(JSON.stringify(plan, null, 2));
|
|
2141
|
-
return { ok: true, plan };
|
|
2142
|
-
}
|
|
2143
|
-
if (options.verb === "promote") {
|
|
2144
|
-
const res2 = await api(
|
|
2145
|
-
cfg,
|
|
2146
|
-
token,
|
|
2147
|
-
`/admin/apps/${encodeURIComponent(route2.target)}/promote-definitions`,
|
|
2148
|
-
{ method: "POST", body: JSON.stringify({ from: "dev" }) },
|
|
2149
|
-
doFetch
|
|
2150
|
-
);
|
|
2151
|
-
if (res2.status !== 200) throw new Error(`promote failed${res2.body.error?.code ? ` (${res2.body.error.code})` : ""}: ${res2.body.error?.message ?? res2.status}`);
|
|
2152
|
-
if (options.json) out.log(JSON.stringify(res2.body, null, 2));
|
|
2153
|
-
else say(`${route2.target}: promoted ${(res2.body.applied ?? []).join(", ")}`);
|
|
2154
|
-
return { ok: true, plan, result: res2.body };
|
|
2155
|
-
}
|
|
2156
|
-
const res = await api(
|
|
2157
|
-
cfg,
|
|
2158
|
-
token,
|
|
2159
|
-
`/admin/apps/${encodeURIComponent(route2.target)}/copy-db`,
|
|
2160
|
-
{
|
|
2161
|
-
method: "POST",
|
|
2162
|
-
body: JSON.stringify({
|
|
2163
|
-
from: route2.from,
|
|
2164
|
-
mode: route2.mode,
|
|
2165
|
-
...options.includeIdentity ? { includeUsers: true } : {},
|
|
2166
|
-
...options.includeFiles ? { includeFiles: true } : {}
|
|
2167
|
-
})
|
|
2168
|
-
},
|
|
2169
|
-
doFetch
|
|
2170
|
-
);
|
|
2171
|
-
if (res.status !== 200) throw new Error(`${options.verb} failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
|
|
2172
|
-
say(`${route2.target}: replaced from ${route2.source} (tx ${res.body.destination?.maxTx}, epoch ${res.body.destination?.epoch}) \u2014 connected clients resync automatically`);
|
|
2173
|
-
if (options.includeFiles) await copyFiles(cfg, token, route2, doFetch, out);
|
|
2174
|
-
if (options.json) out.log(JSON.stringify(res.body, null, 2));
|
|
2175
|
-
return { ok: true, plan, result: res.body };
|
|
2176
|
-
}
|
|
2177
|
-
async function copyFiles(cfg, token, route2, doFetch, out) {
|
|
2178
|
-
for (let attempt = 1; attempt <= 20; attempt++) {
|
|
2179
|
-
const res = await api(
|
|
2180
|
-
cfg,
|
|
2181
|
-
token,
|
|
2182
|
-
`/admin/apps/${encodeURIComponent(route2.target)}/copy-files`,
|
|
2183
|
-
{ method: "POST", body: JSON.stringify({ from: route2.from }) },
|
|
2184
|
-
doFetch
|
|
2185
|
-
);
|
|
2186
|
-
if (res.status === 200) {
|
|
2187
|
-
out.log(`${route2.target}: files copied`);
|
|
2188
|
-
return;
|
|
2189
|
-
}
|
|
2190
|
-
if (!res.body.error?.retry) {
|
|
2191
|
-
throw new Error(`file copy failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
|
|
2192
|
-
}
|
|
2193
|
-
out.log(` files: bounded at attempt ${attempt}, resuming\u2026`);
|
|
2194
|
-
}
|
|
2195
|
-
throw new Error("file copy did not finish within 20 rounds \u2014 re-run to continue where it left off");
|
|
1943
|
+
bothTenants(cfg);
|
|
1944
|
+
return requireStudioHuman(options.configPath, `app ${options.verb}`, "database");
|
|
2196
1945
|
}
|
|
2197
1946
|
|
|
2198
1947
|
// src/app-lifecycle.ts
|
|
2199
|
-
async function lifecycleCall(action2, options) {
|
|
2200
|
-
const cfg = await loadProjectConfig(options.configPath);
|
|
2201
|
-
const out = options.stdout ?? console;
|
|
2202
|
-
const doFetch = options.fetch ?? fetch;
|
|
2203
|
-
const token = await getDeveloperToken(
|
|
2204
|
-
cfg,
|
|
2205
|
-
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
2206
|
-
doFetch,
|
|
2207
|
-
out
|
|
2208
|
-
);
|
|
2209
|
-
const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/${action2}`, {
|
|
2210
|
-
method: "POST",
|
|
2211
|
-
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }
|
|
2212
|
-
});
|
|
2213
|
-
const body = await res.json().catch(() => ({}));
|
|
2214
|
-
if (!res.ok || !body.ok) {
|
|
2215
|
-
throw new Error(`${action2} failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? `registry returned ${res.status}`}`);
|
|
2216
|
-
}
|
|
2217
|
-
return body;
|
|
2218
|
-
}
|
|
2219
1948
|
async function appArchive(options) {
|
|
2220
1949
|
if (options.yes !== true) {
|
|
2221
1950
|
throw new Error(
|
|
2222
1951
|
"app archive suspends EVERY environment: API keys stop working and all services refuse requests until restored. All data is retained. Pass --yes to proceed."
|
|
2223
1952
|
);
|
|
2224
1953
|
}
|
|
2225
|
-
|
|
2226
|
-
const body = await lifecycleCall("archive", options);
|
|
2227
|
-
if (options.json) out.log(JSON.stringify(body, null, 2));
|
|
2228
|
-
else if (body.operation?.state === "noop") out.log(`${body.app?.appId}: already archived`);
|
|
2229
|
-
else out.log(`${body.app?.appId}: archived \u2014 data retained; run \`odla-ai app restore\` (or use Studio) to bring it back`);
|
|
1954
|
+
await requireStudioHuman(options.configPath, "app archive", "app");
|
|
2230
1955
|
}
|
|
2231
1956
|
async function appRestore(options) {
|
|
2232
|
-
|
|
2233
|
-
const body = await lifecycleCall("restore", options);
|
|
2234
|
-
if (options.json) out.log(JSON.stringify(body, null, 2));
|
|
2235
|
-
else if (body.operation?.state === "noop") out.log(`${body.app?.appId}: already active`);
|
|
2236
|
-
else out.log(`${body.app?.appId}: restored \u2014 every service's data plane is live again`);
|
|
1957
|
+
await requireStudioHuman(options.configPath, "app restore", "app");
|
|
2237
1958
|
}
|
|
2238
1959
|
async function appCommand(parsed, dependencies = {}) {
|
|
2239
1960
|
const sub = parsed.positionals[1];
|
|
@@ -2319,7 +2040,7 @@ async function appCommand(parsed, dependencies = {}) {
|
|
|
2319
2040
|
}
|
|
2320
2041
|
|
|
2321
2042
|
// src/brand-command.ts
|
|
2322
|
-
var
|
|
2043
|
+
var import_promises = require("fs/promises");
|
|
2323
2044
|
var import_node_path7 = require("path");
|
|
2324
2045
|
|
|
2325
2046
|
// src/brand-design-unpack.ts
|
|
@@ -2422,7 +2143,7 @@ function describeUnpack(result, outDir) {
|
|
|
2422
2143
|
// src/brand-command.ts
|
|
2423
2144
|
var USAGE = "usage: odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]";
|
|
2424
2145
|
async function readBundle(source, deps) {
|
|
2425
|
-
if (source !== "-") return (0,
|
|
2146
|
+
if (source !== "-") return (0, import_promises.readFile)((0, import_node_path7.resolve)(source), "utf8");
|
|
2426
2147
|
const readStdin = deps.readStdin;
|
|
2427
2148
|
if (!readStdin) throw new Error("reading a bundle from stdin is not supported here");
|
|
2428
2149
|
return readStdin();
|
|
@@ -2430,8 +2151,8 @@ async function readBundle(source, deps) {
|
|
|
2430
2151
|
async function writeAll(result, outDir) {
|
|
2431
2152
|
for (const file of result.files) {
|
|
2432
2153
|
const target = (0, import_node_path7.resolve)(outDir, file.path);
|
|
2433
|
-
await (0,
|
|
2434
|
-
await (0,
|
|
2154
|
+
await (0, import_promises.mkdir)((0, import_node_path7.dirname)(target), { recursive: true });
|
|
2155
|
+
await (0, import_promises.writeFile)(target, file.bytes);
|
|
2435
2156
|
}
|
|
2436
2157
|
}
|
|
2437
2158
|
async function designUnpack(parsed, deps) {
|
|
@@ -2591,12 +2312,6 @@ async function pollCalendarConnection(ctx, attemptId) {
|
|
|
2591
2312
|
ctx.env
|
|
2592
2313
|
);
|
|
2593
2314
|
}
|
|
2594
|
-
async function requestCalendarDisconnect(ctx) {
|
|
2595
|
-
return parseCalendarStatus(
|
|
2596
|
-
await calendarJson(ctx, "/disconnect", { method: "POST", body: JSON.stringify({ purge: false }) }),
|
|
2597
|
-
ctx.env
|
|
2598
|
-
);
|
|
2599
|
-
}
|
|
2600
2315
|
function parseCalendarStatus(raw, env) {
|
|
2601
2316
|
const outer = wrapped(raw, "calendar");
|
|
2602
2317
|
const value2 = record(outer.attempt) ?? record(outer.status) ?? outer;
|
|
@@ -2780,10 +2495,7 @@ async function applyCalendarBookingPage(ctx, bookingPageUrl, out) {
|
|
|
2780
2495
|
}
|
|
2781
2496
|
async function calendarDisconnect(options) {
|
|
2782
2497
|
if (!options.yes) throw new Error("calendar disconnect requires --yes");
|
|
2783
|
-
|
|
2784
|
-
const status = await requestCalendarDisconnect(ctx);
|
|
2785
|
-
out.log(`${ctx.env}: calendar disconnected; no calendar data was stored`);
|
|
2786
|
-
return status;
|
|
2498
|
+
return requireStudioHuman(options.configPath, "calendar disconnect", "calendar", options.env);
|
|
2787
2499
|
}
|
|
2788
2500
|
async function ensureCalendarConnected(ctx, options) {
|
|
2789
2501
|
const out = options.stdout ?? console;
|
|
@@ -2997,9 +2709,9 @@ var import_apps6 = require("@odla-ai/apps");
|
|
|
2997
2709
|
var import_node_path8 = require("path");
|
|
2998
2710
|
|
|
2999
2711
|
// src/version.ts
|
|
3000
|
-
var
|
|
2712
|
+
var import_node_fs8 = require("fs");
|
|
3001
2713
|
function cliVersion() {
|
|
3002
|
-
const pkg = JSON.parse((0,
|
|
2714
|
+
const pkg = JSON.parse((0, import_node_fs8.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
|
|
3003
2715
|
return pkg.version ?? "unknown";
|
|
3004
2716
|
}
|
|
3005
2717
|
|
|
@@ -3015,7 +2727,7 @@ var ConfigOperationCommandError = class extends Error {
|
|
|
3015
2727
|
|
|
3016
2728
|
// src/config-operation-validate.ts
|
|
3017
2729
|
var import_apps3 = require("@odla-ai/apps");
|
|
3018
|
-
var
|
|
2730
|
+
var import_node_fs9 = require("fs");
|
|
3019
2731
|
|
|
3020
2732
|
// src/config-reconcile-digest.ts
|
|
3021
2733
|
var import_node_crypto2 = require("crypto");
|
|
@@ -3051,7 +2763,7 @@ var SERVICE = /^[a-z][a-z0-9-]{0,39}$/;
|
|
|
3051
2763
|
function readPlan(path) {
|
|
3052
2764
|
let value2;
|
|
3053
2765
|
try {
|
|
3054
|
-
const raw = (0,
|
|
2766
|
+
const raw = (0, import_node_fs9.readFileSync)(path, "utf8");
|
|
3055
2767
|
if (Buffer.byteLength(raw) > 128 * 1024) throw new Error("plan exceeds 128 KiB");
|
|
3056
2768
|
value2 = JSON.parse(raw);
|
|
3057
2769
|
} catch (error) {
|
|
@@ -3194,11 +2906,11 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
|
|
|
3194
2906
|
}
|
|
3195
2907
|
if (code === "provision_approval_required") {
|
|
3196
2908
|
throw new Error(
|
|
3197
|
-
`${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.
|
|
2909
|
+
`${env}: the agent credential does not carry the owner-reviewed app.manage grant required to provision "${cfg.app.id}" (tenant ${tenantId}). The human owner id on the token is accountability, not agent authority. Run "odla-ai provision --request-grant --email <odla-account>" to open one fresh exact-project owner review; do not change app ownership unless the human account itself is not an owner`
|
|
3198
2910
|
);
|
|
3199
2911
|
}
|
|
3200
2912
|
throw new Error(
|
|
3201
|
-
`${env}: this credential lacks live app.manage authority for "${cfg.app.id}" (tenant ${tenantId}) \u2014 nothing was minted or written;
|
|
2913
|
+
`${env}: this credential lacks live app.manage authority for "${cfg.app.id}" (tenant ${tenantId}) \u2014 nothing was minted or written; run "odla-ai provision --request-grant --email <odla-account>" to open a fresh owner review. If the human account is not an owner, an existing owner must add it in signed-in Studio; an agent token cannot repair ownership`
|
|
3202
2914
|
);
|
|
3203
2915
|
}
|
|
3204
2916
|
throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText4(res)}`);
|
|
@@ -3764,7 +3476,7 @@ async function configPlan(options) {
|
|
|
3764
3476
|
apply,
|
|
3765
3477
|
nextActions: planNextActions(reconciliation, options.configPath)
|
|
3766
3478
|
};
|
|
3767
|
-
|
|
3479
|
+
printPlan(document2, options);
|
|
3768
3480
|
return document2;
|
|
3769
3481
|
}
|
|
3770
3482
|
async function inspectConfig(options) {
|
|
@@ -3804,7 +3516,7 @@ function printDiff(document2, options) {
|
|
|
3804
3516
|
printDifferences(out, document2);
|
|
3805
3517
|
printNext(out, document2.nextActions);
|
|
3806
3518
|
}
|
|
3807
|
-
function
|
|
3519
|
+
function printPlan(document2, options) {
|
|
3808
3520
|
const out = options.stdout ?? console;
|
|
3809
3521
|
if (options.json) {
|
|
3810
3522
|
out.log(JSON.stringify(document2, null, 2));
|
|
@@ -3907,12 +3619,12 @@ function quoteArg2(value2) {
|
|
|
3907
3619
|
|
|
3908
3620
|
// src/doctor-checks.ts
|
|
3909
3621
|
var import_node_child_process3 = require("child_process");
|
|
3910
|
-
var
|
|
3622
|
+
var import_node_fs11 = require("fs");
|
|
3911
3623
|
var import_node_path11 = require("path");
|
|
3912
3624
|
|
|
3913
3625
|
// src/wrangler.ts
|
|
3914
3626
|
var import_node_child_process2 = require("child_process");
|
|
3915
|
-
var
|
|
3627
|
+
var import_node_fs10 = require("fs");
|
|
3916
3628
|
var import_node_path10 = require("path");
|
|
3917
3629
|
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
3918
3630
|
const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -3928,14 +3640,14 @@ var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]
|
|
|
3928
3640
|
function findWranglerConfig(rootDir) {
|
|
3929
3641
|
for (const name of WRANGLER_CONFIG_FILES) {
|
|
3930
3642
|
const path = (0, import_node_path10.join)(rootDir, name);
|
|
3931
|
-
if ((0,
|
|
3643
|
+
if ((0, import_node_fs10.existsSync)(path)) return path;
|
|
3932
3644
|
}
|
|
3933
3645
|
return null;
|
|
3934
3646
|
}
|
|
3935
3647
|
function readWranglerConfig(path) {
|
|
3936
3648
|
if (path.endsWith(".toml")) return null;
|
|
3937
3649
|
try {
|
|
3938
|
-
return JSON.parse(stripJsonComments((0,
|
|
3650
|
+
return JSON.parse(stripJsonComments((0, import_node_fs10.readFileSync)(path, "utf8")));
|
|
3939
3651
|
} catch {
|
|
3940
3652
|
return null;
|
|
3941
3653
|
}
|
|
@@ -4043,7 +3755,7 @@ function wranglerWarnings(rootDir) {
|
|
|
4043
3755
|
const dir = (0, import_node_path11.resolve)(rootDir, assets.directory);
|
|
4044
3756
|
if (dir === (0, import_node_path11.resolve)(rootDir)) {
|
|
4045
3757
|
warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
|
|
4046
|
-
} else if ((0,
|
|
3758
|
+
} else if ((0, import_node_fs11.existsSync)((0, import_node_path11.join)(dir, "node_modules"))) {
|
|
4047
3759
|
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
4048
3760
|
}
|
|
4049
3761
|
}
|
|
@@ -4079,12 +3791,12 @@ function o11yProjectWarnings(rootDir) {
|
|
|
4079
3791
|
return warnings;
|
|
4080
3792
|
}
|
|
4081
3793
|
const main = typeof config.main === "string" ? (0, import_node_path11.resolve)(rootDir, config.main) : null;
|
|
4082
|
-
if (!main || !(0,
|
|
3794
|
+
if (!main || !(0, import_node_fs11.existsSync)(main)) {
|
|
4083
3795
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
|
|
4084
3796
|
} else {
|
|
4085
3797
|
let source = "";
|
|
4086
3798
|
try {
|
|
4087
|
-
source = (0,
|
|
3799
|
+
source = (0, import_node_fs11.readFileSync)(main, "utf8");
|
|
4088
3800
|
} catch {
|
|
4089
3801
|
}
|
|
4090
3802
|
if (!/\bwithObservability\b/.test(source)) {
|
|
@@ -4108,7 +3820,7 @@ function calendarProjectWarnings(rootDir) {
|
|
|
4108
3820
|
}
|
|
4109
3821
|
function readPackageJson(rootDir) {
|
|
4110
3822
|
try {
|
|
4111
|
-
return JSON.parse((0,
|
|
3823
|
+
return JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path11.join)(rootDir, "package.json"), "utf8"));
|
|
4112
3824
|
} catch {
|
|
4113
3825
|
return null;
|
|
4114
3826
|
}
|
|
@@ -4337,14 +4049,14 @@ function harnessOption(value2, flag) {
|
|
|
4337
4049
|
}
|
|
4338
4050
|
|
|
4339
4051
|
// src/init.ts
|
|
4340
|
-
var
|
|
4052
|
+
var import_node_fs12 = require("fs");
|
|
4341
4053
|
var import_node_path12 = require("path");
|
|
4342
4054
|
var import_apps9 = require("@odla-ai/apps");
|
|
4343
4055
|
function initProject(options) {
|
|
4344
4056
|
const out = options.stdout ?? console;
|
|
4345
4057
|
const rootDir = (0, import_node_path12.resolve)(options.rootDir ?? process.cwd());
|
|
4346
4058
|
const configPath = (0, import_node_path12.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
|
|
4347
|
-
if ((0,
|
|
4059
|
+
if ((0, import_node_fs12.existsSync)(configPath) && !options.force) {
|
|
4348
4060
|
throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
|
|
4349
4061
|
}
|
|
4350
4062
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
|
|
@@ -4360,10 +4072,10 @@ function initProject(options) {
|
|
|
4360
4072
|
}
|
|
4361
4073
|
}
|
|
4362
4074
|
const aiProvider = options.aiProvider;
|
|
4363
|
-
(0,
|
|
4364
|
-
(0,
|
|
4365
|
-
(0,
|
|
4366
|
-
(0,
|
|
4075
|
+
(0, import_node_fs12.mkdirSync)((0, import_node_path12.dirname)(configPath), { recursive: true });
|
|
4076
|
+
(0, import_node_fs12.mkdirSync)((0, import_node_path12.resolve)(rootDir, "src/odla"), { recursive: true });
|
|
4077
|
+
(0, import_node_fs12.mkdirSync)((0, import_node_path12.resolve)(rootDir, ".odla"), { recursive: true });
|
|
4078
|
+
(0, import_node_fs12.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
4367
4079
|
writeIfMissing((0, import_node_path12.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
4368
4080
|
writeIfMissing((0, import_node_path12.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
4369
4081
|
ensureGitignore(rootDir);
|
|
@@ -4372,8 +4084,8 @@ function initProject(options) {
|
|
|
4372
4084
|
out.log("updated .gitignore for local odla credentials");
|
|
4373
4085
|
}
|
|
4374
4086
|
function writeIfMissing(path, text2) {
|
|
4375
|
-
if ((0,
|
|
4376
|
-
(0,
|
|
4087
|
+
if ((0, import_node_fs12.existsSync)(path)) return;
|
|
4088
|
+
(0, import_node_fs12.writeFileSync)(path, text2);
|
|
4377
4089
|
}
|
|
4378
4090
|
function configTemplate(input) {
|
|
4379
4091
|
const calendar = input.services.includes("calendar") ? ` calendar: {
|
|
@@ -4580,7 +4292,9 @@ async function secretsSetClerkKey(options) {
|
|
|
4580
4292
|
if (value2.startsWith("sk_live_") && !PROD_ENV_NAMES2.has(options.env) && !options.yes) {
|
|
4581
4293
|
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)`);
|
|
4582
4294
|
}
|
|
4583
|
-
const token = await getDeveloperToken(cfg, options, doFetch, out
|
|
4295
|
+
const token = await getDeveloperToken(cfg, options, doFetch, out, {
|
|
4296
|
+
optionalProjectCapabilities: ["app.manage"]
|
|
4297
|
+
});
|
|
4584
4298
|
const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/clerk-secret`, {
|
|
4585
4299
|
method: "POST",
|
|
4586
4300
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
@@ -4610,7 +4324,7 @@ async function resolveVaultWrite(options) {
|
|
|
4610
4324
|
}
|
|
4611
4325
|
|
|
4612
4326
|
// src/skill.ts
|
|
4613
|
-
var
|
|
4327
|
+
var import_node_fs13 = require("fs");
|
|
4614
4328
|
var import_node_os2 = require("os");
|
|
4615
4329
|
var import_node_path13 = require("path");
|
|
4616
4330
|
var import_node_url2 = require("url");
|
|
@@ -4707,7 +4421,7 @@ function installSkill(options = {}) {
|
|
|
4707
4421
|
plans.set(target, { target, content: content2, boundary, managedMerge });
|
|
4708
4422
|
};
|
|
4709
4423
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
4710
|
-
for (const rel of files) plan((0, import_node_path13.join)(targetDir2, rel), (0,
|
|
4424
|
+
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);
|
|
4711
4425
|
};
|
|
4712
4426
|
let targetDir;
|
|
4713
4427
|
if (options.global) {
|
|
@@ -4727,7 +4441,7 @@ function installSkill(options = {}) {
|
|
|
4727
4441
|
for (const harness of harnesses) rememberTarget(harness, sharedRoot);
|
|
4728
4442
|
if (harnesses.includes("claude")) {
|
|
4729
4443
|
for (const skill of skillNames(files)) {
|
|
4730
|
-
const canonical = (0,
|
|
4444
|
+
const canonical = (0, import_node_fs13.readFileSync)((0, import_node_path13.join)(sourceDir, skill, "SKILL.md"), "utf8");
|
|
4731
4445
|
plan((0, import_node_path13.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
|
|
4732
4446
|
}
|
|
4733
4447
|
rememberTarget("claude", claudeRoot);
|
|
@@ -4762,11 +4476,11 @@ function installSkill(options = {}) {
|
|
|
4762
4476
|
conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
|
|
4763
4477
|
continue;
|
|
4764
4478
|
}
|
|
4765
|
-
if (!(0,
|
|
4479
|
+
if (!(0, import_node_fs13.existsSync)(file.target)) {
|
|
4766
4480
|
writtenPaths.add(file.target);
|
|
4767
4481
|
continue;
|
|
4768
4482
|
}
|
|
4769
|
-
const current = (0,
|
|
4483
|
+
const current = (0, import_node_fs13.readFileSync)(file.target, "utf8");
|
|
4770
4484
|
if (current === file.content) {
|
|
4771
4485
|
unchangedPaths.add(file.target);
|
|
4772
4486
|
} else if (file.managedMerge || options.force) {
|
|
@@ -4783,9 +4497,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
4783
4497
|
);
|
|
4784
4498
|
}
|
|
4785
4499
|
for (const file of plans.values()) {
|
|
4786
|
-
if (!(0,
|
|
4787
|
-
(0,
|
|
4788
|
-
(0,
|
|
4500
|
+
if (!(0, import_node_fs13.existsSync)(file.target) || (0, import_node_fs13.readFileSync)(file.target, "utf8") !== file.content) {
|
|
4501
|
+
(0, import_node_fs13.mkdirSync)((0, import_node_path13.dirname)(file.target), { recursive: true });
|
|
4502
|
+
(0, import_node_fs13.writeFileSync)(file.target, file.content);
|
|
4789
4503
|
}
|
|
4790
4504
|
}
|
|
4791
4505
|
const skills = skillNames(files);
|
|
@@ -4826,9 +4540,9 @@ function normalizeHarnesses(values, global) {
|
|
|
4826
4540
|
function managedFileContent(path, block, force, boundary) {
|
|
4827
4541
|
const symlink = symlinkedComponent(boundary, path);
|
|
4828
4542
|
if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
|
|
4829
|
-
if (!(0,
|
|
4543
|
+
if (!(0, import_node_fs13.existsSync)(path)) return `${block}
|
|
4830
4544
|
`;
|
|
4831
|
-
const current = (0,
|
|
4545
|
+
const current = (0, import_node_fs13.readFileSync)(path, "utf8");
|
|
4832
4546
|
const start = "<!-- odla-ai agent setup:start -->";
|
|
4833
4547
|
const end = "<!-- odla-ai agent setup:end -->";
|
|
4834
4548
|
const startAt = current.indexOf(start);
|
|
@@ -4857,7 +4571,7 @@ function symlinkedComponent(boundary, target) {
|
|
|
4857
4571
|
for (const part of rel.split(import_node_path13.sep).filter(Boolean)) {
|
|
4858
4572
|
current = (0, import_node_path13.join)(current, part);
|
|
4859
4573
|
try {
|
|
4860
|
-
if ((0,
|
|
4574
|
+
if ((0, import_node_fs13.lstatSync)(current).isSymbolicLink()) return current;
|
|
4861
4575
|
} catch (error) {
|
|
4862
4576
|
if (error.code !== "ENOENT") throw error;
|
|
4863
4577
|
}
|
|
@@ -4868,10 +4582,10 @@ function skillNames(files) {
|
|
|
4868
4582
|
return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
|
|
4869
4583
|
}
|
|
4870
4584
|
function listFiles(dir) {
|
|
4871
|
-
if (!(0,
|
|
4585
|
+
if (!(0, import_node_fs13.existsSync)(dir)) return [];
|
|
4872
4586
|
const results = [];
|
|
4873
4587
|
const walk = (current) => {
|
|
4874
|
-
for (const entry of (0,
|
|
4588
|
+
for (const entry of (0, import_node_fs13.readdirSync)(current, { withFileTypes: true })) {
|
|
4875
4589
|
const path = (0, import_node_path13.join)(current, entry.name);
|
|
4876
4590
|
if (entry.isDirectory()) walk(path);
|
|
4877
4591
|
else results.push((0, import_node_path13.relative)(dir, path));
|
|
@@ -5188,7 +4902,7 @@ async function projectCommand(command, parsed, deps) {
|
|
|
5188
4902
|
}
|
|
5189
4903
|
|
|
5190
4904
|
// src/code-connect.ts
|
|
5191
|
-
var
|
|
4905
|
+
var import_node_fs14 = require("fs");
|
|
5192
4906
|
var import_node_os4 = require("os");
|
|
5193
4907
|
var import_node_path15 = require("path");
|
|
5194
4908
|
|
|
@@ -5276,15 +4990,15 @@ function encodeAgentInput(message2) {
|
|
|
5276
4990
|
// ../harness/dist/chunk-PHXQH4YM.js
|
|
5277
4991
|
var import_child_process = require("child_process");
|
|
5278
4992
|
var import_fs = require("fs");
|
|
5279
|
-
var
|
|
4993
|
+
var import_promises2 = require("fs/promises");
|
|
5280
4994
|
var import_path = require("path");
|
|
5281
4995
|
var import_process = require("process");
|
|
5282
|
-
var
|
|
4996
|
+
var import_promises3 = require("fs/promises");
|
|
5283
4997
|
var import_os = require("os");
|
|
5284
4998
|
var import_path2 = require("path");
|
|
5285
4999
|
var import_child_process2 = require("child_process");
|
|
5286
5000
|
var import_path3 = require("path");
|
|
5287
|
-
var
|
|
5001
|
+
var import_promises4 = require("fs/promises");
|
|
5288
5002
|
var import_os2 = require("os");
|
|
5289
5003
|
var import_path4 = require("path");
|
|
5290
5004
|
var import_child_process3 = require("child_process");
|
|
@@ -5295,7 +5009,7 @@ function assertPinnedImage(image) {
|
|
|
5295
5009
|
async function commandAvailable(engine) {
|
|
5296
5010
|
for (const directory of (process.env.PATH ?? "").split(import_path.delimiter).filter(Boolean)) {
|
|
5297
5011
|
try {
|
|
5298
|
-
await (0,
|
|
5012
|
+
await (0, import_promises2.access)((0, import_path.join)(directory, engine), import_fs.constants.X_OK);
|
|
5299
5013
|
return true;
|
|
5300
5014
|
} catch {
|
|
5301
5015
|
}
|
|
@@ -5581,7 +5295,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
|
|
|
5581
5295
|
}
|
|
5582
5296
|
async function materializeGitTree(source, commitSha, options = {}) {
|
|
5583
5297
|
if (!/^[0-9a-f]{40}$/.test(commitSha)) throw new TypeError("Git tree requires an exact commit SHA");
|
|
5584
|
-
const sourceDir = await (0,
|
|
5298
|
+
const sourceDir = await (0, import_promises3.realpath)((0, import_path2.resolve)(source));
|
|
5585
5299
|
const maxFiles = options.maxFiles ?? 2e4;
|
|
5586
5300
|
const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
|
|
5587
5301
|
const inventory = (await gitOutput(sourceDir, ["ls-tree", "-rz", commitSha], 16 * 1024 * 1024)).toString("utf8").split("\0").filter(Boolean);
|
|
@@ -5590,9 +5304,9 @@ async function materializeGitTree(source, commitSha, options = {}) {
|
|
|
5590
5304
|
return match && allowedWorkspacePath(match[3]) ? [{ mode: match[1], hash: match[2], path: match[3] }] : [];
|
|
5591
5305
|
});
|
|
5592
5306
|
if (entries.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
|
|
5593
|
-
const root = await (0,
|
|
5307
|
+
const root = await (0, import_promises3.mkdtemp)((0, import_path2.join)(options.tempRoot ?? (0, import_os.tmpdir)(), "odla-git-tree-"));
|
|
5594
5308
|
const targetRoot = (0, import_path2.join)(root, "source");
|
|
5595
|
-
await (0,
|
|
5309
|
+
await (0, import_promises3.mkdir)(targetRoot);
|
|
5596
5310
|
let byteCount = 0;
|
|
5597
5311
|
try {
|
|
5598
5312
|
const blobs = await gitBlobs(sourceDir, entries, maxBytes);
|
|
@@ -5602,18 +5316,18 @@ async function materializeGitTree(source, commitSha, options = {}) {
|
|
|
5602
5316
|
if (byteCount > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
5603
5317
|
const target = (0, import_path2.resolve)(targetRoot, entry.path);
|
|
5604
5318
|
if (!target.startsWith(`${(0, import_path2.resolve)(targetRoot)}${import_path2.sep}`)) throw new TypeError("Git tree path escapes workspace");
|
|
5605
|
-
await (0,
|
|
5606
|
-
await (0,
|
|
5319
|
+
await (0, import_promises3.mkdir)((0, import_path2.resolve)(target, ".."), { recursive: true });
|
|
5320
|
+
await (0, import_promises3.writeFile)(target, content2, { flag: "wx", mode: entry.mode === "100755" ? 493 : 420 });
|
|
5607
5321
|
}
|
|
5608
5322
|
return {
|
|
5609
5323
|
root,
|
|
5610
5324
|
sourceDir: targetRoot,
|
|
5611
5325
|
fileCount: entries.length,
|
|
5612
5326
|
byteCount,
|
|
5613
|
-
cleanup: () => (0,
|
|
5327
|
+
cleanup: () => (0, import_promises3.rm)(root, { recursive: true, force: true })
|
|
5614
5328
|
};
|
|
5615
5329
|
} catch (error) {
|
|
5616
|
-
await (0,
|
|
5330
|
+
await (0, import_promises3.rm)(root, { recursive: true, force: true });
|
|
5617
5331
|
throw error;
|
|
5618
5332
|
}
|
|
5619
5333
|
}
|
|
@@ -5621,7 +5335,7 @@ async function sourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
|
5621
5335
|
const files = [];
|
|
5622
5336
|
let bytes = 0;
|
|
5623
5337
|
const walk = async (dir) => {
|
|
5624
|
-
for (const entry of await (0,
|
|
5338
|
+
for (const entry of await (0, import_promises4.readdir)(dir, { withFileTypes: true })) {
|
|
5625
5339
|
if (entry.isDirectory() && SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
|
|
5626
5340
|
if (!entry.isDirectory() && SECRET_WORKSPACE_FILE.test(entry.name)) continue;
|
|
5627
5341
|
const path = (0, import_path4.join)(dir, entry.name);
|
|
@@ -5631,7 +5345,7 @@ async function sourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
|
5631
5345
|
continue;
|
|
5632
5346
|
}
|
|
5633
5347
|
if (!entry.isFile()) continue;
|
|
5634
|
-
const metadata2 = await (0,
|
|
5348
|
+
const metadata2 = await (0, import_promises4.stat)(path);
|
|
5635
5349
|
bytes += metadata2.size;
|
|
5636
5350
|
if (files.length + 1 > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
|
|
5637
5351
|
if (bytes > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
@@ -5680,7 +5394,7 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
|
5680
5394
|
if (!source.startsWith(`${root}${import_path4.sep}`)) throw new TypeError("git file path escapes workspace");
|
|
5681
5395
|
let metadata2;
|
|
5682
5396
|
try {
|
|
5683
|
-
metadata2 = await (0,
|
|
5397
|
+
metadata2 = await (0, import_promises4.lstat)(source);
|
|
5684
5398
|
} catch (error) {
|
|
5685
5399
|
if (error.code === "ENOENT") continue;
|
|
5686
5400
|
throw error;
|
|
@@ -5695,9 +5409,9 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
|
5695
5409
|
async function copyTree(files, destination) {
|
|
5696
5410
|
for (const file of files) {
|
|
5697
5411
|
const target = (0, import_path4.join)(destination, file.relativePath);
|
|
5698
|
-
await (0,
|
|
5699
|
-
await (0,
|
|
5700
|
-
await (0,
|
|
5412
|
+
await (0, import_promises4.mkdir)((0, import_path4.resolve)(target, ".."), { recursive: true });
|
|
5413
|
+
await (0, import_promises4.copyFile)(file.source, target);
|
|
5414
|
+
await (0, import_promises4.chmod)(target, file.mode);
|
|
5701
5415
|
}
|
|
5702
5416
|
}
|
|
5703
5417
|
async function captureGitDiff(root, maxBytes) {
|
|
@@ -5734,13 +5448,13 @@ async function captureGitDiff(root, maxBytes) {
|
|
|
5734
5448
|
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");
|
|
5735
5449
|
}
|
|
5736
5450
|
async function stageWorkspace(source, options = {}) {
|
|
5737
|
-
const sourceDir = await (0,
|
|
5738
|
-
const sourceStat = await (0,
|
|
5451
|
+
const sourceDir = await (0, import_promises4.realpath)((0, import_path4.resolve)(source));
|
|
5452
|
+
const sourceStat = await (0, import_promises4.stat)(sourceDir);
|
|
5739
5453
|
if (!sourceStat.isDirectory()) throw new TypeError("workspace source must be a directory");
|
|
5740
|
-
const root = await (0,
|
|
5454
|
+
const root = await (0, import_promises4.mkdtemp)((0, import_path4.join)(options.tempRoot ?? (0, import_os2.tmpdir)(), "odla-harness-"));
|
|
5741
5455
|
const baselineDir = (0, import_path4.join)(root, "baseline");
|
|
5742
5456
|
const workspaceDir = (0, import_path4.join)(root, "workspace");
|
|
5743
|
-
await Promise.all([(0,
|
|
5457
|
+
await Promise.all([(0, import_promises4.mkdir)(baselineDir), (0, import_promises4.mkdir)(workspaceDir)]);
|
|
5744
5458
|
try {
|
|
5745
5459
|
const maxFiles = options.maxFiles ?? 2e4;
|
|
5746
5460
|
const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
|
|
@@ -5753,26 +5467,26 @@ async function stageWorkspace(source, options = {}) {
|
|
|
5753
5467
|
fileCount: files.length,
|
|
5754
5468
|
byteCount: files.reduce((sum, file) => sum + file.bytes, 0),
|
|
5755
5469
|
patch: (maxBytes2) => captureGitDiff(root, maxBytes2),
|
|
5756
|
-
cleanup: () => (0,
|
|
5470
|
+
cleanup: () => (0, import_promises4.rm)(root, { recursive: true, force: true })
|
|
5757
5471
|
};
|
|
5758
5472
|
} catch (error) {
|
|
5759
|
-
await (0,
|
|
5473
|
+
await (0, import_promises4.rm)(root, { recursive: true, force: true });
|
|
5760
5474
|
throw error;
|
|
5761
5475
|
}
|
|
5762
5476
|
}
|
|
5763
5477
|
async function stageWorkspacePair(baselineSource, workspaceSource, options = {}) {
|
|
5764
|
-
const baselineDirSource = await (0,
|
|
5765
|
-
const workspaceDirSource = await (0,
|
|
5478
|
+
const baselineDirSource = await (0, import_promises4.realpath)((0, import_path4.resolve)(baselineSource));
|
|
5479
|
+
const workspaceDirSource = await (0, import_promises4.realpath)((0, import_path4.resolve)(workspaceSource));
|
|
5766
5480
|
const maxFiles = options.maxFiles ?? 2e4;
|
|
5767
5481
|
const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
|
|
5768
5482
|
const [baselineFiles, workspaceFiles] = await Promise.all([
|
|
5769
5483
|
sourceFiles(baselineDirSource, maxFiles, maxBytes),
|
|
5770
5484
|
sourceFiles(workspaceDirSource, maxFiles, maxBytes)
|
|
5771
5485
|
]);
|
|
5772
|
-
const root = await (0,
|
|
5486
|
+
const root = await (0, import_promises4.mkdtemp)((0, import_path4.join)(options.tempRoot ?? (0, import_os2.tmpdir)(), "odla-harness-"));
|
|
5773
5487
|
const baselineDir = (0, import_path4.join)(root, "baseline");
|
|
5774
5488
|
const workspaceDir = (0, import_path4.join)(root, "workspace");
|
|
5775
|
-
await Promise.all([(0,
|
|
5489
|
+
await Promise.all([(0, import_promises4.mkdir)(baselineDir), (0, import_promises4.mkdir)(workspaceDir)]);
|
|
5776
5490
|
try {
|
|
5777
5491
|
await Promise.all([copyTree(baselineFiles, baselineDir), copyTree(workspaceFiles, workspaceDir)]);
|
|
5778
5492
|
return {
|
|
@@ -5782,17 +5496,17 @@ async function stageWorkspacePair(baselineSource, workspaceSource, options = {})
|
|
|
5782
5496
|
fileCount: workspaceFiles.length,
|
|
5783
5497
|
byteCount: workspaceFiles.reduce((sum, file) => sum + file.bytes, 0),
|
|
5784
5498
|
patch: (maxPatchBytes) => captureGitDiff(root, maxPatchBytes),
|
|
5785
|
-
cleanup: () => (0,
|
|
5499
|
+
cleanup: () => (0, import_promises4.rm)(root, { recursive: true, force: true })
|
|
5786
5500
|
};
|
|
5787
5501
|
} catch (error) {
|
|
5788
|
-
await (0,
|
|
5502
|
+
await (0, import_promises4.rm)(root, { recursive: true, force: true });
|
|
5789
5503
|
throw error;
|
|
5790
5504
|
}
|
|
5791
5505
|
}
|
|
5792
5506
|
|
|
5793
5507
|
// ../harness/dist/chunk-GMVZ4LZH.js
|
|
5794
5508
|
var import_crypto = require("crypto");
|
|
5795
|
-
var
|
|
5509
|
+
var import_promises5 = require("fs/promises");
|
|
5796
5510
|
var import_path5 = require("path");
|
|
5797
5511
|
|
|
5798
5512
|
// ../camel/dist/chunk-7FHPOQVP.js
|
|
@@ -6129,19 +5843,19 @@ function validateSnapshot(snapshot, limits) {
|
|
|
6129
5843
|
|
|
6130
5844
|
// ../harness/dist/chunk-GMVZ4LZH.js
|
|
6131
5845
|
var import_child_process4 = require("child_process");
|
|
6132
|
-
var
|
|
5846
|
+
var import_promises6 = require("fs/promises");
|
|
6133
5847
|
var import_path6 = require("path");
|
|
6134
5848
|
var import_child_process5 = require("child_process");
|
|
6135
5849
|
var import_process2 = require("process");
|
|
6136
5850
|
var import_crypto2 = require("crypto");
|
|
6137
5851
|
var import_crypto3 = require("crypto");
|
|
6138
5852
|
var import_fs2 = require("fs");
|
|
6139
|
-
var
|
|
5853
|
+
var import_promises7 = require("fs/promises");
|
|
6140
5854
|
var import_path7 = require("path");
|
|
6141
|
-
var
|
|
5855
|
+
var import_promises8 = require("fs/promises");
|
|
6142
5856
|
var import_os3 = require("os");
|
|
6143
5857
|
var import_path8 = require("path");
|
|
6144
|
-
var
|
|
5858
|
+
var import_promises9 = require("fs/promises");
|
|
6145
5859
|
var import_path9 = require("path");
|
|
6146
5860
|
|
|
6147
5861
|
// ../camel/dist/chunk-4EIRFS3A.js
|
|
@@ -6433,7 +6147,7 @@ var import_crypto4 = require("crypto");
|
|
|
6433
6147
|
async function digestStagedWorkspace(root, limits) {
|
|
6434
6148
|
const files = [];
|
|
6435
6149
|
const walk = async (directory) => {
|
|
6436
|
-
const entries = await (0,
|
|
6150
|
+
const entries = await (0, import_promises5.readdir)(directory, { withFileTypes: true });
|
|
6437
6151
|
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
6438
6152
|
if (entry.isSymbolicLink()) throw new TypeError("workspace digest refuses symbolic links");
|
|
6439
6153
|
const target = (0, import_path5.resolve)(directory, entry.name);
|
|
@@ -6448,7 +6162,7 @@ async function digestStagedWorkspace(root, limits) {
|
|
|
6448
6162
|
const hash = (0, import_crypto.createHash)("sha256");
|
|
6449
6163
|
let bytes = 0;
|
|
6450
6164
|
for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
|
|
6451
|
-
const content2 = await (0,
|
|
6165
|
+
const content2 = await (0, import_promises5.readFile)(file.target);
|
|
6452
6166
|
bytes += Buffer.byteLength(file.path) + content2.byteLength;
|
|
6453
6167
|
if (bytes > limits.maxBytes) throw new TypeError("workspace digest exceeds its byte bound");
|
|
6454
6168
|
hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content2.byteLength}:`);
|
|
@@ -6774,7 +6488,7 @@ async function applyCodePatch(workspaceDir, patch2, paths) {
|
|
|
6774
6488
|
await gitApply(workspaceDir, patch2, false);
|
|
6775
6489
|
for (const path of paths) {
|
|
6776
6490
|
try {
|
|
6777
|
-
const info = await (0,
|
|
6491
|
+
const info = await (0, import_promises6.lstat)(resolveCodePath(workspaceDir, path));
|
|
6778
6492
|
if (info.isSymbolicLink() || !info.isFile() && !info.isDirectory()) {
|
|
6779
6493
|
throw new TypeError("patch created a non-regular workspace entry");
|
|
6780
6494
|
}
|
|
@@ -7065,7 +6779,7 @@ async function inspectArtifacts(workspaceDir, recipe2) {
|
|
|
7065
6779
|
for (const artifact of recipe2.expectedArtifacts ?? []) {
|
|
7066
6780
|
try {
|
|
7067
6781
|
const path = (0, import_path7.join)(workspaceDir, artifact.path);
|
|
7068
|
-
const info = await (0,
|
|
6782
|
+
const info = await (0, import_promises7.lstat)(path);
|
|
7069
6783
|
if (!info.isFile() || info.isSymbolicLink()) {
|
|
7070
6784
|
receipts.push({ artifactId: artifact.id, status: "invalid", bytes: null, digest: null });
|
|
7071
6785
|
} else if (info.size > artifact.maximumBytes) {
|
|
@@ -7246,9 +6960,9 @@ var RESERVED2 = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_mod
|
|
|
7246
6960
|
var SECRET2 = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
|
|
7247
6961
|
async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os3.tmpdir)()) {
|
|
7248
6962
|
if (!snapshot.files.length || snapshot.files.length > 1e4) throw new TypeError("Code source file count is invalid");
|
|
7249
|
-
const root = await (0,
|
|
6963
|
+
const root = await (0, import_promises8.mkdtemp)((0, import_path8.join)(tempRoot, "odla-code-source-"));
|
|
7250
6964
|
const sourceDir = (0, import_path8.join)(root, "source");
|
|
7251
|
-
await (0,
|
|
6965
|
+
await (0, import_promises8.mkdir)(sourceDir);
|
|
7252
6966
|
const seen = /* @__PURE__ */ new Set();
|
|
7253
6967
|
let bytes = 0;
|
|
7254
6968
|
try {
|
|
@@ -7260,8 +6974,8 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os3.
|
|
|
7260
6974
|
if (bytes > 16 * 1024 * 1024) throw new TypeError("Code source exceeds its byte bound");
|
|
7261
6975
|
const target = (0, import_path8.resolve)(sourceDir, file.path);
|
|
7262
6976
|
if (!target.startsWith(`${(0, import_path8.resolve)(sourceDir)}${import_path8.sep}`)) throw new TypeError("Code source path escapes its root");
|
|
7263
|
-
await (0,
|
|
7264
|
-
await (0,
|
|
6977
|
+
await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
|
|
6978
|
+
await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 420 });
|
|
7265
6979
|
}
|
|
7266
6980
|
for (const reference of snapshot.references ?? []) {
|
|
7267
6981
|
validateAlias(reference.alias);
|
|
@@ -7275,13 +6989,13 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os3.
|
|
|
7275
6989
|
if (bytes > 80 * 1024 * 1024) throw new TypeError("Code source set exceeds its byte bound");
|
|
7276
6990
|
const target = (0, import_path8.resolve)(sourceDir, path);
|
|
7277
6991
|
if (!target.startsWith(`${(0, import_path8.resolve)(sourceDir)}${import_path8.sep}`)) throw new TypeError("Code reference path escapes its root");
|
|
7278
|
-
await (0,
|
|
7279
|
-
await (0,
|
|
6992
|
+
await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
|
|
6993
|
+
await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 292 });
|
|
7280
6994
|
}
|
|
7281
6995
|
}
|
|
7282
|
-
return { sourceDir, cleanup: () => (0,
|
|
6996
|
+
return { sourceDir, cleanup: () => (0, import_promises8.rm)(root, { recursive: true, force: true }) };
|
|
7283
6997
|
} catch (cause) {
|
|
7284
|
-
await (0,
|
|
6998
|
+
await (0, import_promises8.rm)(root, { recursive: true, force: true });
|
|
7285
6999
|
throw cause;
|
|
7286
7000
|
}
|
|
7287
7001
|
}
|
|
@@ -7302,8 +7016,8 @@ async function attachCodeRuntimeReferences(workspace, references) {
|
|
|
7302
7016
|
for (const root of [workspace.baselineDir, workspace.workspaceDir]) {
|
|
7303
7017
|
const target = (0, import_path8.resolve)(root, path);
|
|
7304
7018
|
if (!target.startsWith(`${(0, import_path8.resolve)(root)}${import_path8.sep}`)) throw new TypeError("Code reference path escapes its root");
|
|
7305
|
-
await (0,
|
|
7306
|
-
await (0,
|
|
7019
|
+
await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
|
|
7020
|
+
await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 292 });
|
|
7307
7021
|
}
|
|
7308
7022
|
}
|
|
7309
7023
|
}
|
|
@@ -7489,11 +7203,11 @@ async function read(context, request2, options, policy) {
|
|
|
7489
7203
|
const allowed = await policy.read(policyContext(context, request2, options, { paths, path, startLine, endLine }));
|
|
7490
7204
|
if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
|
|
7491
7205
|
const target = resolveCodePath(context.workspaceDir, path);
|
|
7492
|
-
const info = await (0,
|
|
7206
|
+
const info = await (0, import_promises9.stat)(target);
|
|
7493
7207
|
if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
|
|
7494
7208
|
throw new TypeError("file is not a bounded regular source file");
|
|
7495
7209
|
}
|
|
7496
|
-
const source = await (0,
|
|
7210
|
+
const source = await (0, import_promises9.readFile)(target);
|
|
7497
7211
|
if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
|
|
7498
7212
|
const lines = source.toString("utf8").split("\n");
|
|
7499
7213
|
const content2 = lines.slice(startLine - 1, endLine).join("\n");
|
|
@@ -7570,7 +7284,7 @@ function policyContext(context, request2, options, extra) {
|
|
|
7570
7284
|
async function registeredFiles(root, limit) {
|
|
7571
7285
|
const paths = [];
|
|
7572
7286
|
const walk = async (directory) => {
|
|
7573
|
-
for (const entry of await (0,
|
|
7287
|
+
for (const entry of await (0, import_promises9.readdir)(directory, { withFileTypes: true })) {
|
|
7574
7288
|
if (entry.isSymbolicLink()) throw new TypeError("workspace contains a symbolic link");
|
|
7575
7289
|
const target = (0, import_path9.resolve)(directory, entry.name);
|
|
7576
7290
|
if (entry.isDirectory()) await walk(target);
|
|
@@ -8355,7 +8069,7 @@ function digestText(value2) {
|
|
|
8355
8069
|
// src/code-images.ts
|
|
8356
8070
|
var import_node_child_process6 = require("child_process");
|
|
8357
8071
|
var import_node_crypto4 = require("crypto");
|
|
8358
|
-
var
|
|
8072
|
+
var import_promises10 = require("fs/promises");
|
|
8359
8073
|
var import_node_os3 = require("os");
|
|
8360
8074
|
var import_node_path14 = require("path");
|
|
8361
8075
|
var import_node_url3 = require("url");
|
|
@@ -8437,16 +8151,16 @@ function embeddedPiAssetPath() {
|
|
|
8437
8151
|
return (0, import_node_url3.fileURLToPath)(new URL("./runtime/pi-agent.js", importMetaUrl));
|
|
8438
8152
|
}
|
|
8439
8153
|
async function embeddedPiImageName() {
|
|
8440
|
-
const bundle = await (0,
|
|
8154
|
+
const bundle = await (0, import_promises10.readFile)(embeddedPiAssetPath()).catch(() => {
|
|
8441
8155
|
throw new Error("CLI-embedded Pi runtime is missing; reinstall this exact @odla-ai/cli version");
|
|
8442
8156
|
});
|
|
8443
8157
|
return `odla-ai/pi-agent:embedded-sha256-${(0, import_node_crypto4.createHash)("sha256").update(bundle).digest("hex")}`;
|
|
8444
8158
|
}
|
|
8445
8159
|
async function buildEmbeddedPiImage(engine, image, run) {
|
|
8446
|
-
const context = await (0,
|
|
8160
|
+
const context = await (0, import_promises10.mkdtemp)((0, import_node_path14.join)((0, import_node_os3.tmpdir)(), "odla-code-pi-"));
|
|
8447
8161
|
try {
|
|
8448
|
-
await (0,
|
|
8449
|
-
await (0,
|
|
8162
|
+
await (0, import_promises10.copyFile)(embeddedPiAssetPath(), (0, import_node_path14.join)(context, "pi-agent.js"));
|
|
8163
|
+
await (0, import_promises10.writeFile)((0, import_node_path14.join)(context, "Dockerfile"), [
|
|
8450
8164
|
`FROM ${CODE_NODE_IMAGE}`,
|
|
8451
8165
|
"COPY pi-agent.js /opt/odla/pi-agent.js",
|
|
8452
8166
|
"WORKDIR /workspace",
|
|
@@ -8455,7 +8169,7 @@ async function buildEmbeddedPiImage(engine, image, run) {
|
|
|
8455
8169
|
].join("\n"), { mode: 384 });
|
|
8456
8170
|
await run(engine, ["build", "--tag", image, context], "inherit");
|
|
8457
8171
|
} finally {
|
|
8458
|
-
await (0,
|
|
8172
|
+
await (0, import_promises10.rm)(context, { recursive: true, force: true });
|
|
8459
8173
|
}
|
|
8460
8174
|
}
|
|
8461
8175
|
|
|
@@ -8463,7 +8177,7 @@ async function buildEmbeddedPiImage(engine, image, run) {
|
|
|
8463
8177
|
async function codeConnect(options) {
|
|
8464
8178
|
const cwd = options.cwd ?? process.cwd();
|
|
8465
8179
|
const configPath = (0, import_node_path15.resolve)(cwd, options.configPath);
|
|
8466
|
-
const cfg = (0,
|
|
8180
|
+
const cfg = (0, import_node_fs14.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
|
|
8467
8181
|
const requestedAppId = options.appId?.trim();
|
|
8468
8182
|
if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
|
|
8469
8183
|
throw new Error("--app-id must be a valid odla app id");
|
|
@@ -8854,18 +8568,17 @@ Usage:
|
|
|
8854
8568
|
odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
|
|
8855
8569
|
odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
|
|
8856
8570
|
odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
|
|
8857
|
-
odla-ai calendar disconnect [--env dev] [
|
|
8858
|
-
odla-ai app archive [--config odla.config.mjs] [
|
|
8859
|
-
odla-ai app restore [--config odla.config.mjs]
|
|
8860
|
-
odla-ai app export [--env dev]
|
|
8571
|
+
odla-ai calendar disconnect [--env dev] --yes [continue in Studio; human session required]
|
|
8572
|
+
odla-ai app archive [--config odla.config.mjs] --yes [continue in Studio; human session required]
|
|
8573
|
+
odla-ai app restore [--config odla.config.mjs] [continue in Studio; human session required]
|
|
8574
|
+
odla-ai app export [--env dev] [continue in Studio; human session required]
|
|
8861
8575
|
odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
|
|
8862
|
-
|
|
8863
|
-
odla-ai app
|
|
8864
|
-
odla-ai app
|
|
8865
|
-
odla-ai app
|
|
8866
|
-
odla-ai app
|
|
8867
|
-
odla-ai app owners add
|
|
8868
|
-
odla-ai app owners remove <email> [--email <odla-account>] [--json]
|
|
8576
|
+
[dry-run is local; writes continue in Studio]
|
|
8577
|
+
odla-ai app refresh-sandbox [continue in Studio; human session required]
|
|
8578
|
+
odla-ai app go-live [continue in Studio; human session required]
|
|
8579
|
+
odla-ai app promote [continue in Studio; human session required]
|
|
8580
|
+
odla-ai app rename <name> [continue in Studio; human session required]
|
|
8581
|
+
odla-ai app owners <list|add|remove> [...] [continue in Studio; human session required]
|
|
8869
8582
|
odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
|
|
8870
8583
|
odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
8871
8584
|
odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
@@ -8900,8 +8613,8 @@ Usage:
|
|
|
8900
8613
|
odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
|
|
8901
8614
|
odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
|
|
8902
8615
|
odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
|
|
8903
|
-
odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--
|
|
8904
|
-
odla-ai agent retry <job-id> [--env dev] [--
|
|
8616
|
+
odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--token <ODLA_API_KEY>] [--json]
|
|
8617
|
+
odla-ai agent retry <job-id> [--env dev] [--token <ODLA_API_KEY>] [--json]
|
|
8905
8618
|
odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
|
|
8906
8619
|
odla-ai context list [--json]
|
|
8907
8620
|
odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
|
|
@@ -8937,8 +8650,8 @@ Usage:
|
|
|
8937
8650
|
odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
|
|
8938
8651
|
odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
|
|
8939
8652
|
odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
|
|
8940
|
-
odla-ai security github connect [--repo owner/name] [--env dev]
|
|
8941
|
-
odla-ai security github disconnect --source <id> [--env dev]
|
|
8653
|
+
odla-ai security github connect [--repo owner/name] [--env dev] [continue in Studio; human session required]
|
|
8654
|
+
odla-ai security github disconnect --source <id> [--env dev] [continue in Studio; human session required]
|
|
8942
8655
|
odla-ai security plan [--env dev] [--json]
|
|
8943
8656
|
odla-ai security sources [--env dev] [--json]
|
|
8944
8657
|
odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
|
|
@@ -8946,7 +8659,7 @@ Usage:
|
|
|
8946
8659
|
odla-ai security report <job-id> [--json]
|
|
8947
8660
|
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
8948
8661
|
odla-ai security run [target] --self --ack-redacted-source
|
|
8949
|
-
odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
8662
|
+
odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--request-grant] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
8950
8663
|
odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
|
|
8951
8664
|
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
8952
8665
|
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
@@ -9081,6 +8794,10 @@ Safety:
|
|
|
9081
8794
|
The email is a non-secret identity hint: never provide a password or session
|
|
9082
8795
|
token. The matching account must already exist, be signed in, explicitly
|
|
9083
8796
|
review the exact code, and finish any current request before claiming another.
|
|
8797
|
+
If provision reports that the current agent principal has no live app.manage
|
|
8798
|
+
grant, run it once with --request-grant. That flag ignores ODLA_DEV_TOKEN and
|
|
8799
|
+
the local cache, prints and opens a fresh exact-project owner-review URL, then
|
|
8800
|
+
continues provisioning with the approved replacement credential.
|
|
9084
8801
|
Run Code from a GitHub checkout already connected to an app in Studio; an
|
|
9085
8802
|
odla.config.mjs may select the app explicitly but is not required. Code host
|
|
9086
8803
|
approval and credential hashes live in odla-ai/db. The host
|
|
@@ -9449,7 +9166,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
9449
9166
|
throw new WatchRemoteError(cursor, error);
|
|
9450
9167
|
}
|
|
9451
9168
|
if (deadline !== void 0 && now() >= deadline) {
|
|
9452
|
-
const result =
|
|
9169
|
+
const result = report(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
9453
9170
|
throw new WatchTimeoutError(result.cursor);
|
|
9454
9171
|
}
|
|
9455
9172
|
const base = Math.min(intervalMs, 1e3);
|
|
@@ -9490,7 +9207,7 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
9490
9207
|
});
|
|
9491
9208
|
const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
|
|
9492
9209
|
const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
|
|
9493
|
-
return
|
|
9210
|
+
return report(ctx, parsed, {
|
|
9494
9211
|
found: true,
|
|
9495
9212
|
cursor,
|
|
9496
9213
|
events: matching,
|
|
@@ -9517,13 +9234,13 @@ async function discussWatch(ctx, topicId, parsed) {
|
|
|
9517
9234
|
}
|
|
9518
9235
|
if (page2.hasMore) continue;
|
|
9519
9236
|
if (deadline !== void 0 && now() >= deadline) {
|
|
9520
|
-
return
|
|
9237
|
+
return report(ctx, parsed, { found: false, cursor });
|
|
9521
9238
|
}
|
|
9522
9239
|
const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
|
|
9523
9240
|
await sleep(wait2);
|
|
9524
9241
|
}
|
|
9525
9242
|
}
|
|
9526
|
-
function
|
|
9243
|
+
function report(ctx, parsed, result) {
|
|
9527
9244
|
if (ctx.json) {
|
|
9528
9245
|
ctx.out.log(JSON.stringify(result, null, 2));
|
|
9529
9246
|
} else if (parsed.options.jsonl !== true && result.found) {
|
|
@@ -10024,7 +9741,7 @@ function eventLabel(event) {
|
|
|
10024
9741
|
const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
|
|
10025
9742
|
return body || event.payload.entityId;
|
|
10026
9743
|
}
|
|
10027
|
-
function
|
|
9744
|
+
function report2(ctx, parsed, result) {
|
|
10028
9745
|
if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
|
|
10029
9746
|
else if (parsed.options.jsonl !== true && result.found) {
|
|
10030
9747
|
for (const event of result.events ?? []) {
|
|
@@ -10084,7 +9801,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
10084
9801
|
});
|
|
10085
9802
|
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
|
|
10086
9803
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10087
|
-
return
|
|
9804
|
+
return report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
|
|
10088
9805
|
}
|
|
10089
9806
|
const backoff = Math.min(
|
|
10090
9807
|
MAX_BACKOFF_MS2,
|
|
@@ -10125,7 +9842,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
10125
9842
|
cursor,
|
|
10126
9843
|
serverTime: current.serverTime
|
|
10127
9844
|
});
|
|
10128
|
-
return
|
|
9845
|
+
return report2(ctx, parsed, { found: true, cursor, events: matching });
|
|
10129
9846
|
}
|
|
10130
9847
|
if (current.events.length > 0) {
|
|
10131
9848
|
jsonl2(ctx, parsed, {
|
|
@@ -10144,7 +9861,7 @@ async function pmWatch(ctx, parsed) {
|
|
|
10144
9861
|
}
|
|
10145
9862
|
if (current.hasMore) continue;
|
|
10146
9863
|
if (deadline !== void 0 && now() >= deadline) {
|
|
10147
|
-
return
|
|
9864
|
+
return report2(ctx, parsed, { found: false, cursor });
|
|
10148
9865
|
}
|
|
10149
9866
|
await sleep(
|
|
10150
9867
|
deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
|
|
@@ -11046,14 +10763,25 @@ async function provision(options) {
|
|
|
11046
10763
|
}
|
|
11047
10764
|
const doFetch = options.fetch ?? fetch;
|
|
11048
10765
|
const token = await getDeveloperToken(cfg, options, doFetch, out, {
|
|
11049
|
-
optionalProjectCapabilities: ["app.manage"]
|
|
10766
|
+
optionalProjectCapabilities: ["app.manage"],
|
|
10767
|
+
forceReview: options.requestGrant
|
|
11050
10768
|
});
|
|
11051
10769
|
const apps = (0, import_apps12.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
|
|
11052
10770
|
const existing = await apps.resolveApp(cfg.app.id);
|
|
11053
10771
|
if (existing) {
|
|
11054
10772
|
out.log(`app: ${cfg.app.id} already exists`);
|
|
11055
10773
|
} else {
|
|
11056
|
-
|
|
10774
|
+
try {
|
|
10775
|
+
await apps.createApp({ name: cfg.app.name, appId: cfg.app.id });
|
|
10776
|
+
} catch (error) {
|
|
10777
|
+
if (error instanceof import_apps12.AppsError && error.status === 403) {
|
|
10778
|
+
throw new Error(
|
|
10779
|
+
`app "${cfg.app.id}" does not exist, and this authenticated agent credential has no owner-reviewed app.manage bootstrap grant for that exact id. Run "odla-ai provision --request-grant --email <odla-account>" to open the review URL and continue; developer ownership alone is not agent authority`,
|
|
10780
|
+
{ cause: error }
|
|
10781
|
+
);
|
|
10782
|
+
}
|
|
10783
|
+
throw error;
|
|
10784
|
+
}
|
|
11057
10785
|
out.log(`app: created ${cfg.app.id}`);
|
|
11058
10786
|
}
|
|
11059
10787
|
for (const env of cfg.envs) {
|
|
@@ -11177,7 +10905,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
11177
10905
|
}
|
|
11178
10906
|
|
|
11179
10907
|
// src/record.ts
|
|
11180
|
-
var
|
|
10908
|
+
var import_node_fs15 = require("fs");
|
|
11181
10909
|
var import_node_process12 = __toESM(require("process"), 1);
|
|
11182
10910
|
|
|
11183
10911
|
// src/surface.ts
|
|
@@ -11357,14 +11085,14 @@ function recordInvocation(parsed) {
|
|
|
11357
11085
|
options: Object.entries(parsed.options).map(([name, value2]) => value2 === false ? `no-${name}` : name).sort()
|
|
11358
11086
|
};
|
|
11359
11087
|
if (!entry.path.length) return;
|
|
11360
|
-
(0,
|
|
11088
|
+
(0, import_node_fs15.appendFileSync)(file, `${JSON.stringify(entry)}
|
|
11361
11089
|
`);
|
|
11362
11090
|
} catch {
|
|
11363
11091
|
}
|
|
11364
11092
|
}
|
|
11365
11093
|
|
|
11366
11094
|
// src/runbook-actions.ts
|
|
11367
|
-
var
|
|
11095
|
+
var import_node_fs16 = require("fs");
|
|
11368
11096
|
|
|
11369
11097
|
// src/runbook-requires.ts
|
|
11370
11098
|
var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
|
|
@@ -11449,7 +11177,7 @@ async function bySlug(ctx, slug) {
|
|
|
11449
11177
|
function readBody(file, inline) {
|
|
11450
11178
|
if (inline !== void 0) return inline;
|
|
11451
11179
|
if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
|
|
11452
|
-
return (0,
|
|
11180
|
+
return (0, import_node_fs16.readFileSync)(file === "-" ? 0 : file, "utf8");
|
|
11453
11181
|
}
|
|
11454
11182
|
var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
|
|
11455
11183
|
async function runbookList(ctx, all, query) {
|
|
@@ -11541,7 +11269,7 @@ async function runbookRemove(ctx, slug) {
|
|
|
11541
11269
|
}
|
|
11542
11270
|
|
|
11543
11271
|
// src/runbook-import.ts
|
|
11544
|
-
var
|
|
11272
|
+
var import_node_fs17 = require("fs");
|
|
11545
11273
|
var import_node_path16 = require("path");
|
|
11546
11274
|
function parseRunbook(text2, slug) {
|
|
11547
11275
|
let rest = text2;
|
|
@@ -11567,12 +11295,12 @@ function parseRunbook(text2, slug) {
|
|
|
11567
11295
|
};
|
|
11568
11296
|
}
|
|
11569
11297
|
function readRunbookDir(dir) {
|
|
11570
|
-
if (!(0,
|
|
11571
|
-
const files = (0,
|
|
11298
|
+
if (!(0, import_node_fs17.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
|
|
11299
|
+
const files = (0, import_node_fs17.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
|
|
11572
11300
|
if (!files.length) throw new Error(`no .md files in ${dir}`);
|
|
11573
11301
|
return files.map((file) => {
|
|
11574
11302
|
const slug = (0, import_node_path16.basename)(file, ".md");
|
|
11575
|
-
const parsed = parseRunbook((0,
|
|
11303
|
+
const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0, import_node_path16.join)(dir, file), "utf8"), slug);
|
|
11576
11304
|
return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
|
|
11577
11305
|
});
|
|
11578
11306
|
}
|
|
@@ -11645,7 +11373,7 @@ async function upsert(ctx, r, visibility) {
|
|
|
11645
11373
|
|
|
11646
11374
|
// src/runbook-impact.ts
|
|
11647
11375
|
var import_node_child_process7 = require("child_process");
|
|
11648
|
-
var
|
|
11376
|
+
var import_node_fs18 = require("fs");
|
|
11649
11377
|
var import_node_path17 = require("path");
|
|
11650
11378
|
|
|
11651
11379
|
// src/runbook-impact-scan.ts
|
|
@@ -11816,9 +11544,9 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
|
|
|
11816
11544
|
function manifestLabeller(root) {
|
|
11817
11545
|
return (workspace) => {
|
|
11818
11546
|
const manifest = (0, import_node_path17.join)(root, workspace, "package.json");
|
|
11819
|
-
if (!(0,
|
|
11547
|
+
if (!(0, import_node_fs18.existsSync)(manifest)) return void 0;
|
|
11820
11548
|
try {
|
|
11821
|
-
const name = JSON.parse((0,
|
|
11549
|
+
const name = JSON.parse((0, import_node_fs18.readFileSync)(manifest, "utf8")).name;
|
|
11822
11550
|
return typeof name === "string" ? name : void 0;
|
|
11823
11551
|
} catch {
|
|
11824
11552
|
return void 0;
|
|
@@ -11856,7 +11584,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
|
|
|
11856
11584
|
return out;
|
|
11857
11585
|
}
|
|
11858
11586
|
var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
|
|
11859
|
-
function
|
|
11587
|
+
function report3(ctx, impacts) {
|
|
11860
11588
|
const covered = impacts.filter((i) => i.runbooks.length);
|
|
11861
11589
|
ctx.out.log(
|
|
11862
11590
|
`${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.`
|
|
@@ -11885,7 +11613,7 @@ function report4(ctx, impacts) {
|
|
|
11885
11613
|
async function runbookImpact(ctx, options, deps = {}) {
|
|
11886
11614
|
const cwd = deps.cwd ?? process.cwd();
|
|
11887
11615
|
const runGit = deps.runGit ?? gitRunner(cwd);
|
|
11888
|
-
const read3 = deps.readRepoFile ?? ((path) => (0,
|
|
11616
|
+
const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0, import_node_path17.join)(cwd, path), "utf8"));
|
|
11889
11617
|
const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
|
|
11890
11618
|
if (!surfaces.length) {
|
|
11891
11619
|
return ctx.out.log(
|
|
@@ -11894,7 +11622,7 @@ async function runbookImpact(ctx, options, deps = {}) {
|
|
|
11894
11622
|
}
|
|
11895
11623
|
const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
|
|
11896
11624
|
if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
|
|
11897
|
-
|
|
11625
|
+
report3(ctx, impacts);
|
|
11898
11626
|
}
|
|
11899
11627
|
|
|
11900
11628
|
// src/runbook-lint.ts
|
|
@@ -12018,7 +11746,7 @@ async function runbookComment(ctx, slug, body) {
|
|
|
12018
11746
|
|
|
12019
11747
|
// src/runbook-editor.ts
|
|
12020
11748
|
var import_node_child_process8 = require("child_process");
|
|
12021
|
-
var
|
|
11749
|
+
var import_node_fs19 = require("fs");
|
|
12022
11750
|
var import_node_os5 = require("os");
|
|
12023
11751
|
var import_node_path18 = require("path");
|
|
12024
11752
|
var import_node_process13 = __toESM(require("process"), 1);
|
|
@@ -12046,16 +11774,16 @@ function editText(initial, slug, deps = {}) {
|
|
|
12046
11774
|
);
|
|
12047
11775
|
if (!interactive())
|
|
12048
11776
|
throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
|
|
12049
|
-
const dir = (0,
|
|
11777
|
+
const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path18.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
|
|
12050
11778
|
const file = (0, import_node_path18.join)(dir, `${slug}.md`);
|
|
12051
11779
|
try {
|
|
12052
|
-
(0,
|
|
11780
|
+
(0, import_node_fs19.writeFileSync)(file, initial, { mode: 384 });
|
|
12053
11781
|
const code = defaultRunOrInjected(deps)(editor, file);
|
|
12054
11782
|
if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
|
|
12055
|
-
const edited = (0,
|
|
11783
|
+
const edited = (0, import_node_fs19.readFileSync)(file, "utf8");
|
|
12056
11784
|
return edited === initial ? null : edited;
|
|
12057
11785
|
} finally {
|
|
12058
|
-
(0,
|
|
11786
|
+
(0, import_node_fs19.rmSync)(dir, { recursive: true, force: true });
|
|
12059
11787
|
}
|
|
12060
11788
|
}
|
|
12061
11789
|
var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
|
|
@@ -12394,7 +12122,6 @@ async function runbookCommand(parsed, deps = {}) {
|
|
|
12394
12122
|
}
|
|
12395
12123
|
|
|
12396
12124
|
// src/security-command-context.ts
|
|
12397
|
-
var import_promises12 = require("readline/promises");
|
|
12398
12125
|
async function hostedSecurityContext(parsed, dependencies) {
|
|
12399
12126
|
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
12400
12127
|
const cfg = await loadProjectConfig(configPath);
|
|
@@ -12413,21 +12140,11 @@ async function hostedSecurityContext(parsed, dependencies) {
|
|
|
12413
12140
|
cfg,
|
|
12414
12141
|
{ configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
|
|
12415
12142
|
doFetch,
|
|
12416
|
-
stdout
|
|
12143
|
+
stdout,
|
|
12144
|
+
{ optionalProjectCapabilities: ["app.manage"] }
|
|
12417
12145
|
);
|
|
12418
12146
|
return { platform, token, appId: cfg.app.id, env, fetch: doFetch, stdout };
|
|
12419
12147
|
}
|
|
12420
|
-
async function interactiveConfirmation(message2, dependencies) {
|
|
12421
|
-
if (dependencies.confirm) return dependencies.confirm(message2);
|
|
12422
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
12423
|
-
const prompt = (0, import_promises12.createInterface)({ input: process.stdin, output: process.stdout });
|
|
12424
|
-
try {
|
|
12425
|
-
const answer = await prompt.question(`${message2} [y/N] `);
|
|
12426
|
-
return /^y(?:es)?$/i.test(answer.trim());
|
|
12427
|
-
} finally {
|
|
12428
|
-
prompt.close();
|
|
12429
|
-
}
|
|
12430
|
-
}
|
|
12431
12148
|
function requiredSecurityPositional(parsed, index, label) {
|
|
12432
12149
|
const value2 = parsed.positionals[index];
|
|
12433
12150
|
if (!value2) throw new Error(`${label} is required`);
|
|
@@ -12493,31 +12210,31 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
12493
12210
|
url.searchParams.set("job", job.jobId);
|
|
12494
12211
|
out.log(` Studio: ${url.toString()}`);
|
|
12495
12212
|
}
|
|
12496
|
-
function printHostedReport(out,
|
|
12497
|
-
out.log(`security report ${
|
|
12498
|
-
out.log(` coverage: ${
|
|
12499
|
-
out.log(` findings: confirmed=${
|
|
12500
|
-
out.log(` discovery: ${
|
|
12501
|
-
out.log(` validation: ${
|
|
12502
|
-
for (const finding of
|
|
12213
|
+
function printHostedReport(out, report4) {
|
|
12214
|
+
out.log(`security report ${report4.jobId}: ${report4.repository}@${report4.revision}`);
|
|
12215
|
+
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}`);
|
|
12216
|
+
out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates} rejected=${report4.metrics.rejected}`);
|
|
12217
|
+
out.log(` discovery: ${report4.provenance.discovery?.provider ?? "unknown"}/${report4.provenance.discovery?.model ?? "unknown"}`);
|
|
12218
|
+
out.log(` validation: ${report4.provenance.validation?.provider ?? "unknown"}/${report4.provenance.validation?.model ?? "unknown"} independent=${String(report4.provenance.independentValidation)}`);
|
|
12219
|
+
for (const finding of report4.findings) {
|
|
12503
12220
|
const location = finding.locations[0];
|
|
12504
12221
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
12505
12222
|
}
|
|
12506
|
-
for (const limitation of
|
|
12223
|
+
for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
|
|
12507
12224
|
}
|
|
12508
|
-
function enforceHostedReportGate(
|
|
12225
|
+
function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
|
|
12509
12226
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
12510
12227
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
12511
12228
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
12512
12229
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
12513
|
-
const confirmed =
|
|
12514
|
-
const leads = failOnCandidates ?
|
|
12515
|
-
const incomplete =
|
|
12230
|
+
const confirmed = report4.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
|
|
12231
|
+
const leads = failOnCandidates ? report4.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
|
|
12232
|
+
const incomplete = report4.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
|
|
12516
12233
|
if (confirmed.length || leads.length || incomplete) {
|
|
12517
|
-
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${
|
|
12234
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report4.coverageStatus}` : ""}`);
|
|
12518
12235
|
}
|
|
12519
12236
|
if (emitSuccess) {
|
|
12520
|
-
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${
|
|
12237
|
+
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report4.coverageStatus}. This is not proof that the application is secure.`);
|
|
12521
12238
|
}
|
|
12522
12239
|
}
|
|
12523
12240
|
function printHostedSecurityPlanRoute(out, label, route2) {
|
|
@@ -12600,17 +12317,17 @@ async function runHostedSecurity(options) {
|
|
|
12600
12317
|
allowNetwork: false
|
|
12601
12318
|
}
|
|
12602
12319
|
});
|
|
12603
|
-
const
|
|
12604
|
-
await (0, import_node3.writeSecurityArtifacts)(output,
|
|
12605
|
-
const reportDigest = await (0, import_security.securityFingerprint)(
|
|
12320
|
+
const report4 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
12321
|
+
await (0, import_node3.writeSecurityArtifacts)(output, report4);
|
|
12322
|
+
const reportDigest = await (0, import_security.securityFingerprint)(report4);
|
|
12606
12323
|
await hosted.complete({
|
|
12607
12324
|
reportDigest,
|
|
12608
|
-
coverageStatus:
|
|
12609
|
-
confirmed:
|
|
12610
|
-
candidates:
|
|
12325
|
+
coverageStatus: report4.coverageStatus,
|
|
12326
|
+
confirmed: report4.metrics.confirmed,
|
|
12327
|
+
candidates: report4.metrics.candidates
|
|
12611
12328
|
}, { signal: options.signal });
|
|
12612
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run,
|
|
12613
|
-
return Object.freeze({ report:
|
|
12329
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report4, output);
|
|
12330
|
+
return Object.freeze({ report: report4, run: hosted.run, output });
|
|
12614
12331
|
}
|
|
12615
12332
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
12616
12333
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
@@ -12636,14 +12353,14 @@ function profileFor(name, maxHuntTasks) {
|
|
|
12636
12353
|
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
12637
12354
|
return { ...profile, maxHuntTasks };
|
|
12638
12355
|
}
|
|
12639
|
-
function printSummary(out, appId, env, run,
|
|
12640
|
-
const complete =
|
|
12356
|
+
function printSummary(out, appId, env, run, report4, output) {
|
|
12357
|
+
const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
|
|
12641
12358
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
12642
12359
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
12643
12360
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
12644
|
-
out.log(` coverage: ${
|
|
12645
|
-
if (
|
|
12646
|
-
out.log(` findings: confirmed=${
|
|
12361
|
+
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}`);
|
|
12362
|
+
if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
|
|
12363
|
+
out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
|
|
12647
12364
|
out.log(` report: ${(0, import_node_path19.resolve)(output, "REPORT.md")}`);
|
|
12648
12365
|
}
|
|
12649
12366
|
function formatBudget(usage) {
|
|
@@ -12890,13 +12607,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
12890
12607
|
}
|
|
12891
12608
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
12892
12609
|
}
|
|
12893
|
-
const
|
|
12610
|
+
const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
12894
12611
|
if (parsed.options.json === true) {
|
|
12895
|
-
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report:
|
|
12612
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report4 }, null, 2));
|
|
12896
12613
|
} else {
|
|
12897
|
-
printHostedReport(context.stdout,
|
|
12614
|
+
printHostedReport(context.stdout, report4);
|
|
12898
12615
|
}
|
|
12899
|
-
enforceHostedReportGate(
|
|
12616
|
+
enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
|
|
12900
12617
|
}
|
|
12901
12618
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
12902
12619
|
if (parsed.options.source === true) {
|
|
@@ -12957,19 +12674,20 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
12957
12674
|
cfg,
|
|
12958
12675
|
{ configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
|
|
12959
12676
|
doFetch,
|
|
12960
|
-
out
|
|
12677
|
+
out,
|
|
12678
|
+
{ optionalProjectCapabilities: ["app.manage"] }
|
|
12961
12679
|
);
|
|
12962
12680
|
}
|
|
12963
12681
|
});
|
|
12964
12682
|
enforceLocalGate(result.report, parsed);
|
|
12965
12683
|
}
|
|
12966
|
-
function enforceLocalGate(
|
|
12684
|
+
function enforceLocalGate(report4, parsed) {
|
|
12967
12685
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
12968
12686
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
12969
12687
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
12970
|
-
const confirmed = (0, import_security2.findingsAtOrAbove)(
|
|
12971
|
-
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(
|
|
12972
|
-
const incomplete =
|
|
12688
|
+
const confirmed = (0, import_security2.findingsAtOrAbove)(report4, failOn);
|
|
12689
|
+
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report4, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
12690
|
+
const incomplete = report4.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
12973
12691
|
if (confirmed.length || leads.length || incomplete) {
|
|
12974
12692
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
12975
12693
|
}
|
|
@@ -13008,9 +12726,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
13008
12726
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
13009
12727
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
13010
12728
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
13011
|
-
const
|
|
13012
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
13013
|
-
else printHostedReport(context.stdout,
|
|
12729
|
+
const report4 = await getHostedSecurityReport({ ...context, jobId });
|
|
12730
|
+
if (parsed.options.json === true) context.stdout.log(JSON.stringify(report4, null, 2));
|
|
12731
|
+
else printHostedReport(context.stdout, report4);
|
|
13014
12732
|
return;
|
|
13015
12733
|
}
|
|
13016
12734
|
if (sub !== "run") {
|
|
@@ -13024,35 +12742,24 @@ async function githubSecurityCommand(parsed, dependencies) {
|
|
|
13024
12742
|
const action2 = parsed.positionals[2];
|
|
13025
12743
|
if (action2 === "disconnect") {
|
|
13026
12744
|
assertArgs(parsed, ["config", "env", "platform", "source", "email", "open", "yes"], 3);
|
|
13027
|
-
const context2 = await hostedSecurityContext(parsed, dependencies);
|
|
13028
12745
|
const sourceId = requiredString(parsed.options.source, "--source");
|
|
13029
|
-
|
|
13030
|
-
|
|
13031
|
-
|
|
12746
|
+
return requireStudioHuman(
|
|
12747
|
+
stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
12748
|
+
`disconnecting GitHub security source ${sourceId}`,
|
|
12749
|
+
"security",
|
|
12750
|
+
stringOpt(parsed.options.env)
|
|
13032
12751
|
);
|
|
13033
|
-
if (!confirmed) {
|
|
13034
|
-
throw new Error("GitHub source disconnect cancelled; pass --yes in a non-interactive shell");
|
|
13035
|
-
}
|
|
13036
|
-
await disconnectGitHubSecuritySource({ ...context2, sourceId });
|
|
13037
|
-
context2.stdout.log(`github: disconnected ${sourceId} from ${context2.appId}/${context2.env}`);
|
|
13038
|
-
return;
|
|
13039
12752
|
}
|
|
13040
12753
|
if (action2 !== "connect") {
|
|
13041
12754
|
throw new Error('unknown security github command. Try "odla-ai security github connect".');
|
|
13042
12755
|
}
|
|
13043
12756
|
assertArgs(parsed, ["config", "env", "platform", "repo", "email", "open"], 3);
|
|
13044
|
-
|
|
13045
|
-
|
|
13046
|
-
|
|
13047
|
-
|
|
13048
|
-
|
|
13049
|
-
|
|
13050
|
-
openInstallUrl: dependencies.openUrl ?? openUrl,
|
|
13051
|
-
wait: dependencies.pollWait,
|
|
13052
|
-
stdout: context.stdout
|
|
13053
|
-
});
|
|
13054
|
-
context.stdout.log(`github: connected ${connection.repository ?? repository ?? "app repository"} (${connection.sourceId ?? "source pending"})`);
|
|
13055
|
-
context.stdout.log("github: odla.ai stores the installation; no PAT or GitHub token is written locally");
|
|
12757
|
+
await requireStudioHuman(
|
|
12758
|
+
stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
12759
|
+
"connecting the GitHub security repository",
|
|
12760
|
+
"security",
|
|
12761
|
+
stringOpt(parsed.options.env)
|
|
12762
|
+
);
|
|
13056
12763
|
}
|
|
13057
12764
|
async function listSecuritySources(parsed, dependencies) {
|
|
13058
12765
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 2);
|
|
@@ -13193,6 +12900,7 @@ async function provisionCommand(parsed, dependencies) {
|
|
|
13193
12900
|
"write-credentials",
|
|
13194
12901
|
"write-dev-vars",
|
|
13195
12902
|
"token",
|
|
12903
|
+
"request-grant",
|
|
13196
12904
|
"email",
|
|
13197
12905
|
"open",
|
|
13198
12906
|
"wait",
|
|
@@ -13208,6 +12916,7 @@ async function provisionCommand(parsed, dependencies) {
|
|
|
13208
12916
|
writeCredentials: parsed.options["write-credentials"] !== false,
|
|
13209
12917
|
writeDevVars: typeof writeDevVars2 === "string" ? writeDevVars2 : writeDevVars2 === true,
|
|
13210
12918
|
token: stringOpt(parsed.options.token),
|
|
12919
|
+
requestGrant: parsed.options["request-grant"] === true,
|
|
13211
12920
|
email: stringOpt(parsed.options.email),
|
|
13212
12921
|
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
13213
12922
|
wait: numberOpt(parsed.options.wait, "--wait"),
|