@odla-ai/cli 0.30.0 → 0.30.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/dist/bin.js CHANGED
@@ -174,7 +174,7 @@ function absoluteEntryPath(entryPath) {
174
174
 
175
175
  // src/bin.ts
176
176
  var argv = process.argv.slice(2);
177
- requireCurrentCliForProvision(argv).then(() => requireCoherentProvisionRuntime(argv)).then(async () => (await import("./cli-CKBUVTJM.js")).runCli()).catch((err) => {
177
+ requireCurrentCliForProvision(argv).then(() => requireCoherentProvisionRuntime(argv)).then(async () => (await import("./cli-SZUWA6EF.js")).runCli()).catch((err) => {
178
178
  console.error(redactSecrets(`odla-ai: ${err instanceof Error ? err.message : String(err)}`));
179
179
  process.exitCode = exitCodeFor(err);
180
180
  });
@@ -13,7 +13,7 @@ import {
13
13
  import process8 from "process";
14
14
 
15
15
  // src/token.ts
16
- import { collectToken, OdlaError, requestToken } from "@odla-ai/db";
16
+ import { OdlaError, requestToken } from "@odla-ai/db";
17
17
  import { createHash } from "crypto";
18
18
  import process5 from "process";
19
19
 
@@ -117,12 +117,40 @@ function handshakeUrl(platformUrl, userCode) {
117
117
 
118
118
  // src/handshake-state.ts
119
119
  import { rmSync } from "fs";
120
- import { dirname as dirname2, join } from "path";
120
+ import { dirname, join } from "path";
121
121
  import process4 from "process";
122
+ function handshakeFile(cfg) {
123
+ return join(dirname(cfg.local.tokenFile), "handshake.local.json");
124
+ }
125
+ function clearPendingHandshake(path) {
126
+ rmSync(path, { force: true });
127
+ }
128
+ function minutesLeft(expiresAt) {
129
+ return Math.max(1, Math.round((expiresAt - Date.now()) / 6e4));
130
+ }
131
+ function approvalHint(pending) {
132
+ return `approve code ${pending.userCode} at ${pending.approvalUrl} (${minutesLeft(pending.expiresAt)}m left)`;
133
+ }
134
+ function approvalReminder(out, pending, periodMs = 3e4) {
135
+ const timer = setInterval(() => {
136
+ for (const line of reminderLines({
137
+ userCode: pending.userCode,
138
+ approvalUrl: pending.approvalUrl,
139
+ minutesLeft: minutesLeft(pending.expiresAt)
140
+ }))
141
+ out.log(line);
142
+ }, periodMs);
143
+ timer.unref?.();
144
+ return () => clearInterval(timer);
145
+ }
146
+ function handshakeWaitMs(waitSeconds, interactive = process4.stdout.isTTY === true) {
147
+ if (waitSeconds !== void 0) return waitSeconds * 1e3;
148
+ return interactive ? void 0 : 9e4;
149
+ }
122
150
 
123
151
  // src/local.ts
124
152
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
125
- import { dirname, isAbsolute, relative, resolve } from "path";
153
+ import { dirname as dirname2, isAbsolute, relative, resolve } from "path";
126
154
  var GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
127
155
  function readJsonFile(path) {
128
156
  try {
@@ -190,7 +218,7 @@ function o11yDevVars(cfg) {
190
218
  function resolveWriteDevVarsTarget(cfg, requested) {
191
219
  if (!requested) return null;
192
220
  if (requested === true) return cfg.local.devVarsFile;
193
- return resolve(dirname(cfg.configPath), requested);
221
+ return resolve(dirname2(cfg.configPath), requested);
194
222
  }
195
223
  function writeDevVars(path, credentials, env, o11y) {
196
224
  const entry = credentials.envs[env];
@@ -230,7 +258,7 @@ function isManagedDevVar(line) {
230
258
  return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
231
259
  }
232
260
  function writePrivateText(path, text2) {
233
- mkdirSync(dirname(path), { recursive: true });
261
+ mkdirSync(dirname2(path), { recursive: true });
234
262
  const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
235
263
  writeFileSync(temporary, text2, { mode: 384 });
236
264
  chmodSync(temporary, 384);
@@ -246,58 +274,6 @@ function displayPath(path, rootDir = process.cwd()) {
246
274
  return rel && !rel.startsWith("..") ? rel : path;
247
275
  }
248
276
 
249
- // src/handshake-state.ts
250
- function handshakeFile(cfg) {
251
- return join(dirname2(cfg.local.tokenFile), "handshake.local.json");
252
- }
253
- var RESUME_MARGIN_MS = 5e3;
254
- function readPendingHandshake(path, platform, email, requiredGrant) {
255
- const pending = readJsonFile(path);
256
- if (!pending || pending.platform !== platform || pending.email !== email) return null;
257
- if (typeof pending.userCode !== "string" || typeof pending.deviceCode !== "string") return null;
258
- if (typeof pending.expiresAt !== "number" || pending.expiresAt <= Date.now() + RESUME_MARGIN_MS) return null;
259
- if (requiredGrant && !grantCovers(pending, requiredGrant)) return null;
260
- return {
261
- interval: typeof pending.interval === "number" ? pending.interval : 3,
262
- ...pending,
263
- approvalUrl: handshakeUrl(platform, pending.userCode)
264
- };
265
- }
266
- function grantCovers(stored, required) {
267
- if (required.optionalProjectCapabilities.length === 0) return true;
268
- return required.projectIds.every((id) => stored.projectIds?.includes(id)) && required.optionalProjectCapabilities.every(
269
- (capability) => stored.optionalProjectCapabilities?.includes(capability)
270
- );
271
- }
272
- function writePendingHandshake(path, pending) {
273
- writePrivateJson(path, pending);
274
- }
275
- function clearPendingHandshake(path) {
276
- rmSync(path, { force: true });
277
- }
278
- function minutesLeft(expiresAt) {
279
- return Math.max(1, Math.round((expiresAt - Date.now()) / 6e4));
280
- }
281
- function approvalHint(pending) {
282
- return `approve code ${pending.userCode} at ${pending.approvalUrl} (${minutesLeft(pending.expiresAt)}m left)`;
283
- }
284
- function approvalReminder(out, pending, periodMs = 3e4) {
285
- const timer = setInterval(() => {
286
- for (const line of reminderLines({
287
- userCode: pending.userCode,
288
- approvalUrl: pending.approvalUrl,
289
- minutesLeft: minutesLeft(pending.expiresAt)
290
- }))
291
- out.log(line);
292
- }, periodMs);
293
- timer.unref?.();
294
- return () => clearInterval(timer);
295
- }
296
- function handshakeWaitMs(waitSeconds, interactive = process4.stdout.isTTY === true) {
297
- if (waitSeconds !== void 0) return waitSeconds * 1e3;
298
- return interactive ? void 0 : 9e4;
299
- }
300
-
301
277
  // src/token.ts
302
278
  async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {}) {
303
279
  const audience = platformAudience(cfg.platformUrl);
@@ -336,8 +312,8 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
336
312
  grantIntent
337
313
  };
338
314
  const waitMs = handshakeWaitMs(options.wait);
339
- if (grantRequest.freshLogin) clearPendingHandshake(ctx.pendingFile);
340
- const { token, expiresAt } = await resumePendingHandshake(ctx, waitMs) ?? await freshHandshake(ctx, waitMs);
315
+ clearPendingHandshake(ctx.pendingFile);
316
+ const { token, expiresAt } = await freshHandshake(ctx, waitMs);
341
317
  clearPendingHandshake(ctx.pendingFile);
342
318
  writePrivateJson(cfg.local.tokenFile, {
343
319
  platform: audience,
@@ -350,47 +326,6 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
350
326
  out.error(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
351
327
  return token;
352
328
  }
353
- async function resumePendingHandshake(ctx, waitMs) {
354
- const pending = readPendingHandshake(
355
- ctx.pendingFile,
356
- ctx.audience,
357
- ctx.email,
358
- ctx.grantIntent
359
- );
360
- if (!pending) return null;
361
- ctx.out.error("");
362
- ctx.out.error(`auth: resuming pending handshake \u2014 ${approvalHint(pending)}`);
363
- await presentHandshakeApproval(ctx.out, {
364
- userCode: pending.userCode,
365
- approvalUrl: pending.approvalUrl,
366
- minutesLeft: Math.max(1, Math.floor((pending.expiresAt - Date.now()) / 6e4)),
367
- purpose: `sign this terminal in as ${ctx.email}`
368
- }, ctx.options);
369
- ctx.out.error("");
370
- const stopReminder = approvalReminder(ctx.out, pending);
371
- try {
372
- return await collectToken({
373
- endpoint: ctx.cfg.platformUrl,
374
- deviceCode: pending.deviceCode,
375
- expiresAt: pending.expiresAt,
376
- interval: pending.interval,
377
- waitMs,
378
- fetch: ctx.doFetch
379
- });
380
- } catch (err) {
381
- const code = err instanceof OdlaError ? err.code : void 0;
382
- if (code === "handshake_pending") throw stillPending(pending, ctx.email);
383
- if (code === "handshake_expired" || code === "handshake_timeout") {
384
- clearPendingHandshake(ctx.pendingFile);
385
- ctx.out.error("auth: pending handshake lapsed unapproved; starting a fresh one");
386
- return null;
387
- }
388
- if (code === "handshake_denied") clearPendingHandshake(ctx.pendingFile);
389
- throw err;
390
- } finally {
391
- stopReminder();
392
- }
393
- }
394
329
  async function freshHandshake(ctx, waitMs) {
395
330
  let started;
396
331
  let stopReminder;
@@ -418,7 +353,6 @@ async function freshHandshake(ctx, waitMs) {
418
353
  projectIds: ctx.grantIntent.projectIds,
419
354
  optionalProjectCapabilities: ctx.grantIntent.optionalProjectCapabilities
420
355
  };
421
- writePendingHandshake(ctx.pendingFile, started);
422
356
  await presentHandshakeApproval(ctx.out, {
423
357
  userCode,
424
358
  approvalUrl,
@@ -454,7 +388,7 @@ function projectAgentHandle(appId) {
454
388
  function stillPending(pending, email) {
455
389
  return new OdlaError(
456
390
  "handshake_pending",
457
- `handshake still pending \u2014 ask ${email} to ${approvalHint(pending)}, then re-run this command; it resumes the same handshake and collects the token`,
391
+ `handshake still pending \u2014 ask ${email} to ${approvalHint(pending)}; if this command exits, re-run it to request a new code`,
458
392
  { retryable: true }
459
393
  );
460
394
  }
@@ -4781,28 +4715,31 @@ async function smoke(options) {
4781
4715
  const out = options.stdout ?? console;
4782
4716
  const cfg = await loadProjectConfig(options.configPath);
4783
4717
  const env = resolveEnv(cfg, options.env);
4784
- const credentials = readCredentials(cfg.local.credentialsFile);
4785
- if (!credentials) {
4718
+ const runtime = options.runtime === true;
4719
+ const credentials = runtime ? null : readCredentials(cfg.local.credentialsFile);
4720
+ if (!runtime && !credentials) {
4786
4721
  throw new Error(`local credentials missing: ${displayPath(cfg.local.credentialsFile, cfg.rootDir)}. Run "odla-ai provision --write-dev-vars".`);
4787
4722
  }
4788
- if (credentials.appId !== cfg.app.id) {
4723
+ if (credentials && credentials.appId !== cfg.app.id) {
4789
4724
  throw new Error(`local credentials are for app "${credentials.appId}", but config app is "${cfg.app.id}"`);
4790
4725
  }
4791
- const entry = credentials.envs[env];
4792
- if (!entry?.tenantId) {
4726
+ const entry = credentials?.envs[env];
4727
+ if (!runtime && !entry?.tenantId) {
4793
4728
  throw new Error(`local credentials have no tenant for env "${env}". Run "odla-ai provision --write-dev-vars".`);
4794
4729
  }
4795
4730
  const hasDb = cfg.services.includes("db");
4796
4731
  const hasO11y = cfg.services.includes("o11y");
4797
- if (hasDb && !entry.dbKey) {
4732
+ if (!runtime && hasDb && !entry?.dbKey) {
4798
4733
  throw new Error(`local credentials have no db key for env "${env}". Run "odla-ai provision --write-dev-vars".`);
4799
4734
  }
4800
- if (hasO11y && !entry.o11yToken) {
4735
+ if (!runtime && hasO11y && !entry?.o11yToken) {
4801
4736
  throw new Error(`local credentials have no o11y token for env "${env}". Run "odla-ai provision --write-dev-vars".`);
4802
4737
  }
4803
4738
  const doFetch = options.fetch ?? fetch;
4739
+ const tenantId = entry?.tenantId ?? resolveTenant(cfg, env).tenant;
4804
4740
  out.log(`smoke: ${cfg.app.id}/${env}`);
4805
- out.log(` tenant: ${entry.tenantId}`);
4741
+ out.log(` mode: ${runtime ? "runtime (Worker-held credentials)" : "local credentials"}`);
4742
+ out.log(` tenant: ${tenantId}`);
4806
4743
  const publicConfig = await getJson(doFetch, publicConfigUrl(cfg.platformUrl, cfg.app.id, env), void 0);
4807
4744
  out.log(` public-config: ok`);
4808
4745
  if (cfg.services.includes("ai") && cfg.ai?.provider) {
@@ -4816,7 +4753,7 @@ async function smoke(options) {
4816
4753
  if (mode !== "hosted") throw new Error(`ai mode mismatch: expected "hosted", public-config has "${String(mode ?? "none")}"`);
4817
4754
  out.log(" ai: hosted");
4818
4755
  }
4819
- if (hasO11y) out.log(` o11y: credentials present`);
4756
+ if (hasO11y) out.log(runtime ? ` o11y: Worker-held credential` : ` o11y: credentials present`);
4820
4757
  if (cfg.services.includes("calendar")) {
4821
4758
  const token = await getDeveloperToken(
4822
4759
  cfg,
@@ -4835,10 +4772,10 @@ async function smoke(options) {
4835
4772
  out.log(` calendar: ${status.status}, bookable (booking \u2192 ${status.bookingCalendarId ?? "primary"})`);
4836
4773
  }
4837
4774
  const database = await resolveDatabaseConfig(cfg);
4838
- if (hasDb) {
4775
+ if (hasDb && !runtime) {
4839
4776
  const expectedSchema = database.schema;
4840
4777
  const expectedEntities = serializedEntities(expectedSchema);
4841
- const liveSchemaPayload = await getJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/schema`, entry.dbKey);
4778
+ const liveSchemaPayload = await getJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, entry.dbKey);
4842
4779
  const liveSchema = liveSchemaPayload.schema ?? liveSchemaPayload;
4843
4780
  const liveEntities = serializedEntities(liveSchema);
4844
4781
  if (expectedEntities.length) {
@@ -4848,7 +4785,7 @@ async function smoke(options) {
4848
4785
  out.log(` schema: ${liveEntities.length} entities`);
4849
4786
  const aggregateEntity = expectedEntities[0] ?? liveEntities[0];
4850
4787
  if (aggregateEntity) {
4851
- const aggregate = await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(entry.tenantId)}/aggregate`, entry.dbKey, {
4788
+ const aggregate = await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/aggregate`, entry.dbKey, {
4852
4789
  ns: aggregateEntity,
4853
4790
  aggregate: { count: true }
4854
4791
  });
@@ -4857,9 +4794,20 @@ async function smoke(options) {
4857
4794
  } else {
4858
4795
  out.log(` aggregate: skipped (schema has no entities)`);
4859
4796
  }
4797
+ } else if (hasDb) {
4798
+ out.log(` db: Worker-held credential (direct schema/aggregate skipped)`);
4860
4799
  } else {
4861
4800
  out.log(` db: skipped (not enabled)`);
4862
4801
  }
4802
+ if (runtime) {
4803
+ const link = cfg.links?.[env];
4804
+ if (!link) throw new Error(`runtime smoke requires links.${env} in odla.config.mjs`);
4805
+ const res = await doFetch(link, { redirect: "manual" });
4806
+ if (res.status < 200 || res.status >= 500) {
4807
+ throw new Error(`runtime target ${new URL(link).origin} returned ${res.status}`);
4808
+ }
4809
+ out.log(` runtime target: ${res.status}`);
4810
+ }
4863
4811
  const probes = database.integrations.flatMap(
4864
4812
  (integration) => (integration.probes ?? []).map((probe) => ({ integration: integration.id, probe }))
4865
4813
  );
@@ -5062,10 +5010,11 @@ async function projectCommand(command, parsed, deps) {
5062
5010
  return true;
5063
5011
  }
5064
5012
  if (command === "smoke") {
5065
- assertArgs(parsed, ["config", "env", "token", "email", "open"], 1);
5013
+ assertArgs(parsed, ["config", "env", "runtime", "token", "email", "open"], 1);
5066
5014
  await smoke({
5067
5015
  configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
5068
5016
  env: stringOpt(parsed.options.env),
5017
+ runtime: parsed.options.runtime === true,
5069
5018
  token: stringOpt(parsed.options.token),
5070
5019
  email: stringOpt(parsed.options.email),
5071
5020
  open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
@@ -8908,7 +8857,7 @@ Usage:
8908
8857
  odla-ai provision [--live] [--config odla.config.mjs] [--email <odla-account>] [--request-grant] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
8909
8858
  odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
8910
8859
  odla-ai credentials revoke <receipt-id> [--config odla.config.mjs] [--json]
8911
- odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
8860
+ odla-ai smoke [--config odla.config.mjs] [--env dev] [--runtime] [--email <odla-account>] [--no-open]
8912
8861
  odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
8913
8862
  odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
8914
8863
  odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
@@ -9010,7 +8959,7 @@ Commands:
9010
8959
  "provision --live --yes" initializes only the live instance of
9011
8960
  an existing sandbox app and enables every configured service;
9012
8961
  no edit to the dev-first envs list is required.
9013
- smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
8962
+ smoke Verify local service access, or use --runtime for a Worker-held credential deployment.
9014
8963
  skill Same installer; --agent accepts all, claude, codex, cursor,
9015
8964
  copilot, gemini, or agents (repeatable or comma-separated).
9016
8965
  secrets Push configured db/o11y secrets into the Worker via wrangler
@@ -9050,12 +8999,11 @@ Safety:
9050
8999
  attempts to open it \u2014 including in CI, SSH, display-less, and agent-driven
9051
9000
  shells. Only --no-open suppresses the attempt. Browser launch is best-effort:
9052
9001
  agents with browser control must open that exact URL immediately; otherwise
9053
- they must give it to the human verbatim. A started handshake is persisted
9054
- under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
9055
- the same code. Outside an interactive terminal the wait is capped (90s by
9002
+ they must give it to the human verbatim. A device code remains only in the
9003
+ running process. Outside an interactive terminal the wait is capped (90s by
9056
9004
  default, --wait <seconds> to change); a still-pending handshake then exits
9057
- with code 75: open the same URL (or relay it if browser control is unavailable),
9058
- wait for approval, and re-run to collect.
9005
+ with code 75. Rerunning always requests and opens a new code; older clients'
9006
+ persisted pending state is discarded.
9059
9007
  A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
9060
9008
  The email is a non-secret identity hint: never provide a password or session
9061
9009
  token. It is the email shown by the signed-in odla account \u2014 never infer it
@@ -13246,4 +13194,4 @@ export {
13246
13194
  isTerminalHostedSecurityStatus,
13247
13195
  runCli
13248
13196
  };
13249
- //# sourceMappingURL=chunk-PPSXHZJI.js.map
13197
+ //# sourceMappingURL=chunk-7V7WVTDT.js.map