@odla-ai/cli 0.10.2 → 0.11.1

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 CHANGED
@@ -110,10 +110,11 @@ shown-once credential.
110
110
  `--email <account>` or `ODLA_USER_EMAIL`; the matching existing account must
111
111
  sign in, review the exact code, and approve it. Opening the URL alone does
112
112
  not claim the request.
113
- In an interactive terminal it opens the approval page in your browser when
114
- the code is displayed. Pass `--open` to force browser launch from a
115
- non-interactive shell, or `--no-open` to suppress it. Browser launch is
116
- best-effort; the printed URL and code always remain the fallback.
113
+ When the code is displayed it opens the approval page in your browser
114
+ including from scripted and agent-driven shells; only CI, SSH sessions, and
115
+ display-less hosts skip the launch. Pass `--open` to force it anyway, or
116
+ `--no-open` to suppress it. Browser launch is best-effort; the printed URL
117
+ and code always remain the fallback.
117
118
  2. Creates the platform app if needed.
118
119
  3. Enables configured services in every configured environment. Calendar
119
120
  config is normalized per env and fixed to Google read-only access.
package/dist/bin.cjs CHANGED
@@ -237,12 +237,16 @@ function handshakeEmail(value, cached) {
237
237
  }
238
238
  return email;
239
239
  }
240
- function approvalBrowser(options) {
240
+ function approvalBrowser(options, host = {}) {
241
241
  if (options.open === true) return { open: true, mode: "forced" };
242
242
  if (options.open === false) return { open: false, reason: "disabled by --no-open" };
243
- if (import_node_process2.default.env.CI) return { open: false, reason: "CI environment" };
244
- const interactive = options.interactive ?? Boolean(import_node_process2.default.stdin.isTTY && import_node_process2.default.stdout.isTTY);
245
- if (!interactive) return { open: false, reason: "non-interactive shell; pass --open to force" };
243
+ const env = host.env ?? import_node_process2.default.env;
244
+ if (env.CI) return { open: false, reason: "CI environment" };
245
+ if (env.SSH_CONNECTION || env.SSH_TTY) return { open: false, reason: "SSH session; pass --open to force" };
246
+ const platform = host.platform ?? import_node_process2.default.platform;
247
+ if (platform !== "darwin" && platform !== "win32" && !env.DISPLAY && !env.WAYLAND_DISPLAY) {
248
+ return { open: false, reason: "no graphical display; pass --open to force" };
249
+ }
246
250
  return { open: true, mode: "auto" };
247
251
  }
248
252
  function handshakeUrl(platformUrl, userCode) {
@@ -339,8 +343,9 @@ async function scopedToken(platform, scope, options, doFetch, out) {
339
343
  onCode: async ({ userCode, expiresIn, verificationUriComplete }) => {
340
344
  const approvalUrl = verificationUriComplete ?? handshakeUrl(audience, userCode);
341
345
  out.log(`Review, then approve scoped request ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
342
- const shouldOpen = options.open === true || options.open !== false && !import_node_process4.default.env.CI && Boolean(import_node_process4.default.stdin.isTTY && import_node_process4.default.stdout.isTTY);
343
- if (shouldOpen) await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
346
+ if (approvalBrowser({ open: options.open }).open) {
347
+ await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
348
+ }
344
349
  }
345
350
  });
346
351
  const tokens = cache?.platform === audience ? { ...cache.tokens ?? {} } : {};
@@ -1084,6 +1089,28 @@ function unique(values) {
1084
1089
  return [...new Set(values.filter(Boolean))];
1085
1090
  }
1086
1091
 
1092
+ // src/calendar-errors.ts
1093
+ var PLATFORM_NOT_READY_CODES = /* @__PURE__ */ new Set([
1094
+ "calendar_google_oauth_not_configured",
1095
+ "calendar_token_vault_not_configured",
1096
+ "calendar_db_ingress_not_configured"
1097
+ ]);
1098
+ var CalendarRequestError = class extends Error {
1099
+ /** HTTP status returned by the registry. */
1100
+ status;
1101
+ /** Machine-readable `error.code` from the response body, when present. */
1102
+ code;
1103
+ constructor(message, status, code) {
1104
+ super(message);
1105
+ this.name = "CalendarRequestError";
1106
+ this.status = status;
1107
+ if (code !== void 0) this.code = code;
1108
+ }
1109
+ };
1110
+ function isCalendarPlatformNotReady(error) {
1111
+ return error instanceof CalendarRequestError && error.code !== void 0 && PLATFORM_NOT_READY_CODES.has(error.code);
1112
+ }
1113
+
1087
1114
  // src/redact.ts
1088
1115
  var REPLACEMENTS = [
1089
1116
  [/odla_(sk|dev)_[A-Za-z0-9._-]+/g, "odla_$1_[redacted]"],
@@ -1249,7 +1276,8 @@ async function calendarJson(ctx, suffix, init) {
1249
1276
  if (!response.ok) {
1250
1277
  const code = textField(body.error?.code, 128);
1251
1278
  const message = textField(body.error?.message, 500);
1252
- throw new Error(redactSecrets(`calendar request failed (${response.status})${code ? ` ${code}` : ""}${message ? `: ${message}` : ""}`));
1279
+ const detail = `${code ? ` ${code}` : ""}${message ? `: ${message}` : ""}`;
1280
+ throw new CalendarRequestError(redactSecrets(`calendar request failed (${response.status})${detail}`), response.status, code);
1253
1281
  }
1254
1282
  return body;
1255
1283
  }
@@ -1389,7 +1417,6 @@ async function lifecycleContext(options) {
1389
1417
  token: options.token,
1390
1418
  email: options.email,
1391
1419
  open: options.open,
1392
- interactive: options.interactive,
1393
1420
  openApprovalUrl: options.openConsentUrl
1394
1421
  },
1395
1422
  doFetch,
@@ -1398,7 +1425,7 @@ async function lifecycleContext(options) {
1398
1425
  return { cfg, ctx: { platform: cfg.platformUrl, appId: cfg.app.id, env, token, fetch: doFetch }, out };
1399
1426
  }
1400
1427
  function connectionOptions(options, out) {
1401
- const browser = approvalBrowser({ configPath: "odla.config.mjs", open: options.open, interactive: options.interactive });
1428
+ const browser = approvalBrowser({ open: options.open });
1402
1429
  return {
1403
1430
  out,
1404
1431
  open: browser.open,
@@ -1859,8 +1886,9 @@ Safety:
1859
1886
  Provision caches the approved developer token and service credentials under
1860
1887
  .odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
1861
1888
  preflights Wrangler before any shown-once issuance or destructive rotation.
1862
- Provision opens the approval page automatically in interactive terminals;
1863
- use --open to force browser launch or --no-open to suppress it.
1889
+ Provision opens the approval page in your browser automatically whenever the
1890
+ machine can show one, including agent-driven runs; only CI, SSH, and
1891
+ display-less hosts skip it. Use --open to force or --no-open to suppress.
1864
1892
  A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
1865
1893
  The email is a non-secret identity hint: never provide a password or session
1866
1894
  token. The matching account must already exist, be signed in, explicitly
@@ -2300,17 +2328,6 @@ async function provision(options) {
2300
2328
  if (cfg.services.includes("calendar")) {
2301
2329
  const calendarCtx = { platform: cfg.platformUrl, appId: cfg.app.id, env, token, fetch: doFetch };
2302
2330
  await applyCalendarBookingPage(calendarCtx, calendarBookingPageUrl(cfg, env), out);
2303
- await ensureCalendarConnected(
2304
- calendarCtx,
2305
- {
2306
- open: options.open,
2307
- interactive: options.interactive,
2308
- openConsentUrl: options.openApprovalUrl,
2309
- wait: options.calendarPollWait,
2310
- pollTimeoutMs: options.calendarPollTimeoutMs,
2311
- stdout: out
2312
- }
2313
- );
2314
2331
  }
2315
2332
  }
2316
2333
  for (const env of cfg.envs) {
@@ -2374,6 +2391,25 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
2374
2391
  writeDevVars(devVarsTarget, credentials, env, o11yDevVars(cfg));
2375
2392
  out.log(`dev vars: wrote ${displayPath(devVarsTarget, cfg.rootDir)} for ${env}`);
2376
2393
  }
2394
+ if (cfg.services.includes("calendar")) {
2395
+ for (const env of cfg.envs) {
2396
+ try {
2397
+ await ensureCalendarConnected(
2398
+ { platform: cfg.platformUrl, appId: cfg.app.id, env, token, fetch: doFetch },
2399
+ {
2400
+ open: options.open,
2401
+ openConsentUrl: options.openApprovalUrl,
2402
+ wait: options.calendarPollWait,
2403
+ pollTimeoutMs: options.calendarPollTimeoutMs,
2404
+ stdout: out
2405
+ }
2406
+ );
2407
+ } catch (error) {
2408
+ if (!isCalendarPlatformNotReady(error)) throw error;
2409
+ out.log(`${env}: calendar consent skipped (${error.code}); resume with "odla-ai calendar connect --env ${env}" once the platform is configured`);
2410
+ }
2411
+ }
2412
+ }
2377
2413
  }
2378
2414
  function serviceRank(service) {
2379
2415
  if (service === "db") return 0;
@@ -3672,7 +3708,6 @@ async function smoke(options) {
3672
3708
  token: options.token,
3673
3709
  email: options.email,
3674
3710
  open: options.open,
3675
- interactive: options.interactive,
3676
3711
  openApprovalUrl: options.openApprovalUrl
3677
3712
  },
3678
3713
  doFetch,