@odla-ai/cli 0.38.3 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.cjs +675 -292
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-WJAN5CZ2.js → chunk-PJ3RATDD.js} +551 -215
- package/dist/chunk-PJ3RATDD.js.map +1 -0
- package/dist/{cli-A5R2DMM4.js → cli-ALS5OZPB.js} +2 -2
- package/dist/index.cjs +585 -254
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/skills/odla/SKILL.md +50 -11
- package/skills/odla/references/agent-identity.md +94 -111
- package/dist/chunk-WJAN5CZ2.js.map +0 -1
- /package/dist/{cli-A5R2DMM4.js.map → cli-ALS5OZPB.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -97,12 +97,12 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
|
|
|
97
97
|
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
98
98
|
|
|
99
99
|
// src/admin-ai.ts
|
|
100
|
-
var
|
|
100
|
+
var import_node_process12 = __toESM(require("process"), 1);
|
|
101
101
|
|
|
102
102
|
// src/token.ts
|
|
103
103
|
var import_db = require("@odla-ai/db");
|
|
104
104
|
var import_node_crypto = require("crypto");
|
|
105
|
-
var
|
|
105
|
+
var import_node_process9 = __toESM(require("process"), 1);
|
|
106
106
|
|
|
107
107
|
// src/handshake-approval.ts
|
|
108
108
|
var import_node_process2 = __toESM(require("process"), 1);
|
|
@@ -237,30 +237,13 @@ function handshakeWaitMs(waitSeconds, interactive = import_node_process3.default
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
// src/cached-credential.ts
|
|
240
|
-
var
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
function isCredentialRejection(error) {
|
|
246
|
-
const message2 = error instanceof Error ? error.message : String(error ?? "");
|
|
247
|
-
return /\((401|403)\)\s*$/.test(message2.trim());
|
|
248
|
-
}
|
|
249
|
-
function explainRejectedCredential(error) {
|
|
250
|
-
const tokenFile = noted;
|
|
251
|
-
if (!tokenFile || !isCredentialRejection(error)) return null;
|
|
252
|
-
noted = null;
|
|
253
|
-
(0, import_node_fs2.rmSync)(tokenFile, { force: true });
|
|
254
|
-
return [
|
|
255
|
-
"auth: the cached credential was rejected by odla, so it was revoked before its cached expiry.",
|
|
256
|
-
" The usual cause is a newer sign-in for this project: collecting a handshake retires the",
|
|
257
|
-
" principal's other credentials, so a second terminal or worktree supersedes this one.",
|
|
258
|
-
` Discarded ${tokenFile}; re-run this command to request a fresh approval.`
|
|
259
|
-
].join("\n");
|
|
260
|
-
}
|
|
240
|
+
var import_node_fs5 = require("fs");
|
|
241
|
+
|
|
242
|
+
// src/auth-guidance.ts
|
|
243
|
+
var import_node_process6 = __toESM(require("process"), 1);
|
|
261
244
|
|
|
262
245
|
// src/device-session.ts
|
|
263
|
-
var
|
|
246
|
+
var import_node_fs2 = require("fs");
|
|
264
247
|
var import_node_os = require("os");
|
|
265
248
|
var import_node_path2 = require("path");
|
|
266
249
|
var import_node_process4 = __toESM(require("process"), 1);
|
|
@@ -269,9 +252,9 @@ function deviceCredentialPath(env = import_node_process4.default.env) {
|
|
|
269
252
|
}
|
|
270
253
|
function readDeviceCredential(platform, env = import_node_process4.default.env) {
|
|
271
254
|
const path = deviceCredentialPath(env);
|
|
272
|
-
if (!(0,
|
|
255
|
+
if (!(0, import_node_fs2.existsSync)(path)) return null;
|
|
273
256
|
try {
|
|
274
|
-
const parsed = JSON.parse((0,
|
|
257
|
+
const parsed = JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
|
|
275
258
|
if (typeof parsed.token !== "string" || !parsed.token.startsWith("odla_device_")) return null;
|
|
276
259
|
if (parsed.platform !== platform) return null;
|
|
277
260
|
return { ...parsed, token: parsed.token, platform: parsed.platform };
|
|
@@ -293,12 +276,71 @@ async function mintDeviceSession(platformUrl, credential2, doFetch) {
|
|
|
293
276
|
`device session failed: ${detail} (${response2.status})` + (revocable ? " \u2014 if this machine's enrollment was revoked or has expired, enroll it again in Studio" : "")
|
|
294
277
|
);
|
|
295
278
|
}
|
|
296
|
-
return {
|
|
279
|
+
return {
|
|
280
|
+
token: body.token,
|
|
281
|
+
expiresAt: body.expiresAt ?? Date.now(),
|
|
282
|
+
// Absent from a registry that predates rolling expiry and scoped devices.
|
|
283
|
+
// Left undefined rather than defaulted, so a caller can tell "the platform
|
|
284
|
+
// did not say" from "the platform said none".
|
|
285
|
+
...typeof body.deviceExpiresAt === "number" ? { deviceExpiresAt: body.deviceExpiresAt } : {},
|
|
286
|
+
...Array.isArray(body.appIds) ? { appIds: body.appIds } : {},
|
|
287
|
+
...Array.isArray(body.capabilities) ? { capabilities: body.capabilities } : {},
|
|
288
|
+
...Array.isArray(body.scopes) ? { scopes: body.scopes } : {}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/odla-home.ts
|
|
293
|
+
var import_node_fs3 = require("fs");
|
|
294
|
+
var import_node_os2 = require("os");
|
|
295
|
+
var import_node_path3 = require("path");
|
|
296
|
+
var import_node_process5 = __toESM(require("process"), 1);
|
|
297
|
+
function odlaHome(env = import_node_process5.default.env) {
|
|
298
|
+
return env.ODLA_HOME ?? (0, import_node_path3.join)(env.HOME ?? (0, import_node_os2.homedir)(), ".odla");
|
|
299
|
+
}
|
|
300
|
+
function odlaHomePath(segments, env = import_node_process5.default.env) {
|
|
301
|
+
return (0, import_node_path3.join)(odlaHome(env), ...segments);
|
|
302
|
+
}
|
|
303
|
+
function identityFile(env) {
|
|
304
|
+
return odlaHomePath(["identity.json"], env);
|
|
305
|
+
}
|
|
306
|
+
function deviceSessionFile(env) {
|
|
307
|
+
return odlaHomePath(["session.json"], env);
|
|
308
|
+
}
|
|
309
|
+
function appTokenFile(appId, env) {
|
|
310
|
+
return odlaHomePath(["apps", safeSegment(appId), "dev-token.json"], env);
|
|
311
|
+
}
|
|
312
|
+
function appCredentialsFile(appId, env) {
|
|
313
|
+
return odlaHomePath(["apps", safeSegment(appId), "credentials.json"], env);
|
|
314
|
+
}
|
|
315
|
+
function scopedTokenFile(env) {
|
|
316
|
+
return odlaHomePath(["admin-token.local.json"], env);
|
|
317
|
+
}
|
|
318
|
+
function pmContextFile(env) {
|
|
319
|
+
return odlaHomePath(["pm-context.json"], env);
|
|
320
|
+
}
|
|
321
|
+
function adoptRepoLocalCache(legacyPath, machinePath, out) {
|
|
322
|
+
if (!(0, import_node_fs3.existsSync)(legacyPath) || legacyPath === machinePath) return false;
|
|
323
|
+
const superseded = (0, import_node_fs3.existsSync)(machinePath);
|
|
324
|
+
if (!superseded) {
|
|
325
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(machinePath), { recursive: true });
|
|
326
|
+
(0, import_node_fs3.copyFileSync)(legacyPath, machinePath);
|
|
327
|
+
(0, import_node_fs3.chmodSync)(machinePath, 384);
|
|
328
|
+
}
|
|
329
|
+
(0, import_node_fs3.rmSync)(legacyPath, { force: true });
|
|
330
|
+
out?.error(
|
|
331
|
+
superseded ? `auth: removed superseded ${legacyPath}; this machine's credentials live in ${odlaHome()}` : `auth: moved ${legacyPath} into ${machinePath}; credentials are per machine now, not per worktree`
|
|
332
|
+
);
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
function safeSegment(value2) {
|
|
336
|
+
const clean4 = value2.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "");
|
|
337
|
+
if (!clean4) throw new Error(`"${value2}" is not a usable app id`);
|
|
338
|
+
return clean4;
|
|
297
339
|
}
|
|
298
340
|
|
|
299
341
|
// src/local.ts
|
|
300
342
|
var import_node_fs4 = require("fs");
|
|
301
|
-
var
|
|
343
|
+
var import_node_path4 = require("path");
|
|
302
344
|
var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
|
|
303
345
|
function readJsonFile(path) {
|
|
304
346
|
try {
|
|
@@ -345,7 +387,7 @@ function mergeCredential(current, update) {
|
|
|
345
387
|
return next;
|
|
346
388
|
}
|
|
347
389
|
function ensureGitignore(rootDir, localPaths = []) {
|
|
348
|
-
const path = (0,
|
|
390
|
+
const path = (0, import_node_path4.resolve)(rootDir, ".gitignore");
|
|
349
391
|
const existing = (0, import_node_fs4.existsSync)(path) ? (0, import_node_fs4.readFileSync)(path, "utf8") : "";
|
|
350
392
|
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
|
|
351
393
|
const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
|
|
@@ -366,7 +408,7 @@ function o11yDevVars(cfg) {
|
|
|
366
408
|
function resolveWriteDevVarsTarget(cfg, requested) {
|
|
367
409
|
if (!requested) return null;
|
|
368
410
|
if (requested === true) return cfg.local.devVarsFile;
|
|
369
|
-
return (0,
|
|
411
|
+
return (0, import_node_path4.resolve)((0, import_node_path4.dirname)(cfg.configPath), requested);
|
|
370
412
|
}
|
|
371
413
|
function writeDevVars(path, credentials, env, o11y) {
|
|
372
414
|
const entry = credentials.envs[env];
|
|
@@ -406,22 +448,110 @@ function isManagedDevVar(line2) {
|
|
|
406
448
|
return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
|
|
407
449
|
}
|
|
408
450
|
function writePrivateText(path, text3) {
|
|
409
|
-
(0, import_node_fs4.mkdirSync)((0,
|
|
451
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
410
452
|
const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
411
453
|
(0, import_node_fs4.writeFileSync)(temporary, text3, { mode: 384 });
|
|
412
454
|
(0, import_node_fs4.chmodSync)(temporary, 384);
|
|
413
455
|
(0, import_node_fs4.renameSync)(temporary, path);
|
|
414
456
|
}
|
|
415
457
|
function gitignoreEntry(rootDir, path) {
|
|
416
|
-
const rel = (0,
|
|
417
|
-
if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0,
|
|
458
|
+
const rel = (0, import_node_path4.relative)((0, import_node_path4.resolve)(rootDir), (0, import_node_path4.resolve)(path));
|
|
459
|
+
if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path4.isAbsolute)(rel)) return null;
|
|
418
460
|
return rel.replaceAll("\\", "/");
|
|
419
461
|
}
|
|
420
462
|
function displayPath(path, rootDir = process.cwd()) {
|
|
421
|
-
const rel = (0,
|
|
463
|
+
const rel = (0, import_node_path4.relative)(rootDir, path);
|
|
422
464
|
return rel && !rel.startsWith("..") ? rel : path;
|
|
423
465
|
}
|
|
424
466
|
|
|
467
|
+
// src/auth-guidance.ts
|
|
468
|
+
var ENROL_EVERYTHING = "npx odla-ai device enroll --all-apps --capability all --no-open --wait 600";
|
|
469
|
+
var ENROL_PLATFORM_WIDE = "npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600";
|
|
470
|
+
function machineAuthState(audience, env = import_node_process6.default.env) {
|
|
471
|
+
const device = readDeviceCredential(audience, env);
|
|
472
|
+
if (!device) return { enrolled: false };
|
|
473
|
+
const session = readJsonFile(deviceSessionFile(env));
|
|
474
|
+
const current = session?.platform === audience && session.deviceId === device.deviceId ? session : void 0;
|
|
475
|
+
return {
|
|
476
|
+
enrolled: true,
|
|
477
|
+
...device.name ? { deviceName: device.name } : {},
|
|
478
|
+
...current?.appIds ? { appIds: current.appIds } : {},
|
|
479
|
+
...current?.capabilities ? { capabilities: current.capabilities } : {},
|
|
480
|
+
...current?.scopes ? { scopes: current.scopes } : {},
|
|
481
|
+
...current?.deviceExpiresAt ? { lapsesAt: current.deviceExpiresAt } : {}
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function scopeInterruptionNotice(scope, state2) {
|
|
485
|
+
const platformScope = scope.startsWith("platform:");
|
|
486
|
+
const reason = !state2.enrolled ? "this machine is not enrolled, so every privileged command needs its own browser approval" : `this machine is enrolled but its approval did not include "${scope}"`;
|
|
487
|
+
return [
|
|
488
|
+
`odla: ${reason}.`,
|
|
489
|
+
` Approve this one now, then end the interruptions with:`,
|
|
490
|
+
` ${platformScope ? ENROL_PLATFORM_WIDE : ENROL_EVERYTHING}`,
|
|
491
|
+
platformScope ? " A platform scope needs an administrator's approval; an app owner's cannot carry it." : " One approval, every app you own, and it rolls forward while you keep working."
|
|
492
|
+
].join("\n");
|
|
493
|
+
}
|
|
494
|
+
function lapseNotice(state2, now = Date.now()) {
|
|
495
|
+
if (!state2.enrolled || !state2.lapsesAt) return null;
|
|
496
|
+
const days = Math.floor((state2.lapsesAt - now) / (24 * 60 * 60 * 1e3));
|
|
497
|
+
if (days < 0) return "this machine's enrollment has lapsed; the next command will ask for approval";
|
|
498
|
+
return `idle for ${days} more day${days === 1 ? "" : "s"} before this machine needs approving again (using it resets the clock)`;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/cached-credential.ts
|
|
502
|
+
var noted = null;
|
|
503
|
+
function noteCachedCredential(tokenFile) {
|
|
504
|
+
noted = tokenFile;
|
|
505
|
+
}
|
|
506
|
+
function isCredentialRejection(error) {
|
|
507
|
+
const message2 = error instanceof Error ? error.message : String(error ?? "");
|
|
508
|
+
return /\((401|403)\)\s*$/.test(message2.trim());
|
|
509
|
+
}
|
|
510
|
+
function explainRejectedCredential(error) {
|
|
511
|
+
const tokenFile = noted;
|
|
512
|
+
if (!tokenFile || !isCredentialRejection(error)) return null;
|
|
513
|
+
noted = null;
|
|
514
|
+
(0, import_node_fs5.rmSync)(tokenFile, { force: true });
|
|
515
|
+
return [
|
|
516
|
+
"auth: the cached credential was rejected by odla, so it was revoked before its cached expiry.",
|
|
517
|
+
" The usual cause is a newer sign-in for this account: collecting a handshake retires the",
|
|
518
|
+
" principal's other collected credentials, so a second machine supersedes this one.",
|
|
519
|
+
` Discarded ${tokenFile}; re-run this command to request a fresh approval.`,
|
|
520
|
+
` To stop needing one: ${ENROL_EVERYTHING}`
|
|
521
|
+
].join("\n");
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// src/device-session-cache.ts
|
|
525
|
+
var import_node_process7 = __toESM(require("process"), 1);
|
|
526
|
+
var SKEW_MS = 6e4;
|
|
527
|
+
async function deviceSessionToken(platformUrl, audience, credential2, doFetch, env = import_node_process7.default.env) {
|
|
528
|
+
const path = deviceSessionFile(env);
|
|
529
|
+
const cached = readJsonFile(path);
|
|
530
|
+
if (cached?.token && cached.platform === audience && cached.deviceId === credential2.deviceId && (cached.expiresAt ?? 0) > Date.now() + SKEW_MS) return cached;
|
|
531
|
+
const minted = await mintDeviceSession(platformUrl, credential2, doFetch);
|
|
532
|
+
const session = {
|
|
533
|
+
...minted,
|
|
534
|
+
platform: audience,
|
|
535
|
+
...credential2.deviceId ? { deviceId: credential2.deviceId } : {}
|
|
536
|
+
};
|
|
537
|
+
writePrivateJson(path, session);
|
|
538
|
+
return session;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/machine-identity.ts
|
|
542
|
+
var import_node_process8 = __toESM(require("process"), 1);
|
|
543
|
+
function readMachineIdentity(audience, env = import_node_process8.default.env) {
|
|
544
|
+
const stored = readJsonFile(identityFile(env));
|
|
545
|
+
if (!stored || typeof stored.email !== "string" || !stored.email) return null;
|
|
546
|
+
return stored.platform === audience ? { platform: audience, email: stored.email } : null;
|
|
547
|
+
}
|
|
548
|
+
function rememberMachineIdentity(audience, email, env = import_node_process8.default.env) {
|
|
549
|
+
if (!email) return;
|
|
550
|
+
const existing = readMachineIdentity(audience, env);
|
|
551
|
+
if (existing?.email === email) return;
|
|
552
|
+
writePrivateJson(identityFile(env), { platform: audience, email });
|
|
553
|
+
}
|
|
554
|
+
|
|
425
555
|
// src/token.ts
|
|
426
556
|
async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {}) {
|
|
427
557
|
const audience = platformAudience(cfg.platformUrl);
|
|
@@ -430,19 +560,19 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
|
|
|
430
560
|
const cached = readJsonFile(cfg.local.tokenFile);
|
|
431
561
|
if (!grantRequest.forceReview && !grantRequest.freshLogin) {
|
|
432
562
|
if (options.token) return options.token;
|
|
433
|
-
if (
|
|
434
|
-
const declared =
|
|
563
|
+
if (import_node_process9.default.env.ODLA_DEV_TOKEN) {
|
|
564
|
+
const declared = import_node_process9.default.env.ODLA_DEV_TOKEN_AUDIENCE;
|
|
435
565
|
if (declared) {
|
|
436
566
|
if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
|
|
437
567
|
} else if (audience !== "https://odla.ai") {
|
|
438
568
|
throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
|
|
439
569
|
}
|
|
440
|
-
return
|
|
570
|
+
return import_node_process9.default.env.ODLA_DEV_TOKEN;
|
|
441
571
|
}
|
|
442
572
|
const device = readDeviceCredential(audience);
|
|
443
573
|
if (device) {
|
|
444
|
-
const session = await
|
|
445
|
-
out.error(`auth: session
|
|
574
|
+
const session = await deviceSessionToken(cfg.platformUrl, audience, device, doFetch);
|
|
575
|
+
out.error(`auth: session held by this enrolled device (${displayPath(deviceCredentialPath(), cfg.rootDir)})`);
|
|
446
576
|
return session.token;
|
|
447
577
|
}
|
|
448
578
|
if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4 && cachedGrantCovers(cached, grantIntent)) {
|
|
@@ -462,7 +592,10 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
|
|
|
462
592
|
doFetch,
|
|
463
593
|
out,
|
|
464
594
|
audience,
|
|
465
|
-
email: handshakeEmail(
|
|
595
|
+
email: handshakeEmail(
|
|
596
|
+
options.email,
|
|
597
|
+
(cached?.platform === audience ? cached.email : void 0) ?? readMachineIdentity(audience)?.email
|
|
598
|
+
),
|
|
466
599
|
pendingFile: handshakeFile(cfg),
|
|
467
600
|
grantIntent
|
|
468
601
|
};
|
|
@@ -479,6 +612,7 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
|
|
|
479
612
|
expiresAt
|
|
480
613
|
});
|
|
481
614
|
out.error(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
615
|
+
rememberMachineIdentity(audience, ctx.email);
|
|
482
616
|
return token;
|
|
483
617
|
}
|
|
484
618
|
async function freshHandshake(ctx, waitMs) {
|
|
@@ -548,7 +682,7 @@ function stillPending(pending, email) {
|
|
|
548
682
|
);
|
|
549
683
|
}
|
|
550
684
|
function handshakeEmail(value2, cached) {
|
|
551
|
-
const email = (value2 ??
|
|
685
|
+
const email = (value2 ?? import_node_process9.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
|
|
552
686
|
if (/@users\.noreply\.github\.com$/i.test(email)) {
|
|
553
687
|
throw new Error(
|
|
554
688
|
`"${email}" is a GitHub commit identity, not an odla account email; use --email <signed-in-odla-account> or ODLA_USER_EMAIL`
|
|
@@ -579,12 +713,12 @@ function platformAudience(value2) {
|
|
|
579
713
|
}
|
|
580
714
|
|
|
581
715
|
// src/secret-input.ts
|
|
582
|
-
var
|
|
716
|
+
var import_node_process10 = __toESM(require("process"), 1);
|
|
583
717
|
var MAX_BYTES = 64 * 1024;
|
|
584
718
|
async function secretInputValue(options, kind = "credential") {
|
|
585
719
|
if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
|
|
586
720
|
let value2;
|
|
587
|
-
if (options.fromEnv) value2 =
|
|
721
|
+
if (options.fromEnv) value2 = import_node_process10.default.env[options.fromEnv];
|
|
588
722
|
else if (options.stdin) value2 = await (options.readStdin ?? (() => readSecretStream(kind)))();
|
|
589
723
|
else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
|
|
590
724
|
value2 = value2?.replace(/[\r\n]+$/, "");
|
|
@@ -592,7 +726,7 @@ async function secretInputValue(options, kind = "credential") {
|
|
|
592
726
|
if (new TextEncoder().encode(value2).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
|
|
593
727
|
return value2;
|
|
594
728
|
}
|
|
595
|
-
async function readSecretStream(kind, stream =
|
|
729
|
+
async function readSecretStream(kind, stream = import_node_process10.default.stdin) {
|
|
596
730
|
let value2 = "";
|
|
597
731
|
for await (const chunk of stream) {
|
|
598
732
|
value2 += String(chunk);
|
|
@@ -602,9 +736,8 @@ async function readSecretStream(kind, stream = import_node_process6.default.stdi
|
|
|
602
736
|
}
|
|
603
737
|
|
|
604
738
|
// src/admin-ai-auth.ts
|
|
605
|
-
var
|
|
606
|
-
var
|
|
607
|
-
var import_node_process7 = __toESM(require("process"), 1);
|
|
739
|
+
var import_node_path5 = require("path");
|
|
740
|
+
var import_node_process11 = __toESM(require("process"), 1);
|
|
608
741
|
var import_db2 = require("@odla-ai/db");
|
|
609
742
|
async function getScopedPlatformToken(options) {
|
|
610
743
|
return resolveAdminPlatformToken(options);
|
|
@@ -612,7 +745,7 @@ async function getScopedPlatformToken(options) {
|
|
|
612
745
|
async function resolveAdminPlatformToken(options) {
|
|
613
746
|
const audience = platformAudience(options.platform);
|
|
614
747
|
if (options.token) return options.token;
|
|
615
|
-
const fromEnv =
|
|
748
|
+
const fromEnv = import_node_process11.default.env.ODLA_ADMIN_TOKEN;
|
|
616
749
|
if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
|
|
617
750
|
return scopedToken(
|
|
618
751
|
audience,
|
|
@@ -624,7 +757,7 @@ async function resolveAdminPlatformToken(options) {
|
|
|
624
757
|
}
|
|
625
758
|
function audienceBoundEnvToken(token, platform) {
|
|
626
759
|
const audience = platformAudience(platform);
|
|
627
|
-
const declared =
|
|
760
|
+
const declared = import_node_process11.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
|
|
628
761
|
if (declared) {
|
|
629
762
|
if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
|
|
630
763
|
} else if (audience !== "https://odla.ai") {
|
|
@@ -651,15 +784,28 @@ var SCOPE_PURPOSE = {
|
|
|
651
784
|
};
|
|
652
785
|
async function scopedToken(platform, scope, options, doFetch, out) {
|
|
653
786
|
const audience = platformAudience(platform);
|
|
654
|
-
const rootDir = options.rootDir ??
|
|
655
|
-
const tokenFile = options.tokenFile ?? (
|
|
787
|
+
const rootDir = options.rootDir ?? import_node_process11.default.cwd();
|
|
788
|
+
const tokenFile = options.tokenFile ?? scopedTokenFile();
|
|
789
|
+
adoptRepoLocalCache((0, import_node_path5.join)(rootDir, ".odla/admin-token.local.json"), tokenFile, out);
|
|
790
|
+
const device = readDeviceCredential(audience);
|
|
791
|
+
if (device && options.cache !== false) {
|
|
792
|
+
const session = await deviceSessionToken(platform, audience, device, doFetch);
|
|
793
|
+
if (session.scopes?.includes(scope)) {
|
|
794
|
+
out.error(`auth: ${scope} held by this enrolled device`);
|
|
795
|
+
return session.token;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
656
798
|
const cache2 = options.cache === false ? null : readJsonFile(tokenFile);
|
|
657
799
|
const cached = cache2?.platform === audience ? cache2.tokens?.[scope] : void 0;
|
|
658
800
|
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
659
801
|
out.error(`auth: using cached ${scope} grant (${tokenFile})`);
|
|
660
802
|
return cached.token;
|
|
661
803
|
}
|
|
662
|
-
|
|
804
|
+
out.error(scopeInterruptionNotice(scope, machineAuthState(audience)));
|
|
805
|
+
const email = handshakeEmail(
|
|
806
|
+
options.email,
|
|
807
|
+
(cache2?.platform === audience ? cache2.email : void 0) ?? readMachineIdentity(audience)?.email
|
|
808
|
+
);
|
|
663
809
|
const { token, expiresAt } = await (0, import_db2.requestToken)({
|
|
664
810
|
endpoint: audience,
|
|
665
811
|
email,
|
|
@@ -679,8 +825,8 @@ async function scopedToken(platform, scope, options, doFetch, out) {
|
|
|
679
825
|
if (options.cache !== false) {
|
|
680
826
|
const tokens = cache2?.platform === audience ? { ...cache2.tokens ?? {} } : {};
|
|
681
827
|
tokens[scope] = { token, expiresAt };
|
|
682
|
-
if ((0, import_node_fs5.existsSync)((0, import_node_path4.join)(rootDir, ".git"))) ensureGitignore(rootDir, [tokenFile]);
|
|
683
828
|
writePrivateJson(tokenFile, { platform: audience, email, tokens });
|
|
829
|
+
rememberMachineIdentity(audience, email);
|
|
684
830
|
out.error(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
|
|
685
831
|
} else {
|
|
686
832
|
out.error(`auth: ${scope} grant is in memory only; its credential record remains in odla-ai/db`);
|
|
@@ -856,7 +1002,7 @@ function isRecord2(value2) {
|
|
|
856
1002
|
|
|
857
1003
|
// src/admin-ai.ts
|
|
858
1004
|
async function adminAi(options) {
|
|
859
|
-
const platform = platformAudience(options.platform ??
|
|
1005
|
+
const platform = platformAudience(options.platform ?? import_node_process12.default.env.ODLA_PLATFORM ?? "https://odla.ai");
|
|
860
1006
|
const doFetch = options.fetch ?? fetch;
|
|
861
1007
|
const out = options.stdout ?? console;
|
|
862
1008
|
const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
|
|
@@ -1172,12 +1318,12 @@ async function adminSpend(parsed, ctx) {
|
|
|
1172
1318
|
|
|
1173
1319
|
// src/operator-context.ts
|
|
1174
1320
|
var import_node_fs8 = require("fs");
|
|
1175
|
-
var
|
|
1176
|
-
var
|
|
1321
|
+
var import_node_path8 = require("path");
|
|
1322
|
+
var import_node_process14 = __toESM(require("process"), 1);
|
|
1177
1323
|
|
|
1178
1324
|
// src/config.ts
|
|
1179
1325
|
var import_node_fs6 = require("fs");
|
|
1180
|
-
var
|
|
1326
|
+
var import_node_path6 = require("path");
|
|
1181
1327
|
var import_node_url = require("url");
|
|
1182
1328
|
var import_apps = require("@odla-ai/apps");
|
|
1183
1329
|
|
|
@@ -1581,13 +1727,17 @@ var DEFAULT_ENVS = ["dev"];
|
|
|
1581
1727
|
var DEFAULT_SERVICES = ["db", "ai"];
|
|
1582
1728
|
var configImportSerial = 0;
|
|
1583
1729
|
var GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events";
|
|
1730
|
+
var stderr = { error: (message2) => {
|
|
1731
|
+
process.stderr.write(`${message2}
|
|
1732
|
+
`);
|
|
1733
|
+
} };
|
|
1584
1734
|
async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
|
|
1585
|
-
const resolved = (0,
|
|
1735
|
+
const resolved = (0, import_node_path6.resolve)(configPath);
|
|
1586
1736
|
if (!(0, import_node_fs6.existsSync)(resolved)) {
|
|
1587
1737
|
throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
|
|
1588
1738
|
}
|
|
1589
1739
|
const raw = await loadConfigModule(resolved);
|
|
1590
|
-
const rootDir = (0,
|
|
1740
|
+
const rootDir = (0, import_node_path6.dirname)(resolved);
|
|
1591
1741
|
validateRawConfig(raw, resolved);
|
|
1592
1742
|
const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
|
|
1593
1743
|
const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
|
|
@@ -1597,11 +1747,14 @@ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
|
|
|
1597
1747
|
validateCalendarConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
|
|
1598
1748
|
validateMonitoringConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
|
|
1599
1749
|
const local = {
|
|
1600
|
-
tokenFile: (0,
|
|
1601
|
-
credentialsFile: (0,
|
|
1602
|
-
devVarsFile: (0,
|
|
1750
|
+
tokenFile: raw.local?.tokenFile ? (0, import_node_path6.resolve)(rootDir, raw.local.tokenFile) : appTokenFile(raw.app.id),
|
|
1751
|
+
credentialsFile: raw.local?.credentialsFile ? (0, import_node_path6.resolve)(rootDir, raw.local.credentialsFile) : appCredentialsFile(raw.app.id),
|
|
1752
|
+
devVarsFile: (0, import_node_path6.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
|
|
1603
1753
|
gitignore: raw.local?.gitignore ?? true
|
|
1604
1754
|
};
|
|
1755
|
+
adoptRepoLocalCache((0, import_node_path6.resolve)(rootDir, ".odla/dev-token.json"), local.tokenFile, stderr);
|
|
1756
|
+
adoptRepoLocalCache((0, import_node_path6.resolve)(rootDir, ".odla/credentials.local.json"), local.credentialsFile, stderr);
|
|
1757
|
+
(0, import_node_fs6.rmSync)((0, import_node_path6.resolve)(rootDir, ".odla/handshake.local.json"), { force: true });
|
|
1605
1758
|
return {
|
|
1606
1759
|
...raw,
|
|
1607
1760
|
configPath: resolved,
|
|
@@ -1616,7 +1769,7 @@ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
|
|
|
1616
1769
|
async function resolveDataExport(cfg, value2, names) {
|
|
1617
1770
|
if (value2 === void 0 || value2 === null || value2 === false) return void 0;
|
|
1618
1771
|
if (typeof value2 !== "string") return value2;
|
|
1619
|
-
const target = (0,
|
|
1772
|
+
const target = (0, import_node_path6.isAbsolute)(value2) ? value2 : (0, import_node_path6.resolve)(cfg.rootDir, value2);
|
|
1620
1773
|
if (target.endsWith(".json")) {
|
|
1621
1774
|
return JSON.parse((0, import_node_fs6.readFileSync)(target, "utf8"));
|
|
1622
1775
|
}
|
|
@@ -1710,17 +1863,17 @@ function unique3(values) {
|
|
|
1710
1863
|
|
|
1711
1864
|
// src/operator-profiles.ts
|
|
1712
1865
|
var import_node_fs7 = require("fs");
|
|
1713
|
-
var
|
|
1714
|
-
var
|
|
1715
|
-
var
|
|
1866
|
+
var import_node_os3 = require("os");
|
|
1867
|
+
var import_node_path7 = require("path");
|
|
1868
|
+
var import_node_process13 = __toESM(require("process"), 1);
|
|
1716
1869
|
function operatorProfileFile() {
|
|
1717
|
-
return (0,
|
|
1718
|
-
clean(
|
|
1870
|
+
return (0, import_node_path7.resolve)(
|
|
1871
|
+
clean(import_node_process13.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path7.join)((0, import_node_os3.homedir)(), ".odla", "contexts.json")
|
|
1719
1872
|
);
|
|
1720
1873
|
}
|
|
1721
1874
|
function resolveOperatorProfile(parsed) {
|
|
1722
1875
|
const fromFlag = clean(stringOpt(parsed.options.context));
|
|
1723
|
-
const fromEnvironment = clean(
|
|
1876
|
+
const fromEnvironment = clean(import_node_process13.default.env.ODLA_CONTEXT);
|
|
1724
1877
|
const name = fromFlag ?? fromEnvironment ?? null;
|
|
1725
1878
|
const file = operatorProfileFile();
|
|
1726
1879
|
if (!name) {
|
|
@@ -1760,10 +1913,10 @@ function removeOperatorProfile(name, file = operatorProfileFile()) {
|
|
|
1760
1913
|
return true;
|
|
1761
1914
|
}
|
|
1762
1915
|
function operatorCredentialFiles(selection) {
|
|
1763
|
-
const base = selection.name ? (0,
|
|
1916
|
+
const base = selection.name ? (0, import_node_path7.join)((0, import_node_path7.dirname)(selection.file), "profiles", selection.name) : (0, import_node_path7.join)((0, import_node_os3.homedir)(), ".odla");
|
|
1764
1917
|
return {
|
|
1765
|
-
developer: (0,
|
|
1766
|
-
scoped: (0,
|
|
1918
|
+
developer: (0, import_node_path7.join)(base, "dev-token.json"),
|
|
1919
|
+
scoped: (0, import_node_path7.join)(base, "admin-token.local.json")
|
|
1767
1920
|
};
|
|
1768
1921
|
}
|
|
1769
1922
|
function assertOperatorName(value2, label) {
|
|
@@ -1841,7 +1994,7 @@ var DEFAULT_PLATFORM2 = "https://odla.ai";
|
|
|
1841
1994
|
async function resolveOperatorContext(parsed, options = {}) {
|
|
1842
1995
|
const profile = resolveOperatorProfile(parsed);
|
|
1843
1996
|
const configArgument = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
1844
|
-
const configPath = (0,
|
|
1997
|
+
const configPath = (0, import_node_path8.resolve)(configArgument);
|
|
1845
1998
|
const explicitConfig = parsed.options.config !== void 0;
|
|
1846
1999
|
const hasConfig = (0, import_node_fs8.existsSync)(configPath);
|
|
1847
2000
|
if (!hasConfig && (!options.allowMissingConfig || explicitConfig)) {
|
|
@@ -1849,13 +2002,13 @@ async function resolveOperatorContext(parsed, options = {}) {
|
|
|
1849
2002
|
}
|
|
1850
2003
|
const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
|
|
1851
2004
|
const platformFlag = clean2(stringOpt(parsed.options.platform));
|
|
1852
|
-
const platformEnvironment = clean2(
|
|
2005
|
+
const platformEnvironment = clean2(import_node_process14.default.env.ODLA_PLATFORM_URL);
|
|
1853
2006
|
const platformValue = platformAudience(
|
|
1854
2007
|
platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
|
|
1855
2008
|
);
|
|
1856
2009
|
const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
|
|
1857
2010
|
const appFlag = clean2(stringOpt(parsed.options.app));
|
|
1858
|
-
const appEnvironment = clean2(
|
|
2011
|
+
const appEnvironment = clean2(import_node_process14.default.env.ODLA_APP_ID);
|
|
1859
2012
|
const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
|
|
1860
2013
|
const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
|
|
1861
2014
|
if (appValue) {
|
|
@@ -1869,16 +2022,16 @@ async function resolveOperatorContext(parsed, options = {}) {
|
|
|
1869
2022
|
);
|
|
1870
2023
|
}
|
|
1871
2024
|
const envFlag = clean2(stringOpt(parsed.options.env));
|
|
1872
|
-
const envEnvironment = clean2(
|
|
2025
|
+
const envEnvironment = clean2(import_node_process14.default.env.ODLA_ENV);
|
|
1873
2026
|
const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
|
|
1874
2027
|
const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
|
|
1875
2028
|
if (environmentValue) {
|
|
1876
2029
|
assertOperatorName(environmentValue, "environment");
|
|
1877
2030
|
}
|
|
1878
|
-
const rootDir = loaded?.rootDir ??
|
|
2031
|
+
const rootDir = loaded?.rootDir ?? import_node_process14.default.cwd();
|
|
1879
2032
|
const profileCredentials = operatorCredentialFiles(profile);
|
|
1880
|
-
const tokenFile = clean2(
|
|
1881
|
-
const
|
|
2033
|
+
const tokenFile = clean2(import_node_process14.default.env.ODLA_DEV_TOKEN_FILE) ? (0, import_node_path8.resolve)(import_node_process14.default.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
|
|
2034
|
+
const scopedTokenFile2 = clean2(import_node_process14.default.env.ODLA_ADMIN_TOKEN_FILE) ? (0, import_node_path8.resolve)(import_node_process14.default.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? (0, import_node_path8.join)(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
|
|
1882
2035
|
const cfg = loaded ? {
|
|
1883
2036
|
...loaded,
|
|
1884
2037
|
platformUrl: platformValue,
|
|
@@ -1900,8 +2053,8 @@ async function resolveOperatorContext(parsed, options = {}) {
|
|
|
1900
2053
|
services: [],
|
|
1901
2054
|
local: {
|
|
1902
2055
|
tokenFile,
|
|
1903
|
-
credentialsFile: (0,
|
|
1904
|
-
devVarsFile: (0,
|
|
2056
|
+
credentialsFile: (0, import_node_path8.join)(rootDir, ".odla", "credentials.local.json"),
|
|
2057
|
+
devVarsFile: (0, import_node_path8.join)(rootDir, ".dev.vars"),
|
|
1905
2058
|
gitignore: true
|
|
1906
2059
|
}
|
|
1907
2060
|
};
|
|
@@ -1925,7 +2078,7 @@ async function resolveOperatorContext(parsed, options = {}) {
|
|
|
1925
2078
|
},
|
|
1926
2079
|
credentials: {
|
|
1927
2080
|
developerTokenFile: tokenFile,
|
|
1928
|
-
scopedTokenFile
|
|
2081
|
+
scopedTokenFile: scopedTokenFile2
|
|
1929
2082
|
}
|
|
1930
2083
|
};
|
|
1931
2084
|
}
|
|
@@ -2023,7 +2176,7 @@ async function adminCommand(parsed, deps = {}) {
|
|
|
2023
2176
|
}
|
|
2024
2177
|
|
|
2025
2178
|
// src/auth-command.ts
|
|
2026
|
-
var
|
|
2179
|
+
var import_node_process15 = __toESM(require("process"), 1);
|
|
2027
2180
|
|
|
2028
2181
|
// src/whoami-command.ts
|
|
2029
2182
|
var text2 = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
|
|
@@ -2159,6 +2312,7 @@ async function whoamiCommand(parsed, deps = {}) {
|
|
|
2159
2312
|
} else {
|
|
2160
2313
|
out.log("projects: (none \u2014 every pm and discuss call will be refused)");
|
|
2161
2314
|
}
|
|
2315
|
+
printMachineBlock(cfg.platformUrl, out);
|
|
2162
2316
|
if (!identity.admin) {
|
|
2163
2317
|
if (identity.scopes.includes("platform:runbook:write")) {
|
|
2164
2318
|
out.log("\nThis exact scope can read and edit all platform runbook content.");
|
|
@@ -2169,6 +2323,21 @@ async function whoamiCommand(parsed, deps = {}) {
|
|
|
2169
2323
|
}
|
|
2170
2324
|
}
|
|
2171
2325
|
}
|
|
2326
|
+
function printMachineBlock(platformUrl, out) {
|
|
2327
|
+
const state2 = machineAuthState(platformAudience(platformUrl));
|
|
2328
|
+
if (!state2.enrolled) {
|
|
2329
|
+
out.log("\nmachine: not enrolled \u2014 every privileged command needs its own browser approval.");
|
|
2330
|
+
out.log(` End that with:
|
|
2331
|
+
${ENROL_EVERYTHING}`);
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2334
|
+
const reach = state2.appIds?.includes("*") ? "every app you own" : state2.appIds?.join(", ");
|
|
2335
|
+
out.log(`
|
|
2336
|
+
machine: enrolled${state2.deviceName ? ` as "${state2.deviceName}"` : ""}${reach ? ` for ${reach}` : ""}`);
|
|
2337
|
+
if (state2.scopes?.length) out.log(` carrying ${state2.scopes.join(", ")}`);
|
|
2338
|
+
const lapse = lapseNotice(state2);
|
|
2339
|
+
if (lapse) out.log(` ${lapse}`);
|
|
2340
|
+
}
|
|
2172
2341
|
|
|
2173
2342
|
// src/auth-command.ts
|
|
2174
2343
|
async function authCommand(parsed, deps = {}) {
|
|
@@ -2193,7 +2362,7 @@ async function authCommand(parsed, deps = {}) {
|
|
|
2193
2362
|
const { cfg } = context;
|
|
2194
2363
|
const out = deps.stdout ?? console;
|
|
2195
2364
|
const doFetch = deps.fetch ?? fetch;
|
|
2196
|
-
const email = stringOpt(parsed.options.email) ??
|
|
2365
|
+
const email = stringOpt(parsed.options.email) ?? import_node_process15.default.env.ODLA_USER_EMAIL?.trim();
|
|
2197
2366
|
if (!email) {
|
|
2198
2367
|
throw new Error(
|
|
2199
2368
|
"auth login requires --email <odla-account> or ODLA_USER_EMAIL; confirm the signed-in odla email instead of using git or GitHub identity"
|
|
@@ -2554,7 +2723,7 @@ async function appCommand(parsed, dependencies = {}) {
|
|
|
2554
2723
|
|
|
2555
2724
|
// src/brand-command.ts
|
|
2556
2725
|
var import_promises = require("fs/promises");
|
|
2557
|
-
var
|
|
2726
|
+
var import_node_path9 = require("path");
|
|
2558
2727
|
|
|
2559
2728
|
// src/brand-design-unpack.ts
|
|
2560
2729
|
var import_node_zlib = require("zlib");
|
|
@@ -2656,15 +2825,15 @@ function describeUnpack(result, outDir) {
|
|
|
2656
2825
|
// src/brand-command.ts
|
|
2657
2826
|
var USAGE = "usage: odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]";
|
|
2658
2827
|
async function readBundle(source, deps) {
|
|
2659
|
-
if (source !== "-") return (0, import_promises.readFile)((0,
|
|
2828
|
+
if (source !== "-") return (0, import_promises.readFile)((0, import_node_path9.resolve)(source), "utf8");
|
|
2660
2829
|
const readStdin = deps.readStdin;
|
|
2661
2830
|
if (!readStdin) throw new Error("reading a bundle from stdin is not supported here");
|
|
2662
2831
|
return readStdin();
|
|
2663
2832
|
}
|
|
2664
2833
|
async function writeAll(result, outDir) {
|
|
2665
2834
|
for (const file of result.files) {
|
|
2666
|
-
const target = (0,
|
|
2667
|
-
await (0, import_promises.mkdir)((0,
|
|
2835
|
+
const target = (0, import_node_path9.resolve)(outDir, file.path);
|
|
2836
|
+
await (0, import_promises.mkdir)((0, import_node_path9.dirname)(target), { recursive: true });
|
|
2668
2837
|
await (0, import_promises.writeFile)(target, file.bytes);
|
|
2669
2838
|
}
|
|
2670
2839
|
}
|
|
@@ -2672,7 +2841,7 @@ async function designUnpack(parsed, deps) {
|
|
|
2672
2841
|
assertArgs(parsed, ["out", "json"], 4);
|
|
2673
2842
|
const source = parsed.positionals[3];
|
|
2674
2843
|
if (!source) throw new Error(USAGE);
|
|
2675
|
-
const outDir = (0,
|
|
2844
|
+
const outDir = (0, import_node_path9.resolve)(stringOpt(parsed.options.out) ?? "design");
|
|
2676
2845
|
const result = unpackDesign(await readBundle(source, deps));
|
|
2677
2846
|
await writeAll(result, outDir);
|
|
2678
2847
|
const out = deps.stdout ?? console;
|
|
@@ -3277,7 +3446,7 @@ async function safeText4(response2) {
|
|
|
3277
3446
|
|
|
3278
3447
|
// src/config-operation-command.ts
|
|
3279
3448
|
var import_apps6 = require("@odla-ai/apps");
|
|
3280
|
-
var
|
|
3449
|
+
var import_node_path10 = require("path");
|
|
3281
3450
|
|
|
3282
3451
|
// src/version.ts
|
|
3283
3452
|
var import_node_fs10 = require("fs");
|
|
@@ -3707,7 +3876,7 @@ async function operationClient(cfg, options, purpose) {
|
|
|
3707
3876
|
platform: cfg.platformUrl,
|
|
3708
3877
|
scope: "app:config:write",
|
|
3709
3878
|
token: options.token,
|
|
3710
|
-
tokenFile: (0,
|
|
3879
|
+
tokenFile: (0, import_node_path10.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
|
|
3711
3880
|
rootDir: cfg.rootDir,
|
|
3712
3881
|
email: options.email,
|
|
3713
3882
|
open: options.open,
|
|
@@ -3762,7 +3931,7 @@ function record4(value2) {
|
|
|
3762
3931
|
|
|
3763
3932
|
// src/config-reconcile-command.ts
|
|
3764
3933
|
var import_apps8 = require("@odla-ai/apps");
|
|
3765
|
-
var
|
|
3934
|
+
var import_node_path11 = require("path");
|
|
3766
3935
|
|
|
3767
3936
|
// src/config-reconcile.ts
|
|
3768
3937
|
var import_apps7 = require("@odla-ai/apps");
|
|
@@ -4058,7 +4227,7 @@ async function inspectConfig(options) {
|
|
|
4058
4227
|
platform: cfg.platformUrl,
|
|
4059
4228
|
scope: "app:config:read",
|
|
4060
4229
|
token: options.token,
|
|
4061
|
-
tokenFile: (0,
|
|
4230
|
+
tokenFile: (0, import_node_path11.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
|
|
4062
4231
|
rootDir: cfg.rootDir,
|
|
4063
4232
|
email: options.email,
|
|
4064
4233
|
open: options.open,
|
|
@@ -4191,26 +4360,26 @@ function quoteArg2(value2) {
|
|
|
4191
4360
|
// src/doctor-checks.ts
|
|
4192
4361
|
var import_node_child_process3 = require("child_process");
|
|
4193
4362
|
var import_node_fs13 = require("fs");
|
|
4194
|
-
var
|
|
4363
|
+
var import_node_path13 = require("path");
|
|
4195
4364
|
|
|
4196
4365
|
// src/wrangler.ts
|
|
4197
4366
|
var import_node_child_process2 = require("child_process");
|
|
4198
4367
|
var import_node_fs12 = require("fs");
|
|
4199
|
-
var
|
|
4368
|
+
var import_node_path12 = require("path");
|
|
4200
4369
|
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
4201
4370
|
const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
4202
4371
|
let stdout = "";
|
|
4203
|
-
let
|
|
4372
|
+
let stderr2 = "";
|
|
4204
4373
|
child.stdout.on("data", (chunk) => stdout += chunk.toString());
|
|
4205
|
-
child.stderr.on("data", (chunk) =>
|
|
4374
|
+
child.stderr.on("data", (chunk) => stderr2 += chunk.toString());
|
|
4206
4375
|
child.on("error", reject);
|
|
4207
|
-
child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr }));
|
|
4376
|
+
child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr: stderr2 }));
|
|
4208
4377
|
child.stdin.end(opts?.input ?? "");
|
|
4209
4378
|
});
|
|
4210
4379
|
var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
4211
4380
|
function findWranglerConfig(rootDir) {
|
|
4212
4381
|
for (const name of WRANGLER_CONFIG_FILES) {
|
|
4213
|
-
const path = (0,
|
|
4382
|
+
const path = (0, import_node_path12.join)(rootDir, name);
|
|
4214
4383
|
if ((0, import_node_fs12.existsSync)(path)) return path;
|
|
4215
4384
|
}
|
|
4216
4385
|
return null;
|
|
@@ -4361,21 +4530,21 @@ function wranglerWarnings(rootDir) {
|
|
|
4361
4530
|
const blocks = [{ label: "", block: config }];
|
|
4362
4531
|
const envs = config.env;
|
|
4363
4532
|
if (envs && typeof envs === "object") {
|
|
4364
|
-
for (const [name,
|
|
4365
|
-
if (
|
|
4533
|
+
for (const [name, block2] of Object.entries(envs)) {
|
|
4534
|
+
if (block2 && typeof block2 === "object") blocks.push({ label: `env.${name}.`, block: block2 });
|
|
4366
4535
|
}
|
|
4367
4536
|
}
|
|
4368
|
-
for (const { label, block } of blocks) {
|
|
4369
|
-
const assets =
|
|
4537
|
+
for (const { label, block: block2 } of blocks) {
|
|
4538
|
+
const assets = block2.assets;
|
|
4370
4539
|
if (assets?.directory) {
|
|
4371
|
-
const dir = (0,
|
|
4372
|
-
if (dir === (0,
|
|
4540
|
+
const dir = (0, import_node_path13.resolve)(rootDir, assets.directory);
|
|
4541
|
+
if (dir === (0, import_node_path13.resolve)(rootDir)) {
|
|
4373
4542
|
warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
|
|
4374
|
-
} else if ((0, import_node_fs13.existsSync)((0,
|
|
4543
|
+
} else if ((0, import_node_fs13.existsSync)((0, import_node_path13.join)(dir, "node_modules"))) {
|
|
4375
4544
|
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
4376
4545
|
}
|
|
4377
4546
|
}
|
|
4378
|
-
const vars =
|
|
4547
|
+
const vars = block2.vars;
|
|
4379
4548
|
if (vars && typeof vars === "object") {
|
|
4380
4549
|
for (const [name, value2] of Object.entries(vars)) {
|
|
4381
4550
|
if (name === "ODLA_API_KEY" || name === "ODLA_O11Y_TOKEN" || typeof value2 === "string" && looksSecret(value2)) {
|
|
@@ -4406,7 +4575,7 @@ function o11yProjectWarnings(rootDir) {
|
|
|
4406
4575
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
|
|
4407
4576
|
return warnings;
|
|
4408
4577
|
}
|
|
4409
|
-
const main = typeof config.main === "string" ? (0,
|
|
4578
|
+
const main = typeof config.main === "string" ? (0, import_node_path13.resolve)(rootDir, config.main) : null;
|
|
4410
4579
|
if (!main || !(0, import_node_fs13.existsSync)(main)) {
|
|
4411
4580
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
|
|
4412
4581
|
} else {
|
|
@@ -4436,7 +4605,7 @@ function calendarProjectWarnings(rootDir) {
|
|
|
4436
4605
|
}
|
|
4437
4606
|
function readPackageJson(rootDir) {
|
|
4438
4607
|
try {
|
|
4439
|
-
return JSON.parse((0, import_node_fs13.readFileSync)((0,
|
|
4608
|
+
return JSON.parse((0, import_node_fs13.readFileSync)((0, import_node_path13.join)(rootDir, "package.json"), "utf8"));
|
|
4440
4609
|
} catch {
|
|
4441
4610
|
return null;
|
|
4442
4611
|
}
|
|
@@ -4731,12 +4900,12 @@ function harnessOption(value2, flag) {
|
|
|
4731
4900
|
|
|
4732
4901
|
// src/init.ts
|
|
4733
4902
|
var import_node_fs14 = require("fs");
|
|
4734
|
-
var
|
|
4903
|
+
var import_node_path14 = require("path");
|
|
4735
4904
|
var import_apps9 = require("@odla-ai/apps");
|
|
4736
4905
|
function initProject(options) {
|
|
4737
4906
|
const out = options.stdout ?? console;
|
|
4738
|
-
const rootDir = (0,
|
|
4739
|
-
const configPath = (0,
|
|
4907
|
+
const rootDir = (0, import_node_path14.resolve)(options.rootDir ?? process.cwd());
|
|
4908
|
+
const configPath = (0, import_node_path14.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
|
|
4740
4909
|
if ((0, import_node_fs14.existsSync)(configPath) && !options.force) {
|
|
4741
4910
|
throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
|
|
4742
4911
|
}
|
|
@@ -4753,12 +4922,12 @@ function initProject(options) {
|
|
|
4753
4922
|
}
|
|
4754
4923
|
}
|
|
4755
4924
|
const aiProvider = options.aiProvider;
|
|
4756
|
-
(0, import_node_fs14.mkdirSync)((0,
|
|
4757
|
-
(0, import_node_fs14.mkdirSync)((0,
|
|
4758
|
-
(0, import_node_fs14.mkdirSync)((0,
|
|
4925
|
+
(0, import_node_fs14.mkdirSync)((0, import_node_path14.dirname)(configPath), { recursive: true });
|
|
4926
|
+
(0, import_node_fs14.mkdirSync)((0, import_node_path14.resolve)(rootDir, "src/odla"), { recursive: true });
|
|
4927
|
+
(0, import_node_fs14.mkdirSync)((0, import_node_path14.resolve)(rootDir, ".odla"), { recursive: true });
|
|
4759
4928
|
(0, import_node_fs14.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
4760
|
-
writeIfMissing((0,
|
|
4761
|
-
writeIfMissing((0,
|
|
4929
|
+
writeIfMissing((0, import_node_path14.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
4930
|
+
writeIfMissing((0, import_node_path14.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
4762
4931
|
ensureGitignore(rootDir);
|
|
4763
4932
|
out.log(`created ${relativeDisplay(configPath, rootDir)}`);
|
|
4764
4933
|
out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
|
|
@@ -4825,8 +4994,10 @@ ${calendar}
|
|
|
4825
4994
|
// prod: "https://example.com",
|
|
4826
4995
|
},
|
|
4827
4996
|
local: {
|
|
4828
|
-
|
|
4829
|
-
|
|
4997
|
+
// Credentials live in ~/.odla, per machine, so every worktree of this app
|
|
4998
|
+
// shares one approval instead of asking for its own. Pinning tokenFile or
|
|
4999
|
+
// credentialsFile here still works and still overrides that \u2014 it just puts
|
|
5000
|
+
// this checkout back on its own island.
|
|
4830
5001
|
devVarsFile: ".dev.vars",
|
|
4831
5002
|
},
|
|
4832
5003
|
};
|
|
@@ -5063,8 +5234,8 @@ function printReport(report5, out) {
|
|
|
5063
5234
|
|
|
5064
5235
|
// src/skill.ts
|
|
5065
5236
|
var import_node_fs15 = require("fs");
|
|
5066
|
-
var
|
|
5067
|
-
var
|
|
5237
|
+
var import_node_os4 = require("os");
|
|
5238
|
+
var import_node_path15 = require("path");
|
|
5068
5239
|
var import_node_url2 = require("url");
|
|
5069
5240
|
|
|
5070
5241
|
// src/skill-adapters.ts
|
|
@@ -5163,8 +5334,8 @@ function installSkill(options = {}) {
|
|
|
5163
5334
|
const files = listFiles(sourceDir);
|
|
5164
5335
|
if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
|
|
5165
5336
|
const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
|
|
5166
|
-
const root = (0,
|
|
5167
|
-
const home = (0,
|
|
5337
|
+
const root = (0, import_node_path15.resolve)(options.dir ?? process.cwd());
|
|
5338
|
+
const home = (0, import_node_path15.resolve)(options.homeDir ?? (0, import_node_os4.homedir)());
|
|
5168
5339
|
const plans = /* @__PURE__ */ new Map();
|
|
5169
5340
|
const targets = /* @__PURE__ */ new Map();
|
|
5170
5341
|
const rememberTarget = (harness, target) => {
|
|
@@ -5178,48 +5349,48 @@ function installSkill(options = {}) {
|
|
|
5178
5349
|
plans.set(target, { target, content: content2, boundary, managedMerge });
|
|
5179
5350
|
};
|
|
5180
5351
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
5181
|
-
for (const rel of files) plan((0,
|
|
5352
|
+
for (const rel of files) plan((0, import_node_path15.join)(targetDir2, rel), (0, import_node_fs15.readFileSync)((0, import_node_path15.join)(sourceDir, rel), "utf8"), false, boundary);
|
|
5182
5353
|
};
|
|
5183
5354
|
let targetDir;
|
|
5184
5355
|
if (options.global) {
|
|
5185
|
-
const claudeRoot = (0,
|
|
5186
|
-
const codexRoot = (0,
|
|
5356
|
+
const claudeRoot = (0, import_node_path15.join)(home, ".claude", "skills");
|
|
5357
|
+
const codexRoot = (0, import_node_path15.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path15.join)(home, ".codex"), "skills");
|
|
5187
5358
|
targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
|
|
5188
5359
|
for (const harness of harnesses) {
|
|
5189
5360
|
const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
|
|
5190
|
-
planSkillTree(skillRoot, harness === "claude" ? home : (0,
|
|
5361
|
+
planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path15.dirname)((0, import_node_path15.dirname)(codexRoot)));
|
|
5191
5362
|
rememberTarget(harness, skillRoot);
|
|
5192
5363
|
}
|
|
5193
5364
|
} else {
|
|
5194
|
-
const sharedRoot = (0,
|
|
5365
|
+
const sharedRoot = (0, import_node_path15.join)(root, ".agents", "skills");
|
|
5195
5366
|
planSkillTree(sharedRoot);
|
|
5196
|
-
const claudeRoot = (0,
|
|
5367
|
+
const claudeRoot = (0, import_node_path15.join)(root, ".claude", "skills");
|
|
5197
5368
|
targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
|
|
5198
5369
|
for (const harness of harnesses) rememberTarget(harness, sharedRoot);
|
|
5199
5370
|
if (harnesses.includes("claude")) {
|
|
5200
5371
|
for (const skill of skillNames(files)) {
|
|
5201
|
-
const canonical2 = (0, import_node_fs15.readFileSync)((0,
|
|
5202
|
-
plan((0,
|
|
5372
|
+
const canonical2 = (0, import_node_fs15.readFileSync)((0, import_node_path15.join)(sourceDir, skill, "SKILL.md"), "utf8");
|
|
5373
|
+
plan((0, import_node_path15.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical2));
|
|
5203
5374
|
}
|
|
5204
5375
|
rememberTarget("claude", claudeRoot);
|
|
5205
5376
|
}
|
|
5206
5377
|
if (harnesses.includes("cursor")) {
|
|
5207
|
-
const cursorRule = (0,
|
|
5378
|
+
const cursorRule = (0, import_node_path15.join)(root, ".cursor", "rules", "odla.mdc");
|
|
5208
5379
|
plan(cursorRule, CURSOR_RULE);
|
|
5209
5380
|
rememberTarget("cursor", cursorRule);
|
|
5210
5381
|
}
|
|
5211
5382
|
if (harnesses.includes("agents")) {
|
|
5212
|
-
const agentsFile = (0,
|
|
5383
|
+
const agentsFile = (0, import_node_path15.join)(root, "AGENTS.md");
|
|
5213
5384
|
plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
5214
5385
|
rememberTarget("agents", agentsFile);
|
|
5215
5386
|
}
|
|
5216
5387
|
if (harnesses.includes("copilot")) {
|
|
5217
|
-
const copilotFile = (0,
|
|
5388
|
+
const copilotFile = (0, import_node_path15.join)(root, ".github", "copilot-instructions.md");
|
|
5218
5389
|
plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
5219
5390
|
rememberTarget("copilot", copilotFile);
|
|
5220
5391
|
}
|
|
5221
5392
|
if (harnesses.includes("gemini")) {
|
|
5222
|
-
const geminiFile = (0,
|
|
5393
|
+
const geminiFile = (0, import_node_path15.join)(root, "GEMINI.md");
|
|
5223
5394
|
plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
5224
5395
|
rememberTarget("gemini", geminiFile);
|
|
5225
5396
|
}
|
|
@@ -5255,7 +5426,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
5255
5426
|
}
|
|
5256
5427
|
for (const file of plans.values()) {
|
|
5257
5428
|
if (!(0, import_node_fs15.existsSync)(file.target) || (0, import_node_fs15.readFileSync)(file.target, "utf8") !== file.content) {
|
|
5258
|
-
(0, import_node_fs15.mkdirSync)((0,
|
|
5429
|
+
(0, import_node_fs15.mkdirSync)((0, import_node_path15.dirname)(file.target), { recursive: true });
|
|
5259
5430
|
(0, import_node_fs15.writeFileSync)(file.target, file.content);
|
|
5260
5431
|
}
|
|
5261
5432
|
}
|
|
@@ -5275,7 +5446,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
5275
5446
|
};
|
|
5276
5447
|
}
|
|
5277
5448
|
function pathsUnder(root, paths) {
|
|
5278
|
-
return [...paths].map((path) => (0,
|
|
5449
|
+
return [...paths].map((path) => (0, import_node_path15.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path15.sep}`) && !(0, import_node_path15.isAbsolute)(path)).sort();
|
|
5279
5450
|
}
|
|
5280
5451
|
function normalizeHarnesses(values, global) {
|
|
5281
5452
|
const requested = values?.length ? values : ["claude"];
|
|
@@ -5294,10 +5465,10 @@ function normalizeHarnesses(values, global) {
|
|
|
5294
5465
|
}
|
|
5295
5466
|
return expanded;
|
|
5296
5467
|
}
|
|
5297
|
-
function managedFileContent(path,
|
|
5468
|
+
function managedFileContent(path, block2, force, boundary) {
|
|
5298
5469
|
const symlink = symlinkedComponent(boundary, path);
|
|
5299
5470
|
if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
|
|
5300
|
-
if (!(0, import_node_fs15.existsSync)(path)) return `${
|
|
5471
|
+
if (!(0, import_node_fs15.existsSync)(path)) return `${block2}
|
|
5301
5472
|
`;
|
|
5302
5473
|
const current = (0, import_node_fs15.readFileSync)(path, "utf8");
|
|
5303
5474
|
const start = "<!-- odla-ai agent setup:start -->";
|
|
@@ -5309,24 +5480,24 @@ function managedFileContent(path, block, force, boundary) {
|
|
|
5309
5480
|
}
|
|
5310
5481
|
if (startAt === -1) {
|
|
5311
5482
|
const separator = current.length === 0 || current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n";
|
|
5312
|
-
return `${current}${separator}${
|
|
5483
|
+
return `${current}${separator}${block2}
|
|
5313
5484
|
`;
|
|
5314
5485
|
}
|
|
5315
5486
|
const afterEnd = endAt + end.length;
|
|
5316
5487
|
const existing = current.slice(startAt, afterEnd);
|
|
5317
|
-
if (existing !==
|
|
5488
|
+
if (existing !== block2 && !force) {
|
|
5318
5489
|
throw new Error(`odla-managed section modified locally in ${path}; re-run with --force to replace that section`);
|
|
5319
5490
|
}
|
|
5320
|
-
return `${current.slice(0, startAt)}${
|
|
5491
|
+
return `${current.slice(0, startAt)}${block2}${current.slice(afterEnd)}`;
|
|
5321
5492
|
}
|
|
5322
5493
|
function symlinkedComponent(boundary, target) {
|
|
5323
|
-
const rel = (0,
|
|
5324
|
-
if (rel === ".." || rel.startsWith(`..${
|
|
5494
|
+
const rel = (0, import_node_path15.relative)(boundary, target);
|
|
5495
|
+
if (rel === ".." || rel.startsWith(`..${import_node_path15.sep}`) || (0, import_node_path15.isAbsolute)(rel)) {
|
|
5325
5496
|
throw new Error(`agent setup target escapes its install root: ${target}`);
|
|
5326
5497
|
}
|
|
5327
5498
|
let current = boundary;
|
|
5328
|
-
for (const part of rel.split(
|
|
5329
|
-
current = (0,
|
|
5499
|
+
for (const part of rel.split(import_node_path15.sep).filter(Boolean)) {
|
|
5500
|
+
current = (0, import_node_path15.join)(current, part);
|
|
5330
5501
|
try {
|
|
5331
5502
|
if ((0, import_node_fs15.lstatSync)(current).isSymbolicLink()) return current;
|
|
5332
5503
|
} catch (error) {
|
|
@@ -5343,9 +5514,9 @@ function listFiles(dir) {
|
|
|
5343
5514
|
const results = [];
|
|
5344
5515
|
const walk = (current) => {
|
|
5345
5516
|
for (const entry of (0, import_node_fs15.readdirSync)(current, { withFileTypes: true })) {
|
|
5346
|
-
const path = (0,
|
|
5517
|
+
const path = (0, import_node_path15.join)(current, entry.name);
|
|
5347
5518
|
if (entry.isDirectory()) walk(path);
|
|
5348
|
-
else results.push((0,
|
|
5519
|
+
else results.push((0, import_node_path15.relative)(dir, path));
|
|
5349
5520
|
}
|
|
5350
5521
|
};
|
|
5351
5522
|
walk(dir);
|
|
@@ -5701,8 +5872,8 @@ async function projectCommand(command, parsed, deps) {
|
|
|
5701
5872
|
|
|
5702
5873
|
// src/code-connect.ts
|
|
5703
5874
|
var import_node_fs16 = require("fs");
|
|
5704
|
-
var
|
|
5705
|
-
var
|
|
5875
|
+
var import_node_os5 = require("os");
|
|
5876
|
+
var import_node_path16 = require("path");
|
|
5706
5877
|
|
|
5707
5878
|
// ../harness/dist/chunk-LNQNFGQC.js
|
|
5708
5879
|
var HARNESS_PROTOCOL_VERSION = 1;
|
|
@@ -5808,7 +5979,7 @@ function allowedWorkspacePath(relativePath) {
|
|
|
5808
5979
|
async function gitOutput(cwd, args, maxBytes) {
|
|
5809
5980
|
const child = (0, import_child_process2.spawn)("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], shell: false });
|
|
5810
5981
|
const stdout = [];
|
|
5811
|
-
const
|
|
5982
|
+
const stderr2 = [];
|
|
5812
5983
|
let bytes = 0;
|
|
5813
5984
|
child.stdout.on("data", (chunk) => {
|
|
5814
5985
|
bytes += chunk.byteLength;
|
|
@@ -5816,20 +5987,20 @@ async function gitOutput(cwd, args, maxBytes) {
|
|
|
5816
5987
|
else stdout.push(chunk);
|
|
5817
5988
|
});
|
|
5818
5989
|
child.stderr.on("data", (chunk) => {
|
|
5819
|
-
if (
|
|
5990
|
+
if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
|
|
5820
5991
|
});
|
|
5821
5992
|
const code = await new Promise((accept, reject) => {
|
|
5822
5993
|
child.once("error", reject);
|
|
5823
5994
|
child.once("exit", accept);
|
|
5824
5995
|
});
|
|
5825
5996
|
if (bytes > maxBytes) throw new Error(`git output exceeds ${maxBytes} bytes`);
|
|
5826
|
-
if (code !== 0) throw new Error(`git command failed: ${Buffer.concat(
|
|
5997
|
+
if (code !== 0) throw new Error(`git command failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
|
|
5827
5998
|
return Buffer.concat(stdout);
|
|
5828
5999
|
}
|
|
5829
6000
|
async function gitBlobs(cwd, entries, maxBytes) {
|
|
5830
6001
|
const child = (0, import_child_process2.spawn)("git", ["cat-file", "--batch"], { cwd, stdio: ["pipe", "pipe", "pipe"], shell: false });
|
|
5831
6002
|
const stdout = [];
|
|
5832
|
-
const
|
|
6003
|
+
const stderr2 = [];
|
|
5833
6004
|
let bytes = 0;
|
|
5834
6005
|
child.stdout.on("data", (chunk) => {
|
|
5835
6006
|
bytes += chunk.byteLength;
|
|
@@ -5837,7 +6008,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
|
|
|
5837
6008
|
else stdout.push(chunk);
|
|
5838
6009
|
});
|
|
5839
6010
|
child.stderr.on("data", (chunk) => {
|
|
5840
|
-
if (
|
|
6011
|
+
if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
|
|
5841
6012
|
});
|
|
5842
6013
|
child.stdin.end(`${entries.map((entry) => entry.hash).join("\n")}
|
|
5843
6014
|
`);
|
|
@@ -5846,7 +6017,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
|
|
|
5846
6017
|
child.once("exit", accept);
|
|
5847
6018
|
});
|
|
5848
6019
|
if (bytes > maxBytes + entries.length * 100) throw new Error(`Git tree exceeds ${maxBytes} bytes`);
|
|
5849
|
-
if (code !== 0) throw new Error(`git object read failed: ${Buffer.concat(
|
|
6020
|
+
if (code !== 0) throw new Error(`git object read failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
|
|
5850
6021
|
const output = Buffer.concat(stdout);
|
|
5851
6022
|
const blobs = [];
|
|
5852
6023
|
let offset = 0;
|
|
@@ -5943,7 +6114,7 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
|
5943
6114
|
shell: false
|
|
5944
6115
|
});
|
|
5945
6116
|
const stdout = [];
|
|
5946
|
-
const
|
|
6117
|
+
const stderr2 = [];
|
|
5947
6118
|
let outputBytes = 0;
|
|
5948
6119
|
child.stdout.on("data", (chunk) => {
|
|
5949
6120
|
outputBytes += chunk.byteLength;
|
|
@@ -5951,14 +6122,14 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
|
5951
6122
|
else stdout.push(chunk);
|
|
5952
6123
|
});
|
|
5953
6124
|
child.stderr.on("data", (chunk) => {
|
|
5954
|
-
if (
|
|
6125
|
+
if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
|
|
5955
6126
|
});
|
|
5956
6127
|
const code = await new Promise((accept, reject) => {
|
|
5957
6128
|
child.once("error", reject);
|
|
5958
6129
|
child.once("exit", accept);
|
|
5959
6130
|
});
|
|
5960
6131
|
if (outputBytes > 8 * 1024 * 1024) throw new Error("git file inventory exceeds 8 MiB");
|
|
5961
|
-
if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(
|
|
6132
|
+
if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
|
|
5962
6133
|
const paths = Buffer.concat(stdout).toString("utf8").split("\0").filter(Boolean).sort();
|
|
5963
6134
|
if (paths.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
|
|
5964
6135
|
const root = (0, import_path4.resolve)(sourceDir);
|
|
@@ -6003,7 +6174,7 @@ async function captureGitDiff(root, maxBytes) {
|
|
|
6003
6174
|
"workspace"
|
|
6004
6175
|
], { cwd: root, stdio: ["ignore", "pipe", "pipe"], shell: false });
|
|
6005
6176
|
const stdout = [];
|
|
6006
|
-
const
|
|
6177
|
+
const stderr2 = [];
|
|
6007
6178
|
let bytes = 0;
|
|
6008
6179
|
child.stdout.on("data", (chunk) => {
|
|
6009
6180
|
bytes += chunk.byteLength;
|
|
@@ -6011,7 +6182,7 @@ async function captureGitDiff(root, maxBytes) {
|
|
|
6011
6182
|
else stdout.push(chunk);
|
|
6012
6183
|
});
|
|
6013
6184
|
child.stderr.on("data", (chunk) => {
|
|
6014
|
-
if (
|
|
6185
|
+
if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
|
|
6015
6186
|
});
|
|
6016
6187
|
const code = await new Promise((accept, reject) => {
|
|
6017
6188
|
child.once("error", reject);
|
|
@@ -6019,7 +6190,7 @@ async function captureGitDiff(root, maxBytes) {
|
|
|
6019
6190
|
});
|
|
6020
6191
|
if (bytes > maxBytes) throw new Error(`patch exceeds ${maxBytes} bytes`);
|
|
6021
6192
|
if (code !== 0 && code !== 1) {
|
|
6022
|
-
throw new Error(`git diff failed: ${Buffer.concat(
|
|
6193
|
+
throw new Error(`git diff failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
|
|
6023
6194
|
}
|
|
6024
6195
|
return Buffer.concat(stdout).toString("utf8").replaceAll("a/baseline/", "a/").replaceAll("a/workspace/", "a/").replaceAll("b/baseline/", "b/").replaceAll("b/workspace/", "b/").replaceAll("--- a/baseline", "--- a").replaceAll("+++ b/workspace", "+++ b");
|
|
6025
6196
|
}
|
|
@@ -6823,11 +6994,11 @@ function rollup(graph, kind, options = {}) {
|
|
|
6823
6994
|
}
|
|
6824
6995
|
|
|
6825
6996
|
// ../graph/dist/code/index.js
|
|
6826
|
-
function
|
|
6997
|
+
function dirname9(path) {
|
|
6827
6998
|
const at = path.lastIndexOf("/");
|
|
6828
6999
|
return at <= 0 ? "." : path.slice(0, at);
|
|
6829
7000
|
}
|
|
6830
|
-
function
|
|
7001
|
+
function join13(base, specifier) {
|
|
6831
7002
|
const parts = [];
|
|
6832
7003
|
const segments = `${base === "." ? "" : `${base}/`}${specifier}`.split("/");
|
|
6833
7004
|
for (const segment of segments) {
|
|
@@ -6851,7 +7022,7 @@ var BARE_IMPORT = /^\s*import\s*["']([^"']+)["']/gm;
|
|
|
6851
7022
|
var isSourcePath = (path) => SOURCE.test(path);
|
|
6852
7023
|
function resolveImport(fromPath, specifier, known) {
|
|
6853
7024
|
if (!specifier.startsWith(".")) return null;
|
|
6854
|
-
const base =
|
|
7025
|
+
const base = join13(dirname9(fromPath), specifier);
|
|
6855
7026
|
const candidates = [
|
|
6856
7027
|
base,
|
|
6857
7028
|
base.replace(/\.js$/, ".ts"),
|
|
@@ -7382,13 +7553,13 @@ function gitApply(cwd, patch2, check) {
|
|
|
7382
7553
|
stdio: ["pipe", "ignore", "pipe"],
|
|
7383
7554
|
env: { PATH: process.env.PATH ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null" }
|
|
7384
7555
|
});
|
|
7385
|
-
let
|
|
7556
|
+
let stderr2 = "";
|
|
7386
7557
|
child.stderr.setEncoding("utf8");
|
|
7387
7558
|
child.stderr.on("data", (text3) => {
|
|
7388
|
-
if (
|
|
7559
|
+
if (stderr2.length < 4e3) stderr2 += text3.slice(0, 4e3);
|
|
7389
7560
|
});
|
|
7390
7561
|
child.once("error", reject);
|
|
7391
|
-
child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2,
|
|
7562
|
+
child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2, stderr2.trim().slice(0, 500)))));
|
|
7392
7563
|
child.stdin.end(patch2);
|
|
7393
7564
|
});
|
|
7394
7565
|
}
|
|
@@ -7501,7 +7672,7 @@ function execute(engine, args, name, recipe2, signal) {
|
|
|
7501
7672
|
const started = Date.now();
|
|
7502
7673
|
const child = (0, import_child_process5.spawn)(engine, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
7503
7674
|
const stdout = [];
|
|
7504
|
-
const
|
|
7675
|
+
const stderr2 = [];
|
|
7505
7676
|
let bytes = 0;
|
|
7506
7677
|
let outputLimitExceeded = false;
|
|
7507
7678
|
let timedOut = false;
|
|
@@ -7522,7 +7693,7 @@ function execute(engine, args, name, recipe2, signal) {
|
|
|
7522
7693
|
else target.push(chunk);
|
|
7523
7694
|
};
|
|
7524
7695
|
child.stdout.on("data", collect(stdout));
|
|
7525
|
-
child.stderr.on("data", collect(
|
|
7696
|
+
child.stderr.on("data", collect(stderr2));
|
|
7526
7697
|
const abort = () => stop("abort");
|
|
7527
7698
|
signal?.addEventListener("abort", abort, { once: true });
|
|
7528
7699
|
if (signal?.aborted) abort();
|
|
@@ -7538,7 +7709,7 @@ function execute(engine, args, name, recipe2, signal) {
|
|
|
7538
7709
|
accept({
|
|
7539
7710
|
exitCode: code ?? 1,
|
|
7540
7711
|
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
7541
|
-
stderr: Buffer.concat(
|
|
7712
|
+
stderr: Buffer.concat(stderr2).toString("utf8"),
|
|
7542
7713
|
durationMs: Date.now() - started,
|
|
7543
7714
|
outputLimitExceeded,
|
|
7544
7715
|
timedOut
|
|
@@ -7690,11 +7861,11 @@ function checkedResult(result, maximumOutputBytes) {
|
|
|
7690
7861
|
}
|
|
7691
7862
|
function boundedLogs(result, maximum) {
|
|
7692
7863
|
const stdout = Buffer.from(result.stdout);
|
|
7693
|
-
const
|
|
7864
|
+
const stderr2 = Buffer.from(result.stderr);
|
|
7694
7865
|
const first = stdout.subarray(0, maximum);
|
|
7695
7866
|
return {
|
|
7696
7867
|
stdout: first.toString("utf8"),
|
|
7697
|
-
stderr:
|
|
7868
|
+
stderr: stderr2.subarray(0, Math.max(0, maximum - first.byteLength)).toString("utf8")
|
|
7698
7869
|
};
|
|
7699
7870
|
}
|
|
7700
7871
|
function digestPolicy(policy) {
|
|
@@ -9764,7 +9935,7 @@ var CODE_BUILD_RECIPES = Object.freeze([{
|
|
|
9764
9935
|
// src/code-connect.ts
|
|
9765
9936
|
async function codeConnect(options) {
|
|
9766
9937
|
const cwd = options.cwd ?? process.cwd();
|
|
9767
|
-
const configPath = (0,
|
|
9938
|
+
const configPath = (0, import_node_path16.resolve)(cwd, options.configPath);
|
|
9768
9939
|
const cfg = (0, import_node_fs16.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
|
|
9769
9940
|
const requestedAppId = options.appId?.trim();
|
|
9770
9941
|
if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
|
|
@@ -9794,7 +9965,7 @@ async function codeConnect(options) {
|
|
|
9794
9965
|
const doFetch = options.fetch ?? fetch;
|
|
9795
9966
|
const engine = await (options.selectEngine ?? selectContainerEngine)(options.engine ?? "auto");
|
|
9796
9967
|
const hostPlatform = process.platform === "darwin" ? "macos" : "linux";
|
|
9797
|
-
const hostName = (options.name ?? (0,
|
|
9968
|
+
const hostName = (options.name ?? (0, import_node_os5.hostname)()).trim();
|
|
9798
9969
|
if (!hostName || hostName.length > 120) throw new Error("--name must contain 1 to 120 characters");
|
|
9799
9970
|
const repository = await inferGitHubRepository(cwd, options.readGitOrigin);
|
|
9800
9971
|
const localSource = await (options.prepareLocalSource ?? prepareCodeLocalSource)(
|
|
@@ -9832,8 +10003,8 @@ async function codeConnect(options) {
|
|
|
9832
10003
|
platform: hostPlatform,
|
|
9833
10004
|
arch: process.arch,
|
|
9834
10005
|
engines: [engine],
|
|
9835
|
-
cpuCount: (0,
|
|
9836
|
-
memoryBytes: (0,
|
|
10006
|
+
cpuCount: (0, import_node_os5.cpus)().length,
|
|
10007
|
+
memoryBytes: (0, import_node_os5.totalmem)(),
|
|
9837
10008
|
source: descriptor2,
|
|
9838
10009
|
images: {
|
|
9839
10010
|
ready: true,
|
|
@@ -10200,13 +10371,13 @@ async function codeCommand(parsed, dependencies) {
|
|
|
10200
10371
|
}
|
|
10201
10372
|
|
|
10202
10373
|
// src/operator-credentials.ts
|
|
10203
|
-
var
|
|
10374
|
+
var import_node_process16 = __toESM(require("process"), 1);
|
|
10204
10375
|
function developerTokenStatus(context, parsed, now = Date.now()) {
|
|
10205
10376
|
const cached = readJsonFile(context.cfg.local.tokenFile);
|
|
10206
10377
|
const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
|
|
10207
10378
|
const source = clean3(
|
|
10208
10379
|
stringOpt(parsed.options.token)
|
|
10209
|
-
) ? "flag" : clean3(
|
|
10380
|
+
) ? "flag" : clean3(import_node_process16.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
|
|
10210
10381
|
return {
|
|
10211
10382
|
source,
|
|
10212
10383
|
cacheFile: context.cfg.local.tokenFile,
|
|
@@ -10400,6 +10571,27 @@ async function credentialCommand(parsed, deps = {}) {
|
|
|
10400
10571
|
}
|
|
10401
10572
|
|
|
10402
10573
|
// src/help-usage.ts
|
|
10574
|
+
var AUTH_SECTION = `
|
|
10575
|
+
Enrol this machine once, then stop asking:
|
|
10576
|
+
npx odla-ai device enroll --all-apps --capability all --no-open --wait 600
|
|
10577
|
+
One browser approval. Afterwards every worktree on this machine mints its
|
|
10578
|
+
own short-lived credentials with nobody's attention, for every app you
|
|
10579
|
+
own \u2014 including apps you create later. The window rolls forward each time
|
|
10580
|
+
you use it, so continuous work never interrupts anyone; only a real gap
|
|
10581
|
+
does. Give the human the printed /studio?code= URL, keep the process
|
|
10582
|
+
alive, and wait on it.
|
|
10583
|
+
|
|
10584
|
+
npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600
|
|
10585
|
+
The same thing across all of odla, for weeks. Needs a platform
|
|
10586
|
+
administrator's approval \u2014 an app owner's cannot carry platform scopes.
|
|
10587
|
+
|
|
10588
|
+
npx odla-ai whoami what this machine holds and when it lapses
|
|
10589
|
+
npx odla-ai device list every machine you have enrolled
|
|
10590
|
+
|
|
10591
|
+
Enrollment is the only human decision here. Revoking a machine, purging an app,
|
|
10592
|
+
transferring ownership, and rotating credentials still need a signed-in human in
|
|
10593
|
+
Studio, and no machine credential can do them however wide its approval was.
|
|
10594
|
+
`;
|
|
10403
10595
|
var USAGE_SECTION = `
|
|
10404
10596
|
Start here:
|
|
10405
10597
|
odla-ai runbook ask "<question>" The current procedure, from odla's own
|
|
@@ -10438,7 +10630,7 @@ Usage:
|
|
|
10438
10630
|
odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
|
|
10439
10631
|
odla-ai pm project list [--app <product-id>] [--status <s>] [--json]
|
|
10440
10632
|
odla-ai pm project add --app <product-id> --name <name> [--description <text>] [--json]
|
|
10441
|
-
odla-ai pm project use <project-id> [--json] [saved
|
|
10633
|
+
odla-ai pm project use <project-id> [--json] [saved for this app, on this machine]
|
|
10442
10634
|
odla-ai pm goal list [--app <id>] [--project <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
10443
10635
|
odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
10444
10636
|
odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
@@ -10530,7 +10722,8 @@ Usage:
|
|
|
10530
10722
|
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
10531
10723
|
odla-ai security run [target] --self --ack-redacted-source
|
|
10532
10724
|
odla-ai provision [--live] [--config odla.config.mjs] [--email <odla-account>] [--request-grant] [--no-open] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
10533
|
-
odla-ai device enroll [--app <id>[,<id>...]] [--
|
|
10725
|
+
odla-ai device enroll [--all-apps|--app <id>[,<id>...]] [--capability all|<c>[,<c>...]] [--platform-wide]
|
|
10726
|
+
[--name <label>] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--wait <seconds>] [--json]
|
|
10534
10727
|
odla-ai device list [--email <odla-account>] [--json]
|
|
10535
10728
|
odla-ai device revoke <device-id> [--email <odla-account>] [--json]
|
|
10536
10729
|
odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
|
|
@@ -10544,8 +10737,11 @@ Usage:
|
|
|
10544
10737
|
|
|
10545
10738
|
// src/help.ts
|
|
10546
10739
|
function printHelp(output = console) {
|
|
10547
|
-
output.log(
|
|
10548
|
-
|
|
10740
|
+
output.log(helpText());
|
|
10741
|
+
}
|
|
10742
|
+
function helpText() {
|
|
10743
|
+
return `odla-ai
|
|
10744
|
+
${AUTH_SECTION}${USAGE_SECTION}
|
|
10549
10745
|
Commands:
|
|
10550
10746
|
auth Start a fresh, exact-project agent authorization for human review.
|
|
10551
10747
|
The email is the signed-in odla account, never git or GitHub
|
|
@@ -10618,8 +10814,9 @@ Commands:
|
|
|
10618
10814
|
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
10619
10815
|
pm Project management (via @odla-ai/pm): Products contain Projects;
|
|
10620
10816
|
projects contain goals, kanban tasks, decisions, and bugs. Use
|
|
10621
|
-
"pm project list|add|use"
|
|
10622
|
-
|
|
10817
|
+
"pm project list|add|use" selects a project for this app on this
|
|
10818
|
+
machine \u2014 every worktree shares it \u2014 or pass --app/--project
|
|
10819
|
+
explicitly. Same device-grant auth as "app".
|
|
10623
10820
|
Status changes and comments post to each item's @odla-ai/chat
|
|
10624
10821
|
discussion thread.
|
|
10625
10822
|
NOTE: "--column ready" is OWNER-ONLY. Creating a task in Ready
|
|
@@ -10648,9 +10845,20 @@ Commands:
|
|
|
10648
10845
|
platform Read canonical fleet health, releases, provider load/freshness,
|
|
10649
10846
|
explicit unknowns, and next actions through a read-only grant.
|
|
10650
10847
|
device Enrol THIS machine once, then stop asking. A human approves the
|
|
10651
|
-
enrollment in the browser; from then on
|
|
10652
|
-
own short-lived credentials
|
|
10653
|
-
attention, until the device
|
|
10848
|
+
enrollment in the browser; from then on EVERY worktree on this
|
|
10849
|
+
machine mints its own short-lived credentials with nobody's
|
|
10850
|
+
attention, until the device is revoked or goes unused.
|
|
10851
|
+
"--all-apps" covers every app you own, now and later, so creating
|
|
10852
|
+
an app costs no new approval. "--capability all" takes everything
|
|
10853
|
+
that approval is allowed to carry, so a capability you did not
|
|
10854
|
+
think to name is not a 403 next week. "--platform-wide" is the
|
|
10855
|
+
administrator's version, across all of odla.
|
|
10856
|
+
The expiry is a GAP, not a clock: each use rolls it forward, so
|
|
10857
|
+
only going quiet brings a human back into the loop \u2014 which is
|
|
10858
|
+
where anything that changed can be explained.
|
|
10859
|
+
"device list" shows what each machine holds and when it lapses;
|
|
10860
|
+
revoking one takes down every credential it ever minted, and is
|
|
10861
|
+
deliberately a signed-in human's decision in Studio.
|
|
10654
10862
|
provision Register services, compose integrations, persist credentials, optionally push secrets.
|
|
10655
10863
|
"provision --live --yes" initializes only the live instance of
|
|
10656
10864
|
an existing sandbox app and enables every configured service;
|
|
@@ -10746,7 +10954,46 @@ Safety:
|
|
|
10746
10954
|
Run security plan first to inspect the admin-selected providers, models,
|
|
10747
10955
|
per-route bounds, credential readiness, retention, no-execution boundary,
|
|
10748
10956
|
and digest that binds consent to that exact plan.
|
|
10749
|
-
|
|
10957
|
+
`;
|
|
10958
|
+
}
|
|
10959
|
+
|
|
10960
|
+
// src/help-command.ts
|
|
10961
|
+
function printCommandHelp(command, output = console) {
|
|
10962
|
+
const lines = helpText().split("\n");
|
|
10963
|
+
const usage = allBlocks(lines, new RegExp(`^ odla-ai ${escapeRe(command)}(\\s|$)`));
|
|
10964
|
+
const prose = block(lines, (line2) => new RegExp(`^ ${escapeRe(command)}\\s\\s+\\S`).test(line2));
|
|
10965
|
+
if (usage.length === 0 && prose.length === 0) {
|
|
10966
|
+
output.log(`odla-ai: no command "${command}". Run "odla-ai help" for all of them.`);
|
|
10967
|
+
return;
|
|
10968
|
+
}
|
|
10969
|
+
output.log([
|
|
10970
|
+
...prose.length ? [prose.join("\n"), ""] : [],
|
|
10971
|
+
...usage.length ? ["Usage:", ...usage, ""] : [],
|
|
10972
|
+
AUTH_SECTION.trimEnd()
|
|
10973
|
+
].join("\n"));
|
|
10974
|
+
}
|
|
10975
|
+
function allBlocks(lines, pattern) {
|
|
10976
|
+
const out = [];
|
|
10977
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10978
|
+
if (!pattern.test(lines[i])) continue;
|
|
10979
|
+
out.push(...block(lines.slice(i), (line2) => line2 === lines[i]));
|
|
10980
|
+
}
|
|
10981
|
+
return out;
|
|
10982
|
+
}
|
|
10983
|
+
function block(lines, starts) {
|
|
10984
|
+
const first = lines.findIndex(starts);
|
|
10985
|
+
if (first === -1) return [];
|
|
10986
|
+
const indent = lines[first].length - lines[first].trimStart().length;
|
|
10987
|
+
const out = [lines[first]];
|
|
10988
|
+
for (const line2 of lines.slice(first + 1)) {
|
|
10989
|
+
if (!line2.trim()) break;
|
|
10990
|
+
if (line2.length - line2.trimStart().length <= indent) break;
|
|
10991
|
+
out.push(line2);
|
|
10992
|
+
}
|
|
10993
|
+
return out;
|
|
10994
|
+
}
|
|
10995
|
+
function escapeRe(value2) {
|
|
10996
|
+
return value2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10750
10997
|
}
|
|
10751
10998
|
|
|
10752
10999
|
// src/discuss-principals.ts
|
|
@@ -11903,14 +12150,36 @@ async function pmWatch(ctx, parsed) {
|
|
|
11903
12150
|
}
|
|
11904
12151
|
|
|
11905
12152
|
// src/pm-project-context.ts
|
|
11906
|
-
var
|
|
11907
|
-
var
|
|
12153
|
+
var import_node_fs17 = require("fs");
|
|
12154
|
+
var import_node_path17 = require("path");
|
|
12155
|
+
var pmProjectContextFile = () => pmContextFile();
|
|
11908
12156
|
function readPmProjectContext(rootDir) {
|
|
11909
|
-
|
|
11910
|
-
|
|
12157
|
+
adoptLegacySelection(rootDir);
|
|
12158
|
+
const entries = Object.values(readSelections()).filter(isSelection);
|
|
12159
|
+
return entries.sort((a, b) => b.selectedAt.localeCompare(a.selectedAt))[0] ?? null;
|
|
11911
12160
|
}
|
|
11912
12161
|
function writePmProjectContext(rootDir, value2) {
|
|
11913
|
-
|
|
12162
|
+
adoptLegacySelection(rootDir);
|
|
12163
|
+
writePrivateJson(pmProjectContextFile(), {
|
|
12164
|
+
...readSelections(),
|
|
12165
|
+
[value2.appId]: { ...value2, selectedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
12166
|
+
});
|
|
12167
|
+
}
|
|
12168
|
+
function adoptLegacySelection(rootDir) {
|
|
12169
|
+
const legacy = (0, import_node_path17.resolve)(rootDir, ".odla", "pm-project.local.json");
|
|
12170
|
+
if (!(0, import_node_fs17.existsSync)(legacy)) return;
|
|
12171
|
+
const previous = readJsonFile(legacy);
|
|
12172
|
+
(0, import_node_fs17.rmSync)(legacy, { force: true });
|
|
12173
|
+
if (!isSelection(previous)) return;
|
|
12174
|
+
const selections = readSelections();
|
|
12175
|
+
if (selections[previous.appId]) return;
|
|
12176
|
+
writePrivateJson(pmProjectContextFile(), { ...selections, [previous.appId]: previous });
|
|
12177
|
+
}
|
|
12178
|
+
function readSelections() {
|
|
12179
|
+
return readJsonFile(pmProjectContextFile()) ?? {};
|
|
12180
|
+
}
|
|
12181
|
+
function isSelection(value2) {
|
|
12182
|
+
return !!value2 && typeof value2.appId === "string" && typeof value2.projectId === "string" && typeof value2.selectedAt === "string";
|
|
11914
12183
|
}
|
|
11915
12184
|
|
|
11916
12185
|
// src/pm-project-actions.ts
|
|
@@ -12935,7 +13204,7 @@ function percent(value2) {
|
|
|
12935
13204
|
// src/provision.ts
|
|
12936
13205
|
var import_apps13 = require("@odla-ai/apps");
|
|
12937
13206
|
var import_ai5 = require("@odla-ai/ai");
|
|
12938
|
-
var
|
|
13207
|
+
var import_node_process17 = __toESM(require("process"), 1);
|
|
12939
13208
|
|
|
12940
13209
|
// src/integration-provision.ts
|
|
12941
13210
|
var import_db3 = require("@odla-ai/db");
|
|
@@ -13373,7 +13642,7 @@ async function provision(options) {
|
|
|
13373
13642
|
await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
|
|
13374
13643
|
}
|
|
13375
13644
|
if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
13376
|
-
const key =
|
|
13645
|
+
const key = import_node_process17.default.env[cfg.ai.keyEnv];
|
|
13377
13646
|
if (key) {
|
|
13378
13647
|
const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
|
|
13379
13648
|
await (0, import_ai5.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
|
|
@@ -13414,8 +13683,8 @@ async function provision(options) {
|
|
|
13414
13683
|
}
|
|
13415
13684
|
|
|
13416
13685
|
// src/record.ts
|
|
13417
|
-
var
|
|
13418
|
-
var
|
|
13686
|
+
var import_node_fs18 = require("fs");
|
|
13687
|
+
var import_node_process18 = __toESM(require("process"), 1);
|
|
13419
13688
|
|
|
13420
13689
|
// src/surface.ts
|
|
13421
13690
|
var PM_ACTIONS = {
|
|
@@ -13597,7 +13866,7 @@ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
|
|
|
13597
13866
|
|
|
13598
13867
|
// src/record.ts
|
|
13599
13868
|
function recordInvocation(parsed) {
|
|
13600
|
-
const file =
|
|
13869
|
+
const file = import_node_process18.default.env.ODLA_CLI_RECORD;
|
|
13601
13870
|
if (!file) return;
|
|
13602
13871
|
try {
|
|
13603
13872
|
const entry = {
|
|
@@ -13605,7 +13874,7 @@ function recordInvocation(parsed) {
|
|
|
13605
13874
|
options: Object.entries(parsed.options).map(([name, value2]) => value2 === false ? `no-${name}` : name).sort()
|
|
13606
13875
|
};
|
|
13607
13876
|
if (!entry.path.length) return;
|
|
13608
|
-
(0,
|
|
13877
|
+
(0, import_node_fs18.appendFileSync)(file, `${JSON.stringify(entry)}
|
|
13609
13878
|
`);
|
|
13610
13879
|
} catch {
|
|
13611
13880
|
}
|
|
@@ -13634,6 +13903,9 @@ function renderAdvisories(out, advisories, env = process.env) {
|
|
|
13634
13903
|
}
|
|
13635
13904
|
}
|
|
13636
13905
|
|
|
13906
|
+
// src/device-command.ts
|
|
13907
|
+
var import_db4 = require("@odla-ai/db");
|
|
13908
|
+
|
|
13637
13909
|
// src/device-ttl.ts
|
|
13638
13910
|
var OWNER_DEVICE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
13639
13911
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -13653,10 +13925,26 @@ function parseDeviceTtl(raw) {
|
|
|
13653
13925
|
}
|
|
13654
13926
|
|
|
13655
13927
|
// src/device-command.ts
|
|
13656
|
-
var
|
|
13657
|
-
var
|
|
13658
|
-
var
|
|
13928
|
+
var import_node_fs19 = require("fs");
|
|
13929
|
+
var import_node_path18 = require("path");
|
|
13930
|
+
var import_node_process19 = __toESM(require("process"), 1);
|
|
13659
13931
|
async function deviceCommand(parsed, deps) {
|
|
13932
|
+
assertArgs(parsed, [
|
|
13933
|
+
"app",
|
|
13934
|
+
"all-apps",
|
|
13935
|
+
"platform-wide",
|
|
13936
|
+
"name",
|
|
13937
|
+
"capability",
|
|
13938
|
+
"device-ttl",
|
|
13939
|
+
"email",
|
|
13940
|
+
"open",
|
|
13941
|
+
"json",
|
|
13942
|
+
"config",
|
|
13943
|
+
"token",
|
|
13944
|
+
"context",
|
|
13945
|
+
"platform",
|
|
13946
|
+
"wait"
|
|
13947
|
+
], 3);
|
|
13660
13948
|
const action2 = parsed.positionals[1] ?? "";
|
|
13661
13949
|
const out = deps.stdout ?? console;
|
|
13662
13950
|
const doFetch = deps.fetch ?? fetch;
|
|
@@ -13669,10 +13957,12 @@ async function deviceCommand(parsed, deps) {
|
|
|
13669
13957
|
}
|
|
13670
13958
|
async function enroll(parsed, deps, cfg, doFetch, out, json) {
|
|
13671
13959
|
const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
|
|
13672
|
-
const
|
|
13673
|
-
|
|
13960
|
+
const platformWide = parsed.options["platform-wide"] === true;
|
|
13961
|
+
const apps = platformWide || parsed.options["all-apps"] === true ? [import_db4.ALL_OWNED_APPS] : (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
|
|
13962
|
+
if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026], or --all-apps");
|
|
13674
13963
|
const deviceTtlMs = parseDeviceTtl(parsed.options["device-ttl"]);
|
|
13675
|
-
const extended = deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
|
|
13964
|
+
const extended = platformWide || deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
|
|
13965
|
+
const { capabilities, scopes } = requestedEnvelope(parsed, platformWide);
|
|
13676
13966
|
const token = await scopedToken2(
|
|
13677
13967
|
parsed,
|
|
13678
13968
|
deps,
|
|
@@ -13687,9 +13977,10 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
|
|
|
13687
13977
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
13688
13978
|
body: JSON.stringify({
|
|
13689
13979
|
name,
|
|
13690
|
-
platform:
|
|
13980
|
+
platform: import_node_process19.default.platform,
|
|
13691
13981
|
appIds: apps,
|
|
13692
|
-
...
|
|
13982
|
+
...capabilities ? { capabilities } : {},
|
|
13983
|
+
...scopes ? { scopes } : {},
|
|
13693
13984
|
...deviceTtlMs === void 0 ? {} : { deviceTtlMs }
|
|
13694
13985
|
})
|
|
13695
13986
|
});
|
|
@@ -13698,20 +13989,53 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
|
|
|
13698
13989
|
throw new Error(`device enroll failed: ${body.error?.message ?? `registry returned ${response2.status}`} (${response2.status})`);
|
|
13699
13990
|
}
|
|
13700
13991
|
const path = deviceCredentialPath();
|
|
13701
|
-
(0,
|
|
13702
|
-
(0,
|
|
13992
|
+
(0, import_node_fs19.mkdirSync)((0, import_node_path18.dirname)(path), { recursive: true });
|
|
13993
|
+
(0, import_node_fs19.writeFileSync)(path, JSON.stringify({
|
|
13703
13994
|
token: body.token,
|
|
13704
13995
|
platform: cfg.platformUrl.replace(/\/$/, ""),
|
|
13705
13996
|
deviceId: body.device.deviceId,
|
|
13706
13997
|
name
|
|
13707
13998
|
}, null, 2));
|
|
13708
|
-
(0,
|
|
13709
|
-
|
|
13710
|
-
|
|
13999
|
+
(0, import_node_fs19.chmodSync)(path, 384);
|
|
14000
|
+
rememberMachineIdentity(cfg.platformUrl.replace(/\/$/, ""), stringOpt(parsed.options.email));
|
|
14001
|
+
const reach = body.device.appIds.includes(import_db4.ALL_OWNED_APPS) ? "every app you own, now and later" : body.device.appIds.join(", ");
|
|
14002
|
+
out.error(`device: enrolled "${name}" for ${reach}; credential written to ${path}`);
|
|
14003
|
+
out.error(
|
|
14004
|
+
"device: every worktree on this machine mints its own credentials from now on \u2014 no further approvals,"
|
|
14005
|
+
);
|
|
14006
|
+
out.error(
|
|
14007
|
+
`device: and the clock resets each time you use it. Going quiet for ${describeWindow(body.device.expiresAt)} is what ends it.`
|
|
14008
|
+
);
|
|
13711
14009
|
if (json) {
|
|
13712
|
-
out.log(JSON.stringify({
|
|
14010
|
+
out.log(JSON.stringify({
|
|
14011
|
+
deviceId: body.device.deviceId,
|
|
14012
|
+
name,
|
|
14013
|
+
appIds: body.device.appIds,
|
|
14014
|
+
capabilities: body.device.capabilities ?? [],
|
|
14015
|
+
scopes: body.device.scopes ?? [],
|
|
14016
|
+
expiresAt: body.device.expiresAt,
|
|
14017
|
+
hardExpiresAt: body.device.hardExpiresAt ?? null
|
|
14018
|
+
}, null, 2));
|
|
13713
14019
|
}
|
|
13714
14020
|
}
|
|
14021
|
+
function requestedEnvelope(parsed, platformWide) {
|
|
14022
|
+
const raw = stringOpt(parsed.options.capability);
|
|
14023
|
+
const everything = platformWide || raw?.trim().toLowerCase() === "all";
|
|
14024
|
+
if (everything) {
|
|
14025
|
+
return {
|
|
14026
|
+
capabilities: [...import_db4.OPTIONAL_AGENT_PROJECT_CAPABILITIES],
|
|
14027
|
+
scopes: platformWide ? [...import_db4.ADMIN_DEVICE_SCOPES] : [...import_db4.OWNER_DEVICE_SCOPES]
|
|
14028
|
+
};
|
|
14029
|
+
}
|
|
14030
|
+
const named = raw?.split(",").map((c) => c.trim()).filter(Boolean);
|
|
14031
|
+
return named?.length ? { capabilities: named } : {};
|
|
14032
|
+
}
|
|
14033
|
+
function describeWindow(expiresAt, now = Date.now()) {
|
|
14034
|
+
const days = Math.max(1, Math.round((expiresAt - now) / (24 * 60 * 60 * 1e3)));
|
|
14035
|
+
if (days >= 365) return `${Math.round(days / 365)} year${days >= 730 ? "s" : ""}`;
|
|
14036
|
+
if (days % 7 === 0) return `${days / 7} week${days > 7 ? "s" : ""}`;
|
|
14037
|
+
return `${days} day${days === 1 ? "" : "s"}`;
|
|
14038
|
+
}
|
|
13715
14039
|
async function list2(parsed, deps, cfg, doFetch, out, json) {
|
|
13716
14040
|
const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device list)");
|
|
13717
14041
|
const response2 = await doFetch(`${cfg.platformUrl}/registry/devices`, {
|
|
@@ -13725,12 +14049,17 @@ async function list2(parsed, deps, cfg, doFetch, out, json) {
|
|
|
13725
14049
|
if (body.devices.length === 0) return out.log("no enrolled devices");
|
|
13726
14050
|
for (const device of body.devices) {
|
|
13727
14051
|
const state2 = device.revokedAt ? "revoked" : device.expiresAt <= Date.now() ? "expired" : "active";
|
|
13728
|
-
|
|
14052
|
+
const reach = device.appIds.includes("*") ? "every app you own" : device.appIds.join(", ");
|
|
14053
|
+
const gap = state2 === "active" ? ` idle ${describeWindow(device.expiresAt)} left` : "";
|
|
14054
|
+
out.log(`${device.deviceId} ${state2.padEnd(7)} ${device.name} [${reach}]${gap}`);
|
|
13729
14055
|
}
|
|
13730
14056
|
}
|
|
13731
14057
|
async function revoke(parsed, deps, cfg, doFetch, out, json) {
|
|
13732
14058
|
const deviceId = parsed.positionals[2];
|
|
13733
14059
|
if (!deviceId) throw new Error("device revoke needs the device id from `odla-ai device list`");
|
|
14060
|
+
out.error(
|
|
14061
|
+
`device: revoking is a signed-in human's decision; if this is refused, open ${cfg.platformUrl}/studio and revoke it there.`
|
|
14062
|
+
);
|
|
13734
14063
|
const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device revoke)");
|
|
13735
14064
|
const response2 = await doFetch(`${cfg.platformUrl}/registry/devices/${encodeURIComponent(deviceId)}/revoke`, {
|
|
13736
14065
|
method: "POST",
|
|
@@ -13749,7 +14078,7 @@ async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app
|
|
|
13749
14078
|
// A device is granted the apps named in ONE approval, so --app is a list here.
|
|
13750
14079
|
allowAppList: true
|
|
13751
14080
|
});
|
|
13752
|
-
const
|
|
14081
|
+
const scopedTokenFile2 = credentials.scopedTokenFile;
|
|
13753
14082
|
return getScopedPlatformToken({
|
|
13754
14083
|
platform: cfg.platformUrl,
|
|
13755
14084
|
scope,
|
|
@@ -13760,16 +14089,16 @@ async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app
|
|
|
13760
14089
|
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
13761
14090
|
openApprovalUrl: deps.openUrl,
|
|
13762
14091
|
rootDir: cfg.rootDir,
|
|
13763
|
-
tokenFile:
|
|
14092
|
+
tokenFile: scopedTokenFile2,
|
|
13764
14093
|
...stringOpt(parsed.options.token) ? { token: stringOpt(parsed.options.token) } : {}
|
|
13765
14094
|
});
|
|
13766
14095
|
}
|
|
13767
14096
|
function defaultDeviceName() {
|
|
13768
|
-
return `${
|
|
14097
|
+
return `${import_node_process19.default.env.HOSTNAME ?? import_node_process19.default.env.HOST ?? "machine"}-${import_node_process19.default.platform}`;
|
|
13769
14098
|
}
|
|
13770
14099
|
|
|
13771
14100
|
// src/runbook-actions.ts
|
|
13772
|
-
var
|
|
14101
|
+
var import_node_fs20 = require("fs");
|
|
13773
14102
|
|
|
13774
14103
|
// src/runbook-requires.ts
|
|
13775
14104
|
var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
|
|
@@ -13865,7 +14194,7 @@ async function bySlug(ctx, slug) {
|
|
|
13865
14194
|
function readBody(file, inline) {
|
|
13866
14195
|
if (inline !== void 0) return inline;
|
|
13867
14196
|
if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
|
|
13868
|
-
return (0,
|
|
14197
|
+
return (0, import_node_fs20.readFileSync)(file === "-" ? 0 : file, "utf8");
|
|
13869
14198
|
}
|
|
13870
14199
|
var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
|
|
13871
14200
|
async function runbookList(ctx, all, query) {
|
|
@@ -13957,8 +14286,8 @@ async function runbookRemove(ctx, slug) {
|
|
|
13957
14286
|
}
|
|
13958
14287
|
|
|
13959
14288
|
// src/runbook-import.ts
|
|
13960
|
-
var
|
|
13961
|
-
var
|
|
14289
|
+
var import_node_fs21 = require("fs");
|
|
14290
|
+
var import_node_path19 = require("path");
|
|
13962
14291
|
function parseRunbook(text3, slug) {
|
|
13963
14292
|
let rest = text3;
|
|
13964
14293
|
const meta = {};
|
|
@@ -13983,12 +14312,12 @@ function parseRunbook(text3, slug) {
|
|
|
13983
14312
|
};
|
|
13984
14313
|
}
|
|
13985
14314
|
function readRunbookDir(dir) {
|
|
13986
|
-
if (!(0,
|
|
13987
|
-
const files = (0,
|
|
14315
|
+
if (!(0, import_node_fs21.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
|
|
14316
|
+
const files = (0, import_node_fs21.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
|
|
13988
14317
|
if (!files.length) throw new Error(`no .md files in ${dir}`);
|
|
13989
14318
|
return files.map((file) => {
|
|
13990
|
-
const slug = (0,
|
|
13991
|
-
const parsed = parseRunbook((0,
|
|
14319
|
+
const slug = (0, import_node_path19.basename)(file, ".md");
|
|
14320
|
+
const parsed = parseRunbook((0, import_node_fs21.readFileSync)((0, import_node_path19.join)(dir, file), "utf8"), slug);
|
|
13992
14321
|
return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
|
|
13993
14322
|
});
|
|
13994
14323
|
}
|
|
@@ -14056,8 +14385,8 @@ async function upsert(ctx, r, visibility) {
|
|
|
14056
14385
|
|
|
14057
14386
|
// src/runbook-impact.ts
|
|
14058
14387
|
var import_node_child_process6 = require("child_process");
|
|
14059
|
-
var
|
|
14060
|
-
var
|
|
14388
|
+
var import_node_fs22 = require("fs");
|
|
14389
|
+
var import_node_path20 = require("path");
|
|
14061
14390
|
|
|
14062
14391
|
// src/runbook-impact-scan.ts
|
|
14063
14392
|
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$]*)/;
|
|
@@ -14226,10 +14555,10 @@ ${body.split("\n").map((line2) => `+${line2}`).join("\n")}
|
|
|
14226
14555
|
}
|
|
14227
14556
|
function manifestLabeller(root) {
|
|
14228
14557
|
return (workspace) => {
|
|
14229
|
-
const manifest = (0,
|
|
14230
|
-
if (!(0,
|
|
14558
|
+
const manifest = (0, import_node_path20.join)(root, workspace, "package.json");
|
|
14559
|
+
if (!(0, import_node_fs22.existsSync)(manifest)) return void 0;
|
|
14231
14560
|
try {
|
|
14232
|
-
const name = JSON.parse((0,
|
|
14561
|
+
const name = JSON.parse((0, import_node_fs22.readFileSync)(manifest, "utf8")).name;
|
|
14233
14562
|
return typeof name === "string" ? name : void 0;
|
|
14234
14563
|
} catch {
|
|
14235
14564
|
return void 0;
|
|
@@ -14296,7 +14625,7 @@ function report4(ctx, impacts) {
|
|
|
14296
14625
|
async function runbookImpact(ctx, options, deps = {}) {
|
|
14297
14626
|
const cwd = deps.cwd ?? process.cwd();
|
|
14298
14627
|
const runGit = deps.runGit ?? gitRunner(cwd);
|
|
14299
|
-
const read3 = deps.readRepoFile ?? ((path) => (0,
|
|
14628
|
+
const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs22.readFileSync)((0, import_node_path20.join)(cwd, path), "utf8"));
|
|
14300
14629
|
const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
|
|
14301
14630
|
if (!surfaces.length) {
|
|
14302
14631
|
return ctx.out.log(
|
|
@@ -14423,12 +14752,12 @@ async function runbookComment(ctx, slug, body) {
|
|
|
14423
14752
|
|
|
14424
14753
|
// src/runbook-editor.ts
|
|
14425
14754
|
var import_node_child_process7 = require("child_process");
|
|
14426
|
-
var
|
|
14427
|
-
var
|
|
14428
|
-
var
|
|
14429
|
-
var
|
|
14755
|
+
var import_node_fs23 = require("fs");
|
|
14756
|
+
var import_node_os6 = require("os");
|
|
14757
|
+
var import_node_path21 = require("path");
|
|
14758
|
+
var import_node_process20 = __toESM(require("process"), 1);
|
|
14430
14759
|
var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
|
|
14431
|
-
function resolveEditor(env =
|
|
14760
|
+
function resolveEditor(env = import_node_process20.default.env) {
|
|
14432
14761
|
for (const name of EDITOR_ENV) {
|
|
14433
14762
|
const value2 = env[name];
|
|
14434
14763
|
if (value2 && value2.trim()) return value2.trim();
|
|
@@ -14442,8 +14771,8 @@ function defaultRun(command, path) {
|
|
|
14442
14771
|
return result.status ?? 0;
|
|
14443
14772
|
}
|
|
14444
14773
|
function editText(initial, slug, deps = {}) {
|
|
14445
|
-
const env = deps.env ??
|
|
14446
|
-
const interactive = deps.interactive ?? (() => Boolean(
|
|
14774
|
+
const env = deps.env ?? import_node_process20.default.env;
|
|
14775
|
+
const interactive = deps.interactive ?? (() => Boolean(import_node_process20.default.stdin.isTTY));
|
|
14447
14776
|
const editor = resolveEditor(env);
|
|
14448
14777
|
if (!editor)
|
|
14449
14778
|
throw new Error(
|
|
@@ -14451,16 +14780,16 @@ function editText(initial, slug, deps = {}) {
|
|
|
14451
14780
|
);
|
|
14452
14781
|
if (!interactive())
|
|
14453
14782
|
throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
|
|
14454
|
-
const dir = (0,
|
|
14455
|
-
const file = (0,
|
|
14783
|
+
const dir = (0, import_node_fs23.mkdtempSync)((0, import_node_path21.join)((0, import_node_os6.tmpdir)(), "odla-runbook-"));
|
|
14784
|
+
const file = (0, import_node_path21.join)(dir, `${slug}.md`);
|
|
14456
14785
|
try {
|
|
14457
|
-
(0,
|
|
14786
|
+
(0, import_node_fs23.writeFileSync)(file, initial, { mode: 384 });
|
|
14458
14787
|
const code = defaultRunOrInjected(deps)(editor, file);
|
|
14459
14788
|
if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
|
|
14460
|
-
const edited = (0,
|
|
14789
|
+
const edited = (0, import_node_fs23.readFileSync)(file, "utf8");
|
|
14461
14790
|
return edited === initial ? null : edited;
|
|
14462
14791
|
} finally {
|
|
14463
|
-
(0,
|
|
14792
|
+
(0, import_node_fs23.rmSync)(dir, { recursive: true, force: true });
|
|
14464
14793
|
}
|
|
14465
14794
|
}
|
|
14466
14795
|
var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
|
|
@@ -14798,7 +15127,7 @@ function hostedSeverity(value2, flag) {
|
|
|
14798
15127
|
var import_security2 = require("@odla-ai/security");
|
|
14799
15128
|
|
|
14800
15129
|
// src/security.ts
|
|
14801
|
-
var
|
|
15130
|
+
var import_node_path22 = require("path");
|
|
14802
15131
|
var import_security = require("@odla-ai/security");
|
|
14803
15132
|
var import_node3 = require("@odla-ai/security/node");
|
|
14804
15133
|
async function runHostedSecurity(options) {
|
|
@@ -14810,9 +15139,9 @@ async function runHostedSecurity(options) {
|
|
|
14810
15139
|
const appId = selfAudit ? "odla-ai" : cfg.app.id;
|
|
14811
15140
|
const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
|
|
14812
15141
|
const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
|
|
14813
|
-
const target = (0,
|
|
14814
|
-
const output = (0,
|
|
14815
|
-
const outputRelative = (0,
|
|
15142
|
+
const target = (0, import_node_path22.resolve)(options.target ?? cfg?.rootDir ?? ".");
|
|
15143
|
+
const output = (0, import_node_path22.resolve)(options.out ?? (0, import_node_path22.resolve)(target, ".odla/security/hosted"));
|
|
15144
|
+
const outputRelative = (0, import_node_path22.relative)(target, output).split(import_node_path22.sep).join("/");
|
|
14816
15145
|
if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
|
|
14817
15146
|
const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
|
|
14818
15147
|
const tokenRequest = {
|
|
@@ -14824,7 +15153,7 @@ async function runHostedSecurity(options) {
|
|
|
14824
15153
|
};
|
|
14825
15154
|
const token = await injectedToken(options, tokenRequest);
|
|
14826
15155
|
const snapshot = await (0, import_node3.snapshotDirectory)(target, {
|
|
14827
|
-
exclude: !outputRelative.startsWith("../") && !(0,
|
|
15156
|
+
exclude: !outputRelative.startsWith("../") && !(0, import_node_path22.isAbsolute)(outputRelative) ? [outputRelative] : []
|
|
14828
15157
|
});
|
|
14829
15158
|
const hosted = await (0, import_security.createPlatformSecurityReasoners)({
|
|
14830
15159
|
platform,
|
|
@@ -14842,7 +15171,7 @@ async function runHostedSecurity(options) {
|
|
|
14842
15171
|
});
|
|
14843
15172
|
const harness = (0, import_security.createSecurityHarness)({
|
|
14844
15173
|
profile,
|
|
14845
|
-
store: new import_node3.FileRunStore((0,
|
|
15174
|
+
store: new import_node3.FileRunStore((0, import_node_path22.resolve)(output, "state")),
|
|
14846
15175
|
discoveryReasoner: hosted.discoveryReasoner,
|
|
14847
15176
|
validationReasoner: hosted.validationReasoner,
|
|
14848
15177
|
policy: {
|
|
@@ -14866,7 +15195,7 @@ async function runHostedSecurity(options) {
|
|
|
14866
15195
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
14867
15196
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
14868
15197
|
if (!env || !declared.includes(env)) {
|
|
14869
|
-
const shown = (0,
|
|
15198
|
+
const shown = (0, import_node_path22.relative)(rootDir, configPath) || configPath;
|
|
14870
15199
|
throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
|
|
14871
15200
|
}
|
|
14872
15201
|
return env;
|
|
@@ -14895,7 +15224,7 @@ function printSummary(out, appId, env, run, report5, output) {
|
|
|
14895
15224
|
out.log(` coverage: ${report5.coverageStatus} ${complete}/${report5.coverage.length} blocked=${report5.metrics.blockedCells} shallow=${report5.metrics.shallowCells} unscheduled=${report5.metrics.unscheduledCells} budget_exhausted=${report5.metrics.budgetExhaustedCells}`);
|
|
14896
15225
|
if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
|
|
14897
15226
|
out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
|
|
14898
|
-
out.log(` report: ${(0,
|
|
15227
|
+
out.log(` report: ${(0, import_node_path22.resolve)(output, "REPORT.md")}`);
|
|
14899
15228
|
}
|
|
14900
15229
|
function formatBudget(usage) {
|
|
14901
15230
|
return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
|
|
@@ -15376,8 +15705,10 @@ async function dispatchCli(argv, dependencies) {
|
|
|
15376
15705
|
return;
|
|
15377
15706
|
}
|
|
15378
15707
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
15379
|
-
assertArgs(parsed, ["help"],
|
|
15380
|
-
|
|
15708
|
+
assertArgs(parsed, ["help"], 2);
|
|
15709
|
+
const topic = parsed.positionals[1];
|
|
15710
|
+
if (topic) printCommandHelp(topic, runtime.stdout);
|
|
15711
|
+
else printHelp(runtime.stdout);
|
|
15381
15712
|
return;
|
|
15382
15713
|
}
|
|
15383
15714
|
if (command === "whoami") {
|