@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/dist/index.cjs CHANGED
@@ -96,18 +96,22 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
96
96
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
97
97
 
98
98
  // src/admin-ai.ts
99
- var import_node_process6 = __toESM(require("process"), 1);
99
+ var import_node_process7 = __toESM(require("process"), 1);
100
100
 
101
101
  // src/token.ts
102
102
  var import_db = require("@odla-ai/db");
103
- var import_node_process3 = __toESM(require("process"), 1);
103
+ var import_node_process4 = __toESM(require("process"), 1);
104
+
105
+ // src/handshake-approval.ts
106
+ var import_node_process2 = __toESM(require("process"), 1);
104
107
 
105
108
  // src/approval-prompt.ts
106
109
  function approvalLines(prompt) {
107
110
  const lines = [""];
108
111
  lines.push(
109
- `\u23F8 Human approval required${prompt.purpose ? ` to ${prompt.purpose}` : ""} \u2014 code ${prompt.userCode}, ${prompt.minutesLeft}m left.`
112
+ `\u23F8 AUTHORIZATION REQUIRED${prompt.purpose ? ` to ${prompt.purpose}` : ""} \u2014 code ${prompt.userCode}, ${prompt.minutesLeft}m left.`
110
113
  );
114
+ lines.push(" OPEN THIS URL IN THE APPROVER'S SIGNED-IN BROWSER NOW:");
111
115
  lines.push("");
112
116
  lines.push(` ${prompt.approvalUrl}`);
113
117
  lines.push("");
@@ -121,8 +125,9 @@ function approvalLines(prompt) {
121
125
  lines.push(` No browser was opened (${prompt.browserSkipped}).`);
122
126
  }
123
127
  lines.push("");
124
- lines.push(" AGENTS: relay the URL above to the human verbatim, now, before waiting.");
125
- lines.push(" You cannot approve it yourself \u2014 the grant is issued to you, not by you.");
128
+ lines.push(" AGENTS: use browser control to open the URL above now; do not wait silently.");
129
+ lines.push(" If browser control is unavailable, give the exact URL to the human verbatim.");
130
+ lines.push(" You cannot approve it yourself, retry it away, or start a substitute handshake.");
126
131
  lines.push("");
127
132
  return lines;
128
133
  }
@@ -136,10 +141,69 @@ function reminderLines(prompt) {
136
141
  ];
137
142
  }
138
143
 
144
+ // src/open.ts
145
+ var import_node_child_process = require("child_process");
146
+ var import_node_process = __toESM(require("process"), 1);
147
+ async function openUrl(url, options = {}) {
148
+ const command = openerFor(options.platform ?? import_node_process.default.platform);
149
+ const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
150
+ await new Promise((resolve13, reject) => {
151
+ const child = doSpawn(command.cmd, [...command.args, url], {
152
+ stdio: "ignore",
153
+ detached: true
154
+ });
155
+ child.once("error", reject);
156
+ child.once("spawn", () => {
157
+ child.unref();
158
+ resolve13();
159
+ });
160
+ });
161
+ }
162
+ function openerFor(platform) {
163
+ if (platform === "darwin") return { cmd: "open", args: [] };
164
+ if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
165
+ return { cmd: "xdg-open", args: [] };
166
+ }
167
+
168
+ // src/handshake-approval.ts
169
+ function approvalBrowser(options, host = {}) {
170
+ if (options.open === false) return { open: false, reason: "disabled by --no-open" };
171
+ if (options.open === true) return { open: true, mode: "forced" };
172
+ const env = host.env ?? import_node_process2.default.env;
173
+ if (env.VITEST || env.NODE_ENV === "test") {
174
+ return { open: false, reason: "test environment" };
175
+ }
176
+ return { open: true, mode: "auto" };
177
+ }
178
+ async function presentHandshakeApproval(out, prompt, options) {
179
+ const browser = approvalBrowser(options);
180
+ printApproval(out, {
181
+ ...prompt,
182
+ browserAttempted: browser.open,
183
+ browserSkipped: browser.reason
184
+ });
185
+ if (!browser.open) {
186
+ out.error(`auth: browser launch skipped (${browser.reason}) \u2014 open the URL above manually`);
187
+ return;
188
+ }
189
+ try {
190
+ await (options.openApprovalUrl ?? openUrl)(prompt.approvalUrl);
191
+ out.error("auth: browser open requested for the approval URL above");
192
+ } catch (error) {
193
+ const reason = error instanceof Error ? error.message : String(error);
194
+ out.error(`auth: browser did not open (${reason}) \u2014 OPEN THE URL ABOVE NOW`);
195
+ }
196
+ }
197
+ function handshakeUrl(platformUrl, userCode) {
198
+ const url = new URL("/studio", platformUrl);
199
+ url.searchParams.set("code", userCode);
200
+ return url.toString();
201
+ }
202
+
139
203
  // src/handshake-state.ts
140
204
  var import_node_fs2 = require("fs");
141
205
  var import_node_path2 = require("path");
142
- var import_node_process = __toESM(require("process"), 1);
206
+ var import_node_process3 = __toESM(require("process"), 1);
143
207
 
144
208
  // src/local.ts
145
209
  var import_node_fs = require("fs");
@@ -275,9 +339,13 @@ var RESUME_MARGIN_MS = 5e3;
275
339
  function readPendingHandshake(path, platform, email) {
276
340
  const pending = readJsonFile(path);
277
341
  if (!pending || pending.platform !== platform || pending.email !== email) return null;
278
- if (typeof pending.userCode !== "string" || typeof pending.deviceCode !== "string" || typeof pending.approvalUrl !== "string") return null;
342
+ if (typeof pending.userCode !== "string" || typeof pending.deviceCode !== "string") return null;
279
343
  if (typeof pending.expiresAt !== "number" || pending.expiresAt <= Date.now() + RESUME_MARGIN_MS) return null;
280
- return { interval: 3, ...pending };
344
+ return {
345
+ interval: typeof pending.interval === "number" ? pending.interval : 3,
346
+ ...pending,
347
+ approvalUrl: handshakeUrl(platform, pending.userCode)
348
+ };
281
349
  }
282
350
  function writePendingHandshake(path, pending) {
283
351
  writePrivateJson(path, pending);
@@ -303,47 +371,23 @@ function approvalReminder(out, pending, periodMs = 3e4) {
303
371
  timer.unref?.();
304
372
  return () => clearInterval(timer);
305
373
  }
306
- function handshakeWaitMs(waitSeconds, interactive = import_node_process.default.stdout.isTTY === true) {
374
+ function handshakeWaitMs(waitSeconds, interactive = import_node_process3.default.stdout.isTTY === true) {
307
375
  if (waitSeconds !== void 0) return waitSeconds * 1e3;
308
376
  return interactive ? void 0 : 9e4;
309
377
  }
310
378
 
311
- // src/open.ts
312
- var import_node_child_process = require("child_process");
313
- var import_node_process2 = __toESM(require("process"), 1);
314
- async function openUrl(url, options = {}) {
315
- const command = openerFor(options.platform ?? import_node_process2.default.platform);
316
- const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
317
- await new Promise((resolve13, reject) => {
318
- const child = doSpawn(command.cmd, [...command.args, url], {
319
- stdio: "ignore",
320
- detached: true
321
- });
322
- child.once("error", reject);
323
- child.once("spawn", () => {
324
- child.unref();
325
- resolve13();
326
- });
327
- });
328
- }
329
- function openerFor(platform) {
330
- if (platform === "darwin") return { cmd: "open", args: [] };
331
- if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
332
- return { cmd: "xdg-open", args: [] };
333
- }
334
-
335
379
  // src/token.ts
336
380
  async function getDeveloperToken(cfg, options, doFetch, out) {
337
381
  if (options.token) return options.token;
338
382
  const audience = platformAudience(cfg.platformUrl);
339
- if (import_node_process3.default.env.ODLA_DEV_TOKEN) {
340
- const declared = import_node_process3.default.env.ODLA_DEV_TOKEN_AUDIENCE;
383
+ if (import_node_process4.default.env.ODLA_DEV_TOKEN) {
384
+ const declared = import_node_process4.default.env.ODLA_DEV_TOKEN_AUDIENCE;
341
385
  if (declared) {
342
386
  if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
343
387
  } else if (audience !== "https://odla.ai") {
344
388
  throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
345
389
  }
346
- return import_node_process3.default.env.ODLA_DEV_TOKEN;
390
+ return import_node_process4.default.env.ODLA_DEV_TOKEN;
347
391
  }
348
392
  const cached = readJsonFile(cfg.local.tokenFile);
349
393
  if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
@@ -356,7 +400,6 @@ async function getDeveloperToken(cfg, options, doFetch, out) {
356
400
  doFetch,
357
401
  out,
358
402
  audience,
359
- browser: approvalBrowser(options),
360
403
  email: handshakeEmail(options.email, cached?.platform === audience ? cached.email : void 0),
361
404
  pendingFile: handshakeFile(cfg)
362
405
  };
@@ -372,7 +415,12 @@ async function resumePendingHandshake(ctx, waitMs) {
372
415
  if (!pending) return null;
373
416
  ctx.out.error("");
374
417
  ctx.out.error(`auth: resuming pending handshake \u2014 ${approvalHint(pending)}`);
375
- await launchApproval(ctx, pending.approvalUrl);
418
+ await presentHandshakeApproval(ctx.out, {
419
+ userCode: pending.userCode,
420
+ approvalUrl: pending.approvalUrl,
421
+ minutesLeft: Math.max(1, Math.floor((pending.expiresAt - Date.now()) / 6e4)),
422
+ purpose: `sign this terminal in as ${ctx.email}`
423
+ }, ctx.options);
376
424
  ctx.out.error("");
377
425
  const stopReminder = approvalReminder(ctx.out, pending);
378
426
  try {
@@ -406,10 +454,11 @@ async function freshHandshake(ctx, waitMs) {
406
454
  endpoint: ctx.cfg.platformUrl,
407
455
  email: ctx.email,
408
456
  label: `${ctx.cfg.app.id} provisioner`,
457
+ projectIds: [ctx.cfg.app.id],
409
458
  fetch: ctx.doFetch,
410
459
  waitMs,
411
- onCode: async ({ userCode, deviceCode, expiresIn, interval, verificationUriComplete }) => {
412
- const approvalUrl = verificationUriComplete ?? handshakeUrl(ctx.cfg.platformUrl, userCode);
460
+ onCode: async ({ userCode, deviceCode, expiresIn, interval }) => {
461
+ const approvalUrl = handshakeUrl(ctx.audience, userCode);
413
462
  started = {
414
463
  platform: ctx.audience,
415
464
  email: ctx.email,
@@ -420,15 +469,12 @@ async function freshHandshake(ctx, waitMs) {
420
469
  expiresAt: Date.now() + expiresIn * 1e3
421
470
  };
422
471
  writePendingHandshake(ctx.pendingFile, started);
423
- printApproval(ctx.out, {
472
+ await presentHandshakeApproval(ctx.out, {
424
473
  userCode,
425
474
  approvalUrl,
426
475
  minutesLeft: Math.floor(expiresIn / 60),
427
- purpose: `sign this terminal in as ${ctx.email}`,
428
- browserAttempted: ctx.browser.open,
429
- browserSkipped: ctx.browser.reason
430
- });
431
- await launchApproval(ctx, approvalUrl);
476
+ purpose: `sign this terminal in as ${ctx.email}`
477
+ }, ctx.options);
432
478
  stopReminder = approvalReminder(ctx.out, started);
433
479
  }
434
480
  });
@@ -443,18 +489,6 @@ async function freshHandshake(ctx, waitMs) {
443
489
  stopReminder?.();
444
490
  }
445
491
  }
446
- async function launchApproval(ctx, approvalUrl) {
447
- if (ctx.browser.open) {
448
- try {
449
- await (ctx.options.openApprovalUrl ?? openUrl)(approvalUrl);
450
- 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`);
451
- } catch (err) {
452
- ctx.out.error(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
453
- }
454
- } else if (ctx.browser.reason) {
455
- ctx.out.error(`auth: browser launch skipped (${ctx.browser.reason}) \u2014 show the human the URL above`);
456
- }
457
- }
458
492
  function stillPending(pending, email) {
459
493
  return new import_db.OdlaError(
460
494
  "handshake_pending",
@@ -463,30 +497,12 @@ function stillPending(pending, email) {
463
497
  );
464
498
  }
465
499
  function handshakeEmail(value2, cached) {
466
- const email = (value2 ?? import_node_process3.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
500
+ const email = (value2 ?? import_node_process4.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
467
501
  if (email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
468
502
  throw new Error("a fresh odla handshake requires --email <account> or ODLA_USER_EMAIL");
469
503
  }
470
504
  return email;
471
505
  }
472
- function approvalBrowser(options, host = {}) {
473
- if (options.open === true) return { open: true, mode: "forced" };
474
- if (options.open === false) return { open: false, reason: "disabled by --no-open" };
475
- const env = host.env ?? import_node_process3.default.env;
476
- if (env.VITEST || env.NODE_ENV === "test") return { open: false, reason: "test environment" };
477
- if (env.CI) return { open: false, reason: "CI environment" };
478
- if (env.SSH_CONNECTION || env.SSH_TTY) return { open: false, reason: "SSH session; pass --open to force" };
479
- const platform = host.platform ?? import_node_process3.default.platform;
480
- if (platform !== "darwin" && platform !== "win32" && !env.DISPLAY && !env.WAYLAND_DISPLAY) {
481
- return { open: false, reason: "no graphical display; pass --open to force" };
482
- }
483
- return { open: true, mode: "auto" };
484
- }
485
- function handshakeUrl(platformUrl, userCode) {
486
- const url = new URL("/studio", platformUrl);
487
- url.searchParams.set("code", userCode);
488
- return url.toString();
489
- }
490
506
  function platformAudience(value2) {
491
507
  let url;
492
508
  try {
@@ -505,12 +521,12 @@ function platformAudience(value2) {
505
521
  }
506
522
 
507
523
  // src/secret-input.ts
508
- var import_node_process4 = __toESM(require("process"), 1);
524
+ var import_node_process5 = __toESM(require("process"), 1);
509
525
  var MAX_BYTES = 64 * 1024;
510
526
  async function secretInputValue(options, kind = "credential") {
511
527
  if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
512
528
  let value2;
513
- if (options.fromEnv) value2 = import_node_process4.default.env[options.fromEnv];
529
+ if (options.fromEnv) value2 = import_node_process5.default.env[options.fromEnv];
514
530
  else if (options.stdin) value2 = await (options.readStdin ?? (() => readSecretStream(kind)))();
515
531
  else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
516
532
  value2 = value2?.replace(/[\r\n]+$/, "");
@@ -518,7 +534,7 @@ async function secretInputValue(options, kind = "credential") {
518
534
  if (new TextEncoder().encode(value2).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
519
535
  return value2;
520
536
  }
521
- async function readSecretStream(kind, stream = import_node_process4.default.stdin) {
537
+ async function readSecretStream(kind, stream = import_node_process5.default.stdin) {
522
538
  let value2 = "";
523
539
  for await (const chunk of stream) {
524
540
  value2 += String(chunk);
@@ -530,7 +546,7 @@ async function readSecretStream(kind, stream = import_node_process4.default.stdi
530
546
  // src/admin-ai-auth.ts
531
547
  var import_node_fs3 = require("fs");
532
548
  var import_node_path3 = require("path");
533
- var import_node_process5 = __toESM(require("process"), 1);
549
+ var import_node_process6 = __toESM(require("process"), 1);
534
550
  var import_db2 = require("@odla-ai/db");
535
551
  async function getScopedPlatformToken(options) {
536
552
  return resolveAdminPlatformToken(options);
@@ -538,7 +554,7 @@ async function getScopedPlatformToken(options) {
538
554
  async function resolveAdminPlatformToken(options) {
539
555
  const audience = platformAudience(options.platform);
540
556
  if (options.token) return options.token;
541
- const fromEnv = import_node_process5.default.env.ODLA_ADMIN_TOKEN;
557
+ const fromEnv = import_node_process6.default.env.ODLA_ADMIN_TOKEN;
542
558
  if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
543
559
  return scopedToken(
544
560
  audience,
@@ -550,7 +566,7 @@ async function resolveAdminPlatformToken(options) {
550
566
  }
551
567
  function audienceBoundEnvToken(token, platform) {
552
568
  const audience = platformAudience(platform);
553
- const declared = import_node_process5.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
569
+ const declared = import_node_process6.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
554
570
  if (declared) {
555
571
  if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
556
572
  } else if (audience !== "https://odla.ai") {
@@ -575,7 +591,7 @@ var SCOPE_PURPOSE = {
575
591
  };
576
592
  async function scopedToken(platform, scope, options, doFetch, out) {
577
593
  const audience = platformAudience(platform);
578
- const rootDir = options.rootDir ?? import_node_process5.default.cwd();
594
+ const rootDir = options.rootDir ?? import_node_process6.default.cwd();
579
595
  const tokenFile = options.tokenFile ?? (0, import_node_path3.join)(rootDir, ".odla/admin-token.local.json");
580
596
  const cache = options.cache === false ? null : readJsonFile(tokenFile);
581
597
  const cached = cache?.platform === audience ? cache.tokens?.[scope] : void 0;
@@ -590,21 +606,14 @@ async function scopedToken(platform, scope, options, doFetch, out) {
590
606
  label: options.label ?? `odla CLI (${scope})`,
591
607
  scopes: [scope],
592
608
  fetch: doFetch,
593
- onCode: async ({ userCode, expiresIn, verificationUriComplete }) => {
594
- const approvalUrl = verificationUriComplete ?? handshakeUrl(audience, userCode);
595
- const browser = approvalBrowser({ open: options.open });
596
- printApproval(out, {
609
+ onCode: async ({ userCode, expiresIn }) => {
610
+ await presentHandshakeApproval(out, {
597
611
  userCode,
598
- approvalUrl,
612
+ approvalUrl: handshakeUrl(audience, userCode),
599
613
  minutesLeft: Math.floor(expiresIn / 60),
600
614
  purpose: SCOPE_PURPOSE[scope] ?? `use ${scope}`,
601
- approver: scope.startsWith("app:") ? "A signed-in app owner" : "A signed-in odla platform admin",
602
- browserAttempted: browser.open,
603
- browserSkipped: browser.reason
604
- });
605
- if (browser.open) {
606
- await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
607
- }
615
+ approver: scope.startsWith("app:") ? "A signed-in app owner" : "A signed-in odla platform admin"
616
+ }, options);
608
617
  }
609
618
  });
610
619
  if (options.cache !== false) {
@@ -787,7 +796,7 @@ function isRecord2(value2) {
787
796
 
788
797
  // src/admin-ai.ts
789
798
  async function adminAi(options) {
790
- const platform = platformAudience(options.platform ?? import_node_process6.default.env.ODLA_PLATFORM ?? "https://odla.ai");
799
+ const platform = platformAudience(options.platform ?? import_node_process7.default.env.ODLA_PLATFORM ?? "https://odla.ai");
791
800
  const doFetch = options.fetch ?? fetch;
792
801
  const out = options.stdout ?? console;
793
802
  const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
@@ -1044,7 +1053,7 @@ function addOption(options, name, value2) {
1044
1053
  // src/operator-context.ts
1045
1054
  var import_node_fs6 = require("fs");
1046
1055
  var import_node_path6 = require("path");
1047
- var import_node_process8 = __toESM(require("process"), 1);
1056
+ var import_node_process9 = __toESM(require("process"), 1);
1048
1057
 
1049
1058
  // src/config.ts
1050
1059
  var import_node_fs4 = require("fs");
@@ -1350,15 +1359,15 @@ function unique2(values) {
1350
1359
  var import_node_fs5 = require("fs");
1351
1360
  var import_node_os = require("os");
1352
1361
  var import_node_path5 = require("path");
1353
- var import_node_process7 = __toESM(require("process"), 1);
1362
+ var import_node_process8 = __toESM(require("process"), 1);
1354
1363
  function operatorProfileFile() {
1355
1364
  return (0, import_node_path5.resolve)(
1356
- clean(import_node_process7.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path5.join)((0, import_node_os.homedir)(), ".odla", "contexts.json")
1365
+ clean(import_node_process8.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path5.join)((0, import_node_os.homedir)(), ".odla", "contexts.json")
1357
1366
  );
1358
1367
  }
1359
1368
  function resolveOperatorProfile(parsed) {
1360
1369
  const fromFlag = clean(stringOpt(parsed.options.context));
1361
- const fromEnvironment = clean(import_node_process7.default.env.ODLA_CONTEXT);
1370
+ const fromEnvironment = clean(import_node_process8.default.env.ODLA_CONTEXT);
1362
1371
  const name = fromFlag ?? fromEnvironment ?? null;
1363
1372
  const file = operatorProfileFile();
1364
1373
  if (!name) {
@@ -1487,13 +1496,13 @@ async function resolveOperatorContext(parsed, options = {}) {
1487
1496
  }
1488
1497
  const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
1489
1498
  const platformFlag = clean2(stringOpt(parsed.options.platform));
1490
- const platformEnvironment = clean2(import_node_process8.default.env.ODLA_PLATFORM_URL);
1499
+ const platformEnvironment = clean2(import_node_process9.default.env.ODLA_PLATFORM_URL);
1491
1500
  const platformValue = platformAudience(
1492
1501
  platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
1493
1502
  );
1494
1503
  const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
1495
1504
  const appFlag = clean2(stringOpt(parsed.options.app));
1496
- const appEnvironment = clean2(import_node_process8.default.env.ODLA_APP_ID);
1505
+ const appEnvironment = clean2(import_node_process9.default.env.ODLA_APP_ID);
1497
1506
  const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
1498
1507
  const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
1499
1508
  if (appValue) assertOperatorName(appValue, "app");
@@ -1503,16 +1512,16 @@ async function resolveOperatorContext(parsed, options = {}) {
1503
1512
  );
1504
1513
  }
1505
1514
  const envFlag = clean2(stringOpt(parsed.options.env));
1506
- const envEnvironment = clean2(import_node_process8.default.env.ODLA_ENV);
1515
+ const envEnvironment = clean2(import_node_process9.default.env.ODLA_ENV);
1507
1516
  const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
1508
1517
  const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
1509
1518
  if (environmentValue) {
1510
1519
  assertOperatorName(environmentValue, "environment");
1511
1520
  }
1512
- const rootDir = loaded?.rootDir ?? import_node_process8.default.cwd();
1521
+ const rootDir = loaded?.rootDir ?? import_node_process9.default.cwd();
1513
1522
  const profileCredentials = operatorCredentialFiles(profile);
1514
- const tokenFile = clean2(import_node_process8.default.env.ODLA_DEV_TOKEN_FILE) ? (0, import_node_path6.resolve)(import_node_process8.default.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
1515
- const scopedTokenFile = clean2(import_node_process8.default.env.ODLA_ADMIN_TOKEN_FILE) ? (0, import_node_path6.resolve)(import_node_process8.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;
1523
+ 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;
1524
+ 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;
1516
1525
  const cfg = loaded ? {
1517
1526
  ...loaded,
1518
1527
  platformUrl: platformValue,
@@ -8564,13 +8573,13 @@ async function codeCommand(parsed, dependencies) {
8564
8573
  }
8565
8574
 
8566
8575
  // src/operator-credentials.ts
8567
- var import_node_process9 = __toESM(require("process"), 1);
8576
+ var import_node_process10 = __toESM(require("process"), 1);
8568
8577
  function developerTokenStatus(context, parsed, now = Date.now()) {
8569
8578
  const cached = readJsonFile(context.cfg.local.tokenFile);
8570
8579
  const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
8571
8580
  const source = clean3(
8572
8581
  stringOpt(parsed.options.token)
8573
- ) ? "flag" : clean3(import_node_process9.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
8582
+ ) ? "flag" : clean3(import_node_process10.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
8574
8583
  return {
8575
8584
  source,
8576
8585
  cacheFile: context.cfg.local.tokenFile,
@@ -8944,15 +8953,16 @@ Safety:
8944
8953
  the metadata file. Flags and specific ODLA_* scope variables beat a selected
8945
8954
  context, which beats project config. There is no ambient current context.
8946
8955
  "context show" reports only provenance and cache state and never authenticates.
8947
- Provision opens the approval page in your browser automatically whenever the
8948
- machine can show one, including agent-driven runs; only CI, SSH, and
8949
- display-less hosts skip it. Use --open to force or --no-open to suppress.
8950
- Browser launch is best-effort: the printed approval URL is authoritative and
8951
- agents must relay it to the human verbatim. A started handshake is persisted
8956
+ Every real CLI handshake prints one canonical /studio?code= approval URL and
8957
+ attempts to open it \u2014 including in CI, SSH, display-less, and agent-driven
8958
+ shells. Only --no-open suppresses the attempt. Browser launch is best-effort:
8959
+ agents with browser control must open that exact URL immediately; otherwise
8960
+ they must give it to the human verbatim. A started handshake is persisted
8952
8961
  under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
8953
8962
  the same code. Outside an interactive terminal the wait is capped (90s by
8954
8963
  default, --wait <seconds> to change); a still-pending handshake then exits
8955
- with code 75: relay the URL, wait for approval, and re-run to collect.
8964
+ with code 75: open the same URL (or relay it if browser control is unavailable),
8965
+ wait for approval, and re-run to collect.
8956
8966
  A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
8957
8967
  The email is a non-secret identity hint: never provide a password or session
8958
8968
  token. The matching account must already exist, be signed in, explicitly
@@ -10361,7 +10371,7 @@ async function read2(url, headers, doFetch) {
10361
10371
  // src/provision.ts
10362
10372
  var import_apps12 = require("@odla-ai/apps");
10363
10373
  var import_ai3 = require("@odla-ai/ai");
10364
- var import_node_process10 = __toESM(require("process"), 1);
10374
+ var import_node_process11 = __toESM(require("process"), 1);
10365
10375
 
10366
10376
  // src/integration-provision.ts
10367
10377
  var import_db3 = require("@odla-ai/db");
@@ -10668,7 +10678,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
10668
10678
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
10669
10679
  }
10670
10680
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
10671
- const key = import_node_process10.default.env[cfg.ai.keyEnv];
10681
+ const key = import_node_process11.default.env[cfg.ai.keyEnv];
10672
10682
  if (key) {
10673
10683
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
10674
10684
  await (0, import_ai3.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -10710,7 +10720,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
10710
10720
 
10711
10721
  // src/record.ts
10712
10722
  var import_node_fs16 = require("fs");
10713
- var import_node_process11 = __toESM(require("process"), 1);
10723
+ var import_node_process12 = __toESM(require("process"), 1);
10714
10724
 
10715
10725
  // src/surface.ts
10716
10726
  var PM_ACTIONS = {
@@ -10868,7 +10878,7 @@ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
10868
10878
 
10869
10879
  // src/record.ts
10870
10880
  function recordInvocation(parsed) {
10871
- const file = import_node_process11.default.env.ODLA_CLI_RECORD;
10881
+ const file = import_node_process12.default.env.ODLA_CLI_RECORD;
10872
10882
  if (!file) return;
10873
10883
  try {
10874
10884
  const entry = {
@@ -11540,9 +11550,9 @@ var import_node_child_process8 = require("child_process");
11540
11550
  var import_node_fs20 = require("fs");
11541
11551
  var import_node_os5 = require("os");
11542
11552
  var import_node_path18 = require("path");
11543
- var import_node_process12 = __toESM(require("process"), 1);
11553
+ var import_node_process13 = __toESM(require("process"), 1);
11544
11554
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
11545
- function resolveEditor(env = import_node_process12.default.env) {
11555
+ function resolveEditor(env = import_node_process13.default.env) {
11546
11556
  for (const name of EDITOR_ENV) {
11547
11557
  const value2 = env[name];
11548
11558
  if (value2 && value2.trim()) return value2.trim();
@@ -11556,8 +11566,8 @@ function defaultRun(command, path) {
11556
11566
  return result.status ?? 0;
11557
11567
  }
11558
11568
  function editText(initial, slug, deps = {}) {
11559
- const env = deps.env ?? import_node_process12.default.env;
11560
- const interactive = deps.interactive ?? (() => Boolean(import_node_process12.default.stdin.isTTY));
11569
+ const env = deps.env ?? import_node_process13.default.env;
11570
+ const interactive = deps.interactive ?? (() => Boolean(import_node_process13.default.stdin.isTTY));
11561
11571
  const editor = resolveEditor(env);
11562
11572
  if (!editor)
11563
11573
  throw new Error(