@olonjs/cli 3.0.157 → 3.0.158

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.
@@ -1817,9 +1817,9 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
1817
1817
  "@tiptap/extension-link": "^2.11.5",
1818
1818
  "@tiptap/react": "^2.11.5",
1819
1819
  "@tiptap/starter-kit": "^2.11.5",
1820
- "@olonjs/core": "^1.1.25",
1821
- "@olonjs/react": "^0.1.8",
1822
- "@olonjs/studio": "^0.1.8",
1820
+ "@olonjs/core": "^1.1.26",
1821
+ "@olonjs/react": "^0.1.9",
1822
+ "@olonjs/studio": "^0.1.9",
1823
1823
  "class-variance-authority": "^0.7.1",
1824
1824
  "clsx": "^2.1.1",
1825
1825
  "lucide-react": "^0.474.0",
@@ -1817,9 +1817,9 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
1817
1817
  "@tiptap/extension-link": "^2.11.5",
1818
1818
  "@tiptap/react": "^2.11.5",
1819
1819
  "@tiptap/starter-kit": "^2.11.5",
1820
- "@olonjs/core": "^1.1.25",
1821
- "@olonjs/react": "^0.1.8",
1822
- "@olonjs/studio": "^0.1.8",
1820
+ "@olonjs/core": "^1.1.26",
1821
+ "@olonjs/react": "^0.1.9",
1822
+ "@olonjs/studio": "^0.1.9",
1823
1823
  "class-variance-authority": "^0.7.1",
1824
1824
  "clsx": "^2.1.1",
1825
1825
  "lucide-react": "^0.474.0",
@@ -491,10 +491,10 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
491
491
  "dist:dna": "npm run dist"
492
492
  },
493
493
  "dependencies": {
494
- "@olonjs/core": "^1.1.25",
495
- "@olonjs/next": "^0.0.5",
496
- "@olonjs/react": "^0.1.8",
497
- "@olonjs/studio": "^0.1.8",
494
+ "@olonjs/core": "^1.1.26",
495
+ "@olonjs/next": "^0.0.6",
496
+ "@olonjs/react": "^0.1.9",
497
+ "@olonjs/studio": "^0.1.9",
498
498
  "clsx": "^2.1.1",
499
499
  "lucide-react": "^0.474.0",
500
500
  "next": "^15.5.0",
@@ -529,6 +529,151 @@ export default config;
529
529
 
530
530
  END_OF_FILE_CONTENT
531
531
  mkdir -p "scripts"
532
+ echo "Creating scripts/generate-SystemsArchitect-next.test.mjs..."
533
+ cat << 'END_OF_FILE_CONTENT' > "scripts/generate-SystemsArchitect-next.test.mjs"
534
+ /**
535
+ * Static gates for generate_SystemsArchitect_next.sh (TDD for the Next SystemsArchitect generator).
536
+ * Run: node --test scripts/generate-SystemsArchitect-next.test.mjs
537
+ */
538
+ import assert from 'node:assert/strict';
539
+ import fs from 'node:fs';
540
+ import path from 'node:path';
541
+ import { describe, it } from 'node:test';
542
+ import { fileURLToPath } from 'node:url';
543
+
544
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
545
+ const SCRIPT = path.resolve(__dirname, '../templates/generate_SystemsArchitect_next.sh');
546
+
547
+ function readScript() {
548
+ assert.ok(fs.existsSync(SCRIPT), `missing ${SCRIPT}`);
549
+ return fs.readFileSync(SCRIPT, 'utf8');
550
+ }
551
+
552
+ describe('generate_SystemsArchitect_next.sh harness gates', () => {
553
+ it('exists and is a bash script', () => {
554
+ const src = readScript();
555
+ assert.match(src, /^#!\/usr\/bin\/env bash|^#!\/bin\/bash/m);
556
+ });
557
+
558
+ it('must not target Vite-only surfaces', () => {
559
+ const src = readScript();
560
+ assert.doesNotMatch(src, /cat > index\.html/);
561
+ assert.doesNotMatch(src, /cat > src\/index\.css/);
562
+ assert.doesNotMatch(src, /cat > src\/App\.tsx/);
563
+ assert.doesNotMatch(src, /from ['"]@\/components\/ThemeProvider['"]/);
564
+ assert.doesNotMatch(src, /useTheme\s*\(/);
565
+ });
566
+
567
+ it('must write Next theme bridge to app/globals.css', () => {
568
+ const src = readScript();
569
+ assert.match(src, /cat > app\/globals\.css/);
570
+ assert.match(src, /\[data-theme=["']light["']\]/);
571
+ });
572
+
573
+ it('must verify Next admin wiring instead of App.tsx', () => {
574
+ const src = readScript();
575
+ assert.match(src, /AdminStudioClient/);
576
+ assert.doesNotMatch(src, /verifying App\.tsx/);
577
+ });
578
+
579
+ it('must cd to tenant root (parent of templates/) before writing files', () => {
580
+ const src = readScript();
581
+ assert.match(src, /cd "\$\(cd "\$\(dirname "\$\{BASH_SOURCE\[0\]\}"\)\/\.\." && pwd\)"/);
582
+ });
583
+
584
+ it('must clean DNA demo capsules before writing SystemsArchitect', () => {
585
+ const src = readScript();
586
+ assert.match(src, /Cleaning demo capsules/);
587
+ assert.match(src, /rm -rf \\\s*\n\s*src\/components\/books-list/m);
588
+ assert.match(src, /cat > src\/lib\/VisitorSection\.tsx/);
589
+ assert.doesNotMatch(src, /from '@\/components\/books-list'/);
590
+ });
591
+
592
+ it('must install react-markdown deps used by post-detail', () => {
593
+ const src = readScript();
594
+ assert.match(src, /npm install[^\n]*react-markdown/);
595
+ assert.match(src, /from 'react-markdown'/);
596
+ assert.match(src, /from 'remark-gfm'/);
597
+ assert.match(src, /from 'rehype-sanitize'/);
598
+ });
599
+
600
+ it('must not generate empty-tenant registry wiring (cleanup rm path ok)', () => {
601
+ const src = readScript();
602
+ assert.doesNotMatch(src, /@\/components\/empty-tenant/);
603
+ assert.doesNotMatch(src, /['"]empty-tenant['"]/);
604
+ assert.match(src, /src\/components\/empty-tenant/);
605
+ });
606
+
607
+ it('must force shadcn radix base non-interactively', () => {
608
+ const src = readScript();
609
+ assert.match(src, /shadcn@latest init[^\n]*--base radix/);
610
+ assert.match(src, /shadcn@latest init[^\n]*--defaults/);
611
+ });
612
+ });
613
+
614
+ END_OF_FILE_CONTENT
615
+ echo "Creating scripts/generate-inkwell-next.test.mjs..."
616
+ cat << 'END_OF_FILE_CONTENT' > "scripts/generate-inkwell-next.test.mjs"
617
+ /**
618
+ * Static gates for generate_inkwell_next.sh (TDD for the Next Inkwell generator).
619
+ * Run: node --test scripts/generate-inkwell-next.test.mjs
620
+ */
621
+ import assert from 'node:assert/strict';
622
+ import fs from 'node:fs';
623
+ import path from 'node:path';
624
+ import { describe, it } from 'node:test';
625
+ import { fileURLToPath } from 'node:url';
626
+
627
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
628
+ const SCRIPT = path.resolve(__dirname, '../templates/generate_inkwell_next.sh');
629
+
630
+ function readScript() {
631
+ assert.ok(fs.existsSync(SCRIPT), `missing ${SCRIPT}`);
632
+ return fs.readFileSync(SCRIPT, 'utf8');
633
+ }
634
+
635
+ describe('generate_inkwell_next.sh harness gates', () => {
636
+ it('exists and is a bash script', () => {
637
+ const src = readScript();
638
+ assert.match(src, /^#!\/usr\/bin\/env bash|^#!\/bin\/bash/m);
639
+ });
640
+
641
+ it('must not target Vite-only surfaces', () => {
642
+ const src = readScript();
643
+ assert.doesNotMatch(src, /cat > index\.html/);
644
+ assert.doesNotMatch(src, /cat > src\/index\.css/);
645
+ assert.doesNotMatch(src, /cat > src\/App\.tsx/);
646
+ assert.doesNotMatch(src, /from ['"]@\/components\/ThemeProvider['"]/);
647
+ assert.doesNotMatch(src, /useTheme\s*\(/);
648
+ });
649
+
650
+ it('must write Next theme bridge to app/globals.css', () => {
651
+ const src = readScript();
652
+ assert.match(src, /cat > app\/globals\.css/);
653
+ assert.match(src, /\[data-theme=["']light["']\]/);
654
+ });
655
+
656
+ it('must verify Next admin wiring instead of App.tsx', () => {
657
+ const src = readScript();
658
+ assert.match(src, /AdminStudioClient/);
659
+ assert.doesNotMatch(src, /verifying App\.tsx/);
660
+ });
661
+
662
+ it('must cd to tenant root (parent of templates/) before writing files', () => {
663
+ const src = readScript();
664
+ assert.match(src, /cd "\$\(cd "\$\(dirname "\$\{BASH_SOURCE\[0\]\}"\)\/\.\." && pwd\)"/);
665
+ });
666
+
667
+ it('must clean DNA demo capsules before writing Inkwell', () => {
668
+ const src = readScript();
669
+ assert.match(src, /Cleaning demo capsules/);
670
+ assert.match(src, /rm -rf \\\s*\n[\s\S]*?src\/components\/books-list/m);
671
+ assert.match(src, /cat > src\/lib\/VisitorSection\.tsx/);
672
+ assert.doesNotMatch(src, /from '@\/components\/books-list'/);
673
+ });
674
+ });
675
+
676
+ END_OF_FILE_CONTENT
532
677
  echo "Creating scripts/sync-pages-to-public.mjs..."
533
678
  cat << 'END_OF_FILE_CONTENT' > "scripts/sync-pages-to-public.mjs"
534
679
  import fs from 'fs';
@@ -580,6 +725,7 @@ console.log('[sync-pages-to-public] Synced pages, collections, and site config t
580
725
 
581
726
  END_OF_FILE_CONTENT
582
727
  mkdir -p "src"
728
+ # SKIP: src/App.tsx is binary and cannot be embedded as text.
583
729
  mkdir -p "src/collections"
584
730
  mkdir -p "src/collections/autori"
585
731
  echo "Creating src/collections/autori/index.ts..."
@@ -827,7 +973,7 @@ import type {
827
973
  } from '@olonjs/core';
828
974
  import { AdminStudioClient } from '@/components/admin/AdminStudioClient';
829
975
  import { useCloudSave } from '@/lib/admin/useCloudSave';
830
- import { cloudPolicy, TENANT_ID } from '@/lib/env/tenantEnv';
976
+ import { getCloudPolicy, TENANT_ID } from '@/lib/env/tenantEnv';
831
977
 
832
978
  const ColdSaveDrawer = lazy(() =>
833
979
  import('@/components/admin/ColdSaveDrawer').then((m) => ({ default: m.ColdSaveDrawer })),
@@ -846,6 +992,7 @@ export type AdminStudioWithCloudProps = {
846
992
  * Local save when cloud credentials are absent; cold save when Save2Repo is enabled.
847
993
  */
848
994
  export function AdminStudioWithCloud(props: AdminStudioWithCloudProps) {
995
+ const cloudPolicy = getCloudPolicy();
849
996
  const { cloudSaveUi, runCloudSave, closeCloudDrawer, retryCloudSave } = useCloudSave({
850
997
  apiUrl: cloudPolicy.apiUrl,
851
998
  apiKey: cloudPolicy.apiKey,
@@ -2766,6 +2913,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/libri/[slug].json"
2766
2913
  }
2767
2914
 
2768
2915
  END_OF_FILE_CONTENT
2916
+ # SKIP: src/index.css is binary and cannot be embedded as text.
2769
2917
  mkdir -p "src/lib"
2770
2918
  echo "Creating src/lib/CollectionRegistry.ts..."
2771
2919
  cat << 'END_OF_FILE_CONTENT' > "src/lib/CollectionRegistry.ts"
@@ -3458,6 +3606,58 @@ export function buildServerApiCandidates(raw: string): string[] {
3458
3606
  return Array.from(new Set([withApi, base]));
3459
3607
  }
3460
3608
 
3609
+ END_OF_FILE_CONTENT
3610
+ echo "Creating src/lib/env/tenantEnv.test.ts..."
3611
+ cat << 'END_OF_FILE_CONTENT' > "src/lib/env/tenantEnv.test.ts"
3612
+ import assert from 'node:assert/strict';
3613
+ import { describe, it } from 'vitest';
3614
+ import { getCloudPolicy, readCloudEnvFromNext } from './tenantEnv';
3615
+
3616
+ describe('readCloudEnvFromNext', () => {
3617
+ it('maps OLONJS public env to cloud + Save2Repo policy inputs', () => {
3618
+ const input = readCloudEnvFromNext({
3619
+ NEXT_PUBLIC_OLONJS_CLOUD_URL: ' https://api.example.com ',
3620
+ NEXT_PUBLIC_OLONJS_API_KEY: ' key ',
3621
+ NEXT_PUBLIC_OLONJS_SAVE2REPO: 'true',
3622
+ });
3623
+ assert.equal(input.apiUrl, 'https://api.example.com');
3624
+ assert.equal(input.apiKey, 'key');
3625
+ assert.equal(input.save2RepoFlag, true);
3626
+ });
3627
+
3628
+ it('treats SAVE2REPO other than exact "true" as off', () => {
3629
+ const input = readCloudEnvFromNext({
3630
+ NEXT_PUBLIC_OLONJS_CLOUD_URL: 'https://api.example.com',
3631
+ NEXT_PUBLIC_OLONJS_API_KEY: 'key',
3632
+ NEXT_PUBLIC_OLONJS_SAVE2REPO: '1',
3633
+ });
3634
+ assert.equal(input.save2RepoFlag, false);
3635
+ });
3636
+ });
3637
+
3638
+ describe('getCloudPolicy', () => {
3639
+ it('enables cold save and disables local save when credentials + Save2Repo', () => {
3640
+ const policy = getCloudPolicy({
3641
+ NEXT_PUBLIC_OLONJS_CLOUD_URL: 'https://api.example.com',
3642
+ NEXT_PUBLIC_OLONJS_API_KEY: 'key',
3643
+ NEXT_PUBLIC_OLONJS_SAVE2REPO: 'true',
3644
+ });
3645
+ assert.equal(policy.isCloudMode, true);
3646
+ assert.equal(policy.bootSource, 'static');
3647
+ assert.equal(policy.showLocalSave, false);
3648
+ assert.equal(policy.showColdSave, true);
3649
+ });
3650
+
3651
+ it('stays local when credentials are missing', () => {
3652
+ const policy = getCloudPolicy({
3653
+ NEXT_PUBLIC_OLONJS_SAVE2REPO: 'true',
3654
+ });
3655
+ assert.equal(policy.isCloudMode, false);
3656
+ assert.equal(policy.showLocalSave, true);
3657
+ assert.equal(policy.showColdSave, false);
3658
+ });
3659
+ });
3660
+
3461
3661
  END_OF_FILE_CONTENT
3462
3662
  echo "Creating src/lib/env/tenantEnv.ts..."
3463
3663
  cat << 'END_OF_FILE_CONTENT' > "src/lib/env/tenantEnv.ts"
@@ -3468,18 +3668,43 @@ import { resolveCloudPolicy, type CloudEnvInput, type CloudPolicy } from '@olonj
3468
3668
  * Prefer NEXT_PUBLIC_OLONJS_* ; also accept NEXT_PUBLIC_JSONPAGES_* and
3469
3669
  * NEXT_PUBLIC_SAVE2REPO (alpha-style flag name under the Next public prefix).
3470
3670
  *
3471
- * Note: `import.meta.env` / VITE_* are not available in the Next client bundle
3472
- * unless mirrored via next.config — do not call readCloudEnvFromVite here.
3671
+ * CRITICAL: default path MUST use direct `process.env.NEXT_PUBLIC_*` property
3672
+ * access so the Next client bundler can inline values at build time.
3673
+ * Indexing via `const env = process.env; env.NEXT_PUBLIC_…` stays empty in the
3674
+ * browser → Studio always thinks it is Local → save-to-file → EROFS on Vercel.
3675
+ *
3676
+ * Pass `env` only in unit tests.
3473
3677
  */
3474
3678
  export function readCloudEnvFromNext(
3475
- env: Record<string, string | undefined> = typeof process !== 'undefined' ? process.env : {},
3679
+ env?: Record<string, string | undefined>,
3476
3680
  ): CloudEnvInput {
3477
- const apiUrl =
3478
- (env.NEXT_PUBLIC_OLONJS_CLOUD_URL ?? env.NEXT_PUBLIC_JSONPAGES_CLOUD_URL ?? '').trim();
3479
- const apiKey =
3480
- (env.NEXT_PUBLIC_OLONJS_API_KEY ?? env.NEXT_PUBLIC_JSONPAGES_API_KEY ?? '').trim();
3681
+ if (env) {
3682
+ const apiUrl =
3683
+ (env.NEXT_PUBLIC_OLONJS_CLOUD_URL ?? env.NEXT_PUBLIC_JSONPAGES_CLOUD_URL ?? '').trim();
3684
+ const apiKey =
3685
+ (env.NEXT_PUBLIC_OLONJS_API_KEY ?? env.NEXT_PUBLIC_JSONPAGES_API_KEY ?? '').trim();
3686
+ const save2RepoRaw =
3687
+ env.NEXT_PUBLIC_OLONJS_SAVE2REPO ?? env.NEXT_PUBLIC_SAVE2REPO ?? '';
3688
+ return {
3689
+ apiUrl,
3690
+ apiKey,
3691
+ save2RepoFlag: save2RepoRaw === 'true',
3692
+ };
3693
+ }
3694
+
3695
+ const apiUrl = (
3696
+ process.env.NEXT_PUBLIC_OLONJS_CLOUD_URL ??
3697
+ process.env.NEXT_PUBLIC_JSONPAGES_CLOUD_URL ??
3698
+ ''
3699
+ ).trim();
3700
+ const apiKey = (
3701
+ process.env.NEXT_PUBLIC_OLONJS_API_KEY ??
3702
+ process.env.NEXT_PUBLIC_JSONPAGES_API_KEY ??
3703
+ ''
3704
+ ).trim();
3481
3705
  const save2RepoRaw =
3482
- env.NEXT_PUBLIC_OLONJS_SAVE2REPO ?? env.NEXT_PUBLIC_SAVE2REPO ?? '';
3706
+ process.env.NEXT_PUBLIC_OLONJS_SAVE2REPO ?? process.env.NEXT_PUBLIC_SAVE2REPO ?? '';
3707
+
3483
3708
  return {
3484
3709
  apiUrl,
3485
3710
  apiKey,
@@ -3487,8 +3712,13 @@ export function readCloudEnvFromNext(
3487
3712
  };
3488
3713
  }
3489
3714
 
3490
- /** Single policy path for the Next admin island. */
3491
- export const cloudPolicy: CloudPolicy = resolveCloudPolicy(readCloudEnvFromNext());
3715
+ /** Resolve policy (call from client components; do not cache across env shapes in tests). */
3716
+ export function getCloudPolicy(env?: Record<string, string | undefined>): CloudPolicy {
3717
+ return resolveCloudPolicy(readCloudEnvFromNext(env));
3718
+ }
3719
+
3720
+ /** Module policy for the Next admin island (build-time inlined NEXT_PUBLIC_*). */
3721
+ export const cloudPolicy: CloudPolicy = getCloudPolicy();
3492
3722
 
3493
3723
  export const CLOUD_API_URL = cloudPolicy.apiUrl;
3494
3724
  export const CLOUD_API_KEY = cloudPolicy.apiKey;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olonjs/cli",
3
- "version": "3.0.157",
3
+ "version": "3.0.158",
4
4
  "description": "The Sovereign CLI Engine for OlonJS.",
5
5
  "type": "module",
6
6
  "bin": {