@odla-ai/cli 0.27.1 → 0.27.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/REQUIREMENTS.md +6 -4
- package/dist/bin.cjs +132 -123
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-4JWDJN3P.js → chunk-JWEBGIBR.js} +132 -123
- package/dist/chunk-JWEBGIBR.js.map +1 -0
- package/dist/index.cjs +132 -123
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/skills/odla/SKILL.md +10 -8
- package/skills/odla-migrate/references/phase-2-db.md +3 -1
- package/skills/odla-migrate/references/troubleshooting.md +4 -3
- package/dist/chunk-4JWDJN3P.js.map +0 -1
package/README.md
CHANGED
|
@@ -426,12 +426,12 @@ shown-once credential.
|
|
|
426
426
|
`--email <account>` or `ODLA_USER_EMAIL`; the matching existing account must
|
|
427
427
|
sign in, review the exact code, and approve it. Opening the URL alone does
|
|
428
428
|
not claim the request.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
429
|
+
Every real CLI handshake prints exactly one canonical `/studio?code=…` URL
|
|
430
|
+
and attempts to open it — including from CI, SSH, display-less, scripted,
|
|
431
|
+
and agent-driven shells. Only `--no-open` suppresses the attempt. Browser
|
|
432
|
+
launch is best-effort: an agent with browser control must open that exact URL
|
|
433
|
+
immediately; otherwise it gives the URL to the human verbatim. The started
|
|
434
|
+
handshake is persisted privately under `.odla/`, so a
|
|
435
435
|
run killed before approval loses nothing: rerunning resumes the same code.
|
|
436
436
|
Outside an interactive terminal the approval wait is capped (90 seconds by
|
|
437
437
|
default; `--wait <seconds>` overrides), and a still-pending handshake exits
|
package/REQUIREMENTS.md
CHANGED
|
@@ -38,13 +38,15 @@ Agnacl, but none should mention or special-case Agnacl.
|
|
|
38
38
|
credential; agents must not request either from the human.
|
|
39
39
|
- Unknown and never-signed-in accounts must not yield a claimable request, but
|
|
40
40
|
the public response must not disclose account existence.
|
|
41
|
-
- The approval code must be printed in
|
|
42
|
-
URL.
|
|
41
|
+
- The approval code must be printed in the one canonical, copyable
|
|
42
|
+
`<platform>/studio?code=…` URL. Optional server URL hints must not create a
|
|
43
|
+
second CLI path. Opening it must not claim the request; the matching user must
|
|
43
44
|
explicitly review that exact code before approval controls appear.
|
|
44
45
|
- Only one claimed pending or approved-but-uncollected request may be active per
|
|
45
46
|
user. Unclaimed issued codes must not occupy that slot.
|
|
46
|
-
-
|
|
47
|
-
|
|
47
|
+
- Every real CLI handshake must attempt to open that canonical approval URL,
|
|
48
|
+
including in CI, SSH, display-less, and agent-driven shells. `--no-open` is
|
|
49
|
+
the only production suppression; failures must visibly repeat the manual action.
|
|
48
50
|
- Cached developer tokens must be mode `0600`, gitignored, and reused until
|
|
49
51
|
expiry.
|
|
50
52
|
- Minted developer tokens must have a metadata-only inventory (owner, label,
|
package/dist/bin.cjs
CHANGED
|
@@ -28,18 +28,22 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
|
|
|
28
28
|
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
29
29
|
|
|
30
30
|
// src/admin-ai.ts
|
|
31
|
-
var
|
|
31
|
+
var import_node_process7 = __toESM(require("process"), 1);
|
|
32
32
|
|
|
33
33
|
// src/token.ts
|
|
34
34
|
var import_db = require("@odla-ai/db");
|
|
35
|
-
var
|
|
35
|
+
var import_node_process4 = __toESM(require("process"), 1);
|
|
36
|
+
|
|
37
|
+
// src/handshake-approval.ts
|
|
38
|
+
var import_node_process2 = __toESM(require("process"), 1);
|
|
36
39
|
|
|
37
40
|
// src/approval-prompt.ts
|
|
38
41
|
function approvalLines(prompt) {
|
|
39
42
|
const lines = [""];
|
|
40
43
|
lines.push(
|
|
41
|
-
`\u23F8
|
|
44
|
+
`\u23F8 AUTHORIZATION REQUIRED${prompt.purpose ? ` to ${prompt.purpose}` : ""} \u2014 code ${prompt.userCode}, ${prompt.minutesLeft}m left.`
|
|
42
45
|
);
|
|
46
|
+
lines.push(" OPEN THIS URL IN THE APPROVER'S SIGNED-IN BROWSER NOW:");
|
|
43
47
|
lines.push("");
|
|
44
48
|
lines.push(` ${prompt.approvalUrl}`);
|
|
45
49
|
lines.push("");
|
|
@@ -53,8 +57,9 @@ function approvalLines(prompt) {
|
|
|
53
57
|
lines.push(` No browser was opened (${prompt.browserSkipped}).`);
|
|
54
58
|
}
|
|
55
59
|
lines.push("");
|
|
56
|
-
lines.push(" AGENTS:
|
|
57
|
-
lines.push("
|
|
60
|
+
lines.push(" AGENTS: use browser control to open the URL above now; do not wait silently.");
|
|
61
|
+
lines.push(" If browser control is unavailable, give the exact URL to the human verbatim.");
|
|
62
|
+
lines.push(" You cannot approve it yourself, retry it away, or start a substitute handshake.");
|
|
58
63
|
lines.push("");
|
|
59
64
|
return lines;
|
|
60
65
|
}
|
|
@@ -68,10 +73,69 @@ function reminderLines(prompt) {
|
|
|
68
73
|
];
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
// src/open.ts
|
|
77
|
+
var import_node_child_process = require("child_process");
|
|
78
|
+
var import_node_process = __toESM(require("process"), 1);
|
|
79
|
+
async function openUrl(url, options = {}) {
|
|
80
|
+
const command = openerFor(options.platform ?? import_node_process.default.platform);
|
|
81
|
+
const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
|
|
82
|
+
await new Promise((resolve13, reject) => {
|
|
83
|
+
const child = doSpawn(command.cmd, [...command.args, url], {
|
|
84
|
+
stdio: "ignore",
|
|
85
|
+
detached: true
|
|
86
|
+
});
|
|
87
|
+
child.once("error", reject);
|
|
88
|
+
child.once("spawn", () => {
|
|
89
|
+
child.unref();
|
|
90
|
+
resolve13();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function openerFor(platform) {
|
|
95
|
+
if (platform === "darwin") return { cmd: "open", args: [] };
|
|
96
|
+
if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
|
|
97
|
+
return { cmd: "xdg-open", args: [] };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/handshake-approval.ts
|
|
101
|
+
function approvalBrowser(options, host = {}) {
|
|
102
|
+
if (options.open === false) return { open: false, reason: "disabled by --no-open" };
|
|
103
|
+
if (options.open === true) return { open: true, mode: "forced" };
|
|
104
|
+
const env = host.env ?? import_node_process2.default.env;
|
|
105
|
+
if (env.VITEST || env.NODE_ENV === "test") {
|
|
106
|
+
return { open: false, reason: "test environment" };
|
|
107
|
+
}
|
|
108
|
+
return { open: true, mode: "auto" };
|
|
109
|
+
}
|
|
110
|
+
async function presentHandshakeApproval(out, prompt, options) {
|
|
111
|
+
const browser = approvalBrowser(options);
|
|
112
|
+
printApproval(out, {
|
|
113
|
+
...prompt,
|
|
114
|
+
browserAttempted: browser.open,
|
|
115
|
+
browserSkipped: browser.reason
|
|
116
|
+
});
|
|
117
|
+
if (!browser.open) {
|
|
118
|
+
out.error(`auth: browser launch skipped (${browser.reason}) \u2014 open the URL above manually`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
await (options.openApprovalUrl ?? openUrl)(prompt.approvalUrl);
|
|
123
|
+
out.error("auth: browser open requested for the approval URL above");
|
|
124
|
+
} catch (error) {
|
|
125
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
126
|
+
out.error(`auth: browser did not open (${reason}) \u2014 OPEN THE URL ABOVE NOW`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function handshakeUrl(platformUrl, userCode) {
|
|
130
|
+
const url = new URL("/studio", platformUrl);
|
|
131
|
+
url.searchParams.set("code", userCode);
|
|
132
|
+
return url.toString();
|
|
133
|
+
}
|
|
134
|
+
|
|
71
135
|
// src/handshake-state.ts
|
|
72
136
|
var import_node_fs2 = require("fs");
|
|
73
137
|
var import_node_path2 = require("path");
|
|
74
|
-
var
|
|
138
|
+
var import_node_process3 = __toESM(require("process"), 1);
|
|
75
139
|
|
|
76
140
|
// src/local.ts
|
|
77
141
|
var import_node_fs = require("fs");
|
|
@@ -207,9 +271,13 @@ var RESUME_MARGIN_MS = 5e3;
|
|
|
207
271
|
function readPendingHandshake(path, platform, email) {
|
|
208
272
|
const pending = readJsonFile(path);
|
|
209
273
|
if (!pending || pending.platform !== platform || pending.email !== email) return null;
|
|
210
|
-
if (typeof pending.userCode !== "string" || typeof pending.deviceCode !== "string"
|
|
274
|
+
if (typeof pending.userCode !== "string" || typeof pending.deviceCode !== "string") return null;
|
|
211
275
|
if (typeof pending.expiresAt !== "number" || pending.expiresAt <= Date.now() + RESUME_MARGIN_MS) return null;
|
|
212
|
-
return {
|
|
276
|
+
return {
|
|
277
|
+
interval: typeof pending.interval === "number" ? pending.interval : 3,
|
|
278
|
+
...pending,
|
|
279
|
+
approvalUrl: handshakeUrl(platform, pending.userCode)
|
|
280
|
+
};
|
|
213
281
|
}
|
|
214
282
|
function writePendingHandshake(path, pending) {
|
|
215
283
|
writePrivateJson(path, pending);
|
|
@@ -235,47 +303,23 @@ function approvalReminder(out, pending, periodMs = 3e4) {
|
|
|
235
303
|
timer.unref?.();
|
|
236
304
|
return () => clearInterval(timer);
|
|
237
305
|
}
|
|
238
|
-
function handshakeWaitMs(waitSeconds, interactive =
|
|
306
|
+
function handshakeWaitMs(waitSeconds, interactive = import_node_process3.default.stdout.isTTY === true) {
|
|
239
307
|
if (waitSeconds !== void 0) return waitSeconds * 1e3;
|
|
240
308
|
return interactive ? void 0 : 9e4;
|
|
241
309
|
}
|
|
242
310
|
|
|
243
|
-
// src/open.ts
|
|
244
|
-
var import_node_child_process = require("child_process");
|
|
245
|
-
var import_node_process2 = __toESM(require("process"), 1);
|
|
246
|
-
async function openUrl(url, options = {}) {
|
|
247
|
-
const command = openerFor(options.platform ?? import_node_process2.default.platform);
|
|
248
|
-
const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
|
|
249
|
-
await new Promise((resolve13, reject) => {
|
|
250
|
-
const child = doSpawn(command.cmd, [...command.args, url], {
|
|
251
|
-
stdio: "ignore",
|
|
252
|
-
detached: true
|
|
253
|
-
});
|
|
254
|
-
child.once("error", reject);
|
|
255
|
-
child.once("spawn", () => {
|
|
256
|
-
child.unref();
|
|
257
|
-
resolve13();
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
function openerFor(platform) {
|
|
262
|
-
if (platform === "darwin") return { cmd: "open", args: [] };
|
|
263
|
-
if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
|
|
264
|
-
return { cmd: "xdg-open", args: [] };
|
|
265
|
-
}
|
|
266
|
-
|
|
267
311
|
// src/token.ts
|
|
268
312
|
async function getDeveloperToken(cfg, options, doFetch, out) {
|
|
269
313
|
if (options.token) return options.token;
|
|
270
314
|
const audience = platformAudience(cfg.platformUrl);
|
|
271
|
-
if (
|
|
272
|
-
const declared =
|
|
315
|
+
if (import_node_process4.default.env.ODLA_DEV_TOKEN) {
|
|
316
|
+
const declared = import_node_process4.default.env.ODLA_DEV_TOKEN_AUDIENCE;
|
|
273
317
|
if (declared) {
|
|
274
318
|
if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
|
|
275
319
|
} else if (audience !== "https://odla.ai") {
|
|
276
320
|
throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
|
|
277
321
|
}
|
|
278
|
-
return
|
|
322
|
+
return import_node_process4.default.env.ODLA_DEV_TOKEN;
|
|
279
323
|
}
|
|
280
324
|
const cached = readJsonFile(cfg.local.tokenFile);
|
|
281
325
|
if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
|
|
@@ -288,7 +332,6 @@ async function getDeveloperToken(cfg, options, doFetch, out) {
|
|
|
288
332
|
doFetch,
|
|
289
333
|
out,
|
|
290
334
|
audience,
|
|
291
|
-
browser: approvalBrowser(options),
|
|
292
335
|
email: handshakeEmail(options.email, cached?.platform === audience ? cached.email : void 0),
|
|
293
336
|
pendingFile: handshakeFile(cfg)
|
|
294
337
|
};
|
|
@@ -304,7 +347,12 @@ async function resumePendingHandshake(ctx, waitMs) {
|
|
|
304
347
|
if (!pending) return null;
|
|
305
348
|
ctx.out.error("");
|
|
306
349
|
ctx.out.error(`auth: resuming pending handshake \u2014 ${approvalHint(pending)}`);
|
|
307
|
-
await
|
|
350
|
+
await presentHandshakeApproval(ctx.out, {
|
|
351
|
+
userCode: pending.userCode,
|
|
352
|
+
approvalUrl: pending.approvalUrl,
|
|
353
|
+
minutesLeft: Math.max(1, Math.floor((pending.expiresAt - Date.now()) / 6e4)),
|
|
354
|
+
purpose: `sign this terminal in as ${ctx.email}`
|
|
355
|
+
}, ctx.options);
|
|
308
356
|
ctx.out.error("");
|
|
309
357
|
const stopReminder = approvalReminder(ctx.out, pending);
|
|
310
358
|
try {
|
|
@@ -341,8 +389,8 @@ async function freshHandshake(ctx, waitMs) {
|
|
|
341
389
|
projectIds: [ctx.cfg.app.id],
|
|
342
390
|
fetch: ctx.doFetch,
|
|
343
391
|
waitMs,
|
|
344
|
-
onCode: async ({ userCode, deviceCode, expiresIn, interval
|
|
345
|
-
const approvalUrl =
|
|
392
|
+
onCode: async ({ userCode, deviceCode, expiresIn, interval }) => {
|
|
393
|
+
const approvalUrl = handshakeUrl(ctx.audience, userCode);
|
|
346
394
|
started = {
|
|
347
395
|
platform: ctx.audience,
|
|
348
396
|
email: ctx.email,
|
|
@@ -353,15 +401,12 @@ async function freshHandshake(ctx, waitMs) {
|
|
|
353
401
|
expiresAt: Date.now() + expiresIn * 1e3
|
|
354
402
|
};
|
|
355
403
|
writePendingHandshake(ctx.pendingFile, started);
|
|
356
|
-
|
|
404
|
+
await presentHandshakeApproval(ctx.out, {
|
|
357
405
|
userCode,
|
|
358
406
|
approvalUrl,
|
|
359
407
|
minutesLeft: Math.floor(expiresIn / 60),
|
|
360
|
-
purpose: `sign this terminal in as ${ctx.email}
|
|
361
|
-
|
|
362
|
-
browserSkipped: ctx.browser.reason
|
|
363
|
-
});
|
|
364
|
-
await launchApproval(ctx, approvalUrl);
|
|
408
|
+
purpose: `sign this terminal in as ${ctx.email}`
|
|
409
|
+
}, ctx.options);
|
|
365
410
|
stopReminder = approvalReminder(ctx.out, started);
|
|
366
411
|
}
|
|
367
412
|
});
|
|
@@ -376,18 +421,6 @@ async function freshHandshake(ctx, waitMs) {
|
|
|
376
421
|
stopReminder?.();
|
|
377
422
|
}
|
|
378
423
|
}
|
|
379
|
-
async function launchApproval(ctx, approvalUrl) {
|
|
380
|
-
if (ctx.browser.open) {
|
|
381
|
-
try {
|
|
382
|
-
await (ctx.options.openApprovalUrl ?? openUrl)(approvalUrl);
|
|
383
|
-
ctx.out.error(`auth: asked the OS to open a browser${ctx.browser.mode === "auto" ? " (auto)" : ""} \u2014 if no tab appeared, use the URL above`);
|
|
384
|
-
} catch (err) {
|
|
385
|
-
ctx.out.error(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
|
|
386
|
-
}
|
|
387
|
-
} else if (ctx.browser.reason) {
|
|
388
|
-
ctx.out.error(`auth: browser launch skipped (${ctx.browser.reason}) \u2014 show the human the URL above`);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
424
|
function stillPending(pending, email) {
|
|
392
425
|
return new import_db.OdlaError(
|
|
393
426
|
"handshake_pending",
|
|
@@ -396,30 +429,12 @@ function stillPending(pending, email) {
|
|
|
396
429
|
);
|
|
397
430
|
}
|
|
398
431
|
function handshakeEmail(value2, cached) {
|
|
399
|
-
const email = (value2 ??
|
|
432
|
+
const email = (value2 ?? import_node_process4.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
|
|
400
433
|
if (email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
401
434
|
throw new Error("a fresh odla handshake requires --email <account> or ODLA_USER_EMAIL");
|
|
402
435
|
}
|
|
403
436
|
return email;
|
|
404
437
|
}
|
|
405
|
-
function approvalBrowser(options, host = {}) {
|
|
406
|
-
if (options.open === true) return { open: true, mode: "forced" };
|
|
407
|
-
if (options.open === false) return { open: false, reason: "disabled by --no-open" };
|
|
408
|
-
const env = host.env ?? import_node_process3.default.env;
|
|
409
|
-
if (env.VITEST || env.NODE_ENV === "test") return { open: false, reason: "test environment" };
|
|
410
|
-
if (env.CI) return { open: false, reason: "CI environment" };
|
|
411
|
-
if (env.SSH_CONNECTION || env.SSH_TTY) return { open: false, reason: "SSH session; pass --open to force" };
|
|
412
|
-
const platform = host.platform ?? import_node_process3.default.platform;
|
|
413
|
-
if (platform !== "darwin" && platform !== "win32" && !env.DISPLAY && !env.WAYLAND_DISPLAY) {
|
|
414
|
-
return { open: false, reason: "no graphical display; pass --open to force" };
|
|
415
|
-
}
|
|
416
|
-
return { open: true, mode: "auto" };
|
|
417
|
-
}
|
|
418
|
-
function handshakeUrl(platformUrl, userCode) {
|
|
419
|
-
const url = new URL("/studio", platformUrl);
|
|
420
|
-
url.searchParams.set("code", userCode);
|
|
421
|
-
return url.toString();
|
|
422
|
-
}
|
|
423
438
|
function platformAudience(value2) {
|
|
424
439
|
let url;
|
|
425
440
|
try {
|
|
@@ -438,12 +453,12 @@ function platformAudience(value2) {
|
|
|
438
453
|
}
|
|
439
454
|
|
|
440
455
|
// src/secret-input.ts
|
|
441
|
-
var
|
|
456
|
+
var import_node_process5 = __toESM(require("process"), 1);
|
|
442
457
|
var MAX_BYTES = 64 * 1024;
|
|
443
458
|
async function secretInputValue(options, kind = "credential") {
|
|
444
459
|
if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
|
|
445
460
|
let value2;
|
|
446
|
-
if (options.fromEnv) value2 =
|
|
461
|
+
if (options.fromEnv) value2 = import_node_process5.default.env[options.fromEnv];
|
|
447
462
|
else if (options.stdin) value2 = await (options.readStdin ?? (() => readSecretStream(kind)))();
|
|
448
463
|
else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
|
|
449
464
|
value2 = value2?.replace(/[\r\n]+$/, "");
|
|
@@ -451,7 +466,7 @@ async function secretInputValue(options, kind = "credential") {
|
|
|
451
466
|
if (new TextEncoder().encode(value2).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
|
|
452
467
|
return value2;
|
|
453
468
|
}
|
|
454
|
-
async function readSecretStream(kind, stream =
|
|
469
|
+
async function readSecretStream(kind, stream = import_node_process5.default.stdin) {
|
|
455
470
|
let value2 = "";
|
|
456
471
|
for await (const chunk of stream) {
|
|
457
472
|
value2 += String(chunk);
|
|
@@ -463,7 +478,7 @@ async function readSecretStream(kind, stream = import_node_process4.default.stdi
|
|
|
463
478
|
// src/admin-ai-auth.ts
|
|
464
479
|
var import_node_fs3 = require("fs");
|
|
465
480
|
var import_node_path3 = require("path");
|
|
466
|
-
var
|
|
481
|
+
var import_node_process6 = __toESM(require("process"), 1);
|
|
467
482
|
var import_db2 = require("@odla-ai/db");
|
|
468
483
|
async function getScopedPlatformToken(options) {
|
|
469
484
|
return resolveAdminPlatformToken(options);
|
|
@@ -471,7 +486,7 @@ async function getScopedPlatformToken(options) {
|
|
|
471
486
|
async function resolveAdminPlatformToken(options) {
|
|
472
487
|
const audience = platformAudience(options.platform);
|
|
473
488
|
if (options.token) return options.token;
|
|
474
|
-
const fromEnv =
|
|
489
|
+
const fromEnv = import_node_process6.default.env.ODLA_ADMIN_TOKEN;
|
|
475
490
|
if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
|
|
476
491
|
return scopedToken(
|
|
477
492
|
audience,
|
|
@@ -483,7 +498,7 @@ async function resolveAdminPlatformToken(options) {
|
|
|
483
498
|
}
|
|
484
499
|
function audienceBoundEnvToken(token, platform) {
|
|
485
500
|
const audience = platformAudience(platform);
|
|
486
|
-
const declared =
|
|
501
|
+
const declared = import_node_process6.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
|
|
487
502
|
if (declared) {
|
|
488
503
|
if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
|
|
489
504
|
} else if (audience !== "https://odla.ai") {
|
|
@@ -508,7 +523,7 @@ var SCOPE_PURPOSE = {
|
|
|
508
523
|
};
|
|
509
524
|
async function scopedToken(platform, scope, options, doFetch, out) {
|
|
510
525
|
const audience = platformAudience(platform);
|
|
511
|
-
const rootDir = options.rootDir ??
|
|
526
|
+
const rootDir = options.rootDir ?? import_node_process6.default.cwd();
|
|
512
527
|
const tokenFile = options.tokenFile ?? (0, import_node_path3.join)(rootDir, ".odla/admin-token.local.json");
|
|
513
528
|
const cache = options.cache === false ? null : readJsonFile(tokenFile);
|
|
514
529
|
const cached = cache?.platform === audience ? cache.tokens?.[scope] : void 0;
|
|
@@ -523,21 +538,14 @@ async function scopedToken(platform, scope, options, doFetch, out) {
|
|
|
523
538
|
label: options.label ?? `odla CLI (${scope})`,
|
|
524
539
|
scopes: [scope],
|
|
525
540
|
fetch: doFetch,
|
|
526
|
-
onCode: async ({ userCode, expiresIn
|
|
527
|
-
|
|
528
|
-
const browser = approvalBrowser({ open: options.open });
|
|
529
|
-
printApproval(out, {
|
|
541
|
+
onCode: async ({ userCode, expiresIn }) => {
|
|
542
|
+
await presentHandshakeApproval(out, {
|
|
530
543
|
userCode,
|
|
531
|
-
approvalUrl,
|
|
544
|
+
approvalUrl: handshakeUrl(audience, userCode),
|
|
532
545
|
minutesLeft: Math.floor(expiresIn / 60),
|
|
533
546
|
purpose: SCOPE_PURPOSE[scope] ?? `use ${scope}`,
|
|
534
|
-
approver: scope.startsWith("app:") ? "A signed-in app owner" : "A signed-in odla platform admin"
|
|
535
|
-
|
|
536
|
-
browserSkipped: browser.reason
|
|
537
|
-
});
|
|
538
|
-
if (browser.open) {
|
|
539
|
-
await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
|
|
540
|
-
}
|
|
547
|
+
approver: scope.startsWith("app:") ? "A signed-in app owner" : "A signed-in odla platform admin"
|
|
548
|
+
}, options);
|
|
541
549
|
}
|
|
542
550
|
});
|
|
543
551
|
if (options.cache !== false) {
|
|
@@ -720,7 +728,7 @@ function isRecord2(value2) {
|
|
|
720
728
|
|
|
721
729
|
// src/admin-ai.ts
|
|
722
730
|
async function adminAi(options) {
|
|
723
|
-
const platform = platformAudience(options.platform ??
|
|
731
|
+
const platform = platformAudience(options.platform ?? import_node_process7.default.env.ODLA_PLATFORM ?? "https://odla.ai");
|
|
724
732
|
const doFetch = options.fetch ?? fetch;
|
|
725
733
|
const out = options.stdout ?? console;
|
|
726
734
|
const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
|
|
@@ -977,7 +985,7 @@ function addOption(options, name, value2) {
|
|
|
977
985
|
// src/operator-context.ts
|
|
978
986
|
var import_node_fs6 = require("fs");
|
|
979
987
|
var import_node_path6 = require("path");
|
|
980
|
-
var
|
|
988
|
+
var import_node_process9 = __toESM(require("process"), 1);
|
|
981
989
|
|
|
982
990
|
// src/config.ts
|
|
983
991
|
var import_node_fs4 = require("fs");
|
|
@@ -1283,15 +1291,15 @@ function unique2(values) {
|
|
|
1283
1291
|
var import_node_fs5 = require("fs");
|
|
1284
1292
|
var import_node_os = require("os");
|
|
1285
1293
|
var import_node_path5 = require("path");
|
|
1286
|
-
var
|
|
1294
|
+
var import_node_process8 = __toESM(require("process"), 1);
|
|
1287
1295
|
function operatorProfileFile() {
|
|
1288
1296
|
return (0, import_node_path5.resolve)(
|
|
1289
|
-
clean(
|
|
1297
|
+
clean(import_node_process8.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path5.join)((0, import_node_os.homedir)(), ".odla", "contexts.json")
|
|
1290
1298
|
);
|
|
1291
1299
|
}
|
|
1292
1300
|
function resolveOperatorProfile(parsed) {
|
|
1293
1301
|
const fromFlag = clean(stringOpt(parsed.options.context));
|
|
1294
|
-
const fromEnvironment = clean(
|
|
1302
|
+
const fromEnvironment = clean(import_node_process8.default.env.ODLA_CONTEXT);
|
|
1295
1303
|
const name = fromFlag ?? fromEnvironment ?? null;
|
|
1296
1304
|
const file = operatorProfileFile();
|
|
1297
1305
|
if (!name) {
|
|
@@ -1420,13 +1428,13 @@ async function resolveOperatorContext(parsed, options = {}) {
|
|
|
1420
1428
|
}
|
|
1421
1429
|
const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
|
|
1422
1430
|
const platformFlag = clean2(stringOpt(parsed.options.platform));
|
|
1423
|
-
const platformEnvironment = clean2(
|
|
1431
|
+
const platformEnvironment = clean2(import_node_process9.default.env.ODLA_PLATFORM_URL);
|
|
1424
1432
|
const platformValue = platformAudience(
|
|
1425
1433
|
platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
|
|
1426
1434
|
);
|
|
1427
1435
|
const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
|
|
1428
1436
|
const appFlag = clean2(stringOpt(parsed.options.app));
|
|
1429
|
-
const appEnvironment = clean2(
|
|
1437
|
+
const appEnvironment = clean2(import_node_process9.default.env.ODLA_APP_ID);
|
|
1430
1438
|
const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
|
|
1431
1439
|
const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
|
|
1432
1440
|
if (appValue) assertOperatorName(appValue, "app");
|
|
@@ -1436,16 +1444,16 @@ async function resolveOperatorContext(parsed, options = {}) {
|
|
|
1436
1444
|
);
|
|
1437
1445
|
}
|
|
1438
1446
|
const envFlag = clean2(stringOpt(parsed.options.env));
|
|
1439
|
-
const envEnvironment = clean2(
|
|
1447
|
+
const envEnvironment = clean2(import_node_process9.default.env.ODLA_ENV);
|
|
1440
1448
|
const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
|
|
1441
1449
|
const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
|
|
1442
1450
|
if (environmentValue) {
|
|
1443
1451
|
assertOperatorName(environmentValue, "environment");
|
|
1444
1452
|
}
|
|
1445
|
-
const rootDir = loaded?.rootDir ??
|
|
1453
|
+
const rootDir = loaded?.rootDir ?? import_node_process9.default.cwd();
|
|
1446
1454
|
const profileCredentials = operatorCredentialFiles(profile);
|
|
1447
|
-
const tokenFile = clean2(
|
|
1448
|
-
const scopedTokenFile = clean2(
|
|
1455
|
+
const tokenFile = clean2(import_node_process9.default.env.ODLA_DEV_TOKEN_FILE) ? (0, import_node_path6.resolve)(import_node_process9.default.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
|
|
1456
|
+
const scopedTokenFile = clean2(import_node_process9.default.env.ODLA_ADMIN_TOKEN_FILE) ? (0, import_node_path6.resolve)(import_node_process9.default.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? (0, import_node_path6.join)(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
|
|
1449
1457
|
const cfg = loaded ? {
|
|
1450
1458
|
...loaded,
|
|
1451
1459
|
platformUrl: platformValue,
|
|
@@ -8497,13 +8505,13 @@ async function codeCommand(parsed, dependencies) {
|
|
|
8497
8505
|
}
|
|
8498
8506
|
|
|
8499
8507
|
// src/operator-credentials.ts
|
|
8500
|
-
var
|
|
8508
|
+
var import_node_process10 = __toESM(require("process"), 1);
|
|
8501
8509
|
function developerTokenStatus(context, parsed, now = Date.now()) {
|
|
8502
8510
|
const cached = readJsonFile(context.cfg.local.tokenFile);
|
|
8503
8511
|
const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
|
|
8504
8512
|
const source = clean3(
|
|
8505
8513
|
stringOpt(parsed.options.token)
|
|
8506
|
-
) ? "flag" : clean3(
|
|
8514
|
+
) ? "flag" : clean3(import_node_process10.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
|
|
8507
8515
|
return {
|
|
8508
8516
|
source,
|
|
8509
8517
|
cacheFile: context.cfg.local.tokenFile,
|
|
@@ -8877,15 +8885,16 @@ Safety:
|
|
|
8877
8885
|
the metadata file. Flags and specific ODLA_* scope variables beat a selected
|
|
8878
8886
|
context, which beats project config. There is no ambient current context.
|
|
8879
8887
|
"context show" reports only provenance and cache state and never authenticates.
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
8888
|
+
Every real CLI handshake prints one canonical /studio?code= approval URL and
|
|
8889
|
+
attempts to open it \u2014 including in CI, SSH, display-less, and agent-driven
|
|
8890
|
+
shells. Only --no-open suppresses the attempt. Browser launch is best-effort:
|
|
8891
|
+
agents with browser control must open that exact URL immediately; otherwise
|
|
8892
|
+
they must give it to the human verbatim. A started handshake is persisted
|
|
8885
8893
|
under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
|
|
8886
8894
|
the same code. Outside an interactive terminal the wait is capped (90s by
|
|
8887
8895
|
default, --wait <seconds> to change); a still-pending handshake then exits
|
|
8888
|
-
with code 75:
|
|
8896
|
+
with code 75: open the same URL (or relay it if browser control is unavailable),
|
|
8897
|
+
wait for approval, and re-run to collect.
|
|
8889
8898
|
A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
|
|
8890
8899
|
The email is a non-secret identity hint: never provide a password or session
|
|
8891
8900
|
token. The matching account must already exist, be signed in, explicitly
|
|
@@ -10294,7 +10303,7 @@ async function read2(url, headers, doFetch) {
|
|
|
10294
10303
|
// src/provision.ts
|
|
10295
10304
|
var import_apps12 = require("@odla-ai/apps");
|
|
10296
10305
|
var import_ai3 = require("@odla-ai/ai");
|
|
10297
|
-
var
|
|
10306
|
+
var import_node_process11 = __toESM(require("process"), 1);
|
|
10298
10307
|
|
|
10299
10308
|
// src/integration-provision.ts
|
|
10300
10309
|
var import_db3 = require("@odla-ai/db");
|
|
@@ -10601,7 +10610,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
10601
10610
|
await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
|
|
10602
10611
|
}
|
|
10603
10612
|
if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
10604
|
-
const key =
|
|
10613
|
+
const key = import_node_process11.default.env[cfg.ai.keyEnv];
|
|
10605
10614
|
if (key) {
|
|
10606
10615
|
const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
|
|
10607
10616
|
await (0, import_ai3.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
|
|
@@ -10643,7 +10652,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
10643
10652
|
|
|
10644
10653
|
// src/record.ts
|
|
10645
10654
|
var import_node_fs16 = require("fs");
|
|
10646
|
-
var
|
|
10655
|
+
var import_node_process12 = __toESM(require("process"), 1);
|
|
10647
10656
|
|
|
10648
10657
|
// src/surface.ts
|
|
10649
10658
|
var PM_ACTIONS = {
|
|
@@ -10792,7 +10801,7 @@ function invocationPath(words2) {
|
|
|
10792
10801
|
|
|
10793
10802
|
// src/record.ts
|
|
10794
10803
|
function recordInvocation(parsed) {
|
|
10795
|
-
const file =
|
|
10804
|
+
const file = import_node_process12.default.env.ODLA_CLI_RECORD;
|
|
10796
10805
|
if (!file) return;
|
|
10797
10806
|
try {
|
|
10798
10807
|
const entry = {
|
|
@@ -11464,9 +11473,9 @@ var import_node_child_process8 = require("child_process");
|
|
|
11464
11473
|
var import_node_fs20 = require("fs");
|
|
11465
11474
|
var import_node_os5 = require("os");
|
|
11466
11475
|
var import_node_path18 = require("path");
|
|
11467
|
-
var
|
|
11476
|
+
var import_node_process13 = __toESM(require("process"), 1);
|
|
11468
11477
|
var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
|
|
11469
|
-
function resolveEditor(env =
|
|
11478
|
+
function resolveEditor(env = import_node_process13.default.env) {
|
|
11470
11479
|
for (const name of EDITOR_ENV) {
|
|
11471
11480
|
const value2 = env[name];
|
|
11472
11481
|
if (value2 && value2.trim()) return value2.trim();
|
|
@@ -11480,8 +11489,8 @@ function defaultRun(command, path) {
|
|
|
11480
11489
|
return result.status ?? 0;
|
|
11481
11490
|
}
|
|
11482
11491
|
function editText(initial, slug, deps = {}) {
|
|
11483
|
-
const env = deps.env ??
|
|
11484
|
-
const interactive = deps.interactive ?? (() => Boolean(
|
|
11492
|
+
const env = deps.env ?? import_node_process13.default.env;
|
|
11493
|
+
const interactive = deps.interactive ?? (() => Boolean(import_node_process13.default.stdin.isTTY));
|
|
11485
11494
|
const editor = resolveEditor(env);
|
|
11486
11495
|
if (!editor)
|
|
11487
11496
|
throw new Error(
|