@nextsparkjs/cli 0.1.0-beta.11 → 0.1.0-beta.111
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/nextspark.js +9 -0
- package/dist/{add-plugin-GCPJ4FHE.js → add-plugin-KIJLCOEQ.js} +1 -1
- package/dist/{chunk-ALB2C27N.js → chunk-DXL5CEZD.js} +103 -110
- package/dist/cli.js +1808 -332
- package/package.json +19 -10
- package/templates/env.template +165 -0
- package/dist/add-plugin-I3UJFL7U.js +0 -8
- package/dist/chunk-EE6EJYHE.js +0 -704
package/dist/cli.js
CHANGED
|
@@ -7,12 +7,13 @@ import {
|
|
|
7
7
|
installTheme,
|
|
8
8
|
runPostinstall,
|
|
9
9
|
validateTheme
|
|
10
|
-
} from "./chunk-
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
461
|
+
join5(__dirname2, "../package.json"),
|
|
449
462
|
// from dist/
|
|
450
|
-
|
|
463
|
+
join5(__dirname2, "../../package.json"),
|
|
451
464
|
// from dist/wizard/ or src/wizard/
|
|
452
|
-
|
|
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",
|
|
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",
|
|
604
|
-
const teamMode = await
|
|
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
|
|
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",
|
|
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
|
|
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
|
|
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",
|
|
695
|
-
const billingModel = await
|
|
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
|
|
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",
|
|
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",
|
|
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",
|
|
916
|
+
showSection("Authentication", 8, totalSteps);
|
|
848
917
|
showInfo("Configure how users will authenticate to your application.");
|
|
849
918
|
console.log("");
|
|
850
|
-
const
|
|
851
|
-
message: "
|
|
852
|
-
choices:
|
|
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
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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",
|
|
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",
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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",
|
|
1119
|
-
const authConfig = await promptAuthConfig("interactive",
|
|
1120
|
-
const dashboardConfig = await promptDashboardConfig("interactive",
|
|
1121
|
-
const devConfig = await promptDevConfig("interactive",
|
|
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",
|
|
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",
|
|
1163
|
-
const authConfig = await promptAuthConfig("expert",
|
|
1164
|
-
const dashboardConfig = await promptDashboardConfig("expert",
|
|
1165
|
-
const devConfig = await promptDevConfig("expert",
|
|
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
|
|
1181
|
-
import
|
|
1182
|
-
import { fileURLToPath as
|
|
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
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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
|
|
1631
|
+
const appConfigPath = path2.join(
|
|
1498
1632
|
getTargetThemesDir2(),
|
|
1499
1633
|
config2.projectSlug,
|
|
1500
1634
|
"config",
|
|
1501
|
-
"
|
|
1635
|
+
"app.config.ts"
|
|
1502
1636
|
);
|
|
1503
|
-
if (!await fs2.pathExists(
|
|
1637
|
+
if (!await fs2.pathExists(appConfigPath)) {
|
|
1504
1638
|
return;
|
|
1505
1639
|
}
|
|
1506
|
-
let content = await fs2.readFile(
|
|
1640
|
+
let content = await fs2.readFile(appConfigPath, "utf-8");
|
|
1507
1641
|
content = content.replace(
|
|
1508
|
-
/(
|
|
1509
|
-
`$1${config2.auth.
|
|
1642
|
+
/(mode:\s*)'open'(\s*as\s*const)/,
|
|
1643
|
+
`$1'${config2.auth.registrationMode}'$2`
|
|
1510
1644
|
);
|
|
1511
1645
|
content = content.replace(
|
|
1512
|
-
/(google:\s*{
|
|
1646
|
+
/(google:\s*{\s*enabled:\s*)(?:true|false)/s,
|
|
1513
1647
|
`$1${config2.auth.googleOAuth}`
|
|
1514
1648
|
);
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
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
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
2161
|
+
return existsSync5(join6(process.cwd(), "contents", "plugins", name));
|
|
1975
2162
|
}
|
|
1976
2163
|
function getCoreVersion() {
|
|
1977
|
-
const pkgPath =
|
|
2164
|
+
const pkgPath = join6(process.cwd(), "node_modules", "@nextsparkjs", "core", "package.json");
|
|
1978
2165
|
if (existsSync5(pkgPath)) {
|
|
1979
2166
|
try {
|
|
1980
|
-
const
|
|
1981
|
-
return
|
|
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
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
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
|
-
|
|
2028
|
-
|
|
2214
|
+
join7(process.cwd(), ".."),
|
|
2215
|
+
join7(process.cwd(), "..", "..")
|
|
2029
2216
|
];
|
|
2030
2217
|
for (const root of possibleRoots) {
|
|
2031
|
-
if (existsSync6(
|
|
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 =
|
|
2042
|
-
if (existsSync6(packageDir) && existsSync6(
|
|
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 =
|
|
2049
|
-
const themesDir =
|
|
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 =
|
|
2063
|
-
const pluginsDir =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
2240
|
-
var
|
|
2241
|
-
function getTemplatesDir3() {
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
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
|
-
|
|
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: "
|
|
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 =
|
|
2276
|
-
const destPath =
|
|
2277
|
-
if (await
|
|
2278
|
-
if (item.force || !await
|
|
2279
|
-
await
|
|
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 =
|
|
3018
|
+
const packageJsonPath = path7.resolve(process.cwd(), "package.json");
|
|
2286
3019
|
let packageJson;
|
|
2287
|
-
if (!await
|
|
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
|
|
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
|
|
2309
|
-
"cy:open":
|
|
2310
|
-
"cy:run":
|
|
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": "
|
|
2323
|
-
"@nextsparkjs/cli": "
|
|
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 (
|
|
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": "^
|
|
3119
|
+
"cypress": "^15.8.2",
|
|
2383
3120
|
"@testing-library/cypress": "^10.0.2",
|
|
2384
3121
|
"@cypress/webpack-preprocessor": "^6.0.2",
|
|
2385
|
-
"@cypress/grep": "^
|
|
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 (
|
|
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
|
|
3137
|
+
await fs8.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
2397
3138
|
}
|
|
2398
3139
|
async function updateGitignore(config2) {
|
|
2399
|
-
const gitignorePath =
|
|
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
|
|
2418
|
-
const currentContent = await
|
|
3158
|
+
if (await fs8.pathExists(gitignorePath)) {
|
|
3159
|
+
const currentContent = await fs8.readFile(gitignorePath, "utf-8");
|
|
2419
3160
|
if (!currentContent.includes(".nextspark/")) {
|
|
2420
|
-
await
|
|
3161
|
+
await fs8.appendFile(gitignorePath, entriesToAdd);
|
|
2421
3162
|
}
|
|
2422
3163
|
} else {
|
|
2423
|
-
await
|
|
3164
|
+
await fs8.writeFile(gitignorePath, entriesToAdd.trim());
|
|
2424
3165
|
}
|
|
2425
3166
|
}
|
|
2426
3167
|
async function generateProject(config2) {
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
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.
|
|
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:
|
|
2846
|
-
|
|
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
|
|
2859
|
-
|
|
3642
|
+
const registryScript = join8(webDir, "node_modules/@nextsparkjs/core/scripts/build/registry.mjs");
|
|
3643
|
+
registrySpinner.stop();
|
|
2860
3644
|
execSync(`node "${registryScript}" --build`, {
|
|
2861
|
-
cwd:
|
|
2862
|
-
|
|
3645
|
+
cwd: webDir,
|
|
3646
|
+
// Run from web directory
|
|
3647
|
+
stdio: "inherit",
|
|
2863
3648
|
env: {
|
|
2864
3649
|
...process.env,
|
|
2865
|
-
NEXTSPARK_PROJECT_ROOT:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2988
|
-
console.log(chalk11.
|
|
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.
|
|
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
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
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(
|
|
3047
|
-
const usePnpm = existsSync7(
|
|
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: "
|
|
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
|
-
|
|
3072
|
-
const
|
|
3073
|
-
|
|
3074
|
-
|
|
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(
|
|
4014
|
+
return existsSync8(join9(projectRoot, "contents")) || existsSync8(join9(projectRoot, ".nextspark"));
|
|
3097
4015
|
}
|
|
3098
4016
|
function generateInitialRegistries(registriesDir) {
|
|
3099
|
-
writeFileSync2(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
4043
|
+
writeFileSync2(join9(registriesDir, "billing-registry.ts"), `// Auto-generated by nextspark init
|
|
3126
4044
|
export const BILLING_REGISTRY = { plans: [], features: [] }
|
|
3127
4045
|
`);
|
|
3128
|
-
writeFileSync2(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
3159
|
-
const registriesDir =
|
|
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 =
|
|
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 =
|
|
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
|
|
4253
|
+
import chalk15 from "chalk";
|
|
3228
4254
|
|
|
3229
4255
|
// src/doctor/index.ts
|
|
3230
|
-
import
|
|
4256
|
+
import chalk14 from "chalk";
|
|
3231
4257
|
|
|
3232
4258
|
// src/doctor/checks/dependencies.ts
|
|
3233
|
-
import
|
|
3234
|
-
import
|
|
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 =
|
|
3238
|
-
const packageJsonPath =
|
|
3239
|
-
if (!await
|
|
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
|
|
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
|
|
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 =
|
|
3275
|
-
if (!await
|
|
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 =
|
|
3281
|
-
const hasNextSparkCore = await
|
|
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
|
|
3307
|
-
import
|
|
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 =
|
|
4344
|
+
const filePath = path9.join(cwd, file);
|
|
3319
4345
|
if (file.endsWith(".ts")) {
|
|
3320
|
-
const jsPath =
|
|
3321
|
-
const mjsPath =
|
|
3322
|
-
const tsExists = await
|
|
3323
|
-
const jsExists = await
|
|
3324
|
-
const mjsExists = await
|
|
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
|
|
4358
|
+
if (!await fs10.pathExists(filePath)) {
|
|
3333
4359
|
missingFiles.push(file);
|
|
3334
4360
|
}
|
|
3335
4361
|
}
|
|
3336
|
-
const tsconfigPath =
|
|
3337
|
-
if (await
|
|
4362
|
+
const tsconfigPath = path9.join(cwd, "tsconfig.json");
|
|
4363
|
+
if (await fs10.pathExists(tsconfigPath)) {
|
|
3338
4364
|
try {
|
|
3339
|
-
const content = await
|
|
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 =
|
|
3348
|
-
if (await
|
|
3349
|
-
const configFiles = await
|
|
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 =
|
|
4378
|
+
const filePath = path9.join(configDir, file);
|
|
3353
4379
|
try {
|
|
3354
|
-
const content = await
|
|
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
|
|
3388
|
-
import
|
|
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 =
|
|
3412
|
-
const envLocalPath =
|
|
4437
|
+
const envPath = path10.join(cwd, ".env");
|
|
4438
|
+
const envLocalPath = path10.join(cwd, ".env.local");
|
|
3413
4439
|
let envVars = {};
|
|
3414
|
-
if (await
|
|
4440
|
+
if (await fs11.pathExists(envLocalPath)) {
|
|
3415
4441
|
try {
|
|
3416
|
-
const content = await
|
|
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
|
|
4447
|
+
if (await fs11.pathExists(envPath)) {
|
|
3422
4448
|
try {
|
|
3423
|
-
const content = await
|
|
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
|
|
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
|
|
3471
|
-
import
|
|
4496
|
+
import fs12 from "fs-extra";
|
|
4497
|
+
import path11 from "path";
|
|
3472
4498
|
async function checkCorePackage(cwd) {
|
|
3473
|
-
const nodeModulesPath =
|
|
3474
|
-
if (!await
|
|
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 =
|
|
3478
|
-
if (!await
|
|
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
|
|
4508
|
+
const packageJson = await fs12.readJson(packageJsonPath);
|
|
3483
4509
|
const mainEntry = packageJson.main || packageJson.module || "./dist/index.js";
|
|
3484
|
-
const mainPath =
|
|
3485
|
-
if (!await
|
|
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 =
|
|
3499
|
-
if (await
|
|
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
|
|
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 =
|
|
4538
|
+
const filePath = path11.join(dir, file.name);
|
|
3513
4539
|
try {
|
|
3514
|
-
const content = await
|
|
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 =
|
|
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
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
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
|
|
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 =
|
|
4583
|
+
const packageJsonPath = path11.join(cwd, "package.json");
|
|
3558
4584
|
let hasCoreDep = false;
|
|
3559
|
-
if (await
|
|
4585
|
+
if (await fs12.pathExists(packageJsonPath)) {
|
|
3560
4586
|
try {
|
|
3561
|
-
const packageJson = await
|
|
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:
|
|
3599
|
-
warn:
|
|
3600
|
-
fail:
|
|
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" ?
|
|
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 =
|
|
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)}${
|
|
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(
|
|
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(
|
|
4650
|
+
console.log(chalk14.gray("-".repeat(50)));
|
|
3625
4651
|
const summary = [
|
|
3626
|
-
|
|
3627
|
-
warnings > 0 ?
|
|
3628
|
-
failed > 0 ?
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
5181
|
+
writeErr: (str) => process.stderr.write(chalk20.red(str))
|
|
3706
5182
|
});
|
|
3707
5183
|
program.parse();
|