@odla-ai/cli 0.27.11 → 0.27.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.cjs CHANGED
@@ -1668,7 +1668,7 @@ async function agentCommand(parsed, deps = {}) {
1668
1668
  if (action2 !== "jobs" && action2 !== "retry") {
1669
1669
  throw new Error(`unknown agent action "${action2 ?? ""}". Try "odla-ai agent jobs --json".`);
1670
1670
  }
1671
- assertArgs(parsed, ["config", "env", "state", "limit", "json", "token", "email"], action2 === "jobs" ? 2 : 3);
1671
+ assertArgs(parsed, ["config", "env", "state", "limit", "json", "token"], action2 === "jobs" ? 2 : 3);
1672
1672
  if (action2 === "retry" && (parsed.options.state !== void 0 || parsed.options.limit !== void 0)) {
1673
1673
  throw new Error('--state and --limit are supported only by "agent jobs"');
1674
1674
  }
@@ -1676,17 +1676,17 @@ async function agentCommand(parsed, deps = {}) {
1676
1676
  const { env, tenant } = resolveTenant(cfg, stringOpt(parsed.options.env));
1677
1677
  const doFetch = deps.fetch ?? fetch;
1678
1678
  const out = deps.stdout ?? console;
1679
- const credential2 = await getDeveloperToken(
1680
- cfg,
1681
- {
1682
- configPath: cfg.configPath,
1683
- token: stringOpt(parsed.options.token),
1684
- email: stringOpt(parsed.options.email),
1685
- open: false
1686
- },
1687
- doFetch,
1688
- out
1689
- );
1679
+ const credential2 = stringOpt(parsed.options.token) ?? readCredentials(cfg.local.credentialsFile)?.envs[env]?.dbKey;
1680
+ if (!credential2) {
1681
+ throw new Error(
1682
+ `no ${env} app credential found; run \`odla-ai provision --write-dev-vars --yes\` or pass --token <ODLA_API_KEY>`
1683
+ );
1684
+ }
1685
+ if (credential2.startsWith("odla_dev_")) {
1686
+ throw new Error(
1687
+ "agent job administration requires an app credential (ODLA_API_KEY / odla_sk_\u2026), not a developer device token"
1688
+ );
1689
+ }
1690
1690
  const base = `${cfg.dbEndpoint}/app/${encodeURIComponent(tenant)}/admin/agent-jobs`;
1691
1691
  const headers = { authorization: `Bearer ${credential2}` };
1692
1692
  if (action2 === "retry") {
@@ -1742,51 +1742,23 @@ function errorMessage(body) {
1742
1742
  return "request failed";
1743
1743
  }
1744
1744
 
1745
+ // src/human-session.ts
1746
+ async function requireStudioHuman(configPath, action2, destination = "app", env) {
1747
+ const cfg = await loadProjectConfig(configPath);
1748
+ const appEnv = env && cfg.envs.includes(env) ? env : cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
1749
+ const path = destination === "app" ? `/studio/apps/${encodeURIComponent(cfg.app.id)}/${encodeURIComponent(appEnv)}/settings/app` : `/studio/apps/${encodeURIComponent(cfg.app.id)}/${encodeURIComponent(appEnv)}/${destination}`;
1750
+ throw new Error(
1751
+ `human_session_required: ${action2} requires the signed-in owner in Studio. A CLI device token is an agent credential, so another approval or retry cannot work. ${new URL(path, cfg.platformUrl).href}`
1752
+ );
1753
+ }
1754
+
1745
1755
  // src/app-export.ts
1746
- var import_node_fs7 = require("fs");
1747
- var import_node_stream = require("stream");
1748
- var import_promises = require("stream/promises");
1749
1756
  async function appExport(options) {
1750
- const cfg = await loadProjectConfig(options.configPath);
1751
- const out = options.stdout ?? console;
1752
- const doFetch = options.fetch ?? fetch;
1753
- const { tenant } = resolveTenant(cfg, options.env);
1754
- const token = await getDeveloperToken(
1755
- cfg,
1756
- { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
1757
- doFetch,
1758
- out
1759
- );
1760
- const auth = { authorization: `Bearer ${token}` };
1761
- const base = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}`;
1762
- if (options.fresh) {
1763
- const res = await doFetch(`${base}/export`, { method: "POST", headers: auth });
1764
- const body = await res.json().catch(() => ({}));
1765
- if (!res.ok) throw new Error(`export failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? res.status}`);
1766
- }
1767
- const list = await doFetch(`${base}/backups`, { headers: auth });
1768
- if (!list.ok) throw new Error(`couldn't list backups (${list.status})`);
1769
- const { backups } = await list.json();
1770
- const newest = backups[0];
1771
- if (!newest) {
1772
- throw new Error(
1773
- `${tenant} has no backups yet \u2014 run \`odla-ai app export --fresh\` to take one now (nightly snapshots appear after the first day with writes)`
1774
- );
1775
- }
1776
- const download = await doFetch(`${base}/backups/${newest.id}/download`, { headers: auth });
1777
- if (!download.ok || !download.body) throw new Error(`download failed (${download.status})`);
1778
- const file = options.out ?? `${tenant}-${new Date(newest.created_at).toISOString().slice(0, 10)}-tx${newest.max_tx}.jsonl.gz`;
1779
- await (0, import_promises.pipeline)(import_node_stream.Readable.fromWeb(download.body), (0, import_node_fs7.createWriteStream)(file));
1780
- if (options.json) out.log(JSON.stringify({ file, backup: newest }, null, 2));
1781
- else {
1782
- out.log(`${tenant}: wrote ${file} (${newest.bytes} bytes, ${newest.kind} snapshot at tx ${newest.max_tx})`);
1783
- out.log(`sha256 ${download.headers.get("x-odla-sha256") ?? newest.sha256}`);
1784
- }
1785
- return { file, backup: newest };
1757
+ return requireStudioHuman(options.configPath, "database export", "database", options.env);
1786
1758
  }
1787
1759
 
1788
1760
  // src/app-import.ts
1789
- var import_node_fs8 = require("fs");
1761
+ var import_node_fs7 = require("fs");
1790
1762
  var import_import = require("@odla-ai/db/import");
1791
1763
  function chooseIdMode(options, rows) {
1792
1764
  const chosen = [options.idField && "field", options.key && "key", options.generateIds && "generate"].filter(Boolean);
@@ -1803,9 +1775,8 @@ async function appImport(options) {
1803
1775
  const cfg = await loadProjectConfig(options.configPath);
1804
1776
  const out = options.stdout ?? console;
1805
1777
  const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
1806
- const doFetch = options.fetch ?? fetch;
1807
1778
  const { tenant } = resolveTenant(cfg, options.env);
1808
- const text2 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs8.readFileSync)(0, "utf8")))() : (0, import_node_fs8.readFileSync)(options.file, "utf8");
1779
+ const text2 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs7.readFileSync)(0, "utf8")))() : (0, import_node_fs7.readFileSync)(options.file, "utf8");
1809
1780
  const { format, sources } = (0, import_import.parseImport)(text2, options.ns);
1810
1781
  if (format === "namespace-map" && options.ns) {
1811
1782
  throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
@@ -1831,100 +1802,18 @@ ${detail}${more}`);
1831
1802
  if (options.json) out.log(JSON.stringify(result, null, 2));
1832
1803
  return result;
1833
1804
  }
1834
- const token = await getDeveloperToken(
1835
- cfg,
1836
- { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
1837
- doFetch,
1838
- out
1839
- );
1840
- const runId = crypto.randomUUID();
1841
- const url = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}/transact`;
1842
- for (const chunk of chunks) {
1843
- const res = await doFetch(url, {
1844
- method: "POST",
1845
- headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
1846
- body: JSON.stringify({ mutationId: (0, import_import.importMutationId)(runId, chunk.index), ops: chunk.ops })
1847
- });
1848
- const body = await res.json().catch(() => ({}));
1849
- if (!res.ok) {
1850
- const code = body.error?.code ? ` (${body.error.code})` : "";
1851
- throw new Error(
1852
- `chunk ${chunk.index + 1} of ${chunks.length} failed${code}: ${body.error?.message ?? res.status}. ${result.committed} row(s) already committed; fix the input and re-run to import the rest.`
1853
- );
1854
- }
1855
- result.committed += chunk.ops.length;
1856
- if (typeof body.txId === "number") result.txIds.push(body.txId);
1857
- if (body.duplicate) result.duplicate++;
1858
- }
1859
- if (options.json) out.log(JSON.stringify(result, null, 2));
1860
- else {
1861
- const dup = result.duplicate > 0 ? ` (${result.duplicate} chunk(s) were already applied)` : "";
1862
- out.log(`${tenant}: upserted ${result.committed} row(s) in ${chunks.length} transaction(s)${dup}`);
1863
- }
1864
- return result;
1805
+ return requireStudioHuman(options.configPath, "database import", "database", options.env);
1865
1806
  }
1866
1807
 
1867
1808
  // src/app-owners.ts
1868
- var sink = (options) => options.stdout ?? console;
1869
- async function ownersRequest(method, suffix, options, body) {
1870
- const cfg = await loadProjectConfig(options.configPath);
1871
- const doFetch = options.fetch ?? fetch;
1872
- const token = await getDeveloperToken(
1873
- cfg,
1874
- { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
1875
- doFetch,
1876
- sink(options)
1877
- );
1878
- const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/owners${suffix}`, {
1879
- method,
1880
- headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
1881
- body: body === void 0 ? void 0 : JSON.stringify(body)
1882
- });
1883
- const data = await res.json().catch(() => ({}));
1884
- if (!res.ok) {
1885
- throw new Error(
1886
- `owners ${method} failed${data.error?.code ? ` (${data.error.code})` : ""}: ` + (data.error?.message ?? `registry returned ${res.status}`)
1887
- );
1888
- }
1889
- return data.owners ?? [];
1890
- }
1891
- function report(options, owners, headline) {
1892
- const out = sink(options);
1893
- if (options.json === true) {
1894
- out.log(JSON.stringify(owners, null, 2));
1895
- return;
1896
- }
1897
- if (headline) out.log(headline);
1898
- out.log(`owners (${owners.length}):`);
1899
- for (const o of owners) {
1900
- const name = o.email?.trim() || "Unnamed member";
1901
- out.log(
1902
- ` ${o.primary ? "\u2605" : "\xB7"} ${name} [${o.ownerId}]${o.primary ? " (primary)" : ""}`
1903
- );
1904
- }
1905
- }
1906
1809
  async function ownersList(options) {
1907
- report(options, await ownersRequest("GET", "", options));
1810
+ await requireStudioHuman(options.configPath, "listing app owners", "app");
1908
1811
  }
1909
1812
  async function ownersAdd(email, options) {
1910
- const owners = await ownersRequest("POST", "", options, { email });
1911
- report(
1912
- options,
1913
- owners,
1914
- `added ${email} as a co-owner \u2014 they share full access. They can now run \`odla-ai provision\` to mint their own credentials (no secret sharing).`
1915
- );
1813
+ await requireStudioHuman(options.configPath, `adding ${email} as an app owner`, "app");
1916
1814
  }
1917
1815
  async function ownersRemove(target, options) {
1918
- let ownerId = target;
1919
- if (target.includes("@")) {
1920
- const owners2 = await ownersRequest("GET", "", options);
1921
- const match = owners2.find((o) => o.email?.toLowerCase() === target.toLowerCase());
1922
- if (!match) throw new Error(`no co-owner with email ${target}`);
1923
- if (match.primary) throw new Error("can't remove the primary owner");
1924
- ownerId = match.ownerId;
1925
- }
1926
- const owners = await ownersRequest("DELETE", `/${encodeURIComponent(ownerId)}`, options);
1927
- report(options, owners, `removed ${target}`);
1816
+ await requireStudioHuman(options.configPath, `removing ${target} as an app owner`, "app");
1928
1817
  }
1929
1818
  async function appOwnersCommand(parsed, dependencies = {}) {
1930
1819
  const sub = parsed.positionals[2] ?? "list";
@@ -1956,35 +1845,9 @@ async function appOwnersCommand(parsed, dependencies = {}) {
1956
1845
 
1957
1846
  // src/app-rename.ts
1958
1847
  async function appRename(name, options) {
1959
- const out = options.stdout ?? console;
1960
1848
  const trimmed = name.trim();
1961
1849
  if (!trimmed) throw new Error('"app rename" needs a name \u2014 try `odla-ai app rename "Acme Storefront"`.');
1962
- const cfg = await loadProjectConfig(options.configPath);
1963
- const doFetch = options.fetch ?? fetch;
1964
- const token = await getDeveloperToken(
1965
- cfg,
1966
- { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
1967
- doFetch,
1968
- out
1969
- );
1970
- const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/name`, {
1971
- method: "PUT",
1972
- headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
1973
- body: JSON.stringify({ name: trimmed })
1974
- });
1975
- const data = await res.json().catch(() => ({}));
1976
- if (!res.ok || !data.app) {
1977
- throw new Error(
1978
- `rename failed${data.error?.code ? ` (${data.error.code})` : ""}: ` + (data.error?.message ?? `registry returned ${res.status}`)
1979
- );
1980
- }
1981
- if (options.json === true) {
1982
- out.log(JSON.stringify(data.app, null, 2));
1983
- return;
1984
- }
1985
- out.log(`renamed ${data.app.appId} \u2192 "${data.app.name}"`);
1986
- out.log("The app id is unchanged, so credentials, tenants, and URLs keep working.");
1987
- out.log(`Update the "name" in ${cfg.configPath} to match.`);
1850
+ await requireStudioHuman(options.configPath, `renaming the app to "${trimmed}"`, "app");
1988
1851
  }
1989
1852
  async function appRenameCommand(parsed, dependencies = {}) {
1990
1853
  assertArgs(parsed, ["config", "token", "email", "json"], parsed.positionals.length);
@@ -2000,172 +1863,23 @@ async function appRenameCommand(parsed, dependencies = {}) {
2000
1863
  }
2001
1864
 
2002
1865
  // src/app-transfer.ts
2003
- function endpointsFor(verb, tenants) {
2004
- const up = { source: tenants.sandbox, target: tenants.live, from: "dev" };
2005
- const down = { source: tenants.live, target: tenants.sandbox, from: "prod" };
2006
- if (verb === "refresh-sandbox") return { ...down, mode: "refresh" };
2007
- return { ...up, mode: "cutover" };
2008
- }
2009
- async function api(cfg, token, path, init, doFetch) {
2010
- const res = await doFetch(`${cfg.dbEndpoint}${path}`, {
2011
- ...init,
2012
- headers: { authorization: `Bearer ${token}`, "content-type": "application/json", ...init?.headers }
2013
- });
2014
- return { status: res.status, body: await res.json().catch(() => ({})) };
2015
- }
2016
- function describe(side) {
2017
- if (!side.exists) return "not provisioned";
2018
- if (side.maxTx === 0) return "empty \u2014 never written";
2019
- const ns = side.namespaces.length === 1 ? "1 namespace" : `${side.namespaces.length} namespaces`;
2020
- const identity = side.identityRows > 0 ? `, ${side.identityRows} identity row(s)` : "";
2021
- return `${ns} \xB7 ${side.triples} value(s) \xB7 tx ${side.maxTx}${identity}`;
2022
- }
2023
- function printPlan(out, verb, pre, opts) {
2024
- const arrow = verb === "refresh-sandbox" ? "live \u2192 sandbox" : "sandbox \u2192 live";
2025
- out.log(`${verb} (${arrow})`);
2026
- out.log(` from ${pre.source.tenant} ${describe(pre.source)}`);
2027
- out.log(` to ${pre.target.tenant} ${describe(pre.target)}`);
2028
- if (verb === "promote") {
2029
- out.log(" moves schema, rules and gates only \u2014 no rows are copied or removed");
2030
- } else {
2031
- out.log(` ${pre.target.tenant} is REPLACED by ${pre.source.tenant}`);
2032
- out.log(` stays put: ${pre.staysPut.join(", ")}`);
2033
- const identity = opts.includeIdentity ? "INCLUDED (--include-identity)" : `left behind: ${pre.excluded.join(", ")}`;
2034
- out.log(` identity: ${identity}`);
2035
- out.log(` files: ${opts.includeFiles ? "copied (--include-files)" : "not copied"}`);
2036
- }
2037
- for (const blocker of pre.blockers) out.log(` \u2716 ${blocker.code}: ${blocker.message}`);
2038
- }
2039
1866
  async function appTransfer(options) {
2040
1867
  const cfg = await loadProjectConfig(options.configPath);
2041
- const out = options.stdout ?? console;
2042
- const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
2043
- const doFetch = options.fetch ?? fetch;
2044
- const tenants = bothTenants(cfg);
2045
- const route2 = endpointsFor(options.verb, tenants);
2046
- const token = await getDeveloperToken(
2047
- cfg,
2048
- { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
2049
- doFetch,
2050
- out
2051
- );
2052
- const pre = await api(
2053
- cfg,
2054
- token,
2055
- `/admin/apps/${encodeURIComponent(route2.target)}/copy-preflight?from=${route2.from}`,
2056
- void 0,
2057
- doFetch
2058
- );
2059
- if (pre.status !== 200) {
2060
- throw new Error(`pre-flight failed${pre.body.error?.code ? ` (${pre.body.error.code})` : ""}: ${pre.body.error?.message ?? pre.status}`);
2061
- }
2062
- const plan = pre.body;
2063
- printPlan({ log: say }, options.verb, plan, options);
2064
- if (options.verb === "go-live" && !plan.targetEmpty) {
2065
- throw new Error(
2066
- `${route2.target} already has data \u2014 go-live has already happened. Use \`odla-ai app promote --yes\` to push schema and rules, or \`odla-ai app refresh-sandbox --yes\` to pull live back down.`
2067
- );
2068
- }
2069
- if (plan.blockers.length > 0) throw new Error(`cannot ${options.verb}: ${plan.blockers.map((b) => b.message).join("; ")}`);
2070
- if (options.dryRun || options.yes !== true) {
2071
- say(`nothing written (${options.dryRun ? "--dry-run" : "no --yes"})`);
2072
- if (options.json) out.log(JSON.stringify(plan, null, 2));
2073
- return { ok: true, plan };
2074
- }
2075
- if (options.verb === "promote") {
2076
- const res2 = await api(
2077
- cfg,
2078
- token,
2079
- `/admin/apps/${encodeURIComponent(route2.target)}/promote-definitions`,
2080
- { method: "POST", body: JSON.stringify({ from: "dev" }) },
2081
- doFetch
2082
- );
2083
- if (res2.status !== 200) throw new Error(`promote failed${res2.body.error?.code ? ` (${res2.body.error.code})` : ""}: ${res2.body.error?.message ?? res2.status}`);
2084
- if (options.json) out.log(JSON.stringify(res2.body, null, 2));
2085
- else say(`${route2.target}: promoted ${(res2.body.applied ?? []).join(", ")}`);
2086
- return { ok: true, plan, result: res2.body };
2087
- }
2088
- const res = await api(
2089
- cfg,
2090
- token,
2091
- `/admin/apps/${encodeURIComponent(route2.target)}/copy-db`,
2092
- {
2093
- method: "POST",
2094
- body: JSON.stringify({
2095
- from: route2.from,
2096
- mode: route2.mode,
2097
- ...options.includeIdentity ? { includeUsers: true } : {},
2098
- ...options.includeFiles ? { includeFiles: true } : {}
2099
- })
2100
- },
2101
- doFetch
2102
- );
2103
- if (res.status !== 200) throw new Error(`${options.verb} failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
2104
- say(`${route2.target}: replaced from ${route2.source} (tx ${res.body.destination?.maxTx}, epoch ${res.body.destination?.epoch}) \u2014 connected clients resync automatically`);
2105
- if (options.includeFiles) await copyFiles(cfg, token, route2, doFetch, out);
2106
- if (options.json) out.log(JSON.stringify(res.body, null, 2));
2107
- return { ok: true, plan, result: res.body };
2108
- }
2109
- async function copyFiles(cfg, token, route2, doFetch, out) {
2110
- for (let attempt = 1; attempt <= 20; attempt++) {
2111
- const res = await api(
2112
- cfg,
2113
- token,
2114
- `/admin/apps/${encodeURIComponent(route2.target)}/copy-files`,
2115
- { method: "POST", body: JSON.stringify({ from: route2.from }) },
2116
- doFetch
2117
- );
2118
- if (res.status === 200) {
2119
- out.log(`${route2.target}: files copied`);
2120
- return;
2121
- }
2122
- if (!res.body.error?.retry) {
2123
- throw new Error(`file copy failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
2124
- }
2125
- out.log(` files: bounded at attempt ${attempt}, resuming\u2026`);
2126
- }
2127
- throw new Error("file copy did not finish within 20 rounds \u2014 re-run to continue where it left off");
1868
+ bothTenants(cfg);
1869
+ return requireStudioHuman(options.configPath, `app ${options.verb}`, "database");
2128
1870
  }
2129
1871
 
2130
1872
  // src/app-lifecycle.ts
2131
- async function lifecycleCall(action2, options) {
2132
- const cfg = await loadProjectConfig(options.configPath);
2133
- const out = options.stdout ?? console;
2134
- const doFetch = options.fetch ?? fetch;
2135
- const token = await getDeveloperToken(
2136
- cfg,
2137
- { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
2138
- doFetch,
2139
- out
2140
- );
2141
- const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/${action2}`, {
2142
- method: "POST",
2143
- headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }
2144
- });
2145
- const body = await res.json().catch(() => ({}));
2146
- if (!res.ok || !body.ok) {
2147
- throw new Error(`${action2} failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? `registry returned ${res.status}`}`);
2148
- }
2149
- return body;
2150
- }
2151
1873
  async function appArchive(options) {
2152
1874
  if (options.yes !== true) {
2153
1875
  throw new Error(
2154
1876
  "app archive suspends EVERY environment: API keys stop working and all services refuse requests until restored. All data is retained. Pass --yes to proceed."
2155
1877
  );
2156
1878
  }
2157
- const out = options.stdout ?? console;
2158
- const body = await lifecycleCall("archive", options);
2159
- if (options.json) out.log(JSON.stringify(body, null, 2));
2160
- else if (body.operation?.state === "noop") out.log(`${body.app?.appId}: already archived`);
2161
- else out.log(`${body.app?.appId}: archived \u2014 data retained; run \`odla-ai app restore\` (or use Studio) to bring it back`);
1879
+ await requireStudioHuman(options.configPath, "app archive", "app");
2162
1880
  }
2163
1881
  async function appRestore(options) {
2164
- const out = options.stdout ?? console;
2165
- const body = await lifecycleCall("restore", options);
2166
- if (options.json) out.log(JSON.stringify(body, null, 2));
2167
- else if (body.operation?.state === "noop") out.log(`${body.app?.appId}: already active`);
2168
- else out.log(`${body.app?.appId}: restored \u2014 every service's data plane is live again`);
1882
+ await requireStudioHuman(options.configPath, "app restore", "app");
2169
1883
  }
2170
1884
  async function appCommand(parsed, dependencies = {}) {
2171
1885
  const sub = parsed.positionals[1];
@@ -2251,7 +1965,7 @@ async function appCommand(parsed, dependencies = {}) {
2251
1965
  }
2252
1966
 
2253
1967
  // src/brand-command.ts
2254
- var import_promises2 = require("fs/promises");
1968
+ var import_promises = require("fs/promises");
2255
1969
  var import_node_path7 = require("path");
2256
1970
 
2257
1971
  // src/brand-design-unpack.ts
@@ -2354,7 +2068,7 @@ function describeUnpack(result, outDir) {
2354
2068
  // src/brand-command.ts
2355
2069
  var USAGE = "usage: odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]";
2356
2070
  async function readBundle(source, deps) {
2357
- if (source !== "-") return (0, import_promises2.readFile)((0, import_node_path7.resolve)(source), "utf8");
2071
+ if (source !== "-") return (0, import_promises.readFile)((0, import_node_path7.resolve)(source), "utf8");
2358
2072
  const readStdin = deps.readStdin;
2359
2073
  if (!readStdin) throw new Error("reading a bundle from stdin is not supported here");
2360
2074
  return readStdin();
@@ -2362,8 +2076,8 @@ async function readBundle(source, deps) {
2362
2076
  async function writeAll(result, outDir) {
2363
2077
  for (const file of result.files) {
2364
2078
  const target = (0, import_node_path7.resolve)(outDir, file.path);
2365
- await (0, import_promises2.mkdir)((0, import_node_path7.dirname)(target), { recursive: true });
2366
- await (0, import_promises2.writeFile)(target, file.bytes);
2079
+ await (0, import_promises.mkdir)((0, import_node_path7.dirname)(target), { recursive: true });
2080
+ await (0, import_promises.writeFile)(target, file.bytes);
2367
2081
  }
2368
2082
  }
2369
2083
  async function designUnpack(parsed, deps) {
@@ -2523,12 +2237,6 @@ async function pollCalendarConnection(ctx, attemptId) {
2523
2237
  ctx.env
2524
2238
  );
2525
2239
  }
2526
- async function requestCalendarDisconnect(ctx) {
2527
- return parseCalendarStatus(
2528
- await calendarJson(ctx, "/disconnect", { method: "POST", body: JSON.stringify({ purge: false }) }),
2529
- ctx.env
2530
- );
2531
- }
2532
2240
  function parseCalendarStatus(raw, env) {
2533
2241
  const outer = wrapped(raw, "calendar");
2534
2242
  const value2 = record(outer.attempt) ?? record(outer.status) ?? outer;
@@ -2712,10 +2420,7 @@ async function applyCalendarBookingPage(ctx, bookingPageUrl, out) {
2712
2420
  }
2713
2421
  async function calendarDisconnect(options) {
2714
2422
  if (!options.yes) throw new Error("calendar disconnect requires --yes");
2715
- const { ctx, out } = await lifecycleContext(options);
2716
- const status = await requestCalendarDisconnect(ctx);
2717
- out.log(`${ctx.env}: calendar disconnected; no calendar data was stored`);
2718
- return status;
2423
+ return requireStudioHuman(options.configPath, "calendar disconnect", "calendar", options.env);
2719
2424
  }
2720
2425
  async function ensureCalendarConnected(ctx, options) {
2721
2426
  const out = options.stdout ?? console;
@@ -2929,9 +2634,9 @@ var import_apps6 = require("@odla-ai/apps");
2929
2634
  var import_node_path8 = require("path");
2930
2635
 
2931
2636
  // src/version.ts
2932
- var import_node_fs9 = require("fs");
2637
+ var import_node_fs8 = require("fs");
2933
2638
  function cliVersion() {
2934
- const pkg = JSON.parse((0, import_node_fs9.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
2639
+ const pkg = JSON.parse((0, import_node_fs8.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
2935
2640
  return pkg.version ?? "unknown";
2936
2641
  }
2937
2642
 
@@ -2947,7 +2652,7 @@ var ConfigOperationCommandError = class extends Error {
2947
2652
 
2948
2653
  // src/config-operation-validate.ts
2949
2654
  var import_apps3 = require("@odla-ai/apps");
2950
- var import_node_fs10 = require("fs");
2655
+ var import_node_fs9 = require("fs");
2951
2656
 
2952
2657
  // src/config-reconcile-digest.ts
2953
2658
  var import_node_crypto2 = require("crypto");
@@ -2983,7 +2688,7 @@ var SERVICE = /^[a-z][a-z0-9-]{0,39}$/;
2983
2688
  function readPlan(path) {
2984
2689
  let value2;
2985
2690
  try {
2986
- const raw = (0, import_node_fs10.readFileSync)(path, "utf8");
2691
+ const raw = (0, import_node_fs9.readFileSync)(path, "utf8");
2987
2692
  if (Buffer.byteLength(raw) > 128 * 1024) throw new Error("plan exceeds 128 KiB");
2988
2693
  value2 = JSON.parse(raw);
2989
2694
  } catch (error) {
@@ -3696,7 +3401,7 @@ async function configPlan(options) {
3696
3401
  apply,
3697
3402
  nextActions: planNextActions(reconciliation, options.configPath)
3698
3403
  };
3699
- printPlan2(document2, options);
3404
+ printPlan(document2, options);
3700
3405
  return document2;
3701
3406
  }
3702
3407
  async function inspectConfig(options) {
@@ -3736,7 +3441,7 @@ function printDiff(document2, options) {
3736
3441
  printDifferences(out, document2);
3737
3442
  printNext(out, document2.nextActions);
3738
3443
  }
3739
- function printPlan2(document2, options) {
3444
+ function printPlan(document2, options) {
3740
3445
  const out = options.stdout ?? console;
3741
3446
  if (options.json) {
3742
3447
  out.log(JSON.stringify(document2, null, 2));
@@ -3839,12 +3544,12 @@ function quoteArg2(value2) {
3839
3544
 
3840
3545
  // src/doctor-checks.ts
3841
3546
  var import_node_child_process3 = require("child_process");
3842
- var import_node_fs12 = require("fs");
3547
+ var import_node_fs11 = require("fs");
3843
3548
  var import_node_path11 = require("path");
3844
3549
 
3845
3550
  // src/wrangler.ts
3846
3551
  var import_node_child_process2 = require("child_process");
3847
- var import_node_fs11 = require("fs");
3552
+ var import_node_fs10 = require("fs");
3848
3553
  var import_node_path10 = require("path");
3849
3554
  var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
3850
3555
  const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
@@ -3860,14 +3565,14 @@ var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]
3860
3565
  function findWranglerConfig(rootDir) {
3861
3566
  for (const name of WRANGLER_CONFIG_FILES) {
3862
3567
  const path = (0, import_node_path10.join)(rootDir, name);
3863
- if ((0, import_node_fs11.existsSync)(path)) return path;
3568
+ if ((0, import_node_fs10.existsSync)(path)) return path;
3864
3569
  }
3865
3570
  return null;
3866
3571
  }
3867
3572
  function readWranglerConfig(path) {
3868
3573
  if (path.endsWith(".toml")) return null;
3869
3574
  try {
3870
- return JSON.parse(stripJsonComments((0, import_node_fs11.readFileSync)(path, "utf8")));
3575
+ return JSON.parse(stripJsonComments((0, import_node_fs10.readFileSync)(path, "utf8")));
3871
3576
  } catch {
3872
3577
  return null;
3873
3578
  }
@@ -3975,7 +3680,7 @@ function wranglerWarnings(rootDir) {
3975
3680
  const dir = (0, import_node_path11.resolve)(rootDir, assets.directory);
3976
3681
  if (dir === (0, import_node_path11.resolve)(rootDir)) {
3977
3682
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
3978
- } else if ((0, import_node_fs12.existsSync)((0, import_node_path11.join)(dir, "node_modules"))) {
3683
+ } else if ((0, import_node_fs11.existsSync)((0, import_node_path11.join)(dir, "node_modules"))) {
3979
3684
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
3980
3685
  }
3981
3686
  }
@@ -4011,12 +3716,12 @@ function o11yProjectWarnings(rootDir) {
4011
3716
  return warnings;
4012
3717
  }
4013
3718
  const main = typeof config.main === "string" ? (0, import_node_path11.resolve)(rootDir, config.main) : null;
4014
- if (!main || !(0, import_node_fs12.existsSync)(main)) {
3719
+ if (!main || !(0, import_node_fs11.existsSync)(main)) {
4015
3720
  warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
4016
3721
  } else {
4017
3722
  let source = "";
4018
3723
  try {
4019
- source = (0, import_node_fs12.readFileSync)(main, "utf8");
3724
+ source = (0, import_node_fs11.readFileSync)(main, "utf8");
4020
3725
  } catch {
4021
3726
  }
4022
3727
  if (!/\bwithObservability\b/.test(source)) {
@@ -4040,7 +3745,7 @@ function calendarProjectWarnings(rootDir) {
4040
3745
  }
4041
3746
  function readPackageJson(rootDir) {
4042
3747
  try {
4043
- return JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path11.join)(rootDir, "package.json"), "utf8"));
3748
+ return JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path11.join)(rootDir, "package.json"), "utf8"));
4044
3749
  } catch {
4045
3750
  return null;
4046
3751
  }
@@ -4269,14 +3974,14 @@ function harnessOption(value2, flag) {
4269
3974
  }
4270
3975
 
4271
3976
  // src/init.ts
4272
- var import_node_fs13 = require("fs");
3977
+ var import_node_fs12 = require("fs");
4273
3978
  var import_node_path12 = require("path");
4274
3979
  var import_apps9 = require("@odla-ai/apps");
4275
3980
  function initProject(options) {
4276
3981
  const out = options.stdout ?? console;
4277
3982
  const rootDir = (0, import_node_path12.resolve)(options.rootDir ?? process.cwd());
4278
3983
  const configPath = (0, import_node_path12.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
4279
- if ((0, import_node_fs13.existsSync)(configPath) && !options.force) {
3984
+ if ((0, import_node_fs12.existsSync)(configPath) && !options.force) {
4280
3985
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
4281
3986
  }
4282
3987
  if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
@@ -4292,10 +3997,10 @@ function initProject(options) {
4292
3997
  }
4293
3998
  }
4294
3999
  const aiProvider = options.aiProvider;
4295
- (0, import_node_fs13.mkdirSync)((0, import_node_path12.dirname)(configPath), { recursive: true });
4296
- (0, import_node_fs13.mkdirSync)((0, import_node_path12.resolve)(rootDir, "src/odla"), { recursive: true });
4297
- (0, import_node_fs13.mkdirSync)((0, import_node_path12.resolve)(rootDir, ".odla"), { recursive: true });
4298
- (0, import_node_fs13.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
4000
+ (0, import_node_fs12.mkdirSync)((0, import_node_path12.dirname)(configPath), { recursive: true });
4001
+ (0, import_node_fs12.mkdirSync)((0, import_node_path12.resolve)(rootDir, "src/odla"), { recursive: true });
4002
+ (0, import_node_fs12.mkdirSync)((0, import_node_path12.resolve)(rootDir, ".odla"), { recursive: true });
4003
+ (0, import_node_fs12.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
4299
4004
  writeIfMissing((0, import_node_path12.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
4300
4005
  writeIfMissing((0, import_node_path12.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
4301
4006
  ensureGitignore(rootDir);
@@ -4304,8 +4009,8 @@ function initProject(options) {
4304
4009
  out.log("updated .gitignore for local odla credentials");
4305
4010
  }
4306
4011
  function writeIfMissing(path, text2) {
4307
- if ((0, import_node_fs13.existsSync)(path)) return;
4308
- (0, import_node_fs13.writeFileSync)(path, text2);
4012
+ if ((0, import_node_fs12.existsSync)(path)) return;
4013
+ (0, import_node_fs12.writeFileSync)(path, text2);
4309
4014
  }
4310
4015
  function configTemplate(input) {
4311
4016
  const calendar = input.services.includes("calendar") ? ` calendar: {
@@ -4512,7 +4217,9 @@ async function secretsSetClerkKey(options) {
4512
4217
  if (value2.startsWith("sk_live_") && !PROD_ENV_NAMES2.has(options.env) && !options.yes) {
4513
4218
  throw new Error(`refusing to store an sk_live_ Clerk key for "${options.env}" without --yes (live users would sync into a non-prod tenant)`);
4514
4219
  }
4515
- const token = await getDeveloperToken(cfg, options, doFetch, out);
4220
+ const token = await getDeveloperToken(cfg, options, doFetch, out, {
4221
+ optionalProjectCapabilities: ["app.manage"]
4222
+ });
4516
4223
  const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/clerk-secret`, {
4517
4224
  method: "POST",
4518
4225
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
@@ -4542,7 +4249,7 @@ async function resolveVaultWrite(options) {
4542
4249
  }
4543
4250
 
4544
4251
  // src/skill.ts
4545
- var import_node_fs14 = require("fs");
4252
+ var import_node_fs13 = require("fs");
4546
4253
  var import_node_os2 = require("os");
4547
4254
  var import_node_path13 = require("path");
4548
4255
  var import_node_url2 = require("url");
@@ -4639,7 +4346,7 @@ function installSkill(options = {}) {
4639
4346
  plans.set(target, { target, content: content2, boundary, managedMerge });
4640
4347
  };
4641
4348
  const planSkillTree = (targetDir2, boundary = root) => {
4642
- for (const rel of files) plan((0, import_node_path13.join)(targetDir2, rel), (0, import_node_fs14.readFileSync)((0, import_node_path13.join)(sourceDir, rel), "utf8"), false, boundary);
4349
+ for (const rel of files) plan((0, import_node_path13.join)(targetDir2, rel), (0, import_node_fs13.readFileSync)((0, import_node_path13.join)(sourceDir, rel), "utf8"), false, boundary);
4643
4350
  };
4644
4351
  let targetDir;
4645
4352
  if (options.global) {
@@ -4659,7 +4366,7 @@ function installSkill(options = {}) {
4659
4366
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
4660
4367
  if (harnesses.includes("claude")) {
4661
4368
  for (const skill of skillNames(files)) {
4662
- const canonical = (0, import_node_fs14.readFileSync)((0, import_node_path13.join)(sourceDir, skill, "SKILL.md"), "utf8");
4369
+ const canonical = (0, import_node_fs13.readFileSync)((0, import_node_path13.join)(sourceDir, skill, "SKILL.md"), "utf8");
4663
4370
  plan((0, import_node_path13.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
4664
4371
  }
4665
4372
  rememberTarget("claude", claudeRoot);
@@ -4694,11 +4401,11 @@ function installSkill(options = {}) {
4694
4401
  conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
4695
4402
  continue;
4696
4403
  }
4697
- if (!(0, import_node_fs14.existsSync)(file.target)) {
4404
+ if (!(0, import_node_fs13.existsSync)(file.target)) {
4698
4405
  writtenPaths.add(file.target);
4699
4406
  continue;
4700
4407
  }
4701
- const current = (0, import_node_fs14.readFileSync)(file.target, "utf8");
4408
+ const current = (0, import_node_fs13.readFileSync)(file.target, "utf8");
4702
4409
  if (current === file.content) {
4703
4410
  unchangedPaths.add(file.target);
4704
4411
  } else if (file.managedMerge || options.force) {
@@ -4715,9 +4422,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
4715
4422
  );
4716
4423
  }
4717
4424
  for (const file of plans.values()) {
4718
- if (!(0, import_node_fs14.existsSync)(file.target) || (0, import_node_fs14.readFileSync)(file.target, "utf8") !== file.content) {
4719
- (0, import_node_fs14.mkdirSync)((0, import_node_path13.dirname)(file.target), { recursive: true });
4720
- (0, import_node_fs14.writeFileSync)(file.target, file.content);
4425
+ if (!(0, import_node_fs13.existsSync)(file.target) || (0, import_node_fs13.readFileSync)(file.target, "utf8") !== file.content) {
4426
+ (0, import_node_fs13.mkdirSync)((0, import_node_path13.dirname)(file.target), { recursive: true });
4427
+ (0, import_node_fs13.writeFileSync)(file.target, file.content);
4721
4428
  }
4722
4429
  }
4723
4430
  const skills = skillNames(files);
@@ -4758,9 +4465,9 @@ function normalizeHarnesses(values, global) {
4758
4465
  function managedFileContent(path, block, force, boundary) {
4759
4466
  const symlink = symlinkedComponent(boundary, path);
4760
4467
  if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
4761
- if (!(0, import_node_fs14.existsSync)(path)) return `${block}
4468
+ if (!(0, import_node_fs13.existsSync)(path)) return `${block}
4762
4469
  `;
4763
- const current = (0, import_node_fs14.readFileSync)(path, "utf8");
4470
+ const current = (0, import_node_fs13.readFileSync)(path, "utf8");
4764
4471
  const start = "<!-- odla-ai agent setup:start -->";
4765
4472
  const end = "<!-- odla-ai agent setup:end -->";
4766
4473
  const startAt = current.indexOf(start);
@@ -4789,7 +4496,7 @@ function symlinkedComponent(boundary, target) {
4789
4496
  for (const part of rel.split(import_node_path13.sep).filter(Boolean)) {
4790
4497
  current = (0, import_node_path13.join)(current, part);
4791
4498
  try {
4792
- if ((0, import_node_fs14.lstatSync)(current).isSymbolicLink()) return current;
4499
+ if ((0, import_node_fs13.lstatSync)(current).isSymbolicLink()) return current;
4793
4500
  } catch (error) {
4794
4501
  if (error.code !== "ENOENT") throw error;
4795
4502
  }
@@ -4800,10 +4507,10 @@ function skillNames(files) {
4800
4507
  return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
4801
4508
  }
4802
4509
  function listFiles(dir) {
4803
- if (!(0, import_node_fs14.existsSync)(dir)) return [];
4510
+ if (!(0, import_node_fs13.existsSync)(dir)) return [];
4804
4511
  const results = [];
4805
4512
  const walk = (current) => {
4806
- for (const entry of (0, import_node_fs14.readdirSync)(current, { withFileTypes: true })) {
4513
+ for (const entry of (0, import_node_fs13.readdirSync)(current, { withFileTypes: true })) {
4807
4514
  const path = (0, import_node_path13.join)(current, entry.name);
4808
4515
  if (entry.isDirectory()) walk(path);
4809
4516
  else results.push((0, import_node_path13.relative)(dir, path));
@@ -5120,7 +4827,7 @@ async function projectCommand(command, parsed, deps) {
5120
4827
  }
5121
4828
 
5122
4829
  // src/code-connect.ts
5123
- var import_node_fs15 = require("fs");
4830
+ var import_node_fs14 = require("fs");
5124
4831
  var import_node_os4 = require("os");
5125
4832
  var import_node_path15 = require("path");
5126
4833
 
@@ -5208,15 +4915,15 @@ function encodeAgentInput(message2) {
5208
4915
  // ../harness/dist/chunk-PHXQH4YM.js
5209
4916
  var import_child_process = require("child_process");
5210
4917
  var import_fs = require("fs");
5211
- var import_promises3 = require("fs/promises");
4918
+ var import_promises2 = require("fs/promises");
5212
4919
  var import_path = require("path");
5213
4920
  var import_process = require("process");
5214
- var import_promises4 = require("fs/promises");
4921
+ var import_promises3 = require("fs/promises");
5215
4922
  var import_os = require("os");
5216
4923
  var import_path2 = require("path");
5217
4924
  var import_child_process2 = require("child_process");
5218
4925
  var import_path3 = require("path");
5219
- var import_promises5 = require("fs/promises");
4926
+ var import_promises4 = require("fs/promises");
5220
4927
  var import_os2 = require("os");
5221
4928
  var import_path4 = require("path");
5222
4929
  var import_child_process3 = require("child_process");
@@ -5227,7 +4934,7 @@ function assertPinnedImage(image) {
5227
4934
  async function commandAvailable(engine) {
5228
4935
  for (const directory of (process.env.PATH ?? "").split(import_path.delimiter).filter(Boolean)) {
5229
4936
  try {
5230
- await (0, import_promises3.access)((0, import_path.join)(directory, engine), import_fs.constants.X_OK);
4937
+ await (0, import_promises2.access)((0, import_path.join)(directory, engine), import_fs.constants.X_OK);
5231
4938
  return true;
5232
4939
  } catch {
5233
4940
  }
@@ -5513,7 +5220,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
5513
5220
  }
5514
5221
  async function materializeGitTree(source, commitSha, options = {}) {
5515
5222
  if (!/^[0-9a-f]{40}$/.test(commitSha)) throw new TypeError("Git tree requires an exact commit SHA");
5516
- const sourceDir = await (0, import_promises4.realpath)((0, import_path2.resolve)(source));
5223
+ const sourceDir = await (0, import_promises3.realpath)((0, import_path2.resolve)(source));
5517
5224
  const maxFiles = options.maxFiles ?? 2e4;
5518
5225
  const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
5519
5226
  const inventory = (await gitOutput(sourceDir, ["ls-tree", "-rz", commitSha], 16 * 1024 * 1024)).toString("utf8").split("\0").filter(Boolean);
@@ -5522,9 +5229,9 @@ async function materializeGitTree(source, commitSha, options = {}) {
5522
5229
  return match && allowedWorkspacePath(match[3]) ? [{ mode: match[1], hash: match[2], path: match[3] }] : [];
5523
5230
  });
5524
5231
  if (entries.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
5525
- const root = await (0, import_promises4.mkdtemp)((0, import_path2.join)(options.tempRoot ?? (0, import_os.tmpdir)(), "odla-git-tree-"));
5232
+ const root = await (0, import_promises3.mkdtemp)((0, import_path2.join)(options.tempRoot ?? (0, import_os.tmpdir)(), "odla-git-tree-"));
5526
5233
  const targetRoot = (0, import_path2.join)(root, "source");
5527
- await (0, import_promises4.mkdir)(targetRoot);
5234
+ await (0, import_promises3.mkdir)(targetRoot);
5528
5235
  let byteCount = 0;
5529
5236
  try {
5530
5237
  const blobs = await gitBlobs(sourceDir, entries, maxBytes);
@@ -5534,18 +5241,18 @@ async function materializeGitTree(source, commitSha, options = {}) {
5534
5241
  if (byteCount > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
5535
5242
  const target = (0, import_path2.resolve)(targetRoot, entry.path);
5536
5243
  if (!target.startsWith(`${(0, import_path2.resolve)(targetRoot)}${import_path2.sep}`)) throw new TypeError("Git tree path escapes workspace");
5537
- await (0, import_promises4.mkdir)((0, import_path2.resolve)(target, ".."), { recursive: true });
5538
- await (0, import_promises4.writeFile)(target, content2, { flag: "wx", mode: entry.mode === "100755" ? 493 : 420 });
5244
+ await (0, import_promises3.mkdir)((0, import_path2.resolve)(target, ".."), { recursive: true });
5245
+ await (0, import_promises3.writeFile)(target, content2, { flag: "wx", mode: entry.mode === "100755" ? 493 : 420 });
5539
5246
  }
5540
5247
  return {
5541
5248
  root,
5542
5249
  sourceDir: targetRoot,
5543
5250
  fileCount: entries.length,
5544
5251
  byteCount,
5545
- cleanup: () => (0, import_promises4.rm)(root, { recursive: true, force: true })
5252
+ cleanup: () => (0, import_promises3.rm)(root, { recursive: true, force: true })
5546
5253
  };
5547
5254
  } catch (error) {
5548
- await (0, import_promises4.rm)(root, { recursive: true, force: true });
5255
+ await (0, import_promises3.rm)(root, { recursive: true, force: true });
5549
5256
  throw error;
5550
5257
  }
5551
5258
  }
@@ -5553,7 +5260,7 @@ async function sourceFiles(sourceDir, maxFiles, maxBytes) {
5553
5260
  const files = [];
5554
5261
  let bytes = 0;
5555
5262
  const walk = async (dir) => {
5556
- for (const entry of await (0, import_promises5.readdir)(dir, { withFileTypes: true })) {
5263
+ for (const entry of await (0, import_promises4.readdir)(dir, { withFileTypes: true })) {
5557
5264
  if (entry.isDirectory() && SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
5558
5265
  if (!entry.isDirectory() && SECRET_WORKSPACE_FILE.test(entry.name)) continue;
5559
5266
  const path = (0, import_path4.join)(dir, entry.name);
@@ -5563,7 +5270,7 @@ async function sourceFiles(sourceDir, maxFiles, maxBytes) {
5563
5270
  continue;
5564
5271
  }
5565
5272
  if (!entry.isFile()) continue;
5566
- const metadata2 = await (0, import_promises5.stat)(path);
5273
+ const metadata2 = await (0, import_promises4.stat)(path);
5567
5274
  bytes += metadata2.size;
5568
5275
  if (files.length + 1 > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
5569
5276
  if (bytes > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
@@ -5612,7 +5319,7 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
5612
5319
  if (!source.startsWith(`${root}${import_path4.sep}`)) throw new TypeError("git file path escapes workspace");
5613
5320
  let metadata2;
5614
5321
  try {
5615
- metadata2 = await (0, import_promises5.lstat)(source);
5322
+ metadata2 = await (0, import_promises4.lstat)(source);
5616
5323
  } catch (error) {
5617
5324
  if (error.code === "ENOENT") continue;
5618
5325
  throw error;
@@ -5627,9 +5334,9 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
5627
5334
  async function copyTree(files, destination) {
5628
5335
  for (const file of files) {
5629
5336
  const target = (0, import_path4.join)(destination, file.relativePath);
5630
- await (0, import_promises5.mkdir)((0, import_path4.resolve)(target, ".."), { recursive: true });
5631
- await (0, import_promises5.copyFile)(file.source, target);
5632
- await (0, import_promises5.chmod)(target, file.mode);
5337
+ await (0, import_promises4.mkdir)((0, import_path4.resolve)(target, ".."), { recursive: true });
5338
+ await (0, import_promises4.copyFile)(file.source, target);
5339
+ await (0, import_promises4.chmod)(target, file.mode);
5633
5340
  }
5634
5341
  }
5635
5342
  async function captureGitDiff(root, maxBytes) {
@@ -5666,13 +5373,13 @@ async function captureGitDiff(root, maxBytes) {
5666
5373
  return Buffer.concat(stdout).toString("utf8").replaceAll("a/baseline/", "a/").replaceAll("a/workspace/", "a/").replaceAll("b/baseline/", "b/").replaceAll("b/workspace/", "b/").replaceAll("--- a/baseline", "--- a").replaceAll("+++ b/workspace", "+++ b");
5667
5374
  }
5668
5375
  async function stageWorkspace(source, options = {}) {
5669
- const sourceDir = await (0, import_promises5.realpath)((0, import_path4.resolve)(source));
5670
- const sourceStat = await (0, import_promises5.stat)(sourceDir);
5376
+ const sourceDir = await (0, import_promises4.realpath)((0, import_path4.resolve)(source));
5377
+ const sourceStat = await (0, import_promises4.stat)(sourceDir);
5671
5378
  if (!sourceStat.isDirectory()) throw new TypeError("workspace source must be a directory");
5672
- const root = await (0, import_promises5.mkdtemp)((0, import_path4.join)(options.tempRoot ?? (0, import_os2.tmpdir)(), "odla-harness-"));
5379
+ const root = await (0, import_promises4.mkdtemp)((0, import_path4.join)(options.tempRoot ?? (0, import_os2.tmpdir)(), "odla-harness-"));
5673
5380
  const baselineDir = (0, import_path4.join)(root, "baseline");
5674
5381
  const workspaceDir = (0, import_path4.join)(root, "workspace");
5675
- await Promise.all([(0, import_promises5.mkdir)(baselineDir), (0, import_promises5.mkdir)(workspaceDir)]);
5382
+ await Promise.all([(0, import_promises4.mkdir)(baselineDir), (0, import_promises4.mkdir)(workspaceDir)]);
5676
5383
  try {
5677
5384
  const maxFiles = options.maxFiles ?? 2e4;
5678
5385
  const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
@@ -5685,26 +5392,26 @@ async function stageWorkspace(source, options = {}) {
5685
5392
  fileCount: files.length,
5686
5393
  byteCount: files.reduce((sum, file) => sum + file.bytes, 0),
5687
5394
  patch: (maxBytes2) => captureGitDiff(root, maxBytes2),
5688
- cleanup: () => (0, import_promises5.rm)(root, { recursive: true, force: true })
5395
+ cleanup: () => (0, import_promises4.rm)(root, { recursive: true, force: true })
5689
5396
  };
5690
5397
  } catch (error) {
5691
- await (0, import_promises5.rm)(root, { recursive: true, force: true });
5398
+ await (0, import_promises4.rm)(root, { recursive: true, force: true });
5692
5399
  throw error;
5693
5400
  }
5694
5401
  }
5695
5402
  async function stageWorkspacePair(baselineSource, workspaceSource, options = {}) {
5696
- const baselineDirSource = await (0, import_promises5.realpath)((0, import_path4.resolve)(baselineSource));
5697
- const workspaceDirSource = await (0, import_promises5.realpath)((0, import_path4.resolve)(workspaceSource));
5403
+ const baselineDirSource = await (0, import_promises4.realpath)((0, import_path4.resolve)(baselineSource));
5404
+ const workspaceDirSource = await (0, import_promises4.realpath)((0, import_path4.resolve)(workspaceSource));
5698
5405
  const maxFiles = options.maxFiles ?? 2e4;
5699
5406
  const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
5700
5407
  const [baselineFiles, workspaceFiles] = await Promise.all([
5701
5408
  sourceFiles(baselineDirSource, maxFiles, maxBytes),
5702
5409
  sourceFiles(workspaceDirSource, maxFiles, maxBytes)
5703
5410
  ]);
5704
- const root = await (0, import_promises5.mkdtemp)((0, import_path4.join)(options.tempRoot ?? (0, import_os2.tmpdir)(), "odla-harness-"));
5411
+ const root = await (0, import_promises4.mkdtemp)((0, import_path4.join)(options.tempRoot ?? (0, import_os2.tmpdir)(), "odla-harness-"));
5705
5412
  const baselineDir = (0, import_path4.join)(root, "baseline");
5706
5413
  const workspaceDir = (0, import_path4.join)(root, "workspace");
5707
- await Promise.all([(0, import_promises5.mkdir)(baselineDir), (0, import_promises5.mkdir)(workspaceDir)]);
5414
+ await Promise.all([(0, import_promises4.mkdir)(baselineDir), (0, import_promises4.mkdir)(workspaceDir)]);
5708
5415
  try {
5709
5416
  await Promise.all([copyTree(baselineFiles, baselineDir), copyTree(workspaceFiles, workspaceDir)]);
5710
5417
  return {
@@ -5714,17 +5421,17 @@ async function stageWorkspacePair(baselineSource, workspaceSource, options = {})
5714
5421
  fileCount: workspaceFiles.length,
5715
5422
  byteCount: workspaceFiles.reduce((sum, file) => sum + file.bytes, 0),
5716
5423
  patch: (maxPatchBytes) => captureGitDiff(root, maxPatchBytes),
5717
- cleanup: () => (0, import_promises5.rm)(root, { recursive: true, force: true })
5424
+ cleanup: () => (0, import_promises4.rm)(root, { recursive: true, force: true })
5718
5425
  };
5719
5426
  } catch (error) {
5720
- await (0, import_promises5.rm)(root, { recursive: true, force: true });
5427
+ await (0, import_promises4.rm)(root, { recursive: true, force: true });
5721
5428
  throw error;
5722
5429
  }
5723
5430
  }
5724
5431
 
5725
5432
  // ../harness/dist/chunk-GMVZ4LZH.js
5726
5433
  var import_crypto = require("crypto");
5727
- var import_promises6 = require("fs/promises");
5434
+ var import_promises5 = require("fs/promises");
5728
5435
  var import_path5 = require("path");
5729
5436
 
5730
5437
  // ../camel/dist/chunk-7FHPOQVP.js
@@ -6061,19 +5768,19 @@ function validateSnapshot(snapshot, limits) {
6061
5768
 
6062
5769
  // ../harness/dist/chunk-GMVZ4LZH.js
6063
5770
  var import_child_process4 = require("child_process");
6064
- var import_promises7 = require("fs/promises");
5771
+ var import_promises6 = require("fs/promises");
6065
5772
  var import_path6 = require("path");
6066
5773
  var import_child_process5 = require("child_process");
6067
5774
  var import_process2 = require("process");
6068
5775
  var import_crypto2 = require("crypto");
6069
5776
  var import_crypto3 = require("crypto");
6070
5777
  var import_fs2 = require("fs");
6071
- var import_promises8 = require("fs/promises");
5778
+ var import_promises7 = require("fs/promises");
6072
5779
  var import_path7 = require("path");
6073
- var import_promises9 = require("fs/promises");
5780
+ var import_promises8 = require("fs/promises");
6074
5781
  var import_os3 = require("os");
6075
5782
  var import_path8 = require("path");
6076
- var import_promises10 = require("fs/promises");
5783
+ var import_promises9 = require("fs/promises");
6077
5784
  var import_path9 = require("path");
6078
5785
 
6079
5786
  // ../camel/dist/chunk-4EIRFS3A.js
@@ -6365,7 +6072,7 @@ var import_crypto4 = require("crypto");
6365
6072
  async function digestStagedWorkspace(root, limits) {
6366
6073
  const files = [];
6367
6074
  const walk = async (directory) => {
6368
- const entries = await (0, import_promises6.readdir)(directory, { withFileTypes: true });
6075
+ const entries = await (0, import_promises5.readdir)(directory, { withFileTypes: true });
6369
6076
  for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
6370
6077
  if (entry.isSymbolicLink()) throw new TypeError("workspace digest refuses symbolic links");
6371
6078
  const target = (0, import_path5.resolve)(directory, entry.name);
@@ -6380,7 +6087,7 @@ async function digestStagedWorkspace(root, limits) {
6380
6087
  const hash = (0, import_crypto.createHash)("sha256");
6381
6088
  let bytes = 0;
6382
6089
  for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
6383
- const content2 = await (0, import_promises6.readFile)(file.target);
6090
+ const content2 = await (0, import_promises5.readFile)(file.target);
6384
6091
  bytes += Buffer.byteLength(file.path) + content2.byteLength;
6385
6092
  if (bytes > limits.maxBytes) throw new TypeError("workspace digest exceeds its byte bound");
6386
6093
  hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content2.byteLength}:`);
@@ -6706,7 +6413,7 @@ async function applyCodePatch(workspaceDir, patch2, paths) {
6706
6413
  await gitApply(workspaceDir, patch2, false);
6707
6414
  for (const path of paths) {
6708
6415
  try {
6709
- const info = await (0, import_promises7.lstat)(resolveCodePath(workspaceDir, path));
6416
+ const info = await (0, import_promises6.lstat)(resolveCodePath(workspaceDir, path));
6710
6417
  if (info.isSymbolicLink() || !info.isFile() && !info.isDirectory()) {
6711
6418
  throw new TypeError("patch created a non-regular workspace entry");
6712
6419
  }
@@ -6997,7 +6704,7 @@ async function inspectArtifacts(workspaceDir, recipe2) {
6997
6704
  for (const artifact of recipe2.expectedArtifacts ?? []) {
6998
6705
  try {
6999
6706
  const path = (0, import_path7.join)(workspaceDir, artifact.path);
7000
- const info = await (0, import_promises8.lstat)(path);
6707
+ const info = await (0, import_promises7.lstat)(path);
7001
6708
  if (!info.isFile() || info.isSymbolicLink()) {
7002
6709
  receipts.push({ artifactId: artifact.id, status: "invalid", bytes: null, digest: null });
7003
6710
  } else if (info.size > artifact.maximumBytes) {
@@ -7178,9 +6885,9 @@ var RESERVED2 = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_mod
7178
6885
  var SECRET2 = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
7179
6886
  async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os3.tmpdir)()) {
7180
6887
  if (!snapshot.files.length || snapshot.files.length > 1e4) throw new TypeError("Code source file count is invalid");
7181
- const root = await (0, import_promises9.mkdtemp)((0, import_path8.join)(tempRoot, "odla-code-source-"));
6888
+ const root = await (0, import_promises8.mkdtemp)((0, import_path8.join)(tempRoot, "odla-code-source-"));
7182
6889
  const sourceDir = (0, import_path8.join)(root, "source");
7183
- await (0, import_promises9.mkdir)(sourceDir);
6890
+ await (0, import_promises8.mkdir)(sourceDir);
7184
6891
  const seen = /* @__PURE__ */ new Set();
7185
6892
  let bytes = 0;
7186
6893
  try {
@@ -7192,8 +6899,8 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os3.
7192
6899
  if (bytes > 16 * 1024 * 1024) throw new TypeError("Code source exceeds its byte bound");
7193
6900
  const target = (0, import_path8.resolve)(sourceDir, file.path);
7194
6901
  if (!target.startsWith(`${(0, import_path8.resolve)(sourceDir)}${import_path8.sep}`)) throw new TypeError("Code source path escapes its root");
7195
- await (0, import_promises9.mkdir)((0, import_path8.dirname)(target), { recursive: true });
7196
- await (0, import_promises9.writeFile)(target, file.content, { flag: "wx", mode: 420 });
6902
+ await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
6903
+ await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 420 });
7197
6904
  }
7198
6905
  for (const reference of snapshot.references ?? []) {
7199
6906
  validateAlias(reference.alias);
@@ -7207,13 +6914,13 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os3.
7207
6914
  if (bytes > 80 * 1024 * 1024) throw new TypeError("Code source set exceeds its byte bound");
7208
6915
  const target = (0, import_path8.resolve)(sourceDir, path);
7209
6916
  if (!target.startsWith(`${(0, import_path8.resolve)(sourceDir)}${import_path8.sep}`)) throw new TypeError("Code reference path escapes its root");
7210
- await (0, import_promises9.mkdir)((0, import_path8.dirname)(target), { recursive: true });
7211
- await (0, import_promises9.writeFile)(target, file.content, { flag: "wx", mode: 292 });
6917
+ await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
6918
+ await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 292 });
7212
6919
  }
7213
6920
  }
7214
- return { sourceDir, cleanup: () => (0, import_promises9.rm)(root, { recursive: true, force: true }) };
6921
+ return { sourceDir, cleanup: () => (0, import_promises8.rm)(root, { recursive: true, force: true }) };
7215
6922
  } catch (cause) {
7216
- await (0, import_promises9.rm)(root, { recursive: true, force: true });
6923
+ await (0, import_promises8.rm)(root, { recursive: true, force: true });
7217
6924
  throw cause;
7218
6925
  }
7219
6926
  }
@@ -7234,8 +6941,8 @@ async function attachCodeRuntimeReferences(workspace, references) {
7234
6941
  for (const root of [workspace.baselineDir, workspace.workspaceDir]) {
7235
6942
  const target = (0, import_path8.resolve)(root, path);
7236
6943
  if (!target.startsWith(`${(0, import_path8.resolve)(root)}${import_path8.sep}`)) throw new TypeError("Code reference path escapes its root");
7237
- await (0, import_promises9.mkdir)((0, import_path8.dirname)(target), { recursive: true });
7238
- await (0, import_promises9.writeFile)(target, file.content, { flag: "wx", mode: 292 });
6944
+ await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
6945
+ await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 292 });
7239
6946
  }
7240
6947
  }
7241
6948
  }
@@ -7421,11 +7128,11 @@ async function read(context, request2, options, policy) {
7421
7128
  const allowed = await policy.read(policyContext(context, request2, options, { paths, path, startLine, endLine }));
7422
7129
  if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
7423
7130
  const target = resolveCodePath(context.workspaceDir, path);
7424
- const info = await (0, import_promises10.stat)(target);
7131
+ const info = await (0, import_promises9.stat)(target);
7425
7132
  if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
7426
7133
  throw new TypeError("file is not a bounded regular source file");
7427
7134
  }
7428
- const source = await (0, import_promises10.readFile)(target);
7135
+ const source = await (0, import_promises9.readFile)(target);
7429
7136
  if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
7430
7137
  const lines = source.toString("utf8").split("\n");
7431
7138
  const content2 = lines.slice(startLine - 1, endLine).join("\n");
@@ -7502,7 +7209,7 @@ function policyContext(context, request2, options, extra) {
7502
7209
  async function registeredFiles(root, limit) {
7503
7210
  const paths = [];
7504
7211
  const walk = async (directory) => {
7505
- for (const entry of await (0, import_promises10.readdir)(directory, { withFileTypes: true })) {
7212
+ for (const entry of await (0, import_promises9.readdir)(directory, { withFileTypes: true })) {
7506
7213
  if (entry.isSymbolicLink()) throw new TypeError("workspace contains a symbolic link");
7507
7214
  const target = (0, import_path9.resolve)(directory, entry.name);
7508
7215
  if (entry.isDirectory()) await walk(target);
@@ -8075,18 +7782,6 @@ function githubRepositoryName(value2) {
8075
7782
  }
8076
7783
  return `${owner}/${name}`;
8077
7784
  }
8078
- function trustedGitHubInstallUrl(value2) {
8079
- let url;
8080
- try {
8081
- url = new URL(value2);
8082
- } catch {
8083
- throw new Error("odla.ai returned an invalid GitHub installation URL");
8084
- }
8085
- if (url.protocol !== "https:" || url.hostname !== "github.com" || url.username || url.password) {
8086
- throw new Error("odla.ai returned an untrusted GitHub installation URL");
8087
- }
8088
- return url.toString();
8089
- }
8090
7785
  function hostedPollInterval(value2 = 2e3) {
8091
7786
  if (!Number.isSafeInteger(value2) || value2 < 100 || value2 > 3e4) {
8092
7787
  throw new Error("poll interval must be 100-30000ms");
@@ -8122,51 +7817,6 @@ function hostedSecurityCredential(value2) {
8122
7817
  }
8123
7818
 
8124
7819
  // src/security-hosted-github.ts
8125
- async function connectGitHubSecuritySource(options) {
8126
- const out = options.stdout ?? console;
8127
- const repository = options.repository === void 0 ? void 0 : githubRepositoryName(options.repository);
8128
- const attempt = await requestHostedSecurityJson(
8129
- options,
8130
- "/registry/github/connect",
8131
- {
8132
- method: "POST",
8133
- body: JSON.stringify({
8134
- appId: hostedIdentifier(options.appId, "appId"),
8135
- env: hostedIdentifier(options.env, "env"),
8136
- ...repository === void 0 ? {} : { repository }
8137
- })
8138
- },
8139
- "start GitHub connection"
8140
- );
8141
- const serverExpiry = Date.parse(attempt?.expiresAt ?? "");
8142
- if (!attempt?.attemptId || !attempt.installUrl || !Number.isFinite(serverExpiry)) {
8143
- throw new Error("odla.ai returned an invalid GitHub connection attempt");
8144
- }
8145
- const installUrl = trustedGitHubInstallUrl(attempt.installUrl);
8146
- out.log(`GitHub approval: ${installUrl}`);
8147
- if (options.open !== false) {
8148
- await (options.openInstallUrl ?? openUrl)(installUrl);
8149
- out.log("github: opened installation approval in your browser");
8150
- }
8151
- const interval = hostedPollInterval(options.pollIntervalMs);
8152
- const now = options.now ?? Date.now;
8153
- const deadline = Math.min(serverExpiry, now() + hostedPollTimeout(options.pollTimeoutMs));
8154
- const wait2 = options.wait ?? waitForHostedPoll;
8155
- while (true) {
8156
- const state2 = await requestHostedSecurityJson(
8157
- options,
8158
- `/registry/github/connect/${encodeURIComponent(attempt.attemptId)}`,
8159
- {},
8160
- "check GitHub connection"
8161
- );
8162
- if (state2.status !== "pending") {
8163
- if (state2.status === "connected") return state2;
8164
- throw new Error(`GitHub connection ${state2.status}${state2.failureCode ? `: ${state2.failureCode}` : ""}`);
8165
- }
8166
- if (now() >= deadline) throw new Error("GitHub connection approval timed out");
8167
- await wait2(Math.min(interval, Math.max(0, deadline - now())), options.signal);
8168
- }
8169
- }
8170
7820
  async function listGitHubSecuritySources(options) {
8171
7821
  const appId = hostedIdentifier(options.appId, "appId");
8172
7822
  const env = hostedIdentifier(options.env, "env");
@@ -8178,16 +7828,6 @@ async function listGitHubSecuritySources(options) {
8178
7828
  );
8179
7829
  return Array.isArray(body.sources) ? body.sources : [];
8180
7830
  }
8181
- async function disconnectGitHubSecuritySource(options) {
8182
- const appId = hostedIdentifier(options.appId, "appId");
8183
- const sourceId = hostedIdentifier(options.sourceId, "sourceId");
8184
- await requestHostedSecurityJson(
8185
- options,
8186
- `/registry/apps/${encodeURIComponent(appId)}/github/sources/${encodeURIComponent(sourceId)}`,
8187
- { method: "DELETE" },
8188
- "disconnect GitHub security source"
8189
- );
8190
- }
8191
7831
  function repositoryFromGitRemote(remoteInput) {
8192
7832
  const remote = remoteInput.trim();
8193
7833
  const scp = /^git@github\.com:([^/\s]+)\/([^/\s]+?)\/?$/.exec(remote);
@@ -8287,7 +7927,7 @@ function digestText(value2) {
8287
7927
  // src/code-images.ts
8288
7928
  var import_node_child_process6 = require("child_process");
8289
7929
  var import_node_crypto4 = require("crypto");
8290
- var import_promises11 = require("fs/promises");
7930
+ var import_promises10 = require("fs/promises");
8291
7931
  var import_node_os3 = require("os");
8292
7932
  var import_node_path14 = require("path");
8293
7933
  var import_node_url3 = require("url");
@@ -8369,16 +8009,16 @@ function embeddedPiAssetPath() {
8369
8009
  return (0, import_node_url3.fileURLToPath)(new URL("./runtime/pi-agent.js", importMetaUrl));
8370
8010
  }
8371
8011
  async function embeddedPiImageName() {
8372
- const bundle = await (0, import_promises11.readFile)(embeddedPiAssetPath()).catch(() => {
8012
+ const bundle = await (0, import_promises10.readFile)(embeddedPiAssetPath()).catch(() => {
8373
8013
  throw new Error("CLI-embedded Pi runtime is missing; reinstall this exact @odla-ai/cli version");
8374
8014
  });
8375
8015
  return `odla-ai/pi-agent:embedded-sha256-${(0, import_node_crypto4.createHash)("sha256").update(bundle).digest("hex")}`;
8376
8016
  }
8377
8017
  async function buildEmbeddedPiImage(engine, image, run) {
8378
- const context = await (0, import_promises11.mkdtemp)((0, import_node_path14.join)((0, import_node_os3.tmpdir)(), "odla-code-pi-"));
8018
+ const context = await (0, import_promises10.mkdtemp)((0, import_node_path14.join)((0, import_node_os3.tmpdir)(), "odla-code-pi-"));
8379
8019
  try {
8380
- await (0, import_promises11.copyFile)(embeddedPiAssetPath(), (0, import_node_path14.join)(context, "pi-agent.js"));
8381
- await (0, import_promises11.writeFile)((0, import_node_path14.join)(context, "Dockerfile"), [
8020
+ await (0, import_promises10.copyFile)(embeddedPiAssetPath(), (0, import_node_path14.join)(context, "pi-agent.js"));
8021
+ await (0, import_promises10.writeFile)((0, import_node_path14.join)(context, "Dockerfile"), [
8382
8022
  `FROM ${CODE_NODE_IMAGE}`,
8383
8023
  "COPY pi-agent.js /opt/odla/pi-agent.js",
8384
8024
  "WORKDIR /workspace",
@@ -8387,7 +8027,7 @@ async function buildEmbeddedPiImage(engine, image, run) {
8387
8027
  ].join("\n"), { mode: 384 });
8388
8028
  await run(engine, ["build", "--tag", image, context], "inherit");
8389
8029
  } finally {
8390
- await (0, import_promises11.rm)(context, { recursive: true, force: true });
8030
+ await (0, import_promises10.rm)(context, { recursive: true, force: true });
8391
8031
  }
8392
8032
  }
8393
8033
 
@@ -8395,7 +8035,7 @@ async function buildEmbeddedPiImage(engine, image, run) {
8395
8035
  async function codeConnect(options) {
8396
8036
  const cwd = options.cwd ?? process.cwd();
8397
8037
  const configPath = (0, import_node_path15.resolve)(cwd, options.configPath);
8398
- const cfg = (0, import_node_fs15.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
8038
+ const cfg = (0, import_node_fs14.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
8399
8039
  const requestedAppId = options.appId?.trim();
8400
8040
  if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
8401
8041
  throw new Error("--app-id must be a valid odla app id");
@@ -8786,18 +8426,17 @@ Usage:
8786
8426
  odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
8787
8427
  odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
8788
8428
  odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
8789
- odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
8790
- odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
8791
- odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
8792
- odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
8429
+ odla-ai calendar disconnect [--env dev] --yes [continue in Studio; human session required]
8430
+ odla-ai app archive [--config odla.config.mjs] --yes [continue in Studio; human session required]
8431
+ odla-ai app restore [--config odla.config.mjs] [continue in Studio; human session required]
8432
+ odla-ai app export [--env dev] [continue in Studio; human session required]
8793
8433
  odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
8794
- odla-ai app refresh-sandbox [--include-identity] [--include-files] [--dry-run] [--json] --yes
8795
- odla-ai app go-live [--include-identity] [--include-files] [--dry-run] [--json] --yes
8796
- odla-ai app promote [--dry-run] [--json] --yes
8797
- odla-ai app rename <name> [--config odla.config.mjs] [--email <odla-account>] [--json]
8798
- odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
8799
- odla-ai app owners add <email> [--email <odla-account>] [--json]
8800
- odla-ai app owners remove <email> [--email <odla-account>] [--json]
8434
+ [dry-run is local; writes continue in Studio]
8435
+ odla-ai app refresh-sandbox [continue in Studio; human session required]
8436
+ odla-ai app go-live [continue in Studio; human session required]
8437
+ odla-ai app promote [continue in Studio; human session required]
8438
+ odla-ai app rename <name> [continue in Studio; human session required]
8439
+ odla-ai app owners <list|add|remove> [...] [continue in Studio; human session required]
8801
8440
  odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
8802
8441
  odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8803
8442
  odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
@@ -8832,8 +8471,8 @@ Usage:
8832
8471
  odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
8833
8472
  odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
8834
8473
  odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
8835
- odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
8836
- odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
8474
+ odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--token <ODLA_API_KEY>] [--json]
8475
+ odla-ai agent retry <job-id> [--env dev] [--token <ODLA_API_KEY>] [--json]
8837
8476
  odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
8838
8477
  odla-ai context list [--json]
8839
8478
  odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
@@ -8869,8 +8508,8 @@ Usage:
8869
8508
  odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
8870
8509
  odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
8871
8510
  odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
8872
- odla-ai security github connect [--repo owner/name] [--env dev] [--email <odla-account>] [--no-open]
8873
- odla-ai security github disconnect --source <id> [--env dev] [--yes]
8511
+ odla-ai security github connect [--repo owner/name] [--env dev] [continue in Studio; human session required]
8512
+ odla-ai security github disconnect --source <id> [--env dev] [continue in Studio; human session required]
8874
8513
  odla-ai security plan [--env dev] [--json]
8875
8514
  odla-ai security sources [--env dev] [--json]
8876
8515
  odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
@@ -9381,7 +9020,7 @@ async function discussWatch(ctx, topicId, parsed) {
9381
9020
  throw new WatchRemoteError(cursor, error);
9382
9021
  }
9383
9022
  if (deadline !== void 0 && now() >= deadline) {
9384
- const result = report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
9023
+ const result = report(ctx, parsed, { found: false, cursor: cursor ?? "" });
9385
9024
  throw new WatchTimeoutError(result.cursor);
9386
9025
  }
9387
9026
  const base = Math.min(intervalMs, 1e3);
@@ -9422,7 +9061,7 @@ async function discussWatch(ctx, topicId, parsed) {
9422
9061
  });
9423
9062
  const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
9424
9063
  const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
9425
- return report2(ctx, parsed, {
9064
+ return report(ctx, parsed, {
9426
9065
  found: true,
9427
9066
  cursor,
9428
9067
  events: matching,
@@ -9449,13 +9088,13 @@ async function discussWatch(ctx, topicId, parsed) {
9449
9088
  }
9450
9089
  if (page2.hasMore) continue;
9451
9090
  if (deadline !== void 0 && now() >= deadline) {
9452
- return report2(ctx, parsed, { found: false, cursor });
9091
+ return report(ctx, parsed, { found: false, cursor });
9453
9092
  }
9454
9093
  const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
9455
9094
  await sleep(wait2);
9456
9095
  }
9457
9096
  }
9458
- function report2(ctx, parsed, result) {
9097
+ function report(ctx, parsed, result) {
9459
9098
  if (ctx.json) {
9460
9099
  ctx.out.log(JSON.stringify(result, null, 2));
9461
9100
  } else if (parsed.options.jsonl !== true && result.found) {
@@ -9956,7 +9595,7 @@ function eventLabel(event) {
9956
9595
  const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
9957
9596
  return body || event.payload.entityId;
9958
9597
  }
9959
- function report3(ctx, parsed, result) {
9598
+ function report2(ctx, parsed, result) {
9960
9599
  if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
9961
9600
  else if (parsed.options.jsonl !== true && result.found) {
9962
9601
  for (const event of result.events ?? []) {
@@ -10016,7 +9655,7 @@ async function pmWatch(ctx, parsed) {
10016
9655
  });
10017
9656
  if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
10018
9657
  if (deadline !== void 0 && now() >= deadline) {
10019
- return report3(ctx, parsed, { found: false, cursor: cursor ?? "" });
9658
+ return report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
10020
9659
  }
10021
9660
  const backoff = Math.min(
10022
9661
  MAX_BACKOFF_MS2,
@@ -10057,7 +9696,7 @@ async function pmWatch(ctx, parsed) {
10057
9696
  cursor,
10058
9697
  serverTime: current.serverTime
10059
9698
  });
10060
- return report3(ctx, parsed, { found: true, cursor, events: matching });
9699
+ return report2(ctx, parsed, { found: true, cursor, events: matching });
10061
9700
  }
10062
9701
  if (current.events.length > 0) {
10063
9702
  jsonl2(ctx, parsed, {
@@ -10076,7 +9715,7 @@ async function pmWatch(ctx, parsed) {
10076
9715
  }
10077
9716
  if (current.hasMore) continue;
10078
9717
  if (deadline !== void 0 && now() >= deadline) {
10079
- return report3(ctx, parsed, { found: false, cursor });
9718
+ return report2(ctx, parsed, { found: false, cursor });
10080
9719
  }
10081
9720
  await sleep(
10082
9721
  deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
@@ -11109,7 +10748,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
11109
10748
  }
11110
10749
 
11111
10750
  // src/record.ts
11112
- var import_node_fs16 = require("fs");
10751
+ var import_node_fs15 = require("fs");
11113
10752
  var import_node_process12 = __toESM(require("process"), 1);
11114
10753
 
11115
10754
  // src/surface.ts
@@ -11280,14 +10919,14 @@ function recordInvocation(parsed) {
11280
10919
  options: Object.entries(parsed.options).map(([name, value2]) => value2 === false ? `no-${name}` : name).sort()
11281
10920
  };
11282
10921
  if (!entry.path.length) return;
11283
- (0, import_node_fs16.appendFileSync)(file, `${JSON.stringify(entry)}
10922
+ (0, import_node_fs15.appendFileSync)(file, `${JSON.stringify(entry)}
11284
10923
  `);
11285
10924
  } catch {
11286
10925
  }
11287
10926
  }
11288
10927
 
11289
10928
  // src/runbook-actions.ts
11290
- var import_node_fs17 = require("fs");
10929
+ var import_node_fs16 = require("fs");
11291
10930
 
11292
10931
  // src/runbook-requires.ts
11293
10932
  var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
@@ -11372,7 +11011,7 @@ async function bySlug(ctx, slug) {
11372
11011
  function readBody(file, inline) {
11373
11012
  if (inline !== void 0) return inline;
11374
11013
  if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
11375
- return (0, import_node_fs17.readFileSync)(file === "-" ? 0 : file, "utf8");
11014
+ return (0, import_node_fs16.readFileSync)(file === "-" ? 0 : file, "utf8");
11376
11015
  }
11377
11016
  var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
11378
11017
  async function runbookList(ctx, all, query) {
@@ -11464,7 +11103,7 @@ async function runbookRemove(ctx, slug) {
11464
11103
  }
11465
11104
 
11466
11105
  // src/runbook-import.ts
11467
- var import_node_fs18 = require("fs");
11106
+ var import_node_fs17 = require("fs");
11468
11107
  var import_node_path16 = require("path");
11469
11108
  function parseRunbook(text2, slug) {
11470
11109
  let rest = text2;
@@ -11490,12 +11129,12 @@ function parseRunbook(text2, slug) {
11490
11129
  };
11491
11130
  }
11492
11131
  function readRunbookDir(dir) {
11493
- if (!(0, import_node_fs18.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
11494
- const files = (0, import_node_fs18.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
11132
+ if (!(0, import_node_fs17.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
11133
+ const files = (0, import_node_fs17.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
11495
11134
  if (!files.length) throw new Error(`no .md files in ${dir}`);
11496
11135
  return files.map((file) => {
11497
11136
  const slug = (0, import_node_path16.basename)(file, ".md");
11498
- const parsed = parseRunbook((0, import_node_fs18.readFileSync)((0, import_node_path16.join)(dir, file), "utf8"), slug);
11137
+ const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0, import_node_path16.join)(dir, file), "utf8"), slug);
11499
11138
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
11500
11139
  });
11501
11140
  }
@@ -11568,7 +11207,7 @@ async function upsert(ctx, r, visibility) {
11568
11207
 
11569
11208
  // src/runbook-impact.ts
11570
11209
  var import_node_child_process7 = require("child_process");
11571
- var import_node_fs19 = require("fs");
11210
+ var import_node_fs18 = require("fs");
11572
11211
  var import_node_path17 = require("path");
11573
11212
 
11574
11213
  // src/runbook-impact-scan.ts
@@ -11739,9 +11378,9 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
11739
11378
  function manifestLabeller(root) {
11740
11379
  return (workspace) => {
11741
11380
  const manifest = (0, import_node_path17.join)(root, workspace, "package.json");
11742
- if (!(0, import_node_fs19.existsSync)(manifest)) return void 0;
11381
+ if (!(0, import_node_fs18.existsSync)(manifest)) return void 0;
11743
11382
  try {
11744
- const name = JSON.parse((0, import_node_fs19.readFileSync)(manifest, "utf8")).name;
11383
+ const name = JSON.parse((0, import_node_fs18.readFileSync)(manifest, "utf8")).name;
11745
11384
  return typeof name === "string" ? name : void 0;
11746
11385
  } catch {
11747
11386
  return void 0;
@@ -11779,7 +11418,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
11779
11418
  return out;
11780
11419
  }
11781
11420
  var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
11782
- function report4(ctx, impacts) {
11421
+ function report3(ctx, impacts) {
11783
11422
  const covered = impacts.filter((i) => i.runbooks.length);
11784
11423
  ctx.out.log(
11785
11424
  `${impacts.length} changed surface${impacts.length === 1 ? "" : "s"}; ${covered.length} covered by a runbook. Reread each one and fix any step this change made wrong.`
@@ -11808,7 +11447,7 @@ function report4(ctx, impacts) {
11808
11447
  async function runbookImpact(ctx, options, deps = {}) {
11809
11448
  const cwd = deps.cwd ?? process.cwd();
11810
11449
  const runGit = deps.runGit ?? gitRunner(cwd);
11811
- const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs19.readFileSync)((0, import_node_path17.join)(cwd, path), "utf8"));
11450
+ const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0, import_node_path17.join)(cwd, path), "utf8"));
11812
11451
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
11813
11452
  if (!surfaces.length) {
11814
11453
  return ctx.out.log(
@@ -11817,7 +11456,7 @@ async function runbookImpact(ctx, options, deps = {}) {
11817
11456
  }
11818
11457
  const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
11819
11458
  if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
11820
- report4(ctx, impacts);
11459
+ report3(ctx, impacts);
11821
11460
  }
11822
11461
 
11823
11462
  // src/runbook-lint.ts
@@ -11941,7 +11580,7 @@ async function runbookComment(ctx, slug, body) {
11941
11580
 
11942
11581
  // src/runbook-editor.ts
11943
11582
  var import_node_child_process8 = require("child_process");
11944
- var import_node_fs20 = require("fs");
11583
+ var import_node_fs19 = require("fs");
11945
11584
  var import_node_os5 = require("os");
11946
11585
  var import_node_path18 = require("path");
11947
11586
  var import_node_process13 = __toESM(require("process"), 1);
@@ -11969,16 +11608,16 @@ function editText(initial, slug, deps = {}) {
11969
11608
  );
11970
11609
  if (!interactive())
11971
11610
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
11972
- const dir = (0, import_node_fs20.mkdtempSync)((0, import_node_path18.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
11611
+ const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path18.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
11973
11612
  const file = (0, import_node_path18.join)(dir, `${slug}.md`);
11974
11613
  try {
11975
- (0, import_node_fs20.writeFileSync)(file, initial, { mode: 384 });
11614
+ (0, import_node_fs19.writeFileSync)(file, initial, { mode: 384 });
11976
11615
  const code = defaultRunOrInjected(deps)(editor, file);
11977
11616
  if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
11978
- const edited = (0, import_node_fs20.readFileSync)(file, "utf8");
11617
+ const edited = (0, import_node_fs19.readFileSync)(file, "utf8");
11979
11618
  return edited === initial ? null : edited;
11980
11619
  } finally {
11981
- (0, import_node_fs20.rmSync)(dir, { recursive: true, force: true });
11620
+ (0, import_node_fs19.rmSync)(dir, { recursive: true, force: true });
11982
11621
  }
11983
11622
  }
11984
11623
  var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
@@ -12317,7 +11956,6 @@ async function runbookCommand(parsed, deps = {}) {
12317
11956
  }
12318
11957
 
12319
11958
  // src/security-command-context.ts
12320
- var import_promises12 = require("readline/promises");
12321
11959
  async function hostedSecurityContext(parsed, dependencies) {
12322
11960
  const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
12323
11961
  const cfg = await loadProjectConfig(configPath);
@@ -12336,21 +11974,11 @@ async function hostedSecurityContext(parsed, dependencies) {
12336
11974
  cfg,
12337
11975
  { configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
12338
11976
  doFetch,
12339
- stdout
11977
+ stdout,
11978
+ { optionalProjectCapabilities: ["app.manage"] }
12340
11979
  );
12341
11980
  return { platform, token, appId: cfg.app.id, env, fetch: doFetch, stdout };
12342
11981
  }
12343
- async function interactiveConfirmation(message2, dependencies) {
12344
- if (dependencies.confirm) return dependencies.confirm(message2);
12345
- if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
12346
- const prompt = (0, import_promises12.createInterface)({ input: process.stdin, output: process.stdout });
12347
- try {
12348
- const answer = await prompt.question(`${message2} [y/N] `);
12349
- return /^y(?:es)?$/i.test(answer.trim());
12350
- } finally {
12351
- prompt.close();
12352
- }
12353
- }
12354
11982
  function requiredSecurityPositional(parsed, index, label) {
12355
11983
  const value2 = parsed.positionals[index];
12356
11984
  if (!value2) throw new Error(`${label} is required`);
@@ -12416,31 +12044,31 @@ function printHostedJob(out, job, platform, appId) {
12416
12044
  url.searchParams.set("job", job.jobId);
12417
12045
  out.log(` Studio: ${url.toString()}`);
12418
12046
  }
12419
- function printHostedReport(out, report5) {
12420
- out.log(`security report ${report5.jobId}: ${report5.repository}@${report5.revision}`);
12421
- out.log(` coverage: ${report5.coverageStatus} cells=${report5.metrics.coverageCells} shallow=${report5.metrics.shallowCells} blocked=${report5.metrics.blockedCells} unscheduled=${report5.metrics.unscheduledCells} budget_exhausted=${report5.metrics.budgetExhaustedCells}`);
12422
- out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates} rejected=${report5.metrics.rejected}`);
12423
- out.log(` discovery: ${report5.provenance.discovery?.provider ?? "unknown"}/${report5.provenance.discovery?.model ?? "unknown"}`);
12424
- out.log(` validation: ${report5.provenance.validation?.provider ?? "unknown"}/${report5.provenance.validation?.model ?? "unknown"} independent=${String(report5.provenance.independentValidation)}`);
12425
- for (const finding of report5.findings) {
12047
+ function printHostedReport(out, report4) {
12048
+ out.log(`security report ${report4.jobId}: ${report4.repository}@${report4.revision}`);
12049
+ out.log(` coverage: ${report4.coverageStatus} cells=${report4.metrics.coverageCells} shallow=${report4.metrics.shallowCells} blocked=${report4.metrics.blockedCells} unscheduled=${report4.metrics.unscheduledCells} budget_exhausted=${report4.metrics.budgetExhaustedCells}`);
12050
+ out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates} rejected=${report4.metrics.rejected}`);
12051
+ out.log(` discovery: ${report4.provenance.discovery?.provider ?? "unknown"}/${report4.provenance.discovery?.model ?? "unknown"}`);
12052
+ out.log(` validation: ${report4.provenance.validation?.provider ?? "unknown"}/${report4.provenance.validation?.model ?? "unknown"} independent=${String(report4.provenance.independentValidation)}`);
12053
+ for (const finding of report4.findings) {
12426
12054
  const location = finding.locations[0];
12427
12055
  out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
12428
12056
  }
12429
- for (const limitation of report5.limitations) out.log(` limitation: ${limitation}`);
12057
+ for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
12430
12058
  }
12431
- function enforceHostedReportGate(report5, parsed, out, emitSuccess) {
12059
+ function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
12432
12060
  const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
12433
12061
  const candidateValue = parsed.options["fail-on-candidates"];
12434
12062
  const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
12435
12063
  const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
12436
- const confirmed = report5.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
12437
- const leads = failOnCandidates ? report5.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
12438
- const incomplete = report5.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
12064
+ const confirmed = report4.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
12065
+ const leads = failOnCandidates ? report4.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
12066
+ const incomplete = report4.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
12439
12067
  if (confirmed.length || leads.length || incomplete) {
12440
- throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report5.coverageStatus}` : ""}`);
12068
+ throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report4.coverageStatus}` : ""}`);
12441
12069
  }
12442
12070
  if (emitSuccess) {
12443
- out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report5.coverageStatus}. This is not proof that the application is secure.`);
12071
+ out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report4.coverageStatus}. This is not proof that the application is secure.`);
12444
12072
  }
12445
12073
  }
12446
12074
  function printHostedSecurityPlanRoute(out, label, route2) {
@@ -12523,17 +12151,17 @@ async function runHostedSecurity(options) {
12523
12151
  allowNetwork: false
12524
12152
  }
12525
12153
  });
12526
- const report5 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
12527
- await (0, import_node3.writeSecurityArtifacts)(output, report5);
12528
- const reportDigest = await (0, import_security.securityFingerprint)(report5);
12154
+ const report4 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
12155
+ await (0, import_node3.writeSecurityArtifacts)(output, report4);
12156
+ const reportDigest = await (0, import_security.securityFingerprint)(report4);
12529
12157
  await hosted.complete({
12530
12158
  reportDigest,
12531
- coverageStatus: report5.coverageStatus,
12532
- confirmed: report5.metrics.confirmed,
12533
- candidates: report5.metrics.candidates
12159
+ coverageStatus: report4.coverageStatus,
12160
+ confirmed: report4.metrics.confirmed,
12161
+ candidates: report4.metrics.candidates
12534
12162
  }, { signal: options.signal });
12535
- printSummary(options.stdout ?? console, appId, env, hosted.run, report5, output);
12536
- return Object.freeze({ report: report5, run: hosted.run, output });
12163
+ printSummary(options.stdout ?? console, appId, env, hosted.run, report4, output);
12164
+ return Object.freeze({ report: report4, run: hosted.run, output });
12537
12165
  }
12538
12166
  function selectEnv(requested, declared, configPath, rootDir) {
12539
12167
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
@@ -12559,14 +12187,14 @@ function profileFor(name, maxHuntTasks) {
12559
12187
  if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
12560
12188
  return { ...profile, maxHuntTasks };
12561
12189
  }
12562
- function printSummary(out, appId, env, run, report5, output) {
12563
- const complete = report5.coverage.filter((cell) => cell.state === "complete").length;
12190
+ function printSummary(out, appId, env, run, report4, output) {
12191
+ const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
12564
12192
  out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
12565
12193
  out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
12566
12194
  out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
12567
- out.log(` coverage: ${report5.coverageStatus} ${complete}/${report5.coverage.length} blocked=${report5.metrics.blockedCells} shallow=${report5.metrics.shallowCells} unscheduled=${report5.metrics.unscheduledCells} budget_exhausted=${report5.metrics.budgetExhaustedCells}`);
12568
- if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
12569
- out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
12195
+ out.log(` coverage: ${report4.coverageStatus} ${complete}/${report4.coverage.length} blocked=${report4.metrics.blockedCells} shallow=${report4.metrics.shallowCells} unscheduled=${report4.metrics.unscheduledCells} budget_exhausted=${report4.metrics.budgetExhaustedCells}`);
12196
+ if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
12197
+ out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
12570
12198
  out.log(` report: ${(0, import_node_path19.resolve)(output, "REPORT.md")}`);
12571
12199
  }
12572
12200
  function formatBudget(usage) {
@@ -12802,13 +12430,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
12802
12430
  }
12803
12431
  throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
12804
12432
  }
12805
- const report5 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
12433
+ const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
12806
12434
  if (parsed.options.json === true) {
12807
- context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report5 }, null, 2));
12435
+ context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report4 }, null, 2));
12808
12436
  } else {
12809
- printHostedReport(context.stdout, report5);
12437
+ printHostedReport(context.stdout, report4);
12810
12438
  }
12811
- enforceHostedReportGate(report5, parsed, context.stdout, parsed.options.json !== true);
12439
+ enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
12812
12440
  }
12813
12441
  async function runLocalSecurityCommand(parsed, dependencies) {
12814
12442
  if (parsed.options.source === true) {
@@ -12869,19 +12497,20 @@ async function runLocalSecurityCommand(parsed, dependencies) {
12869
12497
  cfg,
12870
12498
  { configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
12871
12499
  doFetch,
12872
- out
12500
+ out,
12501
+ { optionalProjectCapabilities: ["app.manage"] }
12873
12502
  );
12874
12503
  }
12875
12504
  });
12876
12505
  enforceLocalGate(result.report, parsed);
12877
12506
  }
12878
- function enforceLocalGate(report5, parsed) {
12507
+ function enforceLocalGate(report4, parsed) {
12879
12508
  const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
12880
12509
  const candidateValue = parsed.options["fail-on-candidates"];
12881
12510
  const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
12882
- const confirmed = (0, import_security2.findingsAtOrAbove)(report5, failOn);
12883
- const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report5, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
12884
- const incomplete = report5.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
12511
+ const confirmed = (0, import_security2.findingsAtOrAbove)(report4, failOn);
12512
+ const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report4, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
12513
+ const incomplete = report4.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
12885
12514
  if (confirmed.length || leads.length || incomplete) {
12886
12515
  throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
12887
12516
  }
@@ -12920,9 +12549,9 @@ async function securityCommand(parsed, dependencies) {
12920
12549
  assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
12921
12550
  const jobId = requiredSecurityPositional(parsed, 2, "job id");
12922
12551
  const context = await hostedSecurityContext(parsed, dependencies);
12923
- const report5 = await getHostedSecurityReport({ ...context, jobId });
12924
- if (parsed.options.json === true) context.stdout.log(JSON.stringify(report5, null, 2));
12925
- else printHostedReport(context.stdout, report5);
12552
+ const report4 = await getHostedSecurityReport({ ...context, jobId });
12553
+ if (parsed.options.json === true) context.stdout.log(JSON.stringify(report4, null, 2));
12554
+ else printHostedReport(context.stdout, report4);
12926
12555
  return;
12927
12556
  }
12928
12557
  if (sub !== "run") {
@@ -12936,35 +12565,24 @@ async function githubSecurityCommand(parsed, dependencies) {
12936
12565
  const action2 = parsed.positionals[2];
12937
12566
  if (action2 === "disconnect") {
12938
12567
  assertArgs(parsed, ["config", "env", "platform", "source", "email", "open", "yes"], 3);
12939
- const context2 = await hostedSecurityContext(parsed, dependencies);
12940
12568
  const sourceId = requiredString(parsed.options.source, "--source");
12941
- const confirmed = parsed.options.yes === true || await interactiveConfirmation(
12942
- `Disconnect GitHub security source ${sourceId} from ${context2.appId}/${context2.env}?`,
12943
- dependencies
12569
+ return requireStudioHuman(
12570
+ stringOpt(parsed.options.config) ?? "odla.config.mjs",
12571
+ `disconnecting GitHub security source ${sourceId}`,
12572
+ "security",
12573
+ stringOpt(parsed.options.env)
12944
12574
  );
12945
- if (!confirmed) {
12946
- throw new Error("GitHub source disconnect cancelled; pass --yes in a non-interactive shell");
12947
- }
12948
- await disconnectGitHubSecuritySource({ ...context2, sourceId });
12949
- context2.stdout.log(`github: disconnected ${sourceId} from ${context2.appId}/${context2.env}`);
12950
- return;
12951
12575
  }
12952
12576
  if (action2 !== "connect") {
12953
12577
  throw new Error('unknown security github command. Try "odla-ai security github connect".');
12954
12578
  }
12955
12579
  assertArgs(parsed, ["config", "env", "platform", "repo", "email", "open"], 3);
12956
- const context = await hostedSecurityContext(parsed, dependencies);
12957
- const repository = stringOpt(parsed.options.repo) ?? await inferGitHubRepository(process.cwd(), dependencies.readGitOrigin).catch(() => void 0);
12958
- const connection = await connectGitHubSecuritySource({
12959
- ...context,
12960
- ...repository === void 0 ? {} : { repository },
12961
- open: parsed.options.open !== false,
12962
- openInstallUrl: dependencies.openUrl ?? openUrl,
12963
- wait: dependencies.pollWait,
12964
- stdout: context.stdout
12965
- });
12966
- context.stdout.log(`github: connected ${connection.repository ?? repository ?? "app repository"} (${connection.sourceId ?? "source pending"})`);
12967
- context.stdout.log("github: odla.ai stores the installation; no PAT or GitHub token is written locally");
12580
+ await requireStudioHuman(
12581
+ stringOpt(parsed.options.config) ?? "odla.config.mjs",
12582
+ "connecting the GitHub security repository",
12583
+ "security",
12584
+ stringOpt(parsed.options.env)
12585
+ );
12968
12586
  }
12969
12587
  async function listSecuritySources(parsed, dependencies) {
12970
12588
  assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 2);