@odla-ai/cli 0.27.0 → 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 +133 -123
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-DS64H4FY.js → chunk-JWEBGIBR.js} +133 -123
- package/dist/chunk-JWEBGIBR.js.map +1 -0
- package/dist/index.cjs +133 -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-DS64H4FY.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 {
|
|
@@ -338,10 +386,11 @@ async function freshHandshake(ctx, waitMs) {
|
|
|
338
386
|
endpoint: ctx.cfg.platformUrl,
|
|
339
387
|
email: ctx.email,
|
|
340
388
|
label: `${ctx.cfg.app.id} provisioner`,
|
|
389
|
+
projectIds: [ctx.cfg.app.id],
|
|
341
390
|
fetch: ctx.doFetch,
|
|
342
391
|
waitMs,
|
|
343
|
-
onCode: async ({ userCode, deviceCode, expiresIn, interval
|
|
344
|
-
const approvalUrl =
|
|
392
|
+
onCode: async ({ userCode, deviceCode, expiresIn, interval }) => {
|
|
393
|
+
const approvalUrl = handshakeUrl(ctx.audience, userCode);
|
|
345
394
|
started = {
|
|
346
395
|
platform: ctx.audience,
|
|
347
396
|
email: ctx.email,
|
|
@@ -352,15 +401,12 @@ async function freshHandshake(ctx, waitMs) {
|
|
|
352
401
|
expiresAt: Date.now() + expiresIn * 1e3
|
|
353
402
|
};
|
|
354
403
|
writePendingHandshake(ctx.pendingFile, started);
|
|
355
|
-
|
|
404
|
+
await presentHandshakeApproval(ctx.out, {
|
|
356
405
|
userCode,
|
|
357
406
|
approvalUrl,
|
|
358
407
|
minutesLeft: Math.floor(expiresIn / 60),
|
|
359
|
-
purpose: `sign this terminal in as ${ctx.email}
|
|
360
|
-
|
|
361
|
-
browserSkipped: ctx.browser.reason
|
|
362
|
-
});
|
|
363
|
-
await launchApproval(ctx, approvalUrl);
|
|
408
|
+
purpose: `sign this terminal in as ${ctx.email}`
|
|
409
|
+
}, ctx.options);
|
|
364
410
|
stopReminder = approvalReminder(ctx.out, started);
|
|
365
411
|
}
|
|
366
412
|
});
|
|
@@ -375,18 +421,6 @@ async function freshHandshake(ctx, waitMs) {
|
|
|
375
421
|
stopReminder?.();
|
|
376
422
|
}
|
|
377
423
|
}
|
|
378
|
-
async function launchApproval(ctx, approvalUrl) {
|
|
379
|
-
if (ctx.browser.open) {
|
|
380
|
-
try {
|
|
381
|
-
await (ctx.options.openApprovalUrl ?? openUrl)(approvalUrl);
|
|
382
|
-
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`);
|
|
383
|
-
} catch (err) {
|
|
384
|
-
ctx.out.error(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
|
|
385
|
-
}
|
|
386
|
-
} else if (ctx.browser.reason) {
|
|
387
|
-
ctx.out.error(`auth: browser launch skipped (${ctx.browser.reason}) \u2014 show the human the URL above`);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
424
|
function stillPending(pending, email) {
|
|
391
425
|
return new import_db.OdlaError(
|
|
392
426
|
"handshake_pending",
|
|
@@ -395,30 +429,12 @@ function stillPending(pending, email) {
|
|
|
395
429
|
);
|
|
396
430
|
}
|
|
397
431
|
function handshakeEmail(value2, cached) {
|
|
398
|
-
const email = (value2 ??
|
|
432
|
+
const email = (value2 ?? import_node_process4.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
|
|
399
433
|
if (email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
400
434
|
throw new Error("a fresh odla handshake requires --email <account> or ODLA_USER_EMAIL");
|
|
401
435
|
}
|
|
402
436
|
return email;
|
|
403
437
|
}
|
|
404
|
-
function approvalBrowser(options, host = {}) {
|
|
405
|
-
if (options.open === true) return { open: true, mode: "forced" };
|
|
406
|
-
if (options.open === false) return { open: false, reason: "disabled by --no-open" };
|
|
407
|
-
const env = host.env ?? import_node_process3.default.env;
|
|
408
|
-
if (env.VITEST || env.NODE_ENV === "test") return { open: false, reason: "test environment" };
|
|
409
|
-
if (env.CI) return { open: false, reason: "CI environment" };
|
|
410
|
-
if (env.SSH_CONNECTION || env.SSH_TTY) return { open: false, reason: "SSH session; pass --open to force" };
|
|
411
|
-
const platform = host.platform ?? import_node_process3.default.platform;
|
|
412
|
-
if (platform !== "darwin" && platform !== "win32" && !env.DISPLAY && !env.WAYLAND_DISPLAY) {
|
|
413
|
-
return { open: false, reason: "no graphical display; pass --open to force" };
|
|
414
|
-
}
|
|
415
|
-
return { open: true, mode: "auto" };
|
|
416
|
-
}
|
|
417
|
-
function handshakeUrl(platformUrl, userCode) {
|
|
418
|
-
const url = new URL("/studio", platformUrl);
|
|
419
|
-
url.searchParams.set("code", userCode);
|
|
420
|
-
return url.toString();
|
|
421
|
-
}
|
|
422
438
|
function platformAudience(value2) {
|
|
423
439
|
let url;
|
|
424
440
|
try {
|
|
@@ -437,12 +453,12 @@ function platformAudience(value2) {
|
|
|
437
453
|
}
|
|
438
454
|
|
|
439
455
|
// src/secret-input.ts
|
|
440
|
-
var
|
|
456
|
+
var import_node_process5 = __toESM(require("process"), 1);
|
|
441
457
|
var MAX_BYTES = 64 * 1024;
|
|
442
458
|
async function secretInputValue(options, kind = "credential") {
|
|
443
459
|
if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
|
|
444
460
|
let value2;
|
|
445
|
-
if (options.fromEnv) value2 =
|
|
461
|
+
if (options.fromEnv) value2 = import_node_process5.default.env[options.fromEnv];
|
|
446
462
|
else if (options.stdin) value2 = await (options.readStdin ?? (() => readSecretStream(kind)))();
|
|
447
463
|
else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
|
|
448
464
|
value2 = value2?.replace(/[\r\n]+$/, "");
|
|
@@ -450,7 +466,7 @@ async function secretInputValue(options, kind = "credential") {
|
|
|
450
466
|
if (new TextEncoder().encode(value2).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
|
|
451
467
|
return value2;
|
|
452
468
|
}
|
|
453
|
-
async function readSecretStream(kind, stream =
|
|
469
|
+
async function readSecretStream(kind, stream = import_node_process5.default.stdin) {
|
|
454
470
|
let value2 = "";
|
|
455
471
|
for await (const chunk of stream) {
|
|
456
472
|
value2 += String(chunk);
|
|
@@ -462,7 +478,7 @@ async function readSecretStream(kind, stream = import_node_process4.default.stdi
|
|
|
462
478
|
// src/admin-ai-auth.ts
|
|
463
479
|
var import_node_fs3 = require("fs");
|
|
464
480
|
var import_node_path3 = require("path");
|
|
465
|
-
var
|
|
481
|
+
var import_node_process6 = __toESM(require("process"), 1);
|
|
466
482
|
var import_db2 = require("@odla-ai/db");
|
|
467
483
|
async function getScopedPlatformToken(options) {
|
|
468
484
|
return resolveAdminPlatformToken(options);
|
|
@@ -470,7 +486,7 @@ async function getScopedPlatformToken(options) {
|
|
|
470
486
|
async function resolveAdminPlatformToken(options) {
|
|
471
487
|
const audience = platformAudience(options.platform);
|
|
472
488
|
if (options.token) return options.token;
|
|
473
|
-
const fromEnv =
|
|
489
|
+
const fromEnv = import_node_process6.default.env.ODLA_ADMIN_TOKEN;
|
|
474
490
|
if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
|
|
475
491
|
return scopedToken(
|
|
476
492
|
audience,
|
|
@@ -482,7 +498,7 @@ async function resolveAdminPlatformToken(options) {
|
|
|
482
498
|
}
|
|
483
499
|
function audienceBoundEnvToken(token, platform) {
|
|
484
500
|
const audience = platformAudience(platform);
|
|
485
|
-
const declared =
|
|
501
|
+
const declared = import_node_process6.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
|
|
486
502
|
if (declared) {
|
|
487
503
|
if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
|
|
488
504
|
} else if (audience !== "https://odla.ai") {
|
|
@@ -507,7 +523,7 @@ var SCOPE_PURPOSE = {
|
|
|
507
523
|
};
|
|
508
524
|
async function scopedToken(platform, scope, options, doFetch, out) {
|
|
509
525
|
const audience = platformAudience(platform);
|
|
510
|
-
const rootDir = options.rootDir ??
|
|
526
|
+
const rootDir = options.rootDir ?? import_node_process6.default.cwd();
|
|
511
527
|
const tokenFile = options.tokenFile ?? (0, import_node_path3.join)(rootDir, ".odla/admin-token.local.json");
|
|
512
528
|
const cache = options.cache === false ? null : readJsonFile(tokenFile);
|
|
513
529
|
const cached = cache?.platform === audience ? cache.tokens?.[scope] : void 0;
|
|
@@ -522,21 +538,14 @@ async function scopedToken(platform, scope, options, doFetch, out) {
|
|
|
522
538
|
label: options.label ?? `odla CLI (${scope})`,
|
|
523
539
|
scopes: [scope],
|
|
524
540
|
fetch: doFetch,
|
|
525
|
-
onCode: async ({ userCode, expiresIn
|
|
526
|
-
|
|
527
|
-
const browser = approvalBrowser({ open: options.open });
|
|
528
|
-
printApproval(out, {
|
|
541
|
+
onCode: async ({ userCode, expiresIn }) => {
|
|
542
|
+
await presentHandshakeApproval(out, {
|
|
529
543
|
userCode,
|
|
530
|
-
approvalUrl,
|
|
544
|
+
approvalUrl: handshakeUrl(audience, userCode),
|
|
531
545
|
minutesLeft: Math.floor(expiresIn / 60),
|
|
532
546
|
purpose: SCOPE_PURPOSE[scope] ?? `use ${scope}`,
|
|
533
|
-
approver: scope.startsWith("app:") ? "A signed-in app owner" : "A signed-in odla platform admin"
|
|
534
|
-
|
|
535
|
-
browserSkipped: browser.reason
|
|
536
|
-
});
|
|
537
|
-
if (browser.open) {
|
|
538
|
-
await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
|
|
539
|
-
}
|
|
547
|
+
approver: scope.startsWith("app:") ? "A signed-in app owner" : "A signed-in odla platform admin"
|
|
548
|
+
}, options);
|
|
540
549
|
}
|
|
541
550
|
});
|
|
542
551
|
if (options.cache !== false) {
|
|
@@ -719,7 +728,7 @@ function isRecord2(value2) {
|
|
|
719
728
|
|
|
720
729
|
// src/admin-ai.ts
|
|
721
730
|
async function adminAi(options) {
|
|
722
|
-
const platform = platformAudience(options.platform ??
|
|
731
|
+
const platform = platformAudience(options.platform ?? import_node_process7.default.env.ODLA_PLATFORM ?? "https://odla.ai");
|
|
723
732
|
const doFetch = options.fetch ?? fetch;
|
|
724
733
|
const out = options.stdout ?? console;
|
|
725
734
|
const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
|
|
@@ -976,7 +985,7 @@ function addOption(options, name, value2) {
|
|
|
976
985
|
// src/operator-context.ts
|
|
977
986
|
var import_node_fs6 = require("fs");
|
|
978
987
|
var import_node_path6 = require("path");
|
|
979
|
-
var
|
|
988
|
+
var import_node_process9 = __toESM(require("process"), 1);
|
|
980
989
|
|
|
981
990
|
// src/config.ts
|
|
982
991
|
var import_node_fs4 = require("fs");
|
|
@@ -1282,15 +1291,15 @@ function unique2(values) {
|
|
|
1282
1291
|
var import_node_fs5 = require("fs");
|
|
1283
1292
|
var import_node_os = require("os");
|
|
1284
1293
|
var import_node_path5 = require("path");
|
|
1285
|
-
var
|
|
1294
|
+
var import_node_process8 = __toESM(require("process"), 1);
|
|
1286
1295
|
function operatorProfileFile() {
|
|
1287
1296
|
return (0, import_node_path5.resolve)(
|
|
1288
|
-
clean(
|
|
1297
|
+
clean(import_node_process8.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path5.join)((0, import_node_os.homedir)(), ".odla", "contexts.json")
|
|
1289
1298
|
);
|
|
1290
1299
|
}
|
|
1291
1300
|
function resolveOperatorProfile(parsed) {
|
|
1292
1301
|
const fromFlag = clean(stringOpt(parsed.options.context));
|
|
1293
|
-
const fromEnvironment = clean(
|
|
1302
|
+
const fromEnvironment = clean(import_node_process8.default.env.ODLA_CONTEXT);
|
|
1294
1303
|
const name = fromFlag ?? fromEnvironment ?? null;
|
|
1295
1304
|
const file = operatorProfileFile();
|
|
1296
1305
|
if (!name) {
|
|
@@ -1419,13 +1428,13 @@ async function resolveOperatorContext(parsed, options = {}) {
|
|
|
1419
1428
|
}
|
|
1420
1429
|
const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
|
|
1421
1430
|
const platformFlag = clean2(stringOpt(parsed.options.platform));
|
|
1422
|
-
const platformEnvironment = clean2(
|
|
1431
|
+
const platformEnvironment = clean2(import_node_process9.default.env.ODLA_PLATFORM_URL);
|
|
1423
1432
|
const platformValue = platformAudience(
|
|
1424
1433
|
platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
|
|
1425
1434
|
);
|
|
1426
1435
|
const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
|
|
1427
1436
|
const appFlag = clean2(stringOpt(parsed.options.app));
|
|
1428
|
-
const appEnvironment = clean2(
|
|
1437
|
+
const appEnvironment = clean2(import_node_process9.default.env.ODLA_APP_ID);
|
|
1429
1438
|
const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
|
|
1430
1439
|
const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
|
|
1431
1440
|
if (appValue) assertOperatorName(appValue, "app");
|
|
@@ -1435,16 +1444,16 @@ async function resolveOperatorContext(parsed, options = {}) {
|
|
|
1435
1444
|
);
|
|
1436
1445
|
}
|
|
1437
1446
|
const envFlag = clean2(stringOpt(parsed.options.env));
|
|
1438
|
-
const envEnvironment = clean2(
|
|
1447
|
+
const envEnvironment = clean2(import_node_process9.default.env.ODLA_ENV);
|
|
1439
1448
|
const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
|
|
1440
1449
|
const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
|
|
1441
1450
|
if (environmentValue) {
|
|
1442
1451
|
assertOperatorName(environmentValue, "environment");
|
|
1443
1452
|
}
|
|
1444
|
-
const rootDir = loaded?.rootDir ??
|
|
1453
|
+
const rootDir = loaded?.rootDir ?? import_node_process9.default.cwd();
|
|
1445
1454
|
const profileCredentials = operatorCredentialFiles(profile);
|
|
1446
|
-
const tokenFile = clean2(
|
|
1447
|
-
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;
|
|
1448
1457
|
const cfg = loaded ? {
|
|
1449
1458
|
...loaded,
|
|
1450
1459
|
platformUrl: platformValue,
|
|
@@ -8496,13 +8505,13 @@ async function codeCommand(parsed, dependencies) {
|
|
|
8496
8505
|
}
|
|
8497
8506
|
|
|
8498
8507
|
// src/operator-credentials.ts
|
|
8499
|
-
var
|
|
8508
|
+
var import_node_process10 = __toESM(require("process"), 1);
|
|
8500
8509
|
function developerTokenStatus(context, parsed, now = Date.now()) {
|
|
8501
8510
|
const cached = readJsonFile(context.cfg.local.tokenFile);
|
|
8502
8511
|
const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
|
|
8503
8512
|
const source = clean3(
|
|
8504
8513
|
stringOpt(parsed.options.token)
|
|
8505
|
-
) ? "flag" : clean3(
|
|
8514
|
+
) ? "flag" : clean3(import_node_process10.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
|
|
8506
8515
|
return {
|
|
8507
8516
|
source,
|
|
8508
8517
|
cacheFile: context.cfg.local.tokenFile,
|
|
@@ -8876,15 +8885,16 @@ Safety:
|
|
|
8876
8885
|
the metadata file. Flags and specific ODLA_* scope variables beat a selected
|
|
8877
8886
|
context, which beats project config. There is no ambient current context.
|
|
8878
8887
|
"context show" reports only provenance and cache state and never authenticates.
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
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
|
|
8884
8893
|
under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
|
|
8885
8894
|
the same code. Outside an interactive terminal the wait is capped (90s by
|
|
8886
8895
|
default, --wait <seconds> to change); a still-pending handshake then exits
|
|
8887
|
-
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.
|
|
8888
8898
|
A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
|
|
8889
8899
|
The email is a non-secret identity hint: never provide a password or session
|
|
8890
8900
|
token. The matching account must already exist, be signed in, explicitly
|
|
@@ -10293,7 +10303,7 @@ async function read2(url, headers, doFetch) {
|
|
|
10293
10303
|
// src/provision.ts
|
|
10294
10304
|
var import_apps12 = require("@odla-ai/apps");
|
|
10295
10305
|
var import_ai3 = require("@odla-ai/ai");
|
|
10296
|
-
var
|
|
10306
|
+
var import_node_process11 = __toESM(require("process"), 1);
|
|
10297
10307
|
|
|
10298
10308
|
// src/integration-provision.ts
|
|
10299
10309
|
var import_db3 = require("@odla-ai/db");
|
|
@@ -10600,7 +10610,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
10600
10610
|
await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
|
|
10601
10611
|
}
|
|
10602
10612
|
if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
|
|
10603
|
-
const key =
|
|
10613
|
+
const key = import_node_process11.default.env[cfg.ai.keyEnv];
|
|
10604
10614
|
if (key) {
|
|
10605
10615
|
const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
|
|
10606
10616
|
await (0, import_ai3.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
|
|
@@ -10642,7 +10652,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
10642
10652
|
|
|
10643
10653
|
// src/record.ts
|
|
10644
10654
|
var import_node_fs16 = require("fs");
|
|
10645
|
-
var
|
|
10655
|
+
var import_node_process12 = __toESM(require("process"), 1);
|
|
10646
10656
|
|
|
10647
10657
|
// src/surface.ts
|
|
10648
10658
|
var PM_ACTIONS = {
|
|
@@ -10791,7 +10801,7 @@ function invocationPath(words2) {
|
|
|
10791
10801
|
|
|
10792
10802
|
// src/record.ts
|
|
10793
10803
|
function recordInvocation(parsed) {
|
|
10794
|
-
const file =
|
|
10804
|
+
const file = import_node_process12.default.env.ODLA_CLI_RECORD;
|
|
10795
10805
|
if (!file) return;
|
|
10796
10806
|
try {
|
|
10797
10807
|
const entry = {
|
|
@@ -11463,9 +11473,9 @@ var import_node_child_process8 = require("child_process");
|
|
|
11463
11473
|
var import_node_fs20 = require("fs");
|
|
11464
11474
|
var import_node_os5 = require("os");
|
|
11465
11475
|
var import_node_path18 = require("path");
|
|
11466
|
-
var
|
|
11476
|
+
var import_node_process13 = __toESM(require("process"), 1);
|
|
11467
11477
|
var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
|
|
11468
|
-
function resolveEditor(env =
|
|
11478
|
+
function resolveEditor(env = import_node_process13.default.env) {
|
|
11469
11479
|
for (const name of EDITOR_ENV) {
|
|
11470
11480
|
const value2 = env[name];
|
|
11471
11481
|
if (value2 && value2.trim()) return value2.trim();
|
|
@@ -11479,8 +11489,8 @@ function defaultRun(command, path) {
|
|
|
11479
11489
|
return result.status ?? 0;
|
|
11480
11490
|
}
|
|
11481
11491
|
function editText(initial, slug, deps = {}) {
|
|
11482
|
-
const env = deps.env ??
|
|
11483
|
-
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));
|
|
11484
11494
|
const editor = resolveEditor(env);
|
|
11485
11495
|
if (!editor)
|
|
11486
11496
|
throw new Error(
|