@nextsparkjs/cli 0.1.0-beta.11 → 0.1.0-beta.110

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/dist/cli.js CHANGED
@@ -7,12 +7,13 @@ import {
7
7
  installTheme,
8
8
  runPostinstall,
9
9
  validateTheme
10
- } from "./chunk-ALB2C27N.js";
10
+ } from "./chunk-DXL5CEZD.js";
11
11
 
12
12
  // src/cli.ts
13
13
  import { config } from "dotenv";
14
14
  import { Command } from "commander";
15
- import chalk15 from "chalk";
15
+ import chalk20 from "chalk";
16
+ import { readFileSync as readFileSync11 } from "fs";
16
17
 
17
18
  // src/commands/dev.ts
18
19
  import { spawn } from "child_process";
@@ -21,7 +22,7 @@ import ora from "ora";
21
22
 
22
23
  // src/utils/paths.ts
23
24
  import { existsSync } from "fs";
24
- import { resolve, dirname } from "path";
25
+ import { resolve, join, dirname } from "path";
25
26
  import { fileURLToPath } from "url";
26
27
  var __filename = fileURLToPath(import.meta.url);
27
28
  var __dirname = dirname(__filename);
@@ -51,6 +52,16 @@ function isMonorepoMode() {
51
52
  const npmCorePath = resolve(cwd, "node_modules", "@nextsparkjs", "core");
52
53
  return !existsSync(npmCorePath);
53
54
  }
55
+ function getAIWorkflowDir() {
56
+ const cwd = process.cwd();
57
+ const nmPath = join(cwd, "node_modules", "@nextsparkjs", "ai-workflow");
58
+ if (existsSync(nmPath)) return nmPath;
59
+ const webNmPath = join(cwd, "web", "node_modules", "@nextsparkjs", "ai-workflow");
60
+ if (existsSync(webNmPath)) return webNmPath;
61
+ const monoPath = join(cwd, "packages", "ai-workflow");
62
+ if (existsSync(monoPath)) return monoPath;
63
+ return null;
64
+ }
54
65
 
55
66
  // src/commands/dev.ts
56
67
  async function devCommand(options) {
@@ -81,6 +92,7 @@ async function devCommand(options) {
81
92
  const devProcess = spawn("npx", ["next", "dev", "-p", options.port], {
82
93
  cwd: projectRoot,
83
94
  stdio: "inherit",
95
+ shell: true,
84
96
  env: {
85
97
  ...process.env,
86
98
  NEXTSPARK_CORE_DIR: coreDir
@@ -118,11 +130,11 @@ async function devCommand(options) {
118
130
  // src/commands/build.ts
119
131
  import { spawn as spawn2 } from "child_process";
120
132
  import { existsSync as existsSync2, readFileSync } from "fs";
121
- import { join } from "path";
133
+ import { join as join2 } from "path";
122
134
  import chalk2 from "chalk";
123
135
  import ora2 from "ora";
124
136
  function loadProjectEnv(projectRoot) {
125
- const envPath = join(projectRoot, ".env");
137
+ const envPath = join2(projectRoot, ".env");
126
138
  const envVars = {};
127
139
  if (existsSync2(envPath)) {
128
140
  const content = readFileSync(envPath, "utf-8");
@@ -180,6 +192,7 @@ async function buildCommand(options) {
180
192
  const buildProcess = spawn2("npx", ["next", "build"], {
181
193
  cwd: projectRoot,
182
194
  stdio: "inherit",
195
+ shell: true,
183
196
  env: {
184
197
  ...projectEnv,
185
198
  ...process.env,
@@ -214,11 +227,11 @@ Build failed with exit code ${code}`));
214
227
  // src/commands/generate.ts
215
228
  import { spawn as spawn3 } from "child_process";
216
229
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
217
- import { join as join2 } from "path";
230
+ import { join as join3 } from "path";
218
231
  import chalk3 from "chalk";
219
232
  import ora3 from "ora";
220
233
  function loadProjectEnv2(projectRoot) {
221
- const envPath = join2(projectRoot, ".env");
234
+ const envPath = join3(projectRoot, ".env");
222
235
  const envVars = {};
223
236
  if (existsSync3(envPath)) {
224
237
  const content = readFileSync2(envPath, "utf-8");
@@ -298,11 +311,11 @@ Registry generation failed with exit code ${code}`));
298
311
  // src/commands/registry.ts
299
312
  import { spawn as spawn4 } from "child_process";
300
313
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
301
- import { join as join3 } from "path";
314
+ import { join as join4 } from "path";
302
315
  import chalk4 from "chalk";
303
316
  import ora4 from "ora";
304
317
  function loadProjectEnv3(projectRoot) {
305
- const envPath = join3(projectRoot, ".env");
318
+ const envPath = join4(projectRoot, ".env");
306
319
  const envVars = {};
307
320
  if (existsSync4(envPath)) {
308
321
  const content = readFileSync3(envPath, "utf-8");
@@ -424,32 +437,32 @@ Watcher exited with code ${code}`));
424
437
 
425
438
  // src/commands/init.ts
426
439
  import { existsSync as existsSync8, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync7 } from "fs";
427
- import { join as join8 } from "path";
440
+ import { join as join9 } from "path";
428
441
  import chalk12 from "chalk";
429
442
  import ora8 from "ora";
430
443
 
431
444
  // src/wizard/index.ts
432
445
  import chalk11 from "chalk";
433
446
  import ora7 from "ora";
434
- import { confirm as confirm5 } from "@inquirer/prompts";
447
+ import { confirm as confirm5, select as select7 } from "@inquirer/prompts";
435
448
  import { execSync } from "child_process";
436
449
  import { existsSync as existsSync7, readdirSync } from "fs";
437
- import { join as join7 } from "path";
450
+ import { join as join8 } from "path";
438
451
 
439
452
  // src/wizard/banner.ts
440
453
  import chalk5 from "chalk";
441
454
  import { readFileSync as readFileSync4 } from "fs";
442
455
  import { fileURLToPath as fileURLToPath2 } from "url";
443
- import { dirname as dirname2, join as join4 } from "path";
456
+ import { dirname as dirname2, join as join5 } from "path";
444
457
  var __filename2 = fileURLToPath2(import.meta.url);
445
458
  var __dirname2 = dirname2(__filename2);
446
459
  function getCliVersion() {
447
460
  const possiblePaths = [
448
- join4(__dirname2, "../package.json"),
461
+ join5(__dirname2, "../package.json"),
449
462
  // from dist/
450
- join4(__dirname2, "../../package.json"),
463
+ join5(__dirname2, "../../package.json"),
451
464
  // from dist/wizard/ or src/wizard/
452
- join4(__dirname2, "../../../package.json")
465
+ join5(__dirname2, "../../../package.json")
453
466
  // fallback
454
467
  ];
455
468
  for (const packageJsonPath of possiblePaths) {
@@ -485,6 +498,9 @@ function showSection(title, step, totalSteps) {
485
498
  console.log(chalk5.gray(" " + "-".repeat(40)));
486
499
  console.log("");
487
500
  }
501
+ function showSuccess(message) {
502
+ console.log(chalk5.green(` \u2713 ${message}`));
503
+ }
488
504
  function showWarning(message) {
489
505
  console.log(chalk5.yellow(` \u26A0 ${message}`));
490
506
  }
@@ -528,7 +544,7 @@ function validateSlug(slug) {
528
544
  return true;
529
545
  }
530
546
  async function promptProjectInfo() {
531
- showSection("Project Information", 1, 5);
547
+ showSection("Project Information", 2, 10);
532
548
  const projectName = await input({
533
549
  message: "What is your project name?",
534
550
  default: "My SaaS App",
@@ -551,8 +567,43 @@ async function promptProjectInfo() {
551
567
  };
552
568
  }
553
569
 
570
+ // src/wizard/prompts/project-type.ts
571
+ import { select } from "@inquirer/prompts";
572
+ var PROJECT_TYPE_OPTIONS = [
573
+ {
574
+ name: "Web only",
575
+ value: "web",
576
+ description: "Next.js web application with NextSpark. Standard flat project structure."
577
+ },
578
+ {
579
+ name: "Web + Mobile",
580
+ value: "web-mobile",
581
+ description: "Monorepo with Next.js web app and Expo mobile app sharing the same backend."
582
+ }
583
+ ];
584
+ async function promptProjectType() {
585
+ showSection("Project Type", 1, 10);
586
+ const projectType = await select({
587
+ message: "What type of project do you want to create?",
588
+ choices: PROJECT_TYPE_OPTIONS,
589
+ default: "web"
590
+ });
591
+ const selectedOption = PROJECT_TYPE_OPTIONS.find((o) => o.value === projectType);
592
+ if (selectedOption) {
593
+ showInfo(selectedOption.description);
594
+ }
595
+ if (projectType === "web-mobile") {
596
+ console.log("");
597
+ showInfo("Your project will be a pnpm monorepo with web/ and mobile/ directories.");
598
+ showInfo("The mobile app will use Expo and share the same backend API.");
599
+ }
600
+ return {
601
+ projectType
602
+ };
603
+ }
604
+
554
605
  // src/wizard/prompts/team-config.ts
555
- import { select, checkbox } from "@inquirer/prompts";
606
+ import { select as select2, checkbox } from "@inquirer/prompts";
556
607
 
557
608
  // src/wizard/types.ts
558
609
  var AVAILABLE_LOCALES = {
@@ -600,8 +651,8 @@ var ROLE_OPTIONS = [
600
651
  { name: "Viewer (Read-only access)", value: "viewer", checked: true }
601
652
  ];
602
653
  async function promptTeamConfig() {
603
- showSection("Team Configuration", 2, 5);
604
- const teamMode = await select({
654
+ showSection("Team Configuration", 3, 10);
655
+ const teamMode = await select2({
605
656
  message: "What team mode do you need?",
606
657
  choices: TEAM_MODE_OPTIONS,
607
658
  default: "multi-tenant"
@@ -632,7 +683,7 @@ async function promptTeamConfig() {
632
683
  }
633
684
 
634
685
  // src/wizard/prompts/i18n-config.ts
635
- import { select as select2, checkbox as checkbox2 } from "@inquirer/prompts";
686
+ import { select as select3, checkbox as checkbox2 } from "@inquirer/prompts";
636
687
  var LOCALE_OPTIONS = Object.entries(AVAILABLE_LOCALES).map(([value, name]) => ({
637
688
  name: `${name} (${value})`,
638
689
  value,
@@ -640,7 +691,7 @@ var LOCALE_OPTIONS = Object.entries(AVAILABLE_LOCALES).map(([value, name]) => ({
640
691
  // English selected by default
641
692
  }));
642
693
  async function promptI18nConfig() {
643
- showSection("Internationalization", 3, 5);
694
+ showSection("Internationalization", 4, 10);
644
695
  const supportedLocales = await checkbox2({
645
696
  message: "Which languages do you want to support?",
646
697
  choices: LOCALE_OPTIONS,
@@ -656,7 +707,7 @@ async function promptI18nConfig() {
656
707
  showInfo(`Default language set to ${AVAILABLE_LOCALES[defaultLocale]}`);
657
708
  } else {
658
709
  console.log("");
659
- defaultLocale = await select2({
710
+ defaultLocale = await select3({
660
711
  message: "Which should be the default language?",
661
712
  choices: supportedLocales.map((locale) => ({
662
713
  name: `${AVAILABLE_LOCALES[locale]} (${locale})`,
@@ -672,7 +723,7 @@ async function promptI18nConfig() {
672
723
  }
673
724
 
674
725
  // src/wizard/prompts/billing-config.ts
675
- import { select as select3 } from "@inquirer/prompts";
726
+ import { select as select4 } from "@inquirer/prompts";
676
727
  var BILLING_MODEL_OPTIONS = [
677
728
  {
678
729
  name: "Free (No payments)",
@@ -691,8 +742,8 @@ var BILLING_MODEL_OPTIONS = [
691
742
  }
692
743
  ];
693
744
  async function promptBillingConfig() {
694
- showSection("Billing Configuration", 4, 5);
695
- const billingModel = await select3({
745
+ showSection("Billing Configuration", 5, 10);
746
+ const billingModel = await select4({
696
747
  message: "What billing model do you want to use?",
697
748
  choices: BILLING_MODEL_OPTIONS,
698
749
  default: "freemium"
@@ -704,7 +755,7 @@ async function promptBillingConfig() {
704
755
  let currency = "usd";
705
756
  if (billingModel !== "free") {
706
757
  console.log("");
707
- currency = await select3({
758
+ currency = await select4({
708
759
  message: "What currency will you use?",
709
760
  choices: CURRENCIES.map((c) => ({
710
761
  name: c.label,
@@ -756,7 +807,7 @@ var FEATURE_OPTIONS = [
756
807
  }
757
808
  ];
758
809
  async function promptFeaturesConfig() {
759
- showSection("Features", 5, 5);
810
+ showSection("Features", 6, 10);
760
811
  showInfo("Select the features you want to include in your project.");
761
812
  showInfo("You can add or remove features later by editing your config files.");
762
813
  console.log("");
@@ -793,7 +844,7 @@ var CONTENT_FEATURE_OPTIONS = [
793
844
  }
794
845
  ];
795
846
  async function promptContentFeaturesConfig(mode = "interactive", totalSteps = 9) {
796
- showSection("Content Features", 6, totalSteps);
847
+ showSection("Content Features", 7, totalSteps);
797
848
  showInfo("Enable optional content features for your project.");
798
849
  showInfo("These features add entities and blocks for building pages and blog posts.");
799
850
  console.log("");
@@ -813,7 +864,24 @@ async function promptContentFeaturesConfig(mode = "interactive", totalSteps = 9)
813
864
  }
814
865
 
815
866
  // src/wizard/prompts/auth-config.ts
816
- import { checkbox as checkbox5 } from "@inquirer/prompts";
867
+ import { checkbox as checkbox5, input as input2, select as select5 } from "@inquirer/prompts";
868
+ var REGISTRATION_MODE_OPTIONS = [
869
+ {
870
+ name: "Open (anyone can register)",
871
+ value: "open",
872
+ description: "Email+password and Google OAuth signup available to everyone"
873
+ },
874
+ {
875
+ name: "Domain-Restricted (Google OAuth only for specific domains)",
876
+ value: "domain-restricted",
877
+ description: "Only Google OAuth for allowed email domains (e.g., @yourcompany.com)"
878
+ },
879
+ {
880
+ name: "Invitation-Only (registration via invite link)",
881
+ value: "invitation-only",
882
+ description: "Users can only register when invited by an existing user"
883
+ }
884
+ ];
817
885
  var AUTH_METHOD_OPTIONS = [
818
886
  {
819
887
  name: "Email & Password",
@@ -838,19 +906,50 @@ var SECURITY_OPTIONS = [
838
906
  ];
839
907
  function getDefaultAuthConfig() {
840
908
  return {
909
+ registrationMode: "open",
841
910
  emailPassword: true,
842
911
  googleOAuth: false,
843
912
  emailVerification: true
844
913
  };
845
914
  }
846
915
  async function promptAuthConfig(mode = "interactive", totalSteps = 8) {
847
- showSection("Authentication", 6, totalSteps);
916
+ showSection("Authentication", 8, totalSteps);
848
917
  showInfo("Configure how users will authenticate to your application.");
849
918
  console.log("");
850
- const selectedMethods = await checkbox5({
851
- message: "Which authentication methods do you want to enable?",
852
- choices: AUTH_METHOD_OPTIONS
919
+ const registrationMode = await select5({
920
+ message: "How should new users register?",
921
+ choices: REGISTRATION_MODE_OPTIONS,
922
+ default: "open"
853
923
  });
924
+ console.log("");
925
+ let emailPassword = true;
926
+ let googleOAuth = false;
927
+ let allowedDomains;
928
+ if (registrationMode === "domain-restricted") {
929
+ googleOAuth = true;
930
+ emailPassword = false;
931
+ showInfo("Domain-restricted mode: Google OAuth enabled, email login hidden on login page.");
932
+ console.log("");
933
+ const domainsInput = await input2({
934
+ message: "Allowed email domains (comma-separated, e.g. yourcompany.com, partner.org):",
935
+ validate: (value) => {
936
+ if (!value.trim()) return "At least one domain is required for domain-restricted mode";
937
+ const domains = value.split(",").map((d) => d.trim()).filter(Boolean);
938
+ const invalid = domains.find((d) => !d.includes("."));
939
+ if (invalid) return `Invalid domain: "${invalid}" (must contain a dot)`;
940
+ return true;
941
+ }
942
+ });
943
+ allowedDomains = domainsInput.split(",").map((d) => d.trim().toLowerCase()).filter(Boolean);
944
+ console.log("");
945
+ } else {
946
+ const selectedMethods = await checkbox5({
947
+ message: "Which authentication methods do you want to enable?",
948
+ choices: AUTH_METHOD_OPTIONS
949
+ });
950
+ emailPassword = selectedMethods.includes("emailPassword");
951
+ googleOAuth = selectedMethods.includes("googleOAuth");
952
+ }
854
953
  let selectedSecurity = ["emailVerification"];
855
954
  if (mode === "expert") {
856
955
  console.log("");
@@ -862,9 +961,11 @@ async function promptAuthConfig(mode = "interactive", totalSteps = 8) {
862
961
  });
863
962
  }
864
963
  const auth = {
865
- emailPassword: selectedMethods.includes("emailPassword"),
866
- googleOAuth: selectedMethods.includes("googleOAuth"),
867
- emailVerification: selectedSecurity.includes("emailVerification")
964
+ registrationMode,
965
+ emailPassword,
966
+ googleOAuth,
967
+ emailVerification: selectedSecurity.includes("emailVerification"),
968
+ allowedDomains
868
969
  };
869
970
  return { auth };
870
971
  }
@@ -928,7 +1029,7 @@ function getDefaultDashboardConfig() {
928
1029
  };
929
1030
  }
930
1031
  async function promptDashboardConfig(mode = "interactive", totalSteps = 8) {
931
- showSection("Dashboard", 7, totalSteps);
1032
+ showSection("Dashboard", 9, totalSteps);
932
1033
  showInfo("Configure your dashboard user interface.");
933
1034
  showInfo("These settings can be changed later in your theme config.");
934
1035
  console.log("");
@@ -980,7 +1081,7 @@ function getDefaultDevConfig() {
980
1081
  };
981
1082
  }
982
1083
  async function promptDevConfig(mode = "interactive", totalSteps = 8) {
983
- showSection("Development Tools", 8, totalSteps);
1084
+ showSection("Development Tools", 10, totalSteps);
984
1085
  showInfo("Configure development and debugging tools.");
985
1086
  showWarning("These tools are disabled in production builds.");
986
1087
  console.log("");
@@ -1007,7 +1108,7 @@ async function promptDevConfig(mode = "interactive", totalSteps = 8) {
1007
1108
  }
1008
1109
 
1009
1110
  // src/wizard/prompts/theme-selection.ts
1010
- import { select as select4 } from "@inquirer/prompts";
1111
+ import { select as select6 } from "@inquirer/prompts";
1011
1112
  import chalk6 from "chalk";
1012
1113
  async function promptThemeSelection() {
1013
1114
  console.log("");
@@ -1017,7 +1118,7 @@ async function promptThemeSelection() {
1017
1118
  console.log(chalk6.gray(" A reference theme provides a complete example to learn from."));
1018
1119
  console.log(chalk6.gray(" Your custom theme (based on starter) will be your active theme."));
1019
1120
  console.log("");
1020
- const theme = await select4({
1121
+ const theme = await select6({
1021
1122
  message: "Which reference theme would you like to install?",
1022
1123
  choices: [
1023
1124
  {
@@ -1103,24 +1204,26 @@ function getRequiredPlugins(theme) {
1103
1204
  }
1104
1205
 
1105
1206
  // src/wizard/prompts/env-config.ts
1106
- import { confirm as confirm3, input as input2 } from "@inquirer/prompts";
1207
+ import { confirm as confirm3, input as input3 } from "@inquirer/prompts";
1107
1208
 
1108
1209
  // src/wizard/prompts/git-config.ts
1109
- import { confirm as confirm4, input as input3 } from "@inquirer/prompts";
1210
+ import { confirm as confirm4, input as input4 } from "@inquirer/prompts";
1110
1211
 
1111
1212
  // src/wizard/prompts/index.ts
1112
1213
  async function runAllPrompts() {
1214
+ const projectTypeConfig = await promptProjectType();
1113
1215
  const projectInfo = await promptProjectInfo();
1114
1216
  const teamConfig = await promptTeamConfig();
1115
1217
  const i18nConfig = await promptI18nConfig();
1116
1218
  const billingConfig = await promptBillingConfig();
1117
1219
  const featuresConfig = await promptFeaturesConfig();
1118
- const contentFeaturesConfig = await promptContentFeaturesConfig("interactive", 9);
1119
- const authConfig = await promptAuthConfig("interactive", 9);
1120
- const dashboardConfig = await promptDashboardConfig("interactive", 9);
1121
- const devConfig = await promptDevConfig("interactive", 9);
1220
+ const contentFeaturesConfig = await promptContentFeaturesConfig("interactive", 10);
1221
+ const authConfig = await promptAuthConfig("interactive", 10);
1222
+ const dashboardConfig = await promptDashboardConfig("interactive", 10);
1223
+ const devConfig = await promptDevConfig("interactive", 10);
1122
1224
  return {
1123
1225
  ...projectInfo,
1226
+ ...projectTypeConfig,
1124
1227
  ...teamConfig,
1125
1228
  ...i18nConfig,
1126
1229
  ...billingConfig,
@@ -1132,17 +1235,19 @@ async function runAllPrompts() {
1132
1235
  };
1133
1236
  }
1134
1237
  async function runQuickPrompts() {
1238
+ const projectTypeConfig = await promptProjectType();
1135
1239
  const projectInfo = await promptProjectInfo();
1136
1240
  const teamConfig = await promptTeamConfig();
1137
1241
  const i18nConfig = await promptI18nConfig();
1138
1242
  const billingConfig = await promptBillingConfig();
1139
1243
  const featuresConfig = await promptFeaturesConfig();
1140
- const contentFeaturesConfig = await promptContentFeaturesConfig("quick", 9);
1244
+ const contentFeaturesConfig = await promptContentFeaturesConfig("quick", 10);
1141
1245
  const authConfig = { auth: getDefaultAuthConfig() };
1142
1246
  const dashboardConfig = { dashboard: getDefaultDashboardConfig() };
1143
1247
  const devConfig = { dev: getDefaultDevConfig() };
1144
1248
  return {
1145
1249
  ...projectInfo,
1250
+ ...projectTypeConfig,
1146
1251
  ...teamConfig,
1147
1252
  ...i18nConfig,
1148
1253
  ...billingConfig,
@@ -1154,17 +1259,19 @@ async function runQuickPrompts() {
1154
1259
  };
1155
1260
  }
1156
1261
  async function runExpertPrompts() {
1262
+ const projectTypeConfig = await promptProjectType();
1157
1263
  const projectInfo = await promptProjectInfo();
1158
1264
  const teamConfig = await promptTeamConfig();
1159
1265
  const i18nConfig = await promptI18nConfig();
1160
1266
  const billingConfig = await promptBillingConfig();
1161
1267
  const featuresConfig = await promptFeaturesConfig();
1162
- const contentFeaturesConfig = await promptContentFeaturesConfig("expert", 9);
1163
- const authConfig = await promptAuthConfig("expert", 9);
1164
- const dashboardConfig = await promptDashboardConfig("expert", 9);
1165
- const devConfig = await promptDevConfig("expert", 9);
1268
+ const contentFeaturesConfig = await promptContentFeaturesConfig("expert", 10);
1269
+ const authConfig = await promptAuthConfig("expert", 10);
1270
+ const dashboardConfig = await promptDashboardConfig("expert", 10);
1271
+ const devConfig = await promptDevConfig("expert", 10);
1166
1272
  return {
1167
1273
  ...projectInfo,
1274
+ ...projectTypeConfig,
1168
1275
  ...teamConfig,
1169
1276
  ...i18nConfig,
1170
1277
  ...billingConfig,
@@ -1177,9 +1284,9 @@ async function runExpertPrompts() {
1177
1284
  }
1178
1285
 
1179
1286
  // src/wizard/generators/index.ts
1180
- import fs7 from "fs-extra";
1181
- import path5 from "path";
1182
- import { fileURLToPath as fileURLToPath5 } from "url";
1287
+ import fs8 from "fs-extra";
1288
+ import path7 from "path";
1289
+ import { fileURLToPath as fileURLToPath7 } from "url";
1183
1290
 
1184
1291
  // src/wizard/generators/theme-renamer.ts
1185
1292
  import fs from "fs-extra";
@@ -1188,22 +1295,22 @@ import { fileURLToPath as fileURLToPath3 } from "url";
1188
1295
  var __filename3 = fileURLToPath3(import.meta.url);
1189
1296
  var __dirname3 = path.dirname(__filename3);
1190
1297
  function getTemplatesDir() {
1191
- try {
1192
- const corePkgPath = __require.resolve("@nextsparkjs/core/package.json");
1193
- return path.join(path.dirname(corePkgPath), "templates");
1194
- } catch {
1195
- const possiblePaths = [
1196
- path.resolve(__dirname3, "../../../../../core/templates"),
1197
- path.resolve(__dirname3, "../../../../core/templates"),
1198
- path.resolve(process.cwd(), "node_modules/@nextsparkjs/core/templates")
1199
- ];
1200
- for (const p of possiblePaths) {
1201
- if (fs.existsSync(p)) {
1202
- return p;
1203
- }
1298
+ const rootDir = process.cwd();
1299
+ const possiblePaths = [
1300
+ // From project root node_modules (most common for installed packages)
1301
+ path.resolve(rootDir, "node_modules/@nextsparkjs/core/templates"),
1302
+ // From CLI dist folder for development
1303
+ path.resolve(__dirname3, "../../core/templates"),
1304
+ // Legacy paths for different build structures
1305
+ path.resolve(__dirname3, "../../../../../core/templates"),
1306
+ path.resolve(__dirname3, "../../../../core/templates")
1307
+ ];
1308
+ for (const p of possiblePaths) {
1309
+ if (fs.existsSync(p)) {
1310
+ return p;
1204
1311
  }
1205
- throw new Error("Could not find @nextsparkjs/core templates directory");
1206
1312
  }
1313
+ throw new Error(`Could not find @nextsparkjs/core templates directory. Searched: ${possiblePaths.join(", ")}`);
1207
1314
  }
1208
1315
  function getTargetThemesDir() {
1209
1316
  return path.resolve(process.cwd(), "contents", "themes");
@@ -1478,6 +1585,33 @@ async function updateMigrations(config2) {
1478
1585
  await fs.writeFile(filePath, content, "utf-8");
1479
1586
  }
1480
1587
  }
1588
+ async function updateTestFiles(config2) {
1589
+ const testsDir = path.join(getTargetThemesDir(), config2.projectSlug, "tests");
1590
+ if (!await fs.pathExists(testsDir)) {
1591
+ return;
1592
+ }
1593
+ const processDir = async (dir) => {
1594
+ const items = await fs.readdir(dir);
1595
+ for (const item of items) {
1596
+ const itemPath = path.join(dir, item);
1597
+ const stat = await fs.stat(itemPath);
1598
+ if (stat.isDirectory()) {
1599
+ await processDir(itemPath);
1600
+ } else if (item.endsWith(".ts") || item.endsWith(".tsx")) {
1601
+ let content = await fs.readFile(itemPath, "utf-8");
1602
+ const hasChanges = content.includes("@/contents/themes/starter/");
1603
+ if (hasChanges) {
1604
+ content = content.replace(
1605
+ /@\/contents\/themes\/starter\//g,
1606
+ `@/contents/themes/${config2.projectSlug}/`
1607
+ );
1608
+ await fs.writeFile(itemPath, content, "utf-8");
1609
+ }
1610
+ }
1611
+ }
1612
+ };
1613
+ await processDir(testsDir);
1614
+ }
1481
1615
  function toCamelCase(str) {
1482
1616
  return str.split("-").map((word, index) => {
1483
1617
  if (index === 0) {
@@ -1494,29 +1628,32 @@ function getTargetThemesDir2() {
1494
1628
  return path2.resolve(process.cwd(), "contents", "themes");
1495
1629
  }
1496
1630
  async function updateAuthConfig(config2) {
1497
- const authConfigPath = path2.join(
1631
+ const appConfigPath = path2.join(
1498
1632
  getTargetThemesDir2(),
1499
1633
  config2.projectSlug,
1500
1634
  "config",
1501
- "auth.config.ts"
1635
+ "app.config.ts"
1502
1636
  );
1503
- if (!await fs2.pathExists(authConfigPath)) {
1637
+ if (!await fs2.pathExists(appConfigPath)) {
1504
1638
  return;
1505
1639
  }
1506
- let content = await fs2.readFile(authConfigPath, "utf-8");
1640
+ let content = await fs2.readFile(appConfigPath, "utf-8");
1507
1641
  content = content.replace(
1508
- /(emailPassword:\s*{[^}]*enabled:\s*)(?:true|false)/gs,
1509
- `$1${config2.auth.emailPassword}`
1642
+ /(mode:\s*)'open'(\s*as\s*const)/,
1643
+ `$1'${config2.auth.registrationMode}'$2`
1510
1644
  );
1511
1645
  content = content.replace(
1512
- /(google:\s*{[^}]*enabled:\s*)(?:true|false)/gs,
1646
+ /(google:\s*{\s*enabled:\s*)(?:true|false)/s,
1513
1647
  `$1${config2.auth.googleOAuth}`
1514
1648
  );
1515
- content = content.replace(
1516
- /(emailVerification:\s*)(?:true|false)/g,
1517
- `$1${config2.auth.emailVerification}`
1518
- );
1519
- await fs2.writeFile(authConfigPath, content, "utf-8");
1649
+ if (config2.auth.registrationMode === "domain-restricted") {
1650
+ const domains = config2.auth.allowedDomains?.length ? config2.auth.allowedDomains.map((d) => `'${d}'`).join(", ") : "'yourcompany.com'";
1651
+ content = content.replace(
1652
+ /\/\/\s*allowedDomains:\s*\['yourcompany\.com'\],\s*\/\/\s*Only for 'domain-restricted' mode/,
1653
+ `allowedDomains: [${domains}],`
1654
+ );
1655
+ }
1656
+ await fs2.writeFile(appConfigPath, content, "utf-8");
1520
1657
  }
1521
1658
  async function updateDashboardUIConfig(config2) {
1522
1659
  const dashboardConfigPath = path2.join(
@@ -1607,6 +1744,44 @@ async function updatePermissionsConfig(config2) {
1607
1744
  });
1608
1745
  await fs2.writeFile(permissionsConfigPath, content, "utf-8");
1609
1746
  }
1747
+ async function updateEntityPermissions(config2) {
1748
+ const permissionsConfigPath = path2.join(
1749
+ getTargetThemesDir2(),
1750
+ config2.projectSlug,
1751
+ "config",
1752
+ "permissions.config.ts"
1753
+ );
1754
+ if (!await fs2.pathExists(permissionsConfigPath)) {
1755
+ return;
1756
+ }
1757
+ let content = await fs2.readFile(permissionsConfigPath, "utf-8");
1758
+ if (config2.contentFeatures.pages) {
1759
+ content = uncommentPermissionBlock(content, "PAGES");
1760
+ }
1761
+ if (config2.contentFeatures.blog) {
1762
+ content = uncommentPermissionBlock(content, "POSTS");
1763
+ }
1764
+ await fs2.writeFile(permissionsConfigPath, content, "utf-8");
1765
+ }
1766
+ function uncommentPermissionBlock(content, markerName) {
1767
+ const startMarker = `// __${markerName}_PERMISSIONS_START__`;
1768
+ const endMarker = `// __${markerName}_PERMISSIONS_END__`;
1769
+ const startIndex = content.indexOf(startMarker);
1770
+ const endIndex = content.indexOf(endMarker);
1771
+ if (startIndex === -1 || endIndex === -1) {
1772
+ return content;
1773
+ }
1774
+ const beforeBlock = content.slice(0, startIndex);
1775
+ const block = content.slice(startIndex + startMarker.length, endIndex);
1776
+ const afterBlock = content.slice(endIndex + endMarker.length);
1777
+ const uncommentedBlock = block.split("\n").map((line) => {
1778
+ if (line.match(/^\s*\/\/\s+/)) {
1779
+ return line.replace(/^(\s*)\/\/\s*/, "$1");
1780
+ }
1781
+ return line;
1782
+ }).join("\n");
1783
+ return beforeBlock + uncommentedBlock + afterBlock;
1784
+ }
1610
1785
  async function updateDashboardConfig(config2) {
1611
1786
  const dashboardConfigPath = path2.join(
1612
1787
  getTargetThemesDir2(),
@@ -1724,6 +1899,18 @@ async function copyEnvExampleToEnv() {
1724
1899
  await fs2.copy(envExamplePath, envPath);
1725
1900
  }
1726
1901
  }
1902
+ async function updateGlobalsCss(config2) {
1903
+ const globalsCssPath = path2.resolve(process.cwd(), "app", "globals.css");
1904
+ if (!await fs2.pathExists(globalsCssPath)) {
1905
+ return;
1906
+ }
1907
+ let content = await fs2.readFile(globalsCssPath, "utf-8");
1908
+ content = content.replace(
1909
+ /@import\s+["']\.\.\/contents\/themes\/[^/]+\/styles\/globals\.css["'];?/,
1910
+ `@import "../contents/themes/${config2.projectSlug}/styles/globals.css";`
1911
+ );
1912
+ await fs2.writeFile(globalsCssPath, content, "utf-8");
1913
+ }
1727
1914
 
1728
1915
  // src/wizard/generators/messages-generator.ts
1729
1916
  import fs3 from "fs-extra";
@@ -1822,22 +2009,22 @@ import { fileURLToPath as fileURLToPath4 } from "url";
1822
2009
  var __filename4 = fileURLToPath4(import.meta.url);
1823
2010
  var __dirname4 = path4.dirname(__filename4);
1824
2011
  function getTemplatesDir2() {
1825
- try {
1826
- const corePkgPath = __require.resolve("@nextsparkjs/core/package.json");
1827
- return path4.join(path4.dirname(corePkgPath), "templates");
1828
- } catch {
1829
- const possiblePaths = [
1830
- path4.resolve(__dirname4, "../../../../../core/templates"),
1831
- path4.resolve(__dirname4, "../../../../core/templates"),
1832
- path4.resolve(process.cwd(), "node_modules/@nextsparkjs/core/templates")
1833
- ];
1834
- for (const p of possiblePaths) {
1835
- if (fs4.existsSync(p)) {
1836
- return p;
1837
- }
2012
+ const rootDir = process.cwd();
2013
+ const possiblePaths = [
2014
+ // From project root node_modules (most common for installed packages)
2015
+ path4.resolve(rootDir, "node_modules/@nextsparkjs/core/templates"),
2016
+ // From CLI dist folder for development
2017
+ path4.resolve(__dirname4, "../../core/templates"),
2018
+ // Legacy paths for different build structures
2019
+ path4.resolve(__dirname4, "../../../../../core/templates"),
2020
+ path4.resolve(__dirname4, "../../../../core/templates")
2021
+ ];
2022
+ for (const p of possiblePaths) {
2023
+ if (fs4.existsSync(p)) {
2024
+ return p;
1838
2025
  }
1839
- throw new Error("Could not find @nextsparkjs/core templates directory");
1840
2026
  }
2027
+ throw new Error(`Could not find @nextsparkjs/core templates directory. Searched: ${possiblePaths.join(", ")}`);
1841
2028
  }
1842
2029
  function getFeaturesDir() {
1843
2030
  return path4.join(getTemplatesDir2(), "features");
@@ -1895,7 +2082,7 @@ async function copyContentFeatures(config2) {
1895
2082
 
1896
2083
  // src/wizard/generators/theme-plugins-installer.ts
1897
2084
  import { existsSync as existsSync6, cpSync, mkdirSync, readFileSync as readFileSync6, writeFileSync } from "fs";
1898
- import { join as join6, resolve as resolve2 } from "path";
2085
+ import { join as join7, resolve as resolve2 } from "path";
1899
2086
  import chalk9 from "chalk";
1900
2087
  import ora6 from "ora";
1901
2088
 
@@ -1903,12 +2090,12 @@ import ora6 from "ora";
1903
2090
  import chalk8 from "chalk";
1904
2091
  import ora5 from "ora";
1905
2092
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
1906
- import { join as join5 } from "path";
2093
+ import { join as join6 } from "path";
1907
2094
  async function addTheme(packageSpec, options = {}) {
1908
2095
  const spinner = ora5(`Adding theme ${packageSpec}`).start();
1909
2096
  let cleanup = null;
1910
2097
  try {
1911
- const contentsDir = join5(process.cwd(), "contents");
2098
+ const contentsDir = join6(process.cwd(), "contents");
1912
2099
  if (!existsSync5(contentsDir)) {
1913
2100
  spinner.fail('contents/ directory not found. Run "nextspark init" first.');
1914
2101
  return;
@@ -1971,14 +2158,14 @@ async function addTheme(packageSpec, options = {}) {
1971
2158
  }
1972
2159
  function checkPluginExists(pluginName) {
1973
2160
  const name = pluginName.replace(/^@[^/]+\//, "").replace(/^nextspark-plugin-/, "").replace(/^plugin-/, "");
1974
- return existsSync5(join5(process.cwd(), "contents", "plugins", name));
2161
+ return existsSync5(join6(process.cwd(), "contents", "plugins", name));
1975
2162
  }
1976
2163
  function getCoreVersion() {
1977
- const pkgPath = join5(process.cwd(), "node_modules", "@nextsparkjs", "core", "package.json");
2164
+ const pkgPath = join6(process.cwd(), "node_modules", "@nextsparkjs", "core", "package.json");
1978
2165
  if (existsSync5(pkgPath)) {
1979
2166
  try {
1980
- const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
1981
- return pkg.version || "0.0.0";
2167
+ const pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
2168
+ return pkg2.version || "0.0.0";
1982
2169
  } catch {
1983
2170
  return "0.0.0";
1984
2171
  }
@@ -2015,20 +2202,20 @@ var THEME_REQUIRED_PLUGINS2 = {
2015
2202
  };
2016
2203
  function isMonorepoMode2() {
2017
2204
  const possiblePaths = [
2018
- join6(process.cwd(), "pnpm-workspace.yaml"),
2019
- join6(process.cwd(), "..", "pnpm-workspace.yaml"),
2020
- join6(process.cwd(), "..", "..", "pnpm-workspace.yaml")
2205
+ join7(process.cwd(), "pnpm-workspace.yaml"),
2206
+ join7(process.cwd(), "..", "pnpm-workspace.yaml"),
2207
+ join7(process.cwd(), "..", "..", "pnpm-workspace.yaml")
2021
2208
  ];
2022
2209
  return possiblePaths.some((p) => existsSync6(p));
2023
2210
  }
2024
2211
  function getMonorepoRoot() {
2025
2212
  const possibleRoots = [
2026
2213
  process.cwd(),
2027
- join6(process.cwd(), ".."),
2028
- join6(process.cwd(), "..", "..")
2214
+ join7(process.cwd(), ".."),
2215
+ join7(process.cwd(), "..", "..")
2029
2216
  ];
2030
2217
  for (const root of possibleRoots) {
2031
- if (existsSync6(join6(root, "pnpm-workspace.yaml"))) {
2218
+ if (existsSync6(join7(root, "pnpm-workspace.yaml"))) {
2032
2219
  return resolve2(root);
2033
2220
  }
2034
2221
  }
@@ -2038,15 +2225,15 @@ function getLocalPackageDir(type, name) {
2038
2225
  const monorepoRoot = getMonorepoRoot();
2039
2226
  if (!monorepoRoot) return null;
2040
2227
  const baseDir = type === "theme" ? "themes" : "plugins";
2041
- const packageDir = join6(monorepoRoot, baseDir, name);
2042
- if (existsSync6(packageDir) && existsSync6(join6(packageDir, "package.json"))) {
2228
+ const packageDir = join7(monorepoRoot, baseDir, name);
2229
+ if (existsSync6(packageDir) && existsSync6(join7(packageDir, "package.json"))) {
2043
2230
  return packageDir;
2044
2231
  }
2045
2232
  return null;
2046
2233
  }
2047
2234
  async function copyLocalTheme(name, sourceDir) {
2048
- const targetDir = join6(process.cwd(), "contents", "themes", name);
2049
- const themesDir = join6(process.cwd(), "contents", "themes");
2235
+ const targetDir = join7(process.cwd(), "contents", "themes", name);
2236
+ const themesDir = join7(process.cwd(), "contents", "themes");
2050
2237
  if (!existsSync6(themesDir)) {
2051
2238
  mkdirSync(themesDir, { recursive: true });
2052
2239
  }
@@ -2059,8 +2246,8 @@ async function copyLocalTheme(name, sourceDir) {
2059
2246
  return true;
2060
2247
  }
2061
2248
  async function copyLocalPlugin(name, sourceDir) {
2062
- const targetDir = join6(process.cwd(), "contents", "plugins", name);
2063
- const pluginsDir = join6(process.cwd(), "contents", "plugins");
2249
+ const targetDir = join7(process.cwd(), "contents", "plugins", name);
2250
+ const pluginsDir = join7(process.cwd(), "contents", "plugins");
2064
2251
  if (!existsSync6(pluginsDir)) {
2065
2252
  mkdirSync(pluginsDir, { recursive: true });
2066
2253
  }
@@ -2073,7 +2260,7 @@ async function copyLocalPlugin(name, sourceDir) {
2073
2260
  return true;
2074
2261
  }
2075
2262
  async function updateTsConfigPaths(name, type) {
2076
- const tsconfigPath = join6(process.cwd(), "tsconfig.json");
2263
+ const tsconfigPath = join7(process.cwd(), "tsconfig.json");
2077
2264
  if (!existsSync6(tsconfigPath)) {
2078
2265
  return;
2079
2266
  }
@@ -2118,7 +2305,7 @@ async function installTheme2(theme) {
2118
2305
  prefixText: " "
2119
2306
  }).start();
2120
2307
  try {
2121
- const targetDir = join6(process.cwd(), "contents", "themes", theme);
2308
+ const targetDir = join7(process.cwd(), "contents", "themes", theme);
2122
2309
  if (existsSync6(targetDir)) {
2123
2310
  spinner.info(chalk9.gray(`Reference theme ${theme} already exists`));
2124
2311
  return true;
@@ -2171,7 +2358,7 @@ async function installPlugins(plugins) {
2171
2358
  prefixText: " "
2172
2359
  }).start();
2173
2360
  try {
2174
- const pluginDir = join6(process.cwd(), "contents", "plugins", plugin);
2361
+ const pluginDir = join7(process.cwd(), "contents", "plugins", plugin);
2175
2362
  if (existsSync6(pluginDir)) {
2176
2363
  spinner.info(chalk9.gray(`Plugin ${plugin} already installed`));
2177
2364
  continue;
@@ -2231,62 +2418,608 @@ async function installThemeAndPlugins(theme, plugins) {
2231
2418
 
2232
2419
  // src/wizard/generators/env-setup.ts
2233
2420
  import fs5 from "fs-extra";
2421
+ import path5 from "path";
2422
+ import { fileURLToPath as fileURLToPath5 } from "url";
2423
+ var __filename5 = fileURLToPath5(import.meta.url);
2424
+ var __dirname5 = path5.dirname(__filename5);
2425
+ var ENV_TEMPLATE_PATH = path5.resolve(__dirname5, "../../../templates/env.template");
2234
2426
 
2235
2427
  // src/wizard/generators/git-init.ts
2236
2428
  import fs6 from "fs-extra";
2237
2429
 
2430
+ // src/wizard/generators/monorepo-generator.ts
2431
+ import fs7 from "fs-extra";
2432
+ import path6 from "path";
2433
+ import { fileURLToPath as fileURLToPath6 } from "url";
2434
+ var __filename6 = fileURLToPath6(import.meta.url);
2435
+ var __dirname6 = path6.dirname(__filename6);
2436
+ var DIRS = {
2437
+ WEB: "web",
2438
+ MOBILE: "mobile",
2439
+ ASSETS: "assets"
2440
+ };
2441
+ var FILES = {
2442
+ PNPM_WORKSPACE: "pnpm-workspace.yaml",
2443
+ NPMRC: ".npmrc",
2444
+ GITIGNORE: ".gitignore",
2445
+ TSCONFIG: "tsconfig.json",
2446
+ PACKAGE_JSON: "package.json",
2447
+ README: "README.md",
2448
+ APP_CONFIG: "app.config.ts"
2449
+ };
2450
+ var REQUIRED_MOBILE_TEMPLATE_FILES = [
2451
+ "app",
2452
+ "src",
2453
+ "babel.config.js",
2454
+ "metro.config.js"
2455
+ ];
2456
+ var MOBILE_TEMPLATE_FILES = [
2457
+ "app",
2458
+ "src",
2459
+ "assets",
2460
+ "babel.config.js",
2461
+ "metro.config.js",
2462
+ "tailwind.config.js",
2463
+ "tsconfig.json",
2464
+ "jest.config.js",
2465
+ "eas.json"
2466
+ ];
2467
+ var VERSIONS = {
2468
+ // NextSpark packages - use 'latest' for consistency with web packages
2469
+ NEXTSPARK_MOBILE: "latest",
2470
+ NEXTSPARK_UI: "latest",
2471
+ // Core dependencies
2472
+ TANSTACK_QUERY: "^5.62.0",
2473
+ EXPO: "^54.0.0",
2474
+ REACT: "19.1.0",
2475
+ REACT_NATIVE: "0.81.5",
2476
+ TYPESCRIPT: "^5.3.0",
2477
+ // Expo modules (use ~ for patch compatibility)
2478
+ EXPO_CONSTANTS: "~18.0.13",
2479
+ EXPO_LINKING: "~8.0.11",
2480
+ EXPO_ROUTER: "~6.0.22",
2481
+ EXPO_SECURE_STORE: "~15.0.8",
2482
+ EXPO_STATUS_BAR: "~3.0.9",
2483
+ // React Native modules
2484
+ RN_GESTURE_HANDLER: "~2.28.0",
2485
+ RN_REANIMATED: "~4.1.1",
2486
+ RN_SAFE_AREA: "~5.6.0",
2487
+ RN_SCREENS: "~4.16.0",
2488
+ RN_SVG: "15.12.1",
2489
+ RN_WEB: "^0.21.0",
2490
+ // Styling
2491
+ NATIVEWIND: "^4.2.1",
2492
+ TAILWINDCSS: "^3",
2493
+ TAILWIND_MERGE: "^3.4.0",
2494
+ LUCIDE_RN: "^0.563.0",
2495
+ // Dev dependencies
2496
+ BABEL_CORE: "^7.25.0",
2497
+ JEST: "^29.7.0",
2498
+ JEST_EXPO: "^54.0.16",
2499
+ TESTING_LIBRARY_JEST_NATIVE: "^5.4.3",
2500
+ TESTING_LIBRARY_RN: "^13.3.3"
2501
+ };
2502
+ function getMobileTemplatesDir() {
2503
+ const possiblePaths = [
2504
+ // From CWD node_modules (installed package)
2505
+ path6.resolve(process.cwd(), "node_modules/@nextsparkjs/mobile/templates"),
2506
+ // From CLI dist folder: ../../mobile/templates (development)
2507
+ path6.resolve(__dirname6, "../../mobile/templates"),
2508
+ // Legacy paths for different build structures
2509
+ path6.resolve(__dirname6, "../../../../../mobile/templates"),
2510
+ path6.resolve(__dirname6, "../../../../mobile/templates")
2511
+ ];
2512
+ for (const p of possiblePaths) {
2513
+ if (fs7.existsSync(p)) {
2514
+ return p;
2515
+ }
2516
+ }
2517
+ const searchedPaths = possiblePaths.map((p) => ` - ${p}`).join("\n");
2518
+ throw new Error(
2519
+ `Could not find @nextsparkjs/mobile templates directory.
2520
+
2521
+ Searched paths:
2522
+ ${searchedPaths}
2523
+
2524
+ To fix this, ensure @nextsparkjs/mobile is installed:
2525
+ pnpm add @nextsparkjs/mobile
2526
+
2527
+ If you're developing locally, make sure the mobile package is built:
2528
+ cd packages/mobile && pnpm build`
2529
+ );
2530
+ }
2531
+ async function validateMobileTemplate(templateDir) {
2532
+ const missing = [];
2533
+ for (const file of REQUIRED_MOBILE_TEMPLATE_FILES) {
2534
+ const filePath = path6.join(templateDir, file);
2535
+ if (!await fs7.pathExists(filePath)) {
2536
+ missing.push(file);
2537
+ }
2538
+ }
2539
+ if (missing.length > 0) {
2540
+ throw new Error(
2541
+ `Mobile template is incomplete. Missing required files:
2542
+ ` + missing.map((f) => ` - ${f}`).join("\n") + `
2543
+
2544
+ Template location: ${templateDir}
2545
+ Please ensure @nextsparkjs/mobile is properly installed and built.`
2546
+ );
2547
+ }
2548
+ }
2549
+ function slugToBundleId(slug) {
2550
+ return slug.toLowerCase().replace(/[^a-z0-9]+/g, ".").replace(/^\.+|\.+$/g, "").replace(/\.{2,}/g, ".");
2551
+ }
2552
+ async function createRootPackageJson(targetDir, config2) {
2553
+ const rootPkg = {
2554
+ name: config2.projectSlug,
2555
+ version: "0.1.0",
2556
+ private: true,
2557
+ scripts: {
2558
+ // Web commands
2559
+ "dev": `pnpm --filter ${DIRS.WEB} dev`,
2560
+ "build": `pnpm --filter ${DIRS.WEB} build`,
2561
+ "start": `pnpm --filter ${DIRS.WEB} start`,
2562
+ "lint": "pnpm -r lint",
2563
+ // Mobile commands
2564
+ "dev:mobile": `pnpm --filter ${DIRS.MOBILE} start`,
2565
+ "ios": `pnpm --filter ${DIRS.MOBILE} ios`,
2566
+ "android": `pnpm --filter ${DIRS.MOBILE} android`,
2567
+ // Shared commands
2568
+ "typecheck": "pnpm -r typecheck",
2569
+ "test": "pnpm -r test",
2570
+ // Web-specific CLI commands (run from root)
2571
+ "db:migrate": `pnpm --filter ${DIRS.WEB} db:migrate`,
2572
+ "db:seed": `pnpm --filter ${DIRS.WEB} db:seed`,
2573
+ "build:registries": `pnpm --filter ${DIRS.WEB} build:registries`
2574
+ },
2575
+ devDependencies: {
2576
+ "typescript": VERSIONS.TYPESCRIPT
2577
+ }
2578
+ };
2579
+ await fs7.writeJson(path6.join(targetDir, FILES.PACKAGE_JSON), rootPkg, { spaces: 2 });
2580
+ }
2581
+ async function createPnpmWorkspace(targetDir) {
2582
+ const workspaceContent = `packages:
2583
+ - '${DIRS.WEB}'
2584
+ - '${DIRS.MOBILE}'
2585
+ `;
2586
+ await fs7.writeFile(path6.join(targetDir, FILES.PNPM_WORKSPACE), workspaceContent);
2587
+ }
2588
+ async function createNpmrc(targetDir) {
2589
+ const npmrcContent = `# Enable proper hoisting for monorepo
2590
+ public-hoist-pattern[]=*@nextsparkjs/*
2591
+ public-hoist-pattern[]=*expo*
2592
+ public-hoist-pattern[]=*react-native*
2593
+ shamefully-hoist=true
2594
+ `;
2595
+ await fs7.writeFile(path6.join(targetDir, FILES.NPMRC), npmrcContent);
2596
+ }
2597
+ async function createGitignore(targetDir) {
2598
+ const gitignoreContent = `# Dependencies
2599
+ node_modules/
2600
+
2601
+ # Build outputs
2602
+ .next/
2603
+ dist/
2604
+ out/
2605
+ build/
2606
+ .expo/
2607
+
2608
+ # NextSpark
2609
+ .nextspark/
2610
+
2611
+ # Environment
2612
+ .env
2613
+ .env.local
2614
+ .env.*.local
2615
+
2616
+ # IDE
2617
+ .idea/
2618
+ .vscode/
2619
+ *.swp
2620
+ *.swo
2621
+
2622
+ # OS
2623
+ .DS_Store
2624
+ Thumbs.db
2625
+
2626
+ # Testing
2627
+ coverage/
2628
+ .nyc_output/
2629
+
2630
+ # Cypress (theme-based in web/)
2631
+ ${DIRS.WEB}/contents/themes/*/tests/cypress/videos
2632
+ ${DIRS.WEB}/contents/themes/*/tests/cypress/screenshots
2633
+ ${DIRS.WEB}/contents/themes/*/tests/cypress/allure-results
2634
+ ${DIRS.WEB}/contents/themes/*/tests/cypress/allure-report
2635
+
2636
+ # Jest (theme-based in web/)
2637
+ ${DIRS.WEB}/contents/themes/*/tests/jest/coverage
2638
+
2639
+ # Mobile specific
2640
+ ${DIRS.MOBILE}/.expo/
2641
+ *.jks
2642
+ *.p8
2643
+ *.p12
2644
+ *.key
2645
+ *.mobileprovision
2646
+ *.orig.*
2647
+ web-build/
2648
+ `;
2649
+ await fs7.writeFile(path6.join(targetDir, FILES.GITIGNORE), gitignoreContent);
2650
+ }
2651
+ async function createRootTsConfig(targetDir) {
2652
+ const tsConfig = {
2653
+ compilerOptions: {
2654
+ target: "ES2022",
2655
+ module: "ESNext",
2656
+ moduleResolution: "bundler",
2657
+ strict: true,
2658
+ skipLibCheck: true,
2659
+ esModuleInterop: true
2660
+ },
2661
+ references: [
2662
+ { path: `./${DIRS.WEB}` },
2663
+ { path: `./${DIRS.MOBILE}` }
2664
+ ]
2665
+ };
2666
+ await fs7.writeJson(path6.join(targetDir, FILES.TSCONFIG), tsConfig, { spaces: 2 });
2667
+ }
2668
+ async function copyMobileTemplate(targetDir, config2) {
2669
+ const mobileTemplatesDir = getMobileTemplatesDir();
2670
+ await validateMobileTemplate(mobileTemplatesDir);
2671
+ const mobileDir = path6.join(targetDir, DIRS.MOBILE);
2672
+ await fs7.ensureDir(mobileDir);
2673
+ for (const item of MOBILE_TEMPLATE_FILES) {
2674
+ const srcPath = path6.join(mobileTemplatesDir, item);
2675
+ const destPath = path6.join(mobileDir, item);
2676
+ if (await fs7.pathExists(srcPath)) {
2677
+ await fs7.copy(srcPath, destPath);
2678
+ }
2679
+ }
2680
+ await createMobilePackageJson(mobileDir, config2);
2681
+ await createMobileAppConfig(mobileDir, config2);
2682
+ await createMobileAssets(mobileDir, config2);
2683
+ }
2684
+ async function createMobilePackageJson(mobileDir, config2) {
2685
+ const mobileSlug = `${config2.projectSlug}-mobile`;
2686
+ const packageJson = {
2687
+ name: mobileSlug,
2688
+ version: "0.1.0",
2689
+ private: true,
2690
+ main: "expo-router/entry",
2691
+ scripts: {
2692
+ start: "expo start",
2693
+ android: "expo start --android",
2694
+ ios: "expo start --ios",
2695
+ web: "expo start --web",
2696
+ lint: "eslint .",
2697
+ typecheck: "tsc --noEmit",
2698
+ test: "jest",
2699
+ "test:watch": "jest --watch",
2700
+ "test:coverage": "jest --coverage"
2701
+ },
2702
+ dependencies: {
2703
+ "@nextsparkjs/mobile": VERSIONS.NEXTSPARK_MOBILE,
2704
+ "@nextsparkjs/ui": VERSIONS.NEXTSPARK_UI,
2705
+ "@tanstack/react-query": VERSIONS.TANSTACK_QUERY,
2706
+ "expo": VERSIONS.EXPO,
2707
+ "expo-constants": VERSIONS.EXPO_CONSTANTS,
2708
+ "expo-linking": VERSIONS.EXPO_LINKING,
2709
+ "expo-router": VERSIONS.EXPO_ROUTER,
2710
+ "expo-secure-store": VERSIONS.EXPO_SECURE_STORE,
2711
+ "expo-status-bar": VERSIONS.EXPO_STATUS_BAR,
2712
+ "lucide-react-native": VERSIONS.LUCIDE_RN,
2713
+ "nativewind": VERSIONS.NATIVEWIND,
2714
+ "react": VERSIONS.REACT,
2715
+ "react-dom": VERSIONS.REACT,
2716
+ "react-native": VERSIONS.REACT_NATIVE,
2717
+ "react-native-web": VERSIONS.RN_WEB,
2718
+ "react-native-gesture-handler": VERSIONS.RN_GESTURE_HANDLER,
2719
+ "react-native-reanimated": VERSIONS.RN_REANIMATED,
2720
+ "react-native-safe-area-context": VERSIONS.RN_SAFE_AREA,
2721
+ "react-native-screens": VERSIONS.RN_SCREENS,
2722
+ "react-native-svg": VERSIONS.RN_SVG,
2723
+ "tailwind-merge": VERSIONS.TAILWIND_MERGE,
2724
+ "tailwindcss": VERSIONS.TAILWINDCSS
2725
+ },
2726
+ devDependencies: {
2727
+ "@babel/core": VERSIONS.BABEL_CORE,
2728
+ "@testing-library/jest-native": VERSIONS.TESTING_LIBRARY_JEST_NATIVE,
2729
+ "@testing-library/react-native": VERSIONS.TESTING_LIBRARY_RN,
2730
+ "@types/jest": "^29.5.0",
2731
+ "@types/react": "^19",
2732
+ "jest": VERSIONS.JEST,
2733
+ "jest-expo": VERSIONS.JEST_EXPO,
2734
+ "react-test-renderer": VERSIONS.REACT,
2735
+ "typescript": VERSIONS.TYPESCRIPT
2736
+ }
2737
+ };
2738
+ await fs7.writeJson(path6.join(mobileDir, FILES.PACKAGE_JSON), packageJson, { spaces: 2 });
2739
+ }
2740
+ async function createMobileAppConfig(mobileDir, config2) {
2741
+ const bundleId = slugToBundleId(config2.projectSlug);
2742
+ const appConfigContent = `import { ExpoConfig, ConfigContext } from 'expo/config'
2743
+
2744
+ export default ({ config }: ConfigContext): ExpoConfig => ({
2745
+ ...config,
2746
+ name: '${config2.projectName}',
2747
+ slug: '${config2.projectSlug}',
2748
+ version: '1.0.0',
2749
+ orientation: 'portrait',
2750
+ icon: './${DIRS.ASSETS}/icon.png',
2751
+ userInterfaceStyle: 'automatic',
2752
+ splash: {
2753
+ image: './${DIRS.ASSETS}/splash.png',
2754
+ resizeMode: 'contain',
2755
+ backgroundColor: '#ffffff',
2756
+ },
2757
+ assetBundlePatterns: ['**/*'],
2758
+ ios: {
2759
+ supportsTablet: true,
2760
+ bundleIdentifier: 'com.${bundleId}.app',
2761
+ },
2762
+ android: {
2763
+ adaptiveIcon: {
2764
+ foregroundImage: './${DIRS.ASSETS}/adaptive-icon.png',
2765
+ backgroundColor: '#ffffff',
2766
+ },
2767
+ package: 'com.${bundleId}.app',
2768
+ },
2769
+ web: {
2770
+ favicon: './${DIRS.ASSETS}/favicon.png',
2771
+ },
2772
+ extra: {
2773
+ // API URL - Configure for your environment
2774
+ // Development: point to your local web app (e.g., http://localhost:3000)
2775
+ // Production: set via EAS environment variables
2776
+ apiUrl: process.env.EXPO_PUBLIC_API_URL,
2777
+ },
2778
+ plugins: ['expo-router', 'expo-secure-store'],
2779
+ scheme: '${config2.projectSlug}',
2780
+ })
2781
+ `;
2782
+ await fs7.writeFile(path6.join(mobileDir, FILES.APP_CONFIG), appConfigContent);
2783
+ }
2784
+ async function createMobileAssets(mobileDir, config2) {
2785
+ const assetsDir = path6.join(mobileDir, DIRS.ASSETS);
2786
+ await fs7.ensureDir(assetsDir);
2787
+ await fs7.writeFile(
2788
+ path6.join(assetsDir, ".gitkeep"),
2789
+ "# Placeholder - replace with your app icons and splash screens\n"
2790
+ );
2791
+ const assetsReadme = `# Mobile App Assets for ${config2.projectName}
2792
+
2793
+ This directory contains your mobile app icons and splash screens.
2794
+
2795
+ ## Required Files
2796
+
2797
+ | File | Size | Description |
2798
+ |------|------|-------------|
2799
+ | \`icon.png\` | 1024x1024px | Main app icon (iOS & Android) |
2800
+ | \`splash.png\` | 1284x2778px | Splash screen image |
2801
+ | \`adaptive-icon.png\` | 1024x1024px | Android adaptive icon (foreground) |
2802
+ | \`favicon.png\` | 48x48px | Web favicon |
2803
+
2804
+ ## How to Generate
2805
+
2806
+ ### Option 1: Expo Asset Generator (Recommended)
2807
+
2808
+ 1. Create a 1024x1024px icon image
2809
+ 2. Use Expo's icon generator:
2810
+ \`\`\`bash
2811
+ npx expo-optimize
2812
+ \`\`\`
2813
+
2814
+ ### Option 2: Online Tools
2815
+
2816
+ - [Expo Icon Generator](https://docs.expo.dev/develop/user-interface/app-icons/)
2817
+ - [App Icon Generator](https://appicon.co/)
2818
+ - [Figma App Icon Template](https://www.figma.com/community/file/824894885635013116)
2819
+
2820
+ ### Option 3: Manual Creation
2821
+
2822
+ Create each file at the specified sizes above. Use PNG format with transparency for icons.
2823
+
2824
+ ## Tips
2825
+
2826
+ - Use a simple, recognizable design that works at small sizes
2827
+ - Test your icon on both light and dark backgrounds
2828
+ - Avoid text in the icon (it becomes illegible at small sizes)
2829
+ - Keep important content within the "safe zone" (center 80%)
2830
+
2831
+ ## Resources
2832
+
2833
+ - [Expo App Icons Documentation](https://docs.expo.dev/develop/user-interface/app-icons/)
2834
+ - [iOS Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/app-icons)
2835
+ - [Android Adaptive Icons](https://developer.android.com/develop/ui/views/launch/icon_design_adaptive)
2836
+ `;
2837
+ await fs7.writeFile(path6.join(assetsDir, "README.md"), assetsReadme);
2838
+ }
2839
+ async function createMonorepoReadme(targetDir, config2) {
2840
+ const readmeContent = `# ${config2.projectName}
2841
+
2842
+ ${config2.projectDescription}
2843
+
2844
+ ## Project Structure
2845
+
2846
+ This is a monorepo containing both web and mobile applications:
2847
+
2848
+ \`\`\`
2849
+ ${config2.projectSlug}/
2850
+ \u251C\u2500\u2500 ${DIRS.WEB}/ # Next.js web application
2851
+ \u2502 \u251C\u2500\u2500 app/ # Next.js App Router
2852
+ \u2502 \u251C\u2500\u2500 contents/ # Themes and plugins
2853
+ \u2502 \u2514\u2500\u2500 package.json
2854
+ \u251C\u2500\u2500 ${DIRS.MOBILE}/ # Expo mobile application
2855
+ \u2502 \u251C\u2500\u2500 app/ # Expo Router screens
2856
+ \u2502 \u251C\u2500\u2500 src/ # Mobile-specific code
2857
+ \u2502 \u2514\u2500\u2500 package.json
2858
+ \u251C\u2500\u2500 package.json # Root monorepo
2859
+ \u2514\u2500\u2500 ${FILES.PNPM_WORKSPACE}
2860
+ \`\`\`
2861
+
2862
+ ## Getting Started
2863
+
2864
+ ### Prerequisites
2865
+
2866
+ - Node.js 20+
2867
+ - pnpm 9+
2868
+ - For mobile: Expo CLI (\`npm install -g expo-cli\`)
2869
+
2870
+ ### Installation
2871
+
2872
+ \`\`\`bash
2873
+ # Install all dependencies
2874
+ pnpm install
2875
+
2876
+ # Set up environment variables
2877
+ cp ${DIRS.WEB}/.env.example ${DIRS.WEB}/.env
2878
+ # Edit ${DIRS.WEB}/.env with your configuration
2879
+ \`\`\`
2880
+
2881
+ ### Development
2882
+
2883
+ **Web Application:**
2884
+ \`\`\`bash
2885
+ # From root directory
2886
+ pnpm dev
2887
+
2888
+ # Or from web directory
2889
+ cd ${DIRS.WEB} && pnpm dev
2890
+ \`\`\`
2891
+
2892
+ **Mobile Application:**
2893
+ \`\`\`bash
2894
+ # From root directory
2895
+ pnpm dev:mobile
2896
+
2897
+ # Or from mobile directory
2898
+ cd ${DIRS.MOBILE} && pnpm start
2899
+ \`\`\`
2900
+
2901
+ ### Running Tests
2902
+
2903
+ \`\`\`bash
2904
+ # Run all tests
2905
+ pnpm test
2906
+
2907
+ # Run web tests only
2908
+ pnpm --filter ${DIRS.WEB} test
2909
+
2910
+ # Run mobile tests only
2911
+ pnpm --filter ${DIRS.MOBILE} test
2912
+ \`\`\`
2913
+
2914
+ ## Mobile App Configuration
2915
+
2916
+ The mobile app connects to your web API. Configure the API URL:
2917
+
2918
+ - **Development:** The mobile app will auto-detect your local server
2919
+ - **Production:** Set \`EXPO_PUBLIC_API_URL\` in your EAS environment
2920
+
2921
+ ## Building for Production
2922
+
2923
+ **Web:**
2924
+ \`\`\`bash
2925
+ pnpm build
2926
+ \`\`\`
2927
+
2928
+ **Mobile:**
2929
+ \`\`\`bash
2930
+ cd ${DIRS.MOBILE}
2931
+ eas build --platform ios
2932
+ eas build --platform android
2933
+ \`\`\`
2934
+
2935
+ ## Learn More
2936
+
2937
+ - [NextSpark Documentation](https://nextspark.dev/docs)
2938
+ - [Expo Documentation](https://docs.expo.dev)
2939
+ - [Next.js Documentation](https://nextjs.org/docs)
2940
+ `;
2941
+ await fs7.writeFile(path6.join(targetDir, FILES.README), readmeContent);
2942
+ }
2943
+ async function generateMonorepoStructure(targetDir, config2) {
2944
+ await createRootPackageJson(targetDir, config2);
2945
+ await createPnpmWorkspace(targetDir);
2946
+ await createNpmrc(targetDir);
2947
+ await createGitignore(targetDir);
2948
+ await createRootTsConfig(targetDir);
2949
+ await createMonorepoReadme(targetDir, config2);
2950
+ const webDir = path6.join(targetDir, DIRS.WEB);
2951
+ await fs7.ensureDir(webDir);
2952
+ await copyMobileTemplate(targetDir, config2);
2953
+ }
2954
+ function isMonorepoProject(config2) {
2955
+ return config2.projectType === "web-mobile";
2956
+ }
2957
+ function getWebDir(targetDir, config2) {
2958
+ return config2.projectType === "web-mobile" ? path6.join(targetDir, DIRS.WEB) : targetDir;
2959
+ }
2960
+
2238
2961
  // src/wizard/generators/index.ts
2239
- var __filename5 = fileURLToPath5(import.meta.url);
2240
- var __dirname5 = path5.dirname(__filename5);
2241
- function getTemplatesDir3() {
2242
- try {
2243
- const corePkgPath = __require.resolve("@nextsparkjs/core/package.json");
2244
- return path5.join(path5.dirname(corePkgPath), "templates");
2245
- } catch {
2246
- const possiblePaths = [
2247
- path5.resolve(__dirname5, "../../../../../core/templates"),
2248
- path5.resolve(__dirname5, "../../../../core/templates"),
2249
- path5.resolve(process.cwd(), "node_modules/@nextsparkjs/core/templates")
2250
- ];
2251
- for (const p of possiblePaths) {
2252
- if (fs7.existsSync(p)) {
2253
- return p;
2254
- }
2962
+ var __filename7 = fileURLToPath7(import.meta.url);
2963
+ var __dirname7 = path7.dirname(__filename7);
2964
+ function getTemplatesDir3(projectRoot) {
2965
+ const rootDir = projectRoot || process.cwd();
2966
+ const possiblePaths = [
2967
+ // From project root node_modules (most common for installed packages)
2968
+ path7.resolve(rootDir, "node_modules/@nextsparkjs/core/templates"),
2969
+ // From CLI dist folder for development
2970
+ path7.resolve(__dirname7, "../../core/templates"),
2971
+ // Legacy paths for different build structures
2972
+ path7.resolve(__dirname7, "../../../../../core/templates"),
2973
+ path7.resolve(__dirname7, "../../../../core/templates")
2974
+ ];
2975
+ for (const p of possiblePaths) {
2976
+ if (fs8.existsSync(p)) {
2977
+ return p;
2255
2978
  }
2256
- throw new Error("Could not find @nextsparkjs/core templates directory");
2257
2979
  }
2980
+ throw new Error(`Could not find @nextsparkjs/core templates directory. Searched: ${possiblePaths.join(", ")}`);
2258
2981
  }
2982
+ var cachedTemplatesDir = null;
2259
2983
  async function copyProjectFiles() {
2260
- const templatesDir = getTemplatesDir3();
2984
+ if (!cachedTemplatesDir) {
2985
+ throw new Error("Templates directory not cached. Call cacheTemplatesDir() first.");
2986
+ }
2987
+ const templatesDir = cachedTemplatesDir;
2261
2988
  const projectDir = process.cwd();
2262
2989
  const itemsToCopy = [
2263
2990
  { src: "app", dest: "app", force: true },
2264
2991
  { src: "public", dest: "public", force: true },
2992
+ { src: "proxy.ts", dest: "proxy.ts", force: true },
2993
+ // Next.js 16+ proxy (formerly middleware.ts)
2265
2994
  { src: "next.config.mjs", dest: "next.config.mjs", force: true },
2266
2995
  { src: "tsconfig.json", dest: "tsconfig.json", force: true },
2267
2996
  { src: "postcss.config.mjs", dest: "postcss.config.mjs", force: true },
2268
2997
  { src: "i18n.ts", dest: "i18n.ts", force: true },
2269
- { src: "npmrc", dest: ".npmrc", force: false },
2998
+ { src: "pnpm-workspace.yaml", dest: "pnpm-workspace.yaml", force: true },
2999
+ // Enable workspace for themes/plugins - REQUIRED
3000
+ // Note: .npmrc is NOT copied for web-only projects (not needed, pnpm default hoisting works fine)
3001
+ // For monorepo projects, monorepo-generator.ts creates a specific .npmrc with expo/react-native patterns
2270
3002
  { src: "tsconfig.cypress.json", dest: "tsconfig.cypress.json", force: false },
2271
3003
  { src: "cypress.d.ts", dest: "cypress.d.ts", force: false },
2272
- { src: "eslint.config.mjs", dest: "eslint.config.mjs", force: false }
3004
+ { src: "eslint.config.mjs", dest: "eslint.config.mjs", force: false },
3005
+ { src: "scripts/cy-run-prod.cjs", dest: "scripts/cy-run-prod.cjs", force: false }
2273
3006
  ];
2274
3007
  for (const item of itemsToCopy) {
2275
- const srcPath = path5.join(templatesDir, item.src);
2276
- const destPath = path5.join(projectDir, item.dest);
2277
- if (await fs7.pathExists(srcPath)) {
2278
- if (item.force || !await fs7.pathExists(destPath)) {
2279
- await fs7.copy(srcPath, destPath);
3008
+ const srcPath = path7.join(templatesDir, item.src);
3009
+ const destPath = path7.join(projectDir, item.dest);
3010
+ if (await fs8.pathExists(srcPath)) {
3011
+ if (item.force || !await fs8.pathExists(destPath)) {
3012
+ await fs8.copy(srcPath, destPath);
2280
3013
  }
2281
3014
  }
2282
3015
  }
2283
3016
  }
2284
3017
  async function updatePackageJson(config2) {
2285
- const packageJsonPath = path5.resolve(process.cwd(), "package.json");
3018
+ const packageJsonPath = path7.resolve(process.cwd(), "package.json");
2286
3019
  let packageJson;
2287
- if (!await fs7.pathExists(packageJsonPath)) {
3020
+ if (!await fs8.pathExists(packageJsonPath)) {
2288
3021
  packageJson = {
2289
- name: config2.projectSlug,
3022
+ name: isMonorepoProject(config2) ? "web" : config2.projectSlug,
2290
3023
  version: "0.1.0",
2291
3024
  private: true,
2292
3025
  scripts: {},
@@ -2294,7 +3027,7 @@ async function updatePackageJson(config2) {
2294
3027
  devDependencies: {}
2295
3028
  };
2296
3029
  } else {
2297
- packageJson = await fs7.readJson(packageJsonPath);
3030
+ packageJson = await fs8.readJson(packageJsonPath);
2298
3031
  }
2299
3032
  packageJson.scripts = packageJson.scripts || {};
2300
3033
  const scriptsToAdd = {
@@ -2305,9 +3038,11 @@ async function updatePackageJson(config2) {
2305
3038
  "build:registries": "nextspark registry:build",
2306
3039
  "db:migrate": "nextspark db:migrate",
2307
3040
  "db:seed": "nextspark db:seed",
2308
- "test:theme": `jest --config contents/themes/${config2.projectSlug}/tests/jest/jest.config.ts`,
2309
- "cy:open": `cypress open --config-file contents/themes/${config2.projectSlug}/tests/cypress.config.ts`,
2310
- "cy:run": `cypress run --config-file contents/themes/${config2.projectSlug}/tests/cypress.config.ts`,
3041
+ "test": "node node_modules/@nextsparkjs/core/scripts/test/jest-theme.mjs",
3042
+ "cy:open": "node node_modules/@nextsparkjs/core/scripts/test/cy.mjs open",
3043
+ "cy:run": "node node_modules/@nextsparkjs/core/scripts/test/cy.mjs run",
3044
+ "cy:tags": "node node_modules/@nextsparkjs/core/scripts/test/cy.mjs tags",
3045
+ "cy:run:prod": "node scripts/cy-run-prod.cjs",
2311
3046
  "allure:generate": `allure generate contents/themes/${config2.projectSlug}/tests/cypress/allure-results --clean -o contents/themes/${config2.projectSlug}/tests/cypress/allure-report`,
2312
3047
  "allure:open": `allure open contents/themes/${config2.projectSlug}/tests/cypress/allure-report`
2313
3048
  };
@@ -2319,8 +3054,8 @@ async function updatePackageJson(config2) {
2319
3054
  packageJson.dependencies = packageJson.dependencies || {};
2320
3055
  const depsToAdd = {
2321
3056
  // NextSpark
2322
- "@nextsparkjs/core": "^0.1.0-beta.4",
2323
- "@nextsparkjs/cli": "^0.1.0-beta.4",
3057
+ "@nextsparkjs/core": "latest",
3058
+ "@nextsparkjs/cli": "latest",
2324
3059
  // Next.js + React
2325
3060
  "next": "^15.1.0",
2326
3061
  "react": "^19.0.0",
@@ -2351,7 +3086,9 @@ async function updatePackageJson(config2) {
2351
3086
  "slugify": "^1.6.6"
2352
3087
  };
2353
3088
  for (const [name, version] of Object.entries(depsToAdd)) {
2354
- if (!packageJson.dependencies[name]) {
3089
+ if (name.startsWith("@nextsparkjs/")) {
3090
+ packageJson.dependencies[name] = version;
3091
+ } else if (!packageJson.dependencies[name]) {
2355
3092
  packageJson.dependencies[name] = version;
2356
3093
  }
2357
3094
  }
@@ -2379,24 +3116,28 @@ async function updatePackageJson(config2) {
2379
3116
  "@testing-library/react": "^16.3.0",
2380
3117
  "jest-environment-jsdom": "^29.7.0",
2381
3118
  // Cypress
2382
- "cypress": "^14.0.0",
3119
+ "cypress": "^15.8.2",
2383
3120
  "@testing-library/cypress": "^10.0.2",
2384
3121
  "@cypress/webpack-preprocessor": "^6.0.2",
2385
- "@cypress/grep": "^4.1.0",
3122
+ "@cypress/grep": "^5.0.1",
2386
3123
  "ts-loader": "^9.5.1",
2387
3124
  "webpack": "^5.97.0",
2388
3125
  "allure-cypress": "^3.0.0",
2389
- "allure-commandline": "^2.27.0"
3126
+ "allure-commandline": "^2.27.0",
3127
+ // NextSpark Testing
3128
+ "@nextsparkjs/testing": "latest"
2390
3129
  };
2391
3130
  for (const [name, version] of Object.entries(devDepsToAdd)) {
2392
- if (!packageJson.devDependencies[name]) {
3131
+ if (name.startsWith("@nextsparkjs/")) {
3132
+ packageJson.devDependencies[name] = version;
3133
+ } else if (!packageJson.devDependencies[name]) {
2393
3134
  packageJson.devDependencies[name] = version;
2394
3135
  }
2395
3136
  }
2396
- await fs7.writeJson(packageJsonPath, packageJson, { spaces: 2 });
3137
+ await fs8.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2397
3138
  }
2398
3139
  async function updateGitignore(config2) {
2399
- const gitignorePath = path5.resolve(process.cwd(), ".gitignore");
3140
+ const gitignorePath = path7.resolve(process.cwd(), ".gitignore");
2400
3141
  const entriesToAdd = `
2401
3142
  # NextSpark
2402
3143
  .nextspark/
@@ -2414,40 +3155,66 @@ contents/themes/*/tests/jest/coverage
2414
3155
  .env
2415
3156
  .env.local
2416
3157
  `;
2417
- if (await fs7.pathExists(gitignorePath)) {
2418
- const currentContent = await fs7.readFile(gitignorePath, "utf-8");
3158
+ if (await fs8.pathExists(gitignorePath)) {
3159
+ const currentContent = await fs8.readFile(gitignorePath, "utf-8");
2419
3160
  if (!currentContent.includes(".nextspark/")) {
2420
- await fs7.appendFile(gitignorePath, entriesToAdd);
3161
+ await fs8.appendFile(gitignorePath, entriesToAdd);
2421
3162
  }
2422
3163
  } else {
2423
- await fs7.writeFile(gitignorePath, entriesToAdd.trim());
3164
+ await fs8.writeFile(gitignorePath, entriesToAdd.trim());
2424
3165
  }
2425
3166
  }
2426
3167
  async function generateProject(config2) {
2427
- await copyProjectFiles();
2428
- await copyStarterTheme(config2);
2429
- await copyContentFeatures(config2);
2430
- await updateThemeConfig(config2);
2431
- await updateDevConfig(config2);
2432
- await updateAppConfig(config2);
2433
- await updateBillingConfig(config2);
2434
- await updateRolesConfig(config2);
2435
- await updateMigrations(config2);
2436
- await updatePermissionsConfig(config2);
2437
- await updateDashboardConfig(config2);
2438
- await updateAuthConfig(config2);
2439
- await updateDashboardUIConfig(config2);
2440
- await updateDevToolsConfig(config2);
2441
- await processI18n(config2);
2442
- await updatePackageJson(config2);
2443
- await updateGitignore(config2);
2444
- await generateEnvExample(config2);
2445
- await updateReadme(config2);
2446
- await copyEnvExampleToEnv();
3168
+ const projectDir = process.cwd();
3169
+ cachedTemplatesDir = getTemplatesDir3(projectDir);
3170
+ const webDir = getWebDir(projectDir, config2);
3171
+ if (isMonorepoProject(config2)) {
3172
+ await generateMonorepoStructure(projectDir, config2);
3173
+ }
3174
+ const originalCwd = process.cwd();
3175
+ if (isMonorepoProject(config2)) {
3176
+ process.chdir(webDir);
3177
+ }
3178
+ try {
3179
+ await copyProjectFiles();
3180
+ await updateGlobalsCss(config2);
3181
+ await copyStarterTheme(config2);
3182
+ await fs8.ensureDir(path7.join(process.cwd(), "contents", "plugins"));
3183
+ await copyContentFeatures(config2);
3184
+ await updateThemeConfig(config2);
3185
+ await updateDevConfig(config2);
3186
+ await updateAppConfig(config2);
3187
+ await updateBillingConfig(config2);
3188
+ await updateRolesConfig(config2);
3189
+ await updateMigrations(config2);
3190
+ await updateTestFiles(config2);
3191
+ await updatePermissionsConfig(config2);
3192
+ await updateEntityPermissions(config2);
3193
+ await updateDashboardConfig(config2);
3194
+ await updateAuthConfig(config2);
3195
+ await updateDashboardUIConfig(config2);
3196
+ await updateDevToolsConfig(config2);
3197
+ await processI18n(config2);
3198
+ await updatePackageJson(config2);
3199
+ if (!isMonorepoProject(config2)) {
3200
+ await updateGitignore(config2);
3201
+ }
3202
+ await generateEnvExample(config2);
3203
+ if (!isMonorepoProject(config2)) {
3204
+ await updateReadme(config2);
3205
+ }
3206
+ await copyEnvExampleToEnv();
3207
+ } finally {
3208
+ if (isMonorepoProject(config2)) {
3209
+ process.chdir(originalCwd);
3210
+ }
3211
+ cachedTemplatesDir = null;
3212
+ }
2447
3213
  }
2448
3214
 
2449
3215
  // src/wizard/presets.ts
2450
3216
  var SAAS_PRESET = {
3217
+ projectType: "web",
2451
3218
  teamMode: "multi-tenant",
2452
3219
  teamRoles: ["owner", "admin", "member", "viewer"],
2453
3220
  defaultLocale: "en",
@@ -2466,6 +3233,7 @@ var SAAS_PRESET = {
2466
3233
  blog: false
2467
3234
  },
2468
3235
  auth: {
3236
+ registrationMode: "open",
2469
3237
  emailPassword: true,
2470
3238
  googleOAuth: true,
2471
3239
  emailVerification: true
@@ -2486,6 +3254,7 @@ var SAAS_PRESET = {
2486
3254
  }
2487
3255
  };
2488
3256
  var BLOG_PRESET = {
3257
+ projectType: "web",
2489
3258
  teamMode: "single-user",
2490
3259
  teamRoles: ["owner"],
2491
3260
  defaultLocale: "en",
@@ -2504,6 +3273,7 @@ var BLOG_PRESET = {
2504
3273
  blog: true
2505
3274
  },
2506
3275
  auth: {
3276
+ registrationMode: "invitation-only",
2507
3277
  emailPassword: true,
2508
3278
  googleOAuth: false,
2509
3279
  emailVerification: false
@@ -2524,6 +3294,7 @@ var BLOG_PRESET = {
2524
3294
  }
2525
3295
  };
2526
3296
  var CRM_PRESET = {
3297
+ projectType: "web",
2527
3298
  teamMode: "single-tenant",
2528
3299
  teamRoles: ["owner", "admin", "member"],
2529
3300
  defaultLocale: "en",
@@ -2542,6 +3313,7 @@ var CRM_PRESET = {
2542
3313
  blog: false
2543
3314
  },
2544
3315
  auth: {
3316
+ registrationMode: "invitation-only",
2545
3317
  emailPassword: true,
2546
3318
  googleOAuth: true,
2547
3319
  emailVerification: true
@@ -2578,11 +3350,13 @@ function getPreset(name) {
2578
3350
  }
2579
3351
  return preset;
2580
3352
  }
2581
- function applyPreset(projectInfo, presetName) {
3353
+ function applyPreset(projectInfo, presetName, typeOverride) {
2582
3354
  const preset = getPreset(presetName);
2583
3355
  return {
2584
3356
  ...projectInfo,
2585
- ...preset
3357
+ ...preset,
3358
+ // Apply type override if provided
3359
+ ...typeOverride && { projectType: typeOverride }
2586
3360
  };
2587
3361
  }
2588
3362
 
@@ -2637,7 +3411,7 @@ function getFileTree(config2) {
2637
3411
  files.push(`${themeDir}/styles/theme.css`);
2638
3412
  files.push(`${themeDir}/styles/components.css`);
2639
3413
  files.push(`${themeDir}/tests/cypress.config.ts`);
2640
- files.push(`${themeDir}/tests/jest/jest.config.ts`);
3414
+ files.push(`${themeDir}/tests/jest/jest.config.cjs`);
2641
3415
  files.push(`${themeDir}/tests/cypress/e2e/auth.cy.ts`);
2642
3416
  files.push(`${themeDir}/tests/cypress/e2e/dashboard.cy.ts`);
2643
3417
  files.push(`${themeDir}/tests/jest/components/hero.test.tsx`);
@@ -2765,24 +3539,6 @@ async function runWizard(options = { mode: "interactive" }) {
2765
3539
  try {
2766
3540
  let selectedTheme = null;
2767
3541
  let selectedPlugins = [];
2768
- if (!options.preset) {
2769
- if (options.theme !== void 0) {
2770
- selectedTheme = options.theme === "none" ? null : options.theme;
2771
- showInfo(`Reference theme: ${selectedTheme || "None"}`);
2772
- } else if (options.mode !== "quick") {
2773
- selectedTheme = await promptThemeSelection();
2774
- }
2775
- if (options.plugins !== void 0) {
2776
- selectedPlugins = options.plugins;
2777
- if (selectedPlugins.length > 0) {
2778
- showInfo(`Selected plugins: ${selectedPlugins.join(", ")}`);
2779
- }
2780
- } else if (options.mode !== "quick" && !options.yes) {
2781
- selectedPlugins = await promptPluginsSelection(selectedTheme);
2782
- } else if (selectedTheme) {
2783
- selectedPlugins = getRequiredPlugins(selectedTheme);
2784
- }
2785
- }
2786
3542
  let config2;
2787
3543
  if (options.preset) {
2788
3544
  config2 = await runPresetMode(options.preset, options);
@@ -2800,6 +3556,22 @@ async function runWizard(options = { mode: "interactive" }) {
2800
3556
  break;
2801
3557
  }
2802
3558
  }
3559
+ if (options.theme !== void 0) {
3560
+ selectedTheme = options.theme === "none" ? null : options.theme;
3561
+ showInfo(`Reference theme: ${selectedTheme || "None"}`);
3562
+ } else if (!options.preset && options.mode !== "quick") {
3563
+ selectedTheme = await promptThemeSelection();
3564
+ }
3565
+ if (options.plugins !== void 0) {
3566
+ selectedPlugins = options.plugins;
3567
+ if (selectedPlugins.length > 0) {
3568
+ showInfo(`Selected plugins: ${selectedPlugins.join(", ")}`);
3569
+ }
3570
+ } else if (!options.preset && options.mode !== "quick" && !options.yes) {
3571
+ selectedPlugins = await promptPluginsSelection(selectedTheme);
3572
+ } else if (selectedTheme) {
3573
+ selectedPlugins = getRequiredPlugins(selectedTheme);
3574
+ }
2803
3575
  showConfigSummary(config2);
2804
3576
  showConfigPreview(config2);
2805
3577
  if (!options.yes) {
@@ -2814,13 +3586,20 @@ async function runWizard(options = { mode: "interactive" }) {
2814
3586
  process.exit(0);
2815
3587
  }
2816
3588
  }
2817
- await copyNpmrc();
2818
3589
  console.log("");
2819
3590
  const coreInstalled = await installCore();
2820
3591
  if (!coreInstalled) {
2821
3592
  showError("Failed to install @nextsparkjs/core. Cannot generate project.");
2822
3593
  process.exit(1);
2823
3594
  }
3595
+ if (config2.projectType === "web-mobile") {
3596
+ console.log("");
3597
+ const mobileInstalled = await installMobile();
3598
+ if (!mobileInstalled) {
3599
+ showError("Failed to install @nextsparkjs/mobile. Cannot generate monorepo project.");
3600
+ process.exit(1);
3601
+ }
3602
+ }
2824
3603
  console.log("");
2825
3604
  const spinner = ora7({
2826
3605
  text: "Generating your NextSpark project...",
@@ -2836,14 +3615,19 @@ async function runWizard(options = { mode: "interactive" }) {
2836
3615
  if (selectedTheme || selectedPlugins.length > 0) {
2837
3616
  await installThemeAndPlugins(selectedTheme, selectedPlugins);
2838
3617
  }
3618
+ const projectRoot = process.cwd();
3619
+ const webDir = getWebDir(projectRoot, config2);
3620
+ const isMonorepo = isMonorepoProject(config2);
2839
3621
  const installSpinner = ora7({
2840
- text: "Installing dependencies...",
3622
+ text: isMonorepo ? "Installing dependencies (monorepo)..." : "Installing dependencies...",
2841
3623
  prefixText: " "
2842
3624
  }).start();
2843
3625
  try {
3626
+ installSpinner.stop();
2844
3627
  execSync("pnpm install --force", {
2845
- cwd: process.cwd(),
2846
- stdio: "pipe"
3628
+ cwd: projectRoot,
3629
+ // Always install from root (works for both flat and monorepo)
3630
+ stdio: "inherit"
2847
3631
  });
2848
3632
  installSpinner.succeed("Dependencies installed!");
2849
3633
  } catch (error) {
@@ -2855,22 +3639,25 @@ async function runWizard(options = { mode: "interactive" }) {
2855
3639
  prefixText: " "
2856
3640
  }).start();
2857
3641
  try {
2858
- const projectRoot = process.cwd();
2859
- const registryScript = join7(projectRoot, "node_modules/@nextsparkjs/core/scripts/build/registry.mjs");
3642
+ const registryScript = join8(webDir, "node_modules/@nextsparkjs/core/scripts/build/registry.mjs");
3643
+ registrySpinner.stop();
2860
3644
  execSync(`node "${registryScript}" --build`, {
2861
- cwd: projectRoot,
2862
- stdio: "pipe",
3645
+ cwd: webDir,
3646
+ // Run from web directory
3647
+ stdio: "inherit",
2863
3648
  env: {
2864
3649
  ...process.env,
2865
- NEXTSPARK_PROJECT_ROOT: projectRoot
3650
+ NEXTSPARK_PROJECT_ROOT: webDir
2866
3651
  }
2867
3652
  });
2868
3653
  registrySpinner.succeed("Registries built!");
2869
3654
  } catch (error) {
2870
3655
  registrySpinner.fail("Failed to build registries");
2871
- console.log(chalk11.yellow(' Registries will be built automatically when you run "pnpm dev"'));
3656
+ const devCmd = isMonorepo ? "pnpm dev" : "pnpm dev";
3657
+ console.log(chalk11.yellow(` Registries will be built automatically when you run "${devCmd}"`));
2872
3658
  }
2873
- showNextSteps(config2, selectedTheme);
3659
+ const aiChoice = await promptAIWorkflowSetup(config2);
3660
+ showNextSteps(config2, selectedTheme, aiChoice);
2874
3661
  } catch (error) {
2875
3662
  if (error instanceof Error) {
2876
3663
  if (error.message.includes("User force closed")) {
@@ -2910,7 +3697,7 @@ async function runPresetMode(presetName, options) {
2910
3697
  console.log("");
2911
3698
  projectInfo = await promptProjectInfo();
2912
3699
  }
2913
- const config2 = applyPreset(projectInfo, presetName);
3700
+ const config2 = applyPreset(projectInfo, presetName, options.type);
2914
3701
  return config2;
2915
3702
  }
2916
3703
  function showConfigSummary(config2) {
@@ -2923,6 +3710,7 @@ function showConfigSummary(config2) {
2923
3710
  console.log(chalk11.gray(` Name: ${chalk11.white(config2.projectName)}`));
2924
3711
  console.log(chalk11.gray(` Slug: ${chalk11.white(config2.projectSlug)}`));
2925
3712
  console.log(chalk11.gray(` Description: ${chalk11.white(config2.projectDescription)}`));
3713
+ console.log(chalk11.gray(` Type: ${chalk11.white(config2.projectType === "web-mobile" ? "Web + Mobile (Monorepo)" : "Web only")}`));
2926
3714
  console.log("");
2927
3715
  console.log(chalk11.white(" Team Mode:"));
2928
3716
  console.log(chalk11.gray(` Mode: ${chalk11.white(config2.teamMode)}`));
@@ -2941,7 +3729,8 @@ function showConfigSummary(config2) {
2941
3729
  console.log(chalk11.gray(` Enabled: ${chalk11.white(enabledFeatures.join(", ") || "None")}`));
2942
3730
  console.log("");
2943
3731
  console.log(chalk11.white(" Authentication:"));
2944
- const enabledAuth = Object.entries(config2.auth).filter(([_, enabled]) => enabled).map(([method]) => formatAuthMethod(method));
3732
+ console.log(chalk11.gray(` Registration: ${chalk11.white(formatRegistrationMode(config2.auth.registrationMode))}`));
3733
+ const enabledAuth = Object.entries(config2.auth).filter(([key, enabled]) => key !== "registrationMode" && enabled).map(([method]) => formatAuthMethod(method));
2945
3734
  console.log(chalk11.gray(` Methods: ${chalk11.white(enabledAuth.join(", ") || "None")}`));
2946
3735
  console.log("");
2947
3736
  console.log(chalk11.white(" Dashboard:"));
@@ -2952,6 +3741,14 @@ function showConfigSummary(config2) {
2952
3741
  const enabledDevTools = Object.entries(config2.dev).filter(([_, enabled]) => enabled).map(([tool]) => formatDevTool(tool));
2953
3742
  console.log(chalk11.gray(` Enabled: ${chalk11.white(enabledDevTools.join(", ") || "None")}`));
2954
3743
  }
3744
+ function formatRegistrationMode(mode) {
3745
+ const mapping = {
3746
+ "open": "Open (anyone can register)",
3747
+ "domain-restricted": "Domain-Restricted (Google OAuth only)",
3748
+ "invitation-only": "Invitation-Only"
3749
+ };
3750
+ return mapping[mode] || mode;
3751
+ }
2955
3752
  function formatAuthMethod(method) {
2956
3753
  const mapping = {
2957
3754
  emailPassword: "Email/Password",
@@ -2976,7 +3773,9 @@ function formatDevTool(tool) {
2976
3773
  };
2977
3774
  return mapping[tool] || tool;
2978
3775
  }
2979
- function showNextSteps(config2, referenceTheme = null) {
3776
+ function showNextSteps(config2, referenceTheme = null, aiChoice = "skip") {
3777
+ const isMonorepo = config2.projectType === "web-mobile";
3778
+ const aiSetupDone = aiChoice === "claude";
2980
3779
  console.log("");
2981
3780
  console.log(chalk11.cyan(" " + "=".repeat(60)));
2982
3781
  console.log(chalk11.bold.green(" \u2728 NextSpark project ready!"));
@@ -2984,12 +3783,13 @@ function showNextSteps(config2, referenceTheme = null) {
2984
3783
  console.log("");
2985
3784
  console.log(chalk11.bold.white(" Next steps:"));
2986
3785
  console.log("");
2987
- console.log(chalk11.white(" 1. Configure your .env file:"));
2988
- console.log(chalk11.gray(" Edit these values in .env:"));
3786
+ const envPath = isMonorepo ? "web/.env" : ".env";
3787
+ console.log(chalk11.white(" 1. Configure your environment:"));
3788
+ console.log(chalk11.gray(` Edit ${envPath} with your credentials:`));
2989
3789
  console.log("");
2990
3790
  console.log(chalk11.yellow(" DATABASE_URL"));
2991
3791
  console.log(chalk11.gray(" PostgreSQL connection string"));
2992
- console.log(chalk11.dim(" Example: postgresql://user:pass@localhost:5432/mydb"));
3792
+ console.log(chalk11.gray(` Recommended: ${chalk11.cyan("https://supabase.com")} | ${chalk11.cyan("https://neon.com")}`));
2993
3793
  console.log("");
2994
3794
  console.log(chalk11.yellow(" BETTER_AUTH_SECRET"));
2995
3795
  console.log(chalk11.gray(" Generate with:"));
@@ -3001,15 +3801,85 @@ function showNextSteps(config2, referenceTheme = null) {
3001
3801
  console.log(chalk11.white(" 3. Start the development server:"));
3002
3802
  console.log(chalk11.cyan(" pnpm dev"));
3003
3803
  console.log("");
3004
- console.log(chalk11.gray(" " + "-".repeat(60)));
3005
- console.log(chalk11.gray(` Theme: ${chalk11.white(`contents/themes/${config2.projectSlug}/`)}`));
3006
- console.log(chalk11.gray(` Active theme: ${chalk11.green(`NEXT_PUBLIC_ACTIVE_THEME=${config2.projectSlug}`)}`));
3804
+ let nextStep = 4;
3805
+ if (isMonorepo) {
3806
+ console.log(chalk11.white(` ${nextStep}. (Optional) Start the mobile app:`));
3807
+ console.log(chalk11.cyan(" pnpm dev:mobile"));
3808
+ console.log(chalk11.gray(" Or: cd mobile && pnpm start"));
3809
+ console.log("");
3810
+ nextStep++;
3811
+ }
3812
+ if (aiSetupDone) {
3813
+ console.log(chalk11.white(` ${nextStep}. Start building with AI:`));
3814
+ console.log(chalk11.gray(" Open Claude Code in your project and run:"));
3815
+ console.log(chalk11.cyan(" /how-to:start"));
3816
+ console.log("");
3817
+ } else {
3818
+ console.log(chalk11.white(` ${nextStep}. (Optional) Setup AI workflows:`));
3819
+ console.log(chalk11.cyan(" nextspark setup:ai"));
3820
+ console.log("");
3821
+ }
3822
+ console.log(chalk11.gray(" " + "-".repeat(60)));
3823
+ if (isMonorepo) {
3824
+ console.log(chalk11.gray(` Structure: ${chalk11.white("Monorepo (web/ + mobile/)")}`));
3825
+ console.log(chalk11.gray(` Web theme: ${chalk11.white(`web/contents/themes/${config2.projectSlug}/`)}`));
3826
+ console.log(chalk11.gray(` Mobile app: ${chalk11.white("mobile/")}`));
3827
+ console.log(chalk11.gray(` Active theme: ${chalk11.green(`NEXT_PUBLIC_ACTIVE_THEME=${config2.projectSlug}`)}`));
3828
+ } else {
3829
+ console.log(chalk11.gray(` Theme: ${chalk11.white(`contents/themes/${config2.projectSlug}/`)}`));
3830
+ console.log(chalk11.gray(` Active theme: ${chalk11.green(`NEXT_PUBLIC_ACTIVE_THEME=${config2.projectSlug}`)}`));
3831
+ }
3007
3832
  if (referenceTheme) {
3008
- console.log(chalk11.gray(` Reference: ${chalk11.white(`contents/themes/${referenceTheme}/`)}`));
3833
+ const refPath = isMonorepo ? `web/contents/themes/${referenceTheme}/` : `contents/themes/${referenceTheme}/`;
3834
+ console.log(chalk11.gray(` Reference: ${chalk11.white(refPath)}`));
3009
3835
  }
3010
- console.log(chalk11.gray(" Docs: https://nextspark.dev/docs"));
3836
+ console.log(chalk11.gray(` Docs: ${chalk11.cyan("https://nextspark.dev/docs")}`));
3011
3837
  console.log("");
3012
3838
  }
3839
+ async function promptAIWorkflowSetup(config2) {
3840
+ console.log("");
3841
+ const choice = await select7({
3842
+ message: "Setup AI-assisted development workflows?",
3843
+ choices: [
3844
+ { name: "Claude Code (Recommended)", value: "claude" },
3845
+ { name: "Cursor (Coming soon)", value: "cursor" },
3846
+ { name: "Antigravity (Coming soon)", value: "antigravity" },
3847
+ { name: "Skip for now", value: "skip" }
3848
+ ]
3849
+ });
3850
+ if (choice === "skip") {
3851
+ return "skip";
3852
+ }
3853
+ if (choice === "cursor" || choice === "antigravity") {
3854
+ showInfo(`${choice} support is coming soon. For now, use Claude Code.`);
3855
+ return "skip";
3856
+ }
3857
+ const projectRoot = process.cwd();
3858
+ const isMonorepo = isMonorepoProject(config2);
3859
+ try {
3860
+ execSync("pnpm add -D -w @nextsparkjs/ai-workflow", {
3861
+ cwd: projectRoot,
3862
+ stdio: "inherit"
3863
+ });
3864
+ let setupScript = join8(projectRoot, "node_modules", "@nextsparkjs", "ai-workflow", "scripts", "setup.mjs");
3865
+ if (!existsSync7(setupScript) && isMonorepo) {
3866
+ setupScript = join8(projectRoot, "web", "node_modules", "@nextsparkjs", "ai-workflow", "scripts", "setup.mjs");
3867
+ }
3868
+ if (existsSync7(setupScript)) {
3869
+ execSync(`node "${setupScript}" ${choice}`, {
3870
+ cwd: projectRoot,
3871
+ stdio: "inherit"
3872
+ });
3873
+ showSuccess("AI workflow setup complete!");
3874
+ } else {
3875
+ showWarning('AI workflow package installed but setup script not found. Run "nextspark setup:ai" manually.');
3876
+ }
3877
+ } catch (error) {
3878
+ showError('Failed to install AI workflow package. Run "nextspark setup:ai" later.');
3879
+ return "skip";
3880
+ }
3881
+ return choice;
3882
+ }
3013
3883
  function findLocalCoreTarball() {
3014
3884
  const cwd = process.cwd();
3015
3885
  try {
@@ -3018,14 +3888,14 @@ function findLocalCoreTarball() {
3018
3888
  (f) => f.includes("nextsparkjs-core") && f.endsWith(".tgz")
3019
3889
  );
3020
3890
  if (coreTarball) {
3021
- return join7(cwd, coreTarball);
3891
+ return join8(cwd, coreTarball);
3022
3892
  }
3023
3893
  } catch {
3024
3894
  }
3025
3895
  return null;
3026
3896
  }
3027
3897
  function isCoreInstalled() {
3028
- const corePath = join7(process.cwd(), "node_modules", "@nextsparkjs", "core");
3898
+ const corePath = join8(process.cwd(), "node_modules", "@nextsparkjs", "core");
3029
3899
  return existsSync7(corePath);
3030
3900
  }
3031
3901
  async function installCore() {
@@ -3043,8 +3913,8 @@ async function installCore() {
3043
3913
  packageSpec = localTarball;
3044
3914
  spinner.text = "Installing @nextsparkjs/core from local tarball...";
3045
3915
  }
3046
- const useYarn = existsSync7(join7(process.cwd(), "yarn.lock"));
3047
- const usePnpm = existsSync7(join7(process.cwd(), "pnpm-lock.yaml"));
3916
+ const useYarn = existsSync7(join8(process.cwd(), "yarn.lock"));
3917
+ const usePnpm = existsSync7(join8(process.cwd(), "pnpm-lock.yaml"));
3048
3918
  let installCmd;
3049
3919
  if (usePnpm) {
3050
3920
  installCmd = `pnpm add ${packageSpec}`;
@@ -3053,8 +3923,9 @@ async function installCore() {
3053
3923
  } else {
3054
3924
  installCmd = `npm install ${packageSpec}`;
3055
3925
  }
3926
+ spinner.stop();
3056
3927
  execSync(installCmd, {
3057
- stdio: "pipe",
3928
+ stdio: "inherit",
3058
3929
  cwd: process.cwd()
3059
3930
  });
3060
3931
  spinner.succeed(chalk11.green("@nextsparkjs/core installed successfully!"));
@@ -3068,17 +3939,64 @@ async function installCore() {
3068
3939
  return false;
3069
3940
  }
3070
3941
  }
3071
- async function copyNpmrc() {
3072
- const npmrcPath = join7(process.cwd(), ".npmrc");
3073
- if (existsSync7(npmrcPath)) {
3074
- return;
3942
+ function isMobileInstalled() {
3943
+ const mobilePath = join8(process.cwd(), "node_modules", "@nextsparkjs", "mobile");
3944
+ return existsSync7(mobilePath);
3945
+ }
3946
+ function findLocalMobileTarball() {
3947
+ const cwd = process.cwd();
3948
+ try {
3949
+ const files = readdirSync(cwd);
3950
+ const mobileTarball = files.find(
3951
+ (f) => f.includes("nextsparkjs-mobile") && f.endsWith(".tgz")
3952
+ );
3953
+ if (mobileTarball) {
3954
+ return join8(cwd, mobileTarball);
3955
+ }
3956
+ } catch {
3957
+ }
3958
+ return null;
3959
+ }
3960
+ async function installMobile() {
3961
+ if (isMobileInstalled()) {
3962
+ return true;
3963
+ }
3964
+ const spinner = ora7({
3965
+ text: "Installing @nextsparkjs/mobile...",
3966
+ prefixText: " "
3967
+ }).start();
3968
+ try {
3969
+ const localTarball = findLocalMobileTarball();
3970
+ let packageSpec = "@nextsparkjs/mobile";
3971
+ if (localTarball) {
3972
+ packageSpec = localTarball;
3973
+ spinner.text = "Installing @nextsparkjs/mobile from local tarball...";
3974
+ }
3975
+ const useYarn = existsSync7(join8(process.cwd(), "yarn.lock"));
3976
+ const usePnpm = existsSync7(join8(process.cwd(), "pnpm-lock.yaml"));
3977
+ let installCmd;
3978
+ if (usePnpm) {
3979
+ installCmd = `pnpm add ${packageSpec}`;
3980
+ } else if (useYarn) {
3981
+ installCmd = `yarn add ${packageSpec}`;
3982
+ } else {
3983
+ installCmd = `npm install ${packageSpec}`;
3984
+ }
3985
+ spinner.stop();
3986
+ execSync(installCmd, {
3987
+ stdio: "inherit",
3988
+ cwd: process.cwd()
3989
+ });
3990
+ spinner.succeed(chalk11.green("@nextsparkjs/mobile installed successfully!"));
3991
+ return true;
3992
+ } catch (error) {
3993
+ spinner.fail(chalk11.red("Failed to install @nextsparkjs/mobile"));
3994
+ if (error instanceof Error) {
3995
+ console.log(chalk11.red(` Error: ${error.message}`));
3996
+ }
3997
+ console.log(chalk11.gray(" Hint: Make sure the package is available (npm registry or local tarball)"));
3998
+ return false;
3075
3999
  }
3076
- const npmrcContent = `# Hoist @nextsparkjs/core dependencies so they're accessible from the project
3077
- # This is required for pnpm to make peer dependencies available
3078
- public-hoist-pattern[]=*
3079
- `;
3080
- const { writeFileSync: writeFileSync3 } = await import("fs");
3081
- writeFileSync3(npmrcPath, npmrcContent);
3082
4000
  }
3083
4001
 
3084
4002
  // src/commands/init.ts
@@ -3093,10 +4011,10 @@ function parsePlugins(pluginsStr) {
3093
4011
  }
3094
4012
  function hasExistingProject() {
3095
4013
  const projectRoot = process.cwd();
3096
- return existsSync8(join8(projectRoot, "contents")) || existsSync8(join8(projectRoot, ".nextspark"));
4014
+ return existsSync8(join9(projectRoot, "contents")) || existsSync8(join9(projectRoot, ".nextspark"));
3097
4015
  }
3098
4016
  function generateInitialRegistries(registriesDir) {
3099
- writeFileSync2(join8(registriesDir, "block-registry.ts"), `// Auto-generated by nextspark init
4017
+ writeFileSync2(join9(registriesDir, "block-registry.ts"), `// Auto-generated by nextspark init
3100
4018
  import type { ComponentType } from 'react'
3101
4019
 
3102
4020
  export const BLOCK_REGISTRY: Record<string, {
@@ -3109,26 +4027,26 @@ export const BLOCK_REGISTRY: Record<string, {
3109
4027
 
3110
4028
  export const BLOCK_COMPONENTS: Record<string, React.LazyExoticComponent<ComponentType<any>>> = {}
3111
4029
  `);
3112
- writeFileSync2(join8(registriesDir, "theme-registry.ts"), `// Auto-generated by nextspark init
4030
+ writeFileSync2(join9(registriesDir, "theme-registry.ts"), `// Auto-generated by nextspark init
3113
4031
  export const THEME_REGISTRY: Record<string, unknown> = {}
3114
4032
  `);
3115
- writeFileSync2(join8(registriesDir, "entity-registry.ts"), `// Auto-generated by nextspark init
4033
+ writeFileSync2(join9(registriesDir, "entity-registry.ts"), `// Auto-generated by nextspark init
3116
4034
  export const ENTITY_REGISTRY: Record<string, unknown> = {}
3117
4035
  `);
3118
- writeFileSync2(join8(registriesDir, "entity-registry.client.ts"), `// Auto-generated by nextspark init
4036
+ writeFileSync2(join9(registriesDir, "entity-registry.client.ts"), `// Auto-generated by nextspark init
3119
4037
  export const CLIENT_ENTITY_REGISTRY: Record<string, unknown> = {}
3120
4038
  export function parseChildEntity(path: string) { return null }
3121
4039
  export function getEntityApiPath(entity: string) { return \`/api/\${entity}\` }
3122
4040
  export function clientMetaSystemAdapter() { return {} }
3123
4041
  export type ClientEntityConfig = Record<string, unknown>
3124
4042
  `);
3125
- writeFileSync2(join8(registriesDir, "billing-registry.ts"), `// Auto-generated by nextspark init
4043
+ writeFileSync2(join9(registriesDir, "billing-registry.ts"), `// Auto-generated by nextspark init
3126
4044
  export const BILLING_REGISTRY = { plans: [], features: [] }
3127
4045
  `);
3128
- writeFileSync2(join8(registriesDir, "plugin-registry.ts"), `// Auto-generated by nextspark init
4046
+ writeFileSync2(join9(registriesDir, "plugin-registry.ts"), `// Auto-generated by nextspark init
3129
4047
  export const PLUGIN_REGISTRY: Record<string, unknown> = {}
3130
4048
  `);
3131
- writeFileSync2(join8(registriesDir, "testing-registry.ts"), `// Auto-generated by nextspark init
4049
+ writeFileSync2(join9(registriesDir, "testing-registry.ts"), `// Auto-generated by nextspark init
3132
4050
  export const FLOW_REGISTRY: Record<string, unknown> = {}
3133
4051
  export const FEATURE_REGISTRY: Record<string, unknown> = {}
3134
4052
  export const TAGS_REGISTRY: Record<string, unknown> = {}
@@ -3136,11 +4054,11 @@ export const COVERAGE_SUMMARY = { total: 0, covered: 0 }
3136
4054
  export type FlowEntry = unknown
3137
4055
  export type FeatureEntry = unknown
3138
4056
  `);
3139
- writeFileSync2(join8(registriesDir, "docs-registry.ts"), `// Auto-generated by nextspark init
4057
+ writeFileSync2(join9(registriesDir, "docs-registry.ts"), `// Auto-generated by nextspark init
3140
4058
  export const DOCS_REGISTRY = { sections: [], pages: [] }
3141
4059
  export type DocSectionMeta = { title: string; slug: string }
3142
4060
  `);
3143
- writeFileSync2(join8(registriesDir, "index.ts"), `// Auto-generated by nextspark init
4061
+ writeFileSync2(join9(registriesDir, "index.ts"), `// Auto-generated by nextspark init
3144
4062
  export * from './block-registry'
3145
4063
  export * from './theme-registry'
3146
4064
  export * from './entity-registry'
@@ -3155,15 +4073,15 @@ async function simpleInit(options) {
3155
4073
  const spinner = ora8("Initializing NextSpark project...").start();
3156
4074
  const projectRoot = process.cwd();
3157
4075
  try {
3158
- const nextspark = join8(projectRoot, ".nextspark");
3159
- const registriesDir = join8(nextspark, "registries");
4076
+ const nextspark = join9(projectRoot, ".nextspark");
4077
+ const registriesDir = join9(nextspark, "registries");
3160
4078
  if (!existsSync8(registriesDir) || options.force) {
3161
4079
  mkdirSync2(registriesDir, { recursive: true });
3162
4080
  spinner.text = "Creating .nextspark/registries/";
3163
4081
  generateInitialRegistries(registriesDir);
3164
4082
  spinner.text = "Generated initial registries";
3165
4083
  }
3166
- const tsconfigPath = join8(projectRoot, "tsconfig.json");
4084
+ const tsconfigPath = join9(projectRoot, "tsconfig.json");
3167
4085
  if (existsSync8(tsconfigPath)) {
3168
4086
  spinner.text = "Updating tsconfig.json paths...";
3169
4087
  const tsconfig = JSON.parse(readFileSync7(tsconfigPath, "utf-8"));
@@ -3175,7 +4093,7 @@ async function simpleInit(options) {
3175
4093
  };
3176
4094
  writeFileSync2(tsconfigPath, JSON.stringify(tsconfig, null, 2));
3177
4095
  }
3178
- const envExample = join8(projectRoot, ".env.example");
4096
+ const envExample = join9(projectRoot, ".env.example");
3179
4097
  if (!existsSync8(envExample)) {
3180
4098
  const envContent = `# NextSpark Configuration
3181
4099
  DATABASE_URL="postgresql://user:password@localhost:5432/db"
@@ -3218,25 +4136,133 @@ async function initCommand(options) {
3218
4136
  yes: options.yes,
3219
4137
  name: options.name,
3220
4138
  slug: options.slug,
3221
- description: options.description
4139
+ description: options.description,
4140
+ type: options.type
3222
4141
  };
3223
4142
  await runWizard(wizardOptions);
3224
4143
  }
3225
4144
 
4145
+ // src/commands/add-mobile.ts
4146
+ import { existsSync as existsSync9, cpSync as cpSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync3, renameSync, mkdirSync as mkdirSync3 } from "fs";
4147
+ import { join as join10 } from "path";
4148
+ import chalk13 from "chalk";
4149
+ import ora9 from "ora";
4150
+ import { execSync as execSync2 } from "child_process";
4151
+ function findMobileCoreDir() {
4152
+ const projectRoot = process.cwd();
4153
+ const npmPath = join10(projectRoot, "node_modules", "@nextsparkjs", "mobile");
4154
+ if (existsSync9(npmPath)) {
4155
+ return npmPath;
4156
+ }
4157
+ const monoPath = join10(projectRoot, "packages", "mobile");
4158
+ if (existsSync9(monoPath)) {
4159
+ return monoPath;
4160
+ }
4161
+ const parentNpmPath = join10(projectRoot, "..", "node_modules", "@nextsparkjs", "mobile");
4162
+ if (existsSync9(parentNpmPath)) {
4163
+ return parentNpmPath;
4164
+ }
4165
+ throw new Error(
4166
+ "Could not find @nextsparkjs/mobile package.\nRun: npm install @nextsparkjs/mobile"
4167
+ );
4168
+ }
4169
+ async function addMobileCommand(options = {}) {
4170
+ const projectRoot = process.cwd();
4171
+ const mobileDir = join10(projectRoot, "mobile");
4172
+ console.log();
4173
+ console.log(chalk13.bold("Adding NextSpark Mobile App"));
4174
+ console.log();
4175
+ if (existsSync9(mobileDir) && !options.force) {
4176
+ console.log(chalk13.red("Error: Mobile app already exists at mobile/"));
4177
+ console.log(chalk13.gray("Use --force to overwrite"));
4178
+ process.exit(1);
4179
+ }
4180
+ let mobileCoreDir;
4181
+ try {
4182
+ mobileCoreDir = findMobileCoreDir();
4183
+ } catch (error) {
4184
+ console.log(chalk13.red(error.message));
4185
+ process.exit(1);
4186
+ }
4187
+ const templatesDir = join10(mobileCoreDir, "templates");
4188
+ if (!existsSync9(templatesDir)) {
4189
+ console.log(chalk13.red("Error: Could not find mobile templates"));
4190
+ console.log(chalk13.gray(`Expected at: ${templatesDir}`));
4191
+ process.exit(1);
4192
+ }
4193
+ const copySpinner = ora9("Copying mobile app template...").start();
4194
+ try {
4195
+ mkdirSync3(mobileDir, { recursive: true });
4196
+ cpSync2(templatesDir, mobileDir, { recursive: true });
4197
+ const pkgTemplatePath = join10(mobileDir, "package.json.template");
4198
+ const pkgPath = join10(mobileDir, "package.json");
4199
+ if (existsSync9(pkgTemplatePath)) {
4200
+ renameSync(pkgTemplatePath, pkgPath);
4201
+ const pkg2 = JSON.parse(readFileSync8(pkgPath, "utf-8"));
4202
+ const rootPkgPath = join10(projectRoot, "package.json");
4203
+ if (existsSync9(rootPkgPath)) {
4204
+ const rootPkg = JSON.parse(readFileSync8(rootPkgPath, "utf-8"));
4205
+ const rawName = rootPkg.name || "my-project";
4206
+ if (rawName.startsWith("@")) {
4207
+ const scopeMatch = rawName.match(/^@[\w-]+/);
4208
+ const scope = scopeMatch ? scopeMatch[0] : "";
4209
+ const projectName = rawName.replace(/^@[\w-]+\//, "");
4210
+ pkg2.name = `${scope}/${projectName}-mobile`;
4211
+ } else {
4212
+ pkg2.name = `${rawName}-mobile`;
4213
+ }
4214
+ }
4215
+ writeFileSync3(pkgPath, JSON.stringify(pkg2, null, 2));
4216
+ }
4217
+ copySpinner.succeed("Mobile app template copied");
4218
+ } catch (error) {
4219
+ copySpinner.fail("Failed to copy templates");
4220
+ console.log(chalk13.red(error.message));
4221
+ process.exit(1);
4222
+ }
4223
+ if (!options.skipInstall) {
4224
+ const installSpinner = ora9("Installing dependencies...").start();
4225
+ try {
4226
+ execSync2("npm install", {
4227
+ cwd: mobileDir,
4228
+ stdio: "pipe",
4229
+ timeout: 3e5
4230
+ // 5 minutes
4231
+ });
4232
+ installSpinner.succeed("Dependencies installed");
4233
+ } catch (error) {
4234
+ installSpinner.fail("Failed to install dependencies");
4235
+ console.log(chalk13.yellow(" Run `npm install` in mobile/ manually"));
4236
+ }
4237
+ }
4238
+ console.log();
4239
+ console.log(chalk13.green.bold(" Mobile app created successfully!"));
4240
+ console.log();
4241
+ console.log(chalk13.bold(" Next steps:"));
4242
+ console.log();
4243
+ console.log(` ${chalk13.cyan("1.")} cd mobile`);
4244
+ console.log(` ${chalk13.cyan("2.")} Update ${chalk13.bold("app.config.ts")} with your app name and bundle ID`);
4245
+ console.log(` ${chalk13.cyan("3.")} Add your entities in ${chalk13.bold("src/entities/")}`);
4246
+ console.log(` ${chalk13.cyan("4.")} npm start`);
4247
+ console.log();
4248
+ console.log(chalk13.gray(" Documentation: https://nextspark.dev/docs/mobile"));
4249
+ console.log();
4250
+ }
4251
+
3226
4252
  // src/commands/doctor.ts
3227
- import chalk14 from "chalk";
4253
+ import chalk15 from "chalk";
3228
4254
 
3229
4255
  // src/doctor/index.ts
3230
- import chalk13 from "chalk";
4256
+ import chalk14 from "chalk";
3231
4257
 
3232
4258
  // src/doctor/checks/dependencies.ts
3233
- import fs8 from "fs-extra";
3234
- import path6 from "path";
4259
+ import fs9 from "fs-extra";
4260
+ import path8 from "path";
3235
4261
  async function checkDependencies() {
3236
4262
  const cwd = process.cwd();
3237
- const nodeModulesPath = path6.join(cwd, "node_modules");
3238
- const packageJsonPath = path6.join(cwd, "package.json");
3239
- if (!await fs8.pathExists(packageJsonPath)) {
4263
+ const nodeModulesPath = path8.join(cwd, "node_modules");
4264
+ const packageJsonPath = path8.join(cwd, "package.json");
4265
+ if (!await fs9.pathExists(packageJsonPath)) {
3240
4266
  return {
3241
4267
  name: "Dependencies",
3242
4268
  status: "fail",
@@ -3244,7 +4270,7 @@ async function checkDependencies() {
3244
4270
  fix: "Ensure you are in a NextSpark project directory"
3245
4271
  };
3246
4272
  }
3247
- if (!await fs8.pathExists(nodeModulesPath)) {
4273
+ if (!await fs9.pathExists(nodeModulesPath)) {
3248
4274
  return {
3249
4275
  name: "Dependencies",
3250
4276
  status: "fail",
@@ -3254,7 +4280,7 @@ async function checkDependencies() {
3254
4280
  }
3255
4281
  let packageJson;
3256
4282
  try {
3257
- packageJson = await fs8.readJson(packageJsonPath);
4283
+ packageJson = await fs9.readJson(packageJsonPath);
3258
4284
  } catch {
3259
4285
  return {
3260
4286
  name: "Dependencies",
@@ -3271,14 +4297,14 @@ async function checkDependencies() {
3271
4297
  const criticalDeps = ["next", "react", "react-dom"];
3272
4298
  for (const dep of criticalDeps) {
3273
4299
  if (allDependencies[dep]) {
3274
- const depPath = path6.join(nodeModulesPath, dep);
3275
- if (!await fs8.pathExists(depPath)) {
4300
+ const depPath = path8.join(nodeModulesPath, dep);
4301
+ if (!await fs9.pathExists(depPath)) {
3276
4302
  missingDeps.push(dep);
3277
4303
  }
3278
4304
  }
3279
4305
  }
3280
- const nextsparksCorePath = path6.join(nodeModulesPath, "@nextsparkjs", "core");
3281
- const hasNextSparkCore = await fs8.pathExists(nextsparksCorePath);
4306
+ const nextsparksCorePath = path8.join(nodeModulesPath, "@nextsparkjs", "core");
4307
+ const hasNextSparkCore = await fs9.pathExists(nextsparksCorePath);
3282
4308
  if (missingDeps.length > 0) {
3283
4309
  return {
3284
4310
  name: "Dependencies",
@@ -3303,8 +4329,8 @@ async function checkDependencies() {
3303
4329
  }
3304
4330
 
3305
4331
  // src/doctor/checks/config.ts
3306
- import fs9 from "fs-extra";
3307
- import path7 from "path";
4332
+ import fs10 from "fs-extra";
4333
+ import path9 from "path";
3308
4334
  var REQUIRED_CONFIG_FILES = [
3309
4335
  "next.config.ts",
3310
4336
  "tailwind.config.ts",
@@ -3315,13 +4341,13 @@ async function checkConfigs() {
3315
4341
  const missingFiles = [];
3316
4342
  const invalidFiles = [];
3317
4343
  for (const file of REQUIRED_CONFIG_FILES) {
3318
- const filePath = path7.join(cwd, file);
4344
+ const filePath = path9.join(cwd, file);
3319
4345
  if (file.endsWith(".ts")) {
3320
- const jsPath = path7.join(cwd, file.replace(".ts", ".js"));
3321
- const mjsPath = path7.join(cwd, file.replace(".ts", ".mjs"));
3322
- const tsExists = await fs9.pathExists(filePath);
3323
- const jsExists = await fs9.pathExists(jsPath);
3324
- const mjsExists = await fs9.pathExists(mjsPath);
4346
+ const jsPath = path9.join(cwd, file.replace(".ts", ".js"));
4347
+ const mjsPath = path9.join(cwd, file.replace(".ts", ".mjs"));
4348
+ const tsExists = await fs10.pathExists(filePath);
4349
+ const jsExists = await fs10.pathExists(jsPath);
4350
+ const mjsExists = await fs10.pathExists(mjsPath);
3325
4351
  if (!tsExists && !jsExists && !mjsExists) {
3326
4352
  if (!file.includes("next.config") && !file.includes("tailwind.config")) {
3327
4353
  missingFiles.push(file);
@@ -3329,14 +4355,14 @@ async function checkConfigs() {
3329
4355
  }
3330
4356
  continue;
3331
4357
  }
3332
- if (!await fs9.pathExists(filePath)) {
4358
+ if (!await fs10.pathExists(filePath)) {
3333
4359
  missingFiles.push(file);
3334
4360
  }
3335
4361
  }
3336
- const tsconfigPath = path7.join(cwd, "tsconfig.json");
3337
- if (await fs9.pathExists(tsconfigPath)) {
4362
+ const tsconfigPath = path9.join(cwd, "tsconfig.json");
4363
+ if (await fs10.pathExists(tsconfigPath)) {
3338
4364
  try {
3339
- const content = await fs9.readFile(tsconfigPath, "utf-8");
4365
+ const content = await fs10.readFile(tsconfigPath, "utf-8");
3340
4366
  JSON.parse(content);
3341
4367
  } catch {
3342
4368
  invalidFiles.push("tsconfig.json");
@@ -3344,14 +4370,14 @@ async function checkConfigs() {
3344
4370
  } else {
3345
4371
  missingFiles.push("tsconfig.json");
3346
4372
  }
3347
- const configDir = path7.join(cwd, "config");
3348
- if (await fs9.pathExists(configDir)) {
3349
- const configFiles = await fs9.readdir(configDir);
4373
+ const configDir = path9.join(cwd, "config");
4374
+ if (await fs10.pathExists(configDir)) {
4375
+ const configFiles = await fs10.readdir(configDir);
3350
4376
  const tsConfigFiles = configFiles.filter((f) => f.endsWith(".ts"));
3351
4377
  for (const file of tsConfigFiles) {
3352
- const filePath = path7.join(configDir, file);
4378
+ const filePath = path9.join(configDir, file);
3353
4379
  try {
3354
- const content = await fs9.readFile(filePath, "utf-8");
4380
+ const content = await fs10.readFile(filePath, "utf-8");
3355
4381
  if (content.trim().length === 0) {
3356
4382
  invalidFiles.push(`config/${file}`);
3357
4383
  }
@@ -3384,8 +4410,8 @@ async function checkConfigs() {
3384
4410
  }
3385
4411
 
3386
4412
  // src/doctor/checks/database.ts
3387
- import fs10 from "fs-extra";
3388
- import path8 from "path";
4413
+ import fs11 from "fs-extra";
4414
+ import path10 from "path";
3389
4415
  function parseEnvFile(content) {
3390
4416
  const result = {};
3391
4417
  const lines = content.split("\n");
@@ -3408,25 +4434,25 @@ function parseEnvFile(content) {
3408
4434
  }
3409
4435
  async function checkDatabase() {
3410
4436
  const cwd = process.cwd();
3411
- const envPath = path8.join(cwd, ".env");
3412
- const envLocalPath = path8.join(cwd, ".env.local");
4437
+ const envPath = path10.join(cwd, ".env");
4438
+ const envLocalPath = path10.join(cwd, ".env.local");
3413
4439
  let envVars = {};
3414
- if (await fs10.pathExists(envLocalPath)) {
4440
+ if (await fs11.pathExists(envLocalPath)) {
3415
4441
  try {
3416
- const content = await fs10.readFile(envLocalPath, "utf-8");
4442
+ const content = await fs11.readFile(envLocalPath, "utf-8");
3417
4443
  envVars = { ...envVars, ...parseEnvFile(content) };
3418
4444
  } catch {
3419
4445
  }
3420
4446
  }
3421
- if (await fs10.pathExists(envPath)) {
4447
+ if (await fs11.pathExists(envPath)) {
3422
4448
  try {
3423
- const content = await fs10.readFile(envPath, "utf-8");
4449
+ const content = await fs11.readFile(envPath, "utf-8");
3424
4450
  envVars = { ...envVars, ...parseEnvFile(content) };
3425
4451
  } catch {
3426
4452
  }
3427
4453
  }
3428
4454
  const databaseUrl = envVars["DATABASE_URL"] || process.env.DATABASE_URL;
3429
- if (!await fs10.pathExists(envPath) && !await fs10.pathExists(envLocalPath)) {
4455
+ if (!await fs11.pathExists(envPath) && !await fs11.pathExists(envLocalPath)) {
3430
4456
  return {
3431
4457
  name: "Database",
3432
4458
  status: "warn",
@@ -3467,22 +4493,22 @@ async function checkDatabase() {
3467
4493
  }
3468
4494
 
3469
4495
  // src/doctor/checks/imports.ts
3470
- import fs11 from "fs-extra";
3471
- import path9 from "path";
4496
+ import fs12 from "fs-extra";
4497
+ import path11 from "path";
3472
4498
  async function checkCorePackage(cwd) {
3473
- const nodeModulesPath = path9.join(cwd, "node_modules", "@nextsparkjs", "core");
3474
- if (!await fs11.pathExists(nodeModulesPath)) {
4499
+ const nodeModulesPath = path11.join(cwd, "node_modules", "@nextsparkjs", "core");
4500
+ if (!await fs12.pathExists(nodeModulesPath)) {
3475
4501
  return { accessible: false, reason: "@nextsparkjs/core not found in node_modules" };
3476
4502
  }
3477
- const packageJsonPath = path9.join(nodeModulesPath, "package.json");
3478
- if (!await fs11.pathExists(packageJsonPath)) {
4503
+ const packageJsonPath = path11.join(nodeModulesPath, "package.json");
4504
+ if (!await fs12.pathExists(packageJsonPath)) {
3479
4505
  return { accessible: false, reason: "@nextsparkjs/core package.json not found" };
3480
4506
  }
3481
4507
  try {
3482
- const packageJson = await fs11.readJson(packageJsonPath);
4508
+ const packageJson = await fs12.readJson(packageJsonPath);
3483
4509
  const mainEntry = packageJson.main || packageJson.module || "./dist/index.js";
3484
- const mainPath = path9.join(nodeModulesPath, mainEntry);
3485
- if (!await fs11.pathExists(mainPath)) {
4510
+ const mainPath = path11.join(nodeModulesPath, mainEntry);
4511
+ if (!await fs12.pathExists(mainPath)) {
3486
4512
  return { accessible: false, reason: "@nextsparkjs/core entry point not found" };
3487
4513
  }
3488
4514
  } catch {
@@ -3495,8 +4521,8 @@ async function scanForBrokenImports(cwd) {
3495
4521
  const srcDirs = ["src", "app", "pages", "components", "lib"];
3496
4522
  const existingDirs = [];
3497
4523
  for (const dir of srcDirs) {
3498
- const dirPath = path9.join(cwd, dir);
3499
- if (await fs11.pathExists(dirPath)) {
4524
+ const dirPath = path11.join(cwd, dir);
4525
+ if (await fs12.pathExists(dirPath)) {
3500
4526
  existingDirs.push(dirPath);
3501
4527
  }
3502
4528
  }
@@ -3505,32 +4531,32 @@ async function scanForBrokenImports(cwd) {
3505
4531
  for (const dir of existingDirs) {
3506
4532
  if (filesScanned >= maxFilesToScan) break;
3507
4533
  try {
3508
- const files = await fs11.readdir(dir, { withFileTypes: true });
4534
+ const files = await fs12.readdir(dir, { withFileTypes: true });
3509
4535
  for (const file of files) {
3510
4536
  if (filesScanned >= maxFilesToScan) break;
3511
4537
  if (file.isFile() && (file.name.endsWith(".ts") || file.name.endsWith(".tsx"))) {
3512
- const filePath = path9.join(dir, file.name);
4538
+ const filePath = path11.join(dir, file.name);
3513
4539
  try {
3514
- const content = await fs11.readFile(filePath, "utf-8");
4540
+ const content = await fs12.readFile(filePath, "utf-8");
3515
4541
  const importMatches = content.match(/from ['"](@?[^'"]+)['"]/g);
3516
4542
  if (importMatches) {
3517
4543
  for (const match of importMatches) {
3518
4544
  const importPath = match.replace(/from ['"]/g, "").replace(/['"]/g, "");
3519
4545
  if (importPath.startsWith(".")) {
3520
- const absoluteImportPath = path9.resolve(path9.dirname(filePath), importPath);
4546
+ const absoluteImportPath = path11.resolve(path11.dirname(filePath), importPath);
3521
4547
  const possiblePaths = [
3522
4548
  absoluteImportPath,
3523
4549
  `${absoluteImportPath}.ts`,
3524
4550
  `${absoluteImportPath}.tsx`,
3525
4551
  `${absoluteImportPath}.js`,
3526
4552
  `${absoluteImportPath}.jsx`,
3527
- path9.join(absoluteImportPath, "index.ts"),
3528
- path9.join(absoluteImportPath, "index.tsx"),
3529
- path9.join(absoluteImportPath, "index.js")
4553
+ path11.join(absoluteImportPath, "index.ts"),
4554
+ path11.join(absoluteImportPath, "index.tsx"),
4555
+ path11.join(absoluteImportPath, "index.js")
3530
4556
  ];
3531
4557
  const exists = await Promise.any(
3532
4558
  possiblePaths.map(async (p) => {
3533
- if (await fs11.pathExists(p)) return true;
4559
+ if (await fs12.pathExists(p)) return true;
3534
4560
  throw new Error("Not found");
3535
4561
  })
3536
4562
  ).catch(() => false);
@@ -3554,11 +4580,11 @@ async function checkImports() {
3554
4580
  const cwd = process.cwd();
3555
4581
  const coreCheck = await checkCorePackage(cwd);
3556
4582
  if (!coreCheck.accessible) {
3557
- const packageJsonPath = path9.join(cwd, "package.json");
4583
+ const packageJsonPath = path11.join(cwd, "package.json");
3558
4584
  let hasCoreDep = false;
3559
- if (await fs11.pathExists(packageJsonPath)) {
4585
+ if (await fs12.pathExists(packageJsonPath)) {
3560
4586
  try {
3561
- const packageJson = await fs11.readJson(packageJsonPath);
4587
+ const packageJson = await fs12.readJson(packageJsonPath);
3562
4588
  hasCoreDep = !!(packageJson.dependencies?.["@nextsparkjs/core"] || packageJson.devDependencies?.["@nextsparkjs/core"]);
3563
4589
  } catch {
3564
4590
  }
@@ -3595,25 +4621,25 @@ async function checkImports() {
3595
4621
 
3596
4622
  // src/doctor/index.ts
3597
4623
  var STATUS_ICONS = {
3598
- pass: chalk13.green("\u2713"),
3599
- warn: chalk13.yellow("\u26A0"),
3600
- fail: chalk13.red("\u2717")
4624
+ pass: chalk14.green("\u2713"),
4625
+ warn: chalk14.yellow("\u26A0"),
4626
+ fail: chalk14.red("\u2717")
3601
4627
  };
3602
4628
  function formatResult(result) {
3603
4629
  const icon = STATUS_ICONS[result.status];
3604
- const nameColor = result.status === "fail" ? chalk13.red : result.status === "warn" ? chalk13.yellow : chalk13.white;
4630
+ const nameColor = result.status === "fail" ? chalk14.red : result.status === "warn" ? chalk14.yellow : chalk14.white;
3605
4631
  const name = nameColor(result.name.padEnd(18));
3606
- const message = chalk13.gray(result.message);
4632
+ const message = chalk14.gray(result.message);
3607
4633
  let output = `${icon} ${name} ${message}`;
3608
4634
  if (result.fix && result.status !== "pass") {
3609
4635
  output += `
3610
- ${" ".repeat(22)}${chalk13.cyan("\u2192")} ${chalk13.cyan(result.fix)}`;
4636
+ ${" ".repeat(22)}${chalk14.cyan("\u2192")} ${chalk14.cyan(result.fix)}`;
3611
4637
  }
3612
4638
  return output;
3613
4639
  }
3614
4640
  function showHeader() {
3615
4641
  console.log("");
3616
- console.log(chalk13.cyan("\u{1FA7A} NextSpark Health Check"));
4642
+ console.log(chalk14.cyan("\u{1FA7A} NextSpark Health Check"));
3617
4643
  console.log("");
3618
4644
  }
3619
4645
  function showSummary(results) {
@@ -3621,11 +4647,11 @@ function showSummary(results) {
3621
4647
  const warnings = results.filter((r) => r.status === "warn").length;
3622
4648
  const failed = results.filter((r) => r.status === "fail").length;
3623
4649
  console.log("");
3624
- console.log(chalk13.gray("-".repeat(50)));
4650
+ console.log(chalk14.gray("-".repeat(50)));
3625
4651
  const summary = [
3626
- chalk13.green(`${passed} passed`),
3627
- warnings > 0 ? chalk13.yellow(`${warnings} warning${warnings > 1 ? "s" : ""}`) : null,
3628
- failed > 0 ? chalk13.red(`${failed} failed`) : null
4652
+ chalk14.green(`${passed} passed`),
4653
+ warnings > 0 ? chalk14.yellow(`${warnings} warning${warnings > 1 ? "s" : ""}`) : null,
4654
+ failed > 0 ? chalk14.red(`${failed} failed`) : null
3629
4655
  ].filter(Boolean).join(", ");
3630
4656
  console.log(`Summary: ${summary}`);
3631
4657
  console.log("");
@@ -3667,7 +4693,7 @@ async function runDoctorCommand() {
3667
4693
  var isDirectExecution = process.argv[1]?.includes("doctor") || process.argv.includes("doctor");
3668
4694
  if (isDirectExecution && typeof __require !== "undefined") {
3669
4695
  runDoctorCommand().catch((error) => {
3670
- console.error(chalk13.red("An unexpected error occurred:"), error.message);
4696
+ console.error(chalk14.red("An unexpected error occurred:"), error.message);
3671
4697
  process.exit(1);
3672
4698
  });
3673
4699
  }
@@ -3678,7 +4704,447 @@ async function doctorCommand() {
3678
4704
  await runDoctorCommand();
3679
4705
  } catch (error) {
3680
4706
  if (error instanceof Error) {
3681
- console.error(chalk14.red(`Error: ${error.message}`));
4707
+ console.error(chalk15.red(`Error: ${error.message}`));
4708
+ }
4709
+ process.exit(1);
4710
+ }
4711
+ }
4712
+
4713
+ // src/commands/db.ts
4714
+ import { spawn as spawn5 } from "child_process";
4715
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
4716
+ import { join as join11 } from "path";
4717
+ import chalk16 from "chalk";
4718
+ import ora10 from "ora";
4719
+ function loadProjectEnv4(projectRoot) {
4720
+ const envPath = join11(projectRoot, ".env");
4721
+ const envVars = {};
4722
+ if (existsSync10(envPath)) {
4723
+ const content = readFileSync9(envPath, "utf-8");
4724
+ for (const line of content.split("\n")) {
4725
+ const trimmed = line.trim();
4726
+ if (trimmed && !trimmed.startsWith("#")) {
4727
+ const [key, ...valueParts] = trimmed.split("=");
4728
+ if (key && valueParts.length > 0) {
4729
+ let value = valueParts.join("=");
4730
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
4731
+ value = value.slice(1, -1);
4732
+ }
4733
+ envVars[key] = value;
4734
+ }
4735
+ }
4736
+ }
4737
+ }
4738
+ return envVars;
4739
+ }
4740
+ async function dbMigrateCommand() {
4741
+ const spinner = ora10("Preparing to run migrations...").start();
4742
+ try {
4743
+ const coreDir = getCoreDir();
4744
+ const projectRoot = getProjectRoot();
4745
+ const migrationsScript = join11(coreDir, "scripts", "db", "run-migrations.mjs");
4746
+ if (!existsSync10(migrationsScript)) {
4747
+ spinner.fail("Migrations script not found");
4748
+ console.error(chalk16.red(`Expected script at: ${migrationsScript}`));
4749
+ process.exit(1);
4750
+ }
4751
+ spinner.succeed("Core package found");
4752
+ const projectEnv = loadProjectEnv4(projectRoot);
4753
+ if (!projectEnv.DATABASE_URL) {
4754
+ spinner.fail("DATABASE_URL not found in .env file");
4755
+ console.error(chalk16.red("Please configure DATABASE_URL in your .env file"));
4756
+ process.exit(1);
4757
+ }
4758
+ if (!projectEnv.NEXT_PUBLIC_ACTIVE_THEME) {
4759
+ spinner.fail("NEXT_PUBLIC_ACTIVE_THEME not found in .env file");
4760
+ console.error(chalk16.red("Please configure NEXT_PUBLIC_ACTIVE_THEME in your .env file"));
4761
+ process.exit(1);
4762
+ }
4763
+ spinner.start("Running database migrations...");
4764
+ const migrateProcess = spawn5("node", [migrationsScript], {
4765
+ cwd: projectRoot,
4766
+ stdio: "inherit",
4767
+ env: {
4768
+ ...projectEnv,
4769
+ ...process.env,
4770
+ NEXTSPARK_PROJECT_ROOT: projectRoot,
4771
+ NEXTSPARK_CORE_DIR: coreDir
4772
+ }
4773
+ });
4774
+ migrateProcess.on("error", (err) => {
4775
+ spinner.fail("Migration failed");
4776
+ console.error(chalk16.red(err.message));
4777
+ process.exit(1);
4778
+ });
4779
+ migrateProcess.on("close", (code) => {
4780
+ if (code === 0) {
4781
+ console.log(chalk16.green("\n\u2705 Migrations completed successfully!"));
4782
+ process.exit(0);
4783
+ } else {
4784
+ console.error(chalk16.red(`
4785
+ \u274C Migrations failed with exit code ${code}`));
4786
+ process.exit(code ?? 1);
4787
+ }
4788
+ });
4789
+ } catch (error) {
4790
+ spinner.fail("Migration preparation failed");
4791
+ if (error instanceof Error) {
4792
+ console.error(chalk16.red(error.message));
4793
+ }
4794
+ process.exit(1);
4795
+ }
4796
+ }
4797
+ async function dbSeedCommand() {
4798
+ console.log(chalk16.cyan("\u2139\uFE0F Sample data is included as part of the migration process."));
4799
+ console.log(chalk16.cyan(" Running db:migrate to apply all migrations including sample data...\n"));
4800
+ await dbMigrateCommand();
4801
+ }
4802
+
4803
+ // src/commands/sync-app.ts
4804
+ import { existsSync as existsSync11, readdirSync as readdirSync2, mkdirSync as mkdirSync4, copyFileSync, readFileSync as readFileSync10 } from "fs";
4805
+ import { join as join12, dirname as dirname4, relative } from "path";
4806
+ import chalk17 from "chalk";
4807
+ import ora11 from "ora";
4808
+ var EXCLUDED_TEMPLATE_PATTERNS = ["(templates)"];
4809
+ var ROOT_TEMPLATE_FILES = [
4810
+ "proxy.ts",
4811
+ // Next.js 16+ proxy (formerly middleware.ts) - required for auth/permission validation
4812
+ "next.config.mjs",
4813
+ // Required for webpack aliases, transpilePackages, security headers
4814
+ "tsconfig.json",
4815
+ // Required for proper path aliases and test file exclusions
4816
+ "i18n.ts"
4817
+ // Required for next-intl configuration
4818
+ ];
4819
+ var MAX_VERBOSE_FILES = 10;
4820
+ var MAX_SUMMARY_FILES = 5;
4821
+ function getAllFiles(dir, baseDir = dir) {
4822
+ const files = [];
4823
+ if (!existsSync11(dir)) {
4824
+ return files;
4825
+ }
4826
+ const entries = readdirSync2(dir, { withFileTypes: true });
4827
+ for (const entry of entries) {
4828
+ const fullPath = join12(dir, entry.name);
4829
+ const relativePath = relative(baseDir, fullPath);
4830
+ if (entry.isDirectory()) {
4831
+ files.push(...getAllFiles(fullPath, baseDir));
4832
+ } else if (entry.isFile()) {
4833
+ if (entry.name !== ".DS_Store" && entry.name !== "README.md") {
4834
+ files.push(relativePath);
4835
+ }
4836
+ }
4837
+ }
4838
+ return files;
4839
+ }
4840
+ function copyFile(source, target) {
4841
+ const targetDir = dirname4(target);
4842
+ if (!existsSync11(targetDir)) {
4843
+ mkdirSync4(targetDir, { recursive: true });
4844
+ }
4845
+ copyFileSync(source, target);
4846
+ }
4847
+ function backupDirectory(source, target) {
4848
+ if (!existsSync11(source)) {
4849
+ return;
4850
+ }
4851
+ const files = getAllFiles(source);
4852
+ for (const file of files) {
4853
+ const sourcePath = join12(source, file);
4854
+ const targetPath = join12(target, file);
4855
+ copyFile(sourcePath, targetPath);
4856
+ }
4857
+ }
4858
+ function getCoreVersion2(coreDir) {
4859
+ try {
4860
+ const pkgPath = join12(coreDir, "package.json");
4861
+ const pkg2 = JSON.parse(readFileSync10(pkgPath, "utf-8"));
4862
+ return pkg2.version || "unknown";
4863
+ } catch {
4864
+ return "unknown";
4865
+ }
4866
+ }
4867
+ async function syncAppCommand(options) {
4868
+ const spinner = ora11({ text: "Preparing sync...", isSilent: options.dryRun }).start();
4869
+ try {
4870
+ const coreDir = getCoreDir();
4871
+ const projectRoot = getProjectRoot();
4872
+ const coreVersion = getCoreVersion2(coreDir);
4873
+ const templatesDir = join12(coreDir, "templates", "app");
4874
+ const appDir = join12(projectRoot, "app");
4875
+ if (!existsSync11(templatesDir)) {
4876
+ spinner.fail("Templates directory not found in @nextsparkjs/core");
4877
+ console.error(chalk17.red(`
4878
+ Expected path: ${templatesDir}`));
4879
+ process.exit(1);
4880
+ }
4881
+ if (!existsSync11(appDir)) {
4882
+ spinner.fail("No /app directory found");
4883
+ console.error(chalk17.red("\n This project does not have an /app folder."));
4884
+ console.error(chalk17.yellow(' Run "nextspark init" first to initialize your project.\n'));
4885
+ process.exit(1);
4886
+ }
4887
+ spinner.text = "Scanning template files...";
4888
+ const templateFiles = getAllFiles(templatesDir).filter((f) => !EXCLUDED_TEMPLATE_PATTERNS.some((pattern) => f.startsWith(pattern)));
4889
+ const existingAppFiles = getAllFiles(appDir);
4890
+ const customFiles = existingAppFiles.filter((f) => !templateFiles.includes(f));
4891
+ const filesToUpdate = templateFiles;
4892
+ spinner.succeed("Scan complete");
4893
+ console.log(chalk17.cyan(`
4894
+ Syncing /app with @nextsparkjs/core@${coreVersion}...
4895
+ `));
4896
+ if (options.dryRun) {
4897
+ console.log(chalk17.yellow(" [DRY RUN] No changes will be made\n"));
4898
+ }
4899
+ if (options.verbose) {
4900
+ console.log(chalk17.gray(" Files to sync:"));
4901
+ for (const file of filesToUpdate) {
4902
+ const targetPath = join12(appDir, file);
4903
+ const status = existsSync11(targetPath) ? chalk17.yellow("update") : chalk17.green("create");
4904
+ console.log(chalk17.gray(` ${status} ${file}`));
4905
+ }
4906
+ console.log();
4907
+ }
4908
+ console.log(chalk17.white(` Template files: ${filesToUpdate.length}`));
4909
+ console.log(chalk17.white(` Custom files preserved: ${customFiles.length}`));
4910
+ if (customFiles.length > 0 && options.verbose) {
4911
+ console.log(chalk17.gray("\n Custom files (will be preserved):"));
4912
+ for (const file of customFiles.slice(0, MAX_VERBOSE_FILES)) {
4913
+ console.log(chalk17.gray(` - ${file}`));
4914
+ }
4915
+ if (customFiles.length > MAX_VERBOSE_FILES) {
4916
+ console.log(chalk17.gray(` ... and ${customFiles.length - MAX_VERBOSE_FILES} more`));
4917
+ }
4918
+ }
4919
+ if (!options.force && !options.dryRun) {
4920
+ console.log(chalk17.yellow(`
4921
+ This will overwrite ${filesToUpdate.length} core template files.`));
4922
+ console.log(chalk17.gray(" Run with --dry-run to preview changes, or --force to skip this prompt.\n"));
4923
+ try {
4924
+ const { confirm: confirm6 } = await import("@inquirer/prompts");
4925
+ const confirmed = await confirm6({
4926
+ message: "Proceed with sync?",
4927
+ default: true
4928
+ });
4929
+ if (!confirmed) {
4930
+ console.log(chalk17.yellow("\n Sync cancelled.\n"));
4931
+ process.exit(0);
4932
+ }
4933
+ } catch (promptError) {
4934
+ console.error(chalk17.red("\n Failed to load confirmation prompt. Use --force to skip.\n"));
4935
+ process.exit(1);
4936
+ }
4937
+ }
4938
+ if (options.backup && !options.dryRun) {
4939
+ const backupDir = join12(projectRoot, `app.backup.v${coreVersion}.${Date.now()}`);
4940
+ spinner.start("Creating backup...");
4941
+ backupDirectory(appDir, backupDir);
4942
+ spinner.succeed(`Backup created: ${relative(projectRoot, backupDir)}`);
4943
+ }
4944
+ if (!options.dryRun) {
4945
+ spinner.start("Syncing files...");
4946
+ let updated = 0;
4947
+ let created = 0;
4948
+ for (const file of filesToUpdate) {
4949
+ const sourcePath = join12(templatesDir, file);
4950
+ const targetPath = join12(appDir, file);
4951
+ const isNew = !existsSync11(targetPath);
4952
+ copyFile(sourcePath, targetPath);
4953
+ if (isNew) {
4954
+ created++;
4955
+ } else {
4956
+ updated++;
4957
+ }
4958
+ if (options.verbose) {
4959
+ spinner.text = `Syncing: ${file}`;
4960
+ }
4961
+ }
4962
+ spinner.succeed(`Synced ${filesToUpdate.length} files (${updated} updated, ${created} created)`);
4963
+ const rootTemplatesDir = join12(coreDir, "templates");
4964
+ let rootUpdated = 0;
4965
+ let rootCreated = 0;
4966
+ for (const file of ROOT_TEMPLATE_FILES) {
4967
+ const sourcePath = join12(rootTemplatesDir, file);
4968
+ const targetPath = join12(projectRoot, file);
4969
+ if (existsSync11(sourcePath)) {
4970
+ const isNew = !existsSync11(targetPath);
4971
+ copyFile(sourcePath, targetPath);
4972
+ if (isNew) {
4973
+ rootCreated++;
4974
+ if (options.verbose) {
4975
+ console.log(chalk17.green(` + Created: ${file}`));
4976
+ }
4977
+ } else {
4978
+ rootUpdated++;
4979
+ if (options.verbose) {
4980
+ console.log(chalk17.yellow(` ~ Updated: ${file}`));
4981
+ }
4982
+ }
4983
+ }
4984
+ }
4985
+ if (rootUpdated + rootCreated > 0) {
4986
+ console.log(chalk17.gray(` Root files: ${rootUpdated} updated, ${rootCreated} created`));
4987
+ }
4988
+ }
4989
+ console.log(chalk17.green("\n \u2705 Sync complete!\n"));
4990
+ if (customFiles.length > 0) {
4991
+ console.log(chalk17.gray(` Preserved ${customFiles.length} custom file(s):`));
4992
+ for (const file of customFiles.slice(0, MAX_SUMMARY_FILES)) {
4993
+ console.log(chalk17.gray(` - app/${file}`));
4994
+ }
4995
+ if (customFiles.length > MAX_SUMMARY_FILES) {
4996
+ console.log(chalk17.gray(` ... and ${customFiles.length - MAX_SUMMARY_FILES} more
4997
+ `));
4998
+ } else {
4999
+ console.log();
5000
+ }
5001
+ }
5002
+ } catch (error) {
5003
+ spinner.fail("Sync failed");
5004
+ if (error instanceof Error) {
5005
+ console.error(chalk17.red(`
5006
+ Error: ${error.message}
5007
+ `));
5008
+ if (options.verbose && error.stack) {
5009
+ console.error(chalk17.gray(` Stack trace:
5010
+ ${error.stack}
5011
+ `));
5012
+ }
5013
+ }
5014
+ process.exit(1);
5015
+ }
5016
+ }
5017
+
5018
+ // src/commands/setup-ai.ts
5019
+ import { existsSync as existsSync12 } from "fs";
5020
+ import { join as join13 } from "path";
5021
+ import { execSync as execSync3 } from "child_process";
5022
+ import chalk18 from "chalk";
5023
+ import ora12 from "ora";
5024
+ var VALID_EDITORS = ["claude", "cursor", "antigravity", "all"];
5025
+ async function setupAICommand(options) {
5026
+ const editor = options.editor || "claude";
5027
+ if (!VALID_EDITORS.includes(editor)) {
5028
+ console.log(chalk18.red(` Unknown editor: ${editor}`));
5029
+ console.log(chalk18.gray(` Available: ${VALID_EDITORS.join(", ")}`));
5030
+ process.exit(1);
5031
+ }
5032
+ console.log("");
5033
+ console.log(chalk18.cyan(" AI Workflow Setup"));
5034
+ console.log(chalk18.gray(" " + "-".repeat(40)));
5035
+ console.log("");
5036
+ const pkgPath = getAIWorkflowDir();
5037
+ if (!pkgPath) {
5038
+ console.log(chalk18.red(" @nextsparkjs/ai-workflow package not found."));
5039
+ console.log("");
5040
+ console.log(chalk18.gray(" Install it first:"));
5041
+ console.log(chalk18.cyan(" pnpm add -D -w @nextsparkjs/ai-workflow"));
5042
+ console.log("");
5043
+ process.exit(1);
5044
+ }
5045
+ const setupScript = join13(pkgPath, "scripts", "setup.mjs");
5046
+ if (!existsSync12(setupScript)) {
5047
+ console.log(chalk18.red(" Setup script not found in ai-workflow package."));
5048
+ console.log(chalk18.gray(` Expected: ${setupScript}`));
5049
+ process.exit(1);
5050
+ }
5051
+ const spinner = ora12({
5052
+ text: `Setting up AI workflow for ${editor}...`,
5053
+ prefixText: " "
5054
+ }).start();
5055
+ try {
5056
+ spinner.stop();
5057
+ execSync3(`node "${setupScript}" ${editor}`, {
5058
+ cwd: process.cwd(),
5059
+ stdio: "inherit"
5060
+ });
5061
+ } catch (error) {
5062
+ console.log("");
5063
+ console.log(chalk18.red(" AI workflow setup failed."));
5064
+ if (error instanceof Error) {
5065
+ console.log(chalk18.gray(` ${error.message}`));
5066
+ }
5067
+ process.exit(1);
5068
+ }
5069
+ }
5070
+
5071
+ // src/commands/sync-ai.ts
5072
+ import { existsSync as existsSync13 } from "fs";
5073
+ import { join as join14 } from "path";
5074
+ import { execSync as execSync4 } from "child_process";
5075
+ import chalk19 from "chalk";
5076
+ import ora13 from "ora";
5077
+ var VALID_EDITORS2 = ["claude", "cursor", "antigravity", "all"];
5078
+ async function syncAICommand(options) {
5079
+ const editor = options.editor || "claude";
5080
+ if (!VALID_EDITORS2.includes(editor)) {
5081
+ console.log(chalk19.red(` Unknown editor: ${editor}`));
5082
+ console.log(chalk19.gray(` Available: ${VALID_EDITORS2.join(", ")}`));
5083
+ process.exit(1);
5084
+ }
5085
+ console.log("");
5086
+ console.log(chalk19.cyan(" AI Workflow Sync"));
5087
+ console.log(chalk19.gray(" " + "-".repeat(40)));
5088
+ console.log("");
5089
+ const editorDir = editor === "cursor" ? ".cursor" : ".claude";
5090
+ const editorDirPath = join14(process.cwd(), editorDir);
5091
+ if (!existsSync13(editorDirPath)) {
5092
+ console.log(chalk19.red(` No ${editorDir}/ directory found.`));
5093
+ console.log("");
5094
+ console.log(chalk19.gray(" AI workflow must be set up first. Run:"));
5095
+ console.log(chalk19.cyan(" nextspark setup:ai --editor " + editor));
5096
+ console.log("");
5097
+ process.exit(1);
5098
+ }
5099
+ const pkgPath = getAIWorkflowDir();
5100
+ if (!pkgPath) {
5101
+ console.log(chalk19.red(" @nextsparkjs/ai-workflow package not found."));
5102
+ console.log("");
5103
+ console.log(chalk19.gray(" Install it first:"));
5104
+ console.log(chalk19.cyan(" pnpm add -D -w @nextsparkjs/ai-workflow"));
5105
+ console.log("");
5106
+ process.exit(1);
5107
+ }
5108
+ const setupScript = join14(pkgPath, "scripts", "setup.mjs");
5109
+ if (!existsSync13(setupScript)) {
5110
+ console.log(chalk19.red(" Setup script not found in ai-workflow package."));
5111
+ console.log(chalk19.gray(` Expected: ${setupScript}`));
5112
+ process.exit(1);
5113
+ }
5114
+ if (!options.force) {
5115
+ console.log(chalk19.yellow(" This will sync AI workflow files from @nextsparkjs/ai-workflow."));
5116
+ console.log(chalk19.gray(" Framework files will be overwritten. Custom files will be preserved."));
5117
+ console.log(chalk19.gray(" Config JSON files will never be overwritten.\n"));
5118
+ try {
5119
+ const { confirm: confirm6 } = await import("@inquirer/prompts");
5120
+ const confirmed = await confirm6({
5121
+ message: "Proceed with sync?",
5122
+ default: true
5123
+ });
5124
+ if (!confirmed) {
5125
+ console.log(chalk19.yellow("\n Sync cancelled.\n"));
5126
+ process.exit(0);
5127
+ }
5128
+ } catch {
5129
+ console.error(chalk19.red("\n Failed to load confirmation prompt. Use --force to skip.\n"));
5130
+ process.exit(1);
5131
+ }
5132
+ }
5133
+ const spinner = ora13({
5134
+ text: `Syncing AI workflow for ${editor}...`,
5135
+ prefixText: " "
5136
+ }).start();
5137
+ try {
5138
+ spinner.stop();
5139
+ execSync4(`node "${setupScript}" ${editor}`, {
5140
+ cwd: process.cwd(),
5141
+ stdio: "inherit"
5142
+ });
5143
+ } catch (error) {
5144
+ console.log("");
5145
+ console.log(chalk19.red(" AI workflow sync failed."));
5146
+ if (error instanceof Error) {
5147
+ console.log(chalk19.gray(` ${error.message}`));
3682
5148
  }
3683
5149
  process.exit(1);
3684
5150
  }
@@ -3686,8 +5152,9 @@ async function doctorCommand() {
3686
5152
 
3687
5153
  // src/cli.ts
3688
5154
  config();
5155
+ var pkg = JSON.parse(readFileSync11(new URL("../package.json", import.meta.url), "utf-8"));
3689
5156
  var program = new Command();
3690
- program.name("nextspark").description("NextSpark CLI - Professional SaaS Boilerplate").version("0.1.0-beta.4");
5157
+ program.name("nextspark").description("NextSpark CLI - Professional SaaS Boilerplate").version(pkg.version);
3691
5158
  program.command("dev").description("Start development server with registry watcher").option("-p, --port <port>", "Port to run the dev server on", process.env.PORT || "3000").option("--no-registry", "Disable registry watcher").action(devCommand);
3692
5159
  program.command("build").description("Build for production").option("--no-registry", "Skip registry generation before build").action(buildCommand);
3693
5160
  program.command("generate").description("Generate all registries").option("-w, --watch", "Watch for changes").action(generateCommand);
@@ -3696,12 +5163,21 @@ registry.command("build").description("Build all registries").action(registryBui
3696
5163
  registry.command("watch").description("Watch and rebuild registries on changes").action(registryWatchCommand);
3697
5164
  program.command("registry:build").description("Build all registries (alias)").action(registryBuildCommand);
3698
5165
  program.command("registry:watch").description("Watch and rebuild registries (alias)").action(registryWatchCommand);
3699
- program.command("init").description("Initialize NextSpark project").option("-f, --force", "Overwrite existing configuration").option("--wizard", "Run full project wizard").option("--quick", "Quick wizard mode (essential steps only)").option("--expert", "Expert wizard mode (all options)").option("--preset <name>", "Use preset configuration (saas, blog, crm)").option("--theme <name>", "Pre-select theme (default, blog, crm, productivity, none)").option("--plugins <list>", "Pre-select plugins (comma-separated)").option("-y, --yes", "Skip confirmations").option("--registries-only", "Only create registries (no wizard)").option("--name <name>", "Project name (non-interactive mode)").option("--slug <slug>", "Project slug (non-interactive mode)").option("--description <desc>", "Project description (non-interactive mode)").action(initCommand);
5166
+ program.command("init").description("Initialize NextSpark project").option("-f, --force", "Overwrite existing configuration").option("--wizard", "Run full project wizard").option("--quick", "Quick wizard mode (essential steps only)").option("--expert", "Expert wizard mode (all options)").option("--preset <name>", "Use preset configuration (saas, blog, crm)").option("--theme <name>", "Pre-select theme (default, blog, crm, productivity, none)").option("--plugins <list>", "Pre-select plugins (comma-separated)").option("-y, --yes", "Skip confirmations").option("--registries-only", "Only create registries (no wizard)").option("--name <name>", "Project name (non-interactive mode)").option("--slug <slug>", "Project slug (non-interactive mode)").option("--description <desc>", "Project description (non-interactive mode)").option("--type <type>", "Project type: web or web-mobile (non-interactive mode)").action(initCommand);
3700
5167
  program.command("add:plugin <package>").description("Add a plugin to your project").option("-v, --version <version>", "Specific version to install").option("-f, --force", "Overwrite if already exists").option("--skip-postinstall", "Skip postinstall hooks").option("--no-deps", "Skip installing dependencies").option("--dry-run", "Show what would be done without making changes").action(addPluginCommand);
3701
5168
  program.command("add:theme <package>").description("Add a theme to your project").option("-v, --version <version>", "Specific version to install").option("-f, --force", "Overwrite if already exists").option("--skip-postinstall", "Skip postinstall hooks").option("--no-deps", "Skip installing dependencies").option("--dry-run", "Show what would be done without making changes").action(addThemeCommand);
5169
+ program.command("add:mobile").description("Add mobile app to your project").option("-f, --force", "Overwrite if already exists").option("--skip-install", "Skip npm install").action(addMobileCommand);
3702
5170
  program.command("doctor").description("Run health check on NextSpark project").action(doctorCommand);
5171
+ var db = program.command("db").description("Database management commands");
5172
+ db.command("migrate").description("Run database migrations").action(dbMigrateCommand);
5173
+ db.command("seed").description("Seed database with sample data").action(dbSeedCommand);
5174
+ program.command("db:migrate").description("Run database migrations (alias)").action(dbMigrateCommand);
5175
+ program.command("db:seed").description("Seed database with sample data (alias)").action(dbSeedCommand);
5176
+ program.command("sync:app").description("Sync /app folder with @nextsparkjs/core templates").option("--dry-run", "Preview changes without applying").option("-f, --force", "Skip confirmation prompt").option("--backup", "Backup existing files before overwriting").option("-v, --verbose", "Show detailed file operations").action(syncAppCommand);
5177
+ program.command("setup:ai").description("Setup AI workflow for your editor (Claude Code, Cursor, Antigravity)").option("-e, --editor <editor>", "Editor to setup (claude, cursor, antigravity, all)", "claude").action(setupAICommand);
5178
+ program.command("sync:ai").description("Sync AI workflow files from @nextsparkjs/ai-workflow").option("-e, --editor <editor>", "Editor to sync (claude, cursor, antigravity, all)", "claude").option("-f, --force", "Skip confirmation prompt").action(syncAICommand);
3703
5179
  program.showHelpAfterError();
3704
5180
  program.configureOutput({
3705
- writeErr: (str) => process.stderr.write(chalk15.red(str))
5181
+ writeErr: (str) => process.stderr.write(chalk20.red(str))
3706
5182
  });
3707
5183
  program.parse();