@odla-ai/cli 0.30.0 → 0.30.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 +24 -7
- package/dist/bin.cjs +154 -155
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-PPSXHZJI.js → chunk-3HFNKDZV.js} +120 -129
- package/dist/chunk-3HFNKDZV.js.map +1 -0
- package/dist/{cli-CKBUVTJM.js → cli-BGSX5Z4C.js} +2 -2
- package/dist/index.cjs +133 -142
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -7
- package/dist/index.d.ts +8 -7
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/skills/odla/SKILL.md +6 -4
- package/skills/odla/references/pm-work-intake.md +11 -2
- package/skills/odla/references/pm.md +35 -0
- package/dist/chunk-PPSXHZJI.js.map +0 -1
- /package/dist/{cli-CKBUVTJM.js.map → cli-BGSX5Z4C.js.map} +0 -0
package/README.md
CHANGED
|
@@ -147,7 +147,11 @@ Use `pm task ref <id>` (or the equivalent goal/decision/bug command) to print
|
|
|
147
147
|
copy-ready structured markup for Discussion. The same markup may be pasted into
|
|
148
148
|
`pm <entity> comment <id> --body "…"`; PM stores it as a structured reference,
|
|
149
149
|
and `pm <entity> comments <id>` prints copy-ready markup instead of flattening
|
|
150
|
-
the link back to a title.
|
|
150
|
+
the link back to a title. For ordinary chat and handoffs, `pm task link <id>`
|
|
151
|
+
prints a normal Markdown link to the record's durable Studio route. Successful
|
|
152
|
+
create and lifecycle commands print that title-first link directly. Lead with
|
|
153
|
+
the PM type and linked title, explain the state or next action, and keep the id
|
|
154
|
+
hidden in the URL; a bare id list is not a useful handoff.
|
|
151
155
|
|
|
152
156
|
When diagnosing a deployed app, agents can request one parseable observability
|
|
153
157
|
snapshot instead of scraping Studio or composing collector routes themselves:
|
|
@@ -511,12 +515,12 @@ credential set.
|
|
|
511
515
|
and attempts to open it — including from CI, SSH, display-less, scripted,
|
|
512
516
|
and agent-driven shells. Only `--no-open` suppresses the attempt. Browser
|
|
513
517
|
launch is best-effort: an agent with browser control must open that exact URL
|
|
514
|
-
immediately; otherwise it gives the URL to the human verbatim. The
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
518
|
+
immediately; otherwise it gives the URL to the human verbatim. The device
|
|
519
|
+
code stays only in the running process. Outside an interactive terminal the
|
|
520
|
+
approval wait is capped (90 seconds by default; `--wait <seconds>`
|
|
521
|
+
overrides), and a still-pending handshake exits with code 75. Rerunning
|
|
522
|
+
always requests and opens a new code; it never resumes an older request from
|
|
523
|
+
`.odla/`.
|
|
520
524
|
2. Creates the platform app if needed. For a new id, this consumes the exact-id
|
|
521
525
|
reservation approved in step 1; it is not ambient project-creation authority.
|
|
522
526
|
3. Enables configured services in every configured environment. Calendar
|
|
@@ -569,6 +573,19 @@ The inventory contains only receipt, target, state, and timestamp metadata.
|
|
|
569
573
|
Revocation uses the receipt's exact DB and o11y credential ids; no value is
|
|
570
574
|
retrieved or rotated.
|
|
571
575
|
|
|
576
|
+
Verify a deployment that intentionally keeps db/o11y credentials only on the
|
|
577
|
+
Worker:
|
|
578
|
+
|
|
579
|
+
```sh
|
|
580
|
+
npx @odla-ai/cli smoke --env dev --runtime
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
Runtime smoke never reads `.odla/credentials.local.json`. It checks Registry
|
|
584
|
+
public config, the configured Worker link, anonymous integration probes, and
|
|
585
|
+
owner-visible calendar health. Direct schema and aggregate reads remain part
|
|
586
|
+
of ordinary local-credential smoke because a runtime credential is deliberately
|
|
587
|
+
not retrievable from the Worker.
|
|
588
|
+
|
|
572
589
|
### Application capability integrations
|
|
573
590
|
|
|
574
591
|
An integration is an npm capability composed into an app, not a hosted
|
package/dist/bin.cjs
CHANGED
|
@@ -305,10 +305,51 @@ var init_handshake_approval = __esm({
|
|
|
305
305
|
}
|
|
306
306
|
});
|
|
307
307
|
|
|
308
|
+
// src/handshake-state.ts
|
|
309
|
+
function handshakeFile(cfg) {
|
|
310
|
+
return (0, import_node_path2.join)((0, import_node_path2.dirname)(cfg.local.tokenFile), "handshake.local.json");
|
|
311
|
+
}
|
|
312
|
+
function clearPendingHandshake(path) {
|
|
313
|
+
(0, import_node_fs4.rmSync)(path, { force: true });
|
|
314
|
+
}
|
|
315
|
+
function minutesLeft(expiresAt) {
|
|
316
|
+
return Math.max(1, Math.round((expiresAt - Date.now()) / 6e4));
|
|
317
|
+
}
|
|
318
|
+
function approvalHint(pending) {
|
|
319
|
+
return `approve code ${pending.userCode} at ${pending.approvalUrl} (${minutesLeft(pending.expiresAt)}m left)`;
|
|
320
|
+
}
|
|
321
|
+
function approvalReminder(out, pending, periodMs = 3e4) {
|
|
322
|
+
const timer = setInterval(() => {
|
|
323
|
+
for (const line of reminderLines({
|
|
324
|
+
userCode: pending.userCode,
|
|
325
|
+
approvalUrl: pending.approvalUrl,
|
|
326
|
+
minutesLeft: minutesLeft(pending.expiresAt)
|
|
327
|
+
}))
|
|
328
|
+
out.log(line);
|
|
329
|
+
}, periodMs);
|
|
330
|
+
timer.unref?.();
|
|
331
|
+
return () => clearInterval(timer);
|
|
332
|
+
}
|
|
333
|
+
function handshakeWaitMs(waitSeconds, interactive = import_node_process3.default.stdout.isTTY === true) {
|
|
334
|
+
if (waitSeconds !== void 0) return waitSeconds * 1e3;
|
|
335
|
+
return interactive ? void 0 : 9e4;
|
|
336
|
+
}
|
|
337
|
+
var import_node_fs4, import_node_path2, import_node_process3;
|
|
338
|
+
var init_handshake_state = __esm({
|
|
339
|
+
"src/handshake-state.ts"() {
|
|
340
|
+
"use strict";
|
|
341
|
+
init_cjs_shims();
|
|
342
|
+
import_node_fs4 = require("fs");
|
|
343
|
+
import_node_path2 = require("path");
|
|
344
|
+
import_node_process3 = __toESM(require("process"), 1);
|
|
345
|
+
init_approval_prompt();
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
308
349
|
// src/local.ts
|
|
309
350
|
function readJsonFile(path) {
|
|
310
351
|
try {
|
|
311
|
-
return JSON.parse((0,
|
|
352
|
+
return JSON.parse((0, import_node_fs5.readFileSync)(path, "utf8"));
|
|
312
353
|
} catch {
|
|
313
354
|
return null;
|
|
314
355
|
}
|
|
@@ -318,10 +359,10 @@ function writePrivateJson(path, value2) {
|
|
|
318
359
|
`);
|
|
319
360
|
}
|
|
320
361
|
function readCredentials(path) {
|
|
321
|
-
if (!(0,
|
|
362
|
+
if (!(0, import_node_fs5.existsSync)(path)) return null;
|
|
322
363
|
let value2;
|
|
323
364
|
try {
|
|
324
|
-
value2 = JSON.parse((0,
|
|
365
|
+
value2 = JSON.parse((0, import_node_fs5.readFileSync)(path, "utf8"));
|
|
325
366
|
} catch {
|
|
326
367
|
throw new Error(`credentials file ${path} is not valid JSON; fix or remove it before provisioning`);
|
|
327
368
|
}
|
|
@@ -351,14 +392,14 @@ function mergeCredential(current, update) {
|
|
|
351
392
|
return next;
|
|
352
393
|
}
|
|
353
394
|
function ensureGitignore(rootDir, localPaths = []) {
|
|
354
|
-
const path = (0,
|
|
355
|
-
const existing = (0,
|
|
395
|
+
const path = (0, import_node_path3.resolve)(rootDir, ".gitignore");
|
|
396
|
+
const existing = (0, import_node_fs5.existsSync)(path) ? (0, import_node_fs5.readFileSync)(path, "utf8") : "";
|
|
356
397
|
const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line) => !!line);
|
|
357
398
|
const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
|
|
358
399
|
const missing = wanted.filter((line) => !existing.split(/\r?\n/).includes(line));
|
|
359
400
|
if (missing.length === 0) return;
|
|
360
401
|
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
361
|
-
(0,
|
|
402
|
+
(0, import_node_fs5.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
|
|
362
403
|
`);
|
|
363
404
|
}
|
|
364
405
|
function o11yDevVars(cfg) {
|
|
@@ -372,7 +413,7 @@ function o11yDevVars(cfg) {
|
|
|
372
413
|
function resolveWriteDevVarsTarget(cfg, requested) {
|
|
373
414
|
if (!requested) return null;
|
|
374
415
|
if (requested === true) return cfg.local.devVarsFile;
|
|
375
|
-
return (0,
|
|
416
|
+
return (0, import_node_path3.resolve)((0, import_node_path3.dirname)(cfg.configPath), requested);
|
|
376
417
|
}
|
|
377
418
|
function writeDevVars(path, credentials, env, o11y) {
|
|
378
419
|
const entry = credentials.envs[env];
|
|
@@ -386,7 +427,7 @@ function writeDevVars(path, credentials, env, o11y) {
|
|
|
386
427
|
if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
|
|
387
428
|
if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
|
|
388
429
|
}
|
|
389
|
-
const existing = (0,
|
|
430
|
+
const existing = (0, import_node_fs5.existsSync)(path) ? (0, import_node_fs5.readFileSync)(path, "utf8") : "";
|
|
390
431
|
const retained = existing.split(/\r?\n/).filter((line) => !isManagedDevVar(line));
|
|
391
432
|
while (retained.at(-1) === "") retained.pop();
|
|
392
433
|
const prefix = retained.length ? `${retained.join("\n")}
|
|
@@ -400,28 +441,28 @@ function isManagedDevVar(line) {
|
|
|
400
441
|
return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
|
|
401
442
|
}
|
|
402
443
|
function writePrivateText(path, text2) {
|
|
403
|
-
(0,
|
|
444
|
+
(0, import_node_fs5.mkdirSync)((0, import_node_path3.dirname)(path), { recursive: true });
|
|
404
445
|
const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
405
|
-
(0,
|
|
406
|
-
(0,
|
|
407
|
-
(0,
|
|
446
|
+
(0, import_node_fs5.writeFileSync)(temporary, text2, { mode: 384 });
|
|
447
|
+
(0, import_node_fs5.chmodSync)(temporary, 384);
|
|
448
|
+
(0, import_node_fs5.renameSync)(temporary, path);
|
|
408
449
|
}
|
|
409
450
|
function gitignoreEntry(rootDir, path) {
|
|
410
|
-
const rel = (0,
|
|
411
|
-
if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0,
|
|
451
|
+
const rel = (0, import_node_path3.relative)((0, import_node_path3.resolve)(rootDir), (0, import_node_path3.resolve)(path));
|
|
452
|
+
if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path3.isAbsolute)(rel)) return null;
|
|
412
453
|
return rel.replaceAll("\\", "/");
|
|
413
454
|
}
|
|
414
455
|
function displayPath(path, rootDir = process.cwd()) {
|
|
415
|
-
const rel = (0,
|
|
456
|
+
const rel = (0, import_node_path3.relative)(rootDir, path);
|
|
416
457
|
return rel && !rel.startsWith("..") ? rel : path;
|
|
417
458
|
}
|
|
418
|
-
var
|
|
459
|
+
var import_node_fs5, import_node_path3, GITIGNORE_LINES, MANAGED_DEV_VARS;
|
|
419
460
|
var init_local = __esm({
|
|
420
461
|
"src/local.ts"() {
|
|
421
462
|
"use strict";
|
|
422
463
|
init_cjs_shims();
|
|
423
|
-
|
|
424
|
-
|
|
464
|
+
import_node_fs5 = require("fs");
|
|
465
|
+
import_node_path3 = require("path");
|
|
425
466
|
GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
|
|
426
467
|
MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
|
|
427
468
|
"ODLA_PLATFORM",
|
|
@@ -438,71 +479,6 @@ var init_local = __esm({
|
|
|
438
479
|
}
|
|
439
480
|
});
|
|
440
481
|
|
|
441
|
-
// src/handshake-state.ts
|
|
442
|
-
function handshakeFile(cfg) {
|
|
443
|
-
return (0, import_node_path3.join)((0, import_node_path3.dirname)(cfg.local.tokenFile), "handshake.local.json");
|
|
444
|
-
}
|
|
445
|
-
function readPendingHandshake(path, platform, email, requiredGrant) {
|
|
446
|
-
const pending = readJsonFile(path);
|
|
447
|
-
if (!pending || pending.platform !== platform || pending.email !== email) return null;
|
|
448
|
-
if (typeof pending.userCode !== "string" || typeof pending.deviceCode !== "string") return null;
|
|
449
|
-
if (typeof pending.expiresAt !== "number" || pending.expiresAt <= Date.now() + RESUME_MARGIN_MS) return null;
|
|
450
|
-
if (requiredGrant && !grantCovers(pending, requiredGrant)) return null;
|
|
451
|
-
return {
|
|
452
|
-
interval: typeof pending.interval === "number" ? pending.interval : 3,
|
|
453
|
-
...pending,
|
|
454
|
-
approvalUrl: handshakeUrl(platform, pending.userCode)
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
function grantCovers(stored, required) {
|
|
458
|
-
if (required.optionalProjectCapabilities.length === 0) return true;
|
|
459
|
-
return required.projectIds.every((id) => stored.projectIds?.includes(id)) && required.optionalProjectCapabilities.every(
|
|
460
|
-
(capability) => stored.optionalProjectCapabilities?.includes(capability)
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
function writePendingHandshake(path, pending) {
|
|
464
|
-
writePrivateJson(path, pending);
|
|
465
|
-
}
|
|
466
|
-
function clearPendingHandshake(path) {
|
|
467
|
-
(0, import_node_fs5.rmSync)(path, { force: true });
|
|
468
|
-
}
|
|
469
|
-
function minutesLeft(expiresAt) {
|
|
470
|
-
return Math.max(1, Math.round((expiresAt - Date.now()) / 6e4));
|
|
471
|
-
}
|
|
472
|
-
function approvalHint(pending) {
|
|
473
|
-
return `approve code ${pending.userCode} at ${pending.approvalUrl} (${minutesLeft(pending.expiresAt)}m left)`;
|
|
474
|
-
}
|
|
475
|
-
function approvalReminder(out, pending, periodMs = 3e4) {
|
|
476
|
-
const timer = setInterval(() => {
|
|
477
|
-
for (const line of reminderLines({
|
|
478
|
-
userCode: pending.userCode,
|
|
479
|
-
approvalUrl: pending.approvalUrl,
|
|
480
|
-
minutesLeft: minutesLeft(pending.expiresAt)
|
|
481
|
-
}))
|
|
482
|
-
out.log(line);
|
|
483
|
-
}, periodMs);
|
|
484
|
-
timer.unref?.();
|
|
485
|
-
return () => clearInterval(timer);
|
|
486
|
-
}
|
|
487
|
-
function handshakeWaitMs(waitSeconds, interactive = import_node_process3.default.stdout.isTTY === true) {
|
|
488
|
-
if (waitSeconds !== void 0) return waitSeconds * 1e3;
|
|
489
|
-
return interactive ? void 0 : 9e4;
|
|
490
|
-
}
|
|
491
|
-
var import_node_fs5, import_node_path3, import_node_process3, RESUME_MARGIN_MS;
|
|
492
|
-
var init_handshake_state = __esm({
|
|
493
|
-
"src/handshake-state.ts"() {
|
|
494
|
-
"use strict";
|
|
495
|
-
init_cjs_shims();
|
|
496
|
-
import_node_fs5 = require("fs");
|
|
497
|
-
import_node_path3 = require("path");
|
|
498
|
-
import_node_process3 = __toESM(require("process"), 1);
|
|
499
|
-
init_local();
|
|
500
|
-
init_approval_prompt();
|
|
501
|
-
init_handshake_approval();
|
|
502
|
-
RESUME_MARGIN_MS = 5e3;
|
|
503
|
-
}
|
|
504
|
-
});
|
|
505
|
-
|
|
506
482
|
// src/token.ts
|
|
507
483
|
async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {}) {
|
|
508
484
|
const audience = platformAudience(cfg.platformUrl);
|
|
@@ -541,8 +517,8 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
|
|
|
541
517
|
grantIntent
|
|
542
518
|
};
|
|
543
519
|
const waitMs = handshakeWaitMs(options.wait);
|
|
544
|
-
|
|
545
|
-
const { token, expiresAt } = await
|
|
520
|
+
clearPendingHandshake(ctx.pendingFile);
|
|
521
|
+
const { token, expiresAt } = await freshHandshake(ctx, waitMs);
|
|
546
522
|
clearPendingHandshake(ctx.pendingFile);
|
|
547
523
|
writePrivateJson(cfg.local.tokenFile, {
|
|
548
524
|
platform: audience,
|
|
@@ -555,47 +531,6 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
|
|
|
555
531
|
out.error(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
|
|
556
532
|
return token;
|
|
557
533
|
}
|
|
558
|
-
async function resumePendingHandshake(ctx, waitMs) {
|
|
559
|
-
const pending = readPendingHandshake(
|
|
560
|
-
ctx.pendingFile,
|
|
561
|
-
ctx.audience,
|
|
562
|
-
ctx.email,
|
|
563
|
-
ctx.grantIntent
|
|
564
|
-
);
|
|
565
|
-
if (!pending) return null;
|
|
566
|
-
ctx.out.error("");
|
|
567
|
-
ctx.out.error(`auth: resuming pending handshake \u2014 ${approvalHint(pending)}`);
|
|
568
|
-
await presentHandshakeApproval(ctx.out, {
|
|
569
|
-
userCode: pending.userCode,
|
|
570
|
-
approvalUrl: pending.approvalUrl,
|
|
571
|
-
minutesLeft: Math.max(1, Math.floor((pending.expiresAt - Date.now()) / 6e4)),
|
|
572
|
-
purpose: `sign this terminal in as ${ctx.email}`
|
|
573
|
-
}, ctx.options);
|
|
574
|
-
ctx.out.error("");
|
|
575
|
-
const stopReminder = approvalReminder(ctx.out, pending);
|
|
576
|
-
try {
|
|
577
|
-
return await (0, import_db.collectToken)({
|
|
578
|
-
endpoint: ctx.cfg.platformUrl,
|
|
579
|
-
deviceCode: pending.deviceCode,
|
|
580
|
-
expiresAt: pending.expiresAt,
|
|
581
|
-
interval: pending.interval,
|
|
582
|
-
waitMs,
|
|
583
|
-
fetch: ctx.doFetch
|
|
584
|
-
});
|
|
585
|
-
} catch (err) {
|
|
586
|
-
const code = err instanceof import_db.OdlaError ? err.code : void 0;
|
|
587
|
-
if (code === "handshake_pending") throw stillPending(pending, ctx.email);
|
|
588
|
-
if (code === "handshake_expired" || code === "handshake_timeout") {
|
|
589
|
-
clearPendingHandshake(ctx.pendingFile);
|
|
590
|
-
ctx.out.error("auth: pending handshake lapsed unapproved; starting a fresh one");
|
|
591
|
-
return null;
|
|
592
|
-
}
|
|
593
|
-
if (code === "handshake_denied") clearPendingHandshake(ctx.pendingFile);
|
|
594
|
-
throw err;
|
|
595
|
-
} finally {
|
|
596
|
-
stopReminder();
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
534
|
async function freshHandshake(ctx, waitMs) {
|
|
600
535
|
let started;
|
|
601
536
|
let stopReminder;
|
|
@@ -623,7 +558,6 @@ async function freshHandshake(ctx, waitMs) {
|
|
|
623
558
|
projectIds: ctx.grantIntent.projectIds,
|
|
624
559
|
optionalProjectCapabilities: ctx.grantIntent.optionalProjectCapabilities
|
|
625
560
|
};
|
|
626
|
-
writePendingHandshake(ctx.pendingFile, started);
|
|
627
561
|
await presentHandshakeApproval(ctx.out, {
|
|
628
562
|
userCode,
|
|
629
563
|
approvalUrl,
|
|
@@ -659,7 +593,7 @@ function projectAgentHandle(appId) {
|
|
|
659
593
|
function stillPending(pending, email) {
|
|
660
594
|
return new import_db.OdlaError(
|
|
661
595
|
"handshake_pending",
|
|
662
|
-
`handshake still pending \u2014 ask ${email} to ${approvalHint(pending)}
|
|
596
|
+
`handshake still pending \u2014 ask ${email} to ${approvalHint(pending)}; if this command exits, re-run it to request a new code`,
|
|
663
597
|
{ retryable: true }
|
|
664
598
|
);
|
|
665
599
|
}
|
|
@@ -5436,28 +5370,31 @@ async function smoke(options) {
|
|
|
5436
5370
|
const out = options.stdout ?? console;
|
|
5437
5371
|
const cfg = await loadProjectConfig(options.configPath);
|
|
5438
5372
|
const env = resolveEnv(cfg, options.env);
|
|
5439
|
-
const
|
|
5440
|
-
|
|
5373
|
+
const runtime = options.runtime === true;
|
|
5374
|
+
const credentials = runtime ? null : readCredentials(cfg.local.credentialsFile);
|
|
5375
|
+
if (!runtime && !credentials) {
|
|
5441
5376
|
throw new Error(`local credentials missing: ${displayPath(cfg.local.credentialsFile, cfg.rootDir)}. Run "odla-ai provision --write-dev-vars".`);
|
|
5442
5377
|
}
|
|
5443
|
-
if (credentials.appId !== cfg.app.id) {
|
|
5378
|
+
if (credentials && credentials.appId !== cfg.app.id) {
|
|
5444
5379
|
throw new Error(`local credentials are for app "${credentials.appId}", but config app is "${cfg.app.id}"`);
|
|
5445
5380
|
}
|
|
5446
|
-
const entry = credentials
|
|
5447
|
-
if (!entry?.tenantId) {
|
|
5381
|
+
const entry = credentials?.envs[env];
|
|
5382
|
+
if (!runtime && !entry?.tenantId) {
|
|
5448
5383
|
throw new Error(`local credentials have no tenant for env "${env}". Run "odla-ai provision --write-dev-vars".`);
|
|
5449
5384
|
}
|
|
5450
5385
|
const hasDb = cfg.services.includes("db");
|
|
5451
5386
|
const hasO11y = cfg.services.includes("o11y");
|
|
5452
|
-
if (hasDb && !entry
|
|
5387
|
+
if (!runtime && hasDb && !entry?.dbKey) {
|
|
5453
5388
|
throw new Error(`local credentials have no db key for env "${env}". Run "odla-ai provision --write-dev-vars".`);
|
|
5454
5389
|
}
|
|
5455
|
-
if (hasO11y && !entry
|
|
5390
|
+
if (!runtime && hasO11y && !entry?.o11yToken) {
|
|
5456
5391
|
throw new Error(`local credentials have no o11y token for env "${env}". Run "odla-ai provision --write-dev-vars".`);
|
|
5457
5392
|
}
|
|
5458
5393
|
const doFetch = options.fetch ?? fetch;
|
|
5394
|
+
const tenantId = entry?.tenantId ?? resolveTenant(cfg, env).tenant;
|
|
5459
5395
|
out.log(`smoke: ${cfg.app.id}/${env}`);
|
|
5460
|
-
out.log(`
|
|
5396
|
+
out.log(` mode: ${runtime ? "runtime (Worker-held credentials)" : "local credentials"}`);
|
|
5397
|
+
out.log(` tenant: ${tenantId}`);
|
|
5461
5398
|
const publicConfig = await getJson(doFetch, publicConfigUrl(cfg.platformUrl, cfg.app.id, env), void 0);
|
|
5462
5399
|
out.log(` public-config: ok`);
|
|
5463
5400
|
if (cfg.services.includes("ai") && cfg.ai?.provider) {
|
|
@@ -5471,7 +5408,7 @@ async function smoke(options) {
|
|
|
5471
5408
|
if (mode !== "hosted") throw new Error(`ai mode mismatch: expected "hosted", public-config has "${String(mode ?? "none")}"`);
|
|
5472
5409
|
out.log(" ai: hosted");
|
|
5473
5410
|
}
|
|
5474
|
-
if (hasO11y) out.log(` o11y: credentials present`);
|
|
5411
|
+
if (hasO11y) out.log(runtime ? ` o11y: Worker-held credential` : ` o11y: credentials present`);
|
|
5475
5412
|
if (cfg.services.includes("calendar")) {
|
|
5476
5413
|
const token = await getDeveloperToken(
|
|
5477
5414
|
cfg,
|
|
@@ -5490,10 +5427,10 @@ async function smoke(options) {
|
|
|
5490
5427
|
out.log(` calendar: ${status.status}, bookable (booking \u2192 ${status.bookingCalendarId ?? "primary"})`);
|
|
5491
5428
|
}
|
|
5492
5429
|
const database = await resolveDatabaseConfig(cfg);
|
|
5493
|
-
if (hasDb) {
|
|
5430
|
+
if (hasDb && !runtime) {
|
|
5494
5431
|
const expectedSchema = database.schema;
|
|
5495
5432
|
const expectedEntities = serializedEntities(expectedSchema);
|
|
5496
|
-
const liveSchemaPayload = await getJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(
|
|
5433
|
+
const liveSchemaPayload = await getJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, entry.dbKey);
|
|
5497
5434
|
const liveSchema = liveSchemaPayload.schema ?? liveSchemaPayload;
|
|
5498
5435
|
const liveEntities = serializedEntities(liveSchema);
|
|
5499
5436
|
if (expectedEntities.length) {
|
|
@@ -5503,7 +5440,7 @@ async function smoke(options) {
|
|
|
5503
5440
|
out.log(` schema: ${liveEntities.length} entities`);
|
|
5504
5441
|
const aggregateEntity = expectedEntities[0] ?? liveEntities[0];
|
|
5505
5442
|
if (aggregateEntity) {
|
|
5506
|
-
const aggregate = await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(
|
|
5443
|
+
const aggregate = await postJson2(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/aggregate`, entry.dbKey, {
|
|
5507
5444
|
ns: aggregateEntity,
|
|
5508
5445
|
aggregate: { count: true }
|
|
5509
5446
|
});
|
|
@@ -5512,9 +5449,20 @@ async function smoke(options) {
|
|
|
5512
5449
|
} else {
|
|
5513
5450
|
out.log(` aggregate: skipped (schema has no entities)`);
|
|
5514
5451
|
}
|
|
5452
|
+
} else if (hasDb) {
|
|
5453
|
+
out.log(` db: Worker-held credential (direct schema/aggregate skipped)`);
|
|
5515
5454
|
} else {
|
|
5516
5455
|
out.log(` db: skipped (not enabled)`);
|
|
5517
5456
|
}
|
|
5457
|
+
if (runtime) {
|
|
5458
|
+
const link = cfg.links?.[env];
|
|
5459
|
+
if (!link) throw new Error(`runtime smoke requires links.${env} in odla.config.mjs`);
|
|
5460
|
+
const res = await doFetch(link, { redirect: "manual" });
|
|
5461
|
+
if (res.status < 200 || res.status >= 500) {
|
|
5462
|
+
throw new Error(`runtime target ${new URL(link).origin} returned ${res.status}`);
|
|
5463
|
+
}
|
|
5464
|
+
out.log(` runtime target: ${res.status}`);
|
|
5465
|
+
}
|
|
5518
5466
|
const probes = database.integrations.flatMap(
|
|
5519
5467
|
(integration) => (integration.probes ?? []).map((probe) => ({ integration: integration.id, probe }))
|
|
5520
5468
|
);
|
|
@@ -5729,10 +5677,11 @@ async function projectCommand(command, parsed, deps) {
|
|
|
5729
5677
|
return true;
|
|
5730
5678
|
}
|
|
5731
5679
|
if (command === "smoke") {
|
|
5732
|
-
assertArgs(parsed, ["config", "env", "token", "email", "open"], 1);
|
|
5680
|
+
assertArgs(parsed, ["config", "env", "runtime", "token", "email", "open"], 1);
|
|
5733
5681
|
await smoke({
|
|
5734
5682
|
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
5735
5683
|
env: stringOpt(parsed.options.env),
|
|
5684
|
+
runtime: parsed.options.runtime === true,
|
|
5736
5685
|
token: stringOpt(parsed.options.token),
|
|
5737
5686
|
email: stringOpt(parsed.options.email),
|
|
5738
5687
|
open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
|
|
@@ -9659,6 +9608,7 @@ Usage:
|
|
|
9659
9608
|
odla-ai pm bug add --app <id> --title <t> (--description <text>|--body <text>) [--status <s>] [--severity <s>] [--goal <id>] [--assignee <id>] [--decision <id>] [--mutation-id <id>] [--json]
|
|
9660
9609
|
odla-ai bug report --app <id> --title <t> (--description <text>|--body <text>) [--severity <s>] [--json]
|
|
9661
9610
|
odla-ai pm <goal|task|decision|bug> get <id> [--json]
|
|
9611
|
+
odla-ai pm <goal|task|decision|bug> link <id> [--json]
|
|
9662
9612
|
odla-ai pm <goal|task|decision|bug> ref <id> [--json]
|
|
9663
9613
|
odla-ai pm goal set <id> [--title <t>|--status <s>|--proof <text>|--no-proof|--target <pct>|--no-target] [--mutation-id <id>] [--json]
|
|
9664
9614
|
odla-ai pm task set <id> [--title <t>|--column <backlog|ready|doing|review|done>|--rank <n>|--goal <id>|--no-goal|--alignment-decision <id>|--no-alignment-decision|--execution <human|agent|either>|--assignee <id>|--no-assignee|--description <text>|--body <text>|--acceptance <text>|--no-acceptance|--due <epoch-ms>|--no-due|--expected-revision <n>] [--mutation-id <id>] [--json]
|
|
@@ -9727,7 +9677,7 @@ Usage:
|
|
|
9727
9677
|
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]
|
|
9728
9678
|
odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
|
|
9729
9679
|
odla-ai credentials revoke <receipt-id> [--config odla.config.mjs] [--json]
|
|
9730
|
-
odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
|
|
9680
|
+
odla-ai smoke [--config odla.config.mjs] [--env dev] [--runtime] [--email <odla-account>] [--no-open]
|
|
9731
9681
|
odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
|
|
9732
9682
|
odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
|
|
9733
9683
|
odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
|
|
@@ -9831,7 +9781,7 @@ Commands:
|
|
|
9831
9781
|
"provision --live --yes" initializes only the live instance of
|
|
9832
9782
|
an existing sandbox app and enables every configured service;
|
|
9833
9783
|
no edit to the dev-first envs list is required.
|
|
9834
|
-
smoke Verify
|
|
9784
|
+
smoke Verify local service access, or use --runtime for a Worker-held credential deployment.
|
|
9835
9785
|
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
9836
9786
|
copilot, gemini, or agents (repeatable or comma-separated).
|
|
9837
9787
|
secrets Push configured db/o11y secrets into the Worker via wrangler
|
|
@@ -9871,12 +9821,11 @@ Safety:
|
|
|
9871
9821
|
attempts to open it \u2014 including in CI, SSH, display-less, and agent-driven
|
|
9872
9822
|
shells. Only --no-open suppresses the attempt. Browser launch is best-effort:
|
|
9873
9823
|
agents with browser control must open that exact URL immediately; otherwise
|
|
9874
|
-
they must give it to the human verbatim. A
|
|
9875
|
-
|
|
9876
|
-
the same code. Outside an interactive terminal the wait is capped (90s by
|
|
9824
|
+
they must give it to the human verbatim. A device code remains only in the
|
|
9825
|
+
running process. Outside an interactive terminal the wait is capped (90s by
|
|
9877
9826
|
default, --wait <seconds> to change); a still-pending handshake then exits
|
|
9878
|
-
with code 75
|
|
9879
|
-
|
|
9827
|
+
with code 75. Rerunning always requests and opens a new code; older clients'
|
|
9828
|
+
persisted pending state is discarded.
|
|
9880
9829
|
A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
|
|
9881
9830
|
The email is a non-secret identity hint: never provide a password or session
|
|
9882
9831
|
token. It is the email shown by the signed-in odla account \u2014 never infer it
|
|
@@ -10561,16 +10510,26 @@ function referenceMarkup(entity, record10) {
|
|
|
10561
10510
|
const label = (record10.title?.trim() || `${entity} ${record10.id}`).replaceAll("]", ")");
|
|
10562
10511
|
return `@[${label}](pm:${entity}/${record10.id})`;
|
|
10563
10512
|
}
|
|
10513
|
+
function studioRecordUrl(ctx, entity, id) {
|
|
10514
|
+
return new URL(
|
|
10515
|
+
`/studio/pm/${STUDIO_SECTION[entity]}/${encodeURIComponent(id)}`,
|
|
10516
|
+
ctx.platformUrl
|
|
10517
|
+
).href;
|
|
10518
|
+
}
|
|
10519
|
+
function studioRecordLink(ctx, entity, record10) {
|
|
10520
|
+
const label = (record10.title?.trim() || `${entity} ${record10.id}`).replaceAll("]", ")");
|
|
10521
|
+
return `[${label}](${studioRecordUrl(ctx, entity, record10.id)})`;
|
|
10522
|
+
}
|
|
10564
10523
|
function printRecord(ctx, entity, record10) {
|
|
10565
10524
|
ctx.out.log(
|
|
10566
|
-
`${record10.id} [${statusCol(entity, record10)}] ${record10.appId} ${
|
|
10525
|
+
`${record10.id} [${statusCol(entity, record10)}] ${record10.appId} ${studioRecordLink(ctx, entity, record10)}`
|
|
10567
10526
|
);
|
|
10568
10527
|
}
|
|
10569
10528
|
function emit2(ctx, value2, human) {
|
|
10570
10529
|
if (ctx.json) ctx.out.log(JSON.stringify(value2, null, 2));
|
|
10571
10530
|
else human();
|
|
10572
10531
|
}
|
|
10573
|
-
var DONE, writeMutationId2, FIELD_MAP;
|
|
10532
|
+
var DONE, writeMutationId2, FIELD_MAP, STUDIO_SECTION;
|
|
10574
10533
|
var init_pm_action_core = __esm({
|
|
10575
10534
|
"src/pm-action-core.ts"() {
|
|
10576
10535
|
"use strict";
|
|
@@ -10603,6 +10562,12 @@ var init_pm_action_core = __esm({
|
|
|
10603
10562
|
execution: { key: "executionMode" },
|
|
10604
10563
|
"expected-revision": { key: "expectedRevision", num: true }
|
|
10605
10564
|
};
|
|
10565
|
+
STUDIO_SECTION = {
|
|
10566
|
+
goal: "goals",
|
|
10567
|
+
task: "board",
|
|
10568
|
+
decision: "decisions",
|
|
10569
|
+
bug: "bugs"
|
|
10570
|
+
};
|
|
10606
10571
|
}
|
|
10607
10572
|
});
|
|
10608
10573
|
|
|
@@ -10648,7 +10613,8 @@ async function pmAdd(ctx, entity, parsed) {
|
|
|
10648
10613
|
input,
|
|
10649
10614
|
mutationId: writeMutationId2(parsed)
|
|
10650
10615
|
});
|
|
10651
|
-
|
|
10616
|
+
const record10 = { id: res.id, appId, title: String(input.title) };
|
|
10617
|
+
emit2(ctx, res, () => ctx.out.log(`created ${entity}: ${studioRecordLink(ctx, entity, record10)}`));
|
|
10652
10618
|
}
|
|
10653
10619
|
async function pmGet(ctx, entity, id) {
|
|
10654
10620
|
const { record: record10 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
|
|
@@ -10673,7 +10639,10 @@ async function pmSet(ctx, entity, id, parsed) {
|
|
|
10673
10639
|
patch: patch2,
|
|
10674
10640
|
mutationId: writeMutationId2(parsed)
|
|
10675
10641
|
});
|
|
10676
|
-
emit2(ctx, res, () =>
|
|
10642
|
+
emit2(ctx, res, () => {
|
|
10643
|
+
if (!res.record) return ctx.out.log(`updated ${entity} ${id}`);
|
|
10644
|
+
ctx.out.log(`${entity}: ${studioRecordLink(ctx, entity, res.record)} \u2192 ${statusCol(entity, res.record)}`);
|
|
10645
|
+
});
|
|
10677
10646
|
}
|
|
10678
10647
|
async function pmDone(ctx, entity, id, parsed) {
|
|
10679
10648
|
const decisionId = stringOpt(parsed.options.decision);
|
|
@@ -10683,7 +10652,11 @@ async function pmDone(ctx, entity, id, parsed) {
|
|
|
10683
10652
|
patch: patch2,
|
|
10684
10653
|
mutationId: writeMutationId2(parsed)
|
|
10685
10654
|
});
|
|
10686
|
-
emit2(ctx, res, () =>
|
|
10655
|
+
emit2(ctx, res, () => {
|
|
10656
|
+
const label = res.record ? studioRecordLink(ctx, entity, res.record) : id;
|
|
10657
|
+
const state2 = res.record ? statusCol(entity, res.record) : "done";
|
|
10658
|
+
ctx.out.log(`${entity}: ${label} \u2192 ${state2}`);
|
|
10659
|
+
});
|
|
10687
10660
|
}
|
|
10688
10661
|
async function pmTaskLifecycle(ctx, id, action2, parsed) {
|
|
10689
10662
|
const rawRevision = stringOpt(parsed.options["expected-revision"]);
|
|
@@ -10712,7 +10685,8 @@ async function pmTaskLifecycle(ctx, id, action2, parsed) {
|
|
|
10712
10685
|
);
|
|
10713
10686
|
emit2(ctx, res, () => {
|
|
10714
10687
|
const state2 = res.record ? statusCol("task", res.record) : action2;
|
|
10715
|
-
|
|
10688
|
+
const label = res.record ? studioRecordLink(ctx, "task", res.record) : id;
|
|
10689
|
+
ctx.out.log(`task: ${label} \u2192 ${state2}`);
|
|
10716
10690
|
});
|
|
10717
10691
|
}
|
|
10718
10692
|
async function allRecords(ctx, entity, appId) {
|
|
@@ -10817,6 +10791,27 @@ var init_pm_actions = __esm({
|
|
|
10817
10791
|
}
|
|
10818
10792
|
});
|
|
10819
10793
|
|
|
10794
|
+
// src/pm-links.ts
|
|
10795
|
+
async function pmLink(ctx, entity, id) {
|
|
10796
|
+
const { record: record10 } = await pmRequest(
|
|
10797
|
+
ctx,
|
|
10798
|
+
"GET",
|
|
10799
|
+
`/${entity}/${encodeURIComponent(id)}`
|
|
10800
|
+
);
|
|
10801
|
+
const url = studioRecordUrl(ctx, entity, record10.id);
|
|
10802
|
+
const markdown = studioRecordLink(ctx, entity, record10);
|
|
10803
|
+
emit2(ctx, { kind: entity, id: record10.id, label: record10.title ?? "", url, markdown }, () => {
|
|
10804
|
+
ctx.out.log(markdown);
|
|
10805
|
+
});
|
|
10806
|
+
}
|
|
10807
|
+
var init_pm_links = __esm({
|
|
10808
|
+
"src/pm-links.ts"() {
|
|
10809
|
+
"use strict";
|
|
10810
|
+
init_cjs_shims();
|
|
10811
|
+
init_pm_action_core();
|
|
10812
|
+
}
|
|
10813
|
+
});
|
|
10814
|
+
|
|
10820
10815
|
// src/pm-comments.ts
|
|
10821
10816
|
async function pmComment(ctx, entity, id, parsed) {
|
|
10822
10817
|
const body = stringOpt(parsed.options.body);
|
|
@@ -11143,7 +11138,7 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
11143
11138
|
if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
|
|
11144
11139
|
const requestedAction = parsed.positionals[2] ?? "list";
|
|
11145
11140
|
const action2 = canonicalAction(requestedAction);
|
|
11146
|
-
if (!action2) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|comment|comments|rm.`);
|
|
11141
|
+
if (!action2) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|link|ref|comment|comments|rm.`);
|
|
11147
11142
|
assertArgs(parsed, allowedOptions(entity, action2), 4);
|
|
11148
11143
|
if ((action2 === "ready" || action2 === "claim" || action2 === "release") && entity !== "task") {
|
|
11149
11144
|
throw new Error(`pm ${action2} is only valid for tasks`);
|
|
@@ -11167,6 +11162,8 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
11167
11162
|
return pmComments(ctx, entity, requireId2(id, action2));
|
|
11168
11163
|
case "rm":
|
|
11169
11164
|
return pmRemove(ctx, entity, requireId2(id, action2));
|
|
11165
|
+
case "link":
|
|
11166
|
+
return pmLink(ctx, entity, requireId2(id, action2));
|
|
11170
11167
|
case "ref":
|
|
11171
11168
|
return pmReference(ctx, entity, requireId2(id, action2));
|
|
11172
11169
|
case "ready":
|
|
@@ -11183,6 +11180,7 @@ var init_pm_command = __esm({
|
|
|
11183
11180
|
init_argv();
|
|
11184
11181
|
init_operator_context();
|
|
11185
11182
|
init_pm_actions();
|
|
11183
|
+
init_pm_links();
|
|
11186
11184
|
init_pm_comments();
|
|
11187
11185
|
init_token();
|
|
11188
11186
|
init_pm_watch();
|
|
@@ -11207,6 +11205,7 @@ var init_pm_command = __esm({
|
|
|
11207
11205
|
ready: ["goal", "alignment-decision", "execution", "description", "desc", "body", "acceptance", "expected-revision", "mutation-id"],
|
|
11208
11206
|
claim: ["expected-revision", "mutation-id"],
|
|
11209
11207
|
release: ["expected-revision", "mutation-id"],
|
|
11208
|
+
link: [],
|
|
11210
11209
|
ref: []
|
|
11211
11210
|
};
|
|
11212
11211
|
ENTITY_OPTIONS = {
|