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