@odla-ai/cli 0.25.11 → 0.25.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -7
- package/dist/bin.cjs +720 -165
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-FYSV7POZ.js → chunk-5MRY3V2K.js} +703 -148
- package/dist/chunk-5MRY3V2K.js.map +1 -0
- package/dist/index.cjs +720 -165
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/skills/odla-o11y-debug/SKILL.md +30 -10
- package/dist/chunk-FYSV7POZ.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -304,7 +304,7 @@ var import_node_process2 = __toESM(require("process"), 1);
|
|
|
304
304
|
async function openUrl(url, options = {}) {
|
|
305
305
|
const command = openerFor(options.platform ?? import_node_process2.default.platform);
|
|
306
306
|
const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
|
|
307
|
-
await new Promise((
|
|
307
|
+
await new Promise((resolve12, reject) => {
|
|
308
308
|
const child = doSpawn(command.cmd, [...command.args, url], {
|
|
309
309
|
stdio: "ignore",
|
|
310
310
|
detached: true
|
|
@@ -312,7 +312,7 @@ async function openUrl(url, options = {}) {
|
|
|
312
312
|
child.once("error", reject);
|
|
313
313
|
child.once("spawn", () => {
|
|
314
314
|
child.unref();
|
|
315
|
-
|
|
315
|
+
resolve12();
|
|
316
316
|
});
|
|
317
317
|
});
|
|
318
318
|
}
|
|
@@ -2246,8 +2246,8 @@ function credential(value) {
|
|
|
2246
2246
|
// src/calendar-poll.ts
|
|
2247
2247
|
async function waitForCalendarPoll(milliseconds, signal) {
|
|
2248
2248
|
if (signal?.aborted) throw signal.reason ?? new Error("calendar connection aborted");
|
|
2249
|
-
await new Promise((
|
|
2250
|
-
const timer = setTimeout(
|
|
2249
|
+
await new Promise((resolve12, reject) => {
|
|
2250
|
+
const timer = setTimeout(resolve12, milliseconds);
|
|
2251
2251
|
signal?.addEventListener("abort", () => {
|
|
2252
2252
|
clearTimeout(timer);
|
|
2253
2253
|
reject(signal.reason ?? new Error("calendar connection aborted"));
|
|
@@ -2448,7 +2448,8 @@ var CAPABILITIES = {
|
|
|
2448
2448
|
"compose declared app-capability schema/rules, create guarded seeds when absent, and configure platform AI, auth, and deployment links",
|
|
2449
2449
|
"apply Google Calendar booking config, then drive state-bound consent, status, discovery, and disconnect flows (bookings run live through the platform proxy)",
|
|
2450
2450
|
"validate integration contracts offline and smoke-test a provisioned db environment plus anonymous capability routes",
|
|
2451
|
-
"
|
|
2451
|
+
"save and explicitly select non-secret named operator contexts with isolated credential caches; resolve and explain platform, app, environment, and credential provenance without authenticating; then run PM, Discussions, o11y, runbook, and identity operations outside a project checkout",
|
|
2452
|
+
"read one versioned o11y status envelope spanning application RED, exact Worker versions and Cloudflare colos observed in traffic, current live-sync freshness/load, the protected commit-to-visible canary, collector ingest/scheduler trust, provider-owned runtime metrics, account-scoped Durable Object, D1, and R2 evidence under odla-db, and a bounded machine verdict",
|
|
2452
2453
|
"inspect durable agent wakeups as a versioned JSON envelope and explicitly requeue one dead-lettered job with a scoped environment credential",
|
|
2453
2454
|
"run app-attributed hosted security discovery and independent validation without provider keys",
|
|
2454
2455
|
"connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
|
|
@@ -2470,7 +2471,7 @@ var CAPABILITIES = {
|
|
|
2470
2471
|
"approve GitHub App repository access separately from redacted-snippet disclosure"
|
|
2471
2472
|
],
|
|
2472
2473
|
studio: [
|
|
2473
|
-
"view application telemetry, reconciled live-sync load/freshness, commit-to-visible canary health, provider-owned Worker runtime metrics, and environment state",
|
|
2474
|
+
"view application telemetry grouped by exact Worker version, reconciled live-sync load/freshness, commit-to-visible canary health, provider-owned Worker runtime metrics, and environment state",
|
|
2474
2475
|
"let signed-in users inventory/revoke their own agent grants and admins audit/global-revoke them",
|
|
2475
2476
|
"review calendar connection, granted read scope, selected calendars, and sync health without exposing provider tokens",
|
|
2476
2477
|
"perform manual credential recovery \u2014 for the primary owner or any co-owner \u2014 when the CLI's local shown-once copy is unavailable",
|
|
@@ -5476,21 +5477,21 @@ async function verifyCodeCandidate(input) {
|
|
|
5476
5477
|
const recipes = [];
|
|
5477
5478
|
const logs = [];
|
|
5478
5479
|
for (const recipe2 of policy.recipes) {
|
|
5479
|
-
const
|
|
5480
|
+
const clean4 = await stageWorkspace(staged.workspaceDir, limits);
|
|
5480
5481
|
try {
|
|
5481
|
-
if (await digestStagedWorkspace(
|
|
5482
|
+
if (await digestStagedWorkspace(clean4.workspaceDir, limits) !== sourceDigest) {
|
|
5482
5483
|
throw new TypeError("clean verifier source changed before execution");
|
|
5483
5484
|
}
|
|
5484
5485
|
const result = checkedResult(await input.recipeExecutor.run({
|
|
5485
|
-
workspaceDir:
|
|
5486
|
+
workspaceDir: clean4.workspaceDir,
|
|
5486
5487
|
recipe: recipe2,
|
|
5487
5488
|
signal: input.signal
|
|
5488
5489
|
}), recipe2.maxOutputBytes);
|
|
5489
|
-
const artifacts = await inspectArtifacts(
|
|
5490
|
+
const artifacts = await inspectArtifacts(clean4.workspaceDir, recipe2);
|
|
5490
5491
|
recipes.push(recipeReceipt(recipe2, result, artifacts));
|
|
5491
5492
|
logs.push({ recipeId: recipe2.id, ...boundedLogs(result, recipe2.maxOutputBytes) });
|
|
5492
5493
|
} finally {
|
|
5493
|
-
await
|
|
5494
|
+
await clean4.cleanup();
|
|
5494
5495
|
}
|
|
5495
5496
|
}
|
|
5496
5497
|
const fields = {
|
|
@@ -6631,8 +6632,12 @@ Usage:
|
|
|
6631
6632
|
odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
|
|
6632
6633
|
odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
|
|
6633
6634
|
odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
|
|
6634
|
-
odla-ai
|
|
6635
|
-
odla-ai
|
|
6635
|
+
odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
|
|
6636
|
+
odla-ai context list [--json]
|
|
6637
|
+
odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
|
|
6638
|
+
odla-ai context remove <name> --yes [--json]
|
|
6639
|
+
odla-ai o11y status [--app <id>] [--context <name>] [--platform https://odla.ai] [--env prod] [--minutes 60] [--json]
|
|
6640
|
+
odla-ai whoami [--context <name>] [--platform https://odla.ai] [--json]
|
|
6636
6641
|
odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
|
|
6637
6642
|
odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
|
|
6638
6643
|
odla-ai runbook impact [--base origin/main] [--app <id>] [--all] [--limit <n>] [--json]
|
|
@@ -6697,6 +6702,11 @@ Commands:
|
|
|
6697
6702
|
whoami Report who this terminal is authenticated as, and whether it holds
|
|
6698
6703
|
platform admin. A handshake-minted device token is never admin,
|
|
6699
6704
|
however the human who approved it is configured.
|
|
6705
|
+
context Explain selected config, platform, app, environment, and
|
|
6706
|
+
developer-token provenance without printing credentials or
|
|
6707
|
+
starting a device handshake. Operator work can run outside a
|
|
6708
|
+
project checkout with explicit flags or ODLA_* environment
|
|
6709
|
+
variables.
|
|
6700
6710
|
setup Install offline odla runbooks for common coding-agent harnesses.
|
|
6701
6711
|
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
6702
6712
|
doctor Validate and summarize the project config without network calls.
|
|
@@ -6768,6 +6778,15 @@ Safety:
|
|
|
6768
6778
|
Provision caches the approved developer token and service credentials under
|
|
6769
6779
|
.odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
|
|
6770
6780
|
preflights Wrangler before any shown-once issuance or destructive rotation.
|
|
6781
|
+
Projectless PM, Discussions, o11y, runbook, and identity commands use
|
|
6782
|
+
--platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
|
|
6783
|
+
ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
|
|
6784
|
+
select it explicitly with --context or ODLA_CONTEXT. Each named context gets
|
|
6785
|
+
isolated developer and scoped-token caches. Override their locations with
|
|
6786
|
+
ODLA_DEV_TOKEN_FILE and ODLA_ADMIN_TOKEN_FILE; ODLA_CONTEXT_FILE relocates
|
|
6787
|
+
the metadata file. Flags and specific ODLA_* scope variables beat a selected
|
|
6788
|
+
context, which beats project config. There is no ambient current context.
|
|
6789
|
+
"context show" reports only provenance and cache state and never authenticates.
|
|
6771
6790
|
Provision opens the approval page in your browser automatically whenever the
|
|
6772
6791
|
machine can show one, including agent-driven runs; only CI, SSH, and
|
|
6773
6792
|
display-less hosts skip it. Use --open to force or --no-open to suppress.
|
|
@@ -6885,8 +6904,8 @@ function hostedPollTimeout(value = 10 * 6e4) {
|
|
|
6885
6904
|
}
|
|
6886
6905
|
async function waitForHostedPoll(milliseconds, signal) {
|
|
6887
6906
|
if (signal?.aborted) throw signal.reason ?? new DOMException("aborted", "AbortError");
|
|
6888
|
-
await new Promise((
|
|
6889
|
-
const timer = setTimeout(
|
|
6907
|
+
await new Promise((resolve12, reject) => {
|
|
6908
|
+
const timer = setTimeout(resolve12, milliseconds);
|
|
6890
6909
|
signal?.addEventListener("abort", () => {
|
|
6891
6910
|
clearTimeout(timer);
|
|
6892
6911
|
reject(signal.reason ?? new DOMException("aborted", "AbortError"));
|
|
@@ -7396,6 +7415,384 @@ async function codeCommand(parsed, dependencies) {
|
|
|
7396
7415
|
});
|
|
7397
7416
|
}
|
|
7398
7417
|
|
|
7418
|
+
// src/operator-credentials.ts
|
|
7419
|
+
var import_node_process7 = __toESM(require("process"), 1);
|
|
7420
|
+
function developerTokenStatus(context, parsed, now = Date.now()) {
|
|
7421
|
+
const cached = readJsonFile(context.cfg.local.tokenFile);
|
|
7422
|
+
const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
|
|
7423
|
+
const source = clean(
|
|
7424
|
+
stringOpt(parsed.options.token)
|
|
7425
|
+
) ? "flag" : clean(import_node_process7.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
|
|
7426
|
+
return {
|
|
7427
|
+
source,
|
|
7428
|
+
cacheFile: context.cfg.local.tokenFile,
|
|
7429
|
+
cacheStatus
|
|
7430
|
+
};
|
|
7431
|
+
}
|
|
7432
|
+
function clean(value) {
|
|
7433
|
+
const normalized = value?.trim();
|
|
7434
|
+
return normalized || void 0;
|
|
7435
|
+
}
|
|
7436
|
+
|
|
7437
|
+
// src/operator-context.ts
|
|
7438
|
+
var import_node_fs14 = require("fs");
|
|
7439
|
+
var import_node_path12 = require("path");
|
|
7440
|
+
var import_node_process9 = __toESM(require("process"), 1);
|
|
7441
|
+
|
|
7442
|
+
// src/operator-profiles.ts
|
|
7443
|
+
var import_node_fs13 = require("fs");
|
|
7444
|
+
var import_node_os4 = require("os");
|
|
7445
|
+
var import_node_path11 = require("path");
|
|
7446
|
+
var import_node_process8 = __toESM(require("process"), 1);
|
|
7447
|
+
function operatorProfileFile() {
|
|
7448
|
+
return (0, import_node_path11.resolve)(
|
|
7449
|
+
clean2(import_node_process8.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path11.join)((0, import_node_os4.homedir)(), ".odla", "contexts.json")
|
|
7450
|
+
);
|
|
7451
|
+
}
|
|
7452
|
+
function resolveOperatorProfile(parsed) {
|
|
7453
|
+
const fromFlag = clean2(stringOpt(parsed.options.context));
|
|
7454
|
+
const fromEnvironment = clean2(import_node_process8.default.env.ODLA_CONTEXT);
|
|
7455
|
+
const name = fromFlag ?? fromEnvironment ?? null;
|
|
7456
|
+
const file = operatorProfileFile();
|
|
7457
|
+
if (!name) {
|
|
7458
|
+
return { name: null, source: "unresolved", file, value: null };
|
|
7459
|
+
}
|
|
7460
|
+
assertOperatorName(name, "context");
|
|
7461
|
+
const profiles = readOperatorProfiles(file);
|
|
7462
|
+
const value = Object.hasOwn(profiles, name) ? profiles[name] : void 0;
|
|
7463
|
+
if (!value) {
|
|
7464
|
+
throw new Error(
|
|
7465
|
+
`operator context "${name}" not found in ${file}; run "odla-ai context list" or save it first`
|
|
7466
|
+
);
|
|
7467
|
+
}
|
|
7468
|
+
return {
|
|
7469
|
+
name,
|
|
7470
|
+
source: fromFlag ? "flag" : "environment",
|
|
7471
|
+
file,
|
|
7472
|
+
value
|
|
7473
|
+
};
|
|
7474
|
+
}
|
|
7475
|
+
function listOperatorProfiles(file = operatorProfileFile()) {
|
|
7476
|
+
return Object.entries(readOperatorProfiles(file)).sort(([a], [b]) => a.localeCompare(b)).map(([name, profile]) => ({ name, ...profile }));
|
|
7477
|
+
}
|
|
7478
|
+
function saveOperatorProfile(name, profile, file = operatorProfileFile()) {
|
|
7479
|
+
assertOperatorName(name, "context");
|
|
7480
|
+
const normalized = validateProfile(profile, `operator context "${name}"`);
|
|
7481
|
+
const profiles = readOperatorProfiles(file);
|
|
7482
|
+
profiles[name] = normalized;
|
|
7483
|
+
writePrivateJson(file, { schemaVersion: 1, profiles });
|
|
7484
|
+
}
|
|
7485
|
+
function removeOperatorProfile(name, file = operatorProfileFile()) {
|
|
7486
|
+
assertOperatorName(name, "context");
|
|
7487
|
+
const profiles = readOperatorProfiles(file);
|
|
7488
|
+
if (!Object.hasOwn(profiles, name)) return false;
|
|
7489
|
+
delete profiles[name];
|
|
7490
|
+
writePrivateJson(file, { schemaVersion: 1, profiles });
|
|
7491
|
+
return true;
|
|
7492
|
+
}
|
|
7493
|
+
function operatorCredentialFiles(selection) {
|
|
7494
|
+
const base = selection.name ? (0, import_node_path11.join)((0, import_node_path11.dirname)(selection.file), "profiles", selection.name) : (0, import_node_path11.join)((0, import_node_os4.homedir)(), ".odla");
|
|
7495
|
+
return {
|
|
7496
|
+
developer: (0, import_node_path11.join)(base, "dev-token.json"),
|
|
7497
|
+
scoped: (0, import_node_path11.join)(base, "admin-token.local.json")
|
|
7498
|
+
};
|
|
7499
|
+
}
|
|
7500
|
+
function assertOperatorName(value, label) {
|
|
7501
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(value)) {
|
|
7502
|
+
throw new Error(
|
|
7503
|
+
`${label} must contain lowercase letters, numbers, and hyphens`
|
|
7504
|
+
);
|
|
7505
|
+
}
|
|
7506
|
+
}
|
|
7507
|
+
function readOperatorProfiles(file) {
|
|
7508
|
+
if (!(0, import_node_fs13.existsSync)(file)) return emptyProfiles();
|
|
7509
|
+
let raw;
|
|
7510
|
+
try {
|
|
7511
|
+
raw = JSON.parse((0, import_node_fs13.readFileSync)(file, "utf8"));
|
|
7512
|
+
} catch {
|
|
7513
|
+
throw new Error(`operator context file ${file} is not valid JSON`);
|
|
7514
|
+
}
|
|
7515
|
+
if (!raw || typeof raw !== "object" || raw.schemaVersion !== 1 || !raw.profiles || typeof raw.profiles !== "object" || Array.isArray(raw.profiles)) {
|
|
7516
|
+
throw new Error(`operator context file ${file} has an invalid shape`);
|
|
7517
|
+
}
|
|
7518
|
+
const unknown = Object.keys(raw).filter(
|
|
7519
|
+
(key) => !["schemaVersion", "profiles"].includes(key)
|
|
7520
|
+
);
|
|
7521
|
+
if (unknown.length > 0) {
|
|
7522
|
+
throw new Error(
|
|
7523
|
+
`operator context file ${file} contains unsupported field(s): ${unknown.sort().join(", ")}`
|
|
7524
|
+
);
|
|
7525
|
+
}
|
|
7526
|
+
const profiles = emptyProfiles();
|
|
7527
|
+
for (const [name, value] of Object.entries(
|
|
7528
|
+
raw.profiles
|
|
7529
|
+
)) {
|
|
7530
|
+
assertOperatorName(name, "context");
|
|
7531
|
+
profiles[name] = validateProfile(value, `operator context "${name}"`);
|
|
7532
|
+
}
|
|
7533
|
+
return profiles;
|
|
7534
|
+
}
|
|
7535
|
+
function emptyProfiles() {
|
|
7536
|
+
return /* @__PURE__ */ Object.create(null);
|
|
7537
|
+
}
|
|
7538
|
+
function validateProfile(value, label) {
|
|
7539
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
7540
|
+
throw new Error(`${label} has an invalid shape`);
|
|
7541
|
+
}
|
|
7542
|
+
const unknown = Object.keys(value).filter(
|
|
7543
|
+
(key) => !["platform", "app", "environment"].includes(key)
|
|
7544
|
+
);
|
|
7545
|
+
if (unknown.length > 0) {
|
|
7546
|
+
throw new Error(
|
|
7547
|
+
`${label} contains unsupported field(s): ${unknown.sort().join(", ")}; contexts store scope metadata only, never credentials`
|
|
7548
|
+
);
|
|
7549
|
+
}
|
|
7550
|
+
const candidate = value;
|
|
7551
|
+
if (typeof candidate.platform !== "string") {
|
|
7552
|
+
throw new Error(`${label} needs an absolute platform URL`);
|
|
7553
|
+
}
|
|
7554
|
+
const platform = platformAudience(candidate.platform);
|
|
7555
|
+
const app = clean2(candidate.app);
|
|
7556
|
+
const environment2 = clean2(candidate.environment);
|
|
7557
|
+
if (app) assertOperatorName(app, "app");
|
|
7558
|
+
if (environment2) assertOperatorName(environment2, "environment");
|
|
7559
|
+
return {
|
|
7560
|
+
platform,
|
|
7561
|
+
...app ? { app } : {},
|
|
7562
|
+
...environment2 ? { environment: environment2 } : {}
|
|
7563
|
+
};
|
|
7564
|
+
}
|
|
7565
|
+
function clean2(value) {
|
|
7566
|
+
const normalized = value?.trim();
|
|
7567
|
+
return normalized || void 0;
|
|
7568
|
+
}
|
|
7569
|
+
|
|
7570
|
+
// src/operator-context.ts
|
|
7571
|
+
var DEFAULT_PLATFORM2 = "https://odla.ai";
|
|
7572
|
+
async function resolveOperatorContext(parsed, options = {}) {
|
|
7573
|
+
const profile = resolveOperatorProfile(parsed);
|
|
7574
|
+
const configArgument = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
7575
|
+
const configPath = (0, import_node_path12.resolve)(configArgument);
|
|
7576
|
+
const explicitConfig = parsed.options.config !== void 0;
|
|
7577
|
+
const hasConfig = (0, import_node_fs14.existsSync)(configPath);
|
|
7578
|
+
if (!hasConfig && (!options.allowMissingConfig || explicitConfig)) {
|
|
7579
|
+
await loadProjectConfig(configArgument);
|
|
7580
|
+
}
|
|
7581
|
+
const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
|
|
7582
|
+
const platformFlag = clean3(stringOpt(parsed.options.platform));
|
|
7583
|
+
const platformEnvironment = clean3(import_node_process9.default.env.ODLA_PLATFORM_URL);
|
|
7584
|
+
const platformValue = platformAudience(
|
|
7585
|
+
platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
|
|
7586
|
+
);
|
|
7587
|
+
const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
|
|
7588
|
+
const appFlag = clean3(stringOpt(parsed.options.app));
|
|
7589
|
+
const appEnvironment = clean3(import_node_process9.default.env.ODLA_APP_ID);
|
|
7590
|
+
const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
|
|
7591
|
+
const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
|
|
7592
|
+
if (appValue) assertOperatorName(appValue, "app");
|
|
7593
|
+
if (options.requireApp && !appValue) {
|
|
7594
|
+
throw new Error(
|
|
7595
|
+
"app context is unresolved; pass --app <id>, set ODLA_APP_ID, select --context <name>, or run inside a project with odla.config.mjs"
|
|
7596
|
+
);
|
|
7597
|
+
}
|
|
7598
|
+
const envFlag = clean3(stringOpt(parsed.options.env));
|
|
7599
|
+
const envEnvironment = clean3(import_node_process9.default.env.ODLA_ENV);
|
|
7600
|
+
const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
|
|
7601
|
+
const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
|
|
7602
|
+
if (environmentValue) {
|
|
7603
|
+
assertOperatorName(environmentValue, "environment");
|
|
7604
|
+
}
|
|
7605
|
+
const rootDir = loaded?.rootDir ?? import_node_process9.default.cwd();
|
|
7606
|
+
const profileCredentials = operatorCredentialFiles(profile);
|
|
7607
|
+
const tokenFile = clean3(import_node_process9.default.env.ODLA_DEV_TOKEN_FILE) ? (0, import_node_path12.resolve)(import_node_process9.default.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
|
|
7608
|
+
const scopedTokenFile = clean3(import_node_process9.default.env.ODLA_ADMIN_TOKEN_FILE) ? (0, import_node_path12.resolve)(import_node_process9.default.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? (0, import_node_path12.join)(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
|
|
7609
|
+
const cfg = loaded ? {
|
|
7610
|
+
...loaded,
|
|
7611
|
+
platformUrl: platformValue,
|
|
7612
|
+
app: appValue ? {
|
|
7613
|
+
id: appValue,
|
|
7614
|
+
name: appValue === loaded.app.id ? loaded.app.name : appValue
|
|
7615
|
+
} : loaded.app,
|
|
7616
|
+
local: { ...loaded.local, tokenFile }
|
|
7617
|
+
} : {
|
|
7618
|
+
configPath,
|
|
7619
|
+
rootDir,
|
|
7620
|
+
platformUrl: platformValue,
|
|
7621
|
+
dbEndpoint: platformValue,
|
|
7622
|
+
app: {
|
|
7623
|
+
id: appValue ?? "operator",
|
|
7624
|
+
name: appValue ?? "Operator"
|
|
7625
|
+
},
|
|
7626
|
+
envs: environmentValue ? [environmentValue] : [],
|
|
7627
|
+
services: [],
|
|
7628
|
+
local: {
|
|
7629
|
+
tokenFile,
|
|
7630
|
+
credentialsFile: (0, import_node_path12.join)(rootDir, ".odla", "credentials.local.json"),
|
|
7631
|
+
devVarsFile: (0, import_node_path12.join)(rootDir, ".dev.vars"),
|
|
7632
|
+
gitignore: true
|
|
7633
|
+
}
|
|
7634
|
+
};
|
|
7635
|
+
return {
|
|
7636
|
+
cfg,
|
|
7637
|
+
profile: {
|
|
7638
|
+
name: profile.name,
|
|
7639
|
+
source: profile.source,
|
|
7640
|
+
file: profile.file
|
|
7641
|
+
},
|
|
7642
|
+
config: {
|
|
7643
|
+
path: configPath,
|
|
7644
|
+
status: loaded ? "loaded" : "absent",
|
|
7645
|
+
explicit: explicitConfig
|
|
7646
|
+
},
|
|
7647
|
+
platform: { value: platformValue, source: platformSource },
|
|
7648
|
+
app: { value: appValue, source: appSource },
|
|
7649
|
+
environment: {
|
|
7650
|
+
value: environmentValue,
|
|
7651
|
+
source: environmentSource
|
|
7652
|
+
},
|
|
7653
|
+
credentials: {
|
|
7654
|
+
developerTokenFile: tokenFile,
|
|
7655
|
+
scopedTokenFile
|
|
7656
|
+
}
|
|
7657
|
+
};
|
|
7658
|
+
}
|
|
7659
|
+
function clean3(value) {
|
|
7660
|
+
const normalized = value?.trim();
|
|
7661
|
+
return normalized || void 0;
|
|
7662
|
+
}
|
|
7663
|
+
|
|
7664
|
+
// src/context-command.ts
|
|
7665
|
+
async function contextCommand(parsed, deps = {}) {
|
|
7666
|
+
assertArgs(
|
|
7667
|
+
parsed,
|
|
7668
|
+
[
|
|
7669
|
+
"config",
|
|
7670
|
+
"context",
|
|
7671
|
+
"platform",
|
|
7672
|
+
"app",
|
|
7673
|
+
"env",
|
|
7674
|
+
"token",
|
|
7675
|
+
"json",
|
|
7676
|
+
"yes"
|
|
7677
|
+
],
|
|
7678
|
+
3
|
|
7679
|
+
);
|
|
7680
|
+
const action = parsed.positionals[1];
|
|
7681
|
+
const out = deps.stdout ?? console;
|
|
7682
|
+
if (action === "list") {
|
|
7683
|
+
const profiles = listOperatorProfiles();
|
|
7684
|
+
if (parsed.options.json === true) {
|
|
7685
|
+
out.log(JSON.stringify({ schemaVersion: 1, profiles }, null, 2));
|
|
7686
|
+
return;
|
|
7687
|
+
}
|
|
7688
|
+
if (profiles.length === 0) {
|
|
7689
|
+
out.log("No named operator contexts.");
|
|
7690
|
+
return;
|
|
7691
|
+
}
|
|
7692
|
+
out.log("name platform app environment");
|
|
7693
|
+
for (const profile of profiles) {
|
|
7694
|
+
out.log(
|
|
7695
|
+
`${profile.name} ${profile.platform} ${profile.app ?? ""} ${profile.environment ?? ""}`
|
|
7696
|
+
);
|
|
7697
|
+
}
|
|
7698
|
+
return;
|
|
7699
|
+
}
|
|
7700
|
+
if (action === "save") {
|
|
7701
|
+
if (parsed.options.token !== void 0) {
|
|
7702
|
+
throw new Error(
|
|
7703
|
+
"context save does not accept --token; contexts store scope metadata only"
|
|
7704
|
+
);
|
|
7705
|
+
}
|
|
7706
|
+
const name = requireName(parsed);
|
|
7707
|
+
const context2 = await resolveOperatorContext(parsed, {
|
|
7708
|
+
allowMissingConfig: true
|
|
7709
|
+
});
|
|
7710
|
+
const profile = {
|
|
7711
|
+
platform: context2.platform.value,
|
|
7712
|
+
...context2.app.value ? { app: context2.app.value } : {},
|
|
7713
|
+
...context2.environment.value ? { environment: context2.environment.value } : {}
|
|
7714
|
+
};
|
|
7715
|
+
saveOperatorProfile(name, profile, context2.profile.file);
|
|
7716
|
+
if (parsed.options.json === true) {
|
|
7717
|
+
out.log(JSON.stringify({ schemaVersion: 1, name, ...profile }, null, 2));
|
|
7718
|
+
} else {
|
|
7719
|
+
out.log(`saved operator context "${name}" (${profile.platform})`);
|
|
7720
|
+
}
|
|
7721
|
+
return;
|
|
7722
|
+
}
|
|
7723
|
+
if (action === "remove") {
|
|
7724
|
+
const name = requireName(parsed);
|
|
7725
|
+
if (parsed.options.yes !== true) {
|
|
7726
|
+
throw new Error(`context remove "${name}" requires --yes`);
|
|
7727
|
+
}
|
|
7728
|
+
const removed = removeOperatorProfile(name);
|
|
7729
|
+
const result2 = {
|
|
7730
|
+
schemaVersion: 1,
|
|
7731
|
+
name,
|
|
7732
|
+
removed,
|
|
7733
|
+
credentialCachesRemoved: false,
|
|
7734
|
+
grantsRevoked: false
|
|
7735
|
+
};
|
|
7736
|
+
if (parsed.options.json === true) out.log(JSON.stringify(result2, null, 2));
|
|
7737
|
+
else {
|
|
7738
|
+
const status = removed ? `removed operator context "${name}"` : `operator context "${name}" was already absent`;
|
|
7739
|
+
out.log(`${status}; credential caches were not deleted or revoked`);
|
|
7740
|
+
}
|
|
7741
|
+
return;
|
|
7742
|
+
}
|
|
7743
|
+
if (action !== "show") {
|
|
7744
|
+
throw new Error(
|
|
7745
|
+
`unknown context action "${action ?? ""}". Try show|list|save|remove.`
|
|
7746
|
+
);
|
|
7747
|
+
}
|
|
7748
|
+
const context = await resolveOperatorContext(parsed, {
|
|
7749
|
+
allowMissingConfig: true,
|
|
7750
|
+
defaultEnvironment: "prod"
|
|
7751
|
+
});
|
|
7752
|
+
const token = developerTokenStatus(context, parsed);
|
|
7753
|
+
const result = {
|
|
7754
|
+
schemaVersion: 1,
|
|
7755
|
+
profile: context.profile,
|
|
7756
|
+
config: context.config,
|
|
7757
|
+
platform: context.platform,
|
|
7758
|
+
app: context.app,
|
|
7759
|
+
environment: context.environment,
|
|
7760
|
+
authentication: {
|
|
7761
|
+
developerToken: token,
|
|
7762
|
+
scopedTokenCacheFile: context.credentials.scopedTokenFile,
|
|
7763
|
+
handshakeStarted: false
|
|
7764
|
+
}
|
|
7765
|
+
};
|
|
7766
|
+
if (parsed.options.json === true) {
|
|
7767
|
+
out.log(JSON.stringify(result, null, 2));
|
|
7768
|
+
return;
|
|
7769
|
+
}
|
|
7770
|
+
out.log(`config: ${context.config.status} (${context.config.path})`);
|
|
7771
|
+
out.log(
|
|
7772
|
+
`context: ${context.profile.name ?? "(none)"} (${context.profile.source}; ${context.profile.file})`
|
|
7773
|
+
);
|
|
7774
|
+
out.log(
|
|
7775
|
+
`platform: ${context.platform.value} (${context.platform.source})`
|
|
7776
|
+
);
|
|
7777
|
+
out.log(
|
|
7778
|
+
`app: ${context.app.value ?? "(unresolved)"} (${context.app.source})`
|
|
7779
|
+
);
|
|
7780
|
+
out.log(
|
|
7781
|
+
`env: ${context.environment.value ?? "(unresolved)"} (${context.environment.source})`
|
|
7782
|
+
);
|
|
7783
|
+
out.log(
|
|
7784
|
+
`auth: ${token.source} (cache ${token.cacheStatus}: ${token.cacheFile})`
|
|
7785
|
+
);
|
|
7786
|
+
out.log("handshake: not started");
|
|
7787
|
+
}
|
|
7788
|
+
function requireName(parsed) {
|
|
7789
|
+
const name = parsed.positionals[2]?.trim();
|
|
7790
|
+
if (!name) {
|
|
7791
|
+
throw new Error(`context ${parsed.positionals[1]} needs a profile name`);
|
|
7792
|
+
}
|
|
7793
|
+
return name;
|
|
7794
|
+
}
|
|
7795
|
+
|
|
7399
7796
|
// src/discuss-actions.ts
|
|
7400
7797
|
var writeMutationId = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
|
|
7401
7798
|
async function request(ctx, method, path, body) {
|
|
@@ -7447,8 +7844,9 @@ async function discussGroups(ctx) {
|
|
|
7447
7844
|
}
|
|
7448
7845
|
async function discussList(ctx, parsed) {
|
|
7449
7846
|
const query = new URLSearchParams();
|
|
7847
|
+
const app = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
7848
|
+
if (app) query.set("app", app);
|
|
7450
7849
|
for (const [flag, param] of [
|
|
7451
|
-
["app", "app"],
|
|
7452
7850
|
["q", "q"],
|
|
7453
7851
|
["state", "state"],
|
|
7454
7852
|
["limit", "limit"],
|
|
@@ -7524,7 +7922,7 @@ function renderRead(ctx, topic, posts) {
|
|
|
7524
7922
|
}
|
|
7525
7923
|
}
|
|
7526
7924
|
async function discussPost(ctx, parsed) {
|
|
7527
|
-
const appId = stringOpt(parsed.options.app);
|
|
7925
|
+
const appId = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
7528
7926
|
if (!appId) throw new Error("discuss post needs --app <appId>");
|
|
7529
7927
|
const subject = stringOpt(parsed.options.subject);
|
|
7530
7928
|
if (!subject) throw new Error('discuss post needs --subject "\u2026"');
|
|
@@ -7556,7 +7954,7 @@ async function discussResolve(ctx, id, resolved, parsed) {
|
|
|
7556
7954
|
}
|
|
7557
7955
|
async function discussWho(ctx, parsed) {
|
|
7558
7956
|
const query = new URLSearchParams({ q: stringOpt(parsed.options.q) ?? "" });
|
|
7559
|
-
const app = stringOpt(parsed.options.app);
|
|
7957
|
+
const app = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
7560
7958
|
if (app) query.set("app", app);
|
|
7561
7959
|
const kinds = stringOpt(parsed.options.kinds);
|
|
7562
7960
|
if (kinds) query.set("kinds", kinds);
|
|
@@ -7660,14 +8058,14 @@ function jsonl(ctx, parsed, value) {
|
|
|
7660
8058
|
}
|
|
7661
8059
|
async function discussWatch(ctx, topicId, parsed) {
|
|
7662
8060
|
if (ctx.json && parsed.options.jsonl === true) throw new Error("--json and --jsonl cannot be combined");
|
|
7663
|
-
const sleep = ctx.sleep ?? ((ms) => new Promise((
|
|
8061
|
+
const sleep = ctx.sleep ?? ((ms) => new Promise((resolve12) => setTimeout(resolve12, ms)));
|
|
7664
8062
|
const now = ctx.now ?? Date.now;
|
|
7665
8063
|
const intervalMs = (numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) ?? DEFAULT_INTERVAL_MS / 1e3) * 1e3;
|
|
7666
8064
|
const timeoutSeconds = numberOpt2(parsed, "timeout");
|
|
7667
8065
|
const deadline = timeoutSeconds === void 0 ? void 0 : now() + timeoutSeconds * 1e3;
|
|
7668
8066
|
const by = stringOpt(parsed.options.by);
|
|
7669
8067
|
const self = stringOpt(parsed.options.self);
|
|
7670
|
-
const app = stringOpt(parsed.options.app);
|
|
8068
|
+
const app = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
7671
8069
|
let cursor = stringOpt(parsed.options.cursor);
|
|
7672
8070
|
let firstSuccess = true;
|
|
7673
8071
|
let consecutiveFailures = 0;
|
|
@@ -7812,14 +8210,19 @@ var ALLOWED = [
|
|
|
7812
8210
|
"timeout",
|
|
7813
8211
|
"cursor",
|
|
7814
8212
|
"jsonl",
|
|
7815
|
-
"mutation-id"
|
|
8213
|
+
"mutation-id",
|
|
8214
|
+
"platform",
|
|
8215
|
+
"context"
|
|
7816
8216
|
];
|
|
7817
8217
|
function requireId(id, action) {
|
|
7818
8218
|
if (!id) throw new Error(`"discuss ${action}" needs a topic id`);
|
|
7819
8219
|
return id;
|
|
7820
8220
|
}
|
|
7821
8221
|
async function buildContext(parsed, deps) {
|
|
7822
|
-
const
|
|
8222
|
+
const context = await resolveOperatorContext(parsed, {
|
|
8223
|
+
allowMissingConfig: true
|
|
8224
|
+
});
|
|
8225
|
+
const { cfg } = context;
|
|
7823
8226
|
const doFetch = deps.fetch ?? fetch;
|
|
7824
8227
|
const out = deps.stdout ?? console;
|
|
7825
8228
|
const token = await getDeveloperToken(
|
|
@@ -7833,7 +8236,16 @@ async function buildContext(parsed, deps) {
|
|
|
7833
8236
|
doFetch,
|
|
7834
8237
|
out
|
|
7835
8238
|
);
|
|
7836
|
-
return {
|
|
8239
|
+
return {
|
|
8240
|
+
platformUrl: cfg.platformUrl,
|
|
8241
|
+
token,
|
|
8242
|
+
doFetch,
|
|
8243
|
+
out,
|
|
8244
|
+
json: parsed.options.json === true,
|
|
8245
|
+
// Preserve Discussions' existing cross-project default for config-backed
|
|
8246
|
+
// use while allowing remote agents to scope via ODLA_APP_ID or a profile.
|
|
8247
|
+
appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0
|
|
8248
|
+
};
|
|
7837
8249
|
}
|
|
7838
8250
|
async function discussCommand(parsed, deps = {}) {
|
|
7839
8251
|
assertArgs(parsed, ALLOWED, 3);
|
|
@@ -7945,7 +8357,7 @@ async function pmList(ctx, entity, parsed) {
|
|
|
7945
8357
|
assignee: "assigneeId",
|
|
7946
8358
|
decision: "decisionId"
|
|
7947
8359
|
};
|
|
7948
|
-
const app = stringOpt(parsed.options.app);
|
|
8360
|
+
const app = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
7949
8361
|
if (app) q.set("app", app);
|
|
7950
8362
|
for (const [flag, param] of Object.entries(filters)) {
|
|
7951
8363
|
const v = stringOpt(parsed.options[flag]);
|
|
@@ -7964,7 +8376,7 @@ async function pmList(ctx, entity, parsed) {
|
|
|
7964
8376
|
});
|
|
7965
8377
|
}
|
|
7966
8378
|
async function pmAdd(ctx, entity, parsed) {
|
|
7967
|
-
const appId = stringOpt(parsed.options.app);
|
|
8379
|
+
const appId = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
7968
8380
|
if (!appId) throw new Error("pm add needs --app <appId>");
|
|
7969
8381
|
const input = collectEntityFields(entity, parsed, false);
|
|
7970
8382
|
if (!input.title) throw new Error("pm add needs --title <title>");
|
|
@@ -8015,7 +8427,7 @@ async function allRecords(ctx, entity, appId) {
|
|
|
8015
8427
|
}
|
|
8016
8428
|
}
|
|
8017
8429
|
async function pmHandoff(ctx, parsed) {
|
|
8018
|
-
const appId = stringOpt(parsed.options.app);
|
|
8430
|
+
const appId = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
8019
8431
|
if (!appId) throw new Error("pm handoff needs --app <appId>");
|
|
8020
8432
|
const [goals, tasks, bugs] = await Promise.all([
|
|
8021
8433
|
allRecords(ctx, "goal", appId),
|
|
@@ -8110,14 +8522,19 @@ var ALLOWED2 = [
|
|
|
8110
8522
|
"limit",
|
|
8111
8523
|
"offset",
|
|
8112
8524
|
"q",
|
|
8113
|
-
"mutation-id"
|
|
8525
|
+
"mutation-id",
|
|
8526
|
+
"platform",
|
|
8527
|
+
"context"
|
|
8114
8528
|
];
|
|
8115
8529
|
function requireId2(id, action) {
|
|
8116
8530
|
if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
|
|
8117
8531
|
return id;
|
|
8118
8532
|
}
|
|
8119
8533
|
async function buildContext2(parsed, deps) {
|
|
8120
|
-
const
|
|
8534
|
+
const context = await resolveOperatorContext(parsed, {
|
|
8535
|
+
allowMissingConfig: true
|
|
8536
|
+
});
|
|
8537
|
+
const { cfg } = context;
|
|
8121
8538
|
const doFetch = deps.fetch ?? fetch;
|
|
8122
8539
|
const out = deps.stdout ?? console;
|
|
8123
8540
|
const token = await getDeveloperToken(
|
|
@@ -8126,7 +8543,16 @@ async function buildContext2(parsed, deps) {
|
|
|
8126
8543
|
doFetch,
|
|
8127
8544
|
out
|
|
8128
8545
|
);
|
|
8129
|
-
return {
|
|
8546
|
+
return {
|
|
8547
|
+
platformUrl: cfg.platformUrl,
|
|
8548
|
+
token,
|
|
8549
|
+
doFetch,
|
|
8550
|
+
out,
|
|
8551
|
+
json: parsed.options.json === true,
|
|
8552
|
+
// Preserve PM's existing cross-project default when a config is present;
|
|
8553
|
+
// only an explicit remote-agent environment or profile selects an app.
|
|
8554
|
+
appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0
|
|
8555
|
+
};
|
|
8130
8556
|
}
|
|
8131
8557
|
async function pmCommand(parsed, deps = {}) {
|
|
8132
8558
|
const word = parsed.positionals[1] ?? "";
|
|
@@ -8175,6 +8601,20 @@ function statusVerdict(reads) {
|
|
|
8175
8601
|
sourceHttp(reasons, "canary", reads.canary);
|
|
8176
8602
|
sourceHttp(reasons, "collector", reads.collector);
|
|
8177
8603
|
sourceHttp(reasons, "provider", reads.provider);
|
|
8604
|
+
if (reads.providerCapacity) {
|
|
8605
|
+
sourceHttp(
|
|
8606
|
+
reasons,
|
|
8607
|
+
"provider",
|
|
8608
|
+
reads.providerCapacity,
|
|
8609
|
+
"provider_capacity_"
|
|
8610
|
+
);
|
|
8611
|
+
}
|
|
8612
|
+
sourceHttp(
|
|
8613
|
+
reasons,
|
|
8614
|
+
"provider",
|
|
8615
|
+
reads.providerReconciliation,
|
|
8616
|
+
"provider_reconciliation_"
|
|
8617
|
+
);
|
|
8178
8618
|
sourceHttp(reasons, "provider", reads.providerHistory, "provider_history_");
|
|
8179
8619
|
const liveStatus = String(reads.liveSync.body.status ?? "");
|
|
8180
8620
|
if (liveStatus === "partial") {
|
|
@@ -8247,6 +8687,17 @@ function statusVerdict(reads) {
|
|
|
8247
8687
|
severity: "degraded"
|
|
8248
8688
|
});
|
|
8249
8689
|
}
|
|
8690
|
+
const reconciliationStatus = String(
|
|
8691
|
+
reads.providerReconciliation.body.status ?? ""
|
|
8692
|
+
);
|
|
8693
|
+
if (reads.providerReconciliation.httpStatus >= 200 && reads.providerReconciliation.httpStatus < 300 && reconciliationStatus && reconciliationStatus !== "within_budget") {
|
|
8694
|
+
const reconciliationReason = reads.providerReconciliation.body.reason;
|
|
8695
|
+
reasons.push({
|
|
8696
|
+
source: "provider",
|
|
8697
|
+
code: reconciliationStatus === "outside_budget" ? typeof reconciliationReason === "string" && reconciliationReason ? reconciliationReason : "request_coverage_outside_budget" : `request_reconciliation_${reconciliationStatus}`,
|
|
8698
|
+
severity: "degraded"
|
|
8699
|
+
});
|
|
8700
|
+
}
|
|
8250
8701
|
const providerHistoryStatus = String(reads.providerHistory.body.status ?? "");
|
|
8251
8702
|
if (providerHistoryStatus && providerHistoryStatus !== "observed" && providerHistoryStatus !== "no_data") {
|
|
8252
8703
|
reasons.push({
|
|
@@ -8283,11 +8734,138 @@ function sourceHttp(reasons, source, read3, prefix = "") {
|
|
|
8283
8734
|
});
|
|
8284
8735
|
}
|
|
8285
8736
|
|
|
8737
|
+
// src/o11y-output.ts
|
|
8738
|
+
function printO11yStatus(status, out) {
|
|
8739
|
+
out.log(
|
|
8740
|
+
`o11y status ${status.scope.appId}/${status.scope.env} (${status.scope.minutes}m)`
|
|
8741
|
+
);
|
|
8742
|
+
const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record6) : [];
|
|
8743
|
+
const requests = routes.reduce(
|
|
8744
|
+
(total, row) => total + numeric3(row.requests),
|
|
8745
|
+
0
|
|
8746
|
+
);
|
|
8747
|
+
const errors = routes.reduce(
|
|
8748
|
+
(total, row) => total + numeric3(row.errors),
|
|
8749
|
+
0
|
|
8750
|
+
);
|
|
8751
|
+
out.log(
|
|
8752
|
+
`application ${status.application.httpStatus} ${requests} requests ${errors} errors`
|
|
8753
|
+
);
|
|
8754
|
+
const versions = Array.isArray(status.applicationVersions.body.rows) ? status.applicationVersions.body.rows.filter(record6) : [];
|
|
8755
|
+
out.log(
|
|
8756
|
+
`application-versions ${status.applicationVersions.httpStatus} ${versions.length ? versions.slice(0, 5).map(
|
|
8757
|
+
(row) => `${String(row.value || "(unattributed)")}:${numeric3(row.requests)}`
|
|
8758
|
+
).join(", ") : "none observed"}`
|
|
8759
|
+
);
|
|
8760
|
+
out.log(liveSyncLine(status.liveSync));
|
|
8761
|
+
const canaryDurations = record6(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
|
|
8762
|
+
out.log(
|
|
8763
|
+
`canary ${status.canary.httpStatus} ${String(status.canary.body.status ?? status.canary.body.error ?? "unavailable")} ${optionalNumeric(canaryDurations.publishToVisibleMs)} publish-to-visible`
|
|
8764
|
+
);
|
|
8765
|
+
const collectorIngest = record6(status.collector.body.ingest) ? status.collector.body.ingest : {};
|
|
8766
|
+
const collectorStorage = record6(collectorIngest.storage) ? collectorIngest.storage : {};
|
|
8767
|
+
out.log(
|
|
8768
|
+
`collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${numeric3(collectorStorage.affectedPoints)} affected points`
|
|
8769
|
+
);
|
|
8770
|
+
const providerMetrics = record6(status.provider.body.metrics) ? status.provider.body.metrics : {};
|
|
8771
|
+
const providerCapacity = record6(status.provider.body.capacity) ? status.provider.body.capacity : {};
|
|
8772
|
+
const workerMemory = record6(providerCapacity.memory) ? providerCapacity.memory : {};
|
|
8773
|
+
out.log(
|
|
8774
|
+
`cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric3(providerMetrics.requests)} invocations ${numeric3(providerMetrics.errors)} runtime errors ${optionalBytes(workerMemory.headroomBytes)} isolate memory headroom`
|
|
8775
|
+
);
|
|
8776
|
+
for (const line of providerCapacityLines(status.providerCapacity)) {
|
|
8777
|
+
out.log(line);
|
|
8778
|
+
}
|
|
8779
|
+
const coverage = record6(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
|
|
8780
|
+
const coverageCounts = record6(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
|
|
8781
|
+
const coverageBudget = record6(status.providerReconciliation.body.budget) ? status.providerReconciliation.body.budget : {};
|
|
8782
|
+
out.log(
|
|
8783
|
+
`request-coverage ${status.providerReconciliation.httpStatus} ${String(status.providerReconciliation.body.status ?? status.providerReconciliation.body.error ?? "unavailable")} ${optionalPercent(coverage.applicationCoverage)} application/provider ${numeric3(coverageCounts.applicationRequests)}/${numeric3(coverageCounts.providerRequests)} requests \xB1${optionalPercent(coverageBudget.maxRelativeError)} budget`
|
|
8784
|
+
);
|
|
8785
|
+
const providerPoints = Array.isArray(status.providerHistory.body.points) ? status.providerHistory.body.points.length : 0;
|
|
8786
|
+
const providerFreshness = record6(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
|
|
8787
|
+
out.log(
|
|
8788
|
+
`cloudflare-history ${status.providerHistory.httpStatus} ${String(status.providerHistory.body.status ?? status.providerHistory.body.error ?? "unavailable")} ${providerPoints} snapshots ${optionalAge(providerFreshness.ageMs)} old`
|
|
8789
|
+
);
|
|
8790
|
+
out.log(
|
|
8791
|
+
`verdict ${status.verdict.status} ${status.verdict.reasons.map((reason) => `${reason.source}:${reason.code}`).join(", ") || "all required signals healthy"}`
|
|
8792
|
+
);
|
|
8793
|
+
}
|
|
8794
|
+
function providerCapacityLines(read3) {
|
|
8795
|
+
const resources = record6(read3.body.resources) ? read3.body.resources : {};
|
|
8796
|
+
const durableObjects = record6(resources.durableObjects) ? resources.durableObjects : {};
|
|
8797
|
+
const periodic = record6(durableObjects.periodic) ? durableObjects.periodic : {};
|
|
8798
|
+
const storage = record6(durableObjects.sqliteStorage) ? durableObjects.sqliteStorage : {};
|
|
8799
|
+
const d1 = record6(resources.d1) ? resources.d1 : {};
|
|
8800
|
+
const d1Activity = record6(d1.activity) ? d1.activity : {};
|
|
8801
|
+
const d1Storage = record6(d1.storage) ? d1.storage : {};
|
|
8802
|
+
const d1Latency = record6(d1Activity.latency) ? d1Activity.latency : {};
|
|
8803
|
+
const r2 = record6(resources.r2) ? resources.r2 : {};
|
|
8804
|
+
const r2Operations = record6(r2.operations) ? r2.operations : {};
|
|
8805
|
+
const r2Storage = record6(r2.storage) ? r2.storage : {};
|
|
8806
|
+
const status = String(
|
|
8807
|
+
read3.body.status ?? read3.body.error ?? "unavailable"
|
|
8808
|
+
);
|
|
8809
|
+
return [
|
|
8810
|
+
`cloudflare-capacity ${read3.httpStatus} ${status} ${optionalBytes(storage.storedBytes)} SQLite DO storage ${optionalCount(periodic.activeWebsocketConnectionsMax, "active socket max")} ${optionalAgeSeconds(storage.ageSeconds)} storage evidence age`,
|
|
8811
|
+
`cloudflare-d1 ${read3.httpStatus} ${status} ${optionalBytes(d1Storage.databaseSizeBytes)} storage ${numeric3(d1Activity.readQueries) + numeric3(d1Activity.writeQueries)} queries ${numeric3(d1Activity.rowsRead)}/${numeric3(d1Activity.rowsWritten)} rows read/written ${optionalNumeric(d1Latency.worstDatabaseHourP99Ms)} worst database-hour p99 ${optionalAgeSeconds(d1Activity.ageSeconds)} activity evidence age`,
|
|
8812
|
+
`cloudflare-r2 ${read3.httpStatus} ${status} ${optionalBytes(r2Storage.storedBytes)} storage ${numeric3(r2Storage.objectCount)} objects ${numeric3(r2Operations.requests)} operations ${numeric3(r2Operations.userErrorRequests) + numeric3(r2Operations.internalErrorRequests)} provider errors ${optionalAgeSeconds(r2Operations.ageSeconds)} operation evidence age`
|
|
8813
|
+
];
|
|
8814
|
+
}
|
|
8815
|
+
function liveSyncLine(read3) {
|
|
8816
|
+
const performance = record6(read3.body.performance) ? read3.body.performance : {};
|
|
8817
|
+
const commitToSend = record6(performance.commitToSend) ? performance.commitToSend : {};
|
|
8818
|
+
return `live-sync ${read3.httpStatus} ${String(read3.body.status ?? read3.body.error ?? "unavailable")} ${numeric3(read3.body.activeConnections)} active ${optionalNumeric(commitToSend.p95)} commit-to-send p95 ${numeric3(performance.sendFailures)} send failures`;
|
|
8819
|
+
}
|
|
8820
|
+
function record6(value) {
|
|
8821
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
8822
|
+
}
|
|
8823
|
+
function numeric3(value) {
|
|
8824
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
8825
|
+
}
|
|
8826
|
+
function optionalNumeric(value) {
|
|
8827
|
+
return typeof value === "number" && Number.isFinite(value) ? `${value} ms` : "\u2014";
|
|
8828
|
+
}
|
|
8829
|
+
function optionalAge(value) {
|
|
8830
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "unknown";
|
|
8831
|
+
return `${Math.max(0, Math.round(value / 1e3))}s`;
|
|
8832
|
+
}
|
|
8833
|
+
function optionalPercent(value) {
|
|
8834
|
+
return typeof value === "number" && Number.isFinite(value) ? `${Math.round(value * 100)}%` : "\u2014";
|
|
8835
|
+
}
|
|
8836
|
+
function optionalBytes(value) {
|
|
8837
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "\u2014";
|
|
8838
|
+
if (value >= 1024 * 1024 * 1024) {
|
|
8839
|
+
return `${(value / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
|
|
8840
|
+
}
|
|
8841
|
+
if (value >= 1024 * 1024) {
|
|
8842
|
+
return `${(value / (1024 * 1024)).toFixed(1)} MiB`;
|
|
8843
|
+
}
|
|
8844
|
+
if (value >= 1024) return `${(value / 1024).toFixed(1)} KiB`;
|
|
8845
|
+
return `${Math.round(value)} B`;
|
|
8846
|
+
}
|
|
8847
|
+
function optionalCount(value, unit) {
|
|
8848
|
+
return typeof value === "number" && Number.isFinite(value) ? `${value} ${unit}` : `\u2014 ${unit}`;
|
|
8849
|
+
}
|
|
8850
|
+
function optionalAgeSeconds(value) {
|
|
8851
|
+
return typeof value === "number" && Number.isFinite(value) ? `${Math.max(0, Math.round(value))}s` : "unknown";
|
|
8852
|
+
}
|
|
8853
|
+
|
|
8286
8854
|
// src/o11y-command.ts
|
|
8287
8855
|
async function o11yCommand(parsed, deps = {}) {
|
|
8288
8856
|
assertArgs(
|
|
8289
8857
|
parsed,
|
|
8290
|
-
[
|
|
8858
|
+
[
|
|
8859
|
+
"config",
|
|
8860
|
+
"context",
|
|
8861
|
+
"platform",
|
|
8862
|
+
"token",
|
|
8863
|
+
"email",
|
|
8864
|
+
"json",
|
|
8865
|
+
"app",
|
|
8866
|
+
"env",
|
|
8867
|
+
"minutes"
|
|
8868
|
+
],
|
|
8291
8869
|
2
|
|
8292
8870
|
);
|
|
8293
8871
|
const action = parsed.positionals[1];
|
|
@@ -8299,9 +8877,12 @@ async function o11yCommand(parsed, deps = {}) {
|
|
|
8299
8877
|
const minutes = statusMinutes(
|
|
8300
8878
|
numberOpt(parsed.options.minutes, "--minutes") ?? 60
|
|
8301
8879
|
);
|
|
8302
|
-
const
|
|
8303
|
-
|
|
8304
|
-
|
|
8880
|
+
const context = await resolveOperatorContext(parsed, {
|
|
8881
|
+
allowMissingConfig: true,
|
|
8882
|
+
defaultEnvironment: "prod",
|
|
8883
|
+
requireApp: true
|
|
8884
|
+
});
|
|
8885
|
+
const cfg = context.cfg;
|
|
8305
8886
|
const doFetch = deps.fetch ?? fetch;
|
|
8306
8887
|
const out = deps.stdout ?? console;
|
|
8307
8888
|
const token = await getDeveloperToken(
|
|
@@ -8315,13 +8896,30 @@ async function o11yCommand(parsed, deps = {}) {
|
|
|
8315
8896
|
doFetch,
|
|
8316
8897
|
out
|
|
8317
8898
|
);
|
|
8318
|
-
const appId =
|
|
8319
|
-
const env =
|
|
8899
|
+
const appId = context.app.value;
|
|
8900
|
+
const env = context.environment.value;
|
|
8320
8901
|
const base = `${cfg.platformUrl}/o11y/${encodeURIComponent(appId)}`;
|
|
8321
8902
|
const query = new URLSearchParams({ env, minutes: String(minutes) });
|
|
8322
8903
|
const headers = { authorization: `Bearer ${token}` };
|
|
8323
|
-
const
|
|
8904
|
+
const versionsQuery = new URLSearchParams({
|
|
8905
|
+
env,
|
|
8906
|
+
minutes: String(minutes),
|
|
8907
|
+
dimension: "version",
|
|
8908
|
+
metric: "requests"
|
|
8909
|
+
});
|
|
8910
|
+
const [
|
|
8911
|
+
application,
|
|
8912
|
+
applicationVersions,
|
|
8913
|
+
liveSync,
|
|
8914
|
+
canary,
|
|
8915
|
+
collector,
|
|
8916
|
+
provider,
|
|
8917
|
+
providerCapacity,
|
|
8918
|
+
providerReconciliation,
|
|
8919
|
+
providerHistory
|
|
8920
|
+
] = await Promise.all([
|
|
8324
8921
|
read2(`${base}/metrics/red?${query}`, headers, doFetch),
|
|
8922
|
+
read2(`${base}/explore?${versionsQuery}`, headers, doFetch),
|
|
8325
8923
|
read2(
|
|
8326
8924
|
`${base}/live-sync/health?${query}`,
|
|
8327
8925
|
headers,
|
|
@@ -8334,6 +8932,19 @@ async function o11yCommand(parsed, deps = {}) {
|
|
|
8334
8932
|
),
|
|
8335
8933
|
read2(`${base}/self-health?${query}`, headers, doFetch),
|
|
8336
8934
|
read2(`${base}/provider/cloudflare?${query}`, headers, doFetch),
|
|
8935
|
+
appId === "odla-db" ? read2(`${base}/provider/cloudflare/capacity`, headers, doFetch) : Promise.resolve({
|
|
8936
|
+
httpStatus: 204,
|
|
8937
|
+
body: {
|
|
8938
|
+
status: "not_applicable",
|
|
8939
|
+
scope: "cloudflare_account",
|
|
8940
|
+
reason: "account-scoped Durable Object capacity is exposed only through odla-db"
|
|
8941
|
+
}
|
|
8942
|
+
}),
|
|
8943
|
+
read2(
|
|
8944
|
+
`${base}/provider/cloudflare/reconciliation?${query}`,
|
|
8945
|
+
headers,
|
|
8946
|
+
doFetch
|
|
8947
|
+
),
|
|
8337
8948
|
read2(`${base}/provider/cloudflare/history?${query}`, headers, doFetch)
|
|
8338
8949
|
]);
|
|
8339
8950
|
const verdict = statusVerdict({
|
|
@@ -8342,25 +8953,30 @@ async function o11yCommand(parsed, deps = {}) {
|
|
|
8342
8953
|
canary,
|
|
8343
8954
|
collector,
|
|
8344
8955
|
provider,
|
|
8956
|
+
providerCapacity,
|
|
8957
|
+
providerReconciliation,
|
|
8345
8958
|
providerHistory
|
|
8346
8959
|
});
|
|
8347
8960
|
const status = {
|
|
8348
|
-
schemaVersion:
|
|
8961
|
+
schemaVersion: 9,
|
|
8349
8962
|
scope: { appId, env, minutes },
|
|
8350
8963
|
observedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8351
8964
|
verdict,
|
|
8352
8965
|
application,
|
|
8966
|
+
applicationVersions,
|
|
8353
8967
|
liveSync,
|
|
8354
8968
|
canary,
|
|
8355
8969
|
collector,
|
|
8356
8970
|
provider,
|
|
8971
|
+
providerCapacity,
|
|
8972
|
+
providerReconciliation,
|
|
8357
8973
|
providerHistory
|
|
8358
8974
|
};
|
|
8359
8975
|
if (parsed.options.json === true) {
|
|
8360
8976
|
out.log(JSON.stringify(status, null, 2));
|
|
8361
8977
|
return;
|
|
8362
8978
|
}
|
|
8363
|
-
|
|
8979
|
+
printO11yStatus(status, out);
|
|
8364
8980
|
}
|
|
8365
8981
|
function statusMinutes(value) {
|
|
8366
8982
|
if (!Number.isSafeInteger(value) || value < 1 || value > 7 * 24 * 60) {
|
|
@@ -8382,70 +8998,11 @@ async function read2(url, headers, doFetch) {
|
|
|
8382
8998
|
}
|
|
8383
8999
|
return { httpStatus: response2.status, body };
|
|
8384
9000
|
}
|
|
8385
|
-
function printStatus2(status, out) {
|
|
8386
|
-
out.log(
|
|
8387
|
-
`o11y status ${status.scope.appId}/${status.scope.env} (${status.scope.minutes}m)`
|
|
8388
|
-
);
|
|
8389
|
-
const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record6) : [];
|
|
8390
|
-
const requests = routes.reduce(
|
|
8391
|
-
(total, row) => total + numeric3(row.requests),
|
|
8392
|
-
0
|
|
8393
|
-
);
|
|
8394
|
-
const errors = routes.reduce(
|
|
8395
|
-
(total, row) => total + numeric3(row.errors),
|
|
8396
|
-
0
|
|
8397
|
-
);
|
|
8398
|
-
out.log(
|
|
8399
|
-
`application ${status.application.httpStatus} ${requests} requests ${errors} errors`
|
|
8400
|
-
);
|
|
8401
|
-
out.log(
|
|
8402
|
-
liveSyncLine(status.liveSync)
|
|
8403
|
-
);
|
|
8404
|
-
const canaryDurations = record6(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
|
|
8405
|
-
out.log(
|
|
8406
|
-
`canary ${status.canary.httpStatus} ${String(status.canary.body.status ?? status.canary.body.error ?? "unavailable")} ${optionalNumeric(canaryDurations.publishToVisibleMs)} publish-to-visible`
|
|
8407
|
-
);
|
|
8408
|
-
const collectorIngest = record6(status.collector.body.ingest) ? status.collector.body.ingest : {};
|
|
8409
|
-
const collectorStorage = record6(collectorIngest.storage) ? collectorIngest.storage : {};
|
|
8410
|
-
out.log(
|
|
8411
|
-
`collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${numeric3(collectorStorage.affectedPoints)} affected points`
|
|
8412
|
-
);
|
|
8413
|
-
const providerMetrics = record6(status.provider.body.metrics) ? status.provider.body.metrics : {};
|
|
8414
|
-
out.log(
|
|
8415
|
-
`cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric3(providerMetrics.requests)} invocations ${numeric3(providerMetrics.errors)} runtime errors`
|
|
8416
|
-
);
|
|
8417
|
-
const providerPoints = Array.isArray(status.providerHistory.body.points) ? status.providerHistory.body.points.length : 0;
|
|
8418
|
-
const providerFreshness = record6(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
|
|
8419
|
-
out.log(
|
|
8420
|
-
`cloudflare-history ${status.providerHistory.httpStatus} ${String(status.providerHistory.body.status ?? status.providerHistory.body.error ?? "unavailable")} ${providerPoints} snapshots ${optionalAge(providerFreshness.ageMs)} old`
|
|
8421
|
-
);
|
|
8422
|
-
out.log(
|
|
8423
|
-
`verdict ${status.verdict.status} ${status.verdict.reasons.map((reason) => `${reason.source}:${reason.code}`).join(", ") || "all required signals healthy"}`
|
|
8424
|
-
);
|
|
8425
|
-
}
|
|
8426
|
-
function liveSyncLine(read3) {
|
|
8427
|
-
const performance = record6(read3.body.performance) ? read3.body.performance : {};
|
|
8428
|
-
const commitToSend = record6(performance.commitToSend) ? performance.commitToSend : {};
|
|
8429
|
-
return `live-sync ${read3.httpStatus} ${String(read3.body.status ?? read3.body.error ?? "unavailable")} ${numeric3(read3.body.activeConnections)} active ${optionalNumeric(commitToSend.p95)} commit-to-send p95 ${numeric3(performance.sendFailures)} send failures`;
|
|
8430
|
-
}
|
|
8431
|
-
function record6(value) {
|
|
8432
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
8433
|
-
}
|
|
8434
|
-
function numeric3(value) {
|
|
8435
|
-
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
8436
|
-
}
|
|
8437
|
-
function optionalNumeric(value) {
|
|
8438
|
-
return typeof value === "number" && Number.isFinite(value) ? `${value} ms` : "\u2014";
|
|
8439
|
-
}
|
|
8440
|
-
function optionalAge(value) {
|
|
8441
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return "unknown";
|
|
8442
|
-
return `${Math.max(0, Math.round(value / 1e3))}s`;
|
|
8443
|
-
}
|
|
8444
9001
|
|
|
8445
9002
|
// src/provision.ts
|
|
8446
9003
|
var import_apps5 = require("@odla-ai/apps");
|
|
8447
9004
|
var import_ai3 = require("@odla-ai/ai");
|
|
8448
|
-
var
|
|
9005
|
+
var import_node_process10 = __toESM(require("process"), 1);
|
|
8449
9006
|
|
|
8450
9007
|
// src/integration-provision.ts
|
|
8451
9008
|
var import_db3 = require("@odla-ai/db");
|
|
@@ -8804,7 +9361,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
8804
9361
|
await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
|
|
8805
9362
|
}
|
|
8806
9363
|
if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
8807
|
-
const key =
|
|
9364
|
+
const key = import_node_process10.default.env[cfg.ai.keyEnv];
|
|
8808
9365
|
if (key) {
|
|
8809
9366
|
const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
|
|
8810
9367
|
await (0, import_ai3.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
|
|
@@ -8845,8 +9402,8 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
8845
9402
|
}
|
|
8846
9403
|
|
|
8847
9404
|
// src/record.ts
|
|
8848
|
-
var
|
|
8849
|
-
var
|
|
9405
|
+
var import_node_fs15 = require("fs");
|
|
9406
|
+
var import_node_process11 = __toESM(require("process"), 1);
|
|
8850
9407
|
|
|
8851
9408
|
// src/surface.ts
|
|
8852
9409
|
var PM_ACTIONS = {
|
|
@@ -8894,6 +9451,7 @@ var COMMAND_SURFACE = {
|
|
|
8894
9451
|
calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
|
|
8895
9452
|
capabilities: {},
|
|
8896
9453
|
code: { connect: {} },
|
|
9454
|
+
context: { show: {}, list: {}, save: {}, remove: {} },
|
|
8897
9455
|
// `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
|
|
8898
9456
|
discuss: {
|
|
8899
9457
|
groups: {},
|
|
@@ -8997,7 +9555,7 @@ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
|
|
|
8997
9555
|
|
|
8998
9556
|
// src/record.ts
|
|
8999
9557
|
function recordInvocation(parsed) {
|
|
9000
|
-
const file =
|
|
9558
|
+
const file = import_node_process11.default.env.ODLA_CLI_RECORD;
|
|
9001
9559
|
if (!file) return;
|
|
9002
9560
|
try {
|
|
9003
9561
|
const entry = {
|
|
@@ -9005,19 +9563,14 @@ function recordInvocation(parsed) {
|
|
|
9005
9563
|
options: Object.keys(parsed.options).sort()
|
|
9006
9564
|
};
|
|
9007
9565
|
if (!entry.path.length) return;
|
|
9008
|
-
(0,
|
|
9566
|
+
(0, import_node_fs15.appendFileSync)(file, `${JSON.stringify(entry)}
|
|
9009
9567
|
`);
|
|
9010
9568
|
} catch {
|
|
9011
9569
|
}
|
|
9012
9570
|
}
|
|
9013
9571
|
|
|
9014
|
-
// src/runbook-command.ts
|
|
9015
|
-
var import_node_fs18 = require("fs");
|
|
9016
|
-
var import_node_path14 = require("path");
|
|
9017
|
-
var import_node_process10 = __toESM(require("process"), 1);
|
|
9018
|
-
|
|
9019
9572
|
// src/runbook-actions.ts
|
|
9020
|
-
var
|
|
9573
|
+
var import_node_fs16 = require("fs");
|
|
9021
9574
|
|
|
9022
9575
|
// src/runbook-requires.ts
|
|
9023
9576
|
var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
|
|
@@ -9102,7 +9655,7 @@ async function bySlug(ctx, slug) {
|
|
|
9102
9655
|
function readBody(file, inline) {
|
|
9103
9656
|
if (inline !== void 0) return inline;
|
|
9104
9657
|
if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
|
|
9105
|
-
return (0,
|
|
9658
|
+
return (0, import_node_fs16.readFileSync)(file === "-" ? 0 : file, "utf8");
|
|
9106
9659
|
}
|
|
9107
9660
|
var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
|
|
9108
9661
|
async function runbookList(ctx, all, query) {
|
|
@@ -9189,8 +9742,8 @@ async function runbookRemove(ctx, slug) {
|
|
|
9189
9742
|
}
|
|
9190
9743
|
|
|
9191
9744
|
// src/runbook-import.ts
|
|
9192
|
-
var
|
|
9193
|
-
var
|
|
9745
|
+
var import_node_fs17 = require("fs");
|
|
9746
|
+
var import_node_path13 = require("path");
|
|
9194
9747
|
function parseRunbook(text, slug) {
|
|
9195
9748
|
let rest = text;
|
|
9196
9749
|
const meta = {};
|
|
@@ -9215,12 +9768,12 @@ function parseRunbook(text, slug) {
|
|
|
9215
9768
|
};
|
|
9216
9769
|
}
|
|
9217
9770
|
function readRunbookDir(dir) {
|
|
9218
|
-
if (!(0,
|
|
9219
|
-
const files = (0,
|
|
9771
|
+
if (!(0, import_node_fs17.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
|
|
9772
|
+
const files = (0, import_node_fs17.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
|
|
9220
9773
|
if (!files.length) throw new Error(`no .md files in ${dir}`);
|
|
9221
9774
|
return files.map((file) => {
|
|
9222
|
-
const slug = (0,
|
|
9223
|
-
const parsed = parseRunbook((0,
|
|
9775
|
+
const slug = (0, import_node_path13.basename)(file, ".md");
|
|
9776
|
+
const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0, import_node_path13.join)(dir, file), "utf8"), slug);
|
|
9224
9777
|
return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
|
|
9225
9778
|
});
|
|
9226
9779
|
}
|
|
@@ -9292,8 +9845,8 @@ async function upsert(ctx, r, visibility) {
|
|
|
9292
9845
|
|
|
9293
9846
|
// src/runbook-impact.ts
|
|
9294
9847
|
var import_node_child_process7 = require("child_process");
|
|
9295
|
-
var
|
|
9296
|
-
var
|
|
9848
|
+
var import_node_fs18 = require("fs");
|
|
9849
|
+
var import_node_path14 = require("path");
|
|
9297
9850
|
|
|
9298
9851
|
// src/runbook-impact-scan.ts
|
|
9299
9852
|
var DECL = /^[+-]\s*export\s+(?:declare\s+)?(?:default\s+)?(?:abstract\s+)?(?:async\s+)?(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z_$][\w$]*)/;
|
|
@@ -9462,10 +10015,10 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
|
|
|
9462
10015
|
}
|
|
9463
10016
|
function manifestLabeller(root) {
|
|
9464
10017
|
return (workspace) => {
|
|
9465
|
-
const manifest = (0,
|
|
9466
|
-
if (!(0,
|
|
10018
|
+
const manifest = (0, import_node_path14.join)(root, workspace, "package.json");
|
|
10019
|
+
if (!(0, import_node_fs18.existsSync)(manifest)) return void 0;
|
|
9467
10020
|
try {
|
|
9468
|
-
const name = JSON.parse((0,
|
|
10021
|
+
const name = JSON.parse((0, import_node_fs18.readFileSync)(manifest, "utf8")).name;
|
|
9469
10022
|
return typeof name === "string" ? name : void 0;
|
|
9470
10023
|
} catch {
|
|
9471
10024
|
return void 0;
|
|
@@ -9532,7 +10085,7 @@ function report3(ctx, impacts) {
|
|
|
9532
10085
|
async function runbookImpact(ctx, options, deps = {}) {
|
|
9533
10086
|
const cwd = deps.cwd ?? process.cwd();
|
|
9534
10087
|
const runGit = deps.runGit ?? gitRunner(cwd);
|
|
9535
|
-
const read3 = deps.readRepoFile ?? ((path) => (0,
|
|
10088
|
+
const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0, import_node_path14.join)(cwd, path), "utf8"));
|
|
9536
10089
|
const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
|
|
9537
10090
|
if (!surfaces.length) {
|
|
9538
10091
|
return ctx.out.log(
|
|
@@ -9665,12 +10218,12 @@ async function runbookComment(ctx, slug, body) {
|
|
|
9665
10218
|
|
|
9666
10219
|
// src/runbook-editor.ts
|
|
9667
10220
|
var import_node_child_process8 = require("child_process");
|
|
9668
|
-
var
|
|
9669
|
-
var
|
|
9670
|
-
var
|
|
9671
|
-
var
|
|
10221
|
+
var import_node_fs19 = require("fs");
|
|
10222
|
+
var import_node_os5 = require("os");
|
|
10223
|
+
var import_node_path15 = require("path");
|
|
10224
|
+
var import_node_process12 = __toESM(require("process"), 1);
|
|
9672
10225
|
var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
|
|
9673
|
-
function resolveEditor(env =
|
|
10226
|
+
function resolveEditor(env = import_node_process12.default.env) {
|
|
9674
10227
|
for (const name of EDITOR_ENV) {
|
|
9675
10228
|
const value = env[name];
|
|
9676
10229
|
if (value && value.trim()) return value.trim();
|
|
@@ -9684,8 +10237,8 @@ function defaultRun(command, path) {
|
|
|
9684
10237
|
return result.status ?? 0;
|
|
9685
10238
|
}
|
|
9686
10239
|
function editText(initial, slug, deps = {}) {
|
|
9687
|
-
const env = deps.env ??
|
|
9688
|
-
const interactive = deps.interactive ?? (() => Boolean(
|
|
10240
|
+
const env = deps.env ?? import_node_process12.default.env;
|
|
10241
|
+
const interactive = deps.interactive ?? (() => Boolean(import_node_process12.default.stdin.isTTY));
|
|
9689
10242
|
const editor = resolveEditor(env);
|
|
9690
10243
|
if (!editor)
|
|
9691
10244
|
throw new Error(
|
|
@@ -9693,16 +10246,16 @@ function editText(initial, slug, deps = {}) {
|
|
|
9693
10246
|
);
|
|
9694
10247
|
if (!interactive())
|
|
9695
10248
|
throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
|
|
9696
|
-
const dir = (0,
|
|
9697
|
-
const file = (0,
|
|
10249
|
+
const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path15.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
|
|
10250
|
+
const file = (0, import_node_path15.join)(dir, `${slug}.md`);
|
|
9698
10251
|
try {
|
|
9699
|
-
(0,
|
|
10252
|
+
(0, import_node_fs19.writeFileSync)(file, initial, { mode: 384 });
|
|
9700
10253
|
const code = defaultRunOrInjected(deps)(editor, file);
|
|
9701
10254
|
if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
|
|
9702
|
-
const edited = (0,
|
|
10255
|
+
const edited = (0, import_node_fs19.readFileSync)(file, "utf8");
|
|
9703
10256
|
return edited === initial ? null : edited;
|
|
9704
10257
|
} finally {
|
|
9705
|
-
(0,
|
|
10258
|
+
(0, import_node_fs19.rmSync)(dir, { recursive: true, force: true });
|
|
9706
10259
|
}
|
|
9707
10260
|
}
|
|
9708
10261
|
var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
|
|
@@ -9740,10 +10293,16 @@ function credentialKind(identity) {
|
|
|
9740
10293
|
return identity.scopes.length ? "device token (scoped)" : "device token or session";
|
|
9741
10294
|
}
|
|
9742
10295
|
async function whoamiCommand(parsed, deps = {}) {
|
|
9743
|
-
assertArgs(
|
|
10296
|
+
assertArgs(
|
|
10297
|
+
parsed,
|
|
10298
|
+
["config", "context", "platform", "token", "email", "json"],
|
|
10299
|
+
1
|
|
10300
|
+
);
|
|
9744
10301
|
const out = deps.stdout ?? console;
|
|
9745
10302
|
const doFetch = deps.fetch ?? fetch;
|
|
9746
|
-
const cfg = await
|
|
10303
|
+
const { cfg } = await resolveOperatorContext(parsed, {
|
|
10304
|
+
allowMissingConfig: true
|
|
10305
|
+
});
|
|
9747
10306
|
const token = await getDeveloperToken(
|
|
9748
10307
|
cfg,
|
|
9749
10308
|
{
|
|
@@ -9792,34 +10351,24 @@ var ALLOWED3 = [
|
|
|
9792
10351
|
"dry-run",
|
|
9793
10352
|
"limit",
|
|
9794
10353
|
"base",
|
|
9795
|
-
"requires"
|
|
10354
|
+
"requires",
|
|
10355
|
+
"platform",
|
|
10356
|
+
"context"
|
|
9796
10357
|
];
|
|
9797
10358
|
function requireSlug(slug, action) {
|
|
9798
10359
|
if (!slug) throw new Error(`"runbook ${action}" needs a slug, e.g. "odla-ai runbook ${action} release"`);
|
|
9799
10360
|
return slug;
|
|
9800
10361
|
}
|
|
9801
10362
|
var WRITES = /* @__PURE__ */ new Set(["new", "edit", "publish", "archive", "visibility", "revert", "rm", "import"]);
|
|
9802
|
-
var CONFIG_FREE = /* @__PURE__ */ new Set(["list", "search", "ask", "get", "cat", "history", "impact", "lint"]);
|
|
9803
|
-
async function loadOrDefaultConfig(configPath, action, appId) {
|
|
9804
|
-
const projectScoped = appId !== void 0;
|
|
9805
|
-
if (!projectScoped && CONFIG_FREE.has(action) && !(0, import_node_fs18.existsSync)((0, import_node_path14.resolve)(configPath))) {
|
|
9806
|
-
const rootDir = import_node_process10.default.cwd();
|
|
9807
|
-
return {
|
|
9808
|
-
configPath,
|
|
9809
|
-
platformUrl: import_node_process10.default.env.ODLA_PLATFORM_URL ?? "https://odla.ai",
|
|
9810
|
-
rootDir,
|
|
9811
|
-
app: { id: "", name: "" },
|
|
9812
|
-
local: { tokenFile: (0, import_node_path14.join)(rootDir, ".odla/dev-token.json") }
|
|
9813
|
-
};
|
|
9814
|
-
}
|
|
9815
|
-
return loadProjectConfig(configPath);
|
|
9816
|
-
}
|
|
9817
10363
|
async function buildContext3(parsed, deps, action) {
|
|
9818
|
-
const
|
|
9819
|
-
const
|
|
10364
|
+
const appIdOption = stringOpt(parsed.options.app);
|
|
10365
|
+
const context = await resolveOperatorContext(parsed, {
|
|
10366
|
+
allowMissingConfig: true
|
|
10367
|
+
});
|
|
10368
|
+
const { cfg } = context;
|
|
9820
10369
|
const doFetch = deps.fetch ?? fetch;
|
|
9821
10370
|
const out = deps.stdout ?? console;
|
|
9822
|
-
const appId =
|
|
10371
|
+
const appId = appIdOption ?? (context.app.source === "environment" || context.app.source === "profile" ? context.app.value : null) ?? PLATFORM_SCOPE;
|
|
9823
10372
|
const dryRun = parsed.options["dry-run"] === true;
|
|
9824
10373
|
if (action === "import" && dryRun) {
|
|
9825
10374
|
return {
|
|
@@ -9840,8 +10389,10 @@ async function buildContext3(parsed, deps, action) {
|
|
|
9840
10389
|
fetch: doFetch,
|
|
9841
10390
|
stdout: out,
|
|
9842
10391
|
openApprovalUrl: deps.openUrl,
|
|
9843
|
-
//
|
|
9844
|
-
|
|
10392
|
+
// A project, named context, or the global operator context owns the
|
|
10393
|
+
// exact-scope cache; it never follows an arbitrary shell directory.
|
|
10394
|
+
rootDir: cfg.rootDir,
|
|
10395
|
+
tokenFile: context.credentials.scopedTokenFile
|
|
9845
10396
|
}) : await getDeveloperToken(
|
|
9846
10397
|
cfg,
|
|
9847
10398
|
{
|
|
@@ -10116,7 +10667,7 @@ function hostedSeverity(value, flag) {
|
|
|
10116
10667
|
var import_security2 = require("@odla-ai/security");
|
|
10117
10668
|
|
|
10118
10669
|
// src/security.ts
|
|
10119
|
-
var
|
|
10670
|
+
var import_node_path16 = require("path");
|
|
10120
10671
|
var import_security = require("@odla-ai/security");
|
|
10121
10672
|
var import_node3 = require("@odla-ai/security/node");
|
|
10122
10673
|
async function runHostedSecurity(options) {
|
|
@@ -10128,9 +10679,9 @@ async function runHostedSecurity(options) {
|
|
|
10128
10679
|
const appId = selfAudit ? "odla-ai" : cfg.app.id;
|
|
10129
10680
|
const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
|
|
10130
10681
|
const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
|
|
10131
|
-
const target = (0,
|
|
10132
|
-
const output = (0,
|
|
10133
|
-
const outputRelative = (0,
|
|
10682
|
+
const target = (0, import_node_path16.resolve)(options.target ?? cfg?.rootDir ?? ".");
|
|
10683
|
+
const output = (0, import_node_path16.resolve)(options.out ?? (0, import_node_path16.resolve)(target, ".odla/security/hosted"));
|
|
10684
|
+
const outputRelative = (0, import_node_path16.relative)(target, output).split(import_node_path16.sep).join("/");
|
|
10134
10685
|
if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
|
|
10135
10686
|
const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
|
|
10136
10687
|
const tokenRequest = {
|
|
@@ -10142,7 +10693,7 @@ async function runHostedSecurity(options) {
|
|
|
10142
10693
|
};
|
|
10143
10694
|
const token = await injectedToken(options, tokenRequest);
|
|
10144
10695
|
const snapshot = await (0, import_node3.snapshotDirectory)(target, {
|
|
10145
|
-
exclude: !outputRelative.startsWith("../") && !(0,
|
|
10696
|
+
exclude: !outputRelative.startsWith("../") && !(0, import_node_path16.isAbsolute)(outputRelative) ? [outputRelative] : []
|
|
10146
10697
|
});
|
|
10147
10698
|
const hosted = await (0, import_security.createPlatformSecurityReasoners)({
|
|
10148
10699
|
platform,
|
|
@@ -10160,7 +10711,7 @@ async function runHostedSecurity(options) {
|
|
|
10160
10711
|
});
|
|
10161
10712
|
const harness = (0, import_security.createSecurityHarness)({
|
|
10162
10713
|
profile,
|
|
10163
|
-
store: new import_node3.FileRunStore((0,
|
|
10714
|
+
store: new import_node3.FileRunStore((0, import_node_path16.resolve)(output, "state")),
|
|
10164
10715
|
discoveryReasoner: hosted.discoveryReasoner,
|
|
10165
10716
|
validationReasoner: hosted.validationReasoner,
|
|
10166
10717
|
policy: {
|
|
@@ -10184,7 +10735,7 @@ async function runHostedSecurity(options) {
|
|
|
10184
10735
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
10185
10736
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
10186
10737
|
if (!env || !declared.includes(env)) {
|
|
10187
|
-
const shown = (0,
|
|
10738
|
+
const shown = (0, import_node_path16.relative)(rootDir, configPath) || configPath;
|
|
10188
10739
|
throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
|
|
10189
10740
|
}
|
|
10190
10741
|
return env;
|
|
@@ -10213,7 +10764,7 @@ function printSummary(out, appId, env, run, report4, output) {
|
|
|
10213
10764
|
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}`);
|
|
10214
10765
|
if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
|
|
10215
10766
|
out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
|
|
10216
|
-
out.log(` report: ${(0,
|
|
10767
|
+
out.log(` report: ${(0, import_node_path16.resolve)(output, "REPORT.md")}`);
|
|
10217
10768
|
}
|
|
10218
10769
|
function formatBudget(usage) {
|
|
10219
10770
|
return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
|
|
@@ -10693,6 +11244,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
10693
11244
|
await whoamiCommand(parsed, runtime);
|
|
10694
11245
|
return;
|
|
10695
11246
|
}
|
|
11247
|
+
if (command === "context") {
|
|
11248
|
+
await contextCommand(parsed, runtime);
|
|
11249
|
+
return;
|
|
11250
|
+
}
|
|
10696
11251
|
if (command === "runbook") {
|
|
10697
11252
|
await runbookCommand(parsed, runtime);
|
|
10698
11253
|
return;
|