@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/bin.cjs
CHANGED
|
@@ -244,7 +244,7 @@ var import_node_process2 = __toESM(require("process"), 1);
|
|
|
244
244
|
async function openUrl(url, options = {}) {
|
|
245
245
|
const command = openerFor(options.platform ?? import_node_process2.default.platform);
|
|
246
246
|
const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
|
|
247
|
-
await new Promise((
|
|
247
|
+
await new Promise((resolve12, reject) => {
|
|
248
248
|
const child = doSpawn(command.cmd, [...command.args, url], {
|
|
249
249
|
stdio: "ignore",
|
|
250
250
|
detached: true
|
|
@@ -252,7 +252,7 @@ async function openUrl(url, options = {}) {
|
|
|
252
252
|
child.once("error", reject);
|
|
253
253
|
child.once("spawn", () => {
|
|
254
254
|
child.unref();
|
|
255
|
-
|
|
255
|
+
resolve12();
|
|
256
256
|
});
|
|
257
257
|
});
|
|
258
258
|
}
|
|
@@ -2186,8 +2186,8 @@ function credential(value) {
|
|
|
2186
2186
|
// src/calendar-poll.ts
|
|
2187
2187
|
async function waitForCalendarPoll(milliseconds, signal) {
|
|
2188
2188
|
if (signal?.aborted) throw signal.reason ?? new Error("calendar connection aborted");
|
|
2189
|
-
await new Promise((
|
|
2190
|
-
const timer = setTimeout(
|
|
2189
|
+
await new Promise((resolve12, reject) => {
|
|
2190
|
+
const timer = setTimeout(resolve12, milliseconds);
|
|
2191
2191
|
signal?.addEventListener("abort", () => {
|
|
2192
2192
|
clearTimeout(timer);
|
|
2193
2193
|
reject(signal.reason ?? new Error("calendar connection aborted"));
|
|
@@ -2388,7 +2388,8 @@ var CAPABILITIES = {
|
|
|
2388
2388
|
"compose declared app-capability schema/rules, create guarded seeds when absent, and configure platform AI, auth, and deployment links",
|
|
2389
2389
|
"apply Google Calendar booking config, then drive state-bound consent, status, discovery, and disconnect flows (bookings run live through the platform proxy)",
|
|
2390
2390
|
"validate integration contracts offline and smoke-test a provisioned db environment plus anonymous capability routes",
|
|
2391
|
-
"
|
|
2391
|
+
"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",
|
|
2392
|
+
"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",
|
|
2392
2393
|
"inspect durable agent wakeups as a versioned JSON envelope and explicitly requeue one dead-lettered job with a scoped environment credential",
|
|
2393
2394
|
"run app-attributed hosted security discovery and independent validation without provider keys",
|
|
2394
2395
|
"connect/revoke source-read-only GitHub sources and drive commit-pinned hosted security jobs without PATs or provider keys",
|
|
@@ -2410,7 +2411,7 @@ var CAPABILITIES = {
|
|
|
2410
2411
|
"approve GitHub App repository access separately from redacted-snippet disclosure"
|
|
2411
2412
|
],
|
|
2412
2413
|
studio: [
|
|
2413
|
-
"view application telemetry, reconciled live-sync load/freshness, commit-to-visible canary health, provider-owned Worker runtime metrics, and environment state",
|
|
2414
|
+
"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",
|
|
2414
2415
|
"let signed-in users inventory/revoke their own agent grants and admins audit/global-revoke them",
|
|
2415
2416
|
"review calendar connection, granted read scope, selected calendars, and sync health without exposing provider tokens",
|
|
2416
2417
|
"perform manual credential recovery \u2014 for the primary owner or any co-owner \u2014 when the CLI's local shown-once copy is unavailable",
|
|
@@ -5416,21 +5417,21 @@ async function verifyCodeCandidate(input) {
|
|
|
5416
5417
|
const recipes = [];
|
|
5417
5418
|
const logs = [];
|
|
5418
5419
|
for (const recipe2 of policy.recipes) {
|
|
5419
|
-
const
|
|
5420
|
+
const clean4 = await stageWorkspace(staged.workspaceDir, limits);
|
|
5420
5421
|
try {
|
|
5421
|
-
if (await digestStagedWorkspace(
|
|
5422
|
+
if (await digestStagedWorkspace(clean4.workspaceDir, limits) !== sourceDigest) {
|
|
5422
5423
|
throw new TypeError("clean verifier source changed before execution");
|
|
5423
5424
|
}
|
|
5424
5425
|
const result = checkedResult(await input.recipeExecutor.run({
|
|
5425
|
-
workspaceDir:
|
|
5426
|
+
workspaceDir: clean4.workspaceDir,
|
|
5426
5427
|
recipe: recipe2,
|
|
5427
5428
|
signal: input.signal
|
|
5428
5429
|
}), recipe2.maxOutputBytes);
|
|
5429
|
-
const artifacts = await inspectArtifacts(
|
|
5430
|
+
const artifacts = await inspectArtifacts(clean4.workspaceDir, recipe2);
|
|
5430
5431
|
recipes.push(recipeReceipt(recipe2, result, artifacts));
|
|
5431
5432
|
logs.push({ recipeId: recipe2.id, ...boundedLogs(result, recipe2.maxOutputBytes) });
|
|
5432
5433
|
} finally {
|
|
5433
|
-
await
|
|
5434
|
+
await clean4.cleanup();
|
|
5434
5435
|
}
|
|
5435
5436
|
}
|
|
5436
5437
|
const fields = {
|
|
@@ -6571,8 +6572,12 @@ Usage:
|
|
|
6571
6572
|
odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
|
|
6572
6573
|
odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
|
|
6573
6574
|
odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
|
|
6574
|
-
odla-ai
|
|
6575
|
-
odla-ai
|
|
6575
|
+
odla-ai context show [--context <name>] [--platform https://odla.ai] [--app <id>] [--env prod] [--json]
|
|
6576
|
+
odla-ai context list [--json]
|
|
6577
|
+
odla-ai context save <name> [--platform <url>] [--app <id>] [--env <name>] [--json]
|
|
6578
|
+
odla-ai context remove <name> --yes [--json]
|
|
6579
|
+
odla-ai o11y status [--app <id>] [--context <name>] [--platform https://odla.ai] [--env prod] [--minutes 60] [--json]
|
|
6580
|
+
odla-ai whoami [--context <name>] [--platform https://odla.ai] [--json]
|
|
6576
6581
|
odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
|
|
6577
6582
|
odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
|
|
6578
6583
|
odla-ai runbook impact [--base origin/main] [--app <id>] [--all] [--limit <n>] [--json]
|
|
@@ -6637,6 +6642,11 @@ Commands:
|
|
|
6637
6642
|
whoami Report who this terminal is authenticated as, and whether it holds
|
|
6638
6643
|
platform admin. A handshake-minted device token is never admin,
|
|
6639
6644
|
however the human who approved it is configured.
|
|
6645
|
+
context Explain selected config, platform, app, environment, and
|
|
6646
|
+
developer-token provenance without printing credentials or
|
|
6647
|
+
starting a device handshake. Operator work can run outside a
|
|
6648
|
+
project checkout with explicit flags or ODLA_* environment
|
|
6649
|
+
variables.
|
|
6640
6650
|
setup Install offline odla runbooks for common coding-agent harnesses.
|
|
6641
6651
|
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
6642
6652
|
doctor Validate and summarize the project config without network calls.
|
|
@@ -6708,6 +6718,15 @@ Safety:
|
|
|
6708
6718
|
Provision caches the approved developer token and service credentials under
|
|
6709
6719
|
.odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
|
|
6710
6720
|
preflights Wrangler before any shown-once issuance or destructive rotation.
|
|
6721
|
+
Projectless PM, Discussions, o11y, runbook, and identity commands use
|
|
6722
|
+
--platform/--app/--env, ODLA_PLATFORM_URL/ODLA_APP_ID/ODLA_ENV, and
|
|
6723
|
+
ODLA_DEV_TOKEN. Save non-secret scope metadata with "context save", then
|
|
6724
|
+
select it explicitly with --context or ODLA_CONTEXT. Each named context gets
|
|
6725
|
+
isolated developer and scoped-token caches. Override their locations with
|
|
6726
|
+
ODLA_DEV_TOKEN_FILE and ODLA_ADMIN_TOKEN_FILE; ODLA_CONTEXT_FILE relocates
|
|
6727
|
+
the metadata file. Flags and specific ODLA_* scope variables beat a selected
|
|
6728
|
+
context, which beats project config. There is no ambient current context.
|
|
6729
|
+
"context show" reports only provenance and cache state and never authenticates.
|
|
6711
6730
|
Provision opens the approval page in your browser automatically whenever the
|
|
6712
6731
|
machine can show one, including agent-driven runs; only CI, SSH, and
|
|
6713
6732
|
display-less hosts skip it. Use --open to force or --no-open to suppress.
|
|
@@ -6825,8 +6844,8 @@ function hostedPollTimeout(value = 10 * 6e4) {
|
|
|
6825
6844
|
}
|
|
6826
6845
|
async function waitForHostedPoll(milliseconds, signal) {
|
|
6827
6846
|
if (signal?.aborted) throw signal.reason ?? new DOMException("aborted", "AbortError");
|
|
6828
|
-
await new Promise((
|
|
6829
|
-
const timer = setTimeout(
|
|
6847
|
+
await new Promise((resolve12, reject) => {
|
|
6848
|
+
const timer = setTimeout(resolve12, milliseconds);
|
|
6830
6849
|
signal?.addEventListener("abort", () => {
|
|
6831
6850
|
clearTimeout(timer);
|
|
6832
6851
|
reject(signal.reason ?? new DOMException("aborted", "AbortError"));
|
|
@@ -7336,6 +7355,384 @@ async function codeCommand(parsed, dependencies) {
|
|
|
7336
7355
|
});
|
|
7337
7356
|
}
|
|
7338
7357
|
|
|
7358
|
+
// src/operator-credentials.ts
|
|
7359
|
+
var import_node_process7 = __toESM(require("process"), 1);
|
|
7360
|
+
function developerTokenStatus(context, parsed, now = Date.now()) {
|
|
7361
|
+
const cached = readJsonFile(context.cfg.local.tokenFile);
|
|
7362
|
+
const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
|
|
7363
|
+
const source = clean(
|
|
7364
|
+
stringOpt(parsed.options.token)
|
|
7365
|
+
) ? "flag" : clean(import_node_process7.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
|
|
7366
|
+
return {
|
|
7367
|
+
source,
|
|
7368
|
+
cacheFile: context.cfg.local.tokenFile,
|
|
7369
|
+
cacheStatus
|
|
7370
|
+
};
|
|
7371
|
+
}
|
|
7372
|
+
function clean(value) {
|
|
7373
|
+
const normalized = value?.trim();
|
|
7374
|
+
return normalized || void 0;
|
|
7375
|
+
}
|
|
7376
|
+
|
|
7377
|
+
// src/operator-context.ts
|
|
7378
|
+
var import_node_fs14 = require("fs");
|
|
7379
|
+
var import_node_path12 = require("path");
|
|
7380
|
+
var import_node_process9 = __toESM(require("process"), 1);
|
|
7381
|
+
|
|
7382
|
+
// src/operator-profiles.ts
|
|
7383
|
+
var import_node_fs13 = require("fs");
|
|
7384
|
+
var import_node_os4 = require("os");
|
|
7385
|
+
var import_node_path11 = require("path");
|
|
7386
|
+
var import_node_process8 = __toESM(require("process"), 1);
|
|
7387
|
+
function operatorProfileFile() {
|
|
7388
|
+
return (0, import_node_path11.resolve)(
|
|
7389
|
+
clean2(import_node_process8.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path11.join)((0, import_node_os4.homedir)(), ".odla", "contexts.json")
|
|
7390
|
+
);
|
|
7391
|
+
}
|
|
7392
|
+
function resolveOperatorProfile(parsed) {
|
|
7393
|
+
const fromFlag = clean2(stringOpt(parsed.options.context));
|
|
7394
|
+
const fromEnvironment = clean2(import_node_process8.default.env.ODLA_CONTEXT);
|
|
7395
|
+
const name = fromFlag ?? fromEnvironment ?? null;
|
|
7396
|
+
const file = operatorProfileFile();
|
|
7397
|
+
if (!name) {
|
|
7398
|
+
return { name: null, source: "unresolved", file, value: null };
|
|
7399
|
+
}
|
|
7400
|
+
assertOperatorName(name, "context");
|
|
7401
|
+
const profiles = readOperatorProfiles(file);
|
|
7402
|
+
const value = Object.hasOwn(profiles, name) ? profiles[name] : void 0;
|
|
7403
|
+
if (!value) {
|
|
7404
|
+
throw new Error(
|
|
7405
|
+
`operator context "${name}" not found in ${file}; run "odla-ai context list" or save it first`
|
|
7406
|
+
);
|
|
7407
|
+
}
|
|
7408
|
+
return {
|
|
7409
|
+
name,
|
|
7410
|
+
source: fromFlag ? "flag" : "environment",
|
|
7411
|
+
file,
|
|
7412
|
+
value
|
|
7413
|
+
};
|
|
7414
|
+
}
|
|
7415
|
+
function listOperatorProfiles(file = operatorProfileFile()) {
|
|
7416
|
+
return Object.entries(readOperatorProfiles(file)).sort(([a], [b]) => a.localeCompare(b)).map(([name, profile]) => ({ name, ...profile }));
|
|
7417
|
+
}
|
|
7418
|
+
function saveOperatorProfile(name, profile, file = operatorProfileFile()) {
|
|
7419
|
+
assertOperatorName(name, "context");
|
|
7420
|
+
const normalized = validateProfile(profile, `operator context "${name}"`);
|
|
7421
|
+
const profiles = readOperatorProfiles(file);
|
|
7422
|
+
profiles[name] = normalized;
|
|
7423
|
+
writePrivateJson(file, { schemaVersion: 1, profiles });
|
|
7424
|
+
}
|
|
7425
|
+
function removeOperatorProfile(name, file = operatorProfileFile()) {
|
|
7426
|
+
assertOperatorName(name, "context");
|
|
7427
|
+
const profiles = readOperatorProfiles(file);
|
|
7428
|
+
if (!Object.hasOwn(profiles, name)) return false;
|
|
7429
|
+
delete profiles[name];
|
|
7430
|
+
writePrivateJson(file, { schemaVersion: 1, profiles });
|
|
7431
|
+
return true;
|
|
7432
|
+
}
|
|
7433
|
+
function operatorCredentialFiles(selection) {
|
|
7434
|
+
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");
|
|
7435
|
+
return {
|
|
7436
|
+
developer: (0, import_node_path11.join)(base, "dev-token.json"),
|
|
7437
|
+
scoped: (0, import_node_path11.join)(base, "admin-token.local.json")
|
|
7438
|
+
};
|
|
7439
|
+
}
|
|
7440
|
+
function assertOperatorName(value, label) {
|
|
7441
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(value)) {
|
|
7442
|
+
throw new Error(
|
|
7443
|
+
`${label} must contain lowercase letters, numbers, and hyphens`
|
|
7444
|
+
);
|
|
7445
|
+
}
|
|
7446
|
+
}
|
|
7447
|
+
function readOperatorProfiles(file) {
|
|
7448
|
+
if (!(0, import_node_fs13.existsSync)(file)) return emptyProfiles();
|
|
7449
|
+
let raw;
|
|
7450
|
+
try {
|
|
7451
|
+
raw = JSON.parse((0, import_node_fs13.readFileSync)(file, "utf8"));
|
|
7452
|
+
} catch {
|
|
7453
|
+
throw new Error(`operator context file ${file} is not valid JSON`);
|
|
7454
|
+
}
|
|
7455
|
+
if (!raw || typeof raw !== "object" || raw.schemaVersion !== 1 || !raw.profiles || typeof raw.profiles !== "object" || Array.isArray(raw.profiles)) {
|
|
7456
|
+
throw new Error(`operator context file ${file} has an invalid shape`);
|
|
7457
|
+
}
|
|
7458
|
+
const unknown = Object.keys(raw).filter(
|
|
7459
|
+
(key) => !["schemaVersion", "profiles"].includes(key)
|
|
7460
|
+
);
|
|
7461
|
+
if (unknown.length > 0) {
|
|
7462
|
+
throw new Error(
|
|
7463
|
+
`operator context file ${file} contains unsupported field(s): ${unknown.sort().join(", ")}`
|
|
7464
|
+
);
|
|
7465
|
+
}
|
|
7466
|
+
const profiles = emptyProfiles();
|
|
7467
|
+
for (const [name, value] of Object.entries(
|
|
7468
|
+
raw.profiles
|
|
7469
|
+
)) {
|
|
7470
|
+
assertOperatorName(name, "context");
|
|
7471
|
+
profiles[name] = validateProfile(value, `operator context "${name}"`);
|
|
7472
|
+
}
|
|
7473
|
+
return profiles;
|
|
7474
|
+
}
|
|
7475
|
+
function emptyProfiles() {
|
|
7476
|
+
return /* @__PURE__ */ Object.create(null);
|
|
7477
|
+
}
|
|
7478
|
+
function validateProfile(value, label) {
|
|
7479
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
7480
|
+
throw new Error(`${label} has an invalid shape`);
|
|
7481
|
+
}
|
|
7482
|
+
const unknown = Object.keys(value).filter(
|
|
7483
|
+
(key) => !["platform", "app", "environment"].includes(key)
|
|
7484
|
+
);
|
|
7485
|
+
if (unknown.length > 0) {
|
|
7486
|
+
throw new Error(
|
|
7487
|
+
`${label} contains unsupported field(s): ${unknown.sort().join(", ")}; contexts store scope metadata only, never credentials`
|
|
7488
|
+
);
|
|
7489
|
+
}
|
|
7490
|
+
const candidate = value;
|
|
7491
|
+
if (typeof candidate.platform !== "string") {
|
|
7492
|
+
throw new Error(`${label} needs an absolute platform URL`);
|
|
7493
|
+
}
|
|
7494
|
+
const platform = platformAudience(candidate.platform);
|
|
7495
|
+
const app = clean2(candidate.app);
|
|
7496
|
+
const environment2 = clean2(candidate.environment);
|
|
7497
|
+
if (app) assertOperatorName(app, "app");
|
|
7498
|
+
if (environment2) assertOperatorName(environment2, "environment");
|
|
7499
|
+
return {
|
|
7500
|
+
platform,
|
|
7501
|
+
...app ? { app } : {},
|
|
7502
|
+
...environment2 ? { environment: environment2 } : {}
|
|
7503
|
+
};
|
|
7504
|
+
}
|
|
7505
|
+
function clean2(value) {
|
|
7506
|
+
const normalized = value?.trim();
|
|
7507
|
+
return normalized || void 0;
|
|
7508
|
+
}
|
|
7509
|
+
|
|
7510
|
+
// src/operator-context.ts
|
|
7511
|
+
var DEFAULT_PLATFORM2 = "https://odla.ai";
|
|
7512
|
+
async function resolveOperatorContext(parsed, options = {}) {
|
|
7513
|
+
const profile = resolveOperatorProfile(parsed);
|
|
7514
|
+
const configArgument = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
7515
|
+
const configPath = (0, import_node_path12.resolve)(configArgument);
|
|
7516
|
+
const explicitConfig = parsed.options.config !== void 0;
|
|
7517
|
+
const hasConfig = (0, import_node_fs14.existsSync)(configPath);
|
|
7518
|
+
if (!hasConfig && (!options.allowMissingConfig || explicitConfig)) {
|
|
7519
|
+
await loadProjectConfig(configArgument);
|
|
7520
|
+
}
|
|
7521
|
+
const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
|
|
7522
|
+
const platformFlag = clean3(stringOpt(parsed.options.platform));
|
|
7523
|
+
const platformEnvironment = clean3(import_node_process9.default.env.ODLA_PLATFORM_URL);
|
|
7524
|
+
const platformValue = platformAudience(
|
|
7525
|
+
platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
|
|
7526
|
+
);
|
|
7527
|
+
const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
|
|
7528
|
+
const appFlag = clean3(stringOpt(parsed.options.app));
|
|
7529
|
+
const appEnvironment = clean3(import_node_process9.default.env.ODLA_APP_ID);
|
|
7530
|
+
const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
|
|
7531
|
+
const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
|
|
7532
|
+
if (appValue) assertOperatorName(appValue, "app");
|
|
7533
|
+
if (options.requireApp && !appValue) {
|
|
7534
|
+
throw new Error(
|
|
7535
|
+
"app context is unresolved; pass --app <id>, set ODLA_APP_ID, select --context <name>, or run inside a project with odla.config.mjs"
|
|
7536
|
+
);
|
|
7537
|
+
}
|
|
7538
|
+
const envFlag = clean3(stringOpt(parsed.options.env));
|
|
7539
|
+
const envEnvironment = clean3(import_node_process9.default.env.ODLA_ENV);
|
|
7540
|
+
const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
|
|
7541
|
+
const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
|
|
7542
|
+
if (environmentValue) {
|
|
7543
|
+
assertOperatorName(environmentValue, "environment");
|
|
7544
|
+
}
|
|
7545
|
+
const rootDir = loaded?.rootDir ?? import_node_process9.default.cwd();
|
|
7546
|
+
const profileCredentials = operatorCredentialFiles(profile);
|
|
7547
|
+
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;
|
|
7548
|
+
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;
|
|
7549
|
+
const cfg = loaded ? {
|
|
7550
|
+
...loaded,
|
|
7551
|
+
platformUrl: platformValue,
|
|
7552
|
+
app: appValue ? {
|
|
7553
|
+
id: appValue,
|
|
7554
|
+
name: appValue === loaded.app.id ? loaded.app.name : appValue
|
|
7555
|
+
} : loaded.app,
|
|
7556
|
+
local: { ...loaded.local, tokenFile }
|
|
7557
|
+
} : {
|
|
7558
|
+
configPath,
|
|
7559
|
+
rootDir,
|
|
7560
|
+
platformUrl: platformValue,
|
|
7561
|
+
dbEndpoint: platformValue,
|
|
7562
|
+
app: {
|
|
7563
|
+
id: appValue ?? "operator",
|
|
7564
|
+
name: appValue ?? "Operator"
|
|
7565
|
+
},
|
|
7566
|
+
envs: environmentValue ? [environmentValue] : [],
|
|
7567
|
+
services: [],
|
|
7568
|
+
local: {
|
|
7569
|
+
tokenFile,
|
|
7570
|
+
credentialsFile: (0, import_node_path12.join)(rootDir, ".odla", "credentials.local.json"),
|
|
7571
|
+
devVarsFile: (0, import_node_path12.join)(rootDir, ".dev.vars"),
|
|
7572
|
+
gitignore: true
|
|
7573
|
+
}
|
|
7574
|
+
};
|
|
7575
|
+
return {
|
|
7576
|
+
cfg,
|
|
7577
|
+
profile: {
|
|
7578
|
+
name: profile.name,
|
|
7579
|
+
source: profile.source,
|
|
7580
|
+
file: profile.file
|
|
7581
|
+
},
|
|
7582
|
+
config: {
|
|
7583
|
+
path: configPath,
|
|
7584
|
+
status: loaded ? "loaded" : "absent",
|
|
7585
|
+
explicit: explicitConfig
|
|
7586
|
+
},
|
|
7587
|
+
platform: { value: platformValue, source: platformSource },
|
|
7588
|
+
app: { value: appValue, source: appSource },
|
|
7589
|
+
environment: {
|
|
7590
|
+
value: environmentValue,
|
|
7591
|
+
source: environmentSource
|
|
7592
|
+
},
|
|
7593
|
+
credentials: {
|
|
7594
|
+
developerTokenFile: tokenFile,
|
|
7595
|
+
scopedTokenFile
|
|
7596
|
+
}
|
|
7597
|
+
};
|
|
7598
|
+
}
|
|
7599
|
+
function clean3(value) {
|
|
7600
|
+
const normalized = value?.trim();
|
|
7601
|
+
return normalized || void 0;
|
|
7602
|
+
}
|
|
7603
|
+
|
|
7604
|
+
// src/context-command.ts
|
|
7605
|
+
async function contextCommand(parsed, deps = {}) {
|
|
7606
|
+
assertArgs(
|
|
7607
|
+
parsed,
|
|
7608
|
+
[
|
|
7609
|
+
"config",
|
|
7610
|
+
"context",
|
|
7611
|
+
"platform",
|
|
7612
|
+
"app",
|
|
7613
|
+
"env",
|
|
7614
|
+
"token",
|
|
7615
|
+
"json",
|
|
7616
|
+
"yes"
|
|
7617
|
+
],
|
|
7618
|
+
3
|
|
7619
|
+
);
|
|
7620
|
+
const action = parsed.positionals[1];
|
|
7621
|
+
const out = deps.stdout ?? console;
|
|
7622
|
+
if (action === "list") {
|
|
7623
|
+
const profiles = listOperatorProfiles();
|
|
7624
|
+
if (parsed.options.json === true) {
|
|
7625
|
+
out.log(JSON.stringify({ schemaVersion: 1, profiles }, null, 2));
|
|
7626
|
+
return;
|
|
7627
|
+
}
|
|
7628
|
+
if (profiles.length === 0) {
|
|
7629
|
+
out.log("No named operator contexts.");
|
|
7630
|
+
return;
|
|
7631
|
+
}
|
|
7632
|
+
out.log("name platform app environment");
|
|
7633
|
+
for (const profile of profiles) {
|
|
7634
|
+
out.log(
|
|
7635
|
+
`${profile.name} ${profile.platform} ${profile.app ?? ""} ${profile.environment ?? ""}`
|
|
7636
|
+
);
|
|
7637
|
+
}
|
|
7638
|
+
return;
|
|
7639
|
+
}
|
|
7640
|
+
if (action === "save") {
|
|
7641
|
+
if (parsed.options.token !== void 0) {
|
|
7642
|
+
throw new Error(
|
|
7643
|
+
"context save does not accept --token; contexts store scope metadata only"
|
|
7644
|
+
);
|
|
7645
|
+
}
|
|
7646
|
+
const name = requireName(parsed);
|
|
7647
|
+
const context2 = await resolveOperatorContext(parsed, {
|
|
7648
|
+
allowMissingConfig: true
|
|
7649
|
+
});
|
|
7650
|
+
const profile = {
|
|
7651
|
+
platform: context2.platform.value,
|
|
7652
|
+
...context2.app.value ? { app: context2.app.value } : {},
|
|
7653
|
+
...context2.environment.value ? { environment: context2.environment.value } : {}
|
|
7654
|
+
};
|
|
7655
|
+
saveOperatorProfile(name, profile, context2.profile.file);
|
|
7656
|
+
if (parsed.options.json === true) {
|
|
7657
|
+
out.log(JSON.stringify({ schemaVersion: 1, name, ...profile }, null, 2));
|
|
7658
|
+
} else {
|
|
7659
|
+
out.log(`saved operator context "${name}" (${profile.platform})`);
|
|
7660
|
+
}
|
|
7661
|
+
return;
|
|
7662
|
+
}
|
|
7663
|
+
if (action === "remove") {
|
|
7664
|
+
const name = requireName(parsed);
|
|
7665
|
+
if (parsed.options.yes !== true) {
|
|
7666
|
+
throw new Error(`context remove "${name}" requires --yes`);
|
|
7667
|
+
}
|
|
7668
|
+
const removed = removeOperatorProfile(name);
|
|
7669
|
+
const result2 = {
|
|
7670
|
+
schemaVersion: 1,
|
|
7671
|
+
name,
|
|
7672
|
+
removed,
|
|
7673
|
+
credentialCachesRemoved: false,
|
|
7674
|
+
grantsRevoked: false
|
|
7675
|
+
};
|
|
7676
|
+
if (parsed.options.json === true) out.log(JSON.stringify(result2, null, 2));
|
|
7677
|
+
else {
|
|
7678
|
+
const status = removed ? `removed operator context "${name}"` : `operator context "${name}" was already absent`;
|
|
7679
|
+
out.log(`${status}; credential caches were not deleted or revoked`);
|
|
7680
|
+
}
|
|
7681
|
+
return;
|
|
7682
|
+
}
|
|
7683
|
+
if (action !== "show") {
|
|
7684
|
+
throw new Error(
|
|
7685
|
+
`unknown context action "${action ?? ""}". Try show|list|save|remove.`
|
|
7686
|
+
);
|
|
7687
|
+
}
|
|
7688
|
+
const context = await resolveOperatorContext(parsed, {
|
|
7689
|
+
allowMissingConfig: true,
|
|
7690
|
+
defaultEnvironment: "prod"
|
|
7691
|
+
});
|
|
7692
|
+
const token = developerTokenStatus(context, parsed);
|
|
7693
|
+
const result = {
|
|
7694
|
+
schemaVersion: 1,
|
|
7695
|
+
profile: context.profile,
|
|
7696
|
+
config: context.config,
|
|
7697
|
+
platform: context.platform,
|
|
7698
|
+
app: context.app,
|
|
7699
|
+
environment: context.environment,
|
|
7700
|
+
authentication: {
|
|
7701
|
+
developerToken: token,
|
|
7702
|
+
scopedTokenCacheFile: context.credentials.scopedTokenFile,
|
|
7703
|
+
handshakeStarted: false
|
|
7704
|
+
}
|
|
7705
|
+
};
|
|
7706
|
+
if (parsed.options.json === true) {
|
|
7707
|
+
out.log(JSON.stringify(result, null, 2));
|
|
7708
|
+
return;
|
|
7709
|
+
}
|
|
7710
|
+
out.log(`config: ${context.config.status} (${context.config.path})`);
|
|
7711
|
+
out.log(
|
|
7712
|
+
`context: ${context.profile.name ?? "(none)"} (${context.profile.source}; ${context.profile.file})`
|
|
7713
|
+
);
|
|
7714
|
+
out.log(
|
|
7715
|
+
`platform: ${context.platform.value} (${context.platform.source})`
|
|
7716
|
+
);
|
|
7717
|
+
out.log(
|
|
7718
|
+
`app: ${context.app.value ?? "(unresolved)"} (${context.app.source})`
|
|
7719
|
+
);
|
|
7720
|
+
out.log(
|
|
7721
|
+
`env: ${context.environment.value ?? "(unresolved)"} (${context.environment.source})`
|
|
7722
|
+
);
|
|
7723
|
+
out.log(
|
|
7724
|
+
`auth: ${token.source} (cache ${token.cacheStatus}: ${token.cacheFile})`
|
|
7725
|
+
);
|
|
7726
|
+
out.log("handshake: not started");
|
|
7727
|
+
}
|
|
7728
|
+
function requireName(parsed) {
|
|
7729
|
+
const name = parsed.positionals[2]?.trim();
|
|
7730
|
+
if (!name) {
|
|
7731
|
+
throw new Error(`context ${parsed.positionals[1]} needs a profile name`);
|
|
7732
|
+
}
|
|
7733
|
+
return name;
|
|
7734
|
+
}
|
|
7735
|
+
|
|
7339
7736
|
// src/discuss-actions.ts
|
|
7340
7737
|
var writeMutationId = (parsed) => stringOpt(parsed.options["mutation-id"]) ?? crypto.randomUUID();
|
|
7341
7738
|
async function request(ctx, method, path, body) {
|
|
@@ -7387,8 +7784,9 @@ async function discussGroups(ctx) {
|
|
|
7387
7784
|
}
|
|
7388
7785
|
async function discussList(ctx, parsed) {
|
|
7389
7786
|
const query = new URLSearchParams();
|
|
7787
|
+
const app = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
7788
|
+
if (app) query.set("app", app);
|
|
7390
7789
|
for (const [flag, param] of [
|
|
7391
|
-
["app", "app"],
|
|
7392
7790
|
["q", "q"],
|
|
7393
7791
|
["state", "state"],
|
|
7394
7792
|
["limit", "limit"],
|
|
@@ -7464,7 +7862,7 @@ function renderRead(ctx, topic, posts) {
|
|
|
7464
7862
|
}
|
|
7465
7863
|
}
|
|
7466
7864
|
async function discussPost(ctx, parsed) {
|
|
7467
|
-
const appId = stringOpt(parsed.options.app);
|
|
7865
|
+
const appId = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
7468
7866
|
if (!appId) throw new Error("discuss post needs --app <appId>");
|
|
7469
7867
|
const subject = stringOpt(parsed.options.subject);
|
|
7470
7868
|
if (!subject) throw new Error('discuss post needs --subject "\u2026"');
|
|
@@ -7496,7 +7894,7 @@ async function discussResolve(ctx, id, resolved, parsed) {
|
|
|
7496
7894
|
}
|
|
7497
7895
|
async function discussWho(ctx, parsed) {
|
|
7498
7896
|
const query = new URLSearchParams({ q: stringOpt(parsed.options.q) ?? "" });
|
|
7499
|
-
const app = stringOpt(parsed.options.app);
|
|
7897
|
+
const app = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
7500
7898
|
if (app) query.set("app", app);
|
|
7501
7899
|
const kinds = stringOpt(parsed.options.kinds);
|
|
7502
7900
|
if (kinds) query.set("kinds", kinds);
|
|
@@ -7600,14 +7998,14 @@ function jsonl(ctx, parsed, value) {
|
|
|
7600
7998
|
}
|
|
7601
7999
|
async function discussWatch(ctx, topicId, parsed) {
|
|
7602
8000
|
if (ctx.json && parsed.options.jsonl === true) throw new Error("--json and --jsonl cannot be combined");
|
|
7603
|
-
const sleep = ctx.sleep ?? ((ms) => new Promise((
|
|
8001
|
+
const sleep = ctx.sleep ?? ((ms) => new Promise((resolve12) => setTimeout(resolve12, ms)));
|
|
7604
8002
|
const now = ctx.now ?? Date.now;
|
|
7605
8003
|
const intervalMs = (numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) ?? DEFAULT_INTERVAL_MS / 1e3) * 1e3;
|
|
7606
8004
|
const timeoutSeconds = numberOpt2(parsed, "timeout");
|
|
7607
8005
|
const deadline = timeoutSeconds === void 0 ? void 0 : now() + timeoutSeconds * 1e3;
|
|
7608
8006
|
const by = stringOpt(parsed.options.by);
|
|
7609
8007
|
const self = stringOpt(parsed.options.self);
|
|
7610
|
-
const app = stringOpt(parsed.options.app);
|
|
8008
|
+
const app = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
7611
8009
|
let cursor = stringOpt(parsed.options.cursor);
|
|
7612
8010
|
let firstSuccess = true;
|
|
7613
8011
|
let consecutiveFailures = 0;
|
|
@@ -7752,14 +8150,19 @@ var ALLOWED = [
|
|
|
7752
8150
|
"timeout",
|
|
7753
8151
|
"cursor",
|
|
7754
8152
|
"jsonl",
|
|
7755
|
-
"mutation-id"
|
|
8153
|
+
"mutation-id",
|
|
8154
|
+
"platform",
|
|
8155
|
+
"context"
|
|
7756
8156
|
];
|
|
7757
8157
|
function requireId(id, action) {
|
|
7758
8158
|
if (!id) throw new Error(`"discuss ${action}" needs a topic id`);
|
|
7759
8159
|
return id;
|
|
7760
8160
|
}
|
|
7761
8161
|
async function buildContext(parsed, deps) {
|
|
7762
|
-
const
|
|
8162
|
+
const context = await resolveOperatorContext(parsed, {
|
|
8163
|
+
allowMissingConfig: true
|
|
8164
|
+
});
|
|
8165
|
+
const { cfg } = context;
|
|
7763
8166
|
const doFetch = deps.fetch ?? fetch;
|
|
7764
8167
|
const out = deps.stdout ?? console;
|
|
7765
8168
|
const token = await getDeveloperToken(
|
|
@@ -7773,7 +8176,16 @@ async function buildContext(parsed, deps) {
|
|
|
7773
8176
|
doFetch,
|
|
7774
8177
|
out
|
|
7775
8178
|
);
|
|
7776
|
-
return {
|
|
8179
|
+
return {
|
|
8180
|
+
platformUrl: cfg.platformUrl,
|
|
8181
|
+
token,
|
|
8182
|
+
doFetch,
|
|
8183
|
+
out,
|
|
8184
|
+
json: parsed.options.json === true,
|
|
8185
|
+
// Preserve Discussions' existing cross-project default for config-backed
|
|
8186
|
+
// use while allowing remote agents to scope via ODLA_APP_ID or a profile.
|
|
8187
|
+
appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0
|
|
8188
|
+
};
|
|
7777
8189
|
}
|
|
7778
8190
|
async function discussCommand(parsed, deps = {}) {
|
|
7779
8191
|
assertArgs(parsed, ALLOWED, 3);
|
|
@@ -7885,7 +8297,7 @@ async function pmList(ctx, entity, parsed) {
|
|
|
7885
8297
|
assignee: "assigneeId",
|
|
7886
8298
|
decision: "decisionId"
|
|
7887
8299
|
};
|
|
7888
|
-
const app = stringOpt(parsed.options.app);
|
|
8300
|
+
const app = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
7889
8301
|
if (app) q.set("app", app);
|
|
7890
8302
|
for (const [flag, param] of Object.entries(filters)) {
|
|
7891
8303
|
const v = stringOpt(parsed.options[flag]);
|
|
@@ -7904,7 +8316,7 @@ async function pmList(ctx, entity, parsed) {
|
|
|
7904
8316
|
});
|
|
7905
8317
|
}
|
|
7906
8318
|
async function pmAdd(ctx, entity, parsed) {
|
|
7907
|
-
const appId = stringOpt(parsed.options.app);
|
|
8319
|
+
const appId = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
7908
8320
|
if (!appId) throw new Error("pm add needs --app <appId>");
|
|
7909
8321
|
const input = collectEntityFields(entity, parsed, false);
|
|
7910
8322
|
if (!input.title) throw new Error("pm add needs --title <title>");
|
|
@@ -7955,7 +8367,7 @@ async function allRecords(ctx, entity, appId) {
|
|
|
7955
8367
|
}
|
|
7956
8368
|
}
|
|
7957
8369
|
async function pmHandoff(ctx, parsed) {
|
|
7958
|
-
const appId = stringOpt(parsed.options.app);
|
|
8370
|
+
const appId = stringOpt(parsed.options.app) ?? ctx.appId;
|
|
7959
8371
|
if (!appId) throw new Error("pm handoff needs --app <appId>");
|
|
7960
8372
|
const [goals, tasks, bugs] = await Promise.all([
|
|
7961
8373
|
allRecords(ctx, "goal", appId),
|
|
@@ -8050,14 +8462,19 @@ var ALLOWED2 = [
|
|
|
8050
8462
|
"limit",
|
|
8051
8463
|
"offset",
|
|
8052
8464
|
"q",
|
|
8053
|
-
"mutation-id"
|
|
8465
|
+
"mutation-id",
|
|
8466
|
+
"platform",
|
|
8467
|
+
"context"
|
|
8054
8468
|
];
|
|
8055
8469
|
function requireId2(id, action) {
|
|
8056
8470
|
if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
|
|
8057
8471
|
return id;
|
|
8058
8472
|
}
|
|
8059
8473
|
async function buildContext2(parsed, deps) {
|
|
8060
|
-
const
|
|
8474
|
+
const context = await resolveOperatorContext(parsed, {
|
|
8475
|
+
allowMissingConfig: true
|
|
8476
|
+
});
|
|
8477
|
+
const { cfg } = context;
|
|
8061
8478
|
const doFetch = deps.fetch ?? fetch;
|
|
8062
8479
|
const out = deps.stdout ?? console;
|
|
8063
8480
|
const token = await getDeveloperToken(
|
|
@@ -8066,7 +8483,16 @@ async function buildContext2(parsed, deps) {
|
|
|
8066
8483
|
doFetch,
|
|
8067
8484
|
out
|
|
8068
8485
|
);
|
|
8069
|
-
return {
|
|
8486
|
+
return {
|
|
8487
|
+
platformUrl: cfg.platformUrl,
|
|
8488
|
+
token,
|
|
8489
|
+
doFetch,
|
|
8490
|
+
out,
|
|
8491
|
+
json: parsed.options.json === true,
|
|
8492
|
+
// Preserve PM's existing cross-project default when a config is present;
|
|
8493
|
+
// only an explicit remote-agent environment or profile selects an app.
|
|
8494
|
+
appId: context.app.source === "environment" || context.app.source === "profile" ? context.app.value ?? void 0 : void 0
|
|
8495
|
+
};
|
|
8070
8496
|
}
|
|
8071
8497
|
async function pmCommand(parsed, deps = {}) {
|
|
8072
8498
|
const word = parsed.positionals[1] ?? "";
|
|
@@ -8115,6 +8541,20 @@ function statusVerdict(reads) {
|
|
|
8115
8541
|
sourceHttp(reasons, "canary", reads.canary);
|
|
8116
8542
|
sourceHttp(reasons, "collector", reads.collector);
|
|
8117
8543
|
sourceHttp(reasons, "provider", reads.provider);
|
|
8544
|
+
if (reads.providerCapacity) {
|
|
8545
|
+
sourceHttp(
|
|
8546
|
+
reasons,
|
|
8547
|
+
"provider",
|
|
8548
|
+
reads.providerCapacity,
|
|
8549
|
+
"provider_capacity_"
|
|
8550
|
+
);
|
|
8551
|
+
}
|
|
8552
|
+
sourceHttp(
|
|
8553
|
+
reasons,
|
|
8554
|
+
"provider",
|
|
8555
|
+
reads.providerReconciliation,
|
|
8556
|
+
"provider_reconciliation_"
|
|
8557
|
+
);
|
|
8118
8558
|
sourceHttp(reasons, "provider", reads.providerHistory, "provider_history_");
|
|
8119
8559
|
const liveStatus = String(reads.liveSync.body.status ?? "");
|
|
8120
8560
|
if (liveStatus === "partial") {
|
|
@@ -8187,6 +8627,17 @@ function statusVerdict(reads) {
|
|
|
8187
8627
|
severity: "degraded"
|
|
8188
8628
|
});
|
|
8189
8629
|
}
|
|
8630
|
+
const reconciliationStatus = String(
|
|
8631
|
+
reads.providerReconciliation.body.status ?? ""
|
|
8632
|
+
);
|
|
8633
|
+
if (reads.providerReconciliation.httpStatus >= 200 && reads.providerReconciliation.httpStatus < 300 && reconciliationStatus && reconciliationStatus !== "within_budget") {
|
|
8634
|
+
const reconciliationReason = reads.providerReconciliation.body.reason;
|
|
8635
|
+
reasons.push({
|
|
8636
|
+
source: "provider",
|
|
8637
|
+
code: reconciliationStatus === "outside_budget" ? typeof reconciliationReason === "string" && reconciliationReason ? reconciliationReason : "request_coverage_outside_budget" : `request_reconciliation_${reconciliationStatus}`,
|
|
8638
|
+
severity: "degraded"
|
|
8639
|
+
});
|
|
8640
|
+
}
|
|
8190
8641
|
const providerHistoryStatus = String(reads.providerHistory.body.status ?? "");
|
|
8191
8642
|
if (providerHistoryStatus && providerHistoryStatus !== "observed" && providerHistoryStatus !== "no_data") {
|
|
8192
8643
|
reasons.push({
|
|
@@ -8223,11 +8674,138 @@ function sourceHttp(reasons, source, read3, prefix = "") {
|
|
|
8223
8674
|
});
|
|
8224
8675
|
}
|
|
8225
8676
|
|
|
8677
|
+
// src/o11y-output.ts
|
|
8678
|
+
function printO11yStatus(status, out) {
|
|
8679
|
+
out.log(
|
|
8680
|
+
`o11y status ${status.scope.appId}/${status.scope.env} (${status.scope.minutes}m)`
|
|
8681
|
+
);
|
|
8682
|
+
const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record6) : [];
|
|
8683
|
+
const requests = routes.reduce(
|
|
8684
|
+
(total, row) => total + numeric3(row.requests),
|
|
8685
|
+
0
|
|
8686
|
+
);
|
|
8687
|
+
const errors = routes.reduce(
|
|
8688
|
+
(total, row) => total + numeric3(row.errors),
|
|
8689
|
+
0
|
|
8690
|
+
);
|
|
8691
|
+
out.log(
|
|
8692
|
+
`application ${status.application.httpStatus} ${requests} requests ${errors} errors`
|
|
8693
|
+
);
|
|
8694
|
+
const versions = Array.isArray(status.applicationVersions.body.rows) ? status.applicationVersions.body.rows.filter(record6) : [];
|
|
8695
|
+
out.log(
|
|
8696
|
+
`application-versions ${status.applicationVersions.httpStatus} ${versions.length ? versions.slice(0, 5).map(
|
|
8697
|
+
(row) => `${String(row.value || "(unattributed)")}:${numeric3(row.requests)}`
|
|
8698
|
+
).join(", ") : "none observed"}`
|
|
8699
|
+
);
|
|
8700
|
+
out.log(liveSyncLine(status.liveSync));
|
|
8701
|
+
const canaryDurations = record6(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
|
|
8702
|
+
out.log(
|
|
8703
|
+
`canary ${status.canary.httpStatus} ${String(status.canary.body.status ?? status.canary.body.error ?? "unavailable")} ${optionalNumeric(canaryDurations.publishToVisibleMs)} publish-to-visible`
|
|
8704
|
+
);
|
|
8705
|
+
const collectorIngest = record6(status.collector.body.ingest) ? status.collector.body.ingest : {};
|
|
8706
|
+
const collectorStorage = record6(collectorIngest.storage) ? collectorIngest.storage : {};
|
|
8707
|
+
out.log(
|
|
8708
|
+
`collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${numeric3(collectorStorage.affectedPoints)} affected points`
|
|
8709
|
+
);
|
|
8710
|
+
const providerMetrics = record6(status.provider.body.metrics) ? status.provider.body.metrics : {};
|
|
8711
|
+
const providerCapacity = record6(status.provider.body.capacity) ? status.provider.body.capacity : {};
|
|
8712
|
+
const workerMemory = record6(providerCapacity.memory) ? providerCapacity.memory : {};
|
|
8713
|
+
out.log(
|
|
8714
|
+
`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`
|
|
8715
|
+
);
|
|
8716
|
+
for (const line of providerCapacityLines(status.providerCapacity)) {
|
|
8717
|
+
out.log(line);
|
|
8718
|
+
}
|
|
8719
|
+
const coverage = record6(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
|
|
8720
|
+
const coverageCounts = record6(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
|
|
8721
|
+
const coverageBudget = record6(status.providerReconciliation.body.budget) ? status.providerReconciliation.body.budget : {};
|
|
8722
|
+
out.log(
|
|
8723
|
+
`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`
|
|
8724
|
+
);
|
|
8725
|
+
const providerPoints = Array.isArray(status.providerHistory.body.points) ? status.providerHistory.body.points.length : 0;
|
|
8726
|
+
const providerFreshness = record6(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
|
|
8727
|
+
out.log(
|
|
8728
|
+
`cloudflare-history ${status.providerHistory.httpStatus} ${String(status.providerHistory.body.status ?? status.providerHistory.body.error ?? "unavailable")} ${providerPoints} snapshots ${optionalAge(providerFreshness.ageMs)} old`
|
|
8729
|
+
);
|
|
8730
|
+
out.log(
|
|
8731
|
+
`verdict ${status.verdict.status} ${status.verdict.reasons.map((reason) => `${reason.source}:${reason.code}`).join(", ") || "all required signals healthy"}`
|
|
8732
|
+
);
|
|
8733
|
+
}
|
|
8734
|
+
function providerCapacityLines(read3) {
|
|
8735
|
+
const resources = record6(read3.body.resources) ? read3.body.resources : {};
|
|
8736
|
+
const durableObjects = record6(resources.durableObjects) ? resources.durableObjects : {};
|
|
8737
|
+
const periodic = record6(durableObjects.periodic) ? durableObjects.periodic : {};
|
|
8738
|
+
const storage = record6(durableObjects.sqliteStorage) ? durableObjects.sqliteStorage : {};
|
|
8739
|
+
const d1 = record6(resources.d1) ? resources.d1 : {};
|
|
8740
|
+
const d1Activity = record6(d1.activity) ? d1.activity : {};
|
|
8741
|
+
const d1Storage = record6(d1.storage) ? d1.storage : {};
|
|
8742
|
+
const d1Latency = record6(d1Activity.latency) ? d1Activity.latency : {};
|
|
8743
|
+
const r2 = record6(resources.r2) ? resources.r2 : {};
|
|
8744
|
+
const r2Operations = record6(r2.operations) ? r2.operations : {};
|
|
8745
|
+
const r2Storage = record6(r2.storage) ? r2.storage : {};
|
|
8746
|
+
const status = String(
|
|
8747
|
+
read3.body.status ?? read3.body.error ?? "unavailable"
|
|
8748
|
+
);
|
|
8749
|
+
return [
|
|
8750
|
+
`cloudflare-capacity ${read3.httpStatus} ${status} ${optionalBytes(storage.storedBytes)} SQLite DO storage ${optionalCount(periodic.activeWebsocketConnectionsMax, "active socket max")} ${optionalAgeSeconds(storage.ageSeconds)} storage evidence age`,
|
|
8751
|
+
`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`,
|
|
8752
|
+
`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`
|
|
8753
|
+
];
|
|
8754
|
+
}
|
|
8755
|
+
function liveSyncLine(read3) {
|
|
8756
|
+
const performance = record6(read3.body.performance) ? read3.body.performance : {};
|
|
8757
|
+
const commitToSend = record6(performance.commitToSend) ? performance.commitToSend : {};
|
|
8758
|
+
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`;
|
|
8759
|
+
}
|
|
8760
|
+
function record6(value) {
|
|
8761
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
8762
|
+
}
|
|
8763
|
+
function numeric3(value) {
|
|
8764
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
8765
|
+
}
|
|
8766
|
+
function optionalNumeric(value) {
|
|
8767
|
+
return typeof value === "number" && Number.isFinite(value) ? `${value} ms` : "\u2014";
|
|
8768
|
+
}
|
|
8769
|
+
function optionalAge(value) {
|
|
8770
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "unknown";
|
|
8771
|
+
return `${Math.max(0, Math.round(value / 1e3))}s`;
|
|
8772
|
+
}
|
|
8773
|
+
function optionalPercent(value) {
|
|
8774
|
+
return typeof value === "number" && Number.isFinite(value) ? `${Math.round(value * 100)}%` : "\u2014";
|
|
8775
|
+
}
|
|
8776
|
+
function optionalBytes(value) {
|
|
8777
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "\u2014";
|
|
8778
|
+
if (value >= 1024 * 1024 * 1024) {
|
|
8779
|
+
return `${(value / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
|
|
8780
|
+
}
|
|
8781
|
+
if (value >= 1024 * 1024) {
|
|
8782
|
+
return `${(value / (1024 * 1024)).toFixed(1)} MiB`;
|
|
8783
|
+
}
|
|
8784
|
+
if (value >= 1024) return `${(value / 1024).toFixed(1)} KiB`;
|
|
8785
|
+
return `${Math.round(value)} B`;
|
|
8786
|
+
}
|
|
8787
|
+
function optionalCount(value, unit) {
|
|
8788
|
+
return typeof value === "number" && Number.isFinite(value) ? `${value} ${unit}` : `\u2014 ${unit}`;
|
|
8789
|
+
}
|
|
8790
|
+
function optionalAgeSeconds(value) {
|
|
8791
|
+
return typeof value === "number" && Number.isFinite(value) ? `${Math.max(0, Math.round(value))}s` : "unknown";
|
|
8792
|
+
}
|
|
8793
|
+
|
|
8226
8794
|
// src/o11y-command.ts
|
|
8227
8795
|
async function o11yCommand(parsed, deps = {}) {
|
|
8228
8796
|
assertArgs(
|
|
8229
8797
|
parsed,
|
|
8230
|
-
[
|
|
8798
|
+
[
|
|
8799
|
+
"config",
|
|
8800
|
+
"context",
|
|
8801
|
+
"platform",
|
|
8802
|
+
"token",
|
|
8803
|
+
"email",
|
|
8804
|
+
"json",
|
|
8805
|
+
"app",
|
|
8806
|
+
"env",
|
|
8807
|
+
"minutes"
|
|
8808
|
+
],
|
|
8231
8809
|
2
|
|
8232
8810
|
);
|
|
8233
8811
|
const action = parsed.positionals[1];
|
|
@@ -8239,9 +8817,12 @@ async function o11yCommand(parsed, deps = {}) {
|
|
|
8239
8817
|
const minutes = statusMinutes(
|
|
8240
8818
|
numberOpt(parsed.options.minutes, "--minutes") ?? 60
|
|
8241
8819
|
);
|
|
8242
|
-
const
|
|
8243
|
-
|
|
8244
|
-
|
|
8820
|
+
const context = await resolveOperatorContext(parsed, {
|
|
8821
|
+
allowMissingConfig: true,
|
|
8822
|
+
defaultEnvironment: "prod",
|
|
8823
|
+
requireApp: true
|
|
8824
|
+
});
|
|
8825
|
+
const cfg = context.cfg;
|
|
8245
8826
|
const doFetch = deps.fetch ?? fetch;
|
|
8246
8827
|
const out = deps.stdout ?? console;
|
|
8247
8828
|
const token = await getDeveloperToken(
|
|
@@ -8255,13 +8836,30 @@ async function o11yCommand(parsed, deps = {}) {
|
|
|
8255
8836
|
doFetch,
|
|
8256
8837
|
out
|
|
8257
8838
|
);
|
|
8258
|
-
const appId =
|
|
8259
|
-
const env =
|
|
8839
|
+
const appId = context.app.value;
|
|
8840
|
+
const env = context.environment.value;
|
|
8260
8841
|
const base = `${cfg.platformUrl}/o11y/${encodeURIComponent(appId)}`;
|
|
8261
8842
|
const query = new URLSearchParams({ env, minutes: String(minutes) });
|
|
8262
8843
|
const headers = { authorization: `Bearer ${token}` };
|
|
8263
|
-
const
|
|
8844
|
+
const versionsQuery = new URLSearchParams({
|
|
8845
|
+
env,
|
|
8846
|
+
minutes: String(minutes),
|
|
8847
|
+
dimension: "version",
|
|
8848
|
+
metric: "requests"
|
|
8849
|
+
});
|
|
8850
|
+
const [
|
|
8851
|
+
application,
|
|
8852
|
+
applicationVersions,
|
|
8853
|
+
liveSync,
|
|
8854
|
+
canary,
|
|
8855
|
+
collector,
|
|
8856
|
+
provider,
|
|
8857
|
+
providerCapacity,
|
|
8858
|
+
providerReconciliation,
|
|
8859
|
+
providerHistory
|
|
8860
|
+
] = await Promise.all([
|
|
8264
8861
|
read2(`${base}/metrics/red?${query}`, headers, doFetch),
|
|
8862
|
+
read2(`${base}/explore?${versionsQuery}`, headers, doFetch),
|
|
8265
8863
|
read2(
|
|
8266
8864
|
`${base}/live-sync/health?${query}`,
|
|
8267
8865
|
headers,
|
|
@@ -8274,6 +8872,19 @@ async function o11yCommand(parsed, deps = {}) {
|
|
|
8274
8872
|
),
|
|
8275
8873
|
read2(`${base}/self-health?${query}`, headers, doFetch),
|
|
8276
8874
|
read2(`${base}/provider/cloudflare?${query}`, headers, doFetch),
|
|
8875
|
+
appId === "odla-db" ? read2(`${base}/provider/cloudflare/capacity`, headers, doFetch) : Promise.resolve({
|
|
8876
|
+
httpStatus: 204,
|
|
8877
|
+
body: {
|
|
8878
|
+
status: "not_applicable",
|
|
8879
|
+
scope: "cloudflare_account",
|
|
8880
|
+
reason: "account-scoped Durable Object capacity is exposed only through odla-db"
|
|
8881
|
+
}
|
|
8882
|
+
}),
|
|
8883
|
+
read2(
|
|
8884
|
+
`${base}/provider/cloudflare/reconciliation?${query}`,
|
|
8885
|
+
headers,
|
|
8886
|
+
doFetch
|
|
8887
|
+
),
|
|
8277
8888
|
read2(`${base}/provider/cloudflare/history?${query}`, headers, doFetch)
|
|
8278
8889
|
]);
|
|
8279
8890
|
const verdict = statusVerdict({
|
|
@@ -8282,25 +8893,30 @@ async function o11yCommand(parsed, deps = {}) {
|
|
|
8282
8893
|
canary,
|
|
8283
8894
|
collector,
|
|
8284
8895
|
provider,
|
|
8896
|
+
providerCapacity,
|
|
8897
|
+
providerReconciliation,
|
|
8285
8898
|
providerHistory
|
|
8286
8899
|
});
|
|
8287
8900
|
const status = {
|
|
8288
|
-
schemaVersion:
|
|
8901
|
+
schemaVersion: 9,
|
|
8289
8902
|
scope: { appId, env, minutes },
|
|
8290
8903
|
observedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8291
8904
|
verdict,
|
|
8292
8905
|
application,
|
|
8906
|
+
applicationVersions,
|
|
8293
8907
|
liveSync,
|
|
8294
8908
|
canary,
|
|
8295
8909
|
collector,
|
|
8296
8910
|
provider,
|
|
8911
|
+
providerCapacity,
|
|
8912
|
+
providerReconciliation,
|
|
8297
8913
|
providerHistory
|
|
8298
8914
|
};
|
|
8299
8915
|
if (parsed.options.json === true) {
|
|
8300
8916
|
out.log(JSON.stringify(status, null, 2));
|
|
8301
8917
|
return;
|
|
8302
8918
|
}
|
|
8303
|
-
|
|
8919
|
+
printO11yStatus(status, out);
|
|
8304
8920
|
}
|
|
8305
8921
|
function statusMinutes(value) {
|
|
8306
8922
|
if (!Number.isSafeInteger(value) || value < 1 || value > 7 * 24 * 60) {
|
|
@@ -8322,70 +8938,11 @@ async function read2(url, headers, doFetch) {
|
|
|
8322
8938
|
}
|
|
8323
8939
|
return { httpStatus: response2.status, body };
|
|
8324
8940
|
}
|
|
8325
|
-
function printStatus2(status, out) {
|
|
8326
|
-
out.log(
|
|
8327
|
-
`o11y status ${status.scope.appId}/${status.scope.env} (${status.scope.minutes}m)`
|
|
8328
|
-
);
|
|
8329
|
-
const routes = Array.isArray(status.application.body.routes) ? status.application.body.routes.filter(record6) : [];
|
|
8330
|
-
const requests = routes.reduce(
|
|
8331
|
-
(total, row) => total + numeric3(row.requests),
|
|
8332
|
-
0
|
|
8333
|
-
);
|
|
8334
|
-
const errors = routes.reduce(
|
|
8335
|
-
(total, row) => total + numeric3(row.errors),
|
|
8336
|
-
0
|
|
8337
|
-
);
|
|
8338
|
-
out.log(
|
|
8339
|
-
`application ${status.application.httpStatus} ${requests} requests ${errors} errors`
|
|
8340
|
-
);
|
|
8341
|
-
out.log(
|
|
8342
|
-
liveSyncLine(status.liveSync)
|
|
8343
|
-
);
|
|
8344
|
-
const canaryDurations = record6(status.canary.body.durationsMs) ? status.canary.body.durationsMs : {};
|
|
8345
|
-
out.log(
|
|
8346
|
-
`canary ${status.canary.httpStatus} ${String(status.canary.body.status ?? status.canary.body.error ?? "unavailable")} ${optionalNumeric(canaryDurations.publishToVisibleMs)} publish-to-visible`
|
|
8347
|
-
);
|
|
8348
|
-
const collectorIngest = record6(status.collector.body.ingest) ? status.collector.body.ingest : {};
|
|
8349
|
-
const collectorStorage = record6(collectorIngest.storage) ? collectorIngest.storage : {};
|
|
8350
|
-
out.log(
|
|
8351
|
-
`collector ${status.collector.httpStatus} ${String(status.collector.body.status ?? status.collector.body.error ?? "unavailable")} ${numeric3(collectorStorage.affectedPoints)} affected points`
|
|
8352
|
-
);
|
|
8353
|
-
const providerMetrics = record6(status.provider.body.metrics) ? status.provider.body.metrics : {};
|
|
8354
|
-
out.log(
|
|
8355
|
-
`cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric3(providerMetrics.requests)} invocations ${numeric3(providerMetrics.errors)} runtime errors`
|
|
8356
|
-
);
|
|
8357
|
-
const providerPoints = Array.isArray(status.providerHistory.body.points) ? status.providerHistory.body.points.length : 0;
|
|
8358
|
-
const providerFreshness = record6(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
|
|
8359
|
-
out.log(
|
|
8360
|
-
`cloudflare-history ${status.providerHistory.httpStatus} ${String(status.providerHistory.body.status ?? status.providerHistory.body.error ?? "unavailable")} ${providerPoints} snapshots ${optionalAge(providerFreshness.ageMs)} old`
|
|
8361
|
-
);
|
|
8362
|
-
out.log(
|
|
8363
|
-
`verdict ${status.verdict.status} ${status.verdict.reasons.map((reason) => `${reason.source}:${reason.code}`).join(", ") || "all required signals healthy"}`
|
|
8364
|
-
);
|
|
8365
|
-
}
|
|
8366
|
-
function liveSyncLine(read3) {
|
|
8367
|
-
const performance = record6(read3.body.performance) ? read3.body.performance : {};
|
|
8368
|
-
const commitToSend = record6(performance.commitToSend) ? performance.commitToSend : {};
|
|
8369
|
-
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`;
|
|
8370
|
-
}
|
|
8371
|
-
function record6(value) {
|
|
8372
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
8373
|
-
}
|
|
8374
|
-
function numeric3(value) {
|
|
8375
|
-
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
8376
|
-
}
|
|
8377
|
-
function optionalNumeric(value) {
|
|
8378
|
-
return typeof value === "number" && Number.isFinite(value) ? `${value} ms` : "\u2014";
|
|
8379
|
-
}
|
|
8380
|
-
function optionalAge(value) {
|
|
8381
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return "unknown";
|
|
8382
|
-
return `${Math.max(0, Math.round(value / 1e3))}s`;
|
|
8383
|
-
}
|
|
8384
8941
|
|
|
8385
8942
|
// src/provision.ts
|
|
8386
8943
|
var import_apps5 = require("@odla-ai/apps");
|
|
8387
8944
|
var import_ai3 = require("@odla-ai/ai");
|
|
8388
|
-
var
|
|
8945
|
+
var import_node_process10 = __toESM(require("process"), 1);
|
|
8389
8946
|
|
|
8390
8947
|
// src/integration-provision.ts
|
|
8391
8948
|
var import_db3 = require("@odla-ai/db");
|
|
@@ -8744,7 +9301,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
8744
9301
|
await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
|
|
8745
9302
|
}
|
|
8746
9303
|
if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
8747
|
-
const key =
|
|
9304
|
+
const key = import_node_process10.default.env[cfg.ai.keyEnv];
|
|
8748
9305
|
if (key) {
|
|
8749
9306
|
const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
|
|
8750
9307
|
await (0, import_ai3.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
|
|
@@ -8785,8 +9342,8 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
8785
9342
|
}
|
|
8786
9343
|
|
|
8787
9344
|
// src/record.ts
|
|
8788
|
-
var
|
|
8789
|
-
var
|
|
9345
|
+
var import_node_fs15 = require("fs");
|
|
9346
|
+
var import_node_process11 = __toESM(require("process"), 1);
|
|
8790
9347
|
|
|
8791
9348
|
// src/surface.ts
|
|
8792
9349
|
var PM_ACTIONS = {
|
|
@@ -8834,6 +9391,7 @@ var COMMAND_SURFACE = {
|
|
|
8834
9391
|
calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
|
|
8835
9392
|
capabilities: {},
|
|
8836
9393
|
code: { connect: {} },
|
|
9394
|
+
context: { show: {}, list: {}, save: {}, remove: {} },
|
|
8837
9395
|
// `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
|
|
8838
9396
|
discuss: {
|
|
8839
9397
|
groups: {},
|
|
@@ -8928,7 +9486,7 @@ function invocationPath(words2) {
|
|
|
8928
9486
|
|
|
8929
9487
|
// src/record.ts
|
|
8930
9488
|
function recordInvocation(parsed) {
|
|
8931
|
-
const file =
|
|
9489
|
+
const file = import_node_process11.default.env.ODLA_CLI_RECORD;
|
|
8932
9490
|
if (!file) return;
|
|
8933
9491
|
try {
|
|
8934
9492
|
const entry = {
|
|
@@ -8936,19 +9494,14 @@ function recordInvocation(parsed) {
|
|
|
8936
9494
|
options: Object.keys(parsed.options).sort()
|
|
8937
9495
|
};
|
|
8938
9496
|
if (!entry.path.length) return;
|
|
8939
|
-
(0,
|
|
9497
|
+
(0, import_node_fs15.appendFileSync)(file, `${JSON.stringify(entry)}
|
|
8940
9498
|
`);
|
|
8941
9499
|
} catch {
|
|
8942
9500
|
}
|
|
8943
9501
|
}
|
|
8944
9502
|
|
|
8945
|
-
// src/runbook-command.ts
|
|
8946
|
-
var import_node_fs18 = require("fs");
|
|
8947
|
-
var import_node_path14 = require("path");
|
|
8948
|
-
var import_node_process10 = __toESM(require("process"), 1);
|
|
8949
|
-
|
|
8950
9503
|
// src/runbook-actions.ts
|
|
8951
|
-
var
|
|
9504
|
+
var import_node_fs16 = require("fs");
|
|
8952
9505
|
|
|
8953
9506
|
// src/runbook-requires.ts
|
|
8954
9507
|
var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
|
|
@@ -9033,7 +9586,7 @@ async function bySlug(ctx, slug) {
|
|
|
9033
9586
|
function readBody(file, inline) {
|
|
9034
9587
|
if (inline !== void 0) return inline;
|
|
9035
9588
|
if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
|
|
9036
|
-
return (0,
|
|
9589
|
+
return (0, import_node_fs16.readFileSync)(file === "-" ? 0 : file, "utf8");
|
|
9037
9590
|
}
|
|
9038
9591
|
var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
|
|
9039
9592
|
async function runbookList(ctx, all, query) {
|
|
@@ -9120,8 +9673,8 @@ async function runbookRemove(ctx, slug) {
|
|
|
9120
9673
|
}
|
|
9121
9674
|
|
|
9122
9675
|
// src/runbook-import.ts
|
|
9123
|
-
var
|
|
9124
|
-
var
|
|
9676
|
+
var import_node_fs17 = require("fs");
|
|
9677
|
+
var import_node_path13 = require("path");
|
|
9125
9678
|
function parseRunbook(text, slug) {
|
|
9126
9679
|
let rest = text;
|
|
9127
9680
|
const meta = {};
|
|
@@ -9146,12 +9699,12 @@ function parseRunbook(text, slug) {
|
|
|
9146
9699
|
};
|
|
9147
9700
|
}
|
|
9148
9701
|
function readRunbookDir(dir) {
|
|
9149
|
-
if (!(0,
|
|
9150
|
-
const files = (0,
|
|
9702
|
+
if (!(0, import_node_fs17.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
|
|
9703
|
+
const files = (0, import_node_fs17.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
|
|
9151
9704
|
if (!files.length) throw new Error(`no .md files in ${dir}`);
|
|
9152
9705
|
return files.map((file) => {
|
|
9153
|
-
const slug = (0,
|
|
9154
|
-
const parsed = parseRunbook((0,
|
|
9706
|
+
const slug = (0, import_node_path13.basename)(file, ".md");
|
|
9707
|
+
const parsed = parseRunbook((0, import_node_fs17.readFileSync)((0, import_node_path13.join)(dir, file), "utf8"), slug);
|
|
9155
9708
|
return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
|
|
9156
9709
|
});
|
|
9157
9710
|
}
|
|
@@ -9223,8 +9776,8 @@ async function upsert(ctx, r, visibility) {
|
|
|
9223
9776
|
|
|
9224
9777
|
// src/runbook-impact.ts
|
|
9225
9778
|
var import_node_child_process7 = require("child_process");
|
|
9226
|
-
var
|
|
9227
|
-
var
|
|
9779
|
+
var import_node_fs18 = require("fs");
|
|
9780
|
+
var import_node_path14 = require("path");
|
|
9228
9781
|
|
|
9229
9782
|
// src/runbook-impact-scan.ts
|
|
9230
9783
|
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$]*)/;
|
|
@@ -9393,10 +9946,10 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
|
|
|
9393
9946
|
}
|
|
9394
9947
|
function manifestLabeller(root) {
|
|
9395
9948
|
return (workspace) => {
|
|
9396
|
-
const manifest = (0,
|
|
9397
|
-
if (!(0,
|
|
9949
|
+
const manifest = (0, import_node_path14.join)(root, workspace, "package.json");
|
|
9950
|
+
if (!(0, import_node_fs18.existsSync)(manifest)) return void 0;
|
|
9398
9951
|
try {
|
|
9399
|
-
const name = JSON.parse((0,
|
|
9952
|
+
const name = JSON.parse((0, import_node_fs18.readFileSync)(manifest, "utf8")).name;
|
|
9400
9953
|
return typeof name === "string" ? name : void 0;
|
|
9401
9954
|
} catch {
|
|
9402
9955
|
return void 0;
|
|
@@ -9463,7 +10016,7 @@ function report3(ctx, impacts) {
|
|
|
9463
10016
|
async function runbookImpact(ctx, options, deps = {}) {
|
|
9464
10017
|
const cwd = deps.cwd ?? process.cwd();
|
|
9465
10018
|
const runGit = deps.runGit ?? gitRunner(cwd);
|
|
9466
|
-
const read3 = deps.readRepoFile ?? ((path) => (0,
|
|
10019
|
+
const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs18.readFileSync)((0, import_node_path14.join)(cwd, path), "utf8"));
|
|
9467
10020
|
const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
|
|
9468
10021
|
if (!surfaces.length) {
|
|
9469
10022
|
return ctx.out.log(
|
|
@@ -9596,12 +10149,12 @@ async function runbookComment(ctx, slug, body) {
|
|
|
9596
10149
|
|
|
9597
10150
|
// src/runbook-editor.ts
|
|
9598
10151
|
var import_node_child_process8 = require("child_process");
|
|
9599
|
-
var
|
|
9600
|
-
var
|
|
9601
|
-
var
|
|
9602
|
-
var
|
|
10152
|
+
var import_node_fs19 = require("fs");
|
|
10153
|
+
var import_node_os5 = require("os");
|
|
10154
|
+
var import_node_path15 = require("path");
|
|
10155
|
+
var import_node_process12 = __toESM(require("process"), 1);
|
|
9603
10156
|
var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
|
|
9604
|
-
function resolveEditor(env =
|
|
10157
|
+
function resolveEditor(env = import_node_process12.default.env) {
|
|
9605
10158
|
for (const name of EDITOR_ENV) {
|
|
9606
10159
|
const value = env[name];
|
|
9607
10160
|
if (value && value.trim()) return value.trim();
|
|
@@ -9615,8 +10168,8 @@ function defaultRun(command, path) {
|
|
|
9615
10168
|
return result.status ?? 0;
|
|
9616
10169
|
}
|
|
9617
10170
|
function editText(initial, slug, deps = {}) {
|
|
9618
|
-
const env = deps.env ??
|
|
9619
|
-
const interactive = deps.interactive ?? (() => Boolean(
|
|
10171
|
+
const env = deps.env ?? import_node_process12.default.env;
|
|
10172
|
+
const interactive = deps.interactive ?? (() => Boolean(import_node_process12.default.stdin.isTTY));
|
|
9620
10173
|
const editor = resolveEditor(env);
|
|
9621
10174
|
if (!editor)
|
|
9622
10175
|
throw new Error(
|
|
@@ -9624,16 +10177,16 @@ function editText(initial, slug, deps = {}) {
|
|
|
9624
10177
|
);
|
|
9625
10178
|
if (!interactive())
|
|
9626
10179
|
throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
|
|
9627
|
-
const dir = (0,
|
|
9628
|
-
const file = (0,
|
|
10180
|
+
const dir = (0, import_node_fs19.mkdtempSync)((0, import_node_path15.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
|
|
10181
|
+
const file = (0, import_node_path15.join)(dir, `${slug}.md`);
|
|
9629
10182
|
try {
|
|
9630
|
-
(0,
|
|
10183
|
+
(0, import_node_fs19.writeFileSync)(file, initial, { mode: 384 });
|
|
9631
10184
|
const code = defaultRunOrInjected(deps)(editor, file);
|
|
9632
10185
|
if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
|
|
9633
|
-
const edited = (0,
|
|
10186
|
+
const edited = (0, import_node_fs19.readFileSync)(file, "utf8");
|
|
9634
10187
|
return edited === initial ? null : edited;
|
|
9635
10188
|
} finally {
|
|
9636
|
-
(0,
|
|
10189
|
+
(0, import_node_fs19.rmSync)(dir, { recursive: true, force: true });
|
|
9637
10190
|
}
|
|
9638
10191
|
}
|
|
9639
10192
|
var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
|
|
@@ -9671,10 +10224,16 @@ function credentialKind(identity) {
|
|
|
9671
10224
|
return identity.scopes.length ? "device token (scoped)" : "device token or session";
|
|
9672
10225
|
}
|
|
9673
10226
|
async function whoamiCommand(parsed, deps = {}) {
|
|
9674
|
-
assertArgs(
|
|
10227
|
+
assertArgs(
|
|
10228
|
+
parsed,
|
|
10229
|
+
["config", "context", "platform", "token", "email", "json"],
|
|
10230
|
+
1
|
|
10231
|
+
);
|
|
9675
10232
|
const out = deps.stdout ?? console;
|
|
9676
10233
|
const doFetch = deps.fetch ?? fetch;
|
|
9677
|
-
const cfg = await
|
|
10234
|
+
const { cfg } = await resolveOperatorContext(parsed, {
|
|
10235
|
+
allowMissingConfig: true
|
|
10236
|
+
});
|
|
9678
10237
|
const token = await getDeveloperToken(
|
|
9679
10238
|
cfg,
|
|
9680
10239
|
{
|
|
@@ -9723,34 +10282,24 @@ var ALLOWED3 = [
|
|
|
9723
10282
|
"dry-run",
|
|
9724
10283
|
"limit",
|
|
9725
10284
|
"base",
|
|
9726
|
-
"requires"
|
|
10285
|
+
"requires",
|
|
10286
|
+
"platform",
|
|
10287
|
+
"context"
|
|
9727
10288
|
];
|
|
9728
10289
|
function requireSlug(slug, action) {
|
|
9729
10290
|
if (!slug) throw new Error(`"runbook ${action}" needs a slug, e.g. "odla-ai runbook ${action} release"`);
|
|
9730
10291
|
return slug;
|
|
9731
10292
|
}
|
|
9732
10293
|
var WRITES = /* @__PURE__ */ new Set(["new", "edit", "publish", "archive", "visibility", "revert", "rm", "import"]);
|
|
9733
|
-
var CONFIG_FREE = /* @__PURE__ */ new Set(["list", "search", "ask", "get", "cat", "history", "impact", "lint"]);
|
|
9734
|
-
async function loadOrDefaultConfig(configPath, action, appId) {
|
|
9735
|
-
const projectScoped = appId !== void 0;
|
|
9736
|
-
if (!projectScoped && CONFIG_FREE.has(action) && !(0, import_node_fs18.existsSync)((0, import_node_path14.resolve)(configPath))) {
|
|
9737
|
-
const rootDir = import_node_process10.default.cwd();
|
|
9738
|
-
return {
|
|
9739
|
-
configPath,
|
|
9740
|
-
platformUrl: import_node_process10.default.env.ODLA_PLATFORM_URL ?? "https://odla.ai",
|
|
9741
|
-
rootDir,
|
|
9742
|
-
app: { id: "", name: "" },
|
|
9743
|
-
local: { tokenFile: (0, import_node_path14.join)(rootDir, ".odla/dev-token.json") }
|
|
9744
|
-
};
|
|
9745
|
-
}
|
|
9746
|
-
return loadProjectConfig(configPath);
|
|
9747
|
-
}
|
|
9748
10294
|
async function buildContext3(parsed, deps, action) {
|
|
9749
|
-
const
|
|
9750
|
-
const
|
|
10295
|
+
const appIdOption = stringOpt(parsed.options.app);
|
|
10296
|
+
const context = await resolveOperatorContext(parsed, {
|
|
10297
|
+
allowMissingConfig: true
|
|
10298
|
+
});
|
|
10299
|
+
const { cfg } = context;
|
|
9751
10300
|
const doFetch = deps.fetch ?? fetch;
|
|
9752
10301
|
const out = deps.stdout ?? console;
|
|
9753
|
-
const appId =
|
|
10302
|
+
const appId = appIdOption ?? (context.app.source === "environment" || context.app.source === "profile" ? context.app.value : null) ?? PLATFORM_SCOPE;
|
|
9754
10303
|
const dryRun = parsed.options["dry-run"] === true;
|
|
9755
10304
|
if (action === "import" && dryRun) {
|
|
9756
10305
|
return {
|
|
@@ -9771,8 +10320,10 @@ async function buildContext3(parsed, deps, action) {
|
|
|
9771
10320
|
fetch: doFetch,
|
|
9772
10321
|
stdout: out,
|
|
9773
10322
|
openApprovalUrl: deps.openUrl,
|
|
9774
|
-
//
|
|
9775
|
-
|
|
10323
|
+
// A project, named context, or the global operator context owns the
|
|
10324
|
+
// exact-scope cache; it never follows an arbitrary shell directory.
|
|
10325
|
+
rootDir: cfg.rootDir,
|
|
10326
|
+
tokenFile: context.credentials.scopedTokenFile
|
|
9776
10327
|
}) : await getDeveloperToken(
|
|
9777
10328
|
cfg,
|
|
9778
10329
|
{
|
|
@@ -10047,7 +10598,7 @@ function hostedSeverity(value, flag) {
|
|
|
10047
10598
|
var import_security2 = require("@odla-ai/security");
|
|
10048
10599
|
|
|
10049
10600
|
// src/security.ts
|
|
10050
|
-
var
|
|
10601
|
+
var import_node_path16 = require("path");
|
|
10051
10602
|
var import_security = require("@odla-ai/security");
|
|
10052
10603
|
var import_node3 = require("@odla-ai/security/node");
|
|
10053
10604
|
async function runHostedSecurity(options) {
|
|
@@ -10059,9 +10610,9 @@ async function runHostedSecurity(options) {
|
|
|
10059
10610
|
const appId = selfAudit ? "odla-ai" : cfg.app.id;
|
|
10060
10611
|
const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
|
|
10061
10612
|
const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
|
|
10062
|
-
const target = (0,
|
|
10063
|
-
const output = (0,
|
|
10064
|
-
const outputRelative = (0,
|
|
10613
|
+
const target = (0, import_node_path16.resolve)(options.target ?? cfg?.rootDir ?? ".");
|
|
10614
|
+
const output = (0, import_node_path16.resolve)(options.out ?? (0, import_node_path16.resolve)(target, ".odla/security/hosted"));
|
|
10615
|
+
const outputRelative = (0, import_node_path16.relative)(target, output).split(import_node_path16.sep).join("/");
|
|
10065
10616
|
if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
|
|
10066
10617
|
const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
|
|
10067
10618
|
const tokenRequest = {
|
|
@@ -10073,7 +10624,7 @@ async function runHostedSecurity(options) {
|
|
|
10073
10624
|
};
|
|
10074
10625
|
const token = await injectedToken(options, tokenRequest);
|
|
10075
10626
|
const snapshot = await (0, import_node3.snapshotDirectory)(target, {
|
|
10076
|
-
exclude: !outputRelative.startsWith("../") && !(0,
|
|
10627
|
+
exclude: !outputRelative.startsWith("../") && !(0, import_node_path16.isAbsolute)(outputRelative) ? [outputRelative] : []
|
|
10077
10628
|
});
|
|
10078
10629
|
const hosted = await (0, import_security.createPlatformSecurityReasoners)({
|
|
10079
10630
|
platform,
|
|
@@ -10091,7 +10642,7 @@ async function runHostedSecurity(options) {
|
|
|
10091
10642
|
});
|
|
10092
10643
|
const harness = (0, import_security.createSecurityHarness)({
|
|
10093
10644
|
profile,
|
|
10094
|
-
store: new import_node3.FileRunStore((0,
|
|
10645
|
+
store: new import_node3.FileRunStore((0, import_node_path16.resolve)(output, "state")),
|
|
10095
10646
|
discoveryReasoner: hosted.discoveryReasoner,
|
|
10096
10647
|
validationReasoner: hosted.validationReasoner,
|
|
10097
10648
|
policy: {
|
|
@@ -10115,7 +10666,7 @@ async function runHostedSecurity(options) {
|
|
|
10115
10666
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
10116
10667
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
10117
10668
|
if (!env || !declared.includes(env)) {
|
|
10118
|
-
const shown = (0,
|
|
10669
|
+
const shown = (0, import_node_path16.relative)(rootDir, configPath) || configPath;
|
|
10119
10670
|
throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
|
|
10120
10671
|
}
|
|
10121
10672
|
return env;
|
|
@@ -10144,7 +10695,7 @@ function printSummary(out, appId, env, run, report4, output) {
|
|
|
10144
10695
|
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}`);
|
|
10145
10696
|
if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
|
|
10146
10697
|
out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
|
|
10147
|
-
out.log(` report: ${(0,
|
|
10698
|
+
out.log(` report: ${(0, import_node_path16.resolve)(output, "REPORT.md")}`);
|
|
10148
10699
|
}
|
|
10149
10700
|
function formatBudget(usage) {
|
|
10150
10701
|
return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
|
|
@@ -10613,6 +11164,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
10613
11164
|
await whoamiCommand(parsed, runtime);
|
|
10614
11165
|
return;
|
|
10615
11166
|
}
|
|
11167
|
+
if (command === "context") {
|
|
11168
|
+
await contextCommand(parsed, runtime);
|
|
11169
|
+
return;
|
|
11170
|
+
}
|
|
10616
11171
|
if (command === "runbook") {
|
|
10617
11172
|
await runbookCommand(parsed, runtime);
|
|
10618
11173
|
return;
|