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.
- package/CHANGELOG.md +23 -0
- package/dist/index.js +174 -86
- 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/
|
|
339
|
-
headers:
|
|
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
|
|
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
|
|
447
|
-
|
|
448
|
-
|
|
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',
|
|
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
|
|
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
|
|
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 +
|
|
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
|
|
502
|
-
const project = flags.project
|
|
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
|
|
510
|
-
return { url, tenant, project
|
|
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
|
|
513
|
-
*
|
|
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
|
|
519
|
-
const project = flags.project
|
|
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 || !
|
|
554
|
+
if (!url || !token)
|
|
522
555
|
return null;
|
|
523
|
-
return { url, tenant, project
|
|
556
|
+
return { url, token, tenant, project };
|
|
524
557
|
}
|
|
525
558
|
async function cmdWhoami(flags) {
|
|
526
559
|
const packDir = resolve(flags.dir ?? '.');
|
|
527
|
-
const
|
|
528
|
-
console.log(`→ Checking the
|
|
529
|
-
const res = await fetchOrDie(`${url}/api/
|
|
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
|
-
|
|
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
|
|
571
|
+
: res.status === 403 ? 'token not authorized for this workspace'
|
|
536
572
|
: await res.text();
|
|
537
|
-
die(`token check failed
|
|
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
|
-
|
|
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,
|
|
580
|
-
console.log(`✓ Deployed ${id}@${version} and installed onto ${
|
|
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
|
|
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
|
|
646
|
+
const t = resolveTarget(flags, packDir);
|
|
647
|
+
const { url } = t;
|
|
602
648
|
const { id, version, files } = localValidate(packDir);
|
|
603
|
-
const endpoint = `${url}/api/
|
|
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 ${
|
|
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',
|
|
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,
|
|
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,
|
|
693
|
+
printDeploySuccess(id, version, t, json);
|
|
639
694
|
}
|
|
640
695
|
async function cmdStatus(flags) {
|
|
641
696
|
const packDir = resolve(flags.dir ?? '.');
|
|
642
|
-
const
|
|
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 ${
|
|
652
|
-
const res = await fetchOrDie(`${url}/api/
|
|
653
|
-
headers:
|
|
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 ${
|
|
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 ${
|
|
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
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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,
|
|
759
|
-
const res = await fetchOrDie(endpoint, { headers:
|
|
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
|
|
774
|
-
const
|
|
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 ${
|
|
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`,
|
|
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 ${
|
|
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}`,
|
|
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)}`,
|
|
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
|
|
834
|
-
const
|
|
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 ${
|
|
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`,
|
|
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 ${
|
|
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)}`,
|
|
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
|
|
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
|
|
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
|
|
1148
|
-
const res = await fetchOrDie(`${url}/api/
|
|
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',
|
|
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
|
|
1195
|
-
const
|
|
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 ${
|
|
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`,
|
|
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 ${
|
|
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}`,
|
|
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 ${
|
|
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)}`,
|
|
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