@rs-x/cli 2.0.0-next.20 → 2.0.0-next.21

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/bin/rsx.cjs CHANGED
@@ -211,6 +211,22 @@ function detectPackageManager(explicitPm) {
211
211
  return 'npm';
212
212
  }
213
213
 
214
+ function resolveCliPackageManager(projectRoot, explicitPm) {
215
+ if (explicitPm) {
216
+ return explicitPm;
217
+ }
218
+
219
+ const cliConfig = resolveRsxCliConfig(projectRoot);
220
+ if (
221
+ typeof cliConfig.packageManager === 'string' &&
222
+ ['pnpm', 'npm', 'yarn', 'bun'].includes(cliConfig.packageManager)
223
+ ) {
224
+ return cliConfig.packageManager;
225
+ }
226
+
227
+ return detectPackageManager(undefined);
228
+ }
229
+
214
230
  function applyTagToPackages(packages, tag) {
215
231
  return packages.map((pkg) => {
216
232
  const lastAt = pkg.lastIndexOf('@');
@@ -249,6 +265,41 @@ function resolveInstallTag(flags) {
249
265
  return undefined;
250
266
  }
251
267
 
268
+ function resolveConfiguredInstallTag(projectRoot, flags) {
269
+ const explicitTag = resolveInstallTag(flags);
270
+ if (explicitTag) {
271
+ return explicitTag;
272
+ }
273
+
274
+ const cliConfig = resolveRsxCliConfig(projectRoot);
275
+ if (
276
+ typeof cliConfig.installTag === 'string' &&
277
+ ['latest', 'next'].includes(cliConfig.installTag)
278
+ ) {
279
+ return cliConfig.installTag;
280
+ }
281
+
282
+ return undefined;
283
+ }
284
+
285
+ function resolveCliVerifyFlag(projectRoot, flags, sectionName) {
286
+ if (flags.verify !== undefined) {
287
+ return parseBooleanFlag(flags.verify, false);
288
+ }
289
+
290
+ const cliConfig = resolveRsxCliConfig(projectRoot);
291
+ const sectionConfig = cliConfig[sectionName];
292
+ if (
293
+ typeof sectionConfig === 'object' &&
294
+ sectionConfig &&
295
+ typeof sectionConfig.verify === 'boolean'
296
+ ) {
297
+ return sectionConfig.verify;
298
+ }
299
+
300
+ return false;
301
+ }
302
+
252
303
  function installPackages(pm, packages, options = {}) {
253
304
  const { dev = false, dryRun = false, label = 'packages', tag, cwd } = options;
254
305
  const resolvedPackages = tag ? applyTagToPackages(packages, tag) : packages;
@@ -484,6 +535,13 @@ function findRepoRoot(startDir) {
484
535
  function runDoctor() {
485
536
  const nodeMajor = Number.parseInt(process.versions.node.split('.')[0], 10);
486
537
  const hasCode = hasCommand('code');
538
+ const availablePackageManagers = ['pnpm', 'npm', 'yarn', 'bun'].filter((pm) =>
539
+ hasCommand(pm),
540
+ );
541
+ const packageRoot = findNearestPackageRoot(process.cwd());
542
+ const duplicateRsxPackages = packageRoot
543
+ ? findDuplicateRsxPackages(packageRoot)
544
+ : [];
487
545
  const checks = [
488
546
  {
489
547
  name: 'Node.js >= 20',
@@ -497,12 +555,25 @@ function runDoctor() {
497
555
  },
498
556
  {
499
557
  name: 'Package manager (pnpm/npm/yarn/bun)',
500
- ok:
501
- hasCommand('pnpm') ||
502
- hasCommand('npm') ||
503
- hasCommand('yarn') ||
504
- hasCommand('bun'),
505
- details: 'required for compiler package installation',
558
+ ok: availablePackageManagers.length > 0,
559
+ details:
560
+ availablePackageManagers.length > 0
561
+ ? `available: ${availablePackageManagers.join(', ')}`
562
+ : 'required for compiler package installation',
563
+ },
564
+ {
565
+ name: 'Duplicate @rs-x package versions',
566
+ ok: duplicateRsxPackages.length === 0,
567
+ details: packageRoot
568
+ ? duplicateRsxPackages.length === 0
569
+ ? `not detected in ${path.relative(process.cwd(), packageRoot) || '.'}`
570
+ : duplicateRsxPackages
571
+ .map(
572
+ ({ name, versions }) =>
573
+ `${name} (${Array.from(versions).join(', ')})`,
574
+ )
575
+ .join('; ')
576
+ : 'no package.json found in current directory tree',
506
577
  },
507
578
  ];
508
579
 
@@ -510,6 +581,149 @@ function runDoctor() {
510
581
  const tag = check.ok ? '[OK]' : '[WARN]';
511
582
  console.log(`${tag} ${check.name} - ${check.details}`);
512
583
  }
584
+
585
+ const failingChecks = checks.filter((check) => !check.ok);
586
+ if (failingChecks.length > 0) {
587
+ console.log('');
588
+ console.log('Suggested next steps:');
589
+ for (const check of failingChecks) {
590
+ if (check.name === 'Node.js >= 20') {
591
+ console.log(
592
+ ' - Install Node.js 20 or newer before running `rsx setup` or `rsx project`.',
593
+ );
594
+ } else if (check.name === 'VS Code CLI (code)') {
595
+ console.log(
596
+ ' - Install the VS Code shell command or use `rsx install vscode --force` later.',
597
+ );
598
+ } else if (check.name === 'Package manager (pnpm/npm/yarn/bun)') {
599
+ console.log(
600
+ ' - Install npm, pnpm, yarn, or bun so the CLI can add RS-X packages.',
601
+ );
602
+ } else if (check.name === 'Duplicate @rs-x package versions') {
603
+ console.log(
604
+ ' - Reinstall dependencies so all `@rs-x/*` packages resolve to a single version, then rerun `rsx doctor`.',
605
+ );
606
+ console.log(
607
+ ' - If you are linking local packages, remove nested `node_modules` inside linked RS-X packages.',
608
+ );
609
+ }
610
+ }
611
+ return;
612
+ }
613
+
614
+ console.log('');
615
+ console.log('Suggested next steps:');
616
+ console.log(
617
+ ' - Run `rsx project <framework>` to scaffold a starter, or `rsx setup` inside an existing app.',
618
+ );
619
+ console.log(' - Use `rsx add` to create your first expression file.');
620
+ }
621
+
622
+ function findNearestPackageRoot(startDirectory) {
623
+ let currentDirectory = path.resolve(startDirectory);
624
+ while (true) {
625
+ if (fs.existsSync(path.join(currentDirectory, 'package.json'))) {
626
+ return currentDirectory;
627
+ }
628
+ const parentDirectory = path.dirname(currentDirectory);
629
+ if (parentDirectory === currentDirectory) {
630
+ return null;
631
+ }
632
+ currentDirectory = parentDirectory;
633
+ }
634
+ }
635
+
636
+ function readRsxPackageVersionsFromNodeModules(
637
+ nodeModulesDirectory,
638
+ versionsByPackage,
639
+ ) {
640
+ const scopeDirectory = path.join(nodeModulesDirectory, '@rs-x');
641
+ if (!fs.existsSync(scopeDirectory)) {
642
+ return [];
643
+ }
644
+
645
+ const packageDirectories = fs
646
+ .readdirSync(scopeDirectory, { withFileTypes: true })
647
+ .filter((entry) => entry.isDirectory())
648
+ .map((entry) => path.join(scopeDirectory, entry.name));
649
+
650
+ for (const packageDirectory of packageDirectories) {
651
+ const packageJsonPath = path.join(packageDirectory, 'package.json');
652
+ if (!fs.existsSync(packageJsonPath)) {
653
+ continue;
654
+ }
655
+
656
+ try {
657
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
658
+ const packageName = packageJson.name;
659
+ const packageVersion = packageJson.version;
660
+ if (
661
+ typeof packageName === 'string' &&
662
+ typeof packageVersion === 'string'
663
+ ) {
664
+ if (!versionsByPackage.has(packageName)) {
665
+ versionsByPackage.set(packageName, new Set());
666
+ }
667
+ versionsByPackage.get(packageName).add(packageVersion);
668
+ }
669
+ } catch {
670
+ // Ignore malformed package.json files during doctor output.
671
+ }
672
+ }
673
+
674
+ return packageDirectories;
675
+ }
676
+
677
+ function collectNestedRsxPackageVersions(
678
+ packageDirectory,
679
+ versionsByPackage,
680
+ depth,
681
+ ) {
682
+ if (depth <= 0) {
683
+ return;
684
+ }
685
+
686
+ const nestedNodeModulesDirectory = path.join(
687
+ packageDirectory,
688
+ 'node_modules',
689
+ );
690
+ if (!fs.existsSync(nestedNodeModulesDirectory)) {
691
+ return;
692
+ }
693
+
694
+ const nestedPackageDirectories = readRsxPackageVersionsFromNodeModules(
695
+ nestedNodeModulesDirectory,
696
+ versionsByPackage,
697
+ );
698
+
699
+ for (const nestedPackageDirectory of nestedPackageDirectories) {
700
+ collectNestedRsxPackageVersions(
701
+ nestedPackageDirectory,
702
+ versionsByPackage,
703
+ depth - 1,
704
+ );
705
+ }
706
+ }
707
+
708
+ function findDuplicateRsxPackages(projectRoot) {
709
+ const rootNodeModulesDirectory = path.join(projectRoot, 'node_modules');
710
+ if (!fs.existsSync(rootNodeModulesDirectory)) {
711
+ return [];
712
+ }
713
+
714
+ const versionsByPackage = new Map();
715
+ const rootPackageDirectories = readRsxPackageVersionsFromNodeModules(
716
+ rootNodeModulesDirectory,
717
+ versionsByPackage,
718
+ );
719
+
720
+ for (const packageDirectory of rootPackageDirectories) {
721
+ collectNestedRsxPackageVersions(packageDirectory, versionsByPackage, 3);
722
+ }
723
+
724
+ return Array.from(versionsByPackage.entries())
725
+ .filter(([, versions]) => versions.size > 1)
726
+ .map(([name, versions]) => ({ name, versions }));
513
727
  }
514
728
 
515
729
  function isValidTsIdentifier(input) {
@@ -587,25 +801,311 @@ function stripTsLikeExtension(fileName) {
587
801
  return fileName.replace(/\.[cm]?[jt]sx?$/u, '');
588
802
  }
589
803
 
590
- function createModelTemplate() {
804
+ function inferModelKeysFromExpression(expressionSource) {
805
+ const matches = expressionSource.matchAll(
806
+ /(?<![\w$.])([A-Za-z_$][A-Za-z0-9_$]*)(?![\w$])/gu,
807
+ );
808
+ const identifiers = [];
809
+ const ignoredIdentifiers = new Set([
810
+ 'undefined',
811
+ 'null',
812
+ 'true',
813
+ 'false',
814
+ 'Math',
815
+ 'Date',
816
+ 'Number',
817
+ 'String',
818
+ 'Boolean',
819
+ 'Array',
820
+ 'Object',
821
+ 'JSON',
822
+ 'console',
823
+ ]);
824
+
825
+ for (const match of matches) {
826
+ const identifier = match[1];
827
+ if (
828
+ TS_RESERVED_WORDS.has(identifier) ||
829
+ ignoredIdentifiers.has(identifier)
830
+ ) {
831
+ continue;
832
+ }
833
+ if (!identifiers.includes(identifier)) {
834
+ identifiers.push(identifier);
835
+ }
836
+ }
837
+
838
+ return identifiers;
839
+ }
840
+
841
+ function createModelTemplate(expressionSource) {
842
+ const modelKeys = inferModelKeysFromExpression(expressionSource);
843
+ const modelBody =
844
+ modelKeys.length > 0
845
+ ? modelKeys.map((key) => ` ${key}: 1,`).join('\n')
846
+ : ' a: 1,';
847
+
591
848
  return `export const model = {
592
- a: 1,
849
+ ${modelBody}
593
850
  };
594
851
  `;
595
852
  }
596
853
 
854
+ function createInlineExpressionTemplate(expressionName, expressionSource) {
855
+ return `import { rsx } from '@rs-x/expression-parser';
856
+
857
+ ${createModelTemplate(expressionSource).trim()}
858
+
859
+ export const ${expressionName} = rsx(${JSON.stringify(expressionSource)})(model);
860
+ `;
861
+ }
862
+
863
+ function createInlineExpressionAppendTemplate(
864
+ expressionName,
865
+ expressionSource,
866
+ fileContent,
867
+ ) {
868
+ const hasRsxImport = fileContent.includes("from '@rs-x/expression-parser'");
869
+ const hasModelExport = /\bexport\s+const\s+model\s*=/u.test(fileContent);
870
+ const sections = [];
871
+
872
+ if (!hasRsxImport) {
873
+ sections.push("import { rsx } from '@rs-x/expression-parser';");
874
+ }
875
+
876
+ if (!hasModelExport) {
877
+ sections.push(createModelTemplate(expressionSource).trim());
878
+ }
879
+
880
+ sections.push(
881
+ `export const ${expressionName} = rsx(${JSON.stringify(expressionSource)})(model);`,
882
+ );
883
+ return `\n${sections.join('\n\n')}\n`;
884
+ }
885
+
597
886
  function createExpressionTemplate(
598
887
  expressionName,
888
+ expressionSource,
599
889
  modelImportPath,
600
890
  modelExportName,
601
891
  ) {
602
892
  return `import { rsx } from '@rs-x/expression-parser';
603
893
  import { ${modelExportName} } from '${modelImportPath}';
604
894
 
605
- export const ${expressionName} = rsx('a')(${modelExportName});
895
+ export const ${expressionName} = rsx(${JSON.stringify(expressionSource)})(${modelExportName});
606
896
  `;
607
897
  }
608
898
 
899
+ function findExpressionFiles(searchRoot) {
900
+ const results = [];
901
+ const expressionPattern =
902
+ /\b(?:export\s+)?const\s+[A-Za-z_$][\w$]*\s*=\s*rsx\(/u;
903
+
904
+ function visit(currentPath) {
905
+ if (!fs.existsSync(currentPath)) {
906
+ return;
907
+ }
908
+
909
+ const stat = fs.statSync(currentPath);
910
+ if (stat.isDirectory()) {
911
+ const baseName = path.basename(currentPath);
912
+ if (
913
+ baseName === 'node_modules' ||
914
+ baseName === 'dist' ||
915
+ baseName === 'build' ||
916
+ baseName === '.git' ||
917
+ baseName === '.next' ||
918
+ baseName === 'coverage' ||
919
+ baseName === '.tests' ||
920
+ baseName === 'tmp'
921
+ ) {
922
+ return;
923
+ }
924
+
925
+ for (const entry of fs.readdirSync(currentPath)) {
926
+ visit(path.join(currentPath, entry));
927
+ }
928
+ return;
929
+ }
930
+
931
+ if (!/\.[cm]?[jt]sx?$/u.test(currentPath)) {
932
+ return;
933
+ }
934
+
935
+ const content = fs.readFileSync(currentPath, 'utf8');
936
+ if (
937
+ content.includes("from '@rs-x/expression-parser'") &&
938
+ expressionPattern.test(content)
939
+ ) {
940
+ results.push(currentPath);
941
+ }
942
+ }
943
+
944
+ visit(searchRoot);
945
+ return results.sort((left, right) => left.localeCompare(right));
946
+ }
947
+
948
+ function resolveRsxCliAddConfig(projectRoot) {
949
+ const cliConfig = resolveRsxCliConfig(projectRoot);
950
+ const addConfig = cliConfig.add ?? {};
951
+ const defaultSearchRoots = ['src', 'app', 'expressions'];
952
+ const configuredSearchRoots = Array.isArray(addConfig.searchRoots)
953
+ ? addConfig.searchRoots.filter(
954
+ (value) => typeof value === 'string' && value.trim() !== '',
955
+ )
956
+ : defaultSearchRoots;
957
+ const defaultDirectory =
958
+ typeof addConfig.defaultDirectory === 'string' &&
959
+ addConfig.defaultDirectory.trim() !== ''
960
+ ? addConfig.defaultDirectory.trim()
961
+ : 'src/expressions';
962
+
963
+ return {
964
+ defaultDirectory,
965
+ searchRoots: configuredSearchRoots,
966
+ };
967
+ }
968
+
969
+ function filterPreferredExpressionFiles(candidates, projectRoot, addConfig) {
970
+ const preferredRootPrefixes = addConfig.searchRoots
971
+ .map((rootPath) =>
972
+ path.isAbsolute(rootPath) ? rootPath : path.join(projectRoot, rootPath),
973
+ )
974
+ .map((value) => `${value.replace(/\\/gu, '/')}/`);
975
+
976
+ const preferredCandidates = candidates.filter((candidate) => {
977
+ const normalizedCandidate = candidate.replace(/\\/gu, '/');
978
+ return preferredRootPrefixes.some((prefix) =>
979
+ normalizedCandidate.startsWith(prefix),
980
+ );
981
+ });
982
+
983
+ return preferredCandidates.length > 0 ? preferredCandidates : candidates;
984
+ }
985
+
986
+ function rankExpressionFiles(candidates, resolvedDirectory, projectRoot) {
987
+ const sourceRootPatterns = ['/src/', '/app/', '/expressions/'];
988
+
989
+ function score(filePath) {
990
+ const normalized = filePath.replace(/\\/gu, '/');
991
+ let value = 0;
992
+
993
+ if (normalized.startsWith(resolvedDirectory.replace(/\\/gu, '/'))) {
994
+ value += 1000;
995
+ }
996
+
997
+ if (sourceRootPatterns.some((pattern) => normalized.includes(pattern))) {
998
+ value += 100;
999
+ }
1000
+
1001
+ const relativeSegments = path
1002
+ .relative(projectRoot, filePath)
1003
+ .replace(/\\/gu, '/')
1004
+ .split('/').length;
1005
+ value -= relativeSegments;
1006
+ return value;
1007
+ }
1008
+
1009
+ return [...candidates].sort((left, right) => {
1010
+ const scoreDifference = score(right) - score(left);
1011
+ if (scoreDifference !== 0) {
1012
+ return scoreDifference;
1013
+ }
1014
+ return left.localeCompare(right);
1015
+ });
1016
+ }
1017
+
1018
+ async function askForExistingExpressionFile(rl, resolvedDirectory) {
1019
+ const projectRoot = process.cwd();
1020
+ const addConfig = resolveRsxCliAddConfig(projectRoot);
1021
+ const directoryCandidates = filterPreferredExpressionFiles(
1022
+ findExpressionFiles(resolvedDirectory),
1023
+ projectRoot,
1024
+ addConfig,
1025
+ );
1026
+ const fallbackCandidates =
1027
+ directoryCandidates.length > 0
1028
+ ? []
1029
+ : rankExpressionFiles(
1030
+ filterPreferredExpressionFiles(
1031
+ findExpressionFiles(projectRoot),
1032
+ projectRoot,
1033
+ addConfig,
1034
+ ),
1035
+ resolvedDirectory,
1036
+ projectRoot,
1037
+ );
1038
+ const candidates =
1039
+ directoryCandidates.length > 0 ? directoryCandidates : fallbackCandidates;
1040
+
1041
+ if (candidates.length > 0) {
1042
+ console.log(
1043
+ directoryCandidates.length > 0
1044
+ ? 'Existing expression files in the selected directory:'
1045
+ : 'Existing expression files in the current project:',
1046
+ );
1047
+ candidates.forEach((candidate, index) => {
1048
+ console.log(
1049
+ ` ${index + 1}. ${path.relative(process.cwd(), candidate).replace(/\\/gu, '/')}`,
1050
+ );
1051
+ });
1052
+ console.log(' 0. Enter a custom path');
1053
+ }
1054
+
1055
+ while (true) {
1056
+ const answer = (
1057
+ await rl.question(
1058
+ candidates.length > 0
1059
+ ? 'Choose a file number or enter a file path: '
1060
+ : 'Existing file path (relative to output dir or absolute): ',
1061
+ )
1062
+ ).trim();
1063
+
1064
+ if (!answer && candidates.length > 0) {
1065
+ logWarn('Please choose a file or enter a path.');
1066
+ continue;
1067
+ }
1068
+
1069
+ if (candidates.length > 0 && /^\d+$/u.test(answer)) {
1070
+ const selectedIndex = Number.parseInt(answer, 10);
1071
+ if (selectedIndex === 0) {
1072
+ const customPath = await askUntilNonEmpty(
1073
+ rl,
1074
+ 'Existing file path (relative to output dir or absolute): ',
1075
+ );
1076
+ const resolvedPath = path.isAbsolute(customPath)
1077
+ ? customPath
1078
+ : path.resolve(resolvedDirectory, customPath);
1079
+
1080
+ if (!fs.existsSync(resolvedPath)) {
1081
+ logWarn(`File not found: ${resolvedPath}`);
1082
+ continue;
1083
+ }
1084
+
1085
+ return resolvedPath;
1086
+ }
1087
+
1088
+ if (selectedIndex >= 1 && selectedIndex <= candidates.length) {
1089
+ return candidates[selectedIndex - 1];
1090
+ }
1091
+
1092
+ logWarn(`"${answer}" is not a valid file number.`);
1093
+ continue;
1094
+ }
1095
+
1096
+ const resolvedPath = path.isAbsolute(answer)
1097
+ ? answer
1098
+ : path.resolve(resolvedDirectory, answer);
1099
+
1100
+ if (!fs.existsSync(resolvedPath)) {
1101
+ logWarn(`File not found: ${resolvedPath}`);
1102
+ continue;
1103
+ }
1104
+
1105
+ return resolvedPath;
1106
+ }
1107
+ }
1108
+
609
1109
  async function askForIdentifierWithDefault(rl, prompt, defaultValue) {
610
1110
  while (true) {
611
1111
  const answer = (await rl.question(prompt)).trim();
@@ -621,6 +1121,64 @@ async function askForIdentifierWithDefault(rl, prompt, defaultValue) {
621
1121
  }
622
1122
  }
623
1123
 
1124
+ async function askForExpressionSource(rl) {
1125
+ while (true) {
1126
+ const answer = (
1127
+ await rl.question("Initial expression string ['a']: ")
1128
+ ).trim();
1129
+
1130
+ if (!answer) {
1131
+ return 'a';
1132
+ }
1133
+
1134
+ return answer;
1135
+ }
1136
+ }
1137
+
1138
+ async function askForDirectoryPath(rl, defaultDirectory) {
1139
+ while (true) {
1140
+ const prompt = defaultDirectory
1141
+ ? `Directory path (relative or absolute) [${defaultDirectory}]: `
1142
+ : 'Directory path (relative or absolute): ';
1143
+ const answer = (await rl.question(prompt)).trim();
1144
+
1145
+ if (answer) {
1146
+ return answer;
1147
+ }
1148
+
1149
+ if (defaultDirectory) {
1150
+ return defaultDirectory;
1151
+ }
1152
+
1153
+ logWarn('Please enter a directory path.');
1154
+ }
1155
+ }
1156
+
1157
+ async function askForAddMode(rl) {
1158
+ console.log('Choose add mode:');
1159
+ console.log(
1160
+ ' 1. Create new expression file (model in same file) [Recommended]',
1161
+ );
1162
+ console.log(' 2. Create new expression file with separate model');
1163
+ console.log(' 3. Update an existing expression file');
1164
+
1165
+ while (true) {
1166
+ const answer = (await rl.question('Add mode [1]: ')).trim();
1167
+
1168
+ if (!answer || answer === '1') {
1169
+ return 'create-inline';
1170
+ }
1171
+ if (answer === '2') {
1172
+ return 'create-separate';
1173
+ }
1174
+ if (answer === '3') {
1175
+ return 'update-existing';
1176
+ }
1177
+
1178
+ logWarn(`"${answer}" is not a valid add mode. Use 1, 2, or 3.`);
1179
+ }
1180
+ }
1181
+
624
1182
  async function runAdd() {
625
1183
  const rl = readline.createInterface({
626
1184
  input: process.stdin,
@@ -628,18 +1186,21 @@ async function runAdd() {
628
1186
  });
629
1187
 
630
1188
  try {
1189
+ const projectRoot = process.cwd();
1190
+ const addConfig = resolveRsxCliAddConfig(projectRoot);
631
1191
  const expressionName = await askUntilValidIdentifier(rl);
1192
+ const expressionSource = await askForExpressionSource(rl);
632
1193
 
633
1194
  const kebabAnswer = await rl.question('Use kebab-case file name? [Y/n]: ');
634
1195
  const useKebabCase = normalizeYesNo(kebabAnswer, true);
635
1196
 
636
- const directoryInput = await askUntilNonEmpty(
1197
+ const directoryInput = await askForDirectoryPath(
637
1198
  rl,
638
- 'Directory path (relative or absolute): ',
1199
+ addConfig.defaultDirectory,
639
1200
  );
640
1201
  const resolvedDirectory = path.isAbsolute(directoryInput)
641
1202
  ? directoryInput
642
- : path.resolve(process.cwd(), directoryInput);
1203
+ : path.resolve(projectRoot, directoryInput);
643
1204
 
644
1205
  const baseFileName = useKebabCase
645
1206
  ? toKebabCase(expressionName)
@@ -649,14 +1210,93 @@ async function runAdd() {
649
1210
  const modelFileName = `${expressionFileBase}.model.ts`;
650
1211
  const expressionPath = path.join(resolvedDirectory, expressionFileName);
651
1212
  const modelPath = path.join(resolvedDirectory, modelFileName);
652
- const useExistingModelAnswer = await rl.question(
653
- 'Use existing model file? [y/N]: ',
654
- );
655
- const useExistingModel = normalizeYesNo(useExistingModelAnswer, false);
1213
+ const addMode = await askForAddMode(rl);
1214
+ const updateExistingFile = addMode === 'update-existing';
1215
+ const keepModelInSameFile =
1216
+ addMode === 'create-inline'
1217
+ ? true
1218
+ : addMode === 'create-separate'
1219
+ ? false
1220
+ : normalizeYesNo(
1221
+ await rl.question('Keep model in the same file? [Y/n]: '),
1222
+ true,
1223
+ );
1224
+
1225
+ if (updateExistingFile) {
1226
+ const resolvedExistingFilePath = await askForExistingExpressionFile(
1227
+ rl,
1228
+ resolvedDirectory,
1229
+ );
1230
+ const existingFileContent = fs.readFileSync(
1231
+ resolvedExistingFilePath,
1232
+ 'utf8',
1233
+ );
1234
+ let appendContent;
1235
+
1236
+ if (keepModelInSameFile) {
1237
+ appendContent = createInlineExpressionAppendTemplate(
1238
+ expressionName,
1239
+ expressionSource,
1240
+ existingFileContent,
1241
+ );
1242
+ } else {
1243
+ const useExistingModelAnswer = await rl.question(
1244
+ 'Use existing model file? [y/N]: ',
1245
+ );
1246
+ const useExistingModel = normalizeYesNo(useExistingModelAnswer, false);
1247
+ let modelImportPath = './model';
1248
+ let modelExportName = 'model';
1249
+
1250
+ if (useExistingModel) {
1251
+ const existingModelPathInput = await askUntilNonEmpty(
1252
+ rl,
1253
+ 'Existing model file path (relative to output dir or absolute): ',
1254
+ );
1255
+ const resolvedExistingModelPath = path.isAbsolute(
1256
+ existingModelPathInput,
1257
+ )
1258
+ ? existingModelPathInput
1259
+ : path.resolve(resolvedDirectory, existingModelPathInput);
1260
+
1261
+ if (!fs.existsSync(resolvedExistingModelPath)) {
1262
+ logError(`Model file not found: ${resolvedExistingModelPath}`);
1263
+ return;
1264
+ }
1265
+
1266
+ modelImportPath = toModuleImportPath(
1267
+ resolvedExistingFilePath,
1268
+ resolvedExistingModelPath,
1269
+ );
1270
+ modelExportName = await askForIdentifierWithDefault(
1271
+ rl,
1272
+ 'Model export name [model]: ',
1273
+ 'model',
1274
+ );
1275
+ }
1276
+
1277
+ appendContent = `\n${createExpressionTemplate(
1278
+ expressionName,
1279
+ expressionSource,
1280
+ modelImportPath,
1281
+ modelExportName,
1282
+ )}`;
1283
+ }
1284
+
1285
+ fs.appendFileSync(resolvedExistingFilePath, appendContent, 'utf8');
1286
+ logOk(`Updated ${resolvedExistingFilePath}`);
1287
+ return;
1288
+ }
1289
+
1290
+ const useExistingModel = !keepModelInSameFile
1291
+ ? normalizeYesNo(
1292
+ await rl.question('Use existing model file? [y/N]: '),
1293
+ false,
1294
+ )
1295
+ : false;
656
1296
 
657
1297
  if (
658
1298
  fs.existsSync(expressionPath) ||
659
- (!useExistingModel && fs.existsSync(modelPath))
1299
+ (!keepModelInSameFile && !useExistingModel && fs.existsSync(modelPath))
660
1300
  ) {
661
1301
  const overwriteAnswer = await rl.question(
662
1302
  `One or more target files already exist. Overwrite? [y/N]: `,
@@ -672,6 +1312,16 @@ async function runAdd() {
672
1312
  let modelImportPath = `./${expressionFileBase}.model`;
673
1313
  let modelExportName = 'model';
674
1314
 
1315
+ if (keepModelInSameFile) {
1316
+ fs.writeFileSync(
1317
+ expressionPath,
1318
+ createInlineExpressionTemplate(expressionName, expressionSource),
1319
+ 'utf8',
1320
+ );
1321
+ logOk(`Created ${expressionPath}`);
1322
+ return;
1323
+ }
1324
+
675
1325
  if (useExistingModel) {
676
1326
  const existingModelPathInput = await askUntilNonEmpty(
677
1327
  rl,
@@ -696,7 +1346,11 @@ async function runAdd() {
696
1346
  'model',
697
1347
  );
698
1348
  } else {
699
- fs.writeFileSync(modelPath, createModelTemplate(), 'utf8');
1349
+ fs.writeFileSync(
1350
+ modelPath,
1351
+ createModelTemplate(expressionSource),
1352
+ 'utf8',
1353
+ );
700
1354
  logOk(`Created ${modelPath}`);
701
1355
  }
702
1356
 
@@ -704,6 +1358,7 @@ async function runAdd() {
704
1358
  expressionPath,
705
1359
  createExpressionTemplate(
706
1360
  expressionName,
1361
+ expressionSource,
707
1362
  modelImportPath,
708
1363
  modelExportName,
709
1364
  ),
@@ -833,7 +1488,7 @@ function removeFileOrDirectoryWithDryRun(targetPath, dryRun) {
833
1488
  fs.rmSync(targetPath, { recursive: true, force: true });
834
1489
  }
835
1490
 
836
- function resolveAngularProjectTsConfig(projectRoot) {
1491
+ function resolveProjectTsConfig(projectRoot) {
837
1492
  const appTsConfigPath = path.join(projectRoot, 'tsconfig.app.json');
838
1493
  if (fs.existsSync(appTsConfigPath)) {
839
1494
  return appTsConfigPath;
@@ -842,6 +1497,10 @@ function resolveAngularProjectTsConfig(projectRoot) {
842
1497
  return path.join(projectRoot, 'tsconfig.json');
843
1498
  }
844
1499
 
1500
+ function resolveAngularProjectTsConfig(projectRoot) {
1501
+ return resolveProjectTsConfig(projectRoot);
1502
+ }
1503
+
845
1504
  function upsertTypescriptPluginInTsConfig(configPath, dryRun) {
846
1505
  if (!fs.existsSync(configPath)) {
847
1506
  logWarn(`TypeScript config not found: ${configPath}`);
@@ -1333,8 +1992,9 @@ function applyVueRsxTemplate(projectRoot, dryRun) {
1333
1992
  async function runProject(flags) {
1334
1993
  const dryRun = Boolean(flags['dry-run']);
1335
1994
  const skipInstall = Boolean(flags['skip-install']);
1336
- const pm = detectPackageManager(flags.pm);
1337
- const tag = resolveInstallTag(flags);
1995
+ const invocationRoot = process.cwd();
1996
+ const pm = resolveCliPackageManager(invocationRoot, flags.pm);
1997
+ const tag = resolveConfiguredInstallTag(invocationRoot, flags);
1338
1998
  let projectName = typeof flags.name === 'string' ? flags.name.trim() : '';
1339
1999
 
1340
2000
  if (!projectName) {
@@ -1613,6 +2273,7 @@ function scaffoldProjectTemplate(
1613
2273
 
1614
2274
  function applyAngularDemoStarter(projectRoot, projectName, pm, flags) {
1615
2275
  const dryRun = Boolean(flags['dry-run']);
2276
+ ensureRsxConfigFile(projectRoot, 'angular', dryRun);
1616
2277
  const tag = resolveInstallTag(flags);
1617
2278
  const tarballsDir =
1618
2279
  typeof flags['tarballs-dir'] === 'string'
@@ -1687,16 +2348,6 @@ function applyAngularDemoStarter(projectRoot, projectName, pm, flags) {
1687
2348
  start: 'npm run build:rsx && ng serve',
1688
2349
  build: 'ng build',
1689
2350
  };
1690
- packageJson.rsx = {
1691
- build: {
1692
- preparse: true,
1693
- preparseFile: 'src/rsx-generated/rsx-aot-preparsed.generated.ts',
1694
- compiled: true,
1695
- compiledFile: 'src/rsx-generated/rsx-aot-compiled.generated.ts',
1696
- registrationFile: 'src/rsx-generated/rsx-aot-registration.generated.ts',
1697
- compiledResolvedEvaluator: false,
1698
- },
1699
- };
1700
2351
  packageJson.dependencies = {
1701
2352
  ...(packageJson.dependencies ?? {}),
1702
2353
  '@rs-x/angular': rsxSpecs['@rs-x/angular'],
@@ -1790,6 +2441,7 @@ function applyAngularDemoStarter(projectRoot, projectName, pm, flags) {
1790
2441
 
1791
2442
  function applyReactDemoStarter(projectRoot, projectName, pm, flags) {
1792
2443
  const dryRun = Boolean(flags['dry-run']);
2444
+ ensureRsxConfigFile(projectRoot, 'react', dryRun);
1793
2445
  const tag = resolveInstallTag(flags);
1794
2446
  const tarballsDir =
1795
2447
  typeof flags['tarballs-dir'] === 'string'
@@ -1870,15 +2522,6 @@ function applyReactDemoStarter(projectRoot, projectName, pm, flags) {
1870
2522
  build: 'npm run build:rsx && vite build',
1871
2523
  preview: 'vite preview',
1872
2524
  };
1873
- packageJson.rsx = {
1874
- build: {
1875
- preparse: true,
1876
- preparseFile: 'src/rsx-generated/rsx-aot-preparsed.generated.ts',
1877
- compiled: true,
1878
- compiledFile: 'src/rsx-generated/rsx-aot-compiled.generated.ts',
1879
- compiledResolvedEvaluator: false,
1880
- },
1881
- };
1882
2525
  packageJson.dependencies = {
1883
2526
  react: packageJson.dependencies?.react ?? '^19.2.4',
1884
2527
  'react-dom': packageJson.dependencies?.['react-dom'] ?? '^19.2.4',
@@ -1924,6 +2567,7 @@ function applyReactDemoStarter(projectRoot, projectName, pm, flags) {
1924
2567
 
1925
2568
  function applyVueDemoStarter(projectRoot, projectName, pm, flags) {
1926
2569
  const dryRun = Boolean(flags['dry-run']);
2570
+ ensureRsxConfigFile(projectRoot, 'vuejs', dryRun);
1927
2571
  const tag = resolveInstallTag(flags);
1928
2572
  const tarballsDir =
1929
2573
  typeof flags['tarballs-dir'] === 'string'
@@ -1990,15 +2634,6 @@ function applyVueDemoStarter(projectRoot, projectName, pm, flags) {
1990
2634
  build: 'npm run build:rsx && vue-tsc -b && vite build',
1991
2635
  preview: 'vite preview',
1992
2636
  };
1993
- packageJson.rsx = {
1994
- build: {
1995
- preparse: true,
1996
- preparseFile: 'src/rsx-generated/rsx-aot-preparsed.generated.ts',
1997
- compiled: true,
1998
- compiledFile: 'src/rsx-generated/rsx-aot-compiled.generated.ts',
1999
- compiledResolvedEvaluator: false,
2000
- },
2001
- };
2002
2637
  packageJson.dependencies = {
2003
2638
  vue: packageJson.dependencies?.vue ?? '^3.5.30',
2004
2639
  '@rs-x/core': rsxSpecs['@rs-x/core'],
@@ -2038,6 +2673,7 @@ function applyVueDemoStarter(projectRoot, projectName, pm, flags) {
2038
2673
 
2039
2674
  function applyNextDemoStarter(projectRoot, projectName, pm, flags) {
2040
2675
  const dryRun = Boolean(flags['dry-run']);
2676
+ ensureRsxConfigFile(projectRoot, 'nextjs', dryRun);
2041
2677
  const tag = resolveInstallTag(flags);
2042
2678
  const tarballsDir =
2043
2679
  typeof flags['tarballs-dir'] === 'string'
@@ -2099,15 +2735,6 @@ function applyNextDemoStarter(projectRoot, projectName, pm, flags) {
2099
2735
  build: 'npm run build:rsx && next build',
2100
2736
  start: 'next start',
2101
2737
  };
2102
- packageJson.rsx = {
2103
- build: {
2104
- preparse: true,
2105
- preparseFile: 'app/rsx-generated/rsx-aot-preparsed.generated.ts',
2106
- compiled: true,
2107
- compiledFile: 'app/rsx-generated/rsx-aot-compiled.generated.ts',
2108
- compiledResolvedEvaluator: false,
2109
- },
2110
- };
2111
2738
  packageJson.dependencies = {
2112
2739
  ...(packageJson.dependencies ?? {}),
2113
2740
  '@rs-x/core': rsxSpecs['@rs-x/core'],
@@ -2158,7 +2785,8 @@ async function runProjectWithTemplate(template, flags) {
2158
2785
  return;
2159
2786
  }
2160
2787
 
2161
- const pm = detectPackageManager(flags.pm);
2788
+ const invocationRoot = process.cwd();
2789
+ const pm = resolveCliPackageManager(invocationRoot, flags.pm);
2162
2790
  const projectName = await resolveProjectName(flags.name, flags._nameHint);
2163
2791
  const projectRoot = resolveProjectRoot(projectName, flags);
2164
2792
  if (fs.existsSync(projectRoot) && fs.readdirSync(projectRoot).length > 0) {
@@ -2197,17 +2825,205 @@ async function runProjectWithTemplate(template, flags) {
2197
2825
  }
2198
2826
  });
2199
2827
 
2828
+ verifyGeneratedProject(projectRoot, normalizedTemplate);
2829
+ if (resolveCliVerifyFlag(invocationRoot, flags, 'project')) {
2830
+ logInfo('Re-running starter verification (--verify)...');
2831
+ verifyGeneratedProject(projectRoot, normalizedTemplate);
2832
+ }
2833
+
2200
2834
  logOk(`Created RS-X ${normalizedTemplate} project: ${projectRoot}`);
2201
2835
  logInfo('Next steps:');
2202
2836
  console.log(` cd ${projectName}`);
2203
2837
  if (Boolean(flags['skip-install'])) {
2204
2838
  console.log(` ${pm} install`);
2205
2839
  }
2206
- if (normalizedTemplate === 'angular') {
2207
- console.log(` ${pm} run start`);
2208
- } else {
2209
- console.log(` ${pm} run dev`);
2840
+ if (normalizedTemplate === 'angular') {
2841
+ console.log(` ${pm} run start`);
2842
+ } else {
2843
+ console.log(` ${pm} run dev`);
2844
+ }
2845
+ }
2846
+
2847
+ function verifyGeneratedProject(projectRoot, template) {
2848
+ const packageJsonPath = path.join(projectRoot, 'package.json');
2849
+ if (!fs.existsSync(packageJsonPath)) {
2850
+ logError(`Generated project is missing package.json: ${packageJsonPath}`);
2851
+ process.exit(1);
2852
+ }
2853
+
2854
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
2855
+ const scripts = packageJson.scripts ?? {};
2856
+ const dependencies = {
2857
+ ...(packageJson.dependencies ?? {}),
2858
+ ...(packageJson.devDependencies ?? {}),
2859
+ };
2860
+
2861
+ const expectationsByTemplate = {
2862
+ angular: {
2863
+ scripts: ['build:rsx', 'start'],
2864
+ dependencies: [
2865
+ '@rs-x/angular',
2866
+ '@rs-x/compiler',
2867
+ '@rs-x/typescript-plugin',
2868
+ ],
2869
+ files: [
2870
+ 'src/main.ts',
2871
+ 'src/app/app.component.ts',
2872
+ 'src/app/virtual-table/virtual-table.component.ts',
2873
+ ],
2874
+ },
2875
+ react: {
2876
+ scripts: ['build:rsx', 'dev', 'build'],
2877
+ dependencies: [
2878
+ '@rs-x/react',
2879
+ '@rs-x/compiler',
2880
+ '@rs-x/typescript-plugin',
2881
+ ],
2882
+ files: [
2883
+ 'src/main.tsx',
2884
+ 'src/rsx-bootstrap.ts',
2885
+ 'src/app/app.tsx',
2886
+ 'src/app/virtual-table/virtual-table-shell.tsx',
2887
+ ],
2888
+ },
2889
+ vuejs: {
2890
+ scripts: ['build:rsx', 'dev', 'build'],
2891
+ dependencies: ['@rs-x/vue', '@rs-x/compiler', '@rs-x/typescript-plugin'],
2892
+ files: [
2893
+ 'src/main.ts',
2894
+ 'src/App.vue',
2895
+ 'src/lib/rsx-bootstrap.ts',
2896
+ 'src/components/VirtualTableShell.vue',
2897
+ ],
2898
+ },
2899
+ nextjs: {
2900
+ scripts: ['build:rsx', 'dev', 'build'],
2901
+ dependencies: [
2902
+ '@rs-x/react',
2903
+ '@rs-x/compiler',
2904
+ '@rs-x/typescript-plugin',
2905
+ ],
2906
+ files: [
2907
+ 'app/layout.tsx',
2908
+ 'app/page.tsx',
2909
+ 'components/demo-app.tsx',
2910
+ 'lib/rsx-bootstrap.ts',
2911
+ ],
2912
+ },
2913
+ };
2914
+
2915
+ const expected = expectationsByTemplate[template];
2916
+ if (!expected) {
2917
+ return;
2918
+ }
2919
+
2920
+ for (const scriptName of expected.scripts) {
2921
+ if (
2922
+ typeof scripts[scriptName] !== 'string' ||
2923
+ scripts[scriptName].trim() === ''
2924
+ ) {
2925
+ logError(
2926
+ `Generated ${template} project is missing script "${scriptName}" in package.json.`,
2927
+ );
2928
+ process.exit(1);
2929
+ }
2930
+ }
2931
+
2932
+ for (const dependencyName of expected.dependencies) {
2933
+ if (typeof dependencies[dependencyName] !== 'string') {
2934
+ logError(
2935
+ `Generated ${template} project is missing dependency "${dependencyName}" in package.json.`,
2936
+ );
2937
+ process.exit(1);
2938
+ }
2939
+ }
2940
+
2941
+ for (const relativeFilePath of expected.files) {
2942
+ const absoluteFilePath = path.join(projectRoot, relativeFilePath);
2943
+ if (!fs.existsSync(absoluteFilePath)) {
2944
+ logError(
2945
+ `Generated ${template} project is missing expected file: ${absoluteFilePath}`,
2946
+ );
2947
+ process.exit(1);
2948
+ }
2949
+ }
2950
+
2951
+ const rsxConfigPath = path.join(projectRoot, 'rsx.config.json');
2952
+ if (!fs.existsSync(rsxConfigPath)) {
2953
+ logError(
2954
+ `Generated ${template} project is missing expected file: ${rsxConfigPath}`,
2955
+ );
2956
+ process.exit(1);
2957
+ }
2958
+
2959
+ logOk(`Verified generated ${template} project structure.`);
2960
+ }
2961
+
2962
+ function verifySetupOutput(projectRoot, template) {
2963
+ const packageJsonPath = path.join(projectRoot, 'package.json');
2964
+ if (!fs.existsSync(packageJsonPath)) {
2965
+ logError(`Project is missing package.json: ${packageJsonPath}`);
2966
+ process.exit(1);
2967
+ }
2968
+
2969
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
2970
+ const scripts = packageJson.scripts ?? {};
2971
+
2972
+ const setupExpectations = {
2973
+ react: {
2974
+ scripts: ['build:rsx', 'typecheck:rsx', 'dev', 'build'],
2975
+ files: ['vite.config.ts', 'src/rsx-bootstrap.ts'],
2976
+ },
2977
+ vuejs: {
2978
+ scripts: ['build:rsx', 'typecheck:rsx', 'dev', 'build'],
2979
+ files: ['vite.config.ts', 'src/rsx-bootstrap.ts'],
2980
+ },
2981
+ next: {
2982
+ scripts: ['build:rsx', 'typecheck:rsx', 'dev', 'build'],
2983
+ files: [
2984
+ 'next.config.js',
2985
+ 'rsx-webpack-loader.cjs',
2986
+ 'app/rsx-bootstrap.ts',
2987
+ ],
2988
+ },
2989
+ angular: {
2990
+ scripts: ['build:rsx', 'typecheck:rsx', 'prebuild', 'start'],
2991
+ files: ['src/main.ts', 'angular.json'],
2992
+ },
2993
+ };
2994
+
2995
+ const expected = setupExpectations[template];
2996
+ if (!expected) {
2997
+ return;
2998
+ }
2999
+
3000
+ for (const scriptName of expected.scripts) {
3001
+ if (
3002
+ typeof scripts[scriptName] !== 'string' ||
3003
+ scripts[scriptName].trim() === ''
3004
+ ) {
3005
+ logError(
3006
+ `Setup output is missing script "${scriptName}" in package.json.`,
3007
+ );
3008
+ process.exit(1);
3009
+ }
3010
+ }
3011
+
3012
+ for (const relativeFilePath of expected.files) {
3013
+ const absoluteFilePath = path.join(projectRoot, relativeFilePath);
3014
+ if (!fs.existsSync(absoluteFilePath)) {
3015
+ logError(`Setup output is missing expected file: ${absoluteFilePath}`);
3016
+ process.exit(1);
3017
+ }
2210
3018
  }
3019
+
3020
+ const rsxConfigPath = path.join(projectRoot, 'rsx.config.json');
3021
+ if (!fs.existsSync(rsxConfigPath)) {
3022
+ logError(`Setup output is missing expected file: ${rsxConfigPath}`);
3023
+ process.exit(1);
3024
+ }
3025
+
3026
+ logOk(`Verified ${template} setup output.`);
2211
3027
  }
2212
3028
 
2213
3029
  function detectProjectContext(projectRoot) {
@@ -2687,11 +3503,10 @@ function patchEntryFileForRsx(entryFile, bootstrapFile, context, dryRun) {
2687
3503
 
2688
3504
  function runInit(flags) {
2689
3505
  const dryRun = Boolean(flags['dry-run']);
2690
- const skipVscode = Boolean(flags['skip-vscode']);
2691
3506
  const skipInstall = Boolean(flags['skip-install']);
2692
- const pm = detectPackageManager(flags.pm);
2693
- const tag = resolveInstallTag(flags);
2694
3507
  const projectRoot = process.cwd();
3508
+ const pm = resolveCliPackageManager(projectRoot, flags.pm);
3509
+ const tag = resolveConfiguredInstallTag(projectRoot, flags);
2695
3510
 
2696
3511
  if (!skipInstall) {
2697
3512
  installRuntimePackages(pm, dryRun, tag, projectRoot, flags);
@@ -2749,48 +3564,9 @@ function runInit(flags) {
2749
3564
  }
2750
3565
  }
2751
3566
 
2752
- logOk('RS-X init completed.');
2753
- }
2754
-
2755
- function upsertRsxBuildConfigInPackageJson(projectRoot, dryRun) {
2756
- const packageJsonPath = path.join(projectRoot, 'package.json');
2757
- if (!fs.existsSync(packageJsonPath)) {
2758
- return false;
2759
- }
2760
-
2761
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
2762
- const currentRsx = packageJson.rsx ?? {};
2763
- const currentBuild = currentRsx.build ?? {};
2764
- const nextBuild = {
2765
- preparse: true,
2766
- preparseFile: 'src/rsx-generated/rsx-aot-preparsed.generated.ts',
2767
- compiled: true,
2768
- compiledFile: 'src/rsx-generated/rsx-aot-compiled.generated.ts',
2769
- registrationFile: 'src/rsx-generated/rsx-aot-registration.generated.ts',
2770
- compiledResolvedEvaluator: false,
2771
- ...currentBuild,
2772
- };
2773
-
2774
- const nextPackageJson = {
2775
- ...packageJson,
2776
- rsx: {
2777
- ...currentRsx,
2778
- build: nextBuild,
2779
- },
2780
- };
2781
-
2782
- if (dryRun) {
2783
- logInfo(`[dry-run] patch ${packageJsonPath} (rsx.build)`);
2784
- return true;
2785
- }
3567
+ ensureRsxConfigFile(projectRoot, effectiveContext, dryRun);
2786
3568
 
2787
- fs.writeFileSync(
2788
- packageJsonPath,
2789
- `${JSON.stringify(nextPackageJson, null, 2)}\n`,
2790
- 'utf8',
2791
- );
2792
- logOk(`Patched ${packageJsonPath} (rsx.build)`);
2793
- return true;
3569
+ logOk('RS-X init completed.');
2794
3570
  }
2795
3571
 
2796
3572
  function ensureAngularProvidersInEntry(entryFile, dryRun) {
@@ -2799,10 +3575,6 @@ function ensureAngularProvidersInEntry(entryFile, dryRun) {
2799
3575
  }
2800
3576
 
2801
3577
  const original = fs.readFileSync(entryFile, 'utf8');
2802
- if (original.includes('providexRsx')) {
2803
- logInfo(`Angular entry already includes providexRsx: ${entryFile}`);
2804
- return true;
2805
- }
2806
3578
 
2807
3579
  if (!original.includes('bootstrapApplication(')) {
2808
3580
  logWarn(
@@ -2814,6 +3586,109 @@ function ensureAngularProvidersInEntry(entryFile, dryRun) {
2814
3586
  return false;
2815
3587
  }
2816
3588
 
3589
+ const bootstrapCallMatch = original.match(
3590
+ /bootstrapApplication\(\s*([A-Za-z_$][\w$]*)\s*(?:,\s*([A-Za-z_$][\w$]*|\{[\s\S]*?\}))?\s*\)/mu,
3591
+ );
3592
+ const componentIdentifier = bootstrapCallMatch?.[1] ?? null;
3593
+ const configArgument = bootstrapCallMatch?.[2] ?? null;
3594
+ const configImportIdentifier =
3595
+ configArgument && /^[A-Za-z_$][\w$]*$/u.test(configArgument)
3596
+ ? configArgument
3597
+ : original.includes('appConfig')
3598
+ ? 'appConfig'
3599
+ : null;
3600
+
3601
+ const staticImportPattern = new RegExp(
3602
+ String.raw`^\s*import\s+\{\s*([^}]*)\b${componentIdentifier ?? ''}\b([^}]*)\}\s+from\s+['"]([^'"]+)['"];\s*$`,
3603
+ 'mu',
3604
+ );
3605
+ const componentImportMatch = componentIdentifier
3606
+ ? original.match(staticImportPattern)
3607
+ : null;
3608
+ const dynamicComponentImportMatch = componentIdentifier
3609
+ ? original.match(
3610
+ new RegExp(
3611
+ String.raw`const\s+\{\s*${componentIdentifier}\s*\}\s*=\s*await\s+import\('([^']+)'\);`,
3612
+ 'mu',
3613
+ ),
3614
+ )
3615
+ : null;
3616
+ const componentImportPath =
3617
+ componentImportMatch?.[3] ?? dynamicComponentImportMatch?.[1] ?? null;
3618
+
3619
+ const configImportMatch = configImportIdentifier
3620
+ ? original.match(
3621
+ new RegExp(
3622
+ String.raw`^\s*import\s+\{\s*([^}]*)\b${configImportIdentifier}\b([^}]*)\}\s+from\s+['"]([^'"]+)['"];\s*$`,
3623
+ 'mu',
3624
+ ),
3625
+ )
3626
+ : null;
3627
+ const configImportPath = configImportMatch?.[3] ?? null;
3628
+
3629
+ const canPreloadBeforeComponentImport =
3630
+ componentIdentifier !== null && componentImportPath !== null;
3631
+
3632
+ if (canPreloadBeforeComponentImport) {
3633
+ const importLines = original
3634
+ .split('\n')
3635
+ .filter((line) => /^\s*import\s+/u.test(line))
3636
+ .filter((line) => !line.match(staticImportPattern))
3637
+ .filter((line) =>
3638
+ configImportMatch
3639
+ ? !line.match(
3640
+ new RegExp(
3641
+ String.raw`^\s*import\s+\{\s*([^}]*)\b${configImportIdentifier}\b([^}]*)\}\s+from\s+['"][^'"]+['"];\s*$`,
3642
+ 'u',
3643
+ ),
3644
+ )
3645
+ : true,
3646
+ )
3647
+ .filter(
3648
+ (line) =>
3649
+ !line.includes("import { providexRsx } from '@rs-x/angular';"),
3650
+ )
3651
+ .filter(
3652
+ (line) =>
3653
+ !line.includes("import { InjectionContainer } from '@rs-x/core';"),
3654
+ )
3655
+ .filter(
3656
+ (line) =>
3657
+ !line.includes(
3658
+ "import { RsXExpressionParserModule } from '@rs-x/expression-parser';",
3659
+ ),
3660
+ );
3661
+
3662
+ const bootstrapConfigExpression =
3663
+ configImportPath && configImportIdentifier
3664
+ ? `const [{ ${configImportIdentifier} }, { ${componentIdentifier} }] = await Promise.all([\n import('${configImportPath}'),\n import('${componentImportPath}'),\n ]);\n\n await bootstrapApplication(${componentIdentifier}, {\n ...${configImportIdentifier},\n providers: [...(${configImportIdentifier}.providers ?? []), ...providexRsx()],\n });`
3665
+ : `const { ${componentIdentifier} } = await import('${componentImportPath}');\n\n await bootstrapApplication(${componentIdentifier}, {\n providers: [...providexRsx()],\n });`;
3666
+
3667
+ const rewritten = `${importLines.join('\n')}
3668
+ import { providexRsx } from '@rs-x/angular';
3669
+ import { InjectionContainer } from '@rs-x/core';
3670
+ import { RsXExpressionParserModule } from '@rs-x/expression-parser';
3671
+
3672
+ const bootstrap = async (): Promise<void> => {
3673
+ await InjectionContainer.load(RsXExpressionParserModule);
3674
+ ${bootstrapConfigExpression}
3675
+ };
3676
+
3677
+ void bootstrap().catch((error) => {
3678
+ console.error(error);
3679
+ });
3680
+ `;
3681
+
3682
+ if (dryRun) {
3683
+ logInfo(`[dry-run] patch ${entryFile} (providexRsx + preload)`);
3684
+ return true;
3685
+ }
3686
+
3687
+ fs.writeFileSync(entryFile, rewritten, 'utf8');
3688
+ logOk(`Patched ${entryFile} to preload RS-X and include providexRsx.`);
3689
+ return true;
3690
+ }
3691
+
2817
3692
  const sourceWithImport = injectImport(
2818
3693
  original,
2819
3694
  "import { providexRsx } from '@rs-x/angular';",
@@ -3106,9 +3981,13 @@ ${patchBlock}
3106
3981
 
3107
3982
  function runSetupReact(flags) {
3108
3983
  const dryRun = Boolean(flags['dry-run']);
3109
- const pm = detectPackageManager(flags.pm);
3110
- const tag = resolveInstallTag(flags);
3111
3984
  const projectRoot = process.cwd();
3985
+ const pm = resolveCliPackageManager(projectRoot, flags.pm);
3986
+ const tag = resolveConfiguredInstallTag(projectRoot, flags);
3987
+ const reactTsConfigPath = resolveProjectTsConfig(projectRoot);
3988
+ const reactTsConfigRelative = path
3989
+ .relative(projectRoot, reactTsConfigPath)
3990
+ .replace(/\\/gu, '/');
3112
3991
  const packageJsonPath = path.join(projectRoot, 'package.json');
3113
3992
  if (!fs.existsSync(packageJsonPath)) {
3114
3993
  logError(`package.json not found in ${projectRoot}`);
@@ -3154,10 +4033,17 @@ function runSetupReact(flags) {
3154
4033
  } else {
3155
4034
  logInfo('Skipping RS-X React bindings install (--skip-install).');
3156
4035
  }
4036
+ ensureRsxConfigFile(projectRoot, 'react', dryRun);
3157
4037
  upsertScriptInPackageJson(
3158
4038
  projectRoot,
3159
4039
  'build:rsx',
3160
- 'rsx build --project tsconfig.json --no-emit --prod',
4040
+ `rsx build --project ${reactTsConfigRelative} --no-emit --prod`,
4041
+ dryRun,
4042
+ );
4043
+ upsertScriptInPackageJson(
4044
+ projectRoot,
4045
+ 'typecheck:rsx',
4046
+ `rsx typecheck --project ${reactTsConfigRelative}`,
3161
4047
  dryRun,
3162
4048
  );
3163
4049
  upsertScriptInPackageJson(
@@ -3173,14 +4059,21 @@ function runSetupReact(flags) {
3173
4059
  dryRun,
3174
4060
  );
3175
4061
  wireRsxVitePlugin(projectRoot, dryRun);
4062
+ if (resolveCliVerifyFlag(projectRoot, flags, 'setup')) {
4063
+ verifySetupOutput(projectRoot, 'react');
4064
+ }
3176
4065
  logOk('RS-X React setup completed.');
3177
4066
  }
3178
4067
 
3179
4068
  function runSetupNext(flags) {
3180
4069
  const dryRun = Boolean(flags['dry-run']);
3181
- const pm = detectPackageManager(flags.pm);
3182
- const tag = resolveInstallTag(flags);
3183
4070
  const projectRoot = process.cwd();
4071
+ const pm = resolveCliPackageManager(projectRoot, flags.pm);
4072
+ const tag = resolveConfiguredInstallTag(projectRoot, flags);
4073
+ const nextTsConfigPath = resolveProjectTsConfig(projectRoot);
4074
+ const nextTsConfigRelative = path
4075
+ .relative(projectRoot, nextTsConfigPath)
4076
+ .replace(/\\/gu, '/');
3184
4077
  runInit({
3185
4078
  ...flags,
3186
4079
  'skip-vscode': true,
@@ -3201,15 +4094,43 @@ function runSetupNext(flags) {
3201
4094
  } else {
3202
4095
  logInfo('Skipping RS-X React bindings install (--skip-install).');
3203
4096
  }
4097
+ ensureRsxConfigFile(projectRoot, 'next', dryRun);
4098
+ upsertScriptInPackageJson(
4099
+ projectRoot,
4100
+ 'build:rsx',
4101
+ `rsx build --project ${nextTsConfigRelative} --no-emit --prod`,
4102
+ dryRun,
4103
+ );
4104
+ upsertScriptInPackageJson(
4105
+ projectRoot,
4106
+ 'typecheck:rsx',
4107
+ `rsx typecheck --project ${nextTsConfigRelative}`,
4108
+ dryRun,
4109
+ );
4110
+ upsertScriptInPackageJson(
4111
+ projectRoot,
4112
+ 'dev',
4113
+ 'npm run build:rsx && next dev',
4114
+ dryRun,
4115
+ );
4116
+ upsertScriptInPackageJson(
4117
+ projectRoot,
4118
+ 'build',
4119
+ 'npm run build:rsx && next build',
4120
+ dryRun,
4121
+ );
3204
4122
  wireRsxNextWebpack(projectRoot, dryRun);
4123
+ if (resolveCliVerifyFlag(projectRoot, flags, 'setup')) {
4124
+ verifySetupOutput(projectRoot, 'next');
4125
+ }
3205
4126
  logOk('RS-X Next.js setup completed.');
3206
4127
  }
3207
4128
 
3208
4129
  function runSetupVue(flags) {
3209
4130
  const dryRun = Boolean(flags['dry-run']);
3210
- const pm = detectPackageManager(flags.pm);
3211
- const tag = resolveInstallTag(flags);
3212
4131
  const projectRoot = process.cwd();
4132
+ const pm = resolveCliPackageManager(projectRoot, flags.pm);
4133
+ const tag = resolveConfiguredInstallTag(projectRoot, flags);
3213
4134
  runInit({
3214
4135
  ...flags,
3215
4136
  'skip-vscode': true,
@@ -3238,6 +4159,7 @@ function runSetupVue(flags) {
3238
4159
  } else {
3239
4160
  logInfo('Skipping RS-X Vue bindings install (--skip-install).');
3240
4161
  }
4162
+ ensureRsxConfigFile(projectRoot, 'vuejs', dryRun);
3241
4163
  upsertScriptInPackageJson(
3242
4164
  projectRoot,
3243
4165
  'build:rsx',
@@ -3267,14 +4189,17 @@ function runSetupVue(flags) {
3267
4189
  ensureTsConfigIncludePattern(vueTsConfigPath, 'src/**/*.d.ts', dryRun);
3268
4190
  ensureVueEnvTypes(projectRoot, dryRun);
3269
4191
  wireRsxVitePlugin(projectRoot, dryRun);
4192
+ if (resolveCliVerifyFlag(projectRoot, flags, 'setup')) {
4193
+ verifySetupOutput(projectRoot, 'vuejs');
4194
+ }
3270
4195
  logOk('RS-X Vue setup completed.');
3271
4196
  }
3272
4197
 
3273
4198
  function runSetupAngular(flags) {
3274
4199
  const dryRun = Boolean(flags['dry-run']);
3275
- const pm = detectPackageManager(flags.pm);
3276
- const tag = resolveInstallTag(flags);
3277
4200
  const projectRoot = process.cwd();
4201
+ const pm = resolveCliPackageManager(projectRoot, flags.pm);
4202
+ const tag = resolveConfiguredInstallTag(projectRoot, flags);
3278
4203
  const angularTsConfigPath = resolveAngularProjectTsConfig(projectRoot);
3279
4204
  const angularTsConfigRelative = path
3280
4205
  .relative(projectRoot, angularTsConfigPath)
@@ -3318,7 +4243,7 @@ function runSetupAngular(flags) {
3318
4243
  );
3319
4244
  }
3320
4245
 
3321
- upsertRsxBuildConfigInPackageJson(projectRoot, dryRun);
4246
+ ensureRsxConfigFile(projectRoot, 'angular', dryRun);
3322
4247
 
3323
4248
  upsertScriptInPackageJson(
3324
4249
  projectRoot,
@@ -3386,13 +4311,17 @@ function runSetupAngular(flags) {
3386
4311
  dryRun,
3387
4312
  });
3388
4313
 
4314
+ if (resolveCliVerifyFlag(projectRoot, flags, 'setup')) {
4315
+ verifySetupOutput(projectRoot, 'angular');
4316
+ }
4317
+
3389
4318
  logOk('RS-X Angular setup completed.');
3390
4319
  }
3391
4320
 
3392
4321
  function runSetupAuto(flags) {
3393
4322
  const projectRoot = process.cwd();
3394
4323
  const context = detectProjectContext(projectRoot);
3395
- const tag = resolveInstallTag(flags);
4324
+ const tag = resolveConfiguredInstallTag(projectRoot, flags);
3396
4325
 
3397
4326
  if (context === 'react') {
3398
4327
  logInfo('Auto-detected framework: react');
@@ -3419,7 +4348,7 @@ function runSetupAuto(flags) {
3419
4348
  }
3420
4349
 
3421
4350
  logInfo('No framework-specific setup detected; running generic setup.');
3422
- const pm = detectPackageManager(flags.pm);
4351
+ const pm = resolveCliPackageManager(projectRoot, flags.pm);
3423
4352
  installRuntimePackages(
3424
4353
  pm,
3425
4354
  Boolean(flags['dry-run']),
@@ -3450,8 +4379,13 @@ function runBuild(flags) {
3450
4379
  const dryRun = Boolean(flags['dry-run']);
3451
4380
  const noEmit = Boolean(flags['no-emit']);
3452
4381
  const prodMode = parseBooleanFlag(flags.prod, false);
4382
+ const invocationConfig = resolveRsxBuildConfig(invocationRoot);
3453
4383
  const projectArg =
3454
- typeof flags.project === 'string' ? flags.project : 'tsconfig.json';
4384
+ typeof flags.project === 'string'
4385
+ ? flags.project
4386
+ : typeof invocationConfig.tsconfig === 'string'
4387
+ ? invocationConfig.tsconfig
4388
+ : 'tsconfig.json';
3455
4389
  const configPath = path.resolve(invocationRoot, projectArg);
3456
4390
  const projectRoot = path.dirname(configPath);
3457
4391
  const context = detectProjectContext(projectRoot);
@@ -3550,7 +4484,9 @@ function runBuild(flags) {
3550
4484
  const outDirOverride =
3551
4485
  typeof flags['out-dir'] === 'string'
3552
4486
  ? path.resolve(projectRoot, flags['out-dir'])
3553
- : null;
4487
+ : typeof rsxBuildConfig.outDir === 'string'
4488
+ ? path.resolve(projectRoot, rsxBuildConfig.outDir)
4489
+ : null;
3554
4490
  const outDir =
3555
4491
  outDirOverride ??
3556
4492
  parsedConfig.options.outDir ??
@@ -4072,20 +5008,286 @@ function ensureAngularPolyfillsContainsFile({
4072
5008
  logOk(`Updated angular.json to inject RS-X AOT runtime registration.`);
4073
5009
  }
4074
5010
 
4075
- function resolveRsxBuildConfig(projectRoot) {
4076
- const packageJsonPath = path.join(projectRoot, 'package.json');
4077
- if (!fs.existsSync(packageJsonPath)) {
4078
- return {};
5011
+ function readJsonFileIfPresent(filePath) {
5012
+ if (!fs.existsSync(filePath)) {
5013
+ return null;
4079
5014
  }
4080
5015
 
4081
5016
  try {
4082
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
4083
- const rsxConfig = packageJson.rsx ?? {};
4084
- const buildConfig = rsxConfig.build ?? {};
4085
- return typeof buildConfig === 'object' && buildConfig ? buildConfig : {};
5017
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
4086
5018
  } catch {
4087
- return {};
5019
+ return null;
5020
+ }
5021
+ }
5022
+
5023
+ function validateRsxConfigShape(config, filePath) {
5024
+ if (typeof config !== 'object' || !config || Array.isArray(config)) {
5025
+ logError(
5026
+ `Invalid RS-X config in ${filePath}: expected a JSON object at the top level.`,
5027
+ );
5028
+ process.exit(1);
5029
+ }
5030
+
5031
+ const build = config.build;
5032
+ if (build !== undefined) {
5033
+ if (typeof build !== 'object' || !build || Array.isArray(build)) {
5034
+ logError(
5035
+ `Invalid RS-X config in ${filePath}: "build" must be an object.`,
5036
+ );
5037
+ process.exit(1);
5038
+ }
5039
+
5040
+ const stringKeys = ['preparseFile', 'compiledFile', 'registrationFile'];
5041
+ const extraStringKeys = ['tsconfig', 'outDir'];
5042
+ for (const key of extraStringKeys) {
5043
+ if (build[key] !== undefined && typeof build[key] !== 'string') {
5044
+ logError(
5045
+ `Invalid RS-X config in ${filePath}: "build.${key}" must be a string.`,
5046
+ );
5047
+ process.exit(1);
5048
+ }
5049
+ }
5050
+ for (const key of stringKeys) {
5051
+ if (build[key] !== undefined && typeof build[key] !== 'string') {
5052
+ logError(
5053
+ `Invalid RS-X config in ${filePath}: "build.${key}" must be a string.`,
5054
+ );
5055
+ process.exit(1);
5056
+ }
5057
+ }
5058
+
5059
+ const booleanKeys = ['preparse', 'compiled', 'compiledResolvedEvaluator'];
5060
+ for (const key of booleanKeys) {
5061
+ if (build[key] !== undefined && typeof build[key] !== 'boolean') {
5062
+ logError(
5063
+ `Invalid RS-X config in ${filePath}: "build.${key}" must be a boolean.`,
5064
+ );
5065
+ process.exit(1);
5066
+ }
5067
+ }
5068
+ }
5069
+
5070
+ const cli = config.cli;
5071
+ if (cli !== undefined) {
5072
+ if (typeof cli !== 'object' || !cli || Array.isArray(cli)) {
5073
+ logError(`Invalid RS-X config in ${filePath}: "cli" must be an object.`);
5074
+ process.exit(1);
5075
+ }
5076
+
5077
+ const cliStringKeys = ['packageManager', 'installTag'];
5078
+ for (const key of cliStringKeys) {
5079
+ if (cli[key] !== undefined && typeof cli[key] !== 'string') {
5080
+ logError(
5081
+ `Invalid RS-X config in ${filePath}: "cli.${key}" must be a string.`,
5082
+ );
5083
+ process.exit(1);
5084
+ }
5085
+ }
5086
+
5087
+ if (
5088
+ cli.packageManager !== undefined &&
5089
+ !['pnpm', 'npm', 'yarn', 'bun'].includes(cli.packageManager)
5090
+ ) {
5091
+ logError(
5092
+ `Invalid RS-X config in ${filePath}: "cli.packageManager" must be one of pnpm, npm, yarn, or bun.`,
5093
+ );
5094
+ process.exit(1);
5095
+ }
5096
+
5097
+ if (
5098
+ cli.installTag !== undefined &&
5099
+ !['latest', 'next'].includes(cli.installTag)
5100
+ ) {
5101
+ logError(
5102
+ `Invalid RS-X config in ${filePath}: "cli.installTag" must be "latest" or "next".`,
5103
+ );
5104
+ process.exit(1);
5105
+ }
5106
+
5107
+ const cliBooleanSections = ['setup', 'project'];
5108
+ for (const key of cliBooleanSections) {
5109
+ if (cli[key] !== undefined) {
5110
+ if (
5111
+ typeof cli[key] !== 'object' ||
5112
+ !cli[key] ||
5113
+ Array.isArray(cli[key])
5114
+ ) {
5115
+ logError(
5116
+ `Invalid RS-X config in ${filePath}: "cli.${key}" must be an object.`,
5117
+ );
5118
+ process.exit(1);
5119
+ }
5120
+ if (
5121
+ cli[key].verify !== undefined &&
5122
+ typeof cli[key].verify !== 'boolean'
5123
+ ) {
5124
+ logError(
5125
+ `Invalid RS-X config in ${filePath}: "cli.${key}.verify" must be a boolean.`,
5126
+ );
5127
+ process.exit(1);
5128
+ }
5129
+ }
5130
+ }
5131
+
5132
+ const add = cli.add;
5133
+ if (add !== undefined) {
5134
+ if (typeof add !== 'object' || !add || Array.isArray(add)) {
5135
+ logError(
5136
+ `Invalid RS-X config in ${filePath}: "cli.add" must be an object.`,
5137
+ );
5138
+ process.exit(1);
5139
+ }
5140
+
5141
+ if (
5142
+ add.defaultDirectory !== undefined &&
5143
+ typeof add.defaultDirectory !== 'string'
5144
+ ) {
5145
+ logError(
5146
+ `Invalid RS-X config in ${filePath}: "cli.add.defaultDirectory" must be a string.`,
5147
+ );
5148
+ process.exit(1);
5149
+ }
5150
+
5151
+ if (add.searchRoots !== undefined) {
5152
+ if (
5153
+ !Array.isArray(add.searchRoots) ||
5154
+ add.searchRoots.some((entry) => typeof entry !== 'string')
5155
+ ) {
5156
+ logError(
5157
+ `Invalid RS-X config in ${filePath}: "cli.add.searchRoots" must be an array of strings.`,
5158
+ );
5159
+ process.exit(1);
5160
+ }
5161
+ }
5162
+ }
5163
+ }
5164
+ }
5165
+
5166
+ function mergeRsxConfig(baseConfig, overrideConfig) {
5167
+ const base = typeof baseConfig === 'object' && baseConfig ? baseConfig : {};
5168
+ const override =
5169
+ typeof overrideConfig === 'object' && overrideConfig ? overrideConfig : {};
5170
+
5171
+ return {
5172
+ ...base,
5173
+ ...override,
5174
+ build: {
5175
+ ...(typeof base.build === 'object' && base.build ? base.build : {}),
5176
+ ...(typeof override.build === 'object' && override.build
5177
+ ? override.build
5178
+ : {}),
5179
+ },
5180
+ cli: {
5181
+ ...(typeof base.cli === 'object' && base.cli ? base.cli : {}),
5182
+ ...(typeof override.cli === 'object' && override.cli ? override.cli : {}),
5183
+ add: {
5184
+ ...(typeof base.cli?.add === 'object' && base.cli?.add
5185
+ ? base.cli.add
5186
+ : {}),
5187
+ ...(typeof override.cli?.add === 'object' && override.cli?.add
5188
+ ? override.cli.add
5189
+ : {}),
5190
+ },
5191
+ },
5192
+ };
5193
+ }
5194
+
5195
+ function resolveRsxProjectConfig(projectRoot) {
5196
+ const fileConfigPath = path.join(projectRoot, 'rsx.config.json');
5197
+ const fileRsxConfig = readJsonFileIfPresent(fileConfigPath) ?? {};
5198
+ validateRsxConfigShape(fileRsxConfig, fileConfigPath);
5199
+ return mergeRsxConfig({}, fileRsxConfig);
5200
+ }
5201
+
5202
+ function defaultCliAddConfigForTemplate(template) {
5203
+ if (template === 'next' || template === 'nextjs') {
5204
+ return {
5205
+ defaultDirectory: 'app/expressions',
5206
+ searchRoots: ['app', 'src', 'expressions'],
5207
+ };
5208
+ }
5209
+
5210
+ return {
5211
+ defaultDirectory: 'src/expressions',
5212
+ searchRoots: ['src', 'app', 'expressions'],
5213
+ };
5214
+ }
5215
+
5216
+ function defaultCliConfigForTemplate(template) {
5217
+ return {
5218
+ packageManager: 'npm',
5219
+ installTag: 'next',
5220
+ setup: {
5221
+ verify: false,
5222
+ },
5223
+ project: {
5224
+ verify: false,
5225
+ },
5226
+ add: defaultCliAddConfigForTemplate(template),
5227
+ };
5228
+ }
5229
+
5230
+ function defaultRsxBuildConfigForTemplate(template) {
5231
+ if (template === 'next' || template === 'nextjs') {
5232
+ return {
5233
+ preparse: true,
5234
+ preparseFile: 'app/rsx-generated/rsx-aot-preparsed.generated.ts',
5235
+ compiled: true,
5236
+ compiledFile: 'app/rsx-generated/rsx-aot-compiled.generated.ts',
5237
+ registrationFile: 'app/rsx-generated/rsx-aot-registration.generated.ts',
5238
+ compiledResolvedEvaluator: false,
5239
+ };
5240
+ }
5241
+
5242
+ return {
5243
+ preparse: true,
5244
+ preparseFile: 'src/rsx-generated/rsx-aot-preparsed.generated.ts',
5245
+ compiled: true,
5246
+ compiledFile: 'src/rsx-generated/rsx-aot-compiled.generated.ts',
5247
+ registrationFile: 'src/rsx-generated/rsx-aot-registration.generated.ts',
5248
+ compiledResolvedEvaluator: false,
5249
+ };
5250
+ }
5251
+
5252
+ function ensureRsxConfigFile(projectRoot, template, dryRun) {
5253
+ const configPath = path.join(projectRoot, 'rsx.config.json');
5254
+ const defaultConfig = {
5255
+ build: defaultRsxBuildConfigForTemplate(template),
5256
+ cli: defaultCliConfigForTemplate(template),
5257
+ };
5258
+
5259
+ const existingConfig = readJsonFileIfPresent(configPath);
5260
+ const nextConfig = mergeRsxConfig(defaultConfig, existingConfig ?? {});
5261
+
5262
+ if (
5263
+ existingConfig &&
5264
+ JSON.stringify(existingConfig) === JSON.stringify(nextConfig)
5265
+ ) {
5266
+ return false;
5267
+ }
5268
+
5269
+ if (dryRun) {
5270
+ logInfo(`[dry-run] ${existingConfig ? 'patch' : 'create'} ${configPath}`);
5271
+ return true;
4088
5272
  }
5273
+
5274
+ fs.writeFileSync(
5275
+ configPath,
5276
+ `${JSON.stringify(nextConfig, null, 2)}\n`,
5277
+ 'utf8',
5278
+ );
5279
+ logOk(`${existingConfig ? 'Patched' : 'Created'} ${configPath}`);
5280
+ return true;
5281
+ }
5282
+
5283
+ function resolveRsxBuildConfig(projectRoot) {
5284
+ const buildConfig = resolveRsxProjectConfig(projectRoot).build ?? {};
5285
+ return typeof buildConfig === 'object' && buildConfig ? buildConfig : {};
5286
+ }
5287
+
5288
+ function resolveRsxCliConfig(projectRoot) {
5289
+ const cliConfig = resolveRsxProjectConfig(projectRoot).cli ?? {};
5290
+ return typeof cliConfig === 'object' && cliConfig ? cliConfig : {};
4089
5291
  }
4090
5292
 
4091
5293
  function parseBooleanFlag(value, defaultValue) {
@@ -4146,7 +5348,7 @@ function printGeneralHelp() {
4146
5348
  console.log(
4147
5349
  ' typecheck Type-check project + RS-X semantic checks',
4148
5350
  );
4149
- console.log(' version | -v Print CLI version');
5351
+ console.log(' version | v | -v Print CLI version');
4150
5352
  console.log('');
4151
5353
  console.log('Help Aliases:');
4152
5354
  console.log(' rsx -h');
@@ -4180,14 +5382,28 @@ function printAddHelp() {
4180
5382
  console.log(
4181
5383
  ' - Prompts for expression export name (must be valid TS identifier)',
4182
5384
  );
5385
+ console.log(" - Prompts for the initial expression string (default: 'a')");
5386
+ console.log(
5387
+ ' - Seeds the generated model with top-level identifiers found in that expression when possible',
5388
+ );
4183
5389
  console.log(
4184
5390
  ' - Prompts whether file name should be kebab-case (default: yes)',
4185
5391
  );
4186
5392
  console.log(' - Prompts for output directory (relative or absolute)');
4187
- console.log(' - Prompts whether to reuse an existing model file');
4188
- console.log(' - Creates <name>.ts and optionally creates <name>.model.ts');
4189
5393
  console.log(
4190
- ' - Expression file imports selected model and exports rsx expression',
5394
+ ' - Prompts for add mode: new one-file, new separate-model, or update existing',
5395
+ );
5396
+ console.log(
5397
+ ' - Defaults to keeping the model in the same file as the expression',
5398
+ );
5399
+ console.log(
5400
+ ' - If you choose an existing file, shows a list of files that already contain RS-X expressions',
5401
+ );
5402
+ console.log(
5403
+ ' - Can still create or reuse a separate model file when you opt out of same-file mode',
5404
+ );
5405
+ console.log(
5406
+ ' - Respects rsx.config.json (`cli.add`) for add defaults and file discovery',
4191
5407
  );
4192
5408
  }
4193
5409
 
@@ -4226,7 +5442,7 @@ function printInstallHelp(target) {
4226
5442
  function printSetupHelp() {
4227
5443
  console.log('Usage:');
4228
5444
  console.log(
4229
- ' rsx setup [--pm <pnpm|npm|yarn|bun>] [--next] [--force] [--local] [--dry-run]',
5445
+ ' rsx setup [--pm <pnpm|npm|yarn|bun>] [--next] [--verify] [--force] [--local] [--dry-run]',
4230
5446
  );
4231
5447
  console.log('');
4232
5448
  console.log('What it does:');
@@ -4235,12 +5451,17 @@ function printSetupHelp() {
4235
5451
  );
4236
5452
  console.log(' - Installs runtime packages');
4237
5453
  console.log(' - Installs compiler tooling packages');
4238
- console.log(' - Installs VS Code extension');
5454
+ console.log(' - Writes rsx.build config plus build/typecheck scripts');
5455
+ console.log(' - Creates rsx.config.json with CLI defaults you can override');
4239
5456
  console.log(' - Applies framework-specific transform/build integration');
5457
+ console.log(' - Does not install the VS Code extension automatically');
4240
5458
  console.log('');
4241
5459
  console.log('Options:');
4242
5460
  console.log(' --pm Explicit package manager');
4243
5461
  console.log(' --next Install prerelease versions (dist-tag next)');
5462
+ console.log(
5463
+ ' --verify Validate the resulting setup output before returning',
5464
+ );
4244
5465
  console.log(' --force Reinstall extension if already installed');
4245
5466
  console.log(' --local Build/install local VSIX from repo workspace');
4246
5467
  console.log(' --dry-run Print commands without executing them');
@@ -4259,14 +5480,17 @@ function printInitHelp() {
4259
5480
  console.log(
4260
5481
  ' - Detects project context and wires RS-X bootstrap in entry file',
4261
5482
  );
4262
- console.log(' - Installs VS Code extension (unless --skip-vscode)');
5483
+ console.log(' - Creates rsx.config.json with CLI defaults you can override');
5484
+ console.log(' - Does not install the VS Code extension automatically');
4263
5485
  console.log('');
4264
5486
  console.log('Options:');
4265
5487
  console.log(' --pm Explicit package manager');
4266
5488
  console.log(' --entry Explicit application entry file');
4267
5489
  console.log(' --next Install prerelease versions (dist-tag next)');
4268
5490
  console.log(' --skip-install Skip npm/pnpm/yarn/bun package installation');
4269
- console.log(' --skip-vscode Skip VS Code extension installation');
5491
+ console.log(
5492
+ ' --skip-vscode Accepted for compatibility; VS Code is not auto-installed',
5493
+ );
4270
5494
  console.log(' --force Reinstall extension if already installed');
4271
5495
  console.log(' --local Build/install local VSIX from repo workspace');
4272
5496
  console.log(' --dry-run Print commands without executing them');
@@ -4275,7 +5499,7 @@ function printInitHelp() {
4275
5499
  function printProjectHelp() {
4276
5500
  console.log('Usage:');
4277
5501
  console.log(
4278
- ' rsx project [angular|vuejs|react|nextjs|nodejs] [--name <project-name>] [--pm <pnpm|npm|yarn|bun>] [--next] [--template <angular|vuejs|react|nextjs|nodejs>] [--tarballs-dir <path>] [--skip-install] [--skip-vscode] [--dry-run]',
5502
+ ' rsx project [angular|vuejs|react|nextjs|nodejs] [--name <project-name>] [--pm <pnpm|npm|yarn|bun>] [--next] [--template <angular|vuejs|react|nextjs|nodejs>] [--tarballs-dir <path>] [--skip-install] [--skip-vscode] [--verify] [--dry-run]',
4279
5503
  );
4280
5504
  console.log('');
4281
5505
  console.log('What it does:');
@@ -4286,6 +5510,9 @@ function printProjectHelp() {
4286
5510
  );
4287
5511
  console.log(' - Scaffolds framework app and wires RS-X bootstrap/setup');
4288
5512
  console.log(' - Writes package.json with RS-X dependencies');
5513
+ console.log(
5514
+ ' - Creates rsx.config.json with starter CLI defaults you can override',
5515
+ );
4289
5516
  console.log(
4290
5517
  ' - Adds tsconfig + TypeScript plugin config for editor support',
4291
5518
  );
@@ -4295,6 +5522,7 @@ function printProjectHelp() {
4295
5522
  console.log(' - For React/Next templates: also installs @rs-x/react');
4296
5523
  console.log(' - For Vue template: also installs @rs-x/vue');
4297
5524
  console.log(' - Installs dependencies (unless --skip-install)');
5525
+ console.log(' - Verifies the generated starter before reporting success');
4298
5526
  console.log('');
4299
5527
  console.log('Options:');
4300
5528
  console.log(' --name Project folder/package name');
@@ -4308,7 +5536,12 @@ function printProjectHelp() {
4308
5536
  );
4309
5537
  console.log(' (or set RSX_TARBALLS_DIR env var)');
4310
5538
  console.log(' --skip-install Skip dependency installation');
4311
- console.log(' --skip-vscode Skip VS Code extension installation');
5539
+ console.log(
5540
+ ' --skip-vscode Accepted for compatibility; VS Code is not auto-installed',
5541
+ );
5542
+ console.log(
5543
+ ' --verify Re-run starter structure checks explicitly after generation',
5544
+ );
4312
5545
  console.log(' --dry-run Print actions without writing files');
4313
5546
  }
4314
5547
 
@@ -4526,13 +5759,14 @@ function main() {
4526
5759
  }
4527
5760
 
4528
5761
  if (command === 'install' && target === 'compiler') {
4529
- const pm = detectPackageManager(flags.pm);
4530
- const tag = resolveInstallTag(flags);
5762
+ const projectRoot = process.cwd();
5763
+ const pm = resolveCliPackageManager(projectRoot, flags.pm);
5764
+ const tag = resolveConfiguredInstallTag(projectRoot, flags);
4531
5765
  installCompilerPackages(
4532
5766
  pm,
4533
5767
  Boolean(flags['dry-run']),
4534
5768
  tag,
4535
- process.cwd(),
5769
+ projectRoot,
4536
5770
  flags,
4537
5771
  );
4538
5772
  return;