@odla-ai/cli 0.38.3 → 0.40.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 +724 -293
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-WJAN5CZ2.js → chunk-2IHE5U3M.js} +596 -215
- package/dist/chunk-2IHE5U3M.js.map +1 -0
- package/dist/{cli-A5R2DMM4.js → cli-LRJBESE6.js} +2 -2
- package/dist/index.cjs +630 -254
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/skills/odla/SKILL.md +51 -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-LRJBESE6.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,76 @@ 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
|
+
if (env.VITEST && !env.ODLA_HOME) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
"ODLA_HOME must be set under test \u2014 resolving the real ~/.odla would write to the developer's machine"
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
return env.ODLA_HOME ?? (0, import_node_path3.join)(env.HOME ?? (0, import_node_os2.homedir)(), ".odla");
|
|
304
|
+
}
|
|
305
|
+
function odlaHomePath(segments, env = import_node_process5.default.env) {
|
|
306
|
+
return (0, import_node_path3.join)(odlaHome(env), ...segments);
|
|
307
|
+
}
|
|
308
|
+
function identityFile(env) {
|
|
309
|
+
return odlaHomePath(["identity.json"], env);
|
|
310
|
+
}
|
|
311
|
+
function deviceSessionFile(env) {
|
|
312
|
+
return odlaHomePath(["session.json"], env);
|
|
313
|
+
}
|
|
314
|
+
function appTokenFile(appId, env) {
|
|
315
|
+
return odlaHomePath(["apps", safeSegment(appId), "dev-token.json"], env);
|
|
316
|
+
}
|
|
317
|
+
function appCredentialsFile(appId, env) {
|
|
318
|
+
return odlaHomePath(["apps", safeSegment(appId), "credentials.json"], env);
|
|
319
|
+
}
|
|
320
|
+
function scopedTokenFile(env) {
|
|
321
|
+
return odlaHomePath(["admin-token.local.json"], env);
|
|
322
|
+
}
|
|
323
|
+
function pmContextFile(env) {
|
|
324
|
+
return odlaHomePath(["pm-context.json"], env);
|
|
325
|
+
}
|
|
326
|
+
function adoptRepoLocalCache(legacyPath, machinePath, out) {
|
|
327
|
+
if (!(0, import_node_fs3.existsSync)(legacyPath) || legacyPath === machinePath) return false;
|
|
328
|
+
const superseded2 = (0, import_node_fs3.existsSync)(machinePath);
|
|
329
|
+
if (!superseded2) {
|
|
330
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(machinePath), { recursive: true });
|
|
331
|
+
(0, import_node_fs3.copyFileSync)(legacyPath, machinePath);
|
|
332
|
+
(0, import_node_fs3.chmodSync)(machinePath, 384);
|
|
333
|
+
}
|
|
334
|
+
(0, import_node_fs3.rmSync)(legacyPath, { force: true });
|
|
335
|
+
out?.error(
|
|
336
|
+
superseded2 ? `auth: removed superseded ${legacyPath}; this machine's credentials live in ${odlaHome()}` : `auth: moved ${legacyPath} into ${machinePath}; credentials are per machine now, not per worktree`
|
|
337
|
+
);
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
function safeSegment(value2) {
|
|
341
|
+
const clean4 = value2.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "");
|
|
342
|
+
if (!clean4) throw new Error(`"${value2}" is not a usable app id`);
|
|
343
|
+
return clean4;
|
|
297
344
|
}
|
|
298
345
|
|
|
299
346
|
// src/local.ts
|
|
300
347
|
var import_node_fs4 = require("fs");
|
|
301
|
-
var
|
|
348
|
+
var import_node_path4 = require("path");
|
|
302
349
|
var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
|
|
303
350
|
function readJsonFile(path) {
|
|
304
351
|
try {
|
|
@@ -345,7 +392,7 @@ function mergeCredential(current, update) {
|
|
|
345
392
|
return next;
|
|
346
393
|
}
|
|
347
394
|
function ensureGitignore(rootDir, localPaths = []) {
|
|
348
|
-
const path = (0,
|
|
395
|
+
const path = (0, import_node_path4.resolve)(rootDir, ".gitignore");
|
|
349
396
|
const existing = (0, import_node_fs4.existsSync)(path) ? (0, import_node_fs4.readFileSync)(path, "utf8") : "";
|
|
350
397
|
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
|
|
351
398
|
const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
|
|
@@ -366,7 +413,7 @@ function o11yDevVars(cfg) {
|
|
|
366
413
|
function resolveWriteDevVarsTarget(cfg, requested) {
|
|
367
414
|
if (!requested) return null;
|
|
368
415
|
if (requested === true) return cfg.local.devVarsFile;
|
|
369
|
-
return (0,
|
|
416
|
+
return (0, import_node_path4.resolve)((0, import_node_path4.dirname)(cfg.configPath), requested);
|
|
370
417
|
}
|
|
371
418
|
function writeDevVars(path, credentials, env, o11y) {
|
|
372
419
|
const entry = credentials.envs[env];
|
|
@@ -406,22 +453,124 @@ function isManagedDevVar(line2) {
|
|
|
406
453
|
return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
|
|
407
454
|
}
|
|
408
455
|
function writePrivateText(path, text3) {
|
|
409
|
-
(0, import_node_fs4.mkdirSync)((0,
|
|
456
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
410
457
|
const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
411
458
|
(0, import_node_fs4.writeFileSync)(temporary, text3, { mode: 384 });
|
|
412
459
|
(0, import_node_fs4.chmodSync)(temporary, 384);
|
|
413
460
|
(0, import_node_fs4.renameSync)(temporary, path);
|
|
414
461
|
}
|
|
415
462
|
function gitignoreEntry(rootDir, path) {
|
|
416
|
-
const rel = (0,
|
|
417
|
-
if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0,
|
|
463
|
+
const rel = (0, import_node_path4.relative)((0, import_node_path4.resolve)(rootDir), (0, import_node_path4.resolve)(path));
|
|
464
|
+
if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path4.isAbsolute)(rel)) return null;
|
|
418
465
|
return rel.replaceAll("\\", "/");
|
|
419
466
|
}
|
|
420
467
|
function displayPath(path, rootDir = process.cwd()) {
|
|
421
|
-
const rel = (0,
|
|
468
|
+
const rel = (0, import_node_path4.relative)(rootDir, path);
|
|
422
469
|
return rel && !rel.startsWith("..") ? rel : path;
|
|
423
470
|
}
|
|
424
471
|
|
|
472
|
+
// src/auth-guidance.ts
|
|
473
|
+
var ENROL_EVERYTHING = "npx odla-ai device enroll --no-open --wait 600";
|
|
474
|
+
var ENROL_PLATFORM_WIDE = "npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600";
|
|
475
|
+
function machineAuthState(audience, env = import_node_process6.default.env) {
|
|
476
|
+
const device = readDeviceCredential(audience, env);
|
|
477
|
+
if (!device) return { enrolled: false };
|
|
478
|
+
const session = readJsonFile(deviceSessionFile(env));
|
|
479
|
+
const current = session?.platform === audience && session.deviceId === device.deviceId ? session : void 0;
|
|
480
|
+
return {
|
|
481
|
+
enrolled: true,
|
|
482
|
+
...device.name ? { deviceName: device.name } : {},
|
|
483
|
+
...current?.appIds ? { appIds: current.appIds } : {},
|
|
484
|
+
...current?.capabilities ? { capabilities: current.capabilities } : {},
|
|
485
|
+
...current?.scopes ? { scopes: current.scopes } : {},
|
|
486
|
+
...current?.deviceExpiresAt ? { lapsesAt: current.deviceExpiresAt } : {}
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function scopeInterruptionNotice(scope, state2) {
|
|
490
|
+
const platformScope = scope.startsWith("platform:");
|
|
491
|
+
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}"`;
|
|
492
|
+
return [
|
|
493
|
+
`odla: ${reason}.`,
|
|
494
|
+
` Approve this one now, then end the interruptions with:`,
|
|
495
|
+
` ${platformScope ? ENROL_PLATFORM_WIDE : ENROL_EVERYTHING}`,
|
|
496
|
+
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."
|
|
497
|
+
].join("\n");
|
|
498
|
+
}
|
|
499
|
+
function narrowEnrollmentNotice(envelope) {
|
|
500
|
+
const everyApp = envelope.appIds.includes("*");
|
|
501
|
+
if (everyApp && envelope.capabilities.length > 0 && envelope.scopes.length > 0) return null;
|
|
502
|
+
const missing = [
|
|
503
|
+
everyApp ? null : `only ${envelope.appIds.length} app${envelope.appIds.length === 1 ? "" : "s"} \u2014 a new one will need a new approval`,
|
|
504
|
+
envelope.capabilities.length ? null : "no optional capabilities \u2014 app.manage, crm.read and code.session are not included",
|
|
505
|
+
envelope.scopes.length ? null : "no platform scopes \u2014 runbook edits, config plans and host connect will each ask again"
|
|
506
|
+
].filter(Boolean);
|
|
507
|
+
return [
|
|
508
|
+
`odla: this is a NARROW enrollment: ${missing.join("; ")}.`,
|
|
509
|
+
" That is a fine choice if you meant it. If you did not, drop the flags:",
|
|
510
|
+
` ${ENROL_EVERYTHING}`
|
|
511
|
+
].join("\n");
|
|
512
|
+
}
|
|
513
|
+
function lapseNotice(state2, now = Date.now()) {
|
|
514
|
+
if (!state2.enrolled || !state2.lapsesAt) return null;
|
|
515
|
+
const days = Math.floor((state2.lapsesAt - now) / (24 * 60 * 60 * 1e3));
|
|
516
|
+
if (days < 0) return "this machine's enrollment has lapsed; the next command will ask for approval";
|
|
517
|
+
return `idle for ${days} more day${days === 1 ? "" : "s"} before this machine needs approving again (using it resets the clock)`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/cached-credential.ts
|
|
521
|
+
var noted = null;
|
|
522
|
+
function noteCachedCredential(tokenFile) {
|
|
523
|
+
noted = tokenFile;
|
|
524
|
+
}
|
|
525
|
+
function isCredentialRejection(error) {
|
|
526
|
+
const message2 = error instanceof Error ? error.message : String(error ?? "");
|
|
527
|
+
return /\((401|403)\)\s*$/.test(message2.trim());
|
|
528
|
+
}
|
|
529
|
+
function explainRejectedCredential(error) {
|
|
530
|
+
const tokenFile = noted;
|
|
531
|
+
if (!tokenFile || !isCredentialRejection(error)) return null;
|
|
532
|
+
noted = null;
|
|
533
|
+
(0, import_node_fs5.rmSync)(tokenFile, { force: true });
|
|
534
|
+
return [
|
|
535
|
+
"auth: the cached credential was rejected by odla, so it was revoked before its cached expiry.",
|
|
536
|
+
" The usual cause is a newer sign-in for this account: collecting a handshake retires the",
|
|
537
|
+
" principal's other collected credentials, so a second machine supersedes this one.",
|
|
538
|
+
` Discarded ${tokenFile}; re-run this command to request a fresh approval.`,
|
|
539
|
+
` To stop needing one: ${ENROL_EVERYTHING}`
|
|
540
|
+
].join("\n");
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/device-session-cache.ts
|
|
544
|
+
var import_node_process7 = __toESM(require("process"), 1);
|
|
545
|
+
var SKEW_MS = 6e4;
|
|
546
|
+
async function deviceSessionToken(platformUrl, audience, credential2, doFetch, env = import_node_process7.default.env) {
|
|
547
|
+
const path = deviceSessionFile(env);
|
|
548
|
+
const cached = readJsonFile(path);
|
|
549
|
+
if (cached?.token && cached.platform === audience && cached.deviceId === credential2.deviceId && (cached.expiresAt ?? 0) > Date.now() + SKEW_MS) return cached;
|
|
550
|
+
const minted = await mintDeviceSession(platformUrl, credential2, doFetch);
|
|
551
|
+
const session = {
|
|
552
|
+
...minted,
|
|
553
|
+
platform: audience,
|
|
554
|
+
...credential2.deviceId ? { deviceId: credential2.deviceId } : {}
|
|
555
|
+
};
|
|
556
|
+
writePrivateJson(path, session);
|
|
557
|
+
return session;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// src/machine-identity.ts
|
|
561
|
+
var import_node_process8 = __toESM(require("process"), 1);
|
|
562
|
+
function readMachineIdentity(audience, env = import_node_process8.default.env) {
|
|
563
|
+
const stored = readJsonFile(identityFile(env));
|
|
564
|
+
if (!stored || typeof stored.email !== "string" || !stored.email) return null;
|
|
565
|
+
return stored.platform === audience ? { platform: audience, email: stored.email } : null;
|
|
566
|
+
}
|
|
567
|
+
function rememberMachineIdentity(audience, email, env = import_node_process8.default.env) {
|
|
568
|
+
if (!email) return;
|
|
569
|
+
const existing = readMachineIdentity(audience, env);
|
|
570
|
+
if (existing?.email === email) return;
|
|
571
|
+
writePrivateJson(identityFile(env), { platform: audience, email });
|
|
572
|
+
}
|
|
573
|
+
|
|
425
574
|
// src/token.ts
|
|
426
575
|
async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {}) {
|
|
427
576
|
const audience = platformAudience(cfg.platformUrl);
|
|
@@ -430,19 +579,19 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
|
|
|
430
579
|
const cached = readJsonFile(cfg.local.tokenFile);
|
|
431
580
|
if (!grantRequest.forceReview && !grantRequest.freshLogin) {
|
|
432
581
|
if (options.token) return options.token;
|
|
433
|
-
if (
|
|
434
|
-
const declared =
|
|
582
|
+
if (import_node_process9.default.env.ODLA_DEV_TOKEN) {
|
|
583
|
+
const declared = import_node_process9.default.env.ODLA_DEV_TOKEN_AUDIENCE;
|
|
435
584
|
if (declared) {
|
|
436
585
|
if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
|
|
437
586
|
} else if (audience !== "https://odla.ai") {
|
|
438
587
|
throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
|
|
439
588
|
}
|
|
440
|
-
return
|
|
589
|
+
return import_node_process9.default.env.ODLA_DEV_TOKEN;
|
|
441
590
|
}
|
|
442
591
|
const device = readDeviceCredential(audience);
|
|
443
592
|
if (device) {
|
|
444
|
-
const session = await
|
|
445
|
-
out.error(`auth: session
|
|
593
|
+
const session = await deviceSessionToken(cfg.platformUrl, audience, device, doFetch);
|
|
594
|
+
out.error(`auth: session held by this enrolled device (${displayPath(deviceCredentialPath(), cfg.rootDir)})`);
|
|
446
595
|
return session.token;
|
|
447
596
|
}
|
|
448
597
|
if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4 && cachedGrantCovers(cached, grantIntent)) {
|
|
@@ -462,7 +611,10 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
|
|
|
462
611
|
doFetch,
|
|
463
612
|
out,
|
|
464
613
|
audience,
|
|
465
|
-
email: handshakeEmail(
|
|
614
|
+
email: handshakeEmail(
|
|
615
|
+
options.email,
|
|
616
|
+
(cached?.platform === audience ? cached.email : void 0) ?? readMachineIdentity(audience)?.email
|
|
617
|
+
),
|
|
466
618
|
pendingFile: handshakeFile(cfg),
|
|
467
619
|
grantIntent
|
|
468
620
|
};
|
|
@@ -479,6 +631,7 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
|
|
|
479
631
|
expiresAt
|
|
480
632
|
});
|
|
481
633
|
out.error(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
634
|
+
rememberMachineIdentity(audience, ctx.email);
|
|
482
635
|
return token;
|
|
483
636
|
}
|
|
484
637
|
async function freshHandshake(ctx, waitMs) {
|
|
@@ -548,7 +701,7 @@ function stillPending(pending, email) {
|
|
|
548
701
|
);
|
|
549
702
|
}
|
|
550
703
|
function handshakeEmail(value2, cached) {
|
|
551
|
-
const email = (value2 ??
|
|
704
|
+
const email = (value2 ?? import_node_process9.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
|
|
552
705
|
if (/@users\.noreply\.github\.com$/i.test(email)) {
|
|
553
706
|
throw new Error(
|
|
554
707
|
`"${email}" is a GitHub commit identity, not an odla account email; use --email <signed-in-odla-account> or ODLA_USER_EMAIL`
|
|
@@ -579,12 +732,12 @@ function platformAudience(value2) {
|
|
|
579
732
|
}
|
|
580
733
|
|
|
581
734
|
// src/secret-input.ts
|
|
582
|
-
var
|
|
735
|
+
var import_node_process10 = __toESM(require("process"), 1);
|
|
583
736
|
var MAX_BYTES = 64 * 1024;
|
|
584
737
|
async function secretInputValue(options, kind = "credential") {
|
|
585
738
|
if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
|
|
586
739
|
let value2;
|
|
587
|
-
if (options.fromEnv) value2 =
|
|
740
|
+
if (options.fromEnv) value2 = import_node_process10.default.env[options.fromEnv];
|
|
588
741
|
else if (options.stdin) value2 = await (options.readStdin ?? (() => readSecretStream(kind)))();
|
|
589
742
|
else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
|
|
590
743
|
value2 = value2?.replace(/[\r\n]+$/, "");
|
|
@@ -592,7 +745,7 @@ async function secretInputValue(options, kind = "credential") {
|
|
|
592
745
|
if (new TextEncoder().encode(value2).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
|
|
593
746
|
return value2;
|
|
594
747
|
}
|
|
595
|
-
async function readSecretStream(kind, stream =
|
|
748
|
+
async function readSecretStream(kind, stream = import_node_process10.default.stdin) {
|
|
596
749
|
let value2 = "";
|
|
597
750
|
for await (const chunk of stream) {
|
|
598
751
|
value2 += String(chunk);
|
|
@@ -602,9 +755,8 @@ async function readSecretStream(kind, stream = import_node_process6.default.stdi
|
|
|
602
755
|
}
|
|
603
756
|
|
|
604
757
|
// src/admin-ai-auth.ts
|
|
605
|
-
var
|
|
606
|
-
var
|
|
607
|
-
var import_node_process7 = __toESM(require("process"), 1);
|
|
758
|
+
var import_node_path5 = require("path");
|
|
759
|
+
var import_node_process11 = __toESM(require("process"), 1);
|
|
608
760
|
var import_db2 = require("@odla-ai/db");
|
|
609
761
|
async function getScopedPlatformToken(options) {
|
|
610
762
|
return resolveAdminPlatformToken(options);
|
|
@@ -612,7 +764,7 @@ async function getScopedPlatformToken(options) {
|
|
|
612
764
|
async function resolveAdminPlatformToken(options) {
|
|
613
765
|
const audience = platformAudience(options.platform);
|
|
614
766
|
if (options.token) return options.token;
|
|
615
|
-
const fromEnv =
|
|
767
|
+
const fromEnv = import_node_process11.default.env.ODLA_ADMIN_TOKEN;
|
|
616
768
|
if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
|
|
617
769
|
return scopedToken(
|
|
618
770
|
audience,
|
|
@@ -624,7 +776,7 @@ async function resolveAdminPlatformToken(options) {
|
|
|
624
776
|
}
|
|
625
777
|
function audienceBoundEnvToken(token, platform) {
|
|
626
778
|
const audience = platformAudience(platform);
|
|
627
|
-
const declared =
|
|
779
|
+
const declared = import_node_process11.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
|
|
628
780
|
if (declared) {
|
|
629
781
|
if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
|
|
630
782
|
} else if (audience !== "https://odla.ai") {
|
|
@@ -651,15 +803,28 @@ var SCOPE_PURPOSE = {
|
|
|
651
803
|
};
|
|
652
804
|
async function scopedToken(platform, scope, options, doFetch, out) {
|
|
653
805
|
const audience = platformAudience(platform);
|
|
654
|
-
const rootDir = options.rootDir ??
|
|
655
|
-
const tokenFile = options.tokenFile ?? (
|
|
806
|
+
const rootDir = options.rootDir ?? import_node_process11.default.cwd();
|
|
807
|
+
const tokenFile = options.tokenFile ?? scopedTokenFile();
|
|
808
|
+
adoptRepoLocalCache((0, import_node_path5.join)(rootDir, ".odla/admin-token.local.json"), tokenFile, out);
|
|
809
|
+
const device = readDeviceCredential(audience);
|
|
810
|
+
if (device && options.cache !== false) {
|
|
811
|
+
const session = await deviceSessionToken(platform, audience, device, doFetch);
|
|
812
|
+
if (session.scopes?.includes(scope)) {
|
|
813
|
+
out.error(`auth: ${scope} held by this enrolled device`);
|
|
814
|
+
return session.token;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
656
817
|
const cache2 = options.cache === false ? null : readJsonFile(tokenFile);
|
|
657
818
|
const cached = cache2?.platform === audience ? cache2.tokens?.[scope] : void 0;
|
|
658
819
|
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
659
820
|
out.error(`auth: using cached ${scope} grant (${tokenFile})`);
|
|
660
821
|
return cached.token;
|
|
661
822
|
}
|
|
662
|
-
|
|
823
|
+
out.error(scopeInterruptionNotice(scope, machineAuthState(audience)));
|
|
824
|
+
const email = handshakeEmail(
|
|
825
|
+
options.email,
|
|
826
|
+
(cache2?.platform === audience ? cache2.email : void 0) ?? readMachineIdentity(audience)?.email
|
|
827
|
+
);
|
|
663
828
|
const { token, expiresAt } = await (0, import_db2.requestToken)({
|
|
664
829
|
endpoint: audience,
|
|
665
830
|
email,
|
|
@@ -679,8 +844,8 @@ async function scopedToken(platform, scope, options, doFetch, out) {
|
|
|
679
844
|
if (options.cache !== false) {
|
|
680
845
|
const tokens = cache2?.platform === audience ? { ...cache2.tokens ?? {} } : {};
|
|
681
846
|
tokens[scope] = { token, expiresAt };
|
|
682
|
-
if ((0, import_node_fs5.existsSync)((0, import_node_path4.join)(rootDir, ".git"))) ensureGitignore(rootDir, [tokenFile]);
|
|
683
847
|
writePrivateJson(tokenFile, { platform: audience, email, tokens });
|
|
848
|
+
rememberMachineIdentity(audience, email);
|
|
684
849
|
out.error(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
|
|
685
850
|
} else {
|
|
686
851
|
out.error(`auth: ${scope} grant is in memory only; its credential record remains in odla-ai/db`);
|
|
@@ -856,7 +1021,7 @@ function isRecord2(value2) {
|
|
|
856
1021
|
|
|
857
1022
|
// src/admin-ai.ts
|
|
858
1023
|
async function adminAi(options) {
|
|
859
|
-
const platform = platformAudience(options.platform ??
|
|
1024
|
+
const platform = platformAudience(options.platform ?? import_node_process12.default.env.ODLA_PLATFORM ?? "https://odla.ai");
|
|
860
1025
|
const doFetch = options.fetch ?? fetch;
|
|
861
1026
|
const out = options.stdout ?? console;
|
|
862
1027
|
const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
|
|
@@ -1172,12 +1337,12 @@ async function adminSpend(parsed, ctx) {
|
|
|
1172
1337
|
|
|
1173
1338
|
// src/operator-context.ts
|
|
1174
1339
|
var import_node_fs8 = require("fs");
|
|
1175
|
-
var
|
|
1176
|
-
var
|
|
1340
|
+
var import_node_path8 = require("path");
|
|
1341
|
+
var import_node_process14 = __toESM(require("process"), 1);
|
|
1177
1342
|
|
|
1178
1343
|
// src/config.ts
|
|
1179
1344
|
var import_node_fs6 = require("fs");
|
|
1180
|
-
var
|
|
1345
|
+
var import_node_path6 = require("path");
|
|
1181
1346
|
var import_node_url = require("url");
|
|
1182
1347
|
var import_apps = require("@odla-ai/apps");
|
|
1183
1348
|
|
|
@@ -1581,13 +1746,17 @@ var DEFAULT_ENVS = ["dev"];
|
|
|
1581
1746
|
var DEFAULT_SERVICES = ["db", "ai"];
|
|
1582
1747
|
var configImportSerial = 0;
|
|
1583
1748
|
var GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events";
|
|
1749
|
+
var stderr = { error: (message2) => {
|
|
1750
|
+
process.stderr.write(`${message2}
|
|
1751
|
+
`);
|
|
1752
|
+
} };
|
|
1584
1753
|
async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
|
|
1585
|
-
const resolved = (0,
|
|
1754
|
+
const resolved = (0, import_node_path6.resolve)(configPath);
|
|
1586
1755
|
if (!(0, import_node_fs6.existsSync)(resolved)) {
|
|
1587
1756
|
throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
|
|
1588
1757
|
}
|
|
1589
1758
|
const raw = await loadConfigModule(resolved);
|
|
1590
|
-
const rootDir = (0,
|
|
1759
|
+
const rootDir = (0, import_node_path6.dirname)(resolved);
|
|
1591
1760
|
validateRawConfig(raw, resolved);
|
|
1592
1761
|
const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
|
|
1593
1762
|
const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
|
|
@@ -1597,11 +1766,14 @@ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
|
|
|
1597
1766
|
validateCalendarConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
|
|
1598
1767
|
validateMonitoringConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
|
|
1599
1768
|
const local = {
|
|
1600
|
-
tokenFile: (0,
|
|
1601
|
-
credentialsFile: (0,
|
|
1602
|
-
devVarsFile: (0,
|
|
1769
|
+
tokenFile: raw.local?.tokenFile ? (0, import_node_path6.resolve)(rootDir, raw.local.tokenFile) : appTokenFile(raw.app.id),
|
|
1770
|
+
credentialsFile: raw.local?.credentialsFile ? (0, import_node_path6.resolve)(rootDir, raw.local.credentialsFile) : appCredentialsFile(raw.app.id),
|
|
1771
|
+
devVarsFile: (0, import_node_path6.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
|
|
1603
1772
|
gitignore: raw.local?.gitignore ?? true
|
|
1604
1773
|
};
|
|
1774
|
+
adoptRepoLocalCache((0, import_node_path6.resolve)(rootDir, ".odla/dev-token.json"), local.tokenFile, stderr);
|
|
1775
|
+
adoptRepoLocalCache((0, import_node_path6.resolve)(rootDir, ".odla/credentials.local.json"), local.credentialsFile, stderr);
|
|
1776
|
+
(0, import_node_fs6.rmSync)((0, import_node_path6.resolve)(rootDir, ".odla/handshake.local.json"), { force: true });
|
|
1605
1777
|
return {
|
|
1606
1778
|
...raw,
|
|
1607
1779
|
configPath: resolved,
|
|
@@ -1616,7 +1788,7 @@ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
|
|
|
1616
1788
|
async function resolveDataExport(cfg, value2, names) {
|
|
1617
1789
|
if (value2 === void 0 || value2 === null || value2 === false) return void 0;
|
|
1618
1790
|
if (typeof value2 !== "string") return value2;
|
|
1619
|
-
const target = (0,
|
|
1791
|
+
const target = (0, import_node_path6.isAbsolute)(value2) ? value2 : (0, import_node_path6.resolve)(cfg.rootDir, value2);
|
|
1620
1792
|
if (target.endsWith(".json")) {
|
|
1621
1793
|
return JSON.parse((0, import_node_fs6.readFileSync)(target, "utf8"));
|
|
1622
1794
|
}
|
|
@@ -1710,17 +1882,17 @@ function unique3(values) {
|
|
|
1710
1882
|
|
|
1711
1883
|
// src/operator-profiles.ts
|
|
1712
1884
|
var import_node_fs7 = require("fs");
|
|
1713
|
-
var
|
|
1714
|
-
var
|
|
1715
|
-
var
|
|
1885
|
+
var import_node_os3 = require("os");
|
|
1886
|
+
var import_node_path7 = require("path");
|
|
1887
|
+
var import_node_process13 = __toESM(require("process"), 1);
|
|
1716
1888
|
function operatorProfileFile() {
|
|
1717
|
-
return (0,
|
|
1718
|
-
clean(
|
|
1889
|
+
return (0, import_node_path7.resolve)(
|
|
1890
|
+
clean(import_node_process13.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path7.join)((0, import_node_os3.homedir)(), ".odla", "contexts.json")
|
|
1719
1891
|
);
|
|
1720
1892
|
}
|
|
1721
1893
|
function resolveOperatorProfile(parsed) {
|
|
1722
1894
|
const fromFlag = clean(stringOpt(parsed.options.context));
|
|
1723
|
-
const fromEnvironment = clean(
|
|
1895
|
+
const fromEnvironment = clean(import_node_process13.default.env.ODLA_CONTEXT);
|
|
1724
1896
|
const name = fromFlag ?? fromEnvironment ?? null;
|
|
1725
1897
|
const file = operatorProfileFile();
|
|
1726
1898
|
if (!name) {
|
|
@@ -1760,10 +1932,10 @@ function removeOperatorProfile(name, file = operatorProfileFile()) {
|
|
|
1760
1932
|
return true;
|
|
1761
1933
|
}
|
|
1762
1934
|
function operatorCredentialFiles(selection) {
|
|
1763
|
-
const base = selection.name ? (0,
|
|
1935
|
+
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
1936
|
return {
|
|
1765
|
-
developer: (0,
|
|
1766
|
-
scoped: (0,
|
|
1937
|
+
developer: (0, import_node_path7.join)(base, "dev-token.json"),
|
|
1938
|
+
scoped: (0, import_node_path7.join)(base, "admin-token.local.json")
|
|
1767
1939
|
};
|
|
1768
1940
|
}
|
|
1769
1941
|
function assertOperatorName(value2, label) {
|
|
@@ -1841,7 +2013,7 @@ var DEFAULT_PLATFORM2 = "https://odla.ai";
|
|
|
1841
2013
|
async function resolveOperatorContext(parsed, options = {}) {
|
|
1842
2014
|
const profile = resolveOperatorProfile(parsed);
|
|
1843
2015
|
const configArgument = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
1844
|
-
const configPath = (0,
|
|
2016
|
+
const configPath = (0, import_node_path8.resolve)(configArgument);
|
|
1845
2017
|
const explicitConfig = parsed.options.config !== void 0;
|
|
1846
2018
|
const hasConfig = (0, import_node_fs8.existsSync)(configPath);
|
|
1847
2019
|
if (!hasConfig && (!options.allowMissingConfig || explicitConfig)) {
|
|
@@ -1849,13 +2021,13 @@ async function resolveOperatorContext(parsed, options = {}) {
|
|
|
1849
2021
|
}
|
|
1850
2022
|
const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
|
|
1851
2023
|
const platformFlag = clean2(stringOpt(parsed.options.platform));
|
|
1852
|
-
const platformEnvironment = clean2(
|
|
2024
|
+
const platformEnvironment = clean2(import_node_process14.default.env.ODLA_PLATFORM_URL);
|
|
1853
2025
|
const platformValue = platformAudience(
|
|
1854
2026
|
platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
|
|
1855
2027
|
);
|
|
1856
2028
|
const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
|
|
1857
2029
|
const appFlag = clean2(stringOpt(parsed.options.app));
|
|
1858
|
-
const appEnvironment = clean2(
|
|
2030
|
+
const appEnvironment = clean2(import_node_process14.default.env.ODLA_APP_ID);
|
|
1859
2031
|
const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
|
|
1860
2032
|
const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
|
|
1861
2033
|
if (appValue) {
|
|
@@ -1869,16 +2041,16 @@ async function resolveOperatorContext(parsed, options = {}) {
|
|
|
1869
2041
|
);
|
|
1870
2042
|
}
|
|
1871
2043
|
const envFlag = clean2(stringOpt(parsed.options.env));
|
|
1872
|
-
const envEnvironment = clean2(
|
|
2044
|
+
const envEnvironment = clean2(import_node_process14.default.env.ODLA_ENV);
|
|
1873
2045
|
const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
|
|
1874
2046
|
const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
|
|
1875
2047
|
if (environmentValue) {
|
|
1876
2048
|
assertOperatorName(environmentValue, "environment");
|
|
1877
2049
|
}
|
|
1878
|
-
const rootDir = loaded?.rootDir ??
|
|
2050
|
+
const rootDir = loaded?.rootDir ?? import_node_process14.default.cwd();
|
|
1879
2051
|
const profileCredentials = operatorCredentialFiles(profile);
|
|
1880
|
-
const tokenFile = clean2(
|
|
1881
|
-
const
|
|
2052
|
+
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;
|
|
2053
|
+
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
2054
|
const cfg = loaded ? {
|
|
1883
2055
|
...loaded,
|
|
1884
2056
|
platformUrl: platformValue,
|
|
@@ -1900,8 +2072,8 @@ async function resolveOperatorContext(parsed, options = {}) {
|
|
|
1900
2072
|
services: [],
|
|
1901
2073
|
local: {
|
|
1902
2074
|
tokenFile,
|
|
1903
|
-
credentialsFile: (0,
|
|
1904
|
-
devVarsFile: (0,
|
|
2075
|
+
credentialsFile: (0, import_node_path8.join)(rootDir, ".odla", "credentials.local.json"),
|
|
2076
|
+
devVarsFile: (0, import_node_path8.join)(rootDir, ".dev.vars"),
|
|
1905
2077
|
gitignore: true
|
|
1906
2078
|
}
|
|
1907
2079
|
};
|
|
@@ -1925,7 +2097,7 @@ async function resolveOperatorContext(parsed, options = {}) {
|
|
|
1925
2097
|
},
|
|
1926
2098
|
credentials: {
|
|
1927
2099
|
developerTokenFile: tokenFile,
|
|
1928
|
-
scopedTokenFile
|
|
2100
|
+
scopedTokenFile: scopedTokenFile2
|
|
1929
2101
|
}
|
|
1930
2102
|
};
|
|
1931
2103
|
}
|
|
@@ -2023,7 +2195,7 @@ async function adminCommand(parsed, deps = {}) {
|
|
|
2023
2195
|
}
|
|
2024
2196
|
|
|
2025
2197
|
// src/auth-command.ts
|
|
2026
|
-
var
|
|
2198
|
+
var import_node_process15 = __toESM(require("process"), 1);
|
|
2027
2199
|
|
|
2028
2200
|
// src/whoami-command.ts
|
|
2029
2201
|
var text2 = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
|
|
@@ -2159,6 +2331,7 @@ async function whoamiCommand(parsed, deps = {}) {
|
|
|
2159
2331
|
} else {
|
|
2160
2332
|
out.log("projects: (none \u2014 every pm and discuss call will be refused)");
|
|
2161
2333
|
}
|
|
2334
|
+
printMachineBlock(cfg.platformUrl, out);
|
|
2162
2335
|
if (!identity.admin) {
|
|
2163
2336
|
if (identity.scopes.includes("platform:runbook:write")) {
|
|
2164
2337
|
out.log("\nThis exact scope can read and edit all platform runbook content.");
|
|
@@ -2169,6 +2342,21 @@ async function whoamiCommand(parsed, deps = {}) {
|
|
|
2169
2342
|
}
|
|
2170
2343
|
}
|
|
2171
2344
|
}
|
|
2345
|
+
function printMachineBlock(platformUrl, out) {
|
|
2346
|
+
const state2 = machineAuthState(platformAudience(platformUrl));
|
|
2347
|
+
if (!state2.enrolled) {
|
|
2348
|
+
out.log("\nmachine: not enrolled \u2014 every privileged command needs its own browser approval.");
|
|
2349
|
+
out.log(` End that with:
|
|
2350
|
+
${ENROL_EVERYTHING}`);
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
const reach = state2.appIds?.includes("*") ? "every app you own" : state2.appIds?.join(", ");
|
|
2354
|
+
out.log(`
|
|
2355
|
+
machine: enrolled${state2.deviceName ? ` as "${state2.deviceName}"` : ""}${reach ? ` for ${reach}` : ""}`);
|
|
2356
|
+
if (state2.scopes?.length) out.log(` carrying ${state2.scopes.join(", ")}`);
|
|
2357
|
+
const lapse = lapseNotice(state2);
|
|
2358
|
+
if (lapse) out.log(` ${lapse}`);
|
|
2359
|
+
}
|
|
2172
2360
|
|
|
2173
2361
|
// src/auth-command.ts
|
|
2174
2362
|
async function authCommand(parsed, deps = {}) {
|
|
@@ -2193,12 +2381,17 @@ async function authCommand(parsed, deps = {}) {
|
|
|
2193
2381
|
const { cfg } = context;
|
|
2194
2382
|
const out = deps.stdout ?? console;
|
|
2195
2383
|
const doFetch = deps.fetch ?? fetch;
|
|
2196
|
-
const email = stringOpt(parsed.options.email) ??
|
|
2384
|
+
const email = stringOpt(parsed.options.email) ?? import_node_process15.default.env.ODLA_USER_EMAIL?.trim();
|
|
2197
2385
|
if (!email) {
|
|
2198
2386
|
throw new Error(
|
|
2199
2387
|
"auth login requires --email <odla-account> or ODLA_USER_EMAIL; confirm the signed-in odla email instead of using git or GitHub identity"
|
|
2200
2388
|
);
|
|
2201
2389
|
}
|
|
2390
|
+
if (!machineAuthState(platformAudience(cfg.platformUrl)).enrolled) {
|
|
2391
|
+
out.error("odla: this machine is not enrolled, so this approval buys one project until it lapses.");
|
|
2392
|
+
out.error(` For one approval that covers every app you own, with no repeats:
|
|
2393
|
+
${ENROL_EVERYTHING}`);
|
|
2394
|
+
}
|
|
2202
2395
|
const token = await getDeveloperToken(
|
|
2203
2396
|
cfg,
|
|
2204
2397
|
{
|
|
@@ -2554,7 +2747,7 @@ async function appCommand(parsed, dependencies = {}) {
|
|
|
2554
2747
|
|
|
2555
2748
|
// src/brand-command.ts
|
|
2556
2749
|
var import_promises = require("fs/promises");
|
|
2557
|
-
var
|
|
2750
|
+
var import_node_path9 = require("path");
|
|
2558
2751
|
|
|
2559
2752
|
// src/brand-design-unpack.ts
|
|
2560
2753
|
var import_node_zlib = require("zlib");
|
|
@@ -2656,15 +2849,15 @@ function describeUnpack(result, outDir) {
|
|
|
2656
2849
|
// src/brand-command.ts
|
|
2657
2850
|
var USAGE = "usage: odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]";
|
|
2658
2851
|
async function readBundle(source, deps) {
|
|
2659
|
-
if (source !== "-") return (0, import_promises.readFile)((0,
|
|
2852
|
+
if (source !== "-") return (0, import_promises.readFile)((0, import_node_path9.resolve)(source), "utf8");
|
|
2660
2853
|
const readStdin = deps.readStdin;
|
|
2661
2854
|
if (!readStdin) throw new Error("reading a bundle from stdin is not supported here");
|
|
2662
2855
|
return readStdin();
|
|
2663
2856
|
}
|
|
2664
2857
|
async function writeAll(result, outDir) {
|
|
2665
2858
|
for (const file of result.files) {
|
|
2666
|
-
const target = (0,
|
|
2667
|
-
await (0, import_promises.mkdir)((0,
|
|
2859
|
+
const target = (0, import_node_path9.resolve)(outDir, file.path);
|
|
2860
|
+
await (0, import_promises.mkdir)((0, import_node_path9.dirname)(target), { recursive: true });
|
|
2668
2861
|
await (0, import_promises.writeFile)(target, file.bytes);
|
|
2669
2862
|
}
|
|
2670
2863
|
}
|
|
@@ -2672,7 +2865,7 @@ async function designUnpack(parsed, deps) {
|
|
|
2672
2865
|
assertArgs(parsed, ["out", "json"], 4);
|
|
2673
2866
|
const source = parsed.positionals[3];
|
|
2674
2867
|
if (!source) throw new Error(USAGE);
|
|
2675
|
-
const outDir = (0,
|
|
2868
|
+
const outDir = (0, import_node_path9.resolve)(stringOpt(parsed.options.out) ?? "design");
|
|
2676
2869
|
const result = unpackDesign(await readBundle(source, deps));
|
|
2677
2870
|
await writeAll(result, outDir);
|
|
2678
2871
|
const out = deps.stdout ?? console;
|
|
@@ -3277,7 +3470,7 @@ async function safeText4(response2) {
|
|
|
3277
3470
|
|
|
3278
3471
|
// src/config-operation-command.ts
|
|
3279
3472
|
var import_apps6 = require("@odla-ai/apps");
|
|
3280
|
-
var
|
|
3473
|
+
var import_node_path10 = require("path");
|
|
3281
3474
|
|
|
3282
3475
|
// src/version.ts
|
|
3283
3476
|
var import_node_fs10 = require("fs");
|
|
@@ -3707,7 +3900,7 @@ async function operationClient(cfg, options, purpose) {
|
|
|
3707
3900
|
platform: cfg.platformUrl,
|
|
3708
3901
|
scope: "app:config:write",
|
|
3709
3902
|
token: options.token,
|
|
3710
|
-
tokenFile: (0,
|
|
3903
|
+
tokenFile: (0, import_node_path10.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
|
|
3711
3904
|
rootDir: cfg.rootDir,
|
|
3712
3905
|
email: options.email,
|
|
3713
3906
|
open: options.open,
|
|
@@ -3762,7 +3955,7 @@ function record4(value2) {
|
|
|
3762
3955
|
|
|
3763
3956
|
// src/config-reconcile-command.ts
|
|
3764
3957
|
var import_apps8 = require("@odla-ai/apps");
|
|
3765
|
-
var
|
|
3958
|
+
var import_node_path11 = require("path");
|
|
3766
3959
|
|
|
3767
3960
|
// src/config-reconcile.ts
|
|
3768
3961
|
var import_apps7 = require("@odla-ai/apps");
|
|
@@ -4058,7 +4251,7 @@ async function inspectConfig(options) {
|
|
|
4058
4251
|
platform: cfg.platformUrl,
|
|
4059
4252
|
scope: "app:config:read",
|
|
4060
4253
|
token: options.token,
|
|
4061
|
-
tokenFile: (0,
|
|
4254
|
+
tokenFile: (0, import_node_path11.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
|
|
4062
4255
|
rootDir: cfg.rootDir,
|
|
4063
4256
|
email: options.email,
|
|
4064
4257
|
open: options.open,
|
|
@@ -4191,26 +4384,26 @@ function quoteArg2(value2) {
|
|
|
4191
4384
|
// src/doctor-checks.ts
|
|
4192
4385
|
var import_node_child_process3 = require("child_process");
|
|
4193
4386
|
var import_node_fs13 = require("fs");
|
|
4194
|
-
var
|
|
4387
|
+
var import_node_path13 = require("path");
|
|
4195
4388
|
|
|
4196
4389
|
// src/wrangler.ts
|
|
4197
4390
|
var import_node_child_process2 = require("child_process");
|
|
4198
4391
|
var import_node_fs12 = require("fs");
|
|
4199
|
-
var
|
|
4392
|
+
var import_node_path12 = require("path");
|
|
4200
4393
|
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
4201
4394
|
const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
4202
4395
|
let stdout = "";
|
|
4203
|
-
let
|
|
4396
|
+
let stderr2 = "";
|
|
4204
4397
|
child.stdout.on("data", (chunk) => stdout += chunk.toString());
|
|
4205
|
-
child.stderr.on("data", (chunk) =>
|
|
4398
|
+
child.stderr.on("data", (chunk) => stderr2 += chunk.toString());
|
|
4206
4399
|
child.on("error", reject);
|
|
4207
|
-
child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr }));
|
|
4400
|
+
child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr: stderr2 }));
|
|
4208
4401
|
child.stdin.end(opts?.input ?? "");
|
|
4209
4402
|
});
|
|
4210
4403
|
var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
4211
4404
|
function findWranglerConfig(rootDir) {
|
|
4212
4405
|
for (const name of WRANGLER_CONFIG_FILES) {
|
|
4213
|
-
const path = (0,
|
|
4406
|
+
const path = (0, import_node_path12.join)(rootDir, name);
|
|
4214
4407
|
if ((0, import_node_fs12.existsSync)(path)) return path;
|
|
4215
4408
|
}
|
|
4216
4409
|
return null;
|
|
@@ -4361,21 +4554,21 @@ function wranglerWarnings(rootDir) {
|
|
|
4361
4554
|
const blocks = [{ label: "", block: config }];
|
|
4362
4555
|
const envs = config.env;
|
|
4363
4556
|
if (envs && typeof envs === "object") {
|
|
4364
|
-
for (const [name,
|
|
4365
|
-
if (
|
|
4557
|
+
for (const [name, block2] of Object.entries(envs)) {
|
|
4558
|
+
if (block2 && typeof block2 === "object") blocks.push({ label: `env.${name}.`, block: block2 });
|
|
4366
4559
|
}
|
|
4367
4560
|
}
|
|
4368
|
-
for (const { label, block } of blocks) {
|
|
4369
|
-
const assets =
|
|
4561
|
+
for (const { label, block: block2 } of blocks) {
|
|
4562
|
+
const assets = block2.assets;
|
|
4370
4563
|
if (assets?.directory) {
|
|
4371
|
-
const dir = (0,
|
|
4372
|
-
if (dir === (0,
|
|
4564
|
+
const dir = (0, import_node_path13.resolve)(rootDir, assets.directory);
|
|
4565
|
+
if (dir === (0, import_node_path13.resolve)(rootDir)) {
|
|
4373
4566
|
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,
|
|
4567
|
+
} else if ((0, import_node_fs13.existsSync)((0, import_node_path13.join)(dir, "node_modules"))) {
|
|
4375
4568
|
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
4376
4569
|
}
|
|
4377
4570
|
}
|
|
4378
|
-
const vars =
|
|
4571
|
+
const vars = block2.vars;
|
|
4379
4572
|
if (vars && typeof vars === "object") {
|
|
4380
4573
|
for (const [name, value2] of Object.entries(vars)) {
|
|
4381
4574
|
if (name === "ODLA_API_KEY" || name === "ODLA_O11Y_TOKEN" || typeof value2 === "string" && looksSecret(value2)) {
|
|
@@ -4406,7 +4599,7 @@ function o11yProjectWarnings(rootDir) {
|
|
|
4406
4599
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
|
|
4407
4600
|
return warnings;
|
|
4408
4601
|
}
|
|
4409
|
-
const main = typeof config.main === "string" ? (0,
|
|
4602
|
+
const main = typeof config.main === "string" ? (0, import_node_path13.resolve)(rootDir, config.main) : null;
|
|
4410
4603
|
if (!main || !(0, import_node_fs13.existsSync)(main)) {
|
|
4411
4604
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
|
|
4412
4605
|
} else {
|
|
@@ -4436,7 +4629,7 @@ function calendarProjectWarnings(rootDir) {
|
|
|
4436
4629
|
}
|
|
4437
4630
|
function readPackageJson(rootDir) {
|
|
4438
4631
|
try {
|
|
4439
|
-
return JSON.parse((0, import_node_fs13.readFileSync)((0,
|
|
4632
|
+
return JSON.parse((0, import_node_fs13.readFileSync)((0, import_node_path13.join)(rootDir, "package.json"), "utf8"));
|
|
4440
4633
|
} catch {
|
|
4441
4634
|
return null;
|
|
4442
4635
|
}
|
|
@@ -4731,12 +4924,12 @@ function harnessOption(value2, flag) {
|
|
|
4731
4924
|
|
|
4732
4925
|
// src/init.ts
|
|
4733
4926
|
var import_node_fs14 = require("fs");
|
|
4734
|
-
var
|
|
4927
|
+
var import_node_path14 = require("path");
|
|
4735
4928
|
var import_apps9 = require("@odla-ai/apps");
|
|
4736
4929
|
function initProject(options) {
|
|
4737
4930
|
const out = options.stdout ?? console;
|
|
4738
|
-
const rootDir = (0,
|
|
4739
|
-
const configPath = (0,
|
|
4931
|
+
const rootDir = (0, import_node_path14.resolve)(options.rootDir ?? process.cwd());
|
|
4932
|
+
const configPath = (0, import_node_path14.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
|
|
4740
4933
|
if ((0, import_node_fs14.existsSync)(configPath) && !options.force) {
|
|
4741
4934
|
throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
|
|
4742
4935
|
}
|
|
@@ -4753,12 +4946,12 @@ function initProject(options) {
|
|
|
4753
4946
|
}
|
|
4754
4947
|
}
|
|
4755
4948
|
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,
|
|
4949
|
+
(0, import_node_fs14.mkdirSync)((0, import_node_path14.dirname)(configPath), { recursive: true });
|
|
4950
|
+
(0, import_node_fs14.mkdirSync)((0, import_node_path14.resolve)(rootDir, "src/odla"), { recursive: true });
|
|
4951
|
+
(0, import_node_fs14.mkdirSync)((0, import_node_path14.resolve)(rootDir, ".odla"), { recursive: true });
|
|
4759
4952
|
(0, import_node_fs14.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
4760
|
-
writeIfMissing((0,
|
|
4761
|
-
writeIfMissing((0,
|
|
4953
|
+
writeIfMissing((0, import_node_path14.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
4954
|
+
writeIfMissing((0, import_node_path14.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
4762
4955
|
ensureGitignore(rootDir);
|
|
4763
4956
|
out.log(`created ${relativeDisplay(configPath, rootDir)}`);
|
|
4764
4957
|
out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
|
|
@@ -4825,8 +5018,10 @@ ${calendar}
|
|
|
4825
5018
|
// prod: "https://example.com",
|
|
4826
5019
|
},
|
|
4827
5020
|
local: {
|
|
4828
|
-
|
|
4829
|
-
|
|
5021
|
+
// Credentials live in ~/.odla, per machine, so every worktree of this app
|
|
5022
|
+
// shares one approval instead of asking for its own. Pinning tokenFile or
|
|
5023
|
+
// credentialsFile here still works and still overrides that \u2014 it just puts
|
|
5024
|
+
// this checkout back on its own island.
|
|
4830
5025
|
devVarsFile: ".dev.vars",
|
|
4831
5026
|
},
|
|
4832
5027
|
};
|
|
@@ -5063,8 +5258,8 @@ function printReport(report5, out) {
|
|
|
5063
5258
|
|
|
5064
5259
|
// src/skill.ts
|
|
5065
5260
|
var import_node_fs15 = require("fs");
|
|
5066
|
-
var
|
|
5067
|
-
var
|
|
5261
|
+
var import_node_os4 = require("os");
|
|
5262
|
+
var import_node_path15 = require("path");
|
|
5068
5263
|
var import_node_url2 = require("url");
|
|
5069
5264
|
|
|
5070
5265
|
// src/skill-adapters.ts
|
|
@@ -5163,8 +5358,8 @@ function installSkill(options = {}) {
|
|
|
5163
5358
|
const files = listFiles(sourceDir);
|
|
5164
5359
|
if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
|
|
5165
5360
|
const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
|
|
5166
|
-
const root = (0,
|
|
5167
|
-
const home = (0,
|
|
5361
|
+
const root = (0, import_node_path15.resolve)(options.dir ?? process.cwd());
|
|
5362
|
+
const home = (0, import_node_path15.resolve)(options.homeDir ?? (0, import_node_os4.homedir)());
|
|
5168
5363
|
const plans = /* @__PURE__ */ new Map();
|
|
5169
5364
|
const targets = /* @__PURE__ */ new Map();
|
|
5170
5365
|
const rememberTarget = (harness, target) => {
|
|
@@ -5178,48 +5373,48 @@ function installSkill(options = {}) {
|
|
|
5178
5373
|
plans.set(target, { target, content: content2, boundary, managedMerge });
|
|
5179
5374
|
};
|
|
5180
5375
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
5181
|
-
for (const rel of files) plan((0,
|
|
5376
|
+
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
5377
|
};
|
|
5183
5378
|
let targetDir;
|
|
5184
5379
|
if (options.global) {
|
|
5185
|
-
const claudeRoot = (0,
|
|
5186
|
-
const codexRoot = (0,
|
|
5380
|
+
const claudeRoot = (0, import_node_path15.join)(home, ".claude", "skills");
|
|
5381
|
+
const codexRoot = (0, import_node_path15.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path15.join)(home, ".codex"), "skills");
|
|
5187
5382
|
targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
|
|
5188
5383
|
for (const harness of harnesses) {
|
|
5189
5384
|
const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
|
|
5190
|
-
planSkillTree(skillRoot, harness === "claude" ? home : (0,
|
|
5385
|
+
planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path15.dirname)((0, import_node_path15.dirname)(codexRoot)));
|
|
5191
5386
|
rememberTarget(harness, skillRoot);
|
|
5192
5387
|
}
|
|
5193
5388
|
} else {
|
|
5194
|
-
const sharedRoot = (0,
|
|
5389
|
+
const sharedRoot = (0, import_node_path15.join)(root, ".agents", "skills");
|
|
5195
5390
|
planSkillTree(sharedRoot);
|
|
5196
|
-
const claudeRoot = (0,
|
|
5391
|
+
const claudeRoot = (0, import_node_path15.join)(root, ".claude", "skills");
|
|
5197
5392
|
targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
|
|
5198
5393
|
for (const harness of harnesses) rememberTarget(harness, sharedRoot);
|
|
5199
5394
|
if (harnesses.includes("claude")) {
|
|
5200
5395
|
for (const skill of skillNames(files)) {
|
|
5201
|
-
const canonical2 = (0, import_node_fs15.readFileSync)((0,
|
|
5202
|
-
plan((0,
|
|
5396
|
+
const canonical2 = (0, import_node_fs15.readFileSync)((0, import_node_path15.join)(sourceDir, skill, "SKILL.md"), "utf8");
|
|
5397
|
+
plan((0, import_node_path15.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical2));
|
|
5203
5398
|
}
|
|
5204
5399
|
rememberTarget("claude", claudeRoot);
|
|
5205
5400
|
}
|
|
5206
5401
|
if (harnesses.includes("cursor")) {
|
|
5207
|
-
const cursorRule = (0,
|
|
5402
|
+
const cursorRule = (0, import_node_path15.join)(root, ".cursor", "rules", "odla.mdc");
|
|
5208
5403
|
plan(cursorRule, CURSOR_RULE);
|
|
5209
5404
|
rememberTarget("cursor", cursorRule);
|
|
5210
5405
|
}
|
|
5211
5406
|
if (harnesses.includes("agents")) {
|
|
5212
|
-
const agentsFile = (0,
|
|
5407
|
+
const agentsFile = (0, import_node_path15.join)(root, "AGENTS.md");
|
|
5213
5408
|
plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
5214
5409
|
rememberTarget("agents", agentsFile);
|
|
5215
5410
|
}
|
|
5216
5411
|
if (harnesses.includes("copilot")) {
|
|
5217
|
-
const copilotFile = (0,
|
|
5412
|
+
const copilotFile = (0, import_node_path15.join)(root, ".github", "copilot-instructions.md");
|
|
5218
5413
|
plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
5219
5414
|
rememberTarget("copilot", copilotFile);
|
|
5220
5415
|
}
|
|
5221
5416
|
if (harnesses.includes("gemini")) {
|
|
5222
|
-
const geminiFile = (0,
|
|
5417
|
+
const geminiFile = (0, import_node_path15.join)(root, "GEMINI.md");
|
|
5223
5418
|
plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
5224
5419
|
rememberTarget("gemini", geminiFile);
|
|
5225
5420
|
}
|
|
@@ -5255,7 +5450,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
5255
5450
|
}
|
|
5256
5451
|
for (const file of plans.values()) {
|
|
5257
5452
|
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,
|
|
5453
|
+
(0, import_node_fs15.mkdirSync)((0, import_node_path15.dirname)(file.target), { recursive: true });
|
|
5259
5454
|
(0, import_node_fs15.writeFileSync)(file.target, file.content);
|
|
5260
5455
|
}
|
|
5261
5456
|
}
|
|
@@ -5275,7 +5470,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
5275
5470
|
};
|
|
5276
5471
|
}
|
|
5277
5472
|
function pathsUnder(root, paths) {
|
|
5278
|
-
return [...paths].map((path) => (0,
|
|
5473
|
+
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
5474
|
}
|
|
5280
5475
|
function normalizeHarnesses(values, global) {
|
|
5281
5476
|
const requested = values?.length ? values : ["claude"];
|
|
@@ -5294,10 +5489,10 @@ function normalizeHarnesses(values, global) {
|
|
|
5294
5489
|
}
|
|
5295
5490
|
return expanded;
|
|
5296
5491
|
}
|
|
5297
|
-
function managedFileContent(path,
|
|
5492
|
+
function managedFileContent(path, block2, force, boundary) {
|
|
5298
5493
|
const symlink = symlinkedComponent(boundary, path);
|
|
5299
5494
|
if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
|
|
5300
|
-
if (!(0, import_node_fs15.existsSync)(path)) return `${
|
|
5495
|
+
if (!(0, import_node_fs15.existsSync)(path)) return `${block2}
|
|
5301
5496
|
`;
|
|
5302
5497
|
const current = (0, import_node_fs15.readFileSync)(path, "utf8");
|
|
5303
5498
|
const start = "<!-- odla-ai agent setup:start -->";
|
|
@@ -5309,24 +5504,24 @@ function managedFileContent(path, block, force, boundary) {
|
|
|
5309
5504
|
}
|
|
5310
5505
|
if (startAt === -1) {
|
|
5311
5506
|
const separator = current.length === 0 || current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n";
|
|
5312
|
-
return `${current}${separator}${
|
|
5507
|
+
return `${current}${separator}${block2}
|
|
5313
5508
|
`;
|
|
5314
5509
|
}
|
|
5315
5510
|
const afterEnd = endAt + end.length;
|
|
5316
5511
|
const existing = current.slice(startAt, afterEnd);
|
|
5317
|
-
if (existing !==
|
|
5512
|
+
if (existing !== block2 && !force) {
|
|
5318
5513
|
throw new Error(`odla-managed section modified locally in ${path}; re-run with --force to replace that section`);
|
|
5319
5514
|
}
|
|
5320
|
-
return `${current.slice(0, startAt)}${
|
|
5515
|
+
return `${current.slice(0, startAt)}${block2}${current.slice(afterEnd)}`;
|
|
5321
5516
|
}
|
|
5322
5517
|
function symlinkedComponent(boundary, target) {
|
|
5323
|
-
const rel = (0,
|
|
5324
|
-
if (rel === ".." || rel.startsWith(`..${
|
|
5518
|
+
const rel = (0, import_node_path15.relative)(boundary, target);
|
|
5519
|
+
if (rel === ".." || rel.startsWith(`..${import_node_path15.sep}`) || (0, import_node_path15.isAbsolute)(rel)) {
|
|
5325
5520
|
throw new Error(`agent setup target escapes its install root: ${target}`);
|
|
5326
5521
|
}
|
|
5327
5522
|
let current = boundary;
|
|
5328
|
-
for (const part of rel.split(
|
|
5329
|
-
current = (0,
|
|
5523
|
+
for (const part of rel.split(import_node_path15.sep).filter(Boolean)) {
|
|
5524
|
+
current = (0, import_node_path15.join)(current, part);
|
|
5330
5525
|
try {
|
|
5331
5526
|
if ((0, import_node_fs15.lstatSync)(current).isSymbolicLink()) return current;
|
|
5332
5527
|
} catch (error) {
|
|
@@ -5343,9 +5538,9 @@ function listFiles(dir) {
|
|
|
5343
5538
|
const results = [];
|
|
5344
5539
|
const walk = (current) => {
|
|
5345
5540
|
for (const entry of (0, import_node_fs15.readdirSync)(current, { withFileTypes: true })) {
|
|
5346
|
-
const path = (0,
|
|
5541
|
+
const path = (0, import_node_path15.join)(current, entry.name);
|
|
5347
5542
|
if (entry.isDirectory()) walk(path);
|
|
5348
|
-
else results.push((0,
|
|
5543
|
+
else results.push((0, import_node_path15.relative)(dir, path));
|
|
5349
5544
|
}
|
|
5350
5545
|
};
|
|
5351
5546
|
walk(dir);
|
|
@@ -5701,8 +5896,8 @@ async function projectCommand(command, parsed, deps) {
|
|
|
5701
5896
|
|
|
5702
5897
|
// src/code-connect.ts
|
|
5703
5898
|
var import_node_fs16 = require("fs");
|
|
5704
|
-
var
|
|
5705
|
-
var
|
|
5899
|
+
var import_node_os5 = require("os");
|
|
5900
|
+
var import_node_path16 = require("path");
|
|
5706
5901
|
|
|
5707
5902
|
// ../harness/dist/chunk-LNQNFGQC.js
|
|
5708
5903
|
var HARNESS_PROTOCOL_VERSION = 1;
|
|
@@ -5808,7 +6003,7 @@ function allowedWorkspacePath(relativePath) {
|
|
|
5808
6003
|
async function gitOutput(cwd, args, maxBytes) {
|
|
5809
6004
|
const child = (0, import_child_process2.spawn)("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], shell: false });
|
|
5810
6005
|
const stdout = [];
|
|
5811
|
-
const
|
|
6006
|
+
const stderr2 = [];
|
|
5812
6007
|
let bytes = 0;
|
|
5813
6008
|
child.stdout.on("data", (chunk) => {
|
|
5814
6009
|
bytes += chunk.byteLength;
|
|
@@ -5816,20 +6011,20 @@ async function gitOutput(cwd, args, maxBytes) {
|
|
|
5816
6011
|
else stdout.push(chunk);
|
|
5817
6012
|
});
|
|
5818
6013
|
child.stderr.on("data", (chunk) => {
|
|
5819
|
-
if (
|
|
6014
|
+
if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
|
|
5820
6015
|
});
|
|
5821
6016
|
const code = await new Promise((accept, reject) => {
|
|
5822
6017
|
child.once("error", reject);
|
|
5823
6018
|
child.once("exit", accept);
|
|
5824
6019
|
});
|
|
5825
6020
|
if (bytes > maxBytes) throw new Error(`git output exceeds ${maxBytes} bytes`);
|
|
5826
|
-
if (code !== 0) throw new Error(`git command failed: ${Buffer.concat(
|
|
6021
|
+
if (code !== 0) throw new Error(`git command failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
|
|
5827
6022
|
return Buffer.concat(stdout);
|
|
5828
6023
|
}
|
|
5829
6024
|
async function gitBlobs(cwd, entries, maxBytes) {
|
|
5830
6025
|
const child = (0, import_child_process2.spawn)("git", ["cat-file", "--batch"], { cwd, stdio: ["pipe", "pipe", "pipe"], shell: false });
|
|
5831
6026
|
const stdout = [];
|
|
5832
|
-
const
|
|
6027
|
+
const stderr2 = [];
|
|
5833
6028
|
let bytes = 0;
|
|
5834
6029
|
child.stdout.on("data", (chunk) => {
|
|
5835
6030
|
bytes += chunk.byteLength;
|
|
@@ -5837,7 +6032,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
|
|
|
5837
6032
|
else stdout.push(chunk);
|
|
5838
6033
|
});
|
|
5839
6034
|
child.stderr.on("data", (chunk) => {
|
|
5840
|
-
if (
|
|
6035
|
+
if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
|
|
5841
6036
|
});
|
|
5842
6037
|
child.stdin.end(`${entries.map((entry) => entry.hash).join("\n")}
|
|
5843
6038
|
`);
|
|
@@ -5846,7 +6041,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
|
|
|
5846
6041
|
child.once("exit", accept);
|
|
5847
6042
|
});
|
|
5848
6043
|
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(
|
|
6044
|
+
if (code !== 0) throw new Error(`git object read failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
|
|
5850
6045
|
const output = Buffer.concat(stdout);
|
|
5851
6046
|
const blobs = [];
|
|
5852
6047
|
let offset = 0;
|
|
@@ -5943,7 +6138,7 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
|
5943
6138
|
shell: false
|
|
5944
6139
|
});
|
|
5945
6140
|
const stdout = [];
|
|
5946
|
-
const
|
|
6141
|
+
const stderr2 = [];
|
|
5947
6142
|
let outputBytes = 0;
|
|
5948
6143
|
child.stdout.on("data", (chunk) => {
|
|
5949
6144
|
outputBytes += chunk.byteLength;
|
|
@@ -5951,14 +6146,14 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
|
5951
6146
|
else stdout.push(chunk);
|
|
5952
6147
|
});
|
|
5953
6148
|
child.stderr.on("data", (chunk) => {
|
|
5954
|
-
if (
|
|
6149
|
+
if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
|
|
5955
6150
|
});
|
|
5956
6151
|
const code = await new Promise((accept, reject) => {
|
|
5957
6152
|
child.once("error", reject);
|
|
5958
6153
|
child.once("exit", accept);
|
|
5959
6154
|
});
|
|
5960
6155
|
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(
|
|
6156
|
+
if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
|
|
5962
6157
|
const paths = Buffer.concat(stdout).toString("utf8").split("\0").filter(Boolean).sort();
|
|
5963
6158
|
if (paths.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
|
|
5964
6159
|
const root = (0, import_path4.resolve)(sourceDir);
|
|
@@ -6003,7 +6198,7 @@ async function captureGitDiff(root, maxBytes) {
|
|
|
6003
6198
|
"workspace"
|
|
6004
6199
|
], { cwd: root, stdio: ["ignore", "pipe", "pipe"], shell: false });
|
|
6005
6200
|
const stdout = [];
|
|
6006
|
-
const
|
|
6201
|
+
const stderr2 = [];
|
|
6007
6202
|
let bytes = 0;
|
|
6008
6203
|
child.stdout.on("data", (chunk) => {
|
|
6009
6204
|
bytes += chunk.byteLength;
|
|
@@ -6011,7 +6206,7 @@ async function captureGitDiff(root, maxBytes) {
|
|
|
6011
6206
|
else stdout.push(chunk);
|
|
6012
6207
|
});
|
|
6013
6208
|
child.stderr.on("data", (chunk) => {
|
|
6014
|
-
if (
|
|
6209
|
+
if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
|
|
6015
6210
|
});
|
|
6016
6211
|
const code = await new Promise((accept, reject) => {
|
|
6017
6212
|
child.once("error", reject);
|
|
@@ -6019,7 +6214,7 @@ async function captureGitDiff(root, maxBytes) {
|
|
|
6019
6214
|
});
|
|
6020
6215
|
if (bytes > maxBytes) throw new Error(`patch exceeds ${maxBytes} bytes`);
|
|
6021
6216
|
if (code !== 0 && code !== 1) {
|
|
6022
|
-
throw new Error(`git diff failed: ${Buffer.concat(
|
|
6217
|
+
throw new Error(`git diff failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
|
|
6023
6218
|
}
|
|
6024
6219
|
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
6220
|
}
|
|
@@ -6823,11 +7018,11 @@ function rollup(graph, kind, options = {}) {
|
|
|
6823
7018
|
}
|
|
6824
7019
|
|
|
6825
7020
|
// ../graph/dist/code/index.js
|
|
6826
|
-
function
|
|
7021
|
+
function dirname9(path) {
|
|
6827
7022
|
const at = path.lastIndexOf("/");
|
|
6828
7023
|
return at <= 0 ? "." : path.slice(0, at);
|
|
6829
7024
|
}
|
|
6830
|
-
function
|
|
7025
|
+
function join13(base, specifier) {
|
|
6831
7026
|
const parts = [];
|
|
6832
7027
|
const segments = `${base === "." ? "" : `${base}/`}${specifier}`.split("/");
|
|
6833
7028
|
for (const segment of segments) {
|
|
@@ -6851,7 +7046,7 @@ var BARE_IMPORT = /^\s*import\s*["']([^"']+)["']/gm;
|
|
|
6851
7046
|
var isSourcePath = (path) => SOURCE.test(path);
|
|
6852
7047
|
function resolveImport(fromPath, specifier, known) {
|
|
6853
7048
|
if (!specifier.startsWith(".")) return null;
|
|
6854
|
-
const base =
|
|
7049
|
+
const base = join13(dirname9(fromPath), specifier);
|
|
6855
7050
|
const candidates = [
|
|
6856
7051
|
base,
|
|
6857
7052
|
base.replace(/\.js$/, ".ts"),
|
|
@@ -7382,13 +7577,13 @@ function gitApply(cwd, patch2, check) {
|
|
|
7382
7577
|
stdio: ["pipe", "ignore", "pipe"],
|
|
7383
7578
|
env: { PATH: process.env.PATH ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null" }
|
|
7384
7579
|
});
|
|
7385
|
-
let
|
|
7580
|
+
let stderr2 = "";
|
|
7386
7581
|
child.stderr.setEncoding("utf8");
|
|
7387
7582
|
child.stderr.on("data", (text3) => {
|
|
7388
|
-
if (
|
|
7583
|
+
if (stderr2.length < 4e3) stderr2 += text3.slice(0, 4e3);
|
|
7389
7584
|
});
|
|
7390
7585
|
child.once("error", reject);
|
|
7391
|
-
child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2,
|
|
7586
|
+
child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2, stderr2.trim().slice(0, 500)))));
|
|
7392
7587
|
child.stdin.end(patch2);
|
|
7393
7588
|
});
|
|
7394
7589
|
}
|
|
@@ -7501,7 +7696,7 @@ function execute(engine, args, name, recipe2, signal) {
|
|
|
7501
7696
|
const started = Date.now();
|
|
7502
7697
|
const child = (0, import_child_process5.spawn)(engine, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
7503
7698
|
const stdout = [];
|
|
7504
|
-
const
|
|
7699
|
+
const stderr2 = [];
|
|
7505
7700
|
let bytes = 0;
|
|
7506
7701
|
let outputLimitExceeded = false;
|
|
7507
7702
|
let timedOut = false;
|
|
@@ -7522,7 +7717,7 @@ function execute(engine, args, name, recipe2, signal) {
|
|
|
7522
7717
|
else target.push(chunk);
|
|
7523
7718
|
};
|
|
7524
7719
|
child.stdout.on("data", collect(stdout));
|
|
7525
|
-
child.stderr.on("data", collect(
|
|
7720
|
+
child.stderr.on("data", collect(stderr2));
|
|
7526
7721
|
const abort = () => stop("abort");
|
|
7527
7722
|
signal?.addEventListener("abort", abort, { once: true });
|
|
7528
7723
|
if (signal?.aborted) abort();
|
|
@@ -7538,7 +7733,7 @@ function execute(engine, args, name, recipe2, signal) {
|
|
|
7538
7733
|
accept({
|
|
7539
7734
|
exitCode: code ?? 1,
|
|
7540
7735
|
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
7541
|
-
stderr: Buffer.concat(
|
|
7736
|
+
stderr: Buffer.concat(stderr2).toString("utf8"),
|
|
7542
7737
|
durationMs: Date.now() - started,
|
|
7543
7738
|
outputLimitExceeded,
|
|
7544
7739
|
timedOut
|
|
@@ -7690,11 +7885,11 @@ function checkedResult(result, maximumOutputBytes) {
|
|
|
7690
7885
|
}
|
|
7691
7886
|
function boundedLogs(result, maximum) {
|
|
7692
7887
|
const stdout = Buffer.from(result.stdout);
|
|
7693
|
-
const
|
|
7888
|
+
const stderr2 = Buffer.from(result.stderr);
|
|
7694
7889
|
const first = stdout.subarray(0, maximum);
|
|
7695
7890
|
return {
|
|
7696
7891
|
stdout: first.toString("utf8"),
|
|
7697
|
-
stderr:
|
|
7892
|
+
stderr: stderr2.subarray(0, Math.max(0, maximum - first.byteLength)).toString("utf8")
|
|
7698
7893
|
};
|
|
7699
7894
|
}
|
|
7700
7895
|
function digestPolicy(policy) {
|
|
@@ -9764,7 +9959,7 @@ var CODE_BUILD_RECIPES = Object.freeze([{
|
|
|
9764
9959
|
// src/code-connect.ts
|
|
9765
9960
|
async function codeConnect(options) {
|
|
9766
9961
|
const cwd = options.cwd ?? process.cwd();
|
|
9767
|
-
const configPath = (0,
|
|
9962
|
+
const configPath = (0, import_node_path16.resolve)(cwd, options.configPath);
|
|
9768
9963
|
const cfg = (0, import_node_fs16.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
|
|
9769
9964
|
const requestedAppId = options.appId?.trim();
|
|
9770
9965
|
if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
|
|
@@ -9794,7 +9989,7 @@ async function codeConnect(options) {
|
|
|
9794
9989
|
const doFetch = options.fetch ?? fetch;
|
|
9795
9990
|
const engine = await (options.selectEngine ?? selectContainerEngine)(options.engine ?? "auto");
|
|
9796
9991
|
const hostPlatform = process.platform === "darwin" ? "macos" : "linux";
|
|
9797
|
-
const hostName = (options.name ?? (0,
|
|
9992
|
+
const hostName = (options.name ?? (0, import_node_os5.hostname)()).trim();
|
|
9798
9993
|
if (!hostName || hostName.length > 120) throw new Error("--name must contain 1 to 120 characters");
|
|
9799
9994
|
const repository = await inferGitHubRepository(cwd, options.readGitOrigin);
|
|
9800
9995
|
const localSource = await (options.prepareLocalSource ?? prepareCodeLocalSource)(
|
|
@@ -9832,8 +10027,8 @@ async function codeConnect(options) {
|
|
|
9832
10027
|
platform: hostPlatform,
|
|
9833
10028
|
arch: process.arch,
|
|
9834
10029
|
engines: [engine],
|
|
9835
|
-
cpuCount: (0,
|
|
9836
|
-
memoryBytes: (0,
|
|
10030
|
+
cpuCount: (0, import_node_os5.cpus)().length,
|
|
10031
|
+
memoryBytes: (0, import_node_os5.totalmem)(),
|
|
9837
10032
|
source: descriptor2,
|
|
9838
10033
|
images: {
|
|
9839
10034
|
ready: true,
|
|
@@ -10200,13 +10395,13 @@ async function codeCommand(parsed, dependencies) {
|
|
|
10200
10395
|
}
|
|
10201
10396
|
|
|
10202
10397
|
// src/operator-credentials.ts
|
|
10203
|
-
var
|
|
10398
|
+
var import_node_process16 = __toESM(require("process"), 1);
|
|
10204
10399
|
function developerTokenStatus(context, parsed, now = Date.now()) {
|
|
10205
10400
|
const cached = readJsonFile(context.cfg.local.tokenFile);
|
|
10206
10401
|
const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
|
|
10207
10402
|
const source = clean3(
|
|
10208
10403
|
stringOpt(parsed.options.token)
|
|
10209
|
-
) ? "flag" : clean3(
|
|
10404
|
+
) ? "flag" : clean3(import_node_process16.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
|
|
10210
10405
|
return {
|
|
10211
10406
|
source,
|
|
10212
10407
|
cacheFile: context.cfg.local.tokenFile,
|
|
@@ -10400,6 +10595,30 @@ async function credentialCommand(parsed, deps = {}) {
|
|
|
10400
10595
|
}
|
|
10401
10596
|
|
|
10402
10597
|
// src/help-usage.ts
|
|
10598
|
+
var AUTH_SECTION = `
|
|
10599
|
+
Enrol this machine once, then stop asking:
|
|
10600
|
+
npx odla-ai device enroll --no-open --wait 600
|
|
10601
|
+
One browser approval, and no flags to remember: this covers every app you
|
|
10602
|
+
own \u2014 including apps you create later \u2014 with every capability that
|
|
10603
|
+
approval can carry. Afterwards every worktree on this machine mints its
|
|
10604
|
+
own short-lived credentials with nobody's attention. The window rolls
|
|
10605
|
+
forward each time you use it, so continuous work never interrupts anyone;
|
|
10606
|
+
only a real gap does. Give the human the printed /studio?code= URL, keep
|
|
10607
|
+
the process alive, and wait on it.
|
|
10608
|
+
Narrow it deliberately with --app <id> or --capability <c>; the CLI then
|
|
10609
|
+
says what that gave up.
|
|
10610
|
+
|
|
10611
|
+
npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600
|
|
10612
|
+
The same thing across all of odla, for weeks. Needs a platform
|
|
10613
|
+
administrator's approval \u2014 an app owner's cannot carry platform scopes.
|
|
10614
|
+
|
|
10615
|
+
npx odla-ai whoami what this machine holds and when it lapses
|
|
10616
|
+
npx odla-ai device list every machine you have enrolled
|
|
10617
|
+
|
|
10618
|
+
Enrollment is the only human decision here. Revoking a machine, purging an app,
|
|
10619
|
+
transferring ownership, and rotating credentials still need a signed-in human in
|
|
10620
|
+
Studio, and no machine credential can do them however wide its approval was.
|
|
10621
|
+
`;
|
|
10403
10622
|
var USAGE_SECTION = `
|
|
10404
10623
|
Start here:
|
|
10405
10624
|
odla-ai runbook ask "<question>" The current procedure, from odla's own
|
|
@@ -10438,7 +10657,7 @@ Usage:
|
|
|
10438
10657
|
odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
|
|
10439
10658
|
odla-ai pm project list [--app <product-id>] [--status <s>] [--json]
|
|
10440
10659
|
odla-ai pm project add --app <product-id> --name <name> [--description <text>] [--json]
|
|
10441
|
-
odla-ai pm project use <project-id> [--json] [saved
|
|
10660
|
+
odla-ai pm project use <project-id> [--json] [saved for this app, on this machine]
|
|
10442
10661
|
odla-ai pm goal list [--app <id>] [--project <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
10443
10662
|
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
10663
|
odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
@@ -10530,7 +10749,8 @@ Usage:
|
|
|
10530
10749
|
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
10531
10750
|
odla-ai security run [target] --self --ack-redacted-source
|
|
10532
10751
|
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>...]] [--
|
|
10752
|
+
odla-ai device enroll [--app <id>[,<id>...]|--all-apps] [--capability all|<c>[,<c>...]] [--platform-wide]
|
|
10753
|
+
[--name <label>] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--wait <seconds>] [--json]
|
|
10534
10754
|
odla-ai device list [--email <odla-account>] [--json]
|
|
10535
10755
|
odla-ai device revoke <device-id> [--email <odla-account>] [--json]
|
|
10536
10756
|
odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
|
|
@@ -10544,8 +10764,11 @@ Usage:
|
|
|
10544
10764
|
|
|
10545
10765
|
// src/help.ts
|
|
10546
10766
|
function printHelp(output = console) {
|
|
10547
|
-
output.log(
|
|
10548
|
-
|
|
10767
|
+
output.log(helpText());
|
|
10768
|
+
}
|
|
10769
|
+
function helpText() {
|
|
10770
|
+
return `odla-ai
|
|
10771
|
+
${AUTH_SECTION}${USAGE_SECTION}
|
|
10549
10772
|
Commands:
|
|
10550
10773
|
auth Start a fresh, exact-project agent authorization for human review.
|
|
10551
10774
|
The email is the signed-in odla account, never git or GitHub
|
|
@@ -10618,8 +10841,9 @@ Commands:
|
|
|
10618
10841
|
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
10619
10842
|
pm Project management (via @odla-ai/pm): Products contain Projects;
|
|
10620
10843
|
projects contain goals, kanban tasks, decisions, and bugs. Use
|
|
10621
|
-
"pm project list|add|use"
|
|
10622
|
-
|
|
10844
|
+
"pm project list|add|use" selects a project for this app on this
|
|
10845
|
+
machine \u2014 every worktree shares it \u2014 or pass --app/--project
|
|
10846
|
+
explicitly. Same device-grant auth as "app".
|
|
10623
10847
|
Status changes and comments post to each item's @odla-ai/chat
|
|
10624
10848
|
discussion thread.
|
|
10625
10849
|
NOTE: "--column ready" is OWNER-ONLY. Creating a task in Ready
|
|
@@ -10648,9 +10872,22 @@ Commands:
|
|
|
10648
10872
|
platform Read canonical fleet health, releases, provider load/freshness,
|
|
10649
10873
|
explicit unknowns, and next actions through a read-only grant.
|
|
10650
10874
|
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
|
|
10875
|
+
enrollment in the browser; from then on EVERY worktree on this
|
|
10876
|
+
machine mints its own short-lived credentials with nobody's
|
|
10877
|
+
attention, until the device is revoked or goes unused.
|
|
10878
|
+
With no flags it covers every app you own, now and later, with
|
|
10879
|
+
every capability that approval can carry \u2014 so creating an app
|
|
10880
|
+
costs no new approval, and a capability nobody thought to name is
|
|
10881
|
+
not a 403 next week. "--app" or "--capability" narrow it
|
|
10882
|
+
deliberately, and the CLI says what that gave up.
|
|
10883
|
+
"--platform-wide" is the administrator's version, across all of
|
|
10884
|
+
odla.
|
|
10885
|
+
The expiry is a GAP, not a clock: each use rolls it forward, so
|
|
10886
|
+
only going quiet brings a human back into the loop \u2014 which is
|
|
10887
|
+
where anything that changed can be explained.
|
|
10888
|
+
"device list" shows what each machine holds and when it lapses;
|
|
10889
|
+
revoking one takes down every credential it ever minted, and is
|
|
10890
|
+
deliberately a signed-in human's decision in Studio.
|
|
10654
10891
|
provision Register services, compose integrations, persist credentials, optionally push secrets.
|
|
10655
10892
|
"provision --live --yes" initializes only the live instance of
|
|
10656
10893
|
an existing sandbox app and enables every configured service;
|
|
@@ -10746,7 +10983,46 @@ Safety:
|
|
|
10746
10983
|
Run security plan first to inspect the admin-selected providers, models,
|
|
10747
10984
|
per-route bounds, credential readiness, retention, no-execution boundary,
|
|
10748
10985
|
and digest that binds consent to that exact plan.
|
|
10749
|
-
|
|
10986
|
+
`;
|
|
10987
|
+
}
|
|
10988
|
+
|
|
10989
|
+
// src/help-command.ts
|
|
10990
|
+
function printCommandHelp(command, output = console) {
|
|
10991
|
+
const lines = helpText().split("\n");
|
|
10992
|
+
const usage = allBlocks(lines, new RegExp(`^ odla-ai ${escapeRe(command)}(\\s|$)`));
|
|
10993
|
+
const prose = block(lines, (line2) => new RegExp(`^ ${escapeRe(command)}\\s\\s+\\S`).test(line2));
|
|
10994
|
+
if (usage.length === 0 && prose.length === 0) {
|
|
10995
|
+
output.log(`odla-ai: no command "${command}". Run "odla-ai help" for all of them.`);
|
|
10996
|
+
return;
|
|
10997
|
+
}
|
|
10998
|
+
output.log([
|
|
10999
|
+
...prose.length ? [prose.join("\n"), ""] : [],
|
|
11000
|
+
...usage.length ? ["Usage:", ...usage, ""] : [],
|
|
11001
|
+
AUTH_SECTION.trimEnd()
|
|
11002
|
+
].join("\n"));
|
|
11003
|
+
}
|
|
11004
|
+
function allBlocks(lines, pattern) {
|
|
11005
|
+
const out = [];
|
|
11006
|
+
for (let i = 0; i < lines.length; i++) {
|
|
11007
|
+
if (!pattern.test(lines[i])) continue;
|
|
11008
|
+
out.push(...block(lines.slice(i), (line2) => line2 === lines[i]));
|
|
11009
|
+
}
|
|
11010
|
+
return out;
|
|
11011
|
+
}
|
|
11012
|
+
function block(lines, starts) {
|
|
11013
|
+
const first = lines.findIndex(starts);
|
|
11014
|
+
if (first === -1) return [];
|
|
11015
|
+
const indent = lines[first].length - lines[first].trimStart().length;
|
|
11016
|
+
const out = [lines[first]];
|
|
11017
|
+
for (const line2 of lines.slice(first + 1)) {
|
|
11018
|
+
if (!line2.trim()) break;
|
|
11019
|
+
if (line2.length - line2.trimStart().length <= indent) break;
|
|
11020
|
+
out.push(line2);
|
|
11021
|
+
}
|
|
11022
|
+
return out;
|
|
11023
|
+
}
|
|
11024
|
+
function escapeRe(value2) {
|
|
11025
|
+
return value2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10750
11026
|
}
|
|
10751
11027
|
|
|
10752
11028
|
// src/discuss-principals.ts
|
|
@@ -11903,14 +12179,36 @@ async function pmWatch(ctx, parsed) {
|
|
|
11903
12179
|
}
|
|
11904
12180
|
|
|
11905
12181
|
// src/pm-project-context.ts
|
|
11906
|
-
var
|
|
11907
|
-
var
|
|
12182
|
+
var import_node_fs17 = require("fs");
|
|
12183
|
+
var import_node_path17 = require("path");
|
|
12184
|
+
var pmProjectContextFile = () => pmContextFile();
|
|
11908
12185
|
function readPmProjectContext(rootDir) {
|
|
11909
|
-
|
|
11910
|
-
|
|
12186
|
+
adoptLegacySelection(rootDir);
|
|
12187
|
+
const entries = Object.values(readSelections()).filter(isSelection);
|
|
12188
|
+
return entries.sort((a, b) => b.selectedAt.localeCompare(a.selectedAt))[0] ?? null;
|
|
11911
12189
|
}
|
|
11912
12190
|
function writePmProjectContext(rootDir, value2) {
|
|
11913
|
-
|
|
12191
|
+
adoptLegacySelection(rootDir);
|
|
12192
|
+
writePrivateJson(pmProjectContextFile(), {
|
|
12193
|
+
...readSelections(),
|
|
12194
|
+
[value2.appId]: { ...value2, selectedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
12195
|
+
});
|
|
12196
|
+
}
|
|
12197
|
+
function adoptLegacySelection(rootDir) {
|
|
12198
|
+
const legacy = (0, import_node_path17.resolve)(rootDir, ".odla", "pm-project.local.json");
|
|
12199
|
+
if (!(0, import_node_fs17.existsSync)(legacy)) return;
|
|
12200
|
+
const previous = readJsonFile(legacy);
|
|
12201
|
+
(0, import_node_fs17.rmSync)(legacy, { force: true });
|
|
12202
|
+
if (!isSelection(previous)) return;
|
|
12203
|
+
const selections = readSelections();
|
|
12204
|
+
if (selections[previous.appId]) return;
|
|
12205
|
+
writePrivateJson(pmProjectContextFile(), { ...selections, [previous.appId]: previous });
|
|
12206
|
+
}
|
|
12207
|
+
function readSelections() {
|
|
12208
|
+
return readJsonFile(pmProjectContextFile()) ?? {};
|
|
12209
|
+
}
|
|
12210
|
+
function isSelection(value2) {
|
|
12211
|
+
return !!value2 && typeof value2.appId === "string" && typeof value2.projectId === "string" && typeof value2.selectedAt === "string";
|
|
11914
12212
|
}
|
|
11915
12213
|
|
|
11916
12214
|
// src/pm-project-actions.ts
|
|
@@ -12935,7 +13233,7 @@ function percent(value2) {
|
|
|
12935
13233
|
// src/provision.ts
|
|
12936
13234
|
var import_apps13 = require("@odla-ai/apps");
|
|
12937
13235
|
var import_ai5 = require("@odla-ai/ai");
|
|
12938
|
-
var
|
|
13236
|
+
var import_node_process17 = __toESM(require("process"), 1);
|
|
12939
13237
|
|
|
12940
13238
|
// src/integration-provision.ts
|
|
12941
13239
|
var import_db3 = require("@odla-ai/db");
|
|
@@ -13373,7 +13671,7 @@ async function provision(options) {
|
|
|
13373
13671
|
await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
|
|
13374
13672
|
}
|
|
13375
13673
|
if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
13376
|
-
const key =
|
|
13674
|
+
const key = import_node_process17.default.env[cfg.ai.keyEnv];
|
|
13377
13675
|
if (key) {
|
|
13378
13676
|
const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
|
|
13379
13677
|
await (0, import_ai5.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
|
|
@@ -13414,8 +13712,8 @@ async function provision(options) {
|
|
|
13414
13712
|
}
|
|
13415
13713
|
|
|
13416
13714
|
// src/record.ts
|
|
13417
|
-
var
|
|
13418
|
-
var
|
|
13715
|
+
var import_node_fs18 = require("fs");
|
|
13716
|
+
var import_node_process18 = __toESM(require("process"), 1);
|
|
13419
13717
|
|
|
13420
13718
|
// src/surface.ts
|
|
13421
13719
|
var PM_ACTIONS = {
|
|
@@ -13597,7 +13895,7 @@ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
|
|
|
13597
13895
|
|
|
13598
13896
|
// src/record.ts
|
|
13599
13897
|
function recordInvocation(parsed) {
|
|
13600
|
-
const file =
|
|
13898
|
+
const file = import_node_process18.default.env.ODLA_CLI_RECORD;
|
|
13601
13899
|
if (!file) return;
|
|
13602
13900
|
try {
|
|
13603
13901
|
const entry = {
|
|
@@ -13605,7 +13903,7 @@ function recordInvocation(parsed) {
|
|
|
13605
13903
|
options: Object.entries(parsed.options).map(([name, value2]) => value2 === false ? `no-${name}` : name).sort()
|
|
13606
13904
|
};
|
|
13607
13905
|
if (!entry.path.length) return;
|
|
13608
|
-
(0,
|
|
13906
|
+
(0, import_node_fs18.appendFileSync)(file, `${JSON.stringify(entry)}
|
|
13609
13907
|
`);
|
|
13610
13908
|
} catch {
|
|
13611
13909
|
}
|
|
@@ -13623,10 +13921,17 @@ function advisoryCollectingFetch(inner, sink) {
|
|
|
13623
13921
|
return response2;
|
|
13624
13922
|
});
|
|
13625
13923
|
}
|
|
13924
|
+
var superseded = /* @__PURE__ */ new Set();
|
|
13925
|
+
function supersedeAdvisory(code) {
|
|
13926
|
+
superseded.add(code);
|
|
13927
|
+
}
|
|
13626
13928
|
function renderAdvisories(out, advisories, env = process.env) {
|
|
13929
|
+
const retracted = new Set(superseded);
|
|
13930
|
+
superseded.clear();
|
|
13627
13931
|
if (env.ODLA_NO_ADVISORIES) return;
|
|
13628
13932
|
const seen = /* @__PURE__ */ new Set();
|
|
13629
13933
|
for (const advisory of advisories) {
|
|
13934
|
+
if (retracted.has(advisory.code)) continue;
|
|
13630
13935
|
const key = `${advisory.code}:${advisory.message}`;
|
|
13631
13936
|
if (seen.has(key)) continue;
|
|
13632
13937
|
seen.add(key);
|
|
@@ -13634,6 +13939,9 @@ function renderAdvisories(out, advisories, env = process.env) {
|
|
|
13634
13939
|
}
|
|
13635
13940
|
}
|
|
13636
13941
|
|
|
13942
|
+
// src/device-command.ts
|
|
13943
|
+
var import_db4 = require("@odla-ai/db");
|
|
13944
|
+
|
|
13637
13945
|
// src/device-ttl.ts
|
|
13638
13946
|
var OWNER_DEVICE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
13639
13947
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -13653,10 +13961,26 @@ function parseDeviceTtl(raw) {
|
|
|
13653
13961
|
}
|
|
13654
13962
|
|
|
13655
13963
|
// src/device-command.ts
|
|
13656
|
-
var
|
|
13657
|
-
var
|
|
13658
|
-
var
|
|
13964
|
+
var import_node_fs19 = require("fs");
|
|
13965
|
+
var import_node_path18 = require("path");
|
|
13966
|
+
var import_node_process19 = __toESM(require("process"), 1);
|
|
13659
13967
|
async function deviceCommand(parsed, deps) {
|
|
13968
|
+
assertArgs(parsed, [
|
|
13969
|
+
"app",
|
|
13970
|
+
"all-apps",
|
|
13971
|
+
"platform-wide",
|
|
13972
|
+
"name",
|
|
13973
|
+
"capability",
|
|
13974
|
+
"device-ttl",
|
|
13975
|
+
"email",
|
|
13976
|
+
"open",
|
|
13977
|
+
"json",
|
|
13978
|
+
"config",
|
|
13979
|
+
"token",
|
|
13980
|
+
"context",
|
|
13981
|
+
"platform",
|
|
13982
|
+
"wait"
|
|
13983
|
+
], 3);
|
|
13660
13984
|
const action2 = parsed.positionals[1] ?? "";
|
|
13661
13985
|
const out = deps.stdout ?? console;
|
|
13662
13986
|
const doFetch = deps.fetch ?? fetch;
|
|
@@ -13669,10 +13993,19 @@ async function deviceCommand(parsed, deps) {
|
|
|
13669
13993
|
}
|
|
13670
13994
|
async function enroll(parsed, deps, cfg, doFetch, out, json) {
|
|
13671
13995
|
const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
|
|
13672
|
-
const
|
|
13673
|
-
|
|
13996
|
+
const platformWide = parsed.options["platform-wide"] === true;
|
|
13997
|
+
const narrowed = stringOpt(parsed.options.app) !== void 0 || stringOpt(parsed.options.capability) !== void 0;
|
|
13998
|
+
const apps = platformWide || parsed.options["all-apps"] === true || !narrowed ? [import_db4.ALL_OWNED_APPS] : (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
|
|
13999
|
+
if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026], or --all-apps");
|
|
13674
14000
|
const deviceTtlMs = parseDeviceTtl(parsed.options["device-ttl"]);
|
|
13675
|
-
const extended = deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
|
|
14001
|
+
const extended = platformWide || deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
|
|
14002
|
+
const { capabilities, scopes } = requestedEnvelope(parsed, platformWide, narrowed);
|
|
14003
|
+
const narrowNotice = narrowEnrollmentNotice({
|
|
14004
|
+
appIds: apps,
|
|
14005
|
+
capabilities: capabilities ?? [],
|
|
14006
|
+
scopes: scopes ?? []
|
|
14007
|
+
});
|
|
14008
|
+
if (narrowNotice) out.error(narrowNotice);
|
|
13676
14009
|
const token = await scopedToken2(
|
|
13677
14010
|
parsed,
|
|
13678
14011
|
deps,
|
|
@@ -13687,9 +14020,10 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
|
|
|
13687
14020
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
13688
14021
|
body: JSON.stringify({
|
|
13689
14022
|
name,
|
|
13690
|
-
platform:
|
|
14023
|
+
platform: import_node_process19.default.platform,
|
|
13691
14024
|
appIds: apps,
|
|
13692
|
-
...
|
|
14025
|
+
...capabilities ? { capabilities } : {},
|
|
14026
|
+
...scopes ? { scopes } : {},
|
|
13693
14027
|
...deviceTtlMs === void 0 ? {} : { deviceTtlMs }
|
|
13694
14028
|
})
|
|
13695
14029
|
});
|
|
@@ -13698,19 +14032,54 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
|
|
|
13698
14032
|
throw new Error(`device enroll failed: ${body.error?.message ?? `registry returned ${response2.status}`} (${response2.status})`);
|
|
13699
14033
|
}
|
|
13700
14034
|
const path = deviceCredentialPath();
|
|
13701
|
-
(0,
|
|
13702
|
-
(0,
|
|
14035
|
+
(0, import_node_fs19.mkdirSync)((0, import_node_path18.dirname)(path), { recursive: true });
|
|
14036
|
+
(0, import_node_fs19.writeFileSync)(path, JSON.stringify({
|
|
13703
14037
|
token: body.token,
|
|
13704
14038
|
platform: cfg.platformUrl.replace(/\/$/, ""),
|
|
13705
14039
|
deviceId: body.device.deviceId,
|
|
13706
14040
|
name
|
|
13707
14041
|
}, null, 2));
|
|
13708
|
-
(0,
|
|
13709
|
-
|
|
13710
|
-
|
|
14042
|
+
(0, import_node_fs19.chmodSync)(path, 384);
|
|
14043
|
+
rememberMachineIdentity(cfg.platformUrl.replace(/\/$/, ""), stringOpt(parsed.options.email));
|
|
14044
|
+
supersedeAdvisory("credential.expiring");
|
|
14045
|
+
const reach = body.device.appIds.includes(import_db4.ALL_OWNED_APPS) ? "every app you own, now and later" : body.device.appIds.join(", ");
|
|
14046
|
+
out.error(`device: enrolled "${name}" for ${reach}; credential written to ${path}`);
|
|
14047
|
+
if (narrowNotice) out.error(narrowNotice);
|
|
14048
|
+
out.error(
|
|
14049
|
+
"device: every worktree on this machine mints its own credentials from now on \u2014 no further approvals,"
|
|
14050
|
+
);
|
|
14051
|
+
out.error(
|
|
14052
|
+
`device: and the clock resets each time you use it. Going quiet for ${describeWindow(body.device.expiresAt)} is what ends it.`
|
|
14053
|
+
);
|
|
13711
14054
|
if (json) {
|
|
13712
|
-
out.log(JSON.stringify({
|
|
14055
|
+
out.log(JSON.stringify({
|
|
14056
|
+
deviceId: body.device.deviceId,
|
|
14057
|
+
name,
|
|
14058
|
+
appIds: body.device.appIds,
|
|
14059
|
+
capabilities: body.device.capabilities ?? [],
|
|
14060
|
+
scopes: body.device.scopes ?? [],
|
|
14061
|
+
expiresAt: body.device.expiresAt,
|
|
14062
|
+
hardExpiresAt: body.device.hardExpiresAt ?? null
|
|
14063
|
+
}, null, 2));
|
|
14064
|
+
}
|
|
14065
|
+
}
|
|
14066
|
+
function requestedEnvelope(parsed, platformWide, narrowed) {
|
|
14067
|
+
const raw = stringOpt(parsed.options.capability);
|
|
14068
|
+
const everything = platformWide || !narrowed || raw?.trim().toLowerCase() === "all";
|
|
14069
|
+
if (everything) {
|
|
14070
|
+
return {
|
|
14071
|
+
capabilities: [...import_db4.OPTIONAL_AGENT_PROJECT_CAPABILITIES],
|
|
14072
|
+
scopes: platformWide ? [...import_db4.ADMIN_DEVICE_SCOPES] : [...import_db4.OWNER_DEVICE_SCOPES]
|
|
14073
|
+
};
|
|
13713
14074
|
}
|
|
14075
|
+
const named = raw?.split(",").map((c) => c.trim()).filter(Boolean);
|
|
14076
|
+
return named?.length ? { capabilities: named } : {};
|
|
14077
|
+
}
|
|
14078
|
+
function describeWindow(expiresAt, now = Date.now()) {
|
|
14079
|
+
const days = Math.max(1, Math.round((expiresAt - now) / (24 * 60 * 60 * 1e3)));
|
|
14080
|
+
if (days >= 365) return `${Math.round(days / 365)} year${days >= 730 ? "s" : ""}`;
|
|
14081
|
+
if (days % 7 === 0) return `${days / 7} week${days > 7 ? "s" : ""}`;
|
|
14082
|
+
return `${days} day${days === 1 ? "" : "s"}`;
|
|
13714
14083
|
}
|
|
13715
14084
|
async function list2(parsed, deps, cfg, doFetch, out, json) {
|
|
13716
14085
|
const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device list)");
|
|
@@ -13725,12 +14094,17 @@ async function list2(parsed, deps, cfg, doFetch, out, json) {
|
|
|
13725
14094
|
if (body.devices.length === 0) return out.log("no enrolled devices");
|
|
13726
14095
|
for (const device of body.devices) {
|
|
13727
14096
|
const state2 = device.revokedAt ? "revoked" : device.expiresAt <= Date.now() ? "expired" : "active";
|
|
13728
|
-
|
|
14097
|
+
const reach = device.appIds.includes("*") ? "every app you own" : device.appIds.join(", ");
|
|
14098
|
+
const gap = state2 === "active" ? ` idle ${describeWindow(device.expiresAt)} left` : "";
|
|
14099
|
+
out.log(`${device.deviceId} ${state2.padEnd(7)} ${device.name} [${reach}]${gap}`);
|
|
13729
14100
|
}
|
|
13730
14101
|
}
|
|
13731
14102
|
async function revoke(parsed, deps, cfg, doFetch, out, json) {
|
|
13732
14103
|
const deviceId = parsed.positionals[2];
|
|
13733
14104
|
if (!deviceId) throw new Error("device revoke needs the device id from `odla-ai device list`");
|
|
14105
|
+
out.error(
|
|
14106
|
+
`device: revoking is a signed-in human's decision; if this is refused, open ${cfg.platformUrl}/studio and revoke it there.`
|
|
14107
|
+
);
|
|
13734
14108
|
const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device revoke)");
|
|
13735
14109
|
const response2 = await doFetch(`${cfg.platformUrl}/registry/devices/${encodeURIComponent(deviceId)}/revoke`, {
|
|
13736
14110
|
method: "POST",
|
|
@@ -13749,7 +14123,7 @@ async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app
|
|
|
13749
14123
|
// A device is granted the apps named in ONE approval, so --app is a list here.
|
|
13750
14124
|
allowAppList: true
|
|
13751
14125
|
});
|
|
13752
|
-
const
|
|
14126
|
+
const scopedTokenFile2 = credentials.scopedTokenFile;
|
|
13753
14127
|
return getScopedPlatformToken({
|
|
13754
14128
|
platform: cfg.platformUrl,
|
|
13755
14129
|
scope,
|
|
@@ -13760,16 +14134,16 @@ async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app
|
|
|
13760
14134
|
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
13761
14135
|
openApprovalUrl: deps.openUrl,
|
|
13762
14136
|
rootDir: cfg.rootDir,
|
|
13763
|
-
tokenFile:
|
|
14137
|
+
tokenFile: scopedTokenFile2,
|
|
13764
14138
|
...stringOpt(parsed.options.token) ? { token: stringOpt(parsed.options.token) } : {}
|
|
13765
14139
|
});
|
|
13766
14140
|
}
|
|
13767
14141
|
function defaultDeviceName() {
|
|
13768
|
-
return `${
|
|
14142
|
+
return `${import_node_process19.default.env.HOSTNAME ?? import_node_process19.default.env.HOST ?? "machine"}-${import_node_process19.default.platform}`;
|
|
13769
14143
|
}
|
|
13770
14144
|
|
|
13771
14145
|
// src/runbook-actions.ts
|
|
13772
|
-
var
|
|
14146
|
+
var import_node_fs20 = require("fs");
|
|
13773
14147
|
|
|
13774
14148
|
// src/runbook-requires.ts
|
|
13775
14149
|
var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
|
|
@@ -13865,7 +14239,7 @@ async function bySlug(ctx, slug) {
|
|
|
13865
14239
|
function readBody(file, inline) {
|
|
13866
14240
|
if (inline !== void 0) return inline;
|
|
13867
14241
|
if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
|
|
13868
|
-
return (0,
|
|
14242
|
+
return (0, import_node_fs20.readFileSync)(file === "-" ? 0 : file, "utf8");
|
|
13869
14243
|
}
|
|
13870
14244
|
var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
|
|
13871
14245
|
async function runbookList(ctx, all, query) {
|
|
@@ -13957,8 +14331,8 @@ async function runbookRemove(ctx, slug) {
|
|
|
13957
14331
|
}
|
|
13958
14332
|
|
|
13959
14333
|
// src/runbook-import.ts
|
|
13960
|
-
var
|
|
13961
|
-
var
|
|
14334
|
+
var import_node_fs21 = require("fs");
|
|
14335
|
+
var import_node_path19 = require("path");
|
|
13962
14336
|
function parseRunbook(text3, slug) {
|
|
13963
14337
|
let rest = text3;
|
|
13964
14338
|
const meta = {};
|
|
@@ -13983,12 +14357,12 @@ function parseRunbook(text3, slug) {
|
|
|
13983
14357
|
};
|
|
13984
14358
|
}
|
|
13985
14359
|
function readRunbookDir(dir) {
|
|
13986
|
-
if (!(0,
|
|
13987
|
-
const files = (0,
|
|
14360
|
+
if (!(0, import_node_fs21.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
|
|
14361
|
+
const files = (0, import_node_fs21.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
|
|
13988
14362
|
if (!files.length) throw new Error(`no .md files in ${dir}`);
|
|
13989
14363
|
return files.map((file) => {
|
|
13990
|
-
const slug = (0,
|
|
13991
|
-
const parsed = parseRunbook((0,
|
|
14364
|
+
const slug = (0, import_node_path19.basename)(file, ".md");
|
|
14365
|
+
const parsed = parseRunbook((0, import_node_fs21.readFileSync)((0, import_node_path19.join)(dir, file), "utf8"), slug);
|
|
13992
14366
|
return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
|
|
13993
14367
|
});
|
|
13994
14368
|
}
|
|
@@ -14056,8 +14430,8 @@ async function upsert(ctx, r, visibility) {
|
|
|
14056
14430
|
|
|
14057
14431
|
// src/runbook-impact.ts
|
|
14058
14432
|
var import_node_child_process6 = require("child_process");
|
|
14059
|
-
var
|
|
14060
|
-
var
|
|
14433
|
+
var import_node_fs22 = require("fs");
|
|
14434
|
+
var import_node_path20 = require("path");
|
|
14061
14435
|
|
|
14062
14436
|
// src/runbook-impact-scan.ts
|
|
14063
14437
|
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 +14600,10 @@ ${body.split("\n").map((line2) => `+${line2}`).join("\n")}
|
|
|
14226
14600
|
}
|
|
14227
14601
|
function manifestLabeller(root) {
|
|
14228
14602
|
return (workspace) => {
|
|
14229
|
-
const manifest = (0,
|
|
14230
|
-
if (!(0,
|
|
14603
|
+
const manifest = (0, import_node_path20.join)(root, workspace, "package.json");
|
|
14604
|
+
if (!(0, import_node_fs22.existsSync)(manifest)) return void 0;
|
|
14231
14605
|
try {
|
|
14232
|
-
const name = JSON.parse((0,
|
|
14606
|
+
const name = JSON.parse((0, import_node_fs22.readFileSync)(manifest, "utf8")).name;
|
|
14233
14607
|
return typeof name === "string" ? name : void 0;
|
|
14234
14608
|
} catch {
|
|
14235
14609
|
return void 0;
|
|
@@ -14296,7 +14670,7 @@ function report4(ctx, impacts) {
|
|
|
14296
14670
|
async function runbookImpact(ctx, options, deps = {}) {
|
|
14297
14671
|
const cwd = deps.cwd ?? process.cwd();
|
|
14298
14672
|
const runGit = deps.runGit ?? gitRunner(cwd);
|
|
14299
|
-
const read3 = deps.readRepoFile ?? ((path) => (0,
|
|
14673
|
+
const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs22.readFileSync)((0, import_node_path20.join)(cwd, path), "utf8"));
|
|
14300
14674
|
const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
|
|
14301
14675
|
if (!surfaces.length) {
|
|
14302
14676
|
return ctx.out.log(
|
|
@@ -14423,12 +14797,12 @@ async function runbookComment(ctx, slug, body) {
|
|
|
14423
14797
|
|
|
14424
14798
|
// src/runbook-editor.ts
|
|
14425
14799
|
var import_node_child_process7 = require("child_process");
|
|
14426
|
-
var
|
|
14427
|
-
var
|
|
14428
|
-
var
|
|
14429
|
-
var
|
|
14800
|
+
var import_node_fs23 = require("fs");
|
|
14801
|
+
var import_node_os6 = require("os");
|
|
14802
|
+
var import_node_path21 = require("path");
|
|
14803
|
+
var import_node_process20 = __toESM(require("process"), 1);
|
|
14430
14804
|
var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
|
|
14431
|
-
function resolveEditor(env =
|
|
14805
|
+
function resolveEditor(env = import_node_process20.default.env) {
|
|
14432
14806
|
for (const name of EDITOR_ENV) {
|
|
14433
14807
|
const value2 = env[name];
|
|
14434
14808
|
if (value2 && value2.trim()) return value2.trim();
|
|
@@ -14442,8 +14816,8 @@ function defaultRun(command, path) {
|
|
|
14442
14816
|
return result.status ?? 0;
|
|
14443
14817
|
}
|
|
14444
14818
|
function editText(initial, slug, deps = {}) {
|
|
14445
|
-
const env = deps.env ??
|
|
14446
|
-
const interactive = deps.interactive ?? (() => Boolean(
|
|
14819
|
+
const env = deps.env ?? import_node_process20.default.env;
|
|
14820
|
+
const interactive = deps.interactive ?? (() => Boolean(import_node_process20.default.stdin.isTTY));
|
|
14447
14821
|
const editor = resolveEditor(env);
|
|
14448
14822
|
if (!editor)
|
|
14449
14823
|
throw new Error(
|
|
@@ -14451,16 +14825,16 @@ function editText(initial, slug, deps = {}) {
|
|
|
14451
14825
|
);
|
|
14452
14826
|
if (!interactive())
|
|
14453
14827
|
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,
|
|
14828
|
+
const dir = (0, import_node_fs23.mkdtempSync)((0, import_node_path21.join)((0, import_node_os6.tmpdir)(), "odla-runbook-"));
|
|
14829
|
+
const file = (0, import_node_path21.join)(dir, `${slug}.md`);
|
|
14456
14830
|
try {
|
|
14457
|
-
(0,
|
|
14831
|
+
(0, import_node_fs23.writeFileSync)(file, initial, { mode: 384 });
|
|
14458
14832
|
const code = defaultRunOrInjected(deps)(editor, file);
|
|
14459
14833
|
if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
|
|
14460
|
-
const edited = (0,
|
|
14834
|
+
const edited = (0, import_node_fs23.readFileSync)(file, "utf8");
|
|
14461
14835
|
return edited === initial ? null : edited;
|
|
14462
14836
|
} finally {
|
|
14463
|
-
(0,
|
|
14837
|
+
(0, import_node_fs23.rmSync)(dir, { recursive: true, force: true });
|
|
14464
14838
|
}
|
|
14465
14839
|
}
|
|
14466
14840
|
var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
|
|
@@ -14798,7 +15172,7 @@ function hostedSeverity(value2, flag) {
|
|
|
14798
15172
|
var import_security2 = require("@odla-ai/security");
|
|
14799
15173
|
|
|
14800
15174
|
// src/security.ts
|
|
14801
|
-
var
|
|
15175
|
+
var import_node_path22 = require("path");
|
|
14802
15176
|
var import_security = require("@odla-ai/security");
|
|
14803
15177
|
var import_node3 = require("@odla-ai/security/node");
|
|
14804
15178
|
async function runHostedSecurity(options) {
|
|
@@ -14810,9 +15184,9 @@ async function runHostedSecurity(options) {
|
|
|
14810
15184
|
const appId = selfAudit ? "odla-ai" : cfg.app.id;
|
|
14811
15185
|
const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
|
|
14812
15186
|
const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
|
|
14813
|
-
const target = (0,
|
|
14814
|
-
const output = (0,
|
|
14815
|
-
const outputRelative = (0,
|
|
15187
|
+
const target = (0, import_node_path22.resolve)(options.target ?? cfg?.rootDir ?? ".");
|
|
15188
|
+
const output = (0, import_node_path22.resolve)(options.out ?? (0, import_node_path22.resolve)(target, ".odla/security/hosted"));
|
|
15189
|
+
const outputRelative = (0, import_node_path22.relative)(target, output).split(import_node_path22.sep).join("/");
|
|
14816
15190
|
if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
|
|
14817
15191
|
const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
|
|
14818
15192
|
const tokenRequest = {
|
|
@@ -14824,7 +15198,7 @@ async function runHostedSecurity(options) {
|
|
|
14824
15198
|
};
|
|
14825
15199
|
const token = await injectedToken(options, tokenRequest);
|
|
14826
15200
|
const snapshot = await (0, import_node3.snapshotDirectory)(target, {
|
|
14827
|
-
exclude: !outputRelative.startsWith("../") && !(0,
|
|
15201
|
+
exclude: !outputRelative.startsWith("../") && !(0, import_node_path22.isAbsolute)(outputRelative) ? [outputRelative] : []
|
|
14828
15202
|
});
|
|
14829
15203
|
const hosted = await (0, import_security.createPlatformSecurityReasoners)({
|
|
14830
15204
|
platform,
|
|
@@ -14842,7 +15216,7 @@ async function runHostedSecurity(options) {
|
|
|
14842
15216
|
});
|
|
14843
15217
|
const harness = (0, import_security.createSecurityHarness)({
|
|
14844
15218
|
profile,
|
|
14845
|
-
store: new import_node3.FileRunStore((0,
|
|
15219
|
+
store: new import_node3.FileRunStore((0, import_node_path22.resolve)(output, "state")),
|
|
14846
15220
|
discoveryReasoner: hosted.discoveryReasoner,
|
|
14847
15221
|
validationReasoner: hosted.validationReasoner,
|
|
14848
15222
|
policy: {
|
|
@@ -14866,7 +15240,7 @@ async function runHostedSecurity(options) {
|
|
|
14866
15240
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
14867
15241
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
14868
15242
|
if (!env || !declared.includes(env)) {
|
|
14869
|
-
const shown = (0,
|
|
15243
|
+
const shown = (0, import_node_path22.relative)(rootDir, configPath) || configPath;
|
|
14870
15244
|
throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
|
|
14871
15245
|
}
|
|
14872
15246
|
return env;
|
|
@@ -14895,7 +15269,7 @@ function printSummary(out, appId, env, run, report5, output) {
|
|
|
14895
15269
|
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
15270
|
if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
|
|
14897
15271
|
out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
|
|
14898
|
-
out.log(` report: ${(0,
|
|
15272
|
+
out.log(` report: ${(0, import_node_path22.resolve)(output, "REPORT.md")}`);
|
|
14899
15273
|
}
|
|
14900
15274
|
function formatBudget(usage) {
|
|
14901
15275
|
return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
|
|
@@ -15376,8 +15750,10 @@ async function dispatchCli(argv, dependencies) {
|
|
|
15376
15750
|
return;
|
|
15377
15751
|
}
|
|
15378
15752
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
15379
|
-
assertArgs(parsed, ["help"],
|
|
15380
|
-
|
|
15753
|
+
assertArgs(parsed, ["help"], 2);
|
|
15754
|
+
const topic = parsed.positionals[1];
|
|
15755
|
+
if (topic) printCommandHelp(topic, runtime.stdout);
|
|
15756
|
+
else printHelp(runtime.stdout);
|
|
15381
15757
|
return;
|
|
15382
15758
|
}
|
|
15383
15759
|
if (command === "whoami") {
|