@odla-ai/cli 0.38.3 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.cjs +724 -293
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-WJAN5CZ2.js → chunk-2IHE5U3M.js} +596 -215
- package/dist/chunk-2IHE5U3M.js.map +1 -0
- package/dist/{cli-A5R2DMM4.js → cli-LRJBESE6.js} +2 -2
- package/dist/index.cjs +630 -254
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/skills/odla/SKILL.md +51 -11
- package/skills/odla/references/agent-identity.md +94 -111
- package/dist/chunk-WJAN5CZ2.js.map +0 -1
- /package/dist/{cli-A5R2DMM4.js.map → cli-LRJBESE6.js.map} +0 -0
|
@@ -10,12 +10,12 @@ import {
|
|
|
10
10
|
} from "./chunk-UKLSRQ5J.js";
|
|
11
11
|
|
|
12
12
|
// src/admin-ai.ts
|
|
13
|
-
import
|
|
13
|
+
import process13 from "process";
|
|
14
14
|
|
|
15
15
|
// src/token.ts
|
|
16
16
|
import { OdlaError, requestToken } from "@odla-ai/db";
|
|
17
17
|
import { createHash } from "crypto";
|
|
18
|
-
import
|
|
18
|
+
import process10 from "process";
|
|
19
19
|
|
|
20
20
|
// src/handshake-approval.ts
|
|
21
21
|
import process3 from "process";
|
|
@@ -150,27 +150,10 @@ function handshakeWaitMs(waitSeconds, interactive = process4.stdout.isTTY === tr
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
// src/cached-credential.ts
|
|
153
|
-
import { rmSync as
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
function isCredentialRejection(error) {
|
|
159
|
-
const message2 = error instanceof Error ? error.message : String(error ?? "");
|
|
160
|
-
return /\((401|403)\)\s*$/.test(message2.trim());
|
|
161
|
-
}
|
|
162
|
-
function explainRejectedCredential(error) {
|
|
163
|
-
const tokenFile = noted;
|
|
164
|
-
if (!tokenFile || !isCredentialRejection(error)) return null;
|
|
165
|
-
noted = null;
|
|
166
|
-
rmSync2(tokenFile, { force: true });
|
|
167
|
-
return [
|
|
168
|
-
"auth: the cached credential was rejected by odla, so it was revoked before its cached expiry.",
|
|
169
|
-
" The usual cause is a newer sign-in for this project: collecting a handshake retires the",
|
|
170
|
-
" principal's other credentials, so a second terminal or worktree supersedes this one.",
|
|
171
|
-
` Discarded ${tokenFile}; re-run this command to request a fresh approval.`
|
|
172
|
-
].join("\n");
|
|
173
|
-
}
|
|
153
|
+
import { rmSync as rmSync3 } from "fs";
|
|
154
|
+
|
|
155
|
+
// src/auth-guidance.ts
|
|
156
|
+
import process7 from "process";
|
|
174
157
|
|
|
175
158
|
// src/device-session.ts
|
|
176
159
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -206,12 +189,76 @@ async function mintDeviceSession(platformUrl, credential2, doFetch) {
|
|
|
206
189
|
`device session failed: ${detail} (${response2.status})` + (revocable ? " \u2014 if this machine's enrollment was revoked or has expired, enroll it again in Studio" : "")
|
|
207
190
|
);
|
|
208
191
|
}
|
|
209
|
-
return {
|
|
192
|
+
return {
|
|
193
|
+
token: body.token,
|
|
194
|
+
expiresAt: body.expiresAt ?? Date.now(),
|
|
195
|
+
// Absent from a registry that predates rolling expiry and scoped devices.
|
|
196
|
+
// Left undefined rather than defaulted, so a caller can tell "the platform
|
|
197
|
+
// did not say" from "the platform said none".
|
|
198
|
+
...typeof body.deviceExpiresAt === "number" ? { deviceExpiresAt: body.deviceExpiresAt } : {},
|
|
199
|
+
...Array.isArray(body.appIds) ? { appIds: body.appIds } : {},
|
|
200
|
+
...Array.isArray(body.capabilities) ? { capabilities: body.capabilities } : {},
|
|
201
|
+
...Array.isArray(body.scopes) ? { scopes: body.scopes } : {}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/odla-home.ts
|
|
206
|
+
import { chmodSync, copyFileSync, existsSync as existsSync2, mkdirSync, rmSync as rmSync2 } from "fs";
|
|
207
|
+
import { homedir as homedir2 } from "os";
|
|
208
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
209
|
+
import process6 from "process";
|
|
210
|
+
function odlaHome(env = process6.env) {
|
|
211
|
+
if (env.VITEST && !env.ODLA_HOME) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
"ODLA_HOME must be set under test \u2014 resolving the real ~/.odla would write to the developer's machine"
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return env.ODLA_HOME ?? join3(env.HOME ?? homedir2(), ".odla");
|
|
217
|
+
}
|
|
218
|
+
function odlaHomePath(segments, env = process6.env) {
|
|
219
|
+
return join3(odlaHome(env), ...segments);
|
|
220
|
+
}
|
|
221
|
+
function identityFile(env) {
|
|
222
|
+
return odlaHomePath(["identity.json"], env);
|
|
223
|
+
}
|
|
224
|
+
function deviceSessionFile(env) {
|
|
225
|
+
return odlaHomePath(["session.json"], env);
|
|
226
|
+
}
|
|
227
|
+
function appTokenFile(appId, env) {
|
|
228
|
+
return odlaHomePath(["apps", safeSegment(appId), "dev-token.json"], env);
|
|
229
|
+
}
|
|
230
|
+
function appCredentialsFile(appId, env) {
|
|
231
|
+
return odlaHomePath(["apps", safeSegment(appId), "credentials.json"], env);
|
|
232
|
+
}
|
|
233
|
+
function scopedTokenFile(env) {
|
|
234
|
+
return odlaHomePath(["admin-token.local.json"], env);
|
|
235
|
+
}
|
|
236
|
+
function pmContextFile(env) {
|
|
237
|
+
return odlaHomePath(["pm-context.json"], env);
|
|
238
|
+
}
|
|
239
|
+
function adoptRepoLocalCache(legacyPath, machinePath, out) {
|
|
240
|
+
if (!existsSync2(legacyPath) || legacyPath === machinePath) return false;
|
|
241
|
+
const superseded2 = existsSync2(machinePath);
|
|
242
|
+
if (!superseded2) {
|
|
243
|
+
mkdirSync(dirname2(machinePath), { recursive: true });
|
|
244
|
+
copyFileSync(legacyPath, machinePath);
|
|
245
|
+
chmodSync(machinePath, 384);
|
|
246
|
+
}
|
|
247
|
+
rmSync2(legacyPath, { force: true });
|
|
248
|
+
out?.error(
|
|
249
|
+
superseded2 ? `auth: removed superseded ${legacyPath}; this machine's credentials live in ${odlaHome()}` : `auth: moved ${legacyPath} into ${machinePath}; credentials are per machine now, not per worktree`
|
|
250
|
+
);
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
function safeSegment(value2) {
|
|
254
|
+
const clean4 = value2.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "");
|
|
255
|
+
if (!clean4) throw new Error(`"${value2}" is not a usable app id`);
|
|
256
|
+
return clean4;
|
|
210
257
|
}
|
|
211
258
|
|
|
212
259
|
// src/local.ts
|
|
213
|
-
import { chmodSync, existsSync as
|
|
214
|
-
import { dirname as
|
|
260
|
+
import { chmodSync as chmodSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
|
|
261
|
+
import { dirname as dirname3, isAbsolute, relative, resolve } from "path";
|
|
215
262
|
var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
|
|
216
263
|
function readJsonFile(path) {
|
|
217
264
|
try {
|
|
@@ -225,7 +272,7 @@ function writePrivateJson(path, value2) {
|
|
|
225
272
|
`);
|
|
226
273
|
}
|
|
227
274
|
function readCredentials(path) {
|
|
228
|
-
if (!
|
|
275
|
+
if (!existsSync3(path)) return null;
|
|
229
276
|
let value2;
|
|
230
277
|
try {
|
|
231
278
|
value2 = JSON.parse(readFileSync2(path, "utf8"));
|
|
@@ -259,7 +306,7 @@ function mergeCredential(current, update) {
|
|
|
259
306
|
}
|
|
260
307
|
function ensureGitignore(rootDir, localPaths = []) {
|
|
261
308
|
const path = resolve(rootDir, ".gitignore");
|
|
262
|
-
const existing =
|
|
309
|
+
const existing = existsSync3(path) ? readFileSync2(path, "utf8") : "";
|
|
263
310
|
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
|
|
264
311
|
const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
|
|
265
312
|
const missing = wanted.filter((line2) => !existing.split(/\r?\n/).includes(line2));
|
|
@@ -279,7 +326,7 @@ function o11yDevVars(cfg) {
|
|
|
279
326
|
function resolveWriteDevVarsTarget(cfg, requested) {
|
|
280
327
|
if (!requested) return null;
|
|
281
328
|
if (requested === true) return cfg.local.devVarsFile;
|
|
282
|
-
return resolve(
|
|
329
|
+
return resolve(dirname3(cfg.configPath), requested);
|
|
283
330
|
}
|
|
284
331
|
function writeDevVars(path, credentials, env, o11y) {
|
|
285
332
|
const entry = credentials.envs[env];
|
|
@@ -293,7 +340,7 @@ function writeDevVars(path, credentials, env, o11y) {
|
|
|
293
340
|
if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
|
|
294
341
|
if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
|
|
295
342
|
}
|
|
296
|
-
const existing =
|
|
343
|
+
const existing = existsSync3(path) ? readFileSync2(path, "utf8") : "";
|
|
297
344
|
const retained = existing.split(/\r?\n/).filter((line2) => !isManagedDevVar(line2));
|
|
298
345
|
while (retained.at(-1) === "") retained.pop();
|
|
299
346
|
const prefix = retained.length ? `${retained.join("\n")}
|
|
@@ -319,10 +366,10 @@ function isManagedDevVar(line2) {
|
|
|
319
366
|
return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
|
|
320
367
|
}
|
|
321
368
|
function writePrivateText(path, text3) {
|
|
322
|
-
|
|
369
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
323
370
|
const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
324
371
|
writeFileSync(temporary, text3, { mode: 384 });
|
|
325
|
-
|
|
372
|
+
chmodSync2(temporary, 384);
|
|
326
373
|
renameSync(temporary, path);
|
|
327
374
|
}
|
|
328
375
|
function gitignoreEntry(rootDir, path) {
|
|
@@ -335,6 +382,108 @@ function displayPath(path, rootDir = process.cwd()) {
|
|
|
335
382
|
return rel && !rel.startsWith("..") ? rel : path;
|
|
336
383
|
}
|
|
337
384
|
|
|
385
|
+
// src/auth-guidance.ts
|
|
386
|
+
var ENROL_EVERYTHING = "npx odla-ai device enroll --no-open --wait 600";
|
|
387
|
+
var ENROL_PLATFORM_WIDE = "npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600";
|
|
388
|
+
function machineAuthState(audience, env = process7.env) {
|
|
389
|
+
const device = readDeviceCredential(audience, env);
|
|
390
|
+
if (!device) return { enrolled: false };
|
|
391
|
+
const session = readJsonFile(deviceSessionFile(env));
|
|
392
|
+
const current = session?.platform === audience && session.deviceId === device.deviceId ? session : void 0;
|
|
393
|
+
return {
|
|
394
|
+
enrolled: true,
|
|
395
|
+
...device.name ? { deviceName: device.name } : {},
|
|
396
|
+
...current?.appIds ? { appIds: current.appIds } : {},
|
|
397
|
+
...current?.capabilities ? { capabilities: current.capabilities } : {},
|
|
398
|
+
...current?.scopes ? { scopes: current.scopes } : {},
|
|
399
|
+
...current?.deviceExpiresAt ? { lapsesAt: current.deviceExpiresAt } : {}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function scopeInterruptionNotice(scope, state2) {
|
|
403
|
+
const platformScope = scope.startsWith("platform:");
|
|
404
|
+
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}"`;
|
|
405
|
+
return [
|
|
406
|
+
`odla: ${reason}.`,
|
|
407
|
+
` Approve this one now, then end the interruptions with:`,
|
|
408
|
+
` ${platformScope ? ENROL_PLATFORM_WIDE : ENROL_EVERYTHING}`,
|
|
409
|
+
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."
|
|
410
|
+
].join("\n");
|
|
411
|
+
}
|
|
412
|
+
function narrowEnrollmentNotice(envelope) {
|
|
413
|
+
const everyApp = envelope.appIds.includes("*");
|
|
414
|
+
if (everyApp && envelope.capabilities.length > 0 && envelope.scopes.length > 0) return null;
|
|
415
|
+
const missing = [
|
|
416
|
+
everyApp ? null : `only ${envelope.appIds.length} app${envelope.appIds.length === 1 ? "" : "s"} \u2014 a new one will need a new approval`,
|
|
417
|
+
envelope.capabilities.length ? null : "no optional capabilities \u2014 app.manage, crm.read and code.session are not included",
|
|
418
|
+
envelope.scopes.length ? null : "no platform scopes \u2014 runbook edits, config plans and host connect will each ask again"
|
|
419
|
+
].filter(Boolean);
|
|
420
|
+
return [
|
|
421
|
+
`odla: this is a NARROW enrollment: ${missing.join("; ")}.`,
|
|
422
|
+
" That is a fine choice if you meant it. If you did not, drop the flags:",
|
|
423
|
+
` ${ENROL_EVERYTHING}`
|
|
424
|
+
].join("\n");
|
|
425
|
+
}
|
|
426
|
+
function lapseNotice(state2, now = Date.now()) {
|
|
427
|
+
if (!state2.enrolled || !state2.lapsesAt) return null;
|
|
428
|
+
const days = Math.floor((state2.lapsesAt - now) / (24 * 60 * 60 * 1e3));
|
|
429
|
+
if (days < 0) return "this machine's enrollment has lapsed; the next command will ask for approval";
|
|
430
|
+
return `idle for ${days} more day${days === 1 ? "" : "s"} before this machine needs approving again (using it resets the clock)`;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/cached-credential.ts
|
|
434
|
+
var noted = null;
|
|
435
|
+
function noteCachedCredential(tokenFile) {
|
|
436
|
+
noted = tokenFile;
|
|
437
|
+
}
|
|
438
|
+
function isCredentialRejection(error) {
|
|
439
|
+
const message2 = error instanceof Error ? error.message : String(error ?? "");
|
|
440
|
+
return /\((401|403)\)\s*$/.test(message2.trim());
|
|
441
|
+
}
|
|
442
|
+
function explainRejectedCredential(error) {
|
|
443
|
+
const tokenFile = noted;
|
|
444
|
+
if (!tokenFile || !isCredentialRejection(error)) return null;
|
|
445
|
+
noted = null;
|
|
446
|
+
rmSync3(tokenFile, { force: true });
|
|
447
|
+
return [
|
|
448
|
+
"auth: the cached credential was rejected by odla, so it was revoked before its cached expiry.",
|
|
449
|
+
" The usual cause is a newer sign-in for this account: collecting a handshake retires the",
|
|
450
|
+
" principal's other collected credentials, so a second machine supersedes this one.",
|
|
451
|
+
` Discarded ${tokenFile}; re-run this command to request a fresh approval.`,
|
|
452
|
+
` To stop needing one: ${ENROL_EVERYTHING}`
|
|
453
|
+
].join("\n");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/device-session-cache.ts
|
|
457
|
+
import process8 from "process";
|
|
458
|
+
var SKEW_MS = 6e4;
|
|
459
|
+
async function deviceSessionToken(platformUrl, audience, credential2, doFetch, env = process8.env) {
|
|
460
|
+
const path = deviceSessionFile(env);
|
|
461
|
+
const cached = readJsonFile(path);
|
|
462
|
+
if (cached?.token && cached.platform === audience && cached.deviceId === credential2.deviceId && (cached.expiresAt ?? 0) > Date.now() + SKEW_MS) return cached;
|
|
463
|
+
const minted = await mintDeviceSession(platformUrl, credential2, doFetch);
|
|
464
|
+
const session = {
|
|
465
|
+
...minted,
|
|
466
|
+
platform: audience,
|
|
467
|
+
...credential2.deviceId ? { deviceId: credential2.deviceId } : {}
|
|
468
|
+
};
|
|
469
|
+
writePrivateJson(path, session);
|
|
470
|
+
return session;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/machine-identity.ts
|
|
474
|
+
import process9 from "process";
|
|
475
|
+
function readMachineIdentity(audience, env = process9.env) {
|
|
476
|
+
const stored = readJsonFile(identityFile(env));
|
|
477
|
+
if (!stored || typeof stored.email !== "string" || !stored.email) return null;
|
|
478
|
+
return stored.platform === audience ? { platform: audience, email: stored.email } : null;
|
|
479
|
+
}
|
|
480
|
+
function rememberMachineIdentity(audience, email, env = process9.env) {
|
|
481
|
+
if (!email) return;
|
|
482
|
+
const existing = readMachineIdentity(audience, env);
|
|
483
|
+
if (existing?.email === email) return;
|
|
484
|
+
writePrivateJson(identityFile(env), { platform: audience, email });
|
|
485
|
+
}
|
|
486
|
+
|
|
338
487
|
// src/token.ts
|
|
339
488
|
async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {}) {
|
|
340
489
|
const audience = platformAudience(cfg.platformUrl);
|
|
@@ -343,19 +492,19 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
|
|
|
343
492
|
const cached = readJsonFile(cfg.local.tokenFile);
|
|
344
493
|
if (!grantRequest.forceReview && !grantRequest.freshLogin) {
|
|
345
494
|
if (options.token) return options.token;
|
|
346
|
-
if (
|
|
347
|
-
const declared =
|
|
495
|
+
if (process10.env.ODLA_DEV_TOKEN) {
|
|
496
|
+
const declared = process10.env.ODLA_DEV_TOKEN_AUDIENCE;
|
|
348
497
|
if (declared) {
|
|
349
498
|
if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
|
|
350
499
|
} else if (audience !== "https://odla.ai") {
|
|
351
500
|
throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
|
|
352
501
|
}
|
|
353
|
-
return
|
|
502
|
+
return process10.env.ODLA_DEV_TOKEN;
|
|
354
503
|
}
|
|
355
504
|
const device = readDeviceCredential(audience);
|
|
356
505
|
if (device) {
|
|
357
|
-
const session = await
|
|
358
|
-
out.error(`auth: session
|
|
506
|
+
const session = await deviceSessionToken(cfg.platformUrl, audience, device, doFetch);
|
|
507
|
+
out.error(`auth: session held by this enrolled device (${displayPath(deviceCredentialPath(), cfg.rootDir)})`);
|
|
359
508
|
return session.token;
|
|
360
509
|
}
|
|
361
510
|
if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4 && cachedGrantCovers(cached, grantIntent)) {
|
|
@@ -375,7 +524,10 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
|
|
|
375
524
|
doFetch,
|
|
376
525
|
out,
|
|
377
526
|
audience,
|
|
378
|
-
email: handshakeEmail(
|
|
527
|
+
email: handshakeEmail(
|
|
528
|
+
options.email,
|
|
529
|
+
(cached?.platform === audience ? cached.email : void 0) ?? readMachineIdentity(audience)?.email
|
|
530
|
+
),
|
|
379
531
|
pendingFile: handshakeFile(cfg),
|
|
380
532
|
grantIntent
|
|
381
533
|
};
|
|
@@ -392,6 +544,7 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
|
|
|
392
544
|
expiresAt
|
|
393
545
|
});
|
|
394
546
|
out.error(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
547
|
+
rememberMachineIdentity(audience, ctx.email);
|
|
395
548
|
return token;
|
|
396
549
|
}
|
|
397
550
|
async function freshHandshake(ctx, waitMs) {
|
|
@@ -461,7 +614,7 @@ function stillPending(pending, email) {
|
|
|
461
614
|
);
|
|
462
615
|
}
|
|
463
616
|
function handshakeEmail(value2, cached) {
|
|
464
|
-
const email = (value2 ??
|
|
617
|
+
const email = (value2 ?? process10.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
|
|
465
618
|
if (/@users\.noreply\.github\.com$/i.test(email)) {
|
|
466
619
|
throw new Error(
|
|
467
620
|
`"${email}" is a GitHub commit identity, not an odla account email; use --email <signed-in-odla-account> or ODLA_USER_EMAIL`
|
|
@@ -492,12 +645,12 @@ function platformAudience(value2) {
|
|
|
492
645
|
}
|
|
493
646
|
|
|
494
647
|
// src/secret-input.ts
|
|
495
|
-
import
|
|
648
|
+
import process11 from "process";
|
|
496
649
|
var MAX_BYTES = 64 * 1024;
|
|
497
650
|
async function secretInputValue(options, kind = "credential") {
|
|
498
651
|
if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
|
|
499
652
|
let value2;
|
|
500
|
-
if (options.fromEnv) value2 =
|
|
653
|
+
if (options.fromEnv) value2 = process11.env[options.fromEnv];
|
|
501
654
|
else if (options.stdin) value2 = await (options.readStdin ?? (() => readSecretStream(kind)))();
|
|
502
655
|
else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
|
|
503
656
|
value2 = value2?.replace(/[\r\n]+$/, "");
|
|
@@ -505,7 +658,7 @@ async function secretInputValue(options, kind = "credential") {
|
|
|
505
658
|
if (new TextEncoder().encode(value2).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
|
|
506
659
|
return value2;
|
|
507
660
|
}
|
|
508
|
-
async function readSecretStream(kind, stream =
|
|
661
|
+
async function readSecretStream(kind, stream = process11.stdin) {
|
|
509
662
|
let value2 = "";
|
|
510
663
|
for await (const chunk of stream) {
|
|
511
664
|
value2 += String(chunk);
|
|
@@ -515,9 +668,8 @@ async function readSecretStream(kind, stream = process7.stdin) {
|
|
|
515
668
|
}
|
|
516
669
|
|
|
517
670
|
// src/admin-ai-auth.ts
|
|
518
|
-
import {
|
|
519
|
-
import
|
|
520
|
-
import process8 from "process";
|
|
671
|
+
import { join as join4 } from "path";
|
|
672
|
+
import process12 from "process";
|
|
521
673
|
import { requestToken as requestToken2 } from "@odla-ai/db";
|
|
522
674
|
async function getScopedPlatformToken(options) {
|
|
523
675
|
return resolveAdminPlatformToken(options);
|
|
@@ -525,7 +677,7 @@ async function getScopedPlatformToken(options) {
|
|
|
525
677
|
async function resolveAdminPlatformToken(options) {
|
|
526
678
|
const audience = platformAudience(options.platform);
|
|
527
679
|
if (options.token) return options.token;
|
|
528
|
-
const fromEnv =
|
|
680
|
+
const fromEnv = process12.env.ODLA_ADMIN_TOKEN;
|
|
529
681
|
if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
|
|
530
682
|
return scopedToken(
|
|
531
683
|
audience,
|
|
@@ -537,7 +689,7 @@ async function resolveAdminPlatformToken(options) {
|
|
|
537
689
|
}
|
|
538
690
|
function audienceBoundEnvToken(token, platform) {
|
|
539
691
|
const audience = platformAudience(platform);
|
|
540
|
-
const declared =
|
|
692
|
+
const declared = process12.env.ODLA_ADMIN_TOKEN_AUDIENCE;
|
|
541
693
|
if (declared) {
|
|
542
694
|
if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
|
|
543
695
|
} else if (audience !== "https://odla.ai") {
|
|
@@ -564,15 +716,28 @@ var SCOPE_PURPOSE = {
|
|
|
564
716
|
};
|
|
565
717
|
async function scopedToken(platform, scope, options, doFetch, out) {
|
|
566
718
|
const audience = platformAudience(platform);
|
|
567
|
-
const rootDir = options.rootDir ??
|
|
568
|
-
const tokenFile = options.tokenFile ??
|
|
719
|
+
const rootDir = options.rootDir ?? process12.cwd();
|
|
720
|
+
const tokenFile = options.tokenFile ?? scopedTokenFile();
|
|
721
|
+
adoptRepoLocalCache(join4(rootDir, ".odla/admin-token.local.json"), tokenFile, out);
|
|
722
|
+
const device = readDeviceCredential(audience);
|
|
723
|
+
if (device && options.cache !== false) {
|
|
724
|
+
const session = await deviceSessionToken(platform, audience, device, doFetch);
|
|
725
|
+
if (session.scopes?.includes(scope)) {
|
|
726
|
+
out.error(`auth: ${scope} held by this enrolled device`);
|
|
727
|
+
return session.token;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
569
730
|
const cache2 = options.cache === false ? null : readJsonFile(tokenFile);
|
|
570
731
|
const cached = cache2?.platform === audience ? cache2.tokens?.[scope] : void 0;
|
|
571
732
|
if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
572
733
|
out.error(`auth: using cached ${scope} grant (${tokenFile})`);
|
|
573
734
|
return cached.token;
|
|
574
735
|
}
|
|
575
|
-
|
|
736
|
+
out.error(scopeInterruptionNotice(scope, machineAuthState(audience)));
|
|
737
|
+
const email = handshakeEmail(
|
|
738
|
+
options.email,
|
|
739
|
+
(cache2?.platform === audience ? cache2.email : void 0) ?? readMachineIdentity(audience)?.email
|
|
740
|
+
);
|
|
576
741
|
const { token, expiresAt } = await requestToken2({
|
|
577
742
|
endpoint: audience,
|
|
578
743
|
email,
|
|
@@ -592,8 +757,8 @@ async function scopedToken(platform, scope, options, doFetch, out) {
|
|
|
592
757
|
if (options.cache !== false) {
|
|
593
758
|
const tokens = cache2?.platform === audience ? { ...cache2.tokens ?? {} } : {};
|
|
594
759
|
tokens[scope] = { token, expiresAt };
|
|
595
|
-
if (existsSync3(join3(rootDir, ".git"))) ensureGitignore(rootDir, [tokenFile]);
|
|
596
760
|
writePrivateJson(tokenFile, { platform: audience, email, tokens });
|
|
761
|
+
rememberMachineIdentity(audience, email);
|
|
597
762
|
out.error(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
|
|
598
763
|
} else {
|
|
599
764
|
out.error(`auth: ${scope} grant is in memory only; its credential record remains in odla-ai/db`);
|
|
@@ -769,7 +934,7 @@ function isRecord2(value2) {
|
|
|
769
934
|
|
|
770
935
|
// src/admin-ai.ts
|
|
771
936
|
async function adminAi(options) {
|
|
772
|
-
const platform = platformAudience(options.platform ??
|
|
937
|
+
const platform = platformAudience(options.platform ?? process13.env.ODLA_PLATFORM ?? "https://odla.ai");
|
|
773
938
|
const doFetch = options.fetch ?? fetch;
|
|
774
939
|
const out = options.stdout ?? console;
|
|
775
940
|
const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
|
|
@@ -1085,12 +1250,12 @@ async function adminSpend(parsed, ctx) {
|
|
|
1085
1250
|
|
|
1086
1251
|
// src/operator-context.ts
|
|
1087
1252
|
import { existsSync as existsSync6 } from "fs";
|
|
1088
|
-
import { join as
|
|
1089
|
-
import
|
|
1253
|
+
import { join as join6, resolve as resolve4 } from "path";
|
|
1254
|
+
import process15 from "process";
|
|
1090
1255
|
|
|
1091
1256
|
// src/config.ts
|
|
1092
|
-
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
1093
|
-
import { dirname as
|
|
1257
|
+
import { existsSync as existsSync4, rmSync as rmSync4, readFileSync as readFileSync3 } from "fs";
|
|
1258
|
+
import { dirname as dirname4, isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
|
|
1094
1259
|
import { pathToFileURL } from "url";
|
|
1095
1260
|
import { appServiceDefinition, appServiceIds } from "@odla-ai/apps";
|
|
1096
1261
|
|
|
@@ -1494,13 +1659,17 @@ var DEFAULT_ENVS = ["dev"];
|
|
|
1494
1659
|
var DEFAULT_SERVICES = ["db", "ai"];
|
|
1495
1660
|
var configImportSerial = 0;
|
|
1496
1661
|
var GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events";
|
|
1662
|
+
var stderr = { error: (message2) => {
|
|
1663
|
+
process.stderr.write(`${message2}
|
|
1664
|
+
`);
|
|
1665
|
+
} };
|
|
1497
1666
|
async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
|
|
1498
1667
|
const resolved = resolve2(configPath);
|
|
1499
1668
|
if (!existsSync4(resolved)) {
|
|
1500
1669
|
throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
|
|
1501
1670
|
}
|
|
1502
1671
|
const raw = await loadConfigModule(resolved);
|
|
1503
|
-
const rootDir =
|
|
1672
|
+
const rootDir = dirname4(resolved);
|
|
1504
1673
|
validateRawConfig(raw, resolved);
|
|
1505
1674
|
const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
|
|
1506
1675
|
const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
|
|
@@ -1510,11 +1679,14 @@ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
|
|
|
1510
1679
|
validateCalendarConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
|
|
1511
1680
|
validateMonitoringConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
|
|
1512
1681
|
const local = {
|
|
1513
|
-
tokenFile: resolve2(rootDir, raw.local
|
|
1514
|
-
credentialsFile: resolve2(rootDir, raw.local
|
|
1682
|
+
tokenFile: raw.local?.tokenFile ? resolve2(rootDir, raw.local.tokenFile) : appTokenFile(raw.app.id),
|
|
1683
|
+
credentialsFile: raw.local?.credentialsFile ? resolve2(rootDir, raw.local.credentialsFile) : appCredentialsFile(raw.app.id),
|
|
1515
1684
|
devVarsFile: resolve2(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
|
|
1516
1685
|
gitignore: raw.local?.gitignore ?? true
|
|
1517
1686
|
};
|
|
1687
|
+
adoptRepoLocalCache(resolve2(rootDir, ".odla/dev-token.json"), local.tokenFile, stderr);
|
|
1688
|
+
adoptRepoLocalCache(resolve2(rootDir, ".odla/credentials.local.json"), local.credentialsFile, stderr);
|
|
1689
|
+
rmSync4(resolve2(rootDir, ".odla/handshake.local.json"), { force: true });
|
|
1518
1690
|
return {
|
|
1519
1691
|
...raw,
|
|
1520
1692
|
configPath: resolved,
|
|
@@ -1623,17 +1795,17 @@ function unique3(values) {
|
|
|
1623
1795
|
|
|
1624
1796
|
// src/operator-profiles.ts
|
|
1625
1797
|
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
1626
|
-
import { homedir as
|
|
1627
|
-
import { dirname as
|
|
1628
|
-
import
|
|
1798
|
+
import { homedir as homedir3 } from "os";
|
|
1799
|
+
import { dirname as dirname5, join as join5, resolve as resolve3 } from "path";
|
|
1800
|
+
import process14 from "process";
|
|
1629
1801
|
function operatorProfileFile() {
|
|
1630
1802
|
return resolve3(
|
|
1631
|
-
clean(
|
|
1803
|
+
clean(process14.env.ODLA_CONTEXT_FILE) ?? join5(homedir3(), ".odla", "contexts.json")
|
|
1632
1804
|
);
|
|
1633
1805
|
}
|
|
1634
1806
|
function resolveOperatorProfile(parsed) {
|
|
1635
1807
|
const fromFlag = clean(stringOpt(parsed.options.context));
|
|
1636
|
-
const fromEnvironment = clean(
|
|
1808
|
+
const fromEnvironment = clean(process14.env.ODLA_CONTEXT);
|
|
1637
1809
|
const name = fromFlag ?? fromEnvironment ?? null;
|
|
1638
1810
|
const file = operatorProfileFile();
|
|
1639
1811
|
if (!name) {
|
|
@@ -1673,10 +1845,10 @@ function removeOperatorProfile(name, file = operatorProfileFile()) {
|
|
|
1673
1845
|
return true;
|
|
1674
1846
|
}
|
|
1675
1847
|
function operatorCredentialFiles(selection) {
|
|
1676
|
-
const base = selection.name ?
|
|
1848
|
+
const base = selection.name ? join5(dirname5(selection.file), "profiles", selection.name) : join5(homedir3(), ".odla");
|
|
1677
1849
|
return {
|
|
1678
|
-
developer:
|
|
1679
|
-
scoped:
|
|
1850
|
+
developer: join5(base, "dev-token.json"),
|
|
1851
|
+
scoped: join5(base, "admin-token.local.json")
|
|
1680
1852
|
};
|
|
1681
1853
|
}
|
|
1682
1854
|
function assertOperatorName(value2, label) {
|
|
@@ -1762,13 +1934,13 @@ async function resolveOperatorContext(parsed, options = {}) {
|
|
|
1762
1934
|
}
|
|
1763
1935
|
const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
|
|
1764
1936
|
const platformFlag = clean2(stringOpt(parsed.options.platform));
|
|
1765
|
-
const platformEnvironment = clean2(
|
|
1937
|
+
const platformEnvironment = clean2(process15.env.ODLA_PLATFORM_URL);
|
|
1766
1938
|
const platformValue = platformAudience(
|
|
1767
1939
|
platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
|
|
1768
1940
|
);
|
|
1769
1941
|
const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
|
|
1770
1942
|
const appFlag = clean2(stringOpt(parsed.options.app));
|
|
1771
|
-
const appEnvironment = clean2(
|
|
1943
|
+
const appEnvironment = clean2(process15.env.ODLA_APP_ID);
|
|
1772
1944
|
const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
|
|
1773
1945
|
const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
|
|
1774
1946
|
if (appValue) {
|
|
@@ -1782,16 +1954,16 @@ async function resolveOperatorContext(parsed, options = {}) {
|
|
|
1782
1954
|
);
|
|
1783
1955
|
}
|
|
1784
1956
|
const envFlag = clean2(stringOpt(parsed.options.env));
|
|
1785
|
-
const envEnvironment = clean2(
|
|
1957
|
+
const envEnvironment = clean2(process15.env.ODLA_ENV);
|
|
1786
1958
|
const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
|
|
1787
1959
|
const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
|
|
1788
1960
|
if (environmentValue) {
|
|
1789
1961
|
assertOperatorName(environmentValue, "environment");
|
|
1790
1962
|
}
|
|
1791
|
-
const rootDir = loaded?.rootDir ??
|
|
1963
|
+
const rootDir = loaded?.rootDir ?? process15.cwd();
|
|
1792
1964
|
const profileCredentials = operatorCredentialFiles(profile);
|
|
1793
|
-
const tokenFile = clean2(
|
|
1794
|
-
const
|
|
1965
|
+
const tokenFile = clean2(process15.env.ODLA_DEV_TOKEN_FILE) ? resolve4(process15.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
|
|
1966
|
+
const scopedTokenFile2 = clean2(process15.env.ODLA_ADMIN_TOKEN_FILE) ? resolve4(process15.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? join6(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
|
|
1795
1967
|
const cfg = loaded ? {
|
|
1796
1968
|
...loaded,
|
|
1797
1969
|
platformUrl: platformValue,
|
|
@@ -1813,8 +1985,8 @@ async function resolveOperatorContext(parsed, options = {}) {
|
|
|
1813
1985
|
services: [],
|
|
1814
1986
|
local: {
|
|
1815
1987
|
tokenFile,
|
|
1816
|
-
credentialsFile:
|
|
1817
|
-
devVarsFile:
|
|
1988
|
+
credentialsFile: join6(rootDir, ".odla", "credentials.local.json"),
|
|
1989
|
+
devVarsFile: join6(rootDir, ".dev.vars"),
|
|
1818
1990
|
gitignore: true
|
|
1819
1991
|
}
|
|
1820
1992
|
};
|
|
@@ -1838,7 +2010,7 @@ async function resolveOperatorContext(parsed, options = {}) {
|
|
|
1838
2010
|
},
|
|
1839
2011
|
credentials: {
|
|
1840
2012
|
developerTokenFile: tokenFile,
|
|
1841
|
-
scopedTokenFile
|
|
2013
|
+
scopedTokenFile: scopedTokenFile2
|
|
1842
2014
|
}
|
|
1843
2015
|
};
|
|
1844
2016
|
}
|
|
@@ -1936,7 +2108,7 @@ async function adminCommand(parsed, deps = {}) {
|
|
|
1936
2108
|
}
|
|
1937
2109
|
|
|
1938
2110
|
// src/auth-command.ts
|
|
1939
|
-
import
|
|
2111
|
+
import process16 from "process";
|
|
1940
2112
|
|
|
1941
2113
|
// src/whoami-command.ts
|
|
1942
2114
|
var text2 = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
|
|
@@ -2072,6 +2244,7 @@ async function whoamiCommand(parsed, deps = {}) {
|
|
|
2072
2244
|
} else {
|
|
2073
2245
|
out.log("projects: (none \u2014 every pm and discuss call will be refused)");
|
|
2074
2246
|
}
|
|
2247
|
+
printMachineBlock(cfg.platformUrl, out);
|
|
2075
2248
|
if (!identity.admin) {
|
|
2076
2249
|
if (identity.scopes.includes("platform:runbook:write")) {
|
|
2077
2250
|
out.log("\nThis exact scope can read and edit all platform runbook content.");
|
|
@@ -2082,6 +2255,21 @@ async function whoamiCommand(parsed, deps = {}) {
|
|
|
2082
2255
|
}
|
|
2083
2256
|
}
|
|
2084
2257
|
}
|
|
2258
|
+
function printMachineBlock(platformUrl, out) {
|
|
2259
|
+
const state2 = machineAuthState(platformAudience(platformUrl));
|
|
2260
|
+
if (!state2.enrolled) {
|
|
2261
|
+
out.log("\nmachine: not enrolled \u2014 every privileged command needs its own browser approval.");
|
|
2262
|
+
out.log(` End that with:
|
|
2263
|
+
${ENROL_EVERYTHING}`);
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
const reach = state2.appIds?.includes("*") ? "every app you own" : state2.appIds?.join(", ");
|
|
2267
|
+
out.log(`
|
|
2268
|
+
machine: enrolled${state2.deviceName ? ` as "${state2.deviceName}"` : ""}${reach ? ` for ${reach}` : ""}`);
|
|
2269
|
+
if (state2.scopes?.length) out.log(` carrying ${state2.scopes.join(", ")}`);
|
|
2270
|
+
const lapse = lapseNotice(state2);
|
|
2271
|
+
if (lapse) out.log(` ${lapse}`);
|
|
2272
|
+
}
|
|
2085
2273
|
|
|
2086
2274
|
// src/auth-command.ts
|
|
2087
2275
|
async function authCommand(parsed, deps = {}) {
|
|
@@ -2106,12 +2294,17 @@ async function authCommand(parsed, deps = {}) {
|
|
|
2106
2294
|
const { cfg } = context;
|
|
2107
2295
|
const out = deps.stdout ?? console;
|
|
2108
2296
|
const doFetch = deps.fetch ?? fetch;
|
|
2109
|
-
const email = stringOpt(parsed.options.email) ??
|
|
2297
|
+
const email = stringOpt(parsed.options.email) ?? process16.env.ODLA_USER_EMAIL?.trim();
|
|
2110
2298
|
if (!email) {
|
|
2111
2299
|
throw new Error(
|
|
2112
2300
|
"auth login requires --email <odla-account> or ODLA_USER_EMAIL; confirm the signed-in odla email instead of using git or GitHub identity"
|
|
2113
2301
|
);
|
|
2114
2302
|
}
|
|
2303
|
+
if (!machineAuthState(platformAudience(cfg.platformUrl)).enrolled) {
|
|
2304
|
+
out.error("odla: this machine is not enrolled, so this approval buys one project until it lapses.");
|
|
2305
|
+
out.error(` For one approval that covers every app you own, with no repeats:
|
|
2306
|
+
${ENROL_EVERYTHING}`);
|
|
2307
|
+
}
|
|
2115
2308
|
const token = await getDeveloperToken(
|
|
2116
2309
|
cfg,
|
|
2117
2310
|
{
|
|
@@ -2471,7 +2664,7 @@ async function appCommand(parsed, dependencies = {}) {
|
|
|
2471
2664
|
|
|
2472
2665
|
// src/brand-command.ts
|
|
2473
2666
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
2474
|
-
import { dirname as
|
|
2667
|
+
import { dirname as dirname6, resolve as resolve5 } from "path";
|
|
2475
2668
|
|
|
2476
2669
|
// src/brand-design-unpack.ts
|
|
2477
2670
|
import { gunzipSync } from "zlib";
|
|
@@ -2586,7 +2779,7 @@ async function readBundle(source, deps) {
|
|
|
2586
2779
|
async function writeAll(result, outDir) {
|
|
2587
2780
|
for (const file of result.files) {
|
|
2588
2781
|
const target = resolve5(outDir, file.path);
|
|
2589
|
-
await mkdir(
|
|
2782
|
+
await mkdir(dirname6(target), { recursive: true });
|
|
2590
2783
|
await writeFile(target, file.bytes);
|
|
2591
2784
|
}
|
|
2592
2785
|
}
|
|
@@ -3152,7 +3345,7 @@ import {
|
|
|
3152
3345
|
AppsError,
|
|
3153
3346
|
createAppsClient
|
|
3154
3347
|
} from "@odla-ai/apps";
|
|
3155
|
-
import { join as
|
|
3348
|
+
import { join as join7 } from "path";
|
|
3156
3349
|
|
|
3157
3350
|
// src/config-operation-error.ts
|
|
3158
3351
|
var ConfigOperationCommandError = class extends Error {
|
|
@@ -3577,7 +3770,7 @@ async function operationClient(cfg, options, purpose) {
|
|
|
3577
3770
|
platform: cfg.platformUrl,
|
|
3578
3771
|
scope: "app:config:write",
|
|
3579
3772
|
token: options.token,
|
|
3580
|
-
tokenFile:
|
|
3773
|
+
tokenFile: join7(cfg.rootDir, ".odla", "admin-token.local.json"),
|
|
3581
3774
|
rootDir: cfg.rootDir,
|
|
3582
3775
|
email: options.email,
|
|
3583
3776
|
open: options.open,
|
|
@@ -3632,7 +3825,7 @@ function record4(value2) {
|
|
|
3632
3825
|
|
|
3633
3826
|
// src/config-reconcile-command.ts
|
|
3634
3827
|
import { createAppsClient as createAppsClient2, studioAppSettingsPath } from "@odla-ai/apps";
|
|
3635
|
-
import { join as
|
|
3828
|
+
import { join as join8 } from "path";
|
|
3636
3829
|
|
|
3637
3830
|
// src/config-reconcile.ts
|
|
3638
3831
|
import { appServiceIds as appServiceIds2, orderAppServices as orderAppServices2 } from "@odla-ai/apps";
|
|
@@ -3928,7 +4121,7 @@ async function inspectConfig(options) {
|
|
|
3928
4121
|
platform: cfg.platformUrl,
|
|
3929
4122
|
scope: "app:config:read",
|
|
3930
4123
|
token: options.token,
|
|
3931
|
-
tokenFile:
|
|
4124
|
+
tokenFile: join8(cfg.rootDir, ".odla", "admin-token.local.json"),
|
|
3932
4125
|
rootDir: cfg.rootDir,
|
|
3933
4126
|
email: options.email,
|
|
3934
4127
|
open: options.open,
|
|
@@ -4061,26 +4254,26 @@ function quoteArg2(value2) {
|
|
|
4061
4254
|
// src/doctor-checks.ts
|
|
4062
4255
|
import { execFileSync } from "child_process";
|
|
4063
4256
|
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
4064
|
-
import { join as
|
|
4257
|
+
import { join as join10, resolve as resolve6 } from "path";
|
|
4065
4258
|
|
|
4066
4259
|
// src/wrangler.ts
|
|
4067
4260
|
import { spawn as spawn2 } from "child_process";
|
|
4068
4261
|
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
4069
|
-
import { join as
|
|
4262
|
+
import { join as join9 } from "path";
|
|
4070
4263
|
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
4071
4264
|
const child = spawn2(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
4072
4265
|
let stdout = "";
|
|
4073
|
-
let
|
|
4266
|
+
let stderr2 = "";
|
|
4074
4267
|
child.stdout.on("data", (chunk) => stdout += chunk.toString());
|
|
4075
|
-
child.stderr.on("data", (chunk) =>
|
|
4268
|
+
child.stderr.on("data", (chunk) => stderr2 += chunk.toString());
|
|
4076
4269
|
child.on("error", reject);
|
|
4077
|
-
child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr }));
|
|
4270
|
+
child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr: stderr2 }));
|
|
4078
4271
|
child.stdin.end(opts?.input ?? "");
|
|
4079
4272
|
});
|
|
4080
4273
|
var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
4081
4274
|
function findWranglerConfig(rootDir) {
|
|
4082
4275
|
for (const name of WRANGLER_CONFIG_FILES) {
|
|
4083
|
-
const path =
|
|
4276
|
+
const path = join9(rootDir, name);
|
|
4084
4277
|
if (existsSync7(path)) return path;
|
|
4085
4278
|
}
|
|
4086
4279
|
return null;
|
|
@@ -4231,21 +4424,21 @@ function wranglerWarnings(rootDir) {
|
|
|
4231
4424
|
const blocks = [{ label: "", block: config }];
|
|
4232
4425
|
const envs = config.env;
|
|
4233
4426
|
if (envs && typeof envs === "object") {
|
|
4234
|
-
for (const [name,
|
|
4235
|
-
if (
|
|
4427
|
+
for (const [name, block2] of Object.entries(envs)) {
|
|
4428
|
+
if (block2 && typeof block2 === "object") blocks.push({ label: `env.${name}.`, block: block2 });
|
|
4236
4429
|
}
|
|
4237
4430
|
}
|
|
4238
|
-
for (const { label, block } of blocks) {
|
|
4239
|
-
const assets =
|
|
4431
|
+
for (const { label, block: block2 } of blocks) {
|
|
4432
|
+
const assets = block2.assets;
|
|
4240
4433
|
if (assets?.directory) {
|
|
4241
4434
|
const dir = resolve6(rootDir, assets.directory);
|
|
4242
4435
|
if (dir === resolve6(rootDir)) {
|
|
4243
4436
|
warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
|
|
4244
|
-
} else if (existsSync8(
|
|
4437
|
+
} else if (existsSync8(join10(dir, "node_modules"))) {
|
|
4245
4438
|
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
4246
4439
|
}
|
|
4247
4440
|
}
|
|
4248
|
-
const vars =
|
|
4441
|
+
const vars = block2.vars;
|
|
4249
4442
|
if (vars && typeof vars === "object") {
|
|
4250
4443
|
for (const [name, value2] of Object.entries(vars)) {
|
|
4251
4444
|
if (name === "ODLA_API_KEY" || name === "ODLA_O11Y_TOKEN" || typeof value2 === "string" && looksSecret(value2)) {
|
|
@@ -4306,7 +4499,7 @@ function calendarProjectWarnings(rootDir) {
|
|
|
4306
4499
|
}
|
|
4307
4500
|
function readPackageJson(rootDir) {
|
|
4308
4501
|
try {
|
|
4309
|
-
return JSON.parse(readFileSync8(
|
|
4502
|
+
return JSON.parse(readFileSync8(join10(rootDir, "package.json"), "utf8"));
|
|
4310
4503
|
} catch {
|
|
4311
4504
|
return null;
|
|
4312
4505
|
}
|
|
@@ -4600,8 +4793,8 @@ function harnessOption(value2, flag) {
|
|
|
4600
4793
|
}
|
|
4601
4794
|
|
|
4602
4795
|
// src/init.ts
|
|
4603
|
-
import { existsSync as existsSync9, mkdirSync as
|
|
4604
|
-
import { dirname as
|
|
4796
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
4797
|
+
import { dirname as dirname7, resolve as resolve7 } from "path";
|
|
4605
4798
|
import { appServiceDefinition as appServiceDefinition3, appServiceIds as appServiceIds3 } from "@odla-ai/apps";
|
|
4606
4799
|
function initProject(options) {
|
|
4607
4800
|
const out = options.stdout ?? console;
|
|
@@ -4623,9 +4816,9 @@ function initProject(options) {
|
|
|
4623
4816
|
}
|
|
4624
4817
|
}
|
|
4625
4818
|
const aiProvider = options.aiProvider;
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4819
|
+
mkdirSync3(dirname7(configPath), { recursive: true });
|
|
4820
|
+
mkdirSync3(resolve7(rootDir, "src/odla"), { recursive: true });
|
|
4821
|
+
mkdirSync3(resolve7(rootDir, ".odla"), { recursive: true });
|
|
4629
4822
|
writeFileSync2(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
4630
4823
|
writeIfMissing(resolve7(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
4631
4824
|
writeIfMissing(resolve7(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
@@ -4695,8 +4888,10 @@ ${calendar}
|
|
|
4695
4888
|
// prod: "https://example.com",
|
|
4696
4889
|
},
|
|
4697
4890
|
local: {
|
|
4698
|
-
|
|
4699
|
-
|
|
4891
|
+
// Credentials live in ~/.odla, per machine, so every worktree of this app
|
|
4892
|
+
// shares one approval instead of asking for its own. Pinning tokenFile or
|
|
4893
|
+
// credentialsFile here still works and still overrides that \u2014 it just puts
|
|
4894
|
+
// this checkout back on its own island.
|
|
4700
4895
|
devVarsFile: ".dev.vars",
|
|
4701
4896
|
},
|
|
4702
4897
|
};
|
|
@@ -4932,9 +5127,9 @@ function printReport(report5, out) {
|
|
|
4932
5127
|
}
|
|
4933
5128
|
|
|
4934
5129
|
// src/skill.ts
|
|
4935
|
-
import { existsSync as existsSync10, lstatSync, mkdirSync as
|
|
4936
|
-
import { homedir as
|
|
4937
|
-
import { dirname as
|
|
5130
|
+
import { existsSync as existsSync10, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync9, readdirSync, writeFileSync as writeFileSync3 } from "fs";
|
|
5131
|
+
import { homedir as homedir4 } from "os";
|
|
5132
|
+
import { dirname as dirname8, isAbsolute as isAbsolute3, join as join11, relative as relative2, resolve as resolve8, sep } from "path";
|
|
4938
5133
|
import { fileURLToPath } from "url";
|
|
4939
5134
|
|
|
4940
5135
|
// src/skill-adapters.ts
|
|
@@ -5034,7 +5229,7 @@ function installSkill(options = {}) {
|
|
|
5034
5229
|
if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
|
|
5035
5230
|
const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
|
|
5036
5231
|
const root = resolve8(options.dir ?? process.cwd());
|
|
5037
|
-
const home = resolve8(options.homeDir ??
|
|
5232
|
+
const home = resolve8(options.homeDir ?? homedir4());
|
|
5038
5233
|
const plans = /* @__PURE__ */ new Map();
|
|
5039
5234
|
const targets = /* @__PURE__ */ new Map();
|
|
5040
5235
|
const rememberTarget = (harness, target) => {
|
|
@@ -5048,48 +5243,48 @@ function installSkill(options = {}) {
|
|
|
5048
5243
|
plans.set(target, { target, content: content2, boundary, managedMerge });
|
|
5049
5244
|
};
|
|
5050
5245
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
5051
|
-
for (const rel of files) plan(
|
|
5246
|
+
for (const rel of files) plan(join11(targetDir2, rel), readFileSync9(join11(sourceDir, rel), "utf8"), false, boundary);
|
|
5052
5247
|
};
|
|
5053
5248
|
let targetDir;
|
|
5054
5249
|
if (options.global) {
|
|
5055
|
-
const claudeRoot =
|
|
5056
|
-
const codexRoot = resolve8(options.codexHomeDir ?? process.env.CODEX_HOME ??
|
|
5250
|
+
const claudeRoot = join11(home, ".claude", "skills");
|
|
5251
|
+
const codexRoot = resolve8(options.codexHomeDir ?? process.env.CODEX_HOME ?? join11(home, ".codex"), "skills");
|
|
5057
5252
|
targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
|
|
5058
5253
|
for (const harness of harnesses) {
|
|
5059
5254
|
const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
|
|
5060
|
-
planSkillTree(skillRoot, harness === "claude" ? home :
|
|
5255
|
+
planSkillTree(skillRoot, harness === "claude" ? home : dirname8(dirname8(codexRoot)));
|
|
5061
5256
|
rememberTarget(harness, skillRoot);
|
|
5062
5257
|
}
|
|
5063
5258
|
} else {
|
|
5064
|
-
const sharedRoot =
|
|
5259
|
+
const sharedRoot = join11(root, ".agents", "skills");
|
|
5065
5260
|
planSkillTree(sharedRoot);
|
|
5066
|
-
const claudeRoot =
|
|
5261
|
+
const claudeRoot = join11(root, ".claude", "skills");
|
|
5067
5262
|
targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
|
|
5068
5263
|
for (const harness of harnesses) rememberTarget(harness, sharedRoot);
|
|
5069
5264
|
if (harnesses.includes("claude")) {
|
|
5070
5265
|
for (const skill of skillNames(files)) {
|
|
5071
|
-
const canonical2 = readFileSync9(
|
|
5072
|
-
plan(
|
|
5266
|
+
const canonical2 = readFileSync9(join11(sourceDir, skill, "SKILL.md"), "utf8");
|
|
5267
|
+
plan(join11(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical2));
|
|
5073
5268
|
}
|
|
5074
5269
|
rememberTarget("claude", claudeRoot);
|
|
5075
5270
|
}
|
|
5076
5271
|
if (harnesses.includes("cursor")) {
|
|
5077
|
-
const cursorRule =
|
|
5272
|
+
const cursorRule = join11(root, ".cursor", "rules", "odla.mdc");
|
|
5078
5273
|
plan(cursorRule, CURSOR_RULE);
|
|
5079
5274
|
rememberTarget("cursor", cursorRule);
|
|
5080
5275
|
}
|
|
5081
5276
|
if (harnesses.includes("agents")) {
|
|
5082
|
-
const agentsFile =
|
|
5277
|
+
const agentsFile = join11(root, "AGENTS.md");
|
|
5083
5278
|
plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
5084
5279
|
rememberTarget("agents", agentsFile);
|
|
5085
5280
|
}
|
|
5086
5281
|
if (harnesses.includes("copilot")) {
|
|
5087
|
-
const copilotFile =
|
|
5282
|
+
const copilotFile = join11(root, ".github", "copilot-instructions.md");
|
|
5088
5283
|
plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
5089
5284
|
rememberTarget("copilot", copilotFile);
|
|
5090
5285
|
}
|
|
5091
5286
|
if (harnesses.includes("gemini")) {
|
|
5092
|
-
const geminiFile =
|
|
5287
|
+
const geminiFile = join11(root, "GEMINI.md");
|
|
5093
5288
|
plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
|
|
5094
5289
|
rememberTarget("gemini", geminiFile);
|
|
5095
5290
|
}
|
|
@@ -5125,7 +5320,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
5125
5320
|
}
|
|
5126
5321
|
for (const file of plans.values()) {
|
|
5127
5322
|
if (!existsSync10(file.target) || readFileSync9(file.target, "utf8") !== file.content) {
|
|
5128
|
-
|
|
5323
|
+
mkdirSync4(dirname8(file.target), { recursive: true });
|
|
5129
5324
|
writeFileSync3(file.target, file.content);
|
|
5130
5325
|
}
|
|
5131
5326
|
}
|
|
@@ -5164,10 +5359,10 @@ function normalizeHarnesses(values, global) {
|
|
|
5164
5359
|
}
|
|
5165
5360
|
return expanded;
|
|
5166
5361
|
}
|
|
5167
|
-
function managedFileContent(path,
|
|
5362
|
+
function managedFileContent(path, block2, force, boundary) {
|
|
5168
5363
|
const symlink = symlinkedComponent(boundary, path);
|
|
5169
5364
|
if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
|
|
5170
|
-
if (!existsSync10(path)) return `${
|
|
5365
|
+
if (!existsSync10(path)) return `${block2}
|
|
5171
5366
|
`;
|
|
5172
5367
|
const current = readFileSync9(path, "utf8");
|
|
5173
5368
|
const start = "<!-- odla-ai agent setup:start -->";
|
|
@@ -5179,15 +5374,15 @@ function managedFileContent(path, block, force, boundary) {
|
|
|
5179
5374
|
}
|
|
5180
5375
|
if (startAt === -1) {
|
|
5181
5376
|
const separator = current.length === 0 || current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n";
|
|
5182
|
-
return `${current}${separator}${
|
|
5377
|
+
return `${current}${separator}${block2}
|
|
5183
5378
|
`;
|
|
5184
5379
|
}
|
|
5185
5380
|
const afterEnd = endAt + end.length;
|
|
5186
5381
|
const existing = current.slice(startAt, afterEnd);
|
|
5187
|
-
if (existing !==
|
|
5382
|
+
if (existing !== block2 && !force) {
|
|
5188
5383
|
throw new Error(`odla-managed section modified locally in ${path}; re-run with --force to replace that section`);
|
|
5189
5384
|
}
|
|
5190
|
-
return `${current.slice(0, startAt)}${
|
|
5385
|
+
return `${current.slice(0, startAt)}${block2}${current.slice(afterEnd)}`;
|
|
5191
5386
|
}
|
|
5192
5387
|
function symlinkedComponent(boundary, target) {
|
|
5193
5388
|
const rel = relative2(boundary, target);
|
|
@@ -5196,7 +5391,7 @@ function symlinkedComponent(boundary, target) {
|
|
|
5196
5391
|
}
|
|
5197
5392
|
let current = boundary;
|
|
5198
5393
|
for (const part of rel.split(sep).filter(Boolean)) {
|
|
5199
|
-
current =
|
|
5394
|
+
current = join11(current, part);
|
|
5200
5395
|
try {
|
|
5201
5396
|
if (lstatSync(current).isSymbolicLink()) return current;
|
|
5202
5397
|
} catch (error) {
|
|
@@ -5213,7 +5408,7 @@ function listFiles(dir) {
|
|
|
5213
5408
|
const results = [];
|
|
5214
5409
|
const walk = (current) => {
|
|
5215
5410
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
5216
|
-
const path =
|
|
5411
|
+
const path = join11(current, entry.name);
|
|
5217
5412
|
if (entry.isDirectory()) walk(path);
|
|
5218
5413
|
else results.push(relative2(dir, path));
|
|
5219
5414
|
}
|
|
@@ -5581,7 +5776,7 @@ var HARNESS_PROTOCOL_VERSION = 1;
|
|
|
5581
5776
|
import { execFile, spawn as spawn3 } from "child_process";
|
|
5582
5777
|
import { constants } from "fs";
|
|
5583
5778
|
import { access } from "fs/promises";
|
|
5584
|
-
import { delimiter, join as
|
|
5779
|
+
import { delimiter, join as join12 } from "path";
|
|
5585
5780
|
import { getgid, getuid } from "process";
|
|
5586
5781
|
import { mkdir as mkdir2, mkdtemp, realpath, rm, writeFile as writeFile2 } from "fs/promises";
|
|
5587
5782
|
import { tmpdir } from "os";
|
|
@@ -5599,7 +5794,7 @@ function assertPinnedImage(image) {
|
|
|
5599
5794
|
async function commandAvailable(engine) {
|
|
5600
5795
|
for (const directory of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
5601
5796
|
try {
|
|
5602
|
-
await access(
|
|
5797
|
+
await access(join12(directory, engine), constants.X_OK);
|
|
5603
5798
|
return true;
|
|
5604
5799
|
} catch {
|
|
5605
5800
|
}
|
|
@@ -5678,7 +5873,7 @@ function allowedWorkspacePath(relativePath) {
|
|
|
5678
5873
|
async function gitOutput(cwd, args, maxBytes) {
|
|
5679
5874
|
const child = spawn22("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], shell: false });
|
|
5680
5875
|
const stdout = [];
|
|
5681
|
-
const
|
|
5876
|
+
const stderr2 = [];
|
|
5682
5877
|
let bytes = 0;
|
|
5683
5878
|
child.stdout.on("data", (chunk) => {
|
|
5684
5879
|
bytes += chunk.byteLength;
|
|
@@ -5686,20 +5881,20 @@ async function gitOutput(cwd, args, maxBytes) {
|
|
|
5686
5881
|
else stdout.push(chunk);
|
|
5687
5882
|
});
|
|
5688
5883
|
child.stderr.on("data", (chunk) => {
|
|
5689
|
-
if (
|
|
5884
|
+
if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
|
|
5690
5885
|
});
|
|
5691
5886
|
const code = await new Promise((accept, reject) => {
|
|
5692
5887
|
child.once("error", reject);
|
|
5693
5888
|
child.once("exit", accept);
|
|
5694
5889
|
});
|
|
5695
5890
|
if (bytes > maxBytes) throw new Error(`git output exceeds ${maxBytes} bytes`);
|
|
5696
|
-
if (code !== 0) throw new Error(`git command failed: ${Buffer.concat(
|
|
5891
|
+
if (code !== 0) throw new Error(`git command failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
|
|
5697
5892
|
return Buffer.concat(stdout);
|
|
5698
5893
|
}
|
|
5699
5894
|
async function gitBlobs(cwd, entries, maxBytes) {
|
|
5700
5895
|
const child = spawn22("git", ["cat-file", "--batch"], { cwd, stdio: ["pipe", "pipe", "pipe"], shell: false });
|
|
5701
5896
|
const stdout = [];
|
|
5702
|
-
const
|
|
5897
|
+
const stderr2 = [];
|
|
5703
5898
|
let bytes = 0;
|
|
5704
5899
|
child.stdout.on("data", (chunk) => {
|
|
5705
5900
|
bytes += chunk.byteLength;
|
|
@@ -5707,7 +5902,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
|
|
|
5707
5902
|
else stdout.push(chunk);
|
|
5708
5903
|
});
|
|
5709
5904
|
child.stderr.on("data", (chunk) => {
|
|
5710
|
-
if (
|
|
5905
|
+
if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
|
|
5711
5906
|
});
|
|
5712
5907
|
child.stdin.end(`${entries.map((entry) => entry.hash).join("\n")}
|
|
5713
5908
|
`);
|
|
@@ -5716,7 +5911,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
|
|
|
5716
5911
|
child.once("exit", accept);
|
|
5717
5912
|
});
|
|
5718
5913
|
if (bytes > maxBytes + entries.length * 100) throw new Error(`Git tree exceeds ${maxBytes} bytes`);
|
|
5719
|
-
if (code !== 0) throw new Error(`git object read failed: ${Buffer.concat(
|
|
5914
|
+
if (code !== 0) throw new Error(`git object read failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
|
|
5720
5915
|
const output = Buffer.concat(stdout);
|
|
5721
5916
|
const blobs = [];
|
|
5722
5917
|
let offset = 0;
|
|
@@ -5813,7 +6008,7 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
|
5813
6008
|
shell: false
|
|
5814
6009
|
});
|
|
5815
6010
|
const stdout = [];
|
|
5816
|
-
const
|
|
6011
|
+
const stderr2 = [];
|
|
5817
6012
|
let outputBytes = 0;
|
|
5818
6013
|
child.stdout.on("data", (chunk) => {
|
|
5819
6014
|
outputBytes += chunk.byteLength;
|
|
@@ -5821,14 +6016,14 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
|
|
|
5821
6016
|
else stdout.push(chunk);
|
|
5822
6017
|
});
|
|
5823
6018
|
child.stderr.on("data", (chunk) => {
|
|
5824
|
-
if (
|
|
6019
|
+
if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
|
|
5825
6020
|
});
|
|
5826
6021
|
const code = await new Promise((accept, reject) => {
|
|
5827
6022
|
child.once("error", reject);
|
|
5828
6023
|
child.once("exit", accept);
|
|
5829
6024
|
});
|
|
5830
6025
|
if (outputBytes > 8 * 1024 * 1024) throw new Error("git file inventory exceeds 8 MiB");
|
|
5831
|
-
if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(
|
|
6026
|
+
if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
|
|
5832
6027
|
const paths = Buffer.concat(stdout).toString("utf8").split("\0").filter(Boolean).sort();
|
|
5833
6028
|
if (paths.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
|
|
5834
6029
|
const root = resolve22(sourceDir);
|
|
@@ -5873,7 +6068,7 @@ async function captureGitDiff(root, maxBytes) {
|
|
|
5873
6068
|
"workspace"
|
|
5874
6069
|
], { cwd: root, stdio: ["ignore", "pipe", "pipe"], shell: false });
|
|
5875
6070
|
const stdout = [];
|
|
5876
|
-
const
|
|
6071
|
+
const stderr2 = [];
|
|
5877
6072
|
let bytes = 0;
|
|
5878
6073
|
child.stdout.on("data", (chunk) => {
|
|
5879
6074
|
bytes += chunk.byteLength;
|
|
@@ -5881,7 +6076,7 @@ async function captureGitDiff(root, maxBytes) {
|
|
|
5881
6076
|
else stdout.push(chunk);
|
|
5882
6077
|
});
|
|
5883
6078
|
child.stderr.on("data", (chunk) => {
|
|
5884
|
-
if (
|
|
6079
|
+
if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
|
|
5885
6080
|
});
|
|
5886
6081
|
const code = await new Promise((accept, reject) => {
|
|
5887
6082
|
child.once("error", reject);
|
|
@@ -5889,7 +6084,7 @@ async function captureGitDiff(root, maxBytes) {
|
|
|
5889
6084
|
});
|
|
5890
6085
|
if (bytes > maxBytes) throw new Error(`patch exceeds ${maxBytes} bytes`);
|
|
5891
6086
|
if (code !== 0 && code !== 1) {
|
|
5892
|
-
throw new Error(`git diff failed: ${Buffer.concat(
|
|
6087
|
+
throw new Error(`git diff failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
|
|
5893
6088
|
}
|
|
5894
6089
|
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");
|
|
5895
6090
|
}
|
|
@@ -6297,10 +6492,10 @@ import { randomUUID } from "crypto";
|
|
|
6297
6492
|
import { createHash as createHash22, randomUUID as randomUUID2 } from "crypto";
|
|
6298
6493
|
import { createReadStream } from "fs";
|
|
6299
6494
|
import { lstat as lstat22 } from "fs/promises";
|
|
6300
|
-
import { join as
|
|
6495
|
+
import { join as join14 } from "path";
|
|
6301
6496
|
import { mkdir as mkdir3, mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
6302
6497
|
import { tmpdir as tmpdir3 } from "os";
|
|
6303
|
-
import { dirname as
|
|
6498
|
+
import { dirname as dirname10, join as join23, resolve as resolve32, sep as sep23 } from "path";
|
|
6304
6499
|
import {
|
|
6305
6500
|
keepRecentExchanges,
|
|
6306
6501
|
runAgent
|
|
@@ -6696,11 +6891,11 @@ function rollup(graph, kind, options = {}) {
|
|
|
6696
6891
|
}
|
|
6697
6892
|
|
|
6698
6893
|
// ../graph/dist/code/index.js
|
|
6699
|
-
function
|
|
6894
|
+
function dirname9(path) {
|
|
6700
6895
|
const at = path.lastIndexOf("/");
|
|
6701
6896
|
return at <= 0 ? "." : path.slice(0, at);
|
|
6702
6897
|
}
|
|
6703
|
-
function
|
|
6898
|
+
function join13(base, specifier) {
|
|
6704
6899
|
const parts = [];
|
|
6705
6900
|
const segments = `${base === "." ? "" : `${base}/`}${specifier}`.split("/");
|
|
6706
6901
|
for (const segment of segments) {
|
|
@@ -6724,7 +6919,7 @@ var BARE_IMPORT = /^\s*import\s*["']([^"']+)["']/gm;
|
|
|
6724
6919
|
var isSourcePath = (path) => SOURCE.test(path);
|
|
6725
6920
|
function resolveImport(fromPath, specifier, known) {
|
|
6726
6921
|
if (!specifier.startsWith(".")) return null;
|
|
6727
|
-
const base =
|
|
6922
|
+
const base = join13(dirname9(fromPath), specifier);
|
|
6728
6923
|
const candidates = [
|
|
6729
6924
|
base,
|
|
6730
6925
|
base.replace(/\.js$/, ".ts"),
|
|
@@ -7255,13 +7450,13 @@ function gitApply(cwd, patch2, check) {
|
|
|
7255
7450
|
stdio: ["pipe", "ignore", "pipe"],
|
|
7256
7451
|
env: { PATH: process.env.PATH ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null" }
|
|
7257
7452
|
});
|
|
7258
|
-
let
|
|
7453
|
+
let stderr2 = "";
|
|
7259
7454
|
child.stderr.setEncoding("utf8");
|
|
7260
7455
|
child.stderr.on("data", (text3) => {
|
|
7261
|
-
if (
|
|
7456
|
+
if (stderr2.length < 4e3) stderr2 += text3.slice(0, 4e3);
|
|
7262
7457
|
});
|
|
7263
7458
|
child.once("error", reject);
|
|
7264
|
-
child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2,
|
|
7459
|
+
child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2, stderr2.trim().slice(0, 500)))));
|
|
7265
7460
|
child.stdin.end(patch2);
|
|
7266
7461
|
});
|
|
7267
7462
|
}
|
|
@@ -7374,7 +7569,7 @@ function execute(engine, args, name, recipe2, signal) {
|
|
|
7374
7569
|
const started = Date.now();
|
|
7375
7570
|
const child = spawn23(engine, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
7376
7571
|
const stdout = [];
|
|
7377
|
-
const
|
|
7572
|
+
const stderr2 = [];
|
|
7378
7573
|
let bytes = 0;
|
|
7379
7574
|
let outputLimitExceeded = false;
|
|
7380
7575
|
let timedOut = false;
|
|
@@ -7395,7 +7590,7 @@ function execute(engine, args, name, recipe2, signal) {
|
|
|
7395
7590
|
else target.push(chunk);
|
|
7396
7591
|
};
|
|
7397
7592
|
child.stdout.on("data", collect(stdout));
|
|
7398
|
-
child.stderr.on("data", collect(
|
|
7593
|
+
child.stderr.on("data", collect(stderr2));
|
|
7399
7594
|
const abort = () => stop("abort");
|
|
7400
7595
|
signal?.addEventListener("abort", abort, { once: true });
|
|
7401
7596
|
if (signal?.aborted) abort();
|
|
@@ -7411,7 +7606,7 @@ function execute(engine, args, name, recipe2, signal) {
|
|
|
7411
7606
|
accept({
|
|
7412
7607
|
exitCode: code ?? 1,
|
|
7413
7608
|
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
7414
|
-
stderr: Buffer.concat(
|
|
7609
|
+
stderr: Buffer.concat(stderr2).toString("utf8"),
|
|
7415
7610
|
durationMs: Date.now() - started,
|
|
7416
7611
|
outputLimitExceeded,
|
|
7417
7612
|
timedOut
|
|
@@ -7527,7 +7722,7 @@ async function inspectArtifacts(workspaceDir, recipe2) {
|
|
|
7527
7722
|
const receipts = [];
|
|
7528
7723
|
for (const artifact of recipe2.expectedArtifacts ?? []) {
|
|
7529
7724
|
try {
|
|
7530
|
-
const path =
|
|
7725
|
+
const path = join14(workspaceDir, artifact.path);
|
|
7531
7726
|
const info = await lstat22(path);
|
|
7532
7727
|
if (!info.isFile() || info.isSymbolicLink()) {
|
|
7533
7728
|
receipts.push({ artifactId: artifact.id, status: "invalid", bytes: null, digest: null });
|
|
@@ -7563,11 +7758,11 @@ function checkedResult(result, maximumOutputBytes) {
|
|
|
7563
7758
|
}
|
|
7564
7759
|
function boundedLogs(result, maximum) {
|
|
7565
7760
|
const stdout = Buffer.from(result.stdout);
|
|
7566
|
-
const
|
|
7761
|
+
const stderr2 = Buffer.from(result.stderr);
|
|
7567
7762
|
const first = stdout.subarray(0, maximum);
|
|
7568
7763
|
return {
|
|
7569
7764
|
stdout: first.toString("utf8"),
|
|
7570
|
-
stderr:
|
|
7765
|
+
stderr: stderr2.subarray(0, Math.max(0, maximum - first.byteLength)).toString("utf8")
|
|
7571
7766
|
};
|
|
7572
7767
|
}
|
|
7573
7768
|
function digestPolicy(policy) {
|
|
@@ -7813,7 +8008,7 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = tmpdir3()) {
|
|
|
7813
8008
|
if (bytes > 16 * 1024 * 1024) throw new TypeError("Code source exceeds its byte bound");
|
|
7814
8009
|
const target = resolve32(sourceDir, file.path);
|
|
7815
8010
|
if (!target.startsWith(`${resolve32(sourceDir)}${sep23}`)) throw new TypeError("Code source path escapes its root");
|
|
7816
|
-
await mkdir3(
|
|
8011
|
+
await mkdir3(dirname10(target), { recursive: true });
|
|
7817
8012
|
await writeFile3(target, file.content, { flag: "wx", mode: 420 });
|
|
7818
8013
|
}
|
|
7819
8014
|
for (const reference of snapshot.references ?? []) {
|
|
@@ -7828,7 +8023,7 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = tmpdir3()) {
|
|
|
7828
8023
|
if (bytes > 80 * 1024 * 1024) throw new TypeError("Code source set exceeds its byte bound");
|
|
7829
8024
|
const target = resolve32(sourceDir, path);
|
|
7830
8025
|
if (!target.startsWith(`${resolve32(sourceDir)}${sep23}`)) throw new TypeError("Code reference path escapes its root");
|
|
7831
|
-
await mkdir3(
|
|
8026
|
+
await mkdir3(dirname10(target), { recursive: true });
|
|
7832
8027
|
await writeFile3(target, file.content, { flag: "wx", mode: 292 });
|
|
7833
8028
|
}
|
|
7834
8029
|
}
|
|
@@ -7855,7 +8050,7 @@ async function attachCodeRuntimeReferences(workspace, references) {
|
|
|
7855
8050
|
for (const root of [workspace.baselineDir, workspace.workspaceDir]) {
|
|
7856
8051
|
const target = resolve32(root, path);
|
|
7857
8052
|
if (!target.startsWith(`${resolve32(root)}${sep23}`)) throw new TypeError("Code reference path escapes its root");
|
|
7858
|
-
await mkdir3(
|
|
8053
|
+
await mkdir3(dirname10(target), { recursive: true });
|
|
7859
8054
|
await writeFile3(target, file.content, { flag: "wx", mode: 292 });
|
|
7860
8055
|
}
|
|
7861
8056
|
}
|
|
@@ -10073,13 +10268,13 @@ async function codeCommand(parsed, dependencies) {
|
|
|
10073
10268
|
}
|
|
10074
10269
|
|
|
10075
10270
|
// src/operator-credentials.ts
|
|
10076
|
-
import
|
|
10271
|
+
import process17 from "process";
|
|
10077
10272
|
function developerTokenStatus(context, parsed, now = Date.now()) {
|
|
10078
10273
|
const cached = readJsonFile(context.cfg.local.tokenFile);
|
|
10079
10274
|
const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
|
|
10080
10275
|
const source = clean3(
|
|
10081
10276
|
stringOpt(parsed.options.token)
|
|
10082
|
-
) ? "flag" : clean3(
|
|
10277
|
+
) ? "flag" : clean3(process17.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
|
|
10083
10278
|
return {
|
|
10084
10279
|
source,
|
|
10085
10280
|
cacheFile: context.cfg.local.tokenFile,
|
|
@@ -10273,6 +10468,30 @@ async function credentialCommand(parsed, deps = {}) {
|
|
|
10273
10468
|
}
|
|
10274
10469
|
|
|
10275
10470
|
// src/help-usage.ts
|
|
10471
|
+
var AUTH_SECTION = `
|
|
10472
|
+
Enrol this machine once, then stop asking:
|
|
10473
|
+
npx odla-ai device enroll --no-open --wait 600
|
|
10474
|
+
One browser approval, and no flags to remember: this covers every app you
|
|
10475
|
+
own \u2014 including apps you create later \u2014 with every capability that
|
|
10476
|
+
approval can carry. Afterwards every worktree on this machine mints its
|
|
10477
|
+
own short-lived credentials with nobody's attention. The window rolls
|
|
10478
|
+
forward each time you use it, so continuous work never interrupts anyone;
|
|
10479
|
+
only a real gap does. Give the human the printed /studio?code= URL, keep
|
|
10480
|
+
the process alive, and wait on it.
|
|
10481
|
+
Narrow it deliberately with --app <id> or --capability <c>; the CLI then
|
|
10482
|
+
says what that gave up.
|
|
10483
|
+
|
|
10484
|
+
npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600
|
|
10485
|
+
The same thing across all of odla, for weeks. Needs a platform
|
|
10486
|
+
administrator's approval \u2014 an app owner's cannot carry platform scopes.
|
|
10487
|
+
|
|
10488
|
+
npx odla-ai whoami what this machine holds and when it lapses
|
|
10489
|
+
npx odla-ai device list every machine you have enrolled
|
|
10490
|
+
|
|
10491
|
+
Enrollment is the only human decision here. Revoking a machine, purging an app,
|
|
10492
|
+
transferring ownership, and rotating credentials still need a signed-in human in
|
|
10493
|
+
Studio, and no machine credential can do them however wide its approval was.
|
|
10494
|
+
`;
|
|
10276
10495
|
var USAGE_SECTION = `
|
|
10277
10496
|
Start here:
|
|
10278
10497
|
odla-ai runbook ask "<question>" The current procedure, from odla's own
|
|
@@ -10311,7 +10530,7 @@ Usage:
|
|
|
10311
10530
|
odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
|
|
10312
10531
|
odla-ai pm project list [--app <product-id>] [--status <s>] [--json]
|
|
10313
10532
|
odla-ai pm project add --app <product-id> --name <name> [--description <text>] [--json]
|
|
10314
|
-
odla-ai pm project use <project-id> [--json] [saved
|
|
10533
|
+
odla-ai pm project use <project-id> [--json] [saved for this app, on this machine]
|
|
10315
10534
|
odla-ai pm goal list [--app <id>] [--project <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
10316
10535
|
odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
10317
10536
|
odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
|
|
@@ -10403,7 +10622,8 @@ Usage:
|
|
|
10403
10622
|
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
10404
10623
|
odla-ai security run [target] --self --ack-redacted-source
|
|
10405
10624
|
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]
|
|
10406
|
-
odla-ai device enroll [--app <id>[,<id>...]] [--
|
|
10625
|
+
odla-ai device enroll [--app <id>[,<id>...]|--all-apps] [--capability all|<c>[,<c>...]] [--platform-wide]
|
|
10626
|
+
[--name <label>] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--wait <seconds>] [--json]
|
|
10407
10627
|
odla-ai device list [--email <odla-account>] [--json]
|
|
10408
10628
|
odla-ai device revoke <device-id> [--email <odla-account>] [--json]
|
|
10409
10629
|
odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
|
|
@@ -10417,8 +10637,11 @@ Usage:
|
|
|
10417
10637
|
|
|
10418
10638
|
// src/help.ts
|
|
10419
10639
|
function printHelp(output = console) {
|
|
10420
|
-
output.log(
|
|
10421
|
-
|
|
10640
|
+
output.log(helpText());
|
|
10641
|
+
}
|
|
10642
|
+
function helpText() {
|
|
10643
|
+
return `odla-ai
|
|
10644
|
+
${AUTH_SECTION}${USAGE_SECTION}
|
|
10422
10645
|
Commands:
|
|
10423
10646
|
auth Start a fresh, exact-project agent authorization for human review.
|
|
10424
10647
|
The email is the signed-in odla account, never git or GitHub
|
|
@@ -10491,8 +10714,9 @@ Commands:
|
|
|
10491
10714
|
security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
|
|
10492
10715
|
pm Project management (via @odla-ai/pm): Products contain Projects;
|
|
10493
10716
|
projects contain goals, kanban tasks, decisions, and bugs. Use
|
|
10494
|
-
"pm project list|add|use"
|
|
10495
|
-
|
|
10717
|
+
"pm project list|add|use" selects a project for this app on this
|
|
10718
|
+
machine \u2014 every worktree shares it \u2014 or pass --app/--project
|
|
10719
|
+
explicitly. Same device-grant auth as "app".
|
|
10496
10720
|
Status changes and comments post to each item's @odla-ai/chat
|
|
10497
10721
|
discussion thread.
|
|
10498
10722
|
NOTE: "--column ready" is OWNER-ONLY. Creating a task in Ready
|
|
@@ -10521,9 +10745,22 @@ Commands:
|
|
|
10521
10745
|
platform Read canonical fleet health, releases, provider load/freshness,
|
|
10522
10746
|
explicit unknowns, and next actions through a read-only grant.
|
|
10523
10747
|
device Enrol THIS machine once, then stop asking. A human approves the
|
|
10524
|
-
enrollment in the browser; from then on
|
|
10525
|
-
own short-lived credentials
|
|
10526
|
-
attention, until the device
|
|
10748
|
+
enrollment in the browser; from then on EVERY worktree on this
|
|
10749
|
+
machine mints its own short-lived credentials with nobody's
|
|
10750
|
+
attention, until the device is revoked or goes unused.
|
|
10751
|
+
With no flags it covers every app you own, now and later, with
|
|
10752
|
+
every capability that approval can carry \u2014 so creating an app
|
|
10753
|
+
costs no new approval, and a capability nobody thought to name is
|
|
10754
|
+
not a 403 next week. "--app" or "--capability" narrow it
|
|
10755
|
+
deliberately, and the CLI says what that gave up.
|
|
10756
|
+
"--platform-wide" is the administrator's version, across all of
|
|
10757
|
+
odla.
|
|
10758
|
+
The expiry is a GAP, not a clock: each use rolls it forward, so
|
|
10759
|
+
only going quiet brings a human back into the loop \u2014 which is
|
|
10760
|
+
where anything that changed can be explained.
|
|
10761
|
+
"device list" shows what each machine holds and when it lapses;
|
|
10762
|
+
revoking one takes down every credential it ever minted, and is
|
|
10763
|
+
deliberately a signed-in human's decision in Studio.
|
|
10527
10764
|
provision Register services, compose integrations, persist credentials, optionally push secrets.
|
|
10528
10765
|
"provision --live --yes" initializes only the live instance of
|
|
10529
10766
|
an existing sandbox app and enables every configured service;
|
|
@@ -10619,7 +10856,46 @@ Safety:
|
|
|
10619
10856
|
Run security plan first to inspect the admin-selected providers, models,
|
|
10620
10857
|
per-route bounds, credential readiness, retention, no-execution boundary,
|
|
10621
10858
|
and digest that binds consent to that exact plan.
|
|
10622
|
-
|
|
10859
|
+
`;
|
|
10860
|
+
}
|
|
10861
|
+
|
|
10862
|
+
// src/help-command.ts
|
|
10863
|
+
function printCommandHelp(command, output = console) {
|
|
10864
|
+
const lines = helpText().split("\n");
|
|
10865
|
+
const usage = allBlocks(lines, new RegExp(`^ odla-ai ${escapeRe(command)}(\\s|$)`));
|
|
10866
|
+
const prose = block(lines, (line2) => new RegExp(`^ ${escapeRe(command)}\\s\\s+\\S`).test(line2));
|
|
10867
|
+
if (usage.length === 0 && prose.length === 0) {
|
|
10868
|
+
output.log(`odla-ai: no command "${command}". Run "odla-ai help" for all of them.`);
|
|
10869
|
+
return;
|
|
10870
|
+
}
|
|
10871
|
+
output.log([
|
|
10872
|
+
...prose.length ? [prose.join("\n"), ""] : [],
|
|
10873
|
+
...usage.length ? ["Usage:", ...usage, ""] : [],
|
|
10874
|
+
AUTH_SECTION.trimEnd()
|
|
10875
|
+
].join("\n"));
|
|
10876
|
+
}
|
|
10877
|
+
function allBlocks(lines, pattern) {
|
|
10878
|
+
const out = [];
|
|
10879
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10880
|
+
if (!pattern.test(lines[i])) continue;
|
|
10881
|
+
out.push(...block(lines.slice(i), (line2) => line2 === lines[i]));
|
|
10882
|
+
}
|
|
10883
|
+
return out;
|
|
10884
|
+
}
|
|
10885
|
+
function block(lines, starts) {
|
|
10886
|
+
const first = lines.findIndex(starts);
|
|
10887
|
+
if (first === -1) return [];
|
|
10888
|
+
const indent = lines[first].length - lines[first].trimStart().length;
|
|
10889
|
+
const out = [lines[first]];
|
|
10890
|
+
for (const line2 of lines.slice(first + 1)) {
|
|
10891
|
+
if (!line2.trim()) break;
|
|
10892
|
+
if (line2.length - line2.trimStart().length <= indent) break;
|
|
10893
|
+
out.push(line2);
|
|
10894
|
+
}
|
|
10895
|
+
return out;
|
|
10896
|
+
}
|
|
10897
|
+
function escapeRe(value2) {
|
|
10898
|
+
return value2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10623
10899
|
}
|
|
10624
10900
|
|
|
10625
10901
|
// src/discuss-principals.ts
|
|
@@ -11776,14 +12052,36 @@ async function pmWatch(ctx, parsed) {
|
|
|
11776
12052
|
}
|
|
11777
12053
|
|
|
11778
12054
|
// src/pm-project-context.ts
|
|
12055
|
+
import { existsSync as existsSync12, rmSync as rmSync5 } from "fs";
|
|
11779
12056
|
import { resolve as resolve12 } from "path";
|
|
11780
|
-
var pmProjectContextFile = (
|
|
12057
|
+
var pmProjectContextFile = () => pmContextFile();
|
|
11781
12058
|
function readPmProjectContext(rootDir) {
|
|
11782
|
-
|
|
11783
|
-
|
|
12059
|
+
adoptLegacySelection(rootDir);
|
|
12060
|
+
const entries = Object.values(readSelections()).filter(isSelection);
|
|
12061
|
+
return entries.sort((a, b) => b.selectedAt.localeCompare(a.selectedAt))[0] ?? null;
|
|
11784
12062
|
}
|
|
11785
12063
|
function writePmProjectContext(rootDir, value2) {
|
|
11786
|
-
|
|
12064
|
+
adoptLegacySelection(rootDir);
|
|
12065
|
+
writePrivateJson(pmProjectContextFile(), {
|
|
12066
|
+
...readSelections(),
|
|
12067
|
+
[value2.appId]: { ...value2, selectedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
12068
|
+
});
|
|
12069
|
+
}
|
|
12070
|
+
function adoptLegacySelection(rootDir) {
|
|
12071
|
+
const legacy = resolve12(rootDir, ".odla", "pm-project.local.json");
|
|
12072
|
+
if (!existsSync12(legacy)) return;
|
|
12073
|
+
const previous = readJsonFile(legacy);
|
|
12074
|
+
rmSync5(legacy, { force: true });
|
|
12075
|
+
if (!isSelection(previous)) return;
|
|
12076
|
+
const selections = readSelections();
|
|
12077
|
+
if (selections[previous.appId]) return;
|
|
12078
|
+
writePrivateJson(pmProjectContextFile(), { ...selections, [previous.appId]: previous });
|
|
12079
|
+
}
|
|
12080
|
+
function readSelections() {
|
|
12081
|
+
return readJsonFile(pmProjectContextFile()) ?? {};
|
|
12082
|
+
}
|
|
12083
|
+
function isSelection(value2) {
|
|
12084
|
+
return !!value2 && typeof value2.appId === "string" && typeof value2.projectId === "string" && typeof value2.selectedAt === "string";
|
|
11787
12085
|
}
|
|
11788
12086
|
|
|
11789
12087
|
// src/pm-project-actions.ts
|
|
@@ -12808,7 +13106,7 @@ function percent(value2) {
|
|
|
12808
13106
|
// src/provision.ts
|
|
12809
13107
|
import { AppsError as AppsError2, createAppsClient as createAppsClient3, orderAppServices as orderAppServices3, tenantIdFor as tenantIdFor6 } from "@odla-ai/apps";
|
|
12810
13108
|
import { putSecret as putSecret2 } from "@odla-ai/ai";
|
|
12811
|
-
import
|
|
13109
|
+
import process18 from "process";
|
|
12812
13110
|
|
|
12813
13111
|
// src/integration-provision.ts
|
|
12814
13112
|
import { uuidv7 } from "@odla-ai/db";
|
|
@@ -13246,7 +13544,7 @@ async function provision(options) {
|
|
|
13246
13544
|
await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
|
|
13247
13545
|
}
|
|
13248
13546
|
if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
13249
|
-
const key =
|
|
13547
|
+
const key = process18.env[cfg.ai.keyEnv];
|
|
13250
13548
|
if (key) {
|
|
13251
13549
|
const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
|
|
13252
13550
|
await putSecret2({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
|
|
@@ -13288,7 +13586,7 @@ async function provision(options) {
|
|
|
13288
13586
|
|
|
13289
13587
|
// src/record.ts
|
|
13290
13588
|
import { appendFileSync } from "fs";
|
|
13291
|
-
import
|
|
13589
|
+
import process19 from "process";
|
|
13292
13590
|
|
|
13293
13591
|
// src/surface.ts
|
|
13294
13592
|
var PM_ACTIONS = {
|
|
@@ -13470,7 +13768,7 @@ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
|
|
|
13470
13768
|
|
|
13471
13769
|
// src/record.ts
|
|
13472
13770
|
function recordInvocation(parsed) {
|
|
13473
|
-
const file =
|
|
13771
|
+
const file = process19.env.ODLA_CLI_RECORD;
|
|
13474
13772
|
if (!file) return;
|
|
13475
13773
|
try {
|
|
13476
13774
|
const entry = {
|
|
@@ -13496,10 +13794,17 @@ function advisoryCollectingFetch(inner, sink) {
|
|
|
13496
13794
|
return response2;
|
|
13497
13795
|
});
|
|
13498
13796
|
}
|
|
13797
|
+
var superseded = /* @__PURE__ */ new Set();
|
|
13798
|
+
function supersedeAdvisory(code) {
|
|
13799
|
+
superseded.add(code);
|
|
13800
|
+
}
|
|
13499
13801
|
function renderAdvisories(out, advisories, env = process.env) {
|
|
13802
|
+
const retracted = new Set(superseded);
|
|
13803
|
+
superseded.clear();
|
|
13500
13804
|
if (env.ODLA_NO_ADVISORIES) return;
|
|
13501
13805
|
const seen = /* @__PURE__ */ new Set();
|
|
13502
13806
|
for (const advisory of advisories) {
|
|
13807
|
+
if (retracted.has(advisory.code)) continue;
|
|
13503
13808
|
const key = `${advisory.code}:${advisory.message}`;
|
|
13504
13809
|
if (seen.has(key)) continue;
|
|
13505
13810
|
seen.add(key);
|
|
@@ -13507,6 +13812,14 @@ function renderAdvisories(out, advisories, env = process.env) {
|
|
|
13507
13812
|
}
|
|
13508
13813
|
}
|
|
13509
13814
|
|
|
13815
|
+
// src/device-command.ts
|
|
13816
|
+
import {
|
|
13817
|
+
ADMIN_DEVICE_SCOPES,
|
|
13818
|
+
ALL_OWNED_APPS,
|
|
13819
|
+
OPTIONAL_AGENT_PROJECT_CAPABILITIES,
|
|
13820
|
+
OWNER_DEVICE_SCOPES
|
|
13821
|
+
} from "@odla-ai/db";
|
|
13822
|
+
|
|
13510
13823
|
// src/device-ttl.ts
|
|
13511
13824
|
var OWNER_DEVICE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
13512
13825
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -13526,10 +13839,26 @@ function parseDeviceTtl(raw) {
|
|
|
13526
13839
|
}
|
|
13527
13840
|
|
|
13528
13841
|
// src/device-command.ts
|
|
13529
|
-
import { chmodSync as
|
|
13530
|
-
import { dirname as
|
|
13531
|
-
import
|
|
13842
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
13843
|
+
import { dirname as dirname11 } from "path";
|
|
13844
|
+
import process20 from "process";
|
|
13532
13845
|
async function deviceCommand(parsed, deps) {
|
|
13846
|
+
assertArgs(parsed, [
|
|
13847
|
+
"app",
|
|
13848
|
+
"all-apps",
|
|
13849
|
+
"platform-wide",
|
|
13850
|
+
"name",
|
|
13851
|
+
"capability",
|
|
13852
|
+
"device-ttl",
|
|
13853
|
+
"email",
|
|
13854
|
+
"open",
|
|
13855
|
+
"json",
|
|
13856
|
+
"config",
|
|
13857
|
+
"token",
|
|
13858
|
+
"context",
|
|
13859
|
+
"platform",
|
|
13860
|
+
"wait"
|
|
13861
|
+
], 3);
|
|
13533
13862
|
const action2 = parsed.positionals[1] ?? "";
|
|
13534
13863
|
const out = deps.stdout ?? console;
|
|
13535
13864
|
const doFetch = deps.fetch ?? fetch;
|
|
@@ -13542,10 +13871,19 @@ async function deviceCommand(parsed, deps) {
|
|
|
13542
13871
|
}
|
|
13543
13872
|
async function enroll(parsed, deps, cfg, doFetch, out, json) {
|
|
13544
13873
|
const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
|
|
13545
|
-
const
|
|
13546
|
-
|
|
13874
|
+
const platformWide = parsed.options["platform-wide"] === true;
|
|
13875
|
+
const narrowed = stringOpt(parsed.options.app) !== void 0 || stringOpt(parsed.options.capability) !== void 0;
|
|
13876
|
+
const apps = platformWide || parsed.options["all-apps"] === true || !narrowed ? [ALL_OWNED_APPS] : (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
|
|
13877
|
+
if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026], or --all-apps");
|
|
13547
13878
|
const deviceTtlMs = parseDeviceTtl(parsed.options["device-ttl"]);
|
|
13548
|
-
const extended = deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
|
|
13879
|
+
const extended = platformWide || deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
|
|
13880
|
+
const { capabilities, scopes } = requestedEnvelope(parsed, platformWide, narrowed);
|
|
13881
|
+
const narrowNotice = narrowEnrollmentNotice({
|
|
13882
|
+
appIds: apps,
|
|
13883
|
+
capabilities: capabilities ?? [],
|
|
13884
|
+
scopes: scopes ?? []
|
|
13885
|
+
});
|
|
13886
|
+
if (narrowNotice) out.error(narrowNotice);
|
|
13549
13887
|
const token = await scopedToken2(
|
|
13550
13888
|
parsed,
|
|
13551
13889
|
deps,
|
|
@@ -13560,9 +13898,10 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
|
|
|
13560
13898
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
13561
13899
|
body: JSON.stringify({
|
|
13562
13900
|
name,
|
|
13563
|
-
platform:
|
|
13901
|
+
platform: process20.platform,
|
|
13564
13902
|
appIds: apps,
|
|
13565
|
-
...
|
|
13903
|
+
...capabilities ? { capabilities } : {},
|
|
13904
|
+
...scopes ? { scopes } : {},
|
|
13566
13905
|
...deviceTtlMs === void 0 ? {} : { deviceTtlMs }
|
|
13567
13906
|
})
|
|
13568
13907
|
});
|
|
@@ -13571,19 +13910,54 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
|
|
|
13571
13910
|
throw new Error(`device enroll failed: ${body.error?.message ?? `registry returned ${response2.status}`} (${response2.status})`);
|
|
13572
13911
|
}
|
|
13573
13912
|
const path = deviceCredentialPath();
|
|
13574
|
-
|
|
13913
|
+
mkdirSync5(dirname11(path), { recursive: true });
|
|
13575
13914
|
writeFileSync4(path, JSON.stringify({
|
|
13576
13915
|
token: body.token,
|
|
13577
13916
|
platform: cfg.platformUrl.replace(/\/$/, ""),
|
|
13578
13917
|
deviceId: body.device.deviceId,
|
|
13579
13918
|
name
|
|
13580
13919
|
}, null, 2));
|
|
13581
|
-
|
|
13582
|
-
|
|
13583
|
-
|
|
13920
|
+
chmodSync3(path, 384);
|
|
13921
|
+
rememberMachineIdentity(cfg.platformUrl.replace(/\/$/, ""), stringOpt(parsed.options.email));
|
|
13922
|
+
supersedeAdvisory("credential.expiring");
|
|
13923
|
+
const reach = body.device.appIds.includes(ALL_OWNED_APPS) ? "every app you own, now and later" : body.device.appIds.join(", ");
|
|
13924
|
+
out.error(`device: enrolled "${name}" for ${reach}; credential written to ${path}`);
|
|
13925
|
+
if (narrowNotice) out.error(narrowNotice);
|
|
13926
|
+
out.error(
|
|
13927
|
+
"device: every worktree on this machine mints its own credentials from now on \u2014 no further approvals,"
|
|
13928
|
+
);
|
|
13929
|
+
out.error(
|
|
13930
|
+
`device: and the clock resets each time you use it. Going quiet for ${describeWindow(body.device.expiresAt)} is what ends it.`
|
|
13931
|
+
);
|
|
13584
13932
|
if (json) {
|
|
13585
|
-
out.log(JSON.stringify({
|
|
13933
|
+
out.log(JSON.stringify({
|
|
13934
|
+
deviceId: body.device.deviceId,
|
|
13935
|
+
name,
|
|
13936
|
+
appIds: body.device.appIds,
|
|
13937
|
+
capabilities: body.device.capabilities ?? [],
|
|
13938
|
+
scopes: body.device.scopes ?? [],
|
|
13939
|
+
expiresAt: body.device.expiresAt,
|
|
13940
|
+
hardExpiresAt: body.device.hardExpiresAt ?? null
|
|
13941
|
+
}, null, 2));
|
|
13942
|
+
}
|
|
13943
|
+
}
|
|
13944
|
+
function requestedEnvelope(parsed, platformWide, narrowed) {
|
|
13945
|
+
const raw = stringOpt(parsed.options.capability);
|
|
13946
|
+
const everything = platformWide || !narrowed || raw?.trim().toLowerCase() === "all";
|
|
13947
|
+
if (everything) {
|
|
13948
|
+
return {
|
|
13949
|
+
capabilities: [...OPTIONAL_AGENT_PROJECT_CAPABILITIES],
|
|
13950
|
+
scopes: platformWide ? [...ADMIN_DEVICE_SCOPES] : [...OWNER_DEVICE_SCOPES]
|
|
13951
|
+
};
|
|
13586
13952
|
}
|
|
13953
|
+
const named = raw?.split(",").map((c) => c.trim()).filter(Boolean);
|
|
13954
|
+
return named?.length ? { capabilities: named } : {};
|
|
13955
|
+
}
|
|
13956
|
+
function describeWindow(expiresAt, now = Date.now()) {
|
|
13957
|
+
const days = Math.max(1, Math.round((expiresAt - now) / (24 * 60 * 60 * 1e3)));
|
|
13958
|
+
if (days >= 365) return `${Math.round(days / 365)} year${days >= 730 ? "s" : ""}`;
|
|
13959
|
+
if (days % 7 === 0) return `${days / 7} week${days > 7 ? "s" : ""}`;
|
|
13960
|
+
return `${days} day${days === 1 ? "" : "s"}`;
|
|
13587
13961
|
}
|
|
13588
13962
|
async function list2(parsed, deps, cfg, doFetch, out, json) {
|
|
13589
13963
|
const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device list)");
|
|
@@ -13598,12 +13972,17 @@ async function list2(parsed, deps, cfg, doFetch, out, json) {
|
|
|
13598
13972
|
if (body.devices.length === 0) return out.log("no enrolled devices");
|
|
13599
13973
|
for (const device of body.devices) {
|
|
13600
13974
|
const state2 = device.revokedAt ? "revoked" : device.expiresAt <= Date.now() ? "expired" : "active";
|
|
13601
|
-
|
|
13975
|
+
const reach = device.appIds.includes("*") ? "every app you own" : device.appIds.join(", ");
|
|
13976
|
+
const gap = state2 === "active" ? ` idle ${describeWindow(device.expiresAt)} left` : "";
|
|
13977
|
+
out.log(`${device.deviceId} ${state2.padEnd(7)} ${device.name} [${reach}]${gap}`);
|
|
13602
13978
|
}
|
|
13603
13979
|
}
|
|
13604
13980
|
async function revoke(parsed, deps, cfg, doFetch, out, json) {
|
|
13605
13981
|
const deviceId = parsed.positionals[2];
|
|
13606
13982
|
if (!deviceId) throw new Error("device revoke needs the device id from `odla-ai device list`");
|
|
13983
|
+
out.error(
|
|
13984
|
+
`device: revoking is a signed-in human's decision; if this is refused, open ${cfg.platformUrl}/studio and revoke it there.`
|
|
13985
|
+
);
|
|
13607
13986
|
const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device revoke)");
|
|
13608
13987
|
const response2 = await doFetch(`${cfg.platformUrl}/registry/devices/${encodeURIComponent(deviceId)}/revoke`, {
|
|
13609
13988
|
method: "POST",
|
|
@@ -13622,7 +14001,7 @@ async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app
|
|
|
13622
14001
|
// A device is granted the apps named in ONE approval, so --app is a list here.
|
|
13623
14002
|
allowAppList: true
|
|
13624
14003
|
});
|
|
13625
|
-
const
|
|
14004
|
+
const scopedTokenFile2 = credentials.scopedTokenFile;
|
|
13626
14005
|
return getScopedPlatformToken({
|
|
13627
14006
|
platform: cfg.platformUrl,
|
|
13628
14007
|
scope,
|
|
@@ -13633,12 +14012,12 @@ async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app
|
|
|
13633
14012
|
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
13634
14013
|
openApprovalUrl: deps.openUrl,
|
|
13635
14014
|
rootDir: cfg.rootDir,
|
|
13636
|
-
tokenFile:
|
|
14015
|
+
tokenFile: scopedTokenFile2,
|
|
13637
14016
|
...stringOpt(parsed.options.token) ? { token: stringOpt(parsed.options.token) } : {}
|
|
13638
14017
|
});
|
|
13639
14018
|
}
|
|
13640
14019
|
function defaultDeviceName() {
|
|
13641
|
-
return `${
|
|
14020
|
+
return `${process20.env.HOSTNAME ?? process20.env.HOST ?? "machine"}-${process20.platform}`;
|
|
13642
14021
|
}
|
|
13643
14022
|
|
|
13644
14023
|
// src/runbook-actions.ts
|
|
@@ -13782,7 +14161,7 @@ async function runbookRemove(ctx, slug) {
|
|
|
13782
14161
|
|
|
13783
14162
|
// src/runbook-import.ts
|
|
13784
14163
|
import { readFileSync as readFileSync11, readdirSync as readdirSync2, statSync } from "fs";
|
|
13785
|
-
import { basename as basename2, join as
|
|
14164
|
+
import { basename as basename2, join as join15 } from "path";
|
|
13786
14165
|
function parseRunbook(text3, slug) {
|
|
13787
14166
|
let rest = text3;
|
|
13788
14167
|
const meta = {};
|
|
@@ -13812,7 +14191,7 @@ function readRunbookDir(dir) {
|
|
|
13812
14191
|
if (!files.length) throw new Error(`no .md files in ${dir}`);
|
|
13813
14192
|
return files.map((file) => {
|
|
13814
14193
|
const slug = basename2(file, ".md");
|
|
13815
|
-
const parsed = parseRunbook(readFileSync11(
|
|
14194
|
+
const parsed = parseRunbook(readFileSync11(join15(dir, file), "utf8"), slug);
|
|
13816
14195
|
return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
|
|
13817
14196
|
});
|
|
13818
14197
|
}
|
|
@@ -13880,8 +14259,8 @@ async function upsert(ctx, r, visibility) {
|
|
|
13880
14259
|
|
|
13881
14260
|
// src/runbook-impact.ts
|
|
13882
14261
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
13883
|
-
import { existsSync as
|
|
13884
|
-
import { join as
|
|
14262
|
+
import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
|
|
14263
|
+
import { join as join16 } from "path";
|
|
13885
14264
|
|
|
13886
14265
|
// src/runbook-impact-scan.ts
|
|
13887
14266
|
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$]*)/;
|
|
@@ -14050,8 +14429,8 @@ ${body.split("\n").map((line2) => `+${line2}`).join("\n")}
|
|
|
14050
14429
|
}
|
|
14051
14430
|
function manifestLabeller(root) {
|
|
14052
14431
|
return (workspace) => {
|
|
14053
|
-
const manifest =
|
|
14054
|
-
if (!
|
|
14432
|
+
const manifest = join16(root, workspace, "package.json");
|
|
14433
|
+
if (!existsSync13(manifest)) return void 0;
|
|
14055
14434
|
try {
|
|
14056
14435
|
const name = JSON.parse(readFileSync12(manifest, "utf8")).name;
|
|
14057
14436
|
return typeof name === "string" ? name : void 0;
|
|
@@ -14120,7 +14499,7 @@ function report4(ctx, impacts) {
|
|
|
14120
14499
|
async function runbookImpact(ctx, options, deps = {}) {
|
|
14121
14500
|
const cwd = deps.cwd ?? process.cwd();
|
|
14122
14501
|
const runGit = deps.runGit ?? gitRunner(cwd);
|
|
14123
|
-
const read3 = deps.readRepoFile ?? ((path) => readFileSync12(
|
|
14502
|
+
const read3 = deps.readRepoFile ?? ((path) => readFileSync12(join16(cwd, path), "utf8"));
|
|
14124
14503
|
const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
|
|
14125
14504
|
if (!surfaces.length) {
|
|
14126
14505
|
return ctx.out.log(
|
|
@@ -14247,12 +14626,12 @@ async function runbookComment(ctx, slug, body) {
|
|
|
14247
14626
|
|
|
14248
14627
|
// src/runbook-editor.ts
|
|
14249
14628
|
import { spawnSync } from "child_process";
|
|
14250
|
-
import { mkdtempSync, readFileSync as readFileSync13, rmSync as
|
|
14629
|
+
import { mkdtempSync, readFileSync as readFileSync13, rmSync as rmSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
14251
14630
|
import { tmpdir as tmpdir4 } from "os";
|
|
14252
|
-
import { join as
|
|
14253
|
-
import
|
|
14631
|
+
import { join as join17 } from "path";
|
|
14632
|
+
import process21 from "process";
|
|
14254
14633
|
var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
|
|
14255
|
-
function resolveEditor(env =
|
|
14634
|
+
function resolveEditor(env = process21.env) {
|
|
14256
14635
|
for (const name of EDITOR_ENV) {
|
|
14257
14636
|
const value2 = env[name];
|
|
14258
14637
|
if (value2 && value2.trim()) return value2.trim();
|
|
@@ -14266,8 +14645,8 @@ function defaultRun(command, path) {
|
|
|
14266
14645
|
return result.status ?? 0;
|
|
14267
14646
|
}
|
|
14268
14647
|
function editText(initial, slug, deps = {}) {
|
|
14269
|
-
const env = deps.env ??
|
|
14270
|
-
const interactive = deps.interactive ?? (() => Boolean(
|
|
14648
|
+
const env = deps.env ?? process21.env;
|
|
14649
|
+
const interactive = deps.interactive ?? (() => Boolean(process21.stdin.isTTY));
|
|
14271
14650
|
const editor = resolveEditor(env);
|
|
14272
14651
|
if (!editor)
|
|
14273
14652
|
throw new Error(
|
|
@@ -14275,8 +14654,8 @@ function editText(initial, slug, deps = {}) {
|
|
|
14275
14654
|
);
|
|
14276
14655
|
if (!interactive())
|
|
14277
14656
|
throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
|
|
14278
|
-
const dir = mkdtempSync(
|
|
14279
|
-
const file =
|
|
14657
|
+
const dir = mkdtempSync(join17(tmpdir4(), "odla-runbook-"));
|
|
14658
|
+
const file = join17(dir, `${slug}.md`);
|
|
14280
14659
|
try {
|
|
14281
14660
|
writeFileSync5(file, initial, { mode: 384 });
|
|
14282
14661
|
const code = defaultRunOrInjected(deps)(editor, file);
|
|
@@ -14284,7 +14663,7 @@ function editText(initial, slug, deps = {}) {
|
|
|
14284
14663
|
const edited = readFileSync13(file, "utf8");
|
|
14285
14664
|
return edited === initial ? null : edited;
|
|
14286
14665
|
} finally {
|
|
14287
|
-
|
|
14666
|
+
rmSync6(dir, { recursive: true, force: true });
|
|
14288
14667
|
}
|
|
14289
14668
|
}
|
|
14290
14669
|
var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
|
|
@@ -15196,8 +15575,10 @@ async function dispatchCli(argv, dependencies) {
|
|
|
15196
15575
|
return;
|
|
15197
15576
|
}
|
|
15198
15577
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
15199
|
-
assertArgs(parsed, ["help"],
|
|
15200
|
-
|
|
15578
|
+
assertArgs(parsed, ["help"], 2);
|
|
15579
|
+
const topic = parsed.positionals[1];
|
|
15580
|
+
if (topic) printCommandHelp(topic, runtime.stdout);
|
|
15581
|
+
else printHelp(runtime.stdout);
|
|
15201
15582
|
return;
|
|
15202
15583
|
}
|
|
15203
15584
|
if (command === "whoami") {
|
|
@@ -15411,4 +15792,4 @@ export {
|
|
|
15411
15792
|
isTerminalHostedSecurityStatus,
|
|
15412
15793
|
runCli
|
|
15413
15794
|
};
|
|
15414
|
-
//# sourceMappingURL=chunk-
|
|
15795
|
+
//# sourceMappingURL=chunk-2IHE5U3M.js.map
|