@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/index.cjs CHANGED
@@ -1736,7 +1736,7 @@ async function agentCommand(parsed, deps = {}) {
1736
1736
  if (action2 !== "jobs" && action2 !== "retry") {
1737
1737
  throw new Error(`unknown agent action "${action2 ?? ""}". Try "odla-ai agent jobs --json".`);
1738
1738
  }
1739
- assertArgs(parsed, ["config", "env", "state", "limit", "json", "token", "email"], action2 === "jobs" ? 2 : 3);
1739
+ assertArgs(parsed, ["config", "env", "state", "limit", "json", "token"], action2 === "jobs" ? 2 : 3);
1740
1740
  if (action2 === "retry" && (parsed.options.state !== void 0 || parsed.options.limit !== void 0)) {
1741
1741
  throw new Error('--state and --limit are supported only by "agent jobs"');
1742
1742
  }
@@ -1744,17 +1744,17 @@ async function agentCommand(parsed, deps = {}) {
1744
1744
  const { env, tenant } = resolveTenant(cfg, stringOpt(parsed.options.env));
1745
1745
  const doFetch = deps.fetch ?? fetch;
1746
1746
  const out = deps.stdout ?? console;
1747
- const credential2 = await getDeveloperToken(
1748
- cfg,
1749
- {
1750
- configPath: cfg.configPath,
1751
- token: stringOpt(parsed.options.token),
1752
- email: stringOpt(parsed.options.email),
1753
- open: false
1754
- },
1755
- doFetch,
1756
- out
1757
- );
1747
+ const credential2 = stringOpt(parsed.options.token) ?? readCredentials(cfg.local.credentialsFile)?.envs[env]?.dbKey;
1748
+ if (!credential2) {
1749
+ throw new Error(
1750
+ `no ${env} app credential found; run \`odla-ai provision --write-dev-vars --yes\` or pass --token <ODLA_API_KEY>`
1751
+ );
1752
+ }
1753
+ if (credential2.startsWith("odla_dev_")) {
1754
+ throw new Error(
1755
+ "agent job administration requires an app credential (ODLA_API_KEY / odla_sk_\u2026), not a developer device token"
1756
+ );
1757
+ }
1758
1758
  const base = `${cfg.dbEndpoint}/app/${encodeURIComponent(tenant)}/admin/agent-jobs`;
1759
1759
  const headers = { authorization: `Bearer ${credential2}` };
1760
1760
  if (action2 === "retry") {
@@ -1810,51 +1810,23 @@ function errorMessage(body) {
1810
1810
  return "request failed";
1811
1811
  }
1812
1812
 
1813
+ // src/human-session.ts
1814
+ async function requireStudioHuman(configPath, action2, destination = "app", env) {
1815
+ const cfg = await loadProjectConfig(configPath);
1816
+ const appEnv = env && cfg.envs.includes(env) ? env : cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
1817
+ const path = destination === "app" ? `/studio/apps/${encodeURIComponent(cfg.app.id)}/${encodeURIComponent(appEnv)}/settings/app` : `/studio/apps/${encodeURIComponent(cfg.app.id)}/${encodeURIComponent(appEnv)}/${destination}`;
1818
+ throw new Error(
1819
+ `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}`
1820
+ );
1821
+ }
1822
+
1813
1823
  // src/app-export.ts
1814
- var import_node_fs7 = require("fs");
1815
- var import_node_stream = require("stream");
1816
- var import_promises = require("stream/promises");
1817
1824
  async function appExport(options) {
1818
- const cfg = await loadProjectConfig(options.configPath);
1819
- const out = options.stdout ?? console;
1820
- const doFetch = options.fetch ?? fetch;
1821
- const { tenant } = resolveTenant(cfg, options.env);
1822
- const token = await getDeveloperToken(
1823
- cfg,
1824
- { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
1825
- doFetch,
1826
- out
1827
- );
1828
- const auth = { authorization: `Bearer ${token}` };
1829
- const base = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}`;
1830
- if (options.fresh) {
1831
- const res = await doFetch(`${base}/export`, { method: "POST", headers: auth });
1832
- const body = await res.json().catch(() => ({}));
1833
- if (!res.ok) throw new Error(`export failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? res.status}`);
1834
- }
1835
- const list = await doFetch(`${base}/backups`, { headers: auth });
1836
- if (!list.ok) throw new Error(`couldn't list backups (${list.status})`);
1837
- const { backups } = await list.json();
1838
- const newest = backups[0];
1839
- if (!newest) {
1840
- throw new Error(
1841
- `${tenant} has no backups yet \u2014 run \`odla-ai app export --fresh\` to take one now (nightly snapshots appear after the first day with writes)`
1842
- );
1843
- }
1844
- const download = await doFetch(`${base}/backups/${newest.id}/download`, { headers: auth });
1845
- if (!download.ok || !download.body) throw new Error(`download failed (${download.status})`);
1846
- const file = options.out ?? `${tenant}-${new Date(newest.created_at).toISOString().slice(0, 10)}-tx${newest.max_tx}.jsonl.gz`;
1847
- await (0, import_promises.pipeline)(import_node_stream.Readable.fromWeb(download.body), (0, import_node_fs7.createWriteStream)(file));
1848
- if (options.json) out.log(JSON.stringify({ file, backup: newest }, null, 2));
1849
- else {
1850
- out.log(`${tenant}: wrote ${file} (${newest.bytes} bytes, ${newest.kind} snapshot at tx ${newest.max_tx})`);
1851
- out.log(`sha256 ${download.headers.get("x-odla-sha256") ?? newest.sha256}`);
1852
- }
1853
- return { file, backup: newest };
1825
+ return requireStudioHuman(options.configPath, "database export", "database", options.env);
1854
1826
  }
1855
1827
 
1856
1828
  // src/app-import.ts
1857
- var import_node_fs8 = require("fs");
1829
+ var import_node_fs7 = require("fs");
1858
1830
  var import_import = require("@odla-ai/db/import");
1859
1831
  function chooseIdMode(options, rows) {
1860
1832
  const chosen = [options.idField && "field", options.key && "key", options.generateIds && "generate"].filter(Boolean);
@@ -1871,9 +1843,8 @@ async function appImport(options) {
1871
1843
  const cfg = await loadProjectConfig(options.configPath);
1872
1844
  const out = options.stdout ?? console;
1873
1845
  const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
1874
- const doFetch = options.fetch ?? fetch;
1875
1846
  const { tenant } = resolveTenant(cfg, options.env);
1876
- const text2 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs8.readFileSync)(0, "utf8")))() : (0, import_node_fs8.readFileSync)(options.file, "utf8");
1847
+ const text2 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs7.readFileSync)(0, "utf8")))() : (0, import_node_fs7.readFileSync)(options.file, "utf8");
1877
1848
  const { format, sources } = (0, import_import.parseImport)(text2, options.ns);
1878
1849
  if (format === "namespace-map" && options.ns) {
1879
1850
  throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
@@ -1899,100 +1870,18 @@ ${detail}${more}`);
1899
1870
  if (options.json) out.log(JSON.stringify(result, null, 2));
1900
1871
  return result;
1901
1872
  }
1902
- const token = await getDeveloperToken(
1903
- cfg,
1904
- { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
1905
- doFetch,
1906
- out
1907
- );
1908
- const runId = crypto.randomUUID();
1909
- const url = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}/transact`;
1910
- for (const chunk of chunks) {
1911
- const res = await doFetch(url, {
1912
- method: "POST",
1913
- headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
1914
- body: JSON.stringify({ mutationId: (0, import_import.importMutationId)(runId, chunk.index), ops: chunk.ops })
1915
- });
1916
- const body = await res.json().catch(() => ({}));
1917
- if (!res.ok) {
1918
- const code = body.error?.code ? ` (${body.error.code})` : "";
1919
- throw new Error(
1920
- `chunk ${chunk.index + 1} of ${chunks.length} failed${code}: ${body.error?.message ?? res.status}. ${result.committed} row(s) already committed; fix the input and re-run to import the rest.`
1921
- );
1922
- }
1923
- result.committed += chunk.ops.length;
1924
- if (typeof body.txId === "number") result.txIds.push(body.txId);
1925
- if (body.duplicate) result.duplicate++;
1926
- }
1927
- if (options.json) out.log(JSON.stringify(result, null, 2));
1928
- else {
1929
- const dup = result.duplicate > 0 ? ` (${result.duplicate} chunk(s) were already applied)` : "";
1930
- out.log(`${tenant}: upserted ${result.committed} row(s) in ${chunks.length} transaction(s)${dup}`);
1931
- }
1932
- return result;
1873
+ return requireStudioHuman(options.configPath, "database import", "database", options.env);
1933
1874
  }
1934
1875
 
1935
1876
  // src/app-owners.ts
1936
- var sink = (options) => options.stdout ?? console;
1937
- async function ownersRequest(method, suffix, options, body) {
1938
- const cfg = await loadProjectConfig(options.configPath);
1939
- const doFetch = options.fetch ?? fetch;
1940
- const token = await getDeveloperToken(
1941
- cfg,
1942
- { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
1943
- doFetch,
1944
- sink(options)
1945
- );
1946
- const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/owners${suffix}`, {
1947
- method,
1948
- headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
1949
- body: body === void 0 ? void 0 : JSON.stringify(body)
1950
- });
1951
- const data = await res.json().catch(() => ({}));
1952
- if (!res.ok) {
1953
- throw new Error(
1954
- `owners ${method} failed${data.error?.code ? ` (${data.error.code})` : ""}: ` + (data.error?.message ?? `registry returned ${res.status}`)
1955
- );
1956
- }
1957
- return data.owners ?? [];
1958
- }
1959
- function report(options, owners, headline) {
1960
- const out = sink(options);
1961
- if (options.json === true) {
1962
- out.log(JSON.stringify(owners, null, 2));
1963
- return;
1964
- }
1965
- if (headline) out.log(headline);
1966
- out.log(`owners (${owners.length}):`);
1967
- for (const o of owners) {
1968
- const name = o.email?.trim() || "Unnamed member";
1969
- out.log(
1970
- ` ${o.primary ? "\u2605" : "\xB7"} ${name} [${o.ownerId}]${o.primary ? " (primary)" : ""}`
1971
- );
1972
- }
1973
- }
1974
1877
  async function ownersList(options) {
1975
- report(options, await ownersRequest("GET", "", options));
1878
+ await requireStudioHuman(options.configPath, "listing app owners", "app");
1976
1879
  }
1977
1880
  async function ownersAdd(email, options) {
1978
- const owners = await ownersRequest("POST", "", options, { email });
1979
- report(
1980
- options,
1981
- owners,
1982
- `added ${email} as a co-owner \u2014 they share full access. They can now run \`odla-ai provision\` to mint their own credentials (no secret sharing).`
1983
- );
1881
+ await requireStudioHuman(options.configPath, `adding ${email} as an app owner`, "app");
1984
1882
  }
1985
1883
  async function ownersRemove(target, options) {
1986
- let ownerId = target;
1987
- if (target.includes("@")) {
1988
- const owners2 = await ownersRequest("GET", "", options);
1989
- const match = owners2.find((o) => o.email?.toLowerCase() === target.toLowerCase());
1990
- if (!match) throw new Error(`no co-owner with email ${target}`);
1991
- if (match.primary) throw new Error("can't remove the primary owner");
1992
- ownerId = match.ownerId;
1993
- }
1994
- const owners = await ownersRequest("DELETE", `/${encodeURIComponent(ownerId)}`, options);
1995
- report(options, owners, `removed ${target}`);
1884
+ await requireStudioHuman(options.configPath, `removing ${target} as an app owner`, "app");
1996
1885
  }
1997
1886
  async function appOwnersCommand(parsed, dependencies = {}) {
1998
1887
  const sub = parsed.positionals[2] ?? "list";
@@ -2024,35 +1913,9 @@ async function appOwnersCommand(parsed, dependencies = {}) {
2024
1913
 
2025
1914
  // src/app-rename.ts
2026
1915
  async function appRename(name, options) {
2027
- const out = options.stdout ?? console;
2028
1916
  const trimmed = name.trim();
2029
1917
  if (!trimmed) throw new Error('"app rename" needs a name \u2014 try `odla-ai app rename "Acme Storefront"`.');
2030
- const cfg = await loadProjectConfig(options.configPath);
2031
- const doFetch = options.fetch ?? fetch;
2032
- const token = await getDeveloperToken(
2033
- cfg,
2034
- { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
2035
- doFetch,
2036
- out
2037
- );
2038
- const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/name`, {
2039
- method: "PUT",
2040
- headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
2041
- body: JSON.stringify({ name: trimmed })
2042
- });
2043
- const data = await res.json().catch(() => ({}));
2044
- if (!res.ok || !data.app) {
2045
- throw new Error(
2046
- `rename failed${data.error?.code ? ` (${data.error.code})` : ""}: ` + (data.error?.message ?? `registry returned ${res.status}`)
2047
- );
2048
- }
2049
- if (options.json === true) {
2050
- out.log(JSON.stringify(data.app, null, 2));
2051
- return;
2052
- }
2053
- out.log(`renamed ${data.app.appId} \u2192 "${data.app.name}"`);
2054
- out.log("The app id is unchanged, so credentials, tenants, and URLs keep working.");
2055
- out.log(`Update the "name" in ${cfg.configPath} to match.`);
1918
+ await requireStudioHuman(options.configPath, `renaming the app to "${trimmed}"`, "app");
2056
1919
  }
2057
1920
  async function appRenameCommand(parsed, dependencies = {}) {
2058
1921
  assertArgs(parsed, ["config", "token", "email", "json"], parsed.positionals.length);
@@ -2068,172 +1931,23 @@ async function appRenameCommand(parsed, dependencies = {}) {
2068
1931
  }
2069
1932
 
2070
1933
  // src/app-transfer.ts
2071
- function endpointsFor(verb, tenants) {
2072
- const up = { source: tenants.sandbox, target: tenants.live, from: "dev" };
2073
- const down = { source: tenants.live, target: tenants.sandbox, from: "prod" };
2074
- if (verb === "refresh-sandbox") return { ...down, mode: "refresh" };
2075
- return { ...up, mode: "cutover" };
2076
- }
2077
- async function api(cfg, token, path, init, doFetch) {
2078
- const res = await doFetch(`${cfg.dbEndpoint}${path}`, {
2079
- ...init,
2080
- headers: { authorization: `Bearer ${token}`, "content-type": "application/json", ...init?.headers }
2081
- });
2082
- return { status: res.status, body: await res.json().catch(() => ({})) };
2083
- }
2084
- function describe(side) {
2085
- if (!side.exists) return "not provisioned";
2086
- if (side.maxTx === 0) return "empty \u2014 never written";
2087
- const ns = side.namespaces.length === 1 ? "1 namespace" : `${side.namespaces.length} namespaces`;
2088
- const identity = side.identityRows > 0 ? `, ${side.identityRows} identity row(s)` : "";
2089
- return `${ns} \xB7 ${side.triples} value(s) \xB7 tx ${side.maxTx}${identity}`;
2090
- }
2091
- function printPlan(out, verb, pre, opts) {
2092
- const arrow = verb === "refresh-sandbox" ? "live \u2192 sandbox" : "sandbox \u2192 live";
2093
- out.log(`${verb} (${arrow})`);
2094
- out.log(` from ${pre.source.tenant} ${describe(pre.source)}`);
2095
- out.log(` to ${pre.target.tenant} ${describe(pre.target)}`);
2096
- if (verb === "promote") {
2097
- out.log(" moves schema, rules and gates only \u2014 no rows are copied or removed");
2098
- } else {
2099
- out.log(` ${pre.target.tenant} is REPLACED by ${pre.source.tenant}`);
2100
- out.log(` stays put: ${pre.staysPut.join(", ")}`);
2101
- const identity = opts.includeIdentity ? "INCLUDED (--include-identity)" : `left behind: ${pre.excluded.join(", ")}`;
2102
- out.log(` identity: ${identity}`);
2103
- out.log(` files: ${opts.includeFiles ? "copied (--include-files)" : "not copied"}`);
2104
- }
2105
- for (const blocker of pre.blockers) out.log(` \u2716 ${blocker.code}: ${blocker.message}`);
2106
- }
2107
1934
  async function appTransfer(options) {
2108
1935
  const cfg = await loadProjectConfig(options.configPath);
2109
- const out = options.stdout ?? console;
2110
- const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
2111
- const doFetch = options.fetch ?? fetch;
2112
- const tenants = bothTenants(cfg);
2113
- const route2 = endpointsFor(options.verb, tenants);
2114
- const token = await getDeveloperToken(
2115
- cfg,
2116
- { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
2117
- doFetch,
2118
- out
2119
- );
2120
- const pre = await api(
2121
- cfg,
2122
- token,
2123
- `/admin/apps/${encodeURIComponent(route2.target)}/copy-preflight?from=${route2.from}`,
2124
- void 0,
2125
- doFetch
2126
- );
2127
- if (pre.status !== 200) {
2128
- throw new Error(`pre-flight failed${pre.body.error?.code ? ` (${pre.body.error.code})` : ""}: ${pre.body.error?.message ?? pre.status}`);
2129
- }
2130
- const plan = pre.body;
2131
- printPlan({ log: say }, options.verb, plan, options);
2132
- if (options.verb === "go-live" && !plan.targetEmpty) {
2133
- throw new Error(
2134
- `${route2.target} already has data \u2014 go-live has already happened. Use \`odla-ai app promote --yes\` to push schema and rules, or \`odla-ai app refresh-sandbox --yes\` to pull live back down.`
2135
- );
2136
- }
2137
- if (plan.blockers.length > 0) throw new Error(`cannot ${options.verb}: ${plan.blockers.map((b) => b.message).join("; ")}`);
2138
- if (options.dryRun || options.yes !== true) {
2139
- say(`nothing written (${options.dryRun ? "--dry-run" : "no --yes"})`);
2140
- if (options.json) out.log(JSON.stringify(plan, null, 2));
2141
- return { ok: true, plan };
2142
- }
2143
- if (options.verb === "promote") {
2144
- const res2 = await api(
2145
- cfg,
2146
- token,
2147
- `/admin/apps/${encodeURIComponent(route2.target)}/promote-definitions`,
2148
- { method: "POST", body: JSON.stringify({ from: "dev" }) },
2149
- doFetch
2150
- );
2151
- if (res2.status !== 200) throw new Error(`promote failed${res2.body.error?.code ? ` (${res2.body.error.code})` : ""}: ${res2.body.error?.message ?? res2.status}`);
2152
- if (options.json) out.log(JSON.stringify(res2.body, null, 2));
2153
- else say(`${route2.target}: promoted ${(res2.body.applied ?? []).join(", ")}`);
2154
- return { ok: true, plan, result: res2.body };
2155
- }
2156
- const res = await api(
2157
- cfg,
2158
- token,
2159
- `/admin/apps/${encodeURIComponent(route2.target)}/copy-db`,
2160
- {
2161
- method: "POST",
2162
- body: JSON.stringify({
2163
- from: route2.from,
2164
- mode: route2.mode,
2165
- ...options.includeIdentity ? { includeUsers: true } : {},
2166
- ...options.includeFiles ? { includeFiles: true } : {}
2167
- })
2168
- },
2169
- doFetch
2170
- );
2171
- if (res.status !== 200) throw new Error(`${options.verb} failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
2172
- say(`${route2.target}: replaced from ${route2.source} (tx ${res.body.destination?.maxTx}, epoch ${res.body.destination?.epoch}) \u2014 connected clients resync automatically`);
2173
- if (options.includeFiles) await copyFiles(cfg, token, route2, doFetch, out);
2174
- if (options.json) out.log(JSON.stringify(res.body, null, 2));
2175
- return { ok: true, plan, result: res.body };
2176
- }
2177
- async function copyFiles(cfg, token, route2, doFetch, out) {
2178
- for (let attempt = 1; attempt <= 20; attempt++) {
2179
- const res = await api(
2180
- cfg,
2181
- token,
2182
- `/admin/apps/${encodeURIComponent(route2.target)}/copy-files`,
2183
- { method: "POST", body: JSON.stringify({ from: route2.from }) },
2184
- doFetch
2185
- );
2186
- if (res.status === 200) {
2187
- out.log(`${route2.target}: files copied`);
2188
- return;
2189
- }
2190
- if (!res.body.error?.retry) {
2191
- throw new Error(`file copy failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
2192
- }
2193
- out.log(` files: bounded at attempt ${attempt}, resuming\u2026`);
2194
- }
2195
- throw new Error("file copy did not finish within 20 rounds \u2014 re-run to continue where it left off");
1936
+ bothTenants(cfg);
1937
+ return requireStudioHuman(options.configPath, `app ${options.verb}`, "database");
2196
1938
  }
2197
1939
 
2198
1940
  // src/app-lifecycle.ts
2199
- async function lifecycleCall(action2, options) {
2200
- const cfg = await loadProjectConfig(options.configPath);
2201
- const out = options.stdout ?? console;
2202
- const doFetch = options.fetch ?? fetch;
2203
- const token = await getDeveloperToken(
2204
- cfg,
2205
- { configPath: cfg.configPath, token: options.token, email: options.email, open: false },
2206
- doFetch,
2207
- out
2208
- );
2209
- const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/${action2}`, {
2210
- method: "POST",
2211
- headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }
2212
- });
2213
- const body = await res.json().catch(() => ({}));
2214
- if (!res.ok || !body.ok) {
2215
- throw new Error(`${action2} failed${body.error?.code ? ` (${body.error.code})` : ""}: ${body.error?.message ?? `registry returned ${res.status}`}`);
2216
- }
2217
- return body;
2218
- }
2219
1941
  async function appArchive(options) {
2220
1942
  if (options.yes !== true) {
2221
1943
  throw new Error(
2222
1944
  "app archive suspends EVERY environment: API keys stop working and all services refuse requests until restored. All data is retained. Pass --yes to proceed."
2223
1945
  );
2224
1946
  }
2225
- const out = options.stdout ?? console;
2226
- const body = await lifecycleCall("archive", options);
2227
- if (options.json) out.log(JSON.stringify(body, null, 2));
2228
- else if (body.operation?.state === "noop") out.log(`${body.app?.appId}: already archived`);
2229
- else out.log(`${body.app?.appId}: archived \u2014 data retained; run \`odla-ai app restore\` (or use Studio) to bring it back`);
1947
+ await requireStudioHuman(options.configPath, "app archive", "app");
2230
1948
  }
2231
1949
  async function appRestore(options) {
2232
- const out = options.stdout ?? console;
2233
- const body = await lifecycleCall("restore", options);
2234
- if (options.json) out.log(JSON.stringify(body, null, 2));
2235
- else if (body.operation?.state === "noop") out.log(`${body.app?.appId}: already active`);
2236
- else out.log(`${body.app?.appId}: restored \u2014 every service's data plane is live again`);
1950
+ await requireStudioHuman(options.configPath, "app restore", "app");
2237
1951
  }
2238
1952
  async function appCommand(parsed, dependencies = {}) {
2239
1953
  const sub = parsed.positionals[1];
@@ -2319,7 +2033,7 @@ async function appCommand(parsed, dependencies = {}) {
2319
2033
  }
2320
2034
 
2321
2035
  // src/brand-command.ts
2322
- var import_promises2 = require("fs/promises");
2036
+ var import_promises = require("fs/promises");
2323
2037
  var import_node_path7 = require("path");
2324
2038
 
2325
2039
  // src/brand-design-unpack.ts
@@ -2422,7 +2136,7 @@ function describeUnpack(result, outDir) {
2422
2136
  // src/brand-command.ts
2423
2137
  var USAGE = "usage: odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]";
2424
2138
  async function readBundle(source, deps) {
2425
- if (source !== "-") return (0, import_promises2.readFile)((0, import_node_path7.resolve)(source), "utf8");
2139
+ if (source !== "-") return (0, import_promises.readFile)((0, import_node_path7.resolve)(source), "utf8");
2426
2140
  const readStdin = deps.readStdin;
2427
2141
  if (!readStdin) throw new Error("reading a bundle from stdin is not supported here");
2428
2142
  return readStdin();
@@ -2430,8 +2144,8 @@ async function readBundle(source, deps) {
2430
2144
  async function writeAll(result, outDir) {
2431
2145
  for (const file of result.files) {
2432
2146
  const target = (0, import_node_path7.resolve)(outDir, file.path);
2433
- await (0, import_promises2.mkdir)((0, import_node_path7.dirname)(target), { recursive: true });
2434
- await (0, import_promises2.writeFile)(target, file.bytes);
2147
+ await (0, import_promises.mkdir)((0, import_node_path7.dirname)(target), { recursive: true });
2148
+ await (0, import_promises.writeFile)(target, file.bytes);
2435
2149
  }
2436
2150
  }
2437
2151
  async function designUnpack(parsed, deps) {
@@ -2591,12 +2305,6 @@ async function pollCalendarConnection(ctx, attemptId) {
2591
2305
  ctx.env
2592
2306
  );
2593
2307
  }
2594
- async function requestCalendarDisconnect(ctx) {
2595
- return parseCalendarStatus(
2596
- await calendarJson(ctx, "/disconnect", { method: "POST", body: JSON.stringify({ purge: false }) }),
2597
- ctx.env
2598
- );
2599
- }
2600
2308
  function parseCalendarStatus(raw, env) {
2601
2309
  const outer = wrapped(raw, "calendar");
2602
2310
  const value2 = record(outer.attempt) ?? record(outer.status) ?? outer;
@@ -2780,10 +2488,7 @@ async function applyCalendarBookingPage(ctx, bookingPageUrl, out) {
2780
2488
  }
2781
2489
  async function calendarDisconnect(options) {
2782
2490
  if (!options.yes) throw new Error("calendar disconnect requires --yes");
2783
- const { ctx, out } = await lifecycleContext(options);
2784
- const status = await requestCalendarDisconnect(ctx);
2785
- out.log(`${ctx.env}: calendar disconnected; no calendar data was stored`);
2786
- return status;
2491
+ return requireStudioHuman(options.configPath, "calendar disconnect", "calendar", options.env);
2787
2492
  }
2788
2493
  async function ensureCalendarConnected(ctx, options) {
2789
2494
  const out = options.stdout ?? console;
@@ -2997,9 +2702,9 @@ var import_apps6 = require("@odla-ai/apps");
2997
2702
  var import_node_path8 = require("path");
2998
2703
 
2999
2704
  // src/version.ts
3000
- var import_node_fs9 = require("fs");
2705
+ var import_node_fs8 = require("fs");
3001
2706
  function cliVersion() {
3002
- const pkg = JSON.parse((0, import_node_fs9.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
2707
+ const pkg = JSON.parse((0, import_node_fs8.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
3003
2708
  return pkg.version ?? "unknown";
3004
2709
  }
3005
2710
 
@@ -3015,7 +2720,7 @@ var ConfigOperationCommandError = class extends Error {
3015
2720
 
3016
2721
  // src/config-operation-validate.ts
3017
2722
  var import_apps3 = require("@odla-ai/apps");
3018
- var import_node_fs10 = require("fs");
2723
+ var import_node_fs9 = require("fs");
3019
2724
 
3020
2725
  // src/config-reconcile-digest.ts
3021
2726
  var import_node_crypto2 = require("crypto");
@@ -3051,7 +2756,7 @@ var SERVICE = /^[a-z][a-z0-9-]{0,39}$/;
3051
2756
  function readPlan(path) {
3052
2757
  let value2;
3053
2758
  try {
3054
- const raw = (0, import_node_fs10.readFileSync)(path, "utf8");
2759
+ const raw = (0, import_node_fs9.readFileSync)(path, "utf8");
3055
2760
  if (Buffer.byteLength(raw) > 128 * 1024) throw new Error("plan exceeds 128 KiB");
3056
2761
  value2 = JSON.parse(raw);
3057
2762
  } catch (error) {
@@ -3764,7 +3469,7 @@ async function configPlan(options) {
3764
3469
  apply,
3765
3470
  nextActions: planNextActions(reconciliation, options.configPath)
3766
3471
  };
3767
- printPlan2(document2, options);
3472
+ printPlan(document2, options);
3768
3473
  return document2;
3769
3474
  }
3770
3475
  async function inspectConfig(options) {
@@ -3804,7 +3509,7 @@ function printDiff(document2, options) {
3804
3509
  printDifferences(out, document2);
3805
3510
  printNext(out, document2.nextActions);
3806
3511
  }
3807
- function printPlan2(document2, options) {
3512
+ function printPlan(document2, options) {
3808
3513
  const out = options.stdout ?? console;
3809
3514
  if (options.json) {
3810
3515
  out.log(JSON.stringify(document2, null, 2));
@@ -3907,12 +3612,12 @@ function quoteArg2(value2) {
3907
3612
 
3908
3613
  // src/doctor-checks.ts
3909
3614
  var import_node_child_process3 = require("child_process");
3910
- var import_node_fs12 = require("fs");
3615
+ var import_node_fs11 = require("fs");
3911
3616
  var import_node_path11 = require("path");
3912
3617
 
3913
3618
  // src/wrangler.ts
3914
3619
  var import_node_child_process2 = require("child_process");
3915
- var import_node_fs11 = require("fs");
3620
+ var import_node_fs10 = require("fs");
3916
3621
  var import_node_path10 = require("path");
3917
3622
  var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
3918
3623
  const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
@@ -3928,14 +3633,14 @@ var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]
3928
3633
  function findWranglerConfig(rootDir) {
3929
3634
  for (const name of WRANGLER_CONFIG_FILES) {
3930
3635
  const path = (0, import_node_path10.join)(rootDir, name);
3931
- if ((0, import_node_fs11.existsSync)(path)) return path;
3636
+ if ((0, import_node_fs10.existsSync)(path)) return path;
3932
3637
  }
3933
3638
  return null;
3934
3639
  }
3935
3640
  function readWranglerConfig(path) {
3936
3641
  if (path.endsWith(".toml")) return null;
3937
3642
  try {
3938
- return JSON.parse(stripJsonComments((0, import_node_fs11.readFileSync)(path, "utf8")));
3643
+ return JSON.parse(stripJsonComments((0, import_node_fs10.readFileSync)(path, "utf8")));
3939
3644
  } catch {
3940
3645
  return null;
3941
3646
  }
@@ -4043,7 +3748,7 @@ function wranglerWarnings(rootDir) {
4043
3748
  const dir = (0, import_node_path11.resolve)(rootDir, assets.directory);
4044
3749
  if (dir === (0, import_node_path11.resolve)(rootDir)) {
4045
3750
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
4046
- } else if ((0, import_node_fs12.existsSync)((0, import_node_path11.join)(dir, "node_modules"))) {
3751
+ } else if ((0, import_node_fs11.existsSync)((0, import_node_path11.join)(dir, "node_modules"))) {
4047
3752
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
4048
3753
  }
4049
3754
  }
@@ -4079,12 +3784,12 @@ function o11yProjectWarnings(rootDir) {
4079
3784
  return warnings;
4080
3785
  }
4081
3786
  const main = typeof config.main === "string" ? (0, import_node_path11.resolve)(rootDir, config.main) : null;
4082
- if (!main || !(0, import_node_fs12.existsSync)(main)) {
3787
+ if (!main || !(0, import_node_fs11.existsSync)(main)) {
4083
3788
  warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
4084
3789
  } else {
4085
3790
  let source = "";
4086
3791
  try {
4087
- source = (0, import_node_fs12.readFileSync)(main, "utf8");
3792
+ source = (0, import_node_fs11.readFileSync)(main, "utf8");
4088
3793
  } catch {
4089
3794
  }
4090
3795
  if (!/\bwithObservability\b/.test(source)) {
@@ -4108,7 +3813,7 @@ function calendarProjectWarnings(rootDir) {
4108
3813
  }
4109
3814
  function readPackageJson(rootDir) {
4110
3815
  try {
4111
- return JSON.parse((0, import_node_fs12.readFileSync)((0, import_node_path11.join)(rootDir, "package.json"), "utf8"));
3816
+ return JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path11.join)(rootDir, "package.json"), "utf8"));
4112
3817
  } catch {
4113
3818
  return null;
4114
3819
  }
@@ -4337,14 +4042,14 @@ function harnessOption(value2, flag) {
4337
4042
  }
4338
4043
 
4339
4044
  // src/init.ts
4340
- var import_node_fs13 = require("fs");
4045
+ var import_node_fs12 = require("fs");
4341
4046
  var import_node_path12 = require("path");
4342
4047
  var import_apps9 = require("@odla-ai/apps");
4343
4048
  function initProject(options) {
4344
4049
  const out = options.stdout ?? console;
4345
4050
  const rootDir = (0, import_node_path12.resolve)(options.rootDir ?? process.cwd());
4346
4051
  const configPath = (0, import_node_path12.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
4347
- if ((0, import_node_fs13.existsSync)(configPath) && !options.force) {
4052
+ if ((0, import_node_fs12.existsSync)(configPath) && !options.force) {
4348
4053
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
4349
4054
  }
4350
4055
  if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
@@ -4360,10 +4065,10 @@ function initProject(options) {
4360
4065
  }
4361
4066
  }
4362
4067
  const aiProvider = options.aiProvider;
4363
- (0, import_node_fs13.mkdirSync)((0, import_node_path12.dirname)(configPath), { recursive: true });
4364
- (0, import_node_fs13.mkdirSync)((0, import_node_path12.resolve)(rootDir, "src/odla"), { recursive: true });
4365
- (0, import_node_fs13.mkdirSync)((0, import_node_path12.resolve)(rootDir, ".odla"), { recursive: true });
4366
- (0, import_node_fs13.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
4068
+ (0, import_node_fs12.mkdirSync)((0, import_node_path12.dirname)(configPath), { recursive: true });
4069
+ (0, import_node_fs12.mkdirSync)((0, import_node_path12.resolve)(rootDir, "src/odla"), { recursive: true });
4070
+ (0, import_node_fs12.mkdirSync)((0, import_node_path12.resolve)(rootDir, ".odla"), { recursive: true });
4071
+ (0, import_node_fs12.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
4367
4072
  writeIfMissing((0, import_node_path12.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
4368
4073
  writeIfMissing((0, import_node_path12.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
4369
4074
  ensureGitignore(rootDir);
@@ -4372,8 +4077,8 @@ function initProject(options) {
4372
4077
  out.log("updated .gitignore for local odla credentials");
4373
4078
  }
4374
4079
  function writeIfMissing(path, text2) {
4375
- if ((0, import_node_fs13.existsSync)(path)) return;
4376
- (0, import_node_fs13.writeFileSync)(path, text2);
4080
+ if ((0, import_node_fs12.existsSync)(path)) return;
4081
+ (0, import_node_fs12.writeFileSync)(path, text2);
4377
4082
  }
4378
4083
  function configTemplate(input) {
4379
4084
  const calendar = input.services.includes("calendar") ? ` calendar: {
@@ -4580,7 +4285,9 @@ async function secretsSetClerkKey(options) {
4580
4285
  if (value2.startsWith("sk_live_") && !PROD_ENV_NAMES2.has(options.env) && !options.yes) {
4581
4286
  throw new Error(`refusing to store an sk_live_ Clerk key for "${options.env}" without --yes (live users would sync into a non-prod tenant)`);
4582
4287
  }
4583
- const token = await getDeveloperToken(cfg, options, doFetch, out);
4288
+ const token = await getDeveloperToken(cfg, options, doFetch, out, {
4289
+ optionalProjectCapabilities: ["app.manage"]
4290
+ });
4584
4291
  const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/clerk-secret`, {
4585
4292
  method: "POST",
4586
4293
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
@@ -4610,7 +4317,7 @@ async function resolveVaultWrite(options) {
4610
4317
  }
4611
4318
 
4612
4319
  // src/skill.ts
4613
- var import_node_fs14 = require("fs");
4320
+ var import_node_fs13 = require("fs");
4614
4321
  var import_node_os2 = require("os");
4615
4322
  var import_node_path13 = require("path");
4616
4323
  var import_node_url2 = require("url");
@@ -4707,7 +4414,7 @@ function installSkill(options = {}) {
4707
4414
  plans.set(target, { target, content: content2, boundary, managedMerge });
4708
4415
  };
4709
4416
  const planSkillTree = (targetDir2, boundary = root) => {
4710
- 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);
4417
+ for (const rel of files) plan((0, import_node_path13.join)(targetDir2, rel), (0, import_node_fs13.readFileSync)((0, import_node_path13.join)(sourceDir, rel), "utf8"), false, boundary);
4711
4418
  };
4712
4419
  let targetDir;
4713
4420
  if (options.global) {
@@ -4727,7 +4434,7 @@ function installSkill(options = {}) {
4727
4434
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
4728
4435
  if (harnesses.includes("claude")) {
4729
4436
  for (const skill of skillNames(files)) {
4730
- const canonical = (0, import_node_fs14.readFileSync)((0, import_node_path13.join)(sourceDir, skill, "SKILL.md"), "utf8");
4437
+ const canonical = (0, import_node_fs13.readFileSync)((0, import_node_path13.join)(sourceDir, skill, "SKILL.md"), "utf8");
4731
4438
  plan((0, import_node_path13.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
4732
4439
  }
4733
4440
  rememberTarget("claude", claudeRoot);
@@ -4762,11 +4469,11 @@ function installSkill(options = {}) {
4762
4469
  conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
4763
4470
  continue;
4764
4471
  }
4765
- if (!(0, import_node_fs14.existsSync)(file.target)) {
4472
+ if (!(0, import_node_fs13.existsSync)(file.target)) {
4766
4473
  writtenPaths.add(file.target);
4767
4474
  continue;
4768
4475
  }
4769
- const current = (0, import_node_fs14.readFileSync)(file.target, "utf8");
4476
+ const current = (0, import_node_fs13.readFileSync)(file.target, "utf8");
4770
4477
  if (current === file.content) {
4771
4478
  unchangedPaths.add(file.target);
4772
4479
  } else if (file.managedMerge || options.force) {
@@ -4783,9 +4490,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
4783
4490
  );
4784
4491
  }
4785
4492
  for (const file of plans.values()) {
4786
- if (!(0, import_node_fs14.existsSync)(file.target) || (0, import_node_fs14.readFileSync)(file.target, "utf8") !== file.content) {
4787
- (0, import_node_fs14.mkdirSync)((0, import_node_path13.dirname)(file.target), { recursive: true });
4788
- (0, import_node_fs14.writeFileSync)(file.target, file.content);
4493
+ if (!(0, import_node_fs13.existsSync)(file.target) || (0, import_node_fs13.readFileSync)(file.target, "utf8") !== file.content) {
4494
+ (0, import_node_fs13.mkdirSync)((0, import_node_path13.dirname)(file.target), { recursive: true });
4495
+ (0, import_node_fs13.writeFileSync)(file.target, file.content);
4789
4496
  }
4790
4497
  }
4791
4498
  const skills = skillNames(files);
@@ -4826,9 +4533,9 @@ function normalizeHarnesses(values, global) {
4826
4533
  function managedFileContent(path, block, force, boundary) {
4827
4534
  const symlink = symlinkedComponent(boundary, path);
4828
4535
  if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
4829
- if (!(0, import_node_fs14.existsSync)(path)) return `${block}
4536
+ if (!(0, import_node_fs13.existsSync)(path)) return `${block}
4830
4537
  `;
4831
- const current = (0, import_node_fs14.readFileSync)(path, "utf8");
4538
+ const current = (0, import_node_fs13.readFileSync)(path, "utf8");
4832
4539
  const start = "<!-- odla-ai agent setup:start -->";
4833
4540
  const end = "<!-- odla-ai agent setup:end -->";
4834
4541
  const startAt = current.indexOf(start);
@@ -4857,7 +4564,7 @@ function symlinkedComponent(boundary, target) {
4857
4564
  for (const part of rel.split(import_node_path13.sep).filter(Boolean)) {
4858
4565
  current = (0, import_node_path13.join)(current, part);
4859
4566
  try {
4860
- if ((0, import_node_fs14.lstatSync)(current).isSymbolicLink()) return current;
4567
+ if ((0, import_node_fs13.lstatSync)(current).isSymbolicLink()) return current;
4861
4568
  } catch (error) {
4862
4569
  if (error.code !== "ENOENT") throw error;
4863
4570
  }
@@ -4868,10 +4575,10 @@ function skillNames(files) {
4868
4575
  return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
4869
4576
  }
4870
4577
  function listFiles(dir) {
4871
- if (!(0, import_node_fs14.existsSync)(dir)) return [];
4578
+ if (!(0, import_node_fs13.existsSync)(dir)) return [];
4872
4579
  const results = [];
4873
4580
  const walk = (current) => {
4874
- for (const entry of (0, import_node_fs14.readdirSync)(current, { withFileTypes: true })) {
4581
+ for (const entry of (0, import_node_fs13.readdirSync)(current, { withFileTypes: true })) {
4875
4582
  const path = (0, import_node_path13.join)(current, entry.name);
4876
4583
  if (entry.isDirectory()) walk(path);
4877
4584
  else results.push((0, import_node_path13.relative)(dir, path));
@@ -5188,7 +4895,7 @@ async function projectCommand(command, parsed, deps) {
5188
4895
  }
5189
4896
 
5190
4897
  // src/code-connect.ts
5191
- var import_node_fs15 = require("fs");
4898
+ var import_node_fs14 = require("fs");
5192
4899
  var import_node_os4 = require("os");
5193
4900
  var import_node_path15 = require("path");
5194
4901
 
@@ -5276,15 +4983,15 @@ function encodeAgentInput(message2) {
5276
4983
  // ../harness/dist/chunk-PHXQH4YM.js
5277
4984
  var import_child_process = require("child_process");
5278
4985
  var import_fs = require("fs");
5279
- var import_promises3 = require("fs/promises");
4986
+ var import_promises2 = require("fs/promises");
5280
4987
  var import_path = require("path");
5281
4988
  var import_process = require("process");
5282
- var import_promises4 = require("fs/promises");
4989
+ var import_promises3 = require("fs/promises");
5283
4990
  var import_os = require("os");
5284
4991
  var import_path2 = require("path");
5285
4992
  var import_child_process2 = require("child_process");
5286
4993
  var import_path3 = require("path");
5287
- var import_promises5 = require("fs/promises");
4994
+ var import_promises4 = require("fs/promises");
5288
4995
  var import_os2 = require("os");
5289
4996
  var import_path4 = require("path");
5290
4997
  var import_child_process3 = require("child_process");
@@ -5295,7 +5002,7 @@ function assertPinnedImage(image) {
5295
5002
  async function commandAvailable(engine) {
5296
5003
  for (const directory of (process.env.PATH ?? "").split(import_path.delimiter).filter(Boolean)) {
5297
5004
  try {
5298
- await (0, import_promises3.access)((0, import_path.join)(directory, engine), import_fs.constants.X_OK);
5005
+ await (0, import_promises2.access)((0, import_path.join)(directory, engine), import_fs.constants.X_OK);
5299
5006
  return true;
5300
5007
  } catch {
5301
5008
  }
@@ -5581,7 +5288,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
5581
5288
  }
5582
5289
  async function materializeGitTree(source, commitSha, options = {}) {
5583
5290
  if (!/^[0-9a-f]{40}$/.test(commitSha)) throw new TypeError("Git tree requires an exact commit SHA");
5584
- const sourceDir = await (0, import_promises4.realpath)((0, import_path2.resolve)(source));
5291
+ const sourceDir = await (0, import_promises3.realpath)((0, import_path2.resolve)(source));
5585
5292
  const maxFiles = options.maxFiles ?? 2e4;
5586
5293
  const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
5587
5294
  const inventory = (await gitOutput(sourceDir, ["ls-tree", "-rz", commitSha], 16 * 1024 * 1024)).toString("utf8").split("\0").filter(Boolean);
@@ -5590,9 +5297,9 @@ async function materializeGitTree(source, commitSha, options = {}) {
5590
5297
  return match && allowedWorkspacePath(match[3]) ? [{ mode: match[1], hash: match[2], path: match[3] }] : [];
5591
5298
  });
5592
5299
  if (entries.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
5593
- const root = await (0, import_promises4.mkdtemp)((0, import_path2.join)(options.tempRoot ?? (0, import_os.tmpdir)(), "odla-git-tree-"));
5300
+ const root = await (0, import_promises3.mkdtemp)((0, import_path2.join)(options.tempRoot ?? (0, import_os.tmpdir)(), "odla-git-tree-"));
5594
5301
  const targetRoot = (0, import_path2.join)(root, "source");
5595
- await (0, import_promises4.mkdir)(targetRoot);
5302
+ await (0, import_promises3.mkdir)(targetRoot);
5596
5303
  let byteCount = 0;
5597
5304
  try {
5598
5305
  const blobs = await gitBlobs(sourceDir, entries, maxBytes);
@@ -5602,18 +5309,18 @@ async function materializeGitTree(source, commitSha, options = {}) {
5602
5309
  if (byteCount > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
5603
5310
  const target = (0, import_path2.resolve)(targetRoot, entry.path);
5604
5311
  if (!target.startsWith(`${(0, import_path2.resolve)(targetRoot)}${import_path2.sep}`)) throw new TypeError("Git tree path escapes workspace");
5605
- await (0, import_promises4.mkdir)((0, import_path2.resolve)(target, ".."), { recursive: true });
5606
- await (0, import_promises4.writeFile)(target, content2, { flag: "wx", mode: entry.mode === "100755" ? 493 : 420 });
5312
+ await (0, import_promises3.mkdir)((0, import_path2.resolve)(target, ".."), { recursive: true });
5313
+ await (0, import_promises3.writeFile)(target, content2, { flag: "wx", mode: entry.mode === "100755" ? 493 : 420 });
5607
5314
  }
5608
5315
  return {
5609
5316
  root,
5610
5317
  sourceDir: targetRoot,
5611
5318
  fileCount: entries.length,
5612
5319
  byteCount,
5613
- cleanup: () => (0, import_promises4.rm)(root, { recursive: true, force: true })
5320
+ cleanup: () => (0, import_promises3.rm)(root, { recursive: true, force: true })
5614
5321
  };
5615
5322
  } catch (error) {
5616
- await (0, import_promises4.rm)(root, { recursive: true, force: true });
5323
+ await (0, import_promises3.rm)(root, { recursive: true, force: true });
5617
5324
  throw error;
5618
5325
  }
5619
5326
  }
@@ -5621,7 +5328,7 @@ async function sourceFiles(sourceDir, maxFiles, maxBytes) {
5621
5328
  const files = [];
5622
5329
  let bytes = 0;
5623
5330
  const walk = async (dir) => {
5624
- for (const entry of await (0, import_promises5.readdir)(dir, { withFileTypes: true })) {
5331
+ for (const entry of await (0, import_promises4.readdir)(dir, { withFileTypes: true })) {
5625
5332
  if (entry.isDirectory() && SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
5626
5333
  if (!entry.isDirectory() && SECRET_WORKSPACE_FILE.test(entry.name)) continue;
5627
5334
  const path = (0, import_path4.join)(dir, entry.name);
@@ -5631,7 +5338,7 @@ async function sourceFiles(sourceDir, maxFiles, maxBytes) {
5631
5338
  continue;
5632
5339
  }
5633
5340
  if (!entry.isFile()) continue;
5634
- const metadata2 = await (0, import_promises5.stat)(path);
5341
+ const metadata2 = await (0, import_promises4.stat)(path);
5635
5342
  bytes += metadata2.size;
5636
5343
  if (files.length + 1 > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
5637
5344
  if (bytes > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
@@ -5680,7 +5387,7 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
5680
5387
  if (!source.startsWith(`${root}${import_path4.sep}`)) throw new TypeError("git file path escapes workspace");
5681
5388
  let metadata2;
5682
5389
  try {
5683
- metadata2 = await (0, import_promises5.lstat)(source);
5390
+ metadata2 = await (0, import_promises4.lstat)(source);
5684
5391
  } catch (error) {
5685
5392
  if (error.code === "ENOENT") continue;
5686
5393
  throw error;
@@ -5695,9 +5402,9 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
5695
5402
  async function copyTree(files, destination) {
5696
5403
  for (const file of files) {
5697
5404
  const target = (0, import_path4.join)(destination, file.relativePath);
5698
- await (0, import_promises5.mkdir)((0, import_path4.resolve)(target, ".."), { recursive: true });
5699
- await (0, import_promises5.copyFile)(file.source, target);
5700
- await (0, import_promises5.chmod)(target, file.mode);
5405
+ await (0, import_promises4.mkdir)((0, import_path4.resolve)(target, ".."), { recursive: true });
5406
+ await (0, import_promises4.copyFile)(file.source, target);
5407
+ await (0, import_promises4.chmod)(target, file.mode);
5701
5408
  }
5702
5409
  }
5703
5410
  async function captureGitDiff(root, maxBytes) {
@@ -5734,13 +5441,13 @@ async function captureGitDiff(root, maxBytes) {
5734
5441
  return Buffer.concat(stdout).toString("utf8").replaceAll("a/baseline/", "a/").replaceAll("a/workspace/", "a/").replaceAll("b/baseline/", "b/").replaceAll("b/workspace/", "b/").replaceAll("--- a/baseline", "--- a").replaceAll("+++ b/workspace", "+++ b");
5735
5442
  }
5736
5443
  async function stageWorkspace(source, options = {}) {
5737
- const sourceDir = await (0, import_promises5.realpath)((0, import_path4.resolve)(source));
5738
- const sourceStat = await (0, import_promises5.stat)(sourceDir);
5444
+ const sourceDir = await (0, import_promises4.realpath)((0, import_path4.resolve)(source));
5445
+ const sourceStat = await (0, import_promises4.stat)(sourceDir);
5739
5446
  if (!sourceStat.isDirectory()) throw new TypeError("workspace source must be a directory");
5740
- const root = await (0, import_promises5.mkdtemp)((0, import_path4.join)(options.tempRoot ?? (0, import_os2.tmpdir)(), "odla-harness-"));
5447
+ const root = await (0, import_promises4.mkdtemp)((0, import_path4.join)(options.tempRoot ?? (0, import_os2.tmpdir)(), "odla-harness-"));
5741
5448
  const baselineDir = (0, import_path4.join)(root, "baseline");
5742
5449
  const workspaceDir = (0, import_path4.join)(root, "workspace");
5743
- await Promise.all([(0, import_promises5.mkdir)(baselineDir), (0, import_promises5.mkdir)(workspaceDir)]);
5450
+ await Promise.all([(0, import_promises4.mkdir)(baselineDir), (0, import_promises4.mkdir)(workspaceDir)]);
5744
5451
  try {
5745
5452
  const maxFiles = options.maxFiles ?? 2e4;
5746
5453
  const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
@@ -5753,26 +5460,26 @@ async function stageWorkspace(source, options = {}) {
5753
5460
  fileCount: files.length,
5754
5461
  byteCount: files.reduce((sum, file) => sum + file.bytes, 0),
5755
5462
  patch: (maxBytes2) => captureGitDiff(root, maxBytes2),
5756
- cleanup: () => (0, import_promises5.rm)(root, { recursive: true, force: true })
5463
+ cleanup: () => (0, import_promises4.rm)(root, { recursive: true, force: true })
5757
5464
  };
5758
5465
  } catch (error) {
5759
- await (0, import_promises5.rm)(root, { recursive: true, force: true });
5466
+ await (0, import_promises4.rm)(root, { recursive: true, force: true });
5760
5467
  throw error;
5761
5468
  }
5762
5469
  }
5763
5470
  async function stageWorkspacePair(baselineSource, workspaceSource, options = {}) {
5764
- const baselineDirSource = await (0, import_promises5.realpath)((0, import_path4.resolve)(baselineSource));
5765
- const workspaceDirSource = await (0, import_promises5.realpath)((0, import_path4.resolve)(workspaceSource));
5471
+ const baselineDirSource = await (0, import_promises4.realpath)((0, import_path4.resolve)(baselineSource));
5472
+ const workspaceDirSource = await (0, import_promises4.realpath)((0, import_path4.resolve)(workspaceSource));
5766
5473
  const maxFiles = options.maxFiles ?? 2e4;
5767
5474
  const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
5768
5475
  const [baselineFiles, workspaceFiles] = await Promise.all([
5769
5476
  sourceFiles(baselineDirSource, maxFiles, maxBytes),
5770
5477
  sourceFiles(workspaceDirSource, maxFiles, maxBytes)
5771
5478
  ]);
5772
- const root = await (0, import_promises5.mkdtemp)((0, import_path4.join)(options.tempRoot ?? (0, import_os2.tmpdir)(), "odla-harness-"));
5479
+ const root = await (0, import_promises4.mkdtemp)((0, import_path4.join)(options.tempRoot ?? (0, import_os2.tmpdir)(), "odla-harness-"));
5773
5480
  const baselineDir = (0, import_path4.join)(root, "baseline");
5774
5481
  const workspaceDir = (0, import_path4.join)(root, "workspace");
5775
- await Promise.all([(0, import_promises5.mkdir)(baselineDir), (0, import_promises5.mkdir)(workspaceDir)]);
5482
+ await Promise.all([(0, import_promises4.mkdir)(baselineDir), (0, import_promises4.mkdir)(workspaceDir)]);
5776
5483
  try {
5777
5484
  await Promise.all([copyTree(baselineFiles, baselineDir), copyTree(workspaceFiles, workspaceDir)]);
5778
5485
  return {
@@ -5782,17 +5489,17 @@ async function stageWorkspacePair(baselineSource, workspaceSource, options = {})
5782
5489
  fileCount: workspaceFiles.length,
5783
5490
  byteCount: workspaceFiles.reduce((sum, file) => sum + file.bytes, 0),
5784
5491
  patch: (maxPatchBytes) => captureGitDiff(root, maxPatchBytes),
5785
- cleanup: () => (0, import_promises5.rm)(root, { recursive: true, force: true })
5492
+ cleanup: () => (0, import_promises4.rm)(root, { recursive: true, force: true })
5786
5493
  };
5787
5494
  } catch (error) {
5788
- await (0, import_promises5.rm)(root, { recursive: true, force: true });
5495
+ await (0, import_promises4.rm)(root, { recursive: true, force: true });
5789
5496
  throw error;
5790
5497
  }
5791
5498
  }
5792
5499
 
5793
5500
  // ../harness/dist/chunk-GMVZ4LZH.js
5794
5501
  var import_crypto = require("crypto");
5795
- var import_promises6 = require("fs/promises");
5502
+ var import_promises5 = require("fs/promises");
5796
5503
  var import_path5 = require("path");
5797
5504
 
5798
5505
  // ../camel/dist/chunk-7FHPOQVP.js
@@ -6129,19 +5836,19 @@ function validateSnapshot(snapshot, limits) {
6129
5836
 
6130
5837
  // ../harness/dist/chunk-GMVZ4LZH.js
6131
5838
  var import_child_process4 = require("child_process");
6132
- var import_promises7 = require("fs/promises");
5839
+ var import_promises6 = require("fs/promises");
6133
5840
  var import_path6 = require("path");
6134
5841
  var import_child_process5 = require("child_process");
6135
5842
  var import_process2 = require("process");
6136
5843
  var import_crypto2 = require("crypto");
6137
5844
  var import_crypto3 = require("crypto");
6138
5845
  var import_fs2 = require("fs");
6139
- var import_promises8 = require("fs/promises");
5846
+ var import_promises7 = require("fs/promises");
6140
5847
  var import_path7 = require("path");
6141
- var import_promises9 = require("fs/promises");
5848
+ var import_promises8 = require("fs/promises");
6142
5849
  var import_os3 = require("os");
6143
5850
  var import_path8 = require("path");
6144
- var import_promises10 = require("fs/promises");
5851
+ var import_promises9 = require("fs/promises");
6145
5852
  var import_path9 = require("path");
6146
5853
 
6147
5854
  // ../camel/dist/chunk-4EIRFS3A.js
@@ -6433,7 +6140,7 @@ var import_crypto4 = require("crypto");
6433
6140
  async function digestStagedWorkspace(root, limits) {
6434
6141
  const files = [];
6435
6142
  const walk = async (directory) => {
6436
- const entries = await (0, import_promises6.readdir)(directory, { withFileTypes: true });
6143
+ const entries = await (0, import_promises5.readdir)(directory, { withFileTypes: true });
6437
6144
  for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
6438
6145
  if (entry.isSymbolicLink()) throw new TypeError("workspace digest refuses symbolic links");
6439
6146
  const target = (0, import_path5.resolve)(directory, entry.name);
@@ -6448,7 +6155,7 @@ async function digestStagedWorkspace(root, limits) {
6448
6155
  const hash = (0, import_crypto.createHash)("sha256");
6449
6156
  let bytes = 0;
6450
6157
  for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
6451
- const content2 = await (0, import_promises6.readFile)(file.target);
6158
+ const content2 = await (0, import_promises5.readFile)(file.target);
6452
6159
  bytes += Buffer.byteLength(file.path) + content2.byteLength;
6453
6160
  if (bytes > limits.maxBytes) throw new TypeError("workspace digest exceeds its byte bound");
6454
6161
  hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content2.byteLength}:`);
@@ -6774,7 +6481,7 @@ async function applyCodePatch(workspaceDir, patch2, paths) {
6774
6481
  await gitApply(workspaceDir, patch2, false);
6775
6482
  for (const path of paths) {
6776
6483
  try {
6777
- const info = await (0, import_promises7.lstat)(resolveCodePath(workspaceDir, path));
6484
+ const info = await (0, import_promises6.lstat)(resolveCodePath(workspaceDir, path));
6778
6485
  if (info.isSymbolicLink() || !info.isFile() && !info.isDirectory()) {
6779
6486
  throw new TypeError("patch created a non-regular workspace entry");
6780
6487
  }
@@ -7065,7 +6772,7 @@ async function inspectArtifacts(workspaceDir, recipe2) {
7065
6772
  for (const artifact of recipe2.expectedArtifacts ?? []) {
7066
6773
  try {
7067
6774
  const path = (0, import_path7.join)(workspaceDir, artifact.path);
7068
- const info = await (0, import_promises8.lstat)(path);
6775
+ const info = await (0, import_promises7.lstat)(path);
7069
6776
  if (!info.isFile() || info.isSymbolicLink()) {
7070
6777
  receipts.push({ artifactId: artifact.id, status: "invalid", bytes: null, digest: null });
7071
6778
  } else if (info.size > artifact.maximumBytes) {
@@ -7246,9 +6953,9 @@ var RESERVED2 = /* @__PURE__ */ new Set([".git", ".odla", ".wrangler", "node_mod
7246
6953
  var SECRET2 = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-token(?:\..+)?\.json)$/i;
7247
6954
  async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os3.tmpdir)()) {
7248
6955
  if (!snapshot.files.length || snapshot.files.length > 1e4) throw new TypeError("Code source file count is invalid");
7249
- const root = await (0, import_promises9.mkdtemp)((0, import_path8.join)(tempRoot, "odla-code-source-"));
6956
+ const root = await (0, import_promises8.mkdtemp)((0, import_path8.join)(tempRoot, "odla-code-source-"));
7250
6957
  const sourceDir = (0, import_path8.join)(root, "source");
7251
- await (0, import_promises9.mkdir)(sourceDir);
6958
+ await (0, import_promises8.mkdir)(sourceDir);
7252
6959
  const seen = /* @__PURE__ */ new Set();
7253
6960
  let bytes = 0;
7254
6961
  try {
@@ -7260,8 +6967,8 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os3.
7260
6967
  if (bytes > 16 * 1024 * 1024) throw new TypeError("Code source exceeds its byte bound");
7261
6968
  const target = (0, import_path8.resolve)(sourceDir, file.path);
7262
6969
  if (!target.startsWith(`${(0, import_path8.resolve)(sourceDir)}${import_path8.sep}`)) throw new TypeError("Code source path escapes its root");
7263
- await (0, import_promises9.mkdir)((0, import_path8.dirname)(target), { recursive: true });
7264
- await (0, import_promises9.writeFile)(target, file.content, { flag: "wx", mode: 420 });
6970
+ await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
6971
+ await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 420 });
7265
6972
  }
7266
6973
  for (const reference of snapshot.references ?? []) {
7267
6974
  validateAlias(reference.alias);
@@ -7275,13 +6982,13 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os3.
7275
6982
  if (bytes > 80 * 1024 * 1024) throw new TypeError("Code source set exceeds its byte bound");
7276
6983
  const target = (0, import_path8.resolve)(sourceDir, path);
7277
6984
  if (!target.startsWith(`${(0, import_path8.resolve)(sourceDir)}${import_path8.sep}`)) throw new TypeError("Code reference path escapes its root");
7278
- await (0, import_promises9.mkdir)((0, import_path8.dirname)(target), { recursive: true });
7279
- await (0, import_promises9.writeFile)(target, file.content, { flag: "wx", mode: 292 });
6985
+ await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
6986
+ await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 292 });
7280
6987
  }
7281
6988
  }
7282
- return { sourceDir, cleanup: () => (0, import_promises9.rm)(root, { recursive: true, force: true }) };
6989
+ return { sourceDir, cleanup: () => (0, import_promises8.rm)(root, { recursive: true, force: true }) };
7283
6990
  } catch (cause) {
7284
- await (0, import_promises9.rm)(root, { recursive: true, force: true });
6991
+ await (0, import_promises8.rm)(root, { recursive: true, force: true });
7285
6992
  throw cause;
7286
6993
  }
7287
6994
  }
@@ -7302,8 +7009,8 @@ async function attachCodeRuntimeReferences(workspace, references) {
7302
7009
  for (const root of [workspace.baselineDir, workspace.workspaceDir]) {
7303
7010
  const target = (0, import_path8.resolve)(root, path);
7304
7011
  if (!target.startsWith(`${(0, import_path8.resolve)(root)}${import_path8.sep}`)) throw new TypeError("Code reference path escapes its root");
7305
- await (0, import_promises9.mkdir)((0, import_path8.dirname)(target), { recursive: true });
7306
- await (0, import_promises9.writeFile)(target, file.content, { flag: "wx", mode: 292 });
7012
+ await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
7013
+ await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 292 });
7307
7014
  }
7308
7015
  }
7309
7016
  }
@@ -7489,11 +7196,11 @@ async function read(context, request2, options, policy) {
7489
7196
  const allowed = await policy.read(policyContext(context, request2, options, { paths, path, startLine, endLine }));
7490
7197
  if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
7491
7198
  const target = resolveCodePath(context.workspaceDir, path);
7492
- const info = await (0, import_promises10.stat)(target);
7199
+ const info = await (0, import_promises9.stat)(target);
7493
7200
  if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
7494
7201
  throw new TypeError("file is not a bounded regular source file");
7495
7202
  }
7496
- const source = await (0, import_promises10.readFile)(target);
7203
+ const source = await (0, import_promises9.readFile)(target);
7497
7204
  if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
7498
7205
  const lines = source.toString("utf8").split("\n");
7499
7206
  const content2 = lines.slice(startLine - 1, endLine).join("\n");
@@ -7570,7 +7277,7 @@ function policyContext(context, request2, options, extra) {
7570
7277
  async function registeredFiles(root, limit) {
7571
7278
  const paths = [];
7572
7279
  const walk = async (directory) => {
7573
- for (const entry of await (0, import_promises10.readdir)(directory, { withFileTypes: true })) {
7280
+ for (const entry of await (0, import_promises9.readdir)(directory, { withFileTypes: true })) {
7574
7281
  if (entry.isSymbolicLink()) throw new TypeError("workspace contains a symbolic link");
7575
7282
  const target = (0, import_path9.resolve)(directory, entry.name);
7576
7283
  if (entry.isDirectory()) await walk(target);
@@ -8355,7 +8062,7 @@ function digestText(value2) {
8355
8062
  // src/code-images.ts
8356
8063
  var import_node_child_process6 = require("child_process");
8357
8064
  var import_node_crypto4 = require("crypto");
8358
- var import_promises11 = require("fs/promises");
8065
+ var import_promises10 = require("fs/promises");
8359
8066
  var import_node_os3 = require("os");
8360
8067
  var import_node_path14 = require("path");
8361
8068
  var import_node_url3 = require("url");
@@ -8437,16 +8144,16 @@ function embeddedPiAssetPath() {
8437
8144
  return (0, import_node_url3.fileURLToPath)(new URL("./runtime/pi-agent.js", importMetaUrl));
8438
8145
  }
8439
8146
  async function embeddedPiImageName() {
8440
- const bundle = await (0, import_promises11.readFile)(embeddedPiAssetPath()).catch(() => {
8147
+ const bundle = await (0, import_promises10.readFile)(embeddedPiAssetPath()).catch(() => {
8441
8148
  throw new Error("CLI-embedded Pi runtime is missing; reinstall this exact @odla-ai/cli version");
8442
8149
  });
8443
8150
  return `odla-ai/pi-agent:embedded-sha256-${(0, import_node_crypto4.createHash)("sha256").update(bundle).digest("hex")}`;
8444
8151
  }
8445
8152
  async function buildEmbeddedPiImage(engine, image, run) {
8446
- const context = await (0, import_promises11.mkdtemp)((0, import_node_path14.join)((0, import_node_os3.tmpdir)(), "odla-code-pi-"));
8153
+ const context = await (0, import_promises10.mkdtemp)((0, import_node_path14.join)((0, import_node_os3.tmpdir)(), "odla-code-pi-"));
8447
8154
  try {
8448
- await (0, import_promises11.copyFile)(embeddedPiAssetPath(), (0, import_node_path14.join)(context, "pi-agent.js"));
8449
- await (0, import_promises11.writeFile)((0, import_node_path14.join)(context, "Dockerfile"), [
8155
+ await (0, import_promises10.copyFile)(embeddedPiAssetPath(), (0, import_node_path14.join)(context, "pi-agent.js"));
8156
+ await (0, import_promises10.writeFile)((0, import_node_path14.join)(context, "Dockerfile"), [
8450
8157
  `FROM ${CODE_NODE_IMAGE}`,
8451
8158
  "COPY pi-agent.js /opt/odla/pi-agent.js",
8452
8159
  "WORKDIR /workspace",
@@ -8455,7 +8162,7 @@ async function buildEmbeddedPiImage(engine, image, run) {
8455
8162
  ].join("\n"), { mode: 384 });
8456
8163
  await run(engine, ["build", "--tag", image, context], "inherit");
8457
8164
  } finally {
8458
- await (0, import_promises11.rm)(context, { recursive: true, force: true });
8165
+ await (0, import_promises10.rm)(context, { recursive: true, force: true });
8459
8166
  }
8460
8167
  }
8461
8168
 
@@ -8463,7 +8170,7 @@ async function buildEmbeddedPiImage(engine, image, run) {
8463
8170
  async function codeConnect(options) {
8464
8171
  const cwd = options.cwd ?? process.cwd();
8465
8172
  const configPath = (0, import_node_path15.resolve)(cwd, options.configPath);
8466
- const cfg = (0, import_node_fs15.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
8173
+ const cfg = (0, import_node_fs14.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
8467
8174
  const requestedAppId = options.appId?.trim();
8468
8175
  if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
8469
8176
  throw new Error("--app-id must be a valid odla app id");
@@ -8854,18 +8561,17 @@ Usage:
8854
8561
  odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
8855
8562
  odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
8856
8563
  odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
8857
- odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
8858
- odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
8859
- odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
8860
- odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
8564
+ odla-ai calendar disconnect [--env dev] --yes [continue in Studio; human session required]
8565
+ odla-ai app archive [--config odla.config.mjs] --yes [continue in Studio; human session required]
8566
+ odla-ai app restore [--config odla.config.mjs] [continue in Studio; human session required]
8567
+ odla-ai app export [--env dev] [continue in Studio; human session required]
8861
8568
  odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
8862
- odla-ai app refresh-sandbox [--include-identity] [--include-files] [--dry-run] [--json] --yes
8863
- odla-ai app go-live [--include-identity] [--include-files] [--dry-run] [--json] --yes
8864
- odla-ai app promote [--dry-run] [--json] --yes
8865
- odla-ai app rename <name> [--config odla.config.mjs] [--email <odla-account>] [--json]
8866
- odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
8867
- odla-ai app owners add <email> [--email <odla-account>] [--json]
8868
- odla-ai app owners remove <email> [--email <odla-account>] [--json]
8569
+ [dry-run is local; writes continue in Studio]
8570
+ odla-ai app refresh-sandbox [continue in Studio; human session required]
8571
+ odla-ai app go-live [continue in Studio; human session required]
8572
+ odla-ai app promote [continue in Studio; human session required]
8573
+ odla-ai app rename <name> [continue in Studio; human session required]
8574
+ odla-ai app owners <list|add|remove> [...] [continue in Studio; human session required]
8869
8575
  odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
8870
8576
  odla-ai pm goal list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
8871
8577
  odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
@@ -8900,8 +8606,8 @@ Usage:
8900
8606
  odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
8901
8607
  odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
8902
8608
  odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
8903
- odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
8904
- odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
8609
+ odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--token <ODLA_API_KEY>] [--json]
8610
+ odla-ai agent retry <job-id> [--env dev] [--token <ODLA_API_KEY>] [--json]
8905
8611
  odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
8906
8612
  odla-ai context list [--json]
8907
8613
  odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
@@ -8937,8 +8643,8 @@ Usage:
8937
8643
  odla-ai admin ai credential set <provider> [--context <name>] (--from-env <NAME>|--stdin)
8938
8644
  odla-ai admin ai usage [--context <name>] [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
8939
8645
  odla-ai admin ai audit [--context <name>] [--limit <1-200>] [--json]
8940
- odla-ai security github connect [--repo owner/name] [--env dev] [--email <odla-account>] [--no-open]
8941
- odla-ai security github disconnect --source <id> [--env dev] [--yes]
8646
+ odla-ai security github connect [--repo owner/name] [--env dev] [continue in Studio; human session required]
8647
+ odla-ai security github disconnect --source <id> [--env dev] [continue in Studio; human session required]
8942
8648
  odla-ai security plan [--env dev] [--json]
8943
8649
  odla-ai security sources [--env dev] [--json]
8944
8650
  odla-ai security run --source <id> --plan-digest <sha256:...> --ack-redacted-source [--ref <branch|tag|sha>] [--env dev] [--no-follow]
@@ -9449,7 +9155,7 @@ async function discussWatch(ctx, topicId, parsed) {
9449
9155
  throw new WatchRemoteError(cursor, error);
9450
9156
  }
9451
9157
  if (deadline !== void 0 && now() >= deadline) {
9452
- const result = report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
9158
+ const result = report(ctx, parsed, { found: false, cursor: cursor ?? "" });
9453
9159
  throw new WatchTimeoutError(result.cursor);
9454
9160
  }
9455
9161
  const base = Math.min(intervalMs, 1e3);
@@ -9490,7 +9196,7 @@ async function discussWatch(ctx, topicId, parsed) {
9490
9196
  });
9491
9197
  const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
9492
9198
  const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
9493
- return report2(ctx, parsed, {
9199
+ return report(ctx, parsed, {
9494
9200
  found: true,
9495
9201
  cursor,
9496
9202
  events: matching,
@@ -9517,13 +9223,13 @@ async function discussWatch(ctx, topicId, parsed) {
9517
9223
  }
9518
9224
  if (page2.hasMore) continue;
9519
9225
  if (deadline !== void 0 && now() >= deadline) {
9520
- return report2(ctx, parsed, { found: false, cursor });
9226
+ return report(ctx, parsed, { found: false, cursor });
9521
9227
  }
9522
9228
  const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
9523
9229
  await sleep(wait2);
9524
9230
  }
9525
9231
  }
9526
- function report2(ctx, parsed, result) {
9232
+ function report(ctx, parsed, result) {
9527
9233
  if (ctx.json) {
9528
9234
  ctx.out.log(JSON.stringify(result, null, 2));
9529
9235
  } else if (parsed.options.jsonl !== true && result.found) {
@@ -10024,7 +9730,7 @@ function eventLabel(event) {
10024
9730
  const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
10025
9731
  return body || event.payload.entityId;
10026
9732
  }
10027
- function report3(ctx, parsed, result) {
9733
+ function report2(ctx, parsed, result) {
10028
9734
  if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
10029
9735
  else if (parsed.options.jsonl !== true && result.found) {
10030
9736
  for (const event of result.events ?? []) {
@@ -10084,7 +9790,7 @@ async function pmWatch(ctx, parsed) {
10084
9790
  });
10085
9791
  if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
10086
9792
  if (deadline !== void 0 && now() >= deadline) {
10087
- return report3(ctx, parsed, { found: false, cursor: cursor ?? "" });
9793
+ return report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
10088
9794
  }
10089
9795
  const backoff = Math.min(
10090
9796
  MAX_BACKOFF_MS2,
@@ -10125,7 +9831,7 @@ async function pmWatch(ctx, parsed) {
10125
9831
  cursor,
10126
9832
  serverTime: current.serverTime
10127
9833
  });
10128
- return report3(ctx, parsed, { found: true, cursor, events: matching });
9834
+ return report2(ctx, parsed, { found: true, cursor, events: matching });
10129
9835
  }
10130
9836
  if (current.events.length > 0) {
10131
9837
  jsonl2(ctx, parsed, {
@@ -10144,7 +9850,7 @@ async function pmWatch(ctx, parsed) {
10144
9850
  }
10145
9851
  if (current.hasMore) continue;
10146
9852
  if (deadline !== void 0 && now() >= deadline) {
10147
- return report3(ctx, parsed, { found: false, cursor });
9853
+ return report2(ctx, parsed, { found: false, cursor });
10148
9854
  }
10149
9855
  await sleep(
10150
9856
  deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
@@ -11177,7 +10883,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
11177
10883
  }
11178
10884
 
11179
10885
  // src/record.ts
11180
- var import_node_fs16 = require("fs");
10886
+ var import_node_fs15 = require("fs");
11181
10887
  var import_node_process12 = __toESM(require("process"), 1);
11182
10888
 
11183
10889
  // src/surface.ts
@@ -11357,14 +11063,14 @@ function recordInvocation(parsed) {
11357
11063
  options: Object.entries(parsed.options).map(([name, value2]) => value2 === false ? `no-${name}` : name).sort()
11358
11064
  };
11359
11065
  if (!entry.path.length) return;
11360
- (0, import_node_fs16.appendFileSync)(file, `${JSON.stringify(entry)}
11066
+ (0, import_node_fs15.appendFileSync)(file, `${JSON.stringify(entry)}
11361
11067
  `);
11362
11068
  } catch {
11363
11069
  }
11364
11070
  }
11365
11071
 
11366
11072
  // src/runbook-actions.ts
11367
- var import_node_fs17 = require("fs");
11073
+ var import_node_fs16 = require("fs");
11368
11074
 
11369
11075
  // src/runbook-requires.ts
11370
11076
  var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
@@ -11449,7 +11155,7 @@ async function bySlug(ctx, slug) {
11449
11155
  function readBody(file, inline) {
11450
11156
  if (inline !== void 0) return inline;
11451
11157
  if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
11452
- return (0, import_node_fs17.readFileSync)(file === "-" ? 0 : file, "utf8");
11158
+ return (0, import_node_fs16.readFileSync)(file === "-" ? 0 : file, "utf8");
11453
11159
  }
11454
11160
  var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
11455
11161
  async function runbookList(ctx, all, query) {
@@ -11541,7 +11247,7 @@ async function runbookRemove(ctx, slug) {
11541
11247
  }
11542
11248
 
11543
11249
  // src/runbook-import.ts
11544
- var import_node_fs18 = require("fs");
11250
+ var import_node_fs17 = require("fs");
11545
11251
  var import_node_path16 = require("path");
11546
11252
  function parseRunbook(text2, slug) {
11547
11253
  let rest = text2;
@@ -11567,12 +11273,12 @@ function parseRunbook(text2, slug) {
11567
11273
  };
11568
11274
  }
11569
11275
  function readRunbookDir(dir) {
11570
- if (!(0, import_node_fs18.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
11571
- const files = (0, import_node_fs18.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
11276
+ if (!(0, import_node_fs17.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
11277
+ const files = (0, import_node_fs17.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
11572
11278
  if (!files.length) throw new Error(`no .md files in ${dir}`);
11573
11279
  return files.map((file) => {
11574
11280
  const slug = (0, import_node_path16.basename)(file, ".md");
11575
- const parsed = parseRunbook((0, import_node_fs18.readFileSync)((0, import_node_path16.join)(dir, file), "utf8"), slug);
11281
+ const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0, import_node_path16.join)(dir, file), "utf8"), slug);
11576
11282
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
11577
11283
  });
11578
11284
  }
@@ -11645,7 +11351,7 @@ async function upsert(ctx, r, visibility) {
11645
11351
 
11646
11352
  // src/runbook-impact.ts
11647
11353
  var import_node_child_process7 = require("child_process");
11648
- var import_node_fs19 = require("fs");
11354
+ var import_node_fs18 = require("fs");
11649
11355
  var import_node_path17 = require("path");
11650
11356
 
11651
11357
  // src/runbook-impact-scan.ts
@@ -11816,9 +11522,9 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
11816
11522
  function manifestLabeller(root) {
11817
11523
  return (workspace) => {
11818
11524
  const manifest = (0, import_node_path17.join)(root, workspace, "package.json");
11819
- if (!(0, import_node_fs19.existsSync)(manifest)) return void 0;
11525
+ if (!(0, import_node_fs18.existsSync)(manifest)) return void 0;
11820
11526
  try {
11821
- const name = JSON.parse((0, import_node_fs19.readFileSync)(manifest, "utf8")).name;
11527
+ const name = JSON.parse((0, import_node_fs18.readFileSync)(manifest, "utf8")).name;
11822
11528
  return typeof name === "string" ? name : void 0;
11823
11529
  } catch {
11824
11530
  return void 0;
@@ -11856,7 +11562,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
11856
11562
  return out;
11857
11563
  }
11858
11564
  var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
11859
- function report4(ctx, impacts) {
11565
+ function report3(ctx, impacts) {
11860
11566
  const covered = impacts.filter((i) => i.runbooks.length);
11861
11567
  ctx.out.log(
11862
11568
  `${impacts.length} changed surface${impacts.length === 1 ? "" : "s"}; ${covered.length} covered by a runbook. Reread each one and fix any step this change made wrong.`
@@ -11885,7 +11591,7 @@ function report4(ctx, impacts) {
11885
11591
  async function runbookImpact(ctx, options, deps = {}) {
11886
11592
  const cwd = deps.cwd ?? process.cwd();
11887
11593
  const runGit = deps.runGit ?? gitRunner(cwd);
11888
- const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs19.readFileSync)((0, import_node_path17.join)(cwd, path), "utf8"));
11594
+ const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0, import_node_path17.join)(cwd, path), "utf8"));
11889
11595
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
11890
11596
  if (!surfaces.length) {
11891
11597
  return ctx.out.log(
@@ -11894,7 +11600,7 @@ async function runbookImpact(ctx, options, deps = {}) {
11894
11600
  }
11895
11601
  const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
11896
11602
  if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
11897
- report4(ctx, impacts);
11603
+ report3(ctx, impacts);
11898
11604
  }
11899
11605
 
11900
11606
  // src/runbook-lint.ts
@@ -12018,7 +11724,7 @@ async function runbookComment(ctx, slug, body) {
12018
11724
 
12019
11725
  // src/runbook-editor.ts
12020
11726
  var import_node_child_process8 = require("child_process");
12021
- var import_node_fs20 = require("fs");
11727
+ var import_node_fs19 = require("fs");
12022
11728
  var import_node_os5 = require("os");
12023
11729
  var import_node_path18 = require("path");
12024
11730
  var import_node_process13 = __toESM(require("process"), 1);
@@ -12046,16 +11752,16 @@ function editText(initial, slug, deps = {}) {
12046
11752
  );
12047
11753
  if (!interactive())
12048
11754
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
12049
- const dir = (0, import_node_fs20.mkdtempSync)((0, import_node_path18.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
11755
+ const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path18.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
12050
11756
  const file = (0, import_node_path18.join)(dir, `${slug}.md`);
12051
11757
  try {
12052
- (0, import_node_fs20.writeFileSync)(file, initial, { mode: 384 });
11758
+ (0, import_node_fs19.writeFileSync)(file, initial, { mode: 384 });
12053
11759
  const code = defaultRunOrInjected(deps)(editor, file);
12054
11760
  if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
12055
- const edited = (0, import_node_fs20.readFileSync)(file, "utf8");
11761
+ const edited = (0, import_node_fs19.readFileSync)(file, "utf8");
12056
11762
  return edited === initial ? null : edited;
12057
11763
  } finally {
12058
- (0, import_node_fs20.rmSync)(dir, { recursive: true, force: true });
11764
+ (0, import_node_fs19.rmSync)(dir, { recursive: true, force: true });
12059
11765
  }
12060
11766
  }
12061
11767
  var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
@@ -12394,7 +12100,6 @@ async function runbookCommand(parsed, deps = {}) {
12394
12100
  }
12395
12101
 
12396
12102
  // src/security-command-context.ts
12397
- var import_promises12 = require("readline/promises");
12398
12103
  async function hostedSecurityContext(parsed, dependencies) {
12399
12104
  const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
12400
12105
  const cfg = await loadProjectConfig(configPath);
@@ -12413,21 +12118,11 @@ async function hostedSecurityContext(parsed, dependencies) {
12413
12118
  cfg,
12414
12119
  { configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
12415
12120
  doFetch,
12416
- stdout
12121
+ stdout,
12122
+ { optionalProjectCapabilities: ["app.manage"] }
12417
12123
  );
12418
12124
  return { platform, token, appId: cfg.app.id, env, fetch: doFetch, stdout };
12419
12125
  }
12420
- async function interactiveConfirmation(message2, dependencies) {
12421
- if (dependencies.confirm) return dependencies.confirm(message2);
12422
- if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
12423
- const prompt = (0, import_promises12.createInterface)({ input: process.stdin, output: process.stdout });
12424
- try {
12425
- const answer = await prompt.question(`${message2} [y/N] `);
12426
- return /^y(?:es)?$/i.test(answer.trim());
12427
- } finally {
12428
- prompt.close();
12429
- }
12430
- }
12431
12126
  function requiredSecurityPositional(parsed, index, label) {
12432
12127
  const value2 = parsed.positionals[index];
12433
12128
  if (!value2) throw new Error(`${label} is required`);
@@ -12493,31 +12188,31 @@ function printHostedJob(out, job, platform, appId) {
12493
12188
  url.searchParams.set("job", job.jobId);
12494
12189
  out.log(` Studio: ${url.toString()}`);
12495
12190
  }
12496
- function printHostedReport(out, report5) {
12497
- out.log(`security report ${report5.jobId}: ${report5.repository}@${report5.revision}`);
12498
- 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}`);
12499
- out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates} rejected=${report5.metrics.rejected}`);
12500
- out.log(` discovery: ${report5.provenance.discovery?.provider ?? "unknown"}/${report5.provenance.discovery?.model ?? "unknown"}`);
12501
- out.log(` validation: ${report5.provenance.validation?.provider ?? "unknown"}/${report5.provenance.validation?.model ?? "unknown"} independent=${String(report5.provenance.independentValidation)}`);
12502
- for (const finding of report5.findings) {
12191
+ function printHostedReport(out, report4) {
12192
+ out.log(`security report ${report4.jobId}: ${report4.repository}@${report4.revision}`);
12193
+ 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}`);
12194
+ out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates} rejected=${report4.metrics.rejected}`);
12195
+ out.log(` discovery: ${report4.provenance.discovery?.provider ?? "unknown"}/${report4.provenance.discovery?.model ?? "unknown"}`);
12196
+ out.log(` validation: ${report4.provenance.validation?.provider ?? "unknown"}/${report4.provenance.validation?.model ?? "unknown"} independent=${String(report4.provenance.independentValidation)}`);
12197
+ for (const finding of report4.findings) {
12503
12198
  const location = finding.locations[0];
12504
12199
  out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
12505
12200
  }
12506
- for (const limitation of report5.limitations) out.log(` limitation: ${limitation}`);
12201
+ for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
12507
12202
  }
12508
- function enforceHostedReportGate(report5, parsed, out, emitSuccess) {
12203
+ function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
12509
12204
  const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
12510
12205
  const candidateValue = parsed.options["fail-on-candidates"];
12511
12206
  const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
12512
12207
  const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
12513
- const confirmed = report5.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
12514
- const leads = failOnCandidates ? report5.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
12515
- const incomplete = report5.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
12208
+ const confirmed = report4.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
12209
+ const leads = failOnCandidates ? report4.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
12210
+ const incomplete = report4.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
12516
12211
  if (confirmed.length || leads.length || incomplete) {
12517
- throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report5.coverageStatus}` : ""}`);
12212
+ throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report4.coverageStatus}` : ""}`);
12518
12213
  }
12519
12214
  if (emitSuccess) {
12520
- out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report5.coverageStatus}. This is not proof that the application is secure.`);
12215
+ out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report4.coverageStatus}. This is not proof that the application is secure.`);
12521
12216
  }
12522
12217
  }
12523
12218
  function printHostedSecurityPlanRoute(out, label, route2) {
@@ -12600,17 +12295,17 @@ async function runHostedSecurity(options) {
12600
12295
  allowNetwork: false
12601
12296
  }
12602
12297
  });
12603
- const report5 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
12604
- await (0, import_node3.writeSecurityArtifacts)(output, report5);
12605
- const reportDigest = await (0, import_security.securityFingerprint)(report5);
12298
+ const report4 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
12299
+ await (0, import_node3.writeSecurityArtifacts)(output, report4);
12300
+ const reportDigest = await (0, import_security.securityFingerprint)(report4);
12606
12301
  await hosted.complete({
12607
12302
  reportDigest,
12608
- coverageStatus: report5.coverageStatus,
12609
- confirmed: report5.metrics.confirmed,
12610
- candidates: report5.metrics.candidates
12303
+ coverageStatus: report4.coverageStatus,
12304
+ confirmed: report4.metrics.confirmed,
12305
+ candidates: report4.metrics.candidates
12611
12306
  }, { signal: options.signal });
12612
- printSummary(options.stdout ?? console, appId, env, hosted.run, report5, output);
12613
- return Object.freeze({ report: report5, run: hosted.run, output });
12307
+ printSummary(options.stdout ?? console, appId, env, hosted.run, report4, output);
12308
+ return Object.freeze({ report: report4, run: hosted.run, output });
12614
12309
  }
12615
12310
  function selectEnv(requested, declared, configPath, rootDir) {
12616
12311
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
@@ -12636,14 +12331,14 @@ function profileFor(name, maxHuntTasks) {
12636
12331
  if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
12637
12332
  return { ...profile, maxHuntTasks };
12638
12333
  }
12639
- function printSummary(out, appId, env, run, report5, output) {
12640
- const complete = report5.coverage.filter((cell) => cell.state === "complete").length;
12334
+ function printSummary(out, appId, env, run, report4, output) {
12335
+ const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
12641
12336
  out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
12642
12337
  out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
12643
12338
  out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
12644
- 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}`);
12645
- if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
12646
- out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
12339
+ 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}`);
12340
+ if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
12341
+ out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
12647
12342
  out.log(` report: ${(0, import_node_path19.resolve)(output, "REPORT.md")}`);
12648
12343
  }
12649
12344
  function formatBudget(usage) {
@@ -12890,13 +12585,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
12890
12585
  }
12891
12586
  throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
12892
12587
  }
12893
- const report5 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
12588
+ const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
12894
12589
  if (parsed.options.json === true) {
12895
- context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report5 }, null, 2));
12590
+ context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report4 }, null, 2));
12896
12591
  } else {
12897
- printHostedReport(context.stdout, report5);
12592
+ printHostedReport(context.stdout, report4);
12898
12593
  }
12899
- enforceHostedReportGate(report5, parsed, context.stdout, parsed.options.json !== true);
12594
+ enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
12900
12595
  }
12901
12596
  async function runLocalSecurityCommand(parsed, dependencies) {
12902
12597
  if (parsed.options.source === true) {
@@ -12957,19 +12652,20 @@ async function runLocalSecurityCommand(parsed, dependencies) {
12957
12652
  cfg,
12958
12653
  { configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
12959
12654
  doFetch,
12960
- out
12655
+ out,
12656
+ { optionalProjectCapabilities: ["app.manage"] }
12961
12657
  );
12962
12658
  }
12963
12659
  });
12964
12660
  enforceLocalGate(result.report, parsed);
12965
12661
  }
12966
- function enforceLocalGate(report5, parsed) {
12662
+ function enforceLocalGate(report4, parsed) {
12967
12663
  const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
12968
12664
  const candidateValue = parsed.options["fail-on-candidates"];
12969
12665
  const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
12970
- const confirmed = (0, import_security2.findingsAtOrAbove)(report5, failOn);
12971
- const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report5, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
12972
- const incomplete = report5.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
12666
+ const confirmed = (0, import_security2.findingsAtOrAbove)(report4, failOn);
12667
+ const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report4, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
12668
+ const incomplete = report4.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
12973
12669
  if (confirmed.length || leads.length || incomplete) {
12974
12670
  throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
12975
12671
  }
@@ -13008,9 +12704,9 @@ async function securityCommand(parsed, dependencies) {
13008
12704
  assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
13009
12705
  const jobId = requiredSecurityPositional(parsed, 2, "job id");
13010
12706
  const context = await hostedSecurityContext(parsed, dependencies);
13011
- const report5 = await getHostedSecurityReport({ ...context, jobId });
13012
- if (parsed.options.json === true) context.stdout.log(JSON.stringify(report5, null, 2));
13013
- else printHostedReport(context.stdout, report5);
12707
+ const report4 = await getHostedSecurityReport({ ...context, jobId });
12708
+ if (parsed.options.json === true) context.stdout.log(JSON.stringify(report4, null, 2));
12709
+ else printHostedReport(context.stdout, report4);
13014
12710
  return;
13015
12711
  }
13016
12712
  if (sub !== "run") {
@@ -13024,35 +12720,24 @@ async function githubSecurityCommand(parsed, dependencies) {
13024
12720
  const action2 = parsed.positionals[2];
13025
12721
  if (action2 === "disconnect") {
13026
12722
  assertArgs(parsed, ["config", "env", "platform", "source", "email", "open", "yes"], 3);
13027
- const context2 = await hostedSecurityContext(parsed, dependencies);
13028
12723
  const sourceId = requiredString(parsed.options.source, "--source");
13029
- const confirmed = parsed.options.yes === true || await interactiveConfirmation(
13030
- `Disconnect GitHub security source ${sourceId} from ${context2.appId}/${context2.env}?`,
13031
- dependencies
12724
+ return requireStudioHuman(
12725
+ stringOpt(parsed.options.config) ?? "odla.config.mjs",
12726
+ `disconnecting GitHub security source ${sourceId}`,
12727
+ "security",
12728
+ stringOpt(parsed.options.env)
13032
12729
  );
13033
- if (!confirmed) {
13034
- throw new Error("GitHub source disconnect cancelled; pass --yes in a non-interactive shell");
13035
- }
13036
- await disconnectGitHubSecuritySource({ ...context2, sourceId });
13037
- context2.stdout.log(`github: disconnected ${sourceId} from ${context2.appId}/${context2.env}`);
13038
- return;
13039
12730
  }
13040
12731
  if (action2 !== "connect") {
13041
12732
  throw new Error('unknown security github command. Try "odla-ai security github connect".');
13042
12733
  }
13043
12734
  assertArgs(parsed, ["config", "env", "platform", "repo", "email", "open"], 3);
13044
- const context = await hostedSecurityContext(parsed, dependencies);
13045
- const repository = stringOpt(parsed.options.repo) ?? await inferGitHubRepository(process.cwd(), dependencies.readGitOrigin).catch(() => void 0);
13046
- const connection = await connectGitHubSecuritySource({
13047
- ...context,
13048
- ...repository === void 0 ? {} : { repository },
13049
- open: parsed.options.open !== false,
13050
- openInstallUrl: dependencies.openUrl ?? openUrl,
13051
- wait: dependencies.pollWait,
13052
- stdout: context.stdout
13053
- });
13054
- context.stdout.log(`github: connected ${connection.repository ?? repository ?? "app repository"} (${connection.sourceId ?? "source pending"})`);
13055
- context.stdout.log("github: odla.ai stores the installation; no PAT or GitHub token is written locally");
12735
+ await requireStudioHuman(
12736
+ stringOpt(parsed.options.config) ?? "odla.config.mjs",
12737
+ "connecting the GitHub security repository",
12738
+ "security",
12739
+ stringOpt(parsed.options.env)
12740
+ );
13056
12741
  }
13057
12742
  async function listSecuritySources(parsed, dependencies) {
13058
12743
  assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 2);