octwin-cli 0.1.13 → 0.1.14

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/index.js +174 -86
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ Format: [Keep a Changelog](https://keepachangelog.com/) — newest first, bucket
5
5
  **Added · Changed · Deprecated · Removed · Fixed · Security**. The platform-wide view lives in the
6
6
  repo root [`CHANGELOG.md`](../../CHANGELOG.md); this file is the CLI-only cut that ships with the package.
7
7
 
8
+ ## [0.1.14] - 2026-07-23
9
+
10
+ ### Fixed
11
+ - **A failed demo seed no longer reports a clean `✓`.** `deploy` softens non-fatal
12
+ install steps (e.g. a demo-seed row) to warning frames, but the summary still printed
13
+ `✓ Deployed` and exited 0 — so a deploy that seeded ZERO records read as success (the
14
+ false-✓ trap). `deploy` now collects those step errors, prints `⚠ Deployed with N
15
+ warning(s) — data may be incomplete` with each message, and exits non-zero so CI /
16
+ a `deploy && chat` chain catches it.
17
+
18
+ ### Added
19
+ - **`login` echoes what the token reaches.** After saving, `octwin login` calls
20
+ `/api/self/t/whoami` and prints the workspace, project pin, and scopes — so a fresh
21
+ token self-identifies without a second `octwin whoami` (best-effort; a network failure
22
+ never fails the save).
23
+ - **`validate --remote` surfaces warnings + the data-store trap.** The remote validate now
24
+ renders advisory `⚠` warnings (which don't block deploy) and, for a pack declaring
25
+ `required_adapters: [data-store]`, reports the missing-data-store error the deploy would
26
+ have 409'd on — plus a warning that an `xrm.yaml` pack usually shouldn't declare that
27
+ adapter at all. It also now catches (offline) a list-form `entities:` block and a
28
+ `localized: true` field seeded with a bare string — traps that previously surfaced only
29
+ mid-seed at deploy.
30
+
8
31
  ## [0.1.13] - 2026-07-22
9
32
 
10
33
  ### Added
package/dist/index.js CHANGED
@@ -335,8 +335,8 @@ async function notifyIfKbStale(flags) {
335
335
  return;
336
336
  const ctrl = new AbortController();
337
337
  const timer = setTimeout(() => ctrl.abort(), 2_000);
338
- const res = await fetch(`${t.url}/api/admin/tenants/${t.tenant}/octwin-platform-kb?meta=1`, {
339
- headers: { authorization: `Bearer ${t.token}` }, signal: ctrl.signal,
338
+ const res = await fetch(`${t.url}/api/self/t/octwin-platform-kb?meta=1`, {
339
+ headers: authHeaders(t), signal: ctrl.signal,
340
340
  });
341
341
  clearTimeout(timer);
342
342
  if (!res.ok)
@@ -402,10 +402,11 @@ function cmdInit(flags) {
402
402
  displayName: flags['display-name'] ?? undefined,
403
403
  });
404
404
  // Deploy config + repo hygiene + a README.
405
+ // The token carries its own tenant (and optional project pin), so pack.json
406
+ // needs only the platform URL. `tenant`/`project` may be added as optional
407
+ // overrides (they also seed `octwin chat`, which is tenant/project-pathed).
405
408
  writeFileSync(join(dir, 'pack.json'), JSON.stringify({
406
409
  platform_url: 'http://localhost:3000',
407
- tenant: 'your-tenant-slug',
408
- project: 'main',
409
410
  }, null, 2) + '\n', 'utf8');
410
411
  writeFileSync(join(dir, '.gitignore'), 'node_modules/\n.pack-bundles/\n.octwin/\n', 'utf8');
411
412
  if (!existsSync(join(dir, 'README.md'))) {
@@ -417,7 +418,7 @@ function cmdInit(flags) {
417
418
  console.log(' git init && git add -A && git commit -m "init pack"');
418
419
  console.log(' # edit manifest.yaml / flows / prompts, then:');
419
420
  console.log(' octwin validate');
420
- console.log(' # set platform_url + tenant + project in pack.json, then:');
421
+ console.log(' # set platform_url in pack.json (tenant comes from your token), then:');
421
422
  console.log(' octwin login --url <platformUrl> --token <deploy-token>');
422
423
  console.log(' octwin deploy');
423
424
  }
@@ -443,11 +444,12 @@ async function cmdValidate(flags) {
443
444
  }
444
445
  // Remote: the SAME validation the deploy route runs — manifest `.strict()` +
445
446
  // every flow (schema/expression/structure) — returning ALL errors at once.
446
- const { url, tenant, project, token } = resolveTarget(flags, packDir);
447
- console.log(`→ Validating against ${tenant}/${project} @ ${url} …`);
448
- const res = await fetchOrDie(`${url}/api/admin/tenants/${tenant}/projects/${project}/packs/validate`, {
447
+ const t = resolveTarget(flags, packDir);
448
+ const { url } = t;
449
+ console.log(`→ Validating against ${targetLabel(t)} @ ${url} …`);
450
+ const res = await fetchOrDie(`${url}/api/self/p/packs/validate`, {
449
451
  method: 'POST',
450
- headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
452
+ headers: { 'content-type': 'application/json', ...authHeaders(t) },
451
453
  body: JSON.stringify({ files }),
452
454
  }, 'remote validate');
453
455
  const text = await res.text();
@@ -467,6 +469,12 @@ async function cmdValidate(flags) {
467
469
  console.error(typeof json === 'string' ? json : JSON.stringify(json, null, 2));
468
470
  process.exit(1);
469
471
  }
472
+ const warnings = (json?.warnings ?? []);
473
+ if (warnings.length) {
474
+ console.log(` ⚠ ${warnings.length} warning${warnings.length === 1 ? '' : 's'} (won't block deploy):`);
475
+ for (const w of warnings)
476
+ console.log(` ⚠ [${w.where}] ${w.message}`);
477
+ }
470
478
  if (json?.ok) {
471
479
  console.log(`✓ ${id}@${version} passes the platform's FULL validation — deploy won't reject on schema.`);
472
480
  return;
@@ -486,55 +494,83 @@ async function cmdValidate(flags) {
486
494
  }
487
495
  process.exit(1);
488
496
  }
489
- function cmdLogin(flags) {
490
- const url = flags.url ?? process.env.PACK_PLATFORM_URL ?? die('usage: octwin login --url <platformUrl> --token <t>');
497
+ async function cmdLogin(flags) {
498
+ const rawUrl = flags.url ?? process.env.PACK_PLATFORM_URL ?? die('usage: octwin login --url <platformUrl> --token <t>');
499
+ const url = rawUrl.replace(/\/$/, '');
491
500
  const token = flags.token ?? process.env.PACK_TOKEN ?? die('missing --token');
492
501
  const creds = readCreds();
493
- creds[url.replace(/\/$/, '')] = token;
502
+ creds[url] = token;
494
503
  writeCreds(creds);
495
504
  console.log(`✓ Saved token for ${url}`);
505
+ // Best-effort: echo what the token reaches (workspace + project pin + scopes)
506
+ // so a fresh token self-identifies without a second `octwin whoami`. A network
507
+ // failure never fails the save — the token is stored regardless.
508
+ try {
509
+ const res = await fetch(`${url}/api/self/t/whoami`, { headers: { authorization: `Bearer ${token}` } });
510
+ if (res.ok) {
511
+ const j = await res.json();
512
+ const scopes = Array.isArray(j.scopes) && j.scopes.length ? ` · scopes: ${j.scopes.join(', ')}` : '';
513
+ console.log(` → workspace '${j.tenant_slug}'${j.project_slug ? `, pinned to project '${j.project_slug}'` : ''}${scopes}`);
514
+ }
515
+ else if (res.status === 401 || res.status === 403) {
516
+ console.log(` ⚠ token saved, but the platform rejected it (HTTP ${res.status}) — check it's a current oct_… token`);
517
+ }
518
+ }
519
+ catch { /* platform unreachable — the token is saved regardless */ }
520
+ }
521
+ /** Bearer auth + the optional self-surface overrides, as request headers. */
522
+ function authHeaders(t) {
523
+ const h = { authorization: `Bearer ${t.token}` };
524
+ if (t.tenant)
525
+ h['x-octwin-tenant'] = t.tenant;
526
+ if (t.project)
527
+ h['x-octwin-project'] = t.project;
528
+ return h;
496
529
  }
497
- /** Resolve platform url + tenant + project + token: flags > pack.json > env > saved login. */
530
+ /** Resolve platform url + token (+ optional tenant/project overrides):
531
+ * flags > pack.json > env > saved login. Tenant is derived from the token
532
+ * server-side, so only url + token are required. */
498
533
  function resolveTarget(flags, packDir) {
499
534
  const cfg = readPackConfig(packDir);
500
535
  const url = (flags.url ?? process.env.PACK_PLATFORM_URL ?? cfg.platform_url ?? '').replace(/\/$/, '');
501
- const tenant = flags.tenant ?? process.env.PACK_TENANT ?? cfg.tenant ?? '';
502
- const project = flags.project ?? process.env.PACK_PROJECT ?? cfg.project ?? 'main';
536
+ const tenant = flags.tenant || process.env.PACK_TENANT || cfg.tenant || undefined;
537
+ const project = flags.project || process.env.PACK_PROJECT || cfg.project || undefined;
503
538
  const token = flags.token ?? process.env.PACK_TOKEN ?? readCreds()[url] ?? '';
504
539
  if (!url)
505
540
  die('no platform url — set it in pack.json, --url, or PACK_PLATFORM_URL');
506
- if (!tenant)
507
- die('no tenant — set it in pack.json, --tenant, or PACK_TENANT');
508
541
  if (!token)
509
- die('no token — generate a deploy token in the console (Settings → API tokens), then `octwin login --url <url> --token oct_…` or pass --token');
510
- return { url, tenant, project, token };
542
+ die('no token — generate an API token in the console (Settings → API tokens), then `octwin login --url <url> --token oct_…` or pass --token');
543
+ return { url, token, tenant, project };
511
544
  }
512
- /** Non-fatal `resolveTarget`: returns null (never dies) when any of url/tenant/token
513
- * is missing. Used by the fail-silent KB-staleness observer, which must never
545
+ /** Non-fatal `resolveTarget`: returns null (never dies) when url or token is
546
+ * missing. Used by the fail-silent KB-staleness observer, which must never
514
547
  * interrupt a command over a config gap. */
515
548
  function resolveTargetOrNull(flags, packDir) {
516
549
  const cfg = readPackConfig(packDir);
517
550
  const url = (flags.url ?? process.env.PACK_PLATFORM_URL ?? cfg.platform_url ?? '').replace(/\/$/, '');
518
- const tenant = flags.tenant ?? process.env.PACK_TENANT ?? cfg.tenant ?? '';
519
- const project = flags.project ?? process.env.PACK_PROJECT ?? cfg.project ?? 'main';
551
+ const tenant = flags.tenant || process.env.PACK_TENANT || cfg.tenant || undefined;
552
+ const project = flags.project || process.env.PACK_PROJECT || cfg.project || undefined;
520
553
  const token = flags.token ?? process.env.PACK_TOKEN ?? readCreds()[url] ?? '';
521
- if (!url || !tenant || !token)
554
+ if (!url || !token)
522
555
  return null;
523
- return { url, tenant, project, token };
556
+ return { url, token, tenant, project };
524
557
  }
525
558
  async function cmdWhoami(flags) {
526
559
  const packDir = resolve(flags.dir ?? '.');
527
- const { url, tenant, token } = resolveTarget(flags, packDir);
528
- console.log(`→ Checking the saved token against '${tenant}' @ ${url} …`);
529
- const res = await fetchOrDie(`${url}/api/admin/tenants/${tenant}/packs`, { headers: { authorization: `Bearer ${token}` } }, 'token check');
560
+ const t = resolveTarget(flags, packDir);
561
+ console.log(`→ Checking the token against ${t.url} …`);
562
+ const res = await fetchOrDie(`${t.url}/api/self/t/whoami`, { headers: authHeaders(t) }, 'token check');
530
563
  if (res.ok) {
531
- console.log(`✓ Token valid for tenant '${tenant}' at ${url} (${token.startsWith('oct_') ? 'deploy token' : 'session token'})`);
564
+ const j = await res.json();
565
+ console.log(`✓ Token valid — workspace '${j.tenant_slug}'${j.project_slug ? `, pinned to project '${j.project_slug}'` : ''} (${j.kind === 'api_token' ? 'API token' : 'session'})`);
566
+ if (Array.isArray(j.scopes))
567
+ console.log(` scopes: ${j.scopes.length ? j.scopes.join(', ') : '(none)'}`);
532
568
  return;
533
569
  }
534
570
  const why = res.status === 401 ? 'invalid / expired / revoked token'
535
- : res.status === 403 ? 'token not authorized for this tenant'
571
+ : res.status === 403 ? 'token not authorized for this workspace'
536
572
  : await res.text();
537
- die(`token check failed for '${tenant}' (HTTP ${res.status}) — ${why}`);
573
+ die(`token check failed (HTTP ${res.status}) — ${why}`);
538
574
  }
539
575
  /**
540
576
  * Read the deploy SSE stream, printing each progress frame's message live, and
@@ -547,6 +583,11 @@ async function readDeployProgress(body) {
547
583
  const decoder = new TextDecoder();
548
584
  let buf = '';
549
585
  let terminal = null;
586
+ // Non-terminal frames with `status:'error'` are step failures the install
587
+ // SOFTENS to non-fatal (e.g. a demo-seed row) — the reconcile keeps going and
588
+ // still emits a `done`. We collect them so the deploy is NOT reported as a
589
+ // clean ✓ when a step actually failed (the false-✓ trap).
590
+ const stepErrors = [];
550
591
  for (;;) {
551
592
  const { done, value } = await reader.read();
552
593
  if (done)
@@ -570,14 +611,18 @@ async function readDeployProgress(body) {
570
611
  terminal = ev;
571
612
  continue;
572
613
  }
573
- if (ev.message)
574
- console.log(` ${ev.status === 'error' ? '⚠' : '·'} ${ev.message}`);
614
+ if (ev.message) {
615
+ const isErr = ev.status === 'error';
616
+ if (isErr)
617
+ stepErrors.push(ev.message);
618
+ console.log(` ${isErr ? '⚠' : '·'} ${ev.message}`);
619
+ }
575
620
  }
576
621
  }
577
- return terminal;
622
+ return { terminal, stepErrors };
578
623
  }
579
- function printDeploySuccess(id, version, tenant, project, r) {
580
- console.log(`✓ Deployed ${id}@${version} and installed onto ${tenant}/${project}`);
624
+ function printDeploySuccess(id, version, t, r) {
625
+ console.log(`✓ Deployed ${id}@${version} and installed onto ${targetLabel(t)}`);
581
626
  if (r?.warning)
582
627
  console.log(` ⚠ ${r.warning}`);
583
628
  const s = r?.summary;
@@ -594,29 +639,39 @@ function printDeploySuccess(id, version, tenant, project, r) {
594
639
  if (parts.length)
595
640
  console.log(` Seeded: ${parts.join(', ')}`);
596
641
  }
597
- console.log(`\nChat with it on your tenant (web widget / console test page for ${tenant}/${project}).`);
642
+ console.log(`\nChat with it: octwin chat "hi" --as tester (or the web widget / console test page).`);
598
643
  }
599
644
  async function cmdDeploy(flags) {
600
645
  const packDir = resolve(flags.dir ?? '.');
601
- const { url, tenant, project, token } = resolveTarget(flags, packDir);
646
+ const t = resolveTarget(flags, packDir);
647
+ const { url } = t;
602
648
  const { id, version, files } = localValidate(packDir);
603
- const endpoint = `${url}/api/admin/tenants/${tenant}/projects/${project}/packs/deploy`;
649
+ const endpoint = `${url}/api/self/p/packs/deploy`;
604
650
  const seed = flags.seed === true;
605
- console.log(`→ Deploying ${id}@${version} (${Object.keys(files).length} files) to ${tenant}/${project}${seed ? ' — with demo seed' : ''} …`);
651
+ console.log(`→ Deploying ${id}@${version} (${Object.keys(files).length} files) to ${targetLabel(t)}${seed ? ' — with demo seed' : ''} …`);
606
652
  const res = await fetchOrDie(endpoint, {
607
653
  method: 'POST',
608
654
  // Ask for a progress stream; the platform falls back to plain JSON if it
609
655
  // (or an error before any progress) can't stream — handled below.
610
- headers: { 'content-type': 'application/json', accept: 'text/event-stream', authorization: `Bearer ${token}` },
656
+ headers: { 'content-type': 'application/json', accept: 'text/event-stream', ...authHeaders(t) },
611
657
  body: JSON.stringify({ files, seed }),
612
658
  }, 'deploy');
613
659
  // Streaming path — live install + seed progress (image generation can take a
614
660
  // while, so `--seed` prints per-record / per-image lines as they happen).
615
661
  if (res.ok && (res.headers.get('content-type') ?? '').includes('text/event-stream') && res.body) {
616
- const final = await readDeployProgress(res.body);
662
+ const { terminal: final, stepErrors } = await readDeployProgress(res.body);
617
663
  if (!final || final.stage === 'error')
618
664
  die(`deploy failed${final?.message ? `: ${final.message}` : ' (stream ended early)'}`);
619
- printDeploySuccess(id, version, tenant, project, final);
665
+ printDeploySuccess(id, version, t, final);
666
+ if (stepErrors.length) {
667
+ // The pack IS installed, but a step (e.g. the demo seed) failed — say so
668
+ // plainly and exit non-zero so CI / a `deploy && chat` chain doesn't treat
669
+ // an incomplete install as a clean success.
670
+ console.error(`\n⚠ Deployed with ${stepErrors.length} warning${stepErrors.length === 1 ? '' : 's'} — data may be incomplete:`);
671
+ for (const e of stepErrors)
672
+ console.error(` • ${e}`);
673
+ process.exit(1);
674
+ }
620
675
  return;
621
676
  }
622
677
  // Non-streaming path — plain JSON (older platform, or an error thrown before
@@ -635,11 +690,12 @@ async function cmdDeploy(flags) {
635
690
  console.error(typeof json === 'string' ? json : JSON.stringify(json, null, 2));
636
691
  process.exit(1);
637
692
  }
638
- printDeploySuccess(id, version, tenant, project, json);
693
+ printDeploySuccess(id, version, t, json);
639
694
  }
640
695
  async function cmdStatus(flags) {
641
696
  const packDir = resolve(flags.dir ?? '.');
642
- const { url, tenant, project, token } = resolveTarget(flags, packDir);
697
+ const t = resolveTarget(flags, packDir);
698
+ const { url } = t;
643
699
  const manifestPath = join(packDir, 'manifest.yaml');
644
700
  if (!existsSync(manifestPath))
645
701
  die('no manifest.yaml in the pack directory (run from your pack dir or pass --dir)');
@@ -648,9 +704,9 @@ async function cmdStatus(flags) {
648
704
  die('manifest.yaml must declare a string `id`');
649
705
  const id = doc.id;
650
706
  const localVersion = typeof doc?.version === 'string' ? doc.version : '?';
651
- console.log(`→ Checking ${id}@${localVersion} on ${tenant}/${project} @ ${url} …`);
652
- const res = await fetchOrDie(`${url}/api/admin/tenants/${tenant}/projects/${project}/packs/${id}/runtime`, {
653
- headers: { authorization: `Bearer ${token}` },
707
+ console.log(`→ Checking ${id}@${localVersion} on ${targetLabel(t)} @ ${url} …`);
708
+ const res = await fetchOrDie(`${url}/api/self/p/packs/${id}/runtime`, {
709
+ headers: authHeaders(t),
654
710
  }, 'status check');
655
711
  const text = await res.text();
656
712
  let json;
@@ -662,13 +718,13 @@ async function cmdStatus(flags) {
662
718
  }
663
719
  if (!res.ok) {
664
720
  if (res.status === 404)
665
- die(`'${id}' is not installed on ${tenant}/${project} yet — run \`octwin deploy\` first`);
721
+ die(`'${id}' is not installed on ${targetLabel(t)} yet — run \`octwin deploy\` first`);
666
722
  console.error(`✗ status check failed (HTTP ${res.status})`);
667
723
  printAuthHint(res.status, url);
668
724
  console.error(typeof json === 'string' ? json : JSON.stringify(json, null, 2));
669
725
  process.exit(1);
670
726
  }
671
- console.log(`${id} on ${tenant}/${project} @ ${url}`);
727
+ console.log(`${id} on ${targetLabel(t)} @ ${url}`);
672
728
  console.log(` installed version : ${json.installed_version}`);
673
729
  console.log(` live on instance : registered=${json.registered} source=${json.source} loaded=${json.loaded_version ?? '(none)'}`);
674
730
  console.log(` flows : ${(json.flows ?? []).join(', ') || '(none)'}`);
@@ -687,10 +743,11 @@ async function cmdStatus(flags) {
687
743
  }
688
744
  async function cmdPlatformKb(flags) {
689
745
  const packDir = resolve(flags.dir ?? '.');
690
- const { url, tenant, token } = resolveTarget(flags, packDir);
691
- console.log(`→ Pulling the platform capability reference from '${tenant}' @ ${url} …`);
692
- const res = await fetchOrDie(`${url}/api/admin/tenants/${tenant}/octwin-platform-kb`, {
693
- headers: { authorization: `Bearer ${token}` },
746
+ const t = resolveTarget(flags, packDir);
747
+ const { url } = t;
748
+ console.log(`→ Pulling the platform capability reference from ${url} …`);
749
+ const res = await fetchOrDie(`${url}/api/self/t/octwin-platform-kb`, {
750
+ headers: authHeaders(t),
694
751
  }, 'platform-kb pull');
695
752
  const text = await res.text();
696
753
  if (!res.ok) {
@@ -755,8 +812,8 @@ async function cmdPlatformKb(flags) {
755
812
  /** GET an admin endpoint with the deploy token; returns `{ status, json }`.
756
813
  * Dies (with the URL) on a network failure; auth failures return so the
757
814
  * caller can add command-specific context on top of `authFailureHint`. */
758
- async function apiGet(endpoint, token) {
759
- const res = await fetchOrDie(endpoint, { headers: { authorization: `Bearer ${token}` } }, 'request');
815
+ async function apiGet(endpoint, t) {
816
+ const res = await fetchOrDie(endpoint, { headers: authHeaders(t) }, 'request');
760
817
  const text = await res.text();
761
818
  let json;
762
819
  try {
@@ -767,16 +824,22 @@ async function apiGet(endpoint, token) {
767
824
  }
768
825
  return { status: res.status, json };
769
826
  }
827
+ /** A progress-line label for the target workspace. The token names the tenant, so
828
+ * we surface at most the project (when pinned or overridden by `--project`). */
829
+ function targetLabel(t) {
830
+ return t.project ? `project '${t.project}'` : 'your workspace';
831
+ }
770
832
  /** `octwin records [entity] [id]` — inspect the pack's XRM data (needs a `records:read` token). */
771
833
  async function cmdRecords(flags) {
772
834
  const packDir = resolve(flags.dir ?? '.');
773
- const { url, tenant, project, token } = resolveTarget(flags, packDir);
774
- const base = `${url}/api/admin/tenants/${tenant}/projects/${project}`;
835
+ const t = resolveTarget(flags, packDir);
836
+ const { url } = t;
837
+ const base = `${url}/api/self/p`;
775
838
  const entity = flags._[0];
776
839
  const recordId = flags._[1];
777
- console.log(`→ Reading ${recordId ? `${entity} record ${recordId}` : entity ? `${entity} records` : 'the entity catalog'} from ${tenant}/${project} …`);
840
+ console.log(`→ Reading ${recordId ? `${entity} record ${recordId}` : entity ? `${entity} records` : 'the entity catalog'} from ${targetLabel(t)} …`);
778
841
  if (!entity) {
779
- const { status, json } = await apiGet(`${base}/xrm/entities`, token);
842
+ const { status, json } = await apiGet(`${base}/xrm/entities`, t);
780
843
  if (status !== 200)
781
844
  die(`could not read entities (HTTP ${status})`);
782
845
  if (json?.has_xrm === false) {
@@ -788,7 +851,7 @@ async function cmdRecords(flags) {
788
851
  console.log('No entities visible — mint a token with the `records:read` scope to inspect data.');
789
852
  return;
790
853
  }
791
- console.log(`Entities in ${tenant}/${project}:`);
854
+ console.log(`Entities in ${targetLabel(t)}:`);
792
855
  for (const e of ents)
793
856
  console.log(` ${e.entity} (${e.open_count ?? 0} records)`);
794
857
  console.log('\nList records: octwin records <entity>');
@@ -796,7 +859,7 @@ async function cmdRecords(flags) {
796
859
  }
797
860
  if (!recordId) {
798
861
  const limit = flags.limit ?? '50';
799
- const { status, json } = await apiGet(`${base}/xrm/records?entity=${encodeURIComponent(entity)}&limit=${limit}`, token);
862
+ const { status, json } = await apiGet(`${base}/xrm/records?entity=${encodeURIComponent(entity)}&limit=${limit}`, t);
800
863
  if (status === 403)
801
864
  die('forbidden — the paged record list needs the `records` plan feature on this tenant');
802
865
  if (status !== 200) {
@@ -815,7 +878,7 @@ async function cmdRecords(flags) {
815
878
  console.log(` #${r.record_number ?? '?'} ${r.title ?? '(untitled)'}${r.stage ? ` [${r.stage}]` : ''} ${r.id}`);
816
879
  return;
817
880
  }
818
- const { status, json } = await apiGet(`${base}/xrm/records/${encodeURIComponent(recordId)}`, token);
881
+ const { status, json } = await apiGet(`${base}/xrm/records/${encodeURIComponent(recordId)}`, t);
819
882
  if (status === 403)
820
883
  die('forbidden — mint a token with the `records:read` scope');
821
884
  if (status === 404)
@@ -830,15 +893,16 @@ async function cmdRecords(flags) {
830
893
  * or show one's event timeline (full text + the renders each turn produced). */
831
894
  async function cmdLogs(flags) {
832
895
  const packDir = resolve(flags.dir ?? '.');
833
- const { url, tenant, project, token } = resolveTarget(flags, packDir);
834
- const base = `${url}/api/admin/tenants/${tenant}/projects/${project}`;
896
+ const t = resolveTarget(flags, packDir);
897
+ const { url } = t;
898
+ const base = `${url}/api/self/p`;
835
899
  const convId = flags._[0];
836
900
  const asJson = flags.json === true;
837
901
  const asHandle = typeof flags.as === 'string' ? flags.as : undefined;
838
902
  if (!asJson)
839
- console.log(`→ Reading ${convId ? `conversation ${convId}` : 'recent conversations'} from ${tenant}/${project} …`);
903
+ console.log(`→ Reading ${convId ? `conversation ${convId}` : 'recent conversations'} from ${targetLabel(t)} …`);
840
904
  if (!convId) {
841
- const { status, json } = await apiGet(`${base}/conversations?limit=50`, token);
905
+ const { status, json } = await apiGet(`${base}/conversations?limit=50`, t);
842
906
  if (status !== 200)
843
907
  die(`could not read conversations (HTTP ${status})${errDetail(json)} — ${authFailureHint(status, url)}`);
844
908
  let convs = (json?.conversations ?? []);
@@ -852,7 +916,7 @@ async function cmdLogs(flags) {
852
916
  console.log(JSON.stringify(convs, null, 2));
853
917
  return;
854
918
  }
855
- console.log(`Recent conversations in ${tenant}/${project}${asHandle ? ` (handle: ${asHandle})` : ''}:`);
919
+ console.log(`Recent conversations in ${targetLabel(t)}${asHandle ? ` (handle: ${asHandle})` : ''}:`);
856
920
  for (const c of convs) {
857
921
  const handle = c.contact?.channel_contact_handle ?? '?';
858
922
  const name = c.contact?.display_name && c.contact.display_name !== handle ? ` (${c.contact.display_name})` : '';
@@ -862,7 +926,7 @@ async function cmdLogs(flags) {
862
926
  console.log('\nView a timeline: octwin logs <conversationId> (add --json for full payloads)');
863
927
  return;
864
928
  }
865
- const { status, json } = await apiGet(`${base}/conversations/${encodeURIComponent(convId)}`, token);
929
+ const { status, json } = await apiGet(`${base}/conversations/${encodeURIComponent(convId)}`, t);
866
930
  if (status === 404)
867
931
  die(`conversation '${convId}' not found`);
868
932
  if (status !== 200)
@@ -1017,12 +1081,33 @@ async function cmdChat(flags) {
1017
1081
  const packDir = resolve(flags.dir ?? '.');
1018
1082
  const cfg = readPackConfig(packDir);
1019
1083
  const url = (flags.url ?? process.env.PACK_PLATFORM_URL ?? cfg.platform_url ?? '').replace(/\/$/, '');
1020
- const tenant = flags.tenant ?? process.env.PACK_TENANT ?? cfg.tenant ?? '';
1021
- const project = flags.project ?? process.env.PACK_PROJECT ?? cfg.project ?? 'main';
1022
1084
  if (!url)
1023
1085
  die('no platform url — set it in pack.json, --url, or PACK_PLATFORM_URL');
1086
+ let tenant = flags.tenant || process.env.PACK_TENANT || cfg.tenant || '';
1087
+ let project = flags.project || process.env.PACK_PROJECT || cfg.project || '';
1088
+ // The dev web channel is tenant/project-pathed (it simulates an end-user on a
1089
+ // specific project). When they aren't configured, derive them from the token —
1090
+ // its tenant + optional project pin — via the slug-free `/api/self/t/whoami`.
1091
+ if (!tenant || !project) {
1092
+ const token = flags.token ?? process.env.PACK_TOKEN ?? readCreds()[url] ?? '';
1093
+ if (token) {
1094
+ try {
1095
+ const who = await fetch(`${url}/api/self/t/whoami`, {
1096
+ headers: { authorization: `Bearer ${token}`, ...(tenant ? { 'x-octwin-tenant': tenant } : {}) },
1097
+ });
1098
+ if (who.ok) {
1099
+ const j = await who.json();
1100
+ tenant = tenant || (j.tenant_slug ?? '');
1101
+ project = project || (j.project_slug ?? '');
1102
+ }
1103
+ }
1104
+ catch { /* fall through to the checks below */ }
1105
+ }
1106
+ }
1024
1107
  if (!tenant)
1025
- die('no tenant — set it in pack.json, --tenant, or PACK_TENANT');
1108
+ die('no tenant — set --tenant / PACK_TENANT / pack.json, or pass a --token to derive it');
1109
+ if (!project)
1110
+ project = 'main';
1026
1111
  const from = flags.as ?? 'cli-tester';
1027
1112
  const asJson = flags.json === true;
1028
1113
  const tapId = typeof flags.tap === 'string' ? flags.tap : undefined;
@@ -1136,7 +1221,8 @@ async function cmdMedia(flags) {
1136
1221
  if (sub !== 'generate')
1137
1222
  die('usage: octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json]');
1138
1223
  const packDir = resolve(flags.dir ?? '.');
1139
- const { url, tenant, project, token } = resolveTarget(flags, packDir);
1224
+ const t = resolveTarget(flags, packDir);
1225
+ const { url } = t;
1140
1226
  const prompt = flags._[1];
1141
1227
  if (!prompt)
1142
1228
  die('usage: octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json]');
@@ -1144,10 +1230,10 @@ async function cmdMedia(flags) {
1144
1230
  const size = typeof flags.size === 'string' ? flags.size : undefined;
1145
1231
  const out = typeof flags.out === 'string' ? flags.out : undefined;
1146
1232
  if (!asJson)
1147
- console.log(`→ Generating an image on '${tenant}/${project}' @ ${url} …`);
1148
- const res = await fetchOrDie(`${url}/api/admin/tenants/${tenant}/projects/${project}/media/generate`, {
1233
+ console.log(`→ Generating an image on ${targetLabel(t)} @ ${url} …`);
1234
+ const res = await fetchOrDie(`${url}/api/self/p/media/generate`, {
1149
1235
  method: 'POST',
1150
- headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
1236
+ headers: { 'content-type': 'application/json', ...authHeaders(t) },
1151
1237
  body: JSON.stringify({ prompt, ...(size ? { size } : {}) }),
1152
1238
  }, 'media generate');
1153
1239
  const text = await res.text();
@@ -1191,19 +1277,20 @@ async function cmdMedia(flags) {
1191
1277
  * the aggregate inbox, one case + its timeline, or the queue list. */
1192
1278
  async function cmdCases(flags) {
1193
1279
  const packDir = resolve(flags.dir ?? '.');
1194
- const { url, tenant, project, token } = resolveTarget(flags, packDir);
1195
- const base = `${url}/api/admin/tenants/${tenant}/projects/${project}`;
1280
+ const t = resolveTarget(flags, packDir);
1281
+ const { url } = t;
1282
+ const base = `${url}/api/self/p`;
1196
1283
  const caseId = flags._[0];
1197
1284
  const asJson = flags.json === true;
1198
1285
  if (!asJson)
1199
- console.log(`→ Reading ${flags.queues === true ? 'case queues' : caseId ? `case ${caseId}` : 'the case inbox'} from ${tenant}/${project} …`);
1286
+ console.log(`→ Reading ${flags.queues === true ? 'case queues' : caseId ? `case ${caseId}` : 'the case inbox'} from ${targetLabel(t)} …`);
1200
1287
  const caseFail = (what, status, json) => {
1201
1288
  if (status === 403)
1202
1289
  die(`forbidden — casework needs the 'cases' plan feature on this tenant, and a role whose grants reach the queue`);
1203
1290
  die(`could not read ${what} (HTTP ${status})${errDetail(json)}${status === 401 ? ` — ${authFailureHint(status, url)}` : ''}`);
1204
1291
  };
1205
1292
  if (flags.queues === true) {
1206
- const { status, json } = await apiGet(`${base}/case-queues`, token);
1293
+ const { status, json } = await apiGet(`${base}/case-queues`, t);
1207
1294
  if (status !== 200)
1208
1295
  caseFail('case queues', status, json);
1209
1296
  if (asJson) {
@@ -1211,7 +1298,7 @@ async function cmdCases(flags) {
1211
1298
  return;
1212
1299
  }
1213
1300
  const queues = (json?.queues ?? []);
1214
- console.log(`Case queues in ${tenant}/${project}:`);
1301
+ console.log(`Case queues in ${targetLabel(t)}:`);
1215
1302
  for (const q of queues)
1216
1303
  console.log(` ${q.key}${q.name ? ` (${q.name})` : ''} ${q.open_count} open`);
1217
1304
  if (json?.unrouted_open_count)
@@ -1220,7 +1307,7 @@ async function cmdCases(flags) {
1220
1307
  }
1221
1308
  if (!caseId) {
1222
1309
  const limit = flags.limit ?? '50';
1223
- const { status, json } = await apiGet(`${base}/cases?limit=${limit}`, token);
1310
+ const { status, json } = await apiGet(`${base}/cases?limit=${limit}`, t);
1224
1311
  if (status !== 200)
1225
1312
  caseFail('cases', status, json);
1226
1313
  if (asJson) {
@@ -1228,7 +1315,7 @@ async function cmdCases(flags) {
1228
1315
  return;
1229
1316
  }
1230
1317
  const rows = (json?.cases ?? []);
1231
- console.log(`Cases in ${tenant}/${project}: ${json?.total ?? rows.length} total`);
1318
+ console.log(`Cases in ${targetLabel(t)}: ${json?.total ?? rows.length} total`);
1232
1319
  if (rows.length === 0)
1233
1320
  console.log(' (none)');
1234
1321
  for (const c of rows) {
@@ -1238,7 +1325,7 @@ async function cmdCases(flags) {
1238
1325
  console.log('\nOne case + timeline: octwin cases <caseId> queues: octwin cases --queues');
1239
1326
  return;
1240
1327
  }
1241
- const { status, json } = await apiGet(`${base}/cases/${encodeURIComponent(caseId)}`, token);
1328
+ const { status, json } = await apiGet(`${base}/cases/${encodeURIComponent(caseId)}`, t);
1242
1329
  if (status === 404)
1243
1330
  die(`case '${caseId}' not found`);
1244
1331
  if (status !== 200)
@@ -1304,7 +1391,8 @@ const COMMAND_HELP = {
1304
1391
  Offline structural check; --remote additionally runs the platform's FULL
1305
1392
  manifest + flow-DSL validation (all errors at once) — same check as deploy.`,
1306
1393
  login: `octwin login --url <platformUrl> --token oct_…
1307
- Save a deploy token (console → Settings → API tokens) for that platform url.`,
1394
+ Save a deploy token (console → Settings → API tokens) for that platform url,
1395
+ and echo the workspace + project pin + scopes the token reaches.`,
1308
1396
  whoami: `octwin whoami [--url <url>] [--tenant <slug>]
1309
1397
  Verify the resolved token authenticates against the tenant.`,
1310
1398
  deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
@@ -1358,7 +1446,7 @@ async function main() {
1358
1446
  await cmdValidate(flags);
1359
1447
  break;
1360
1448
  case 'login':
1361
- cmdLogin(flags);
1449
+ await cmdLogin(flags);
1362
1450
  break;
1363
1451
  case 'whoami':
1364
1452
  await cmdWhoami(flags);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octwin-cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Octwin external-pack developer CLI (by CEQUENS) — scaffold, validate, deploy, and check pure-YAML packs on your tenant.",
5
5
  "type": "module",
6
6
  "bin": {