nexu-app 2.0.0

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.
Files changed (106) hide show
  1. package/README.md +149 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +1192 -0
  4. package/package.json +43 -0
  5. package/templates/default/.changeset/config.json +11 -0
  6. package/templates/default/.eslintignore +16 -0
  7. package/templates/default/.eslintrc.js +67 -0
  8. package/templates/default/.github/actions/build/action.yml +35 -0
  9. package/templates/default/.github/actions/quality/action.yml +53 -0
  10. package/templates/default/.github/dependabot.yml +51 -0
  11. package/templates/default/.github/workflows/deploy-dev.yml +83 -0
  12. package/templates/default/.github/workflows/deploy-prod.yml +83 -0
  13. package/templates/default/.github/workflows/deploy-rec.yml +83 -0
  14. package/templates/default/.husky/commit-msg +1 -0
  15. package/templates/default/.husky/pre-commit +1 -0
  16. package/templates/default/.nexu-version +1 -0
  17. package/templates/default/.prettierignore +7 -0
  18. package/templates/default/.prettierrc +19 -0
  19. package/templates/default/.vscode/extensions.json +14 -0
  20. package/templates/default/.vscode/settings.json +36 -0
  21. package/templates/default/apps/gitkeep +0 -0
  22. package/templates/default/commitlint.config.js +26 -0
  23. package/templates/default/docker/docker-compose.dev.yml +49 -0
  24. package/templates/default/docker/docker-compose.prod.yml +64 -0
  25. package/templates/default/docker/docker-compose.yml +6 -0
  26. package/templates/default/docs/architecture.md +452 -0
  27. package/templates/default/docs/cli.md +330 -0
  28. package/templates/default/docs/contributing.md +462 -0
  29. package/templates/default/docs/scripts.md +460 -0
  30. package/templates/default/gitignore +44 -0
  31. package/templates/default/lintstagedrc.cjs +4 -0
  32. package/templates/default/package.json +51 -0
  33. package/templates/default/packages/auth/package.json +61 -0
  34. package/templates/default/packages/auth/src/components/ProtectedRoute.tsx +75 -0
  35. package/templates/default/packages/auth/src/components/SignInForm.tsx +153 -0
  36. package/templates/default/packages/auth/src/components/SignUpForm.tsx +179 -0
  37. package/templates/default/packages/auth/src/components/SocialButtons.tsx +147 -0
  38. package/templates/default/packages/auth/src/components/index.ts +4 -0
  39. package/templates/default/packages/auth/src/hooks/index.ts +4 -0
  40. package/templates/default/packages/auth/src/hooks/useAuth.ts +51 -0
  41. package/templates/default/packages/auth/src/hooks/useRequireAuth.ts +54 -0
  42. package/templates/default/packages/auth/src/hooks/useSession.ts +48 -0
  43. package/templates/default/packages/auth/src/hooks/useUser.ts +48 -0
  44. package/templates/default/packages/auth/src/index.ts +45 -0
  45. package/templates/default/packages/auth/src/next/index.ts +18 -0
  46. package/templates/default/packages/auth/src/next/middleware.ts +183 -0
  47. package/templates/default/packages/auth/src/next/server.ts +219 -0
  48. package/templates/default/packages/auth/src/providers/AuthContext.tsx +435 -0
  49. package/templates/default/packages/auth/src/providers/index.ts +1 -0
  50. package/templates/default/packages/auth/src/types/index.ts +284 -0
  51. package/templates/default/packages/auth/src/utils/api.ts +228 -0
  52. package/templates/default/packages/auth/src/utils/index.ts +3 -0
  53. package/templates/default/packages/auth/src/utils/oauth.ts +230 -0
  54. package/templates/default/packages/auth/src/utils/token.ts +204 -0
  55. package/templates/default/packages/auth/tsconfig.json +14 -0
  56. package/templates/default/packages/auth/tsup.config.ts +18 -0
  57. package/templates/default/packages/cache/package.json +26 -0
  58. package/templates/default/packages/cache/src/index.ts +137 -0
  59. package/templates/default/packages/cache/tsconfig.json +9 -0
  60. package/templates/default/packages/cache/tsup.config.ts +9 -0
  61. package/templates/default/packages/config/eslint/index.js +20 -0
  62. package/templates/default/packages/config/package.json +9 -0
  63. package/templates/default/packages/config/typescript/base.json +26 -0
  64. package/templates/default/packages/constants/package.json +26 -0
  65. package/templates/default/packages/constants/src/index.ts +121 -0
  66. package/templates/default/packages/constants/tsconfig.json +9 -0
  67. package/templates/default/packages/constants/tsup.config.ts +9 -0
  68. package/templates/default/packages/logger/package.json +27 -0
  69. package/templates/default/packages/logger/src/index.ts +197 -0
  70. package/templates/default/packages/logger/tsconfig.json +11 -0
  71. package/templates/default/packages/logger/tsup.config.ts +9 -0
  72. package/templates/default/packages/result/package.json +26 -0
  73. package/templates/default/packages/result/src/index.ts +142 -0
  74. package/templates/default/packages/result/tsconfig.json +9 -0
  75. package/templates/default/packages/result/tsup.config.ts +9 -0
  76. package/templates/default/packages/types/package.json +26 -0
  77. package/templates/default/packages/types/src/index.ts +78 -0
  78. package/templates/default/packages/types/tsconfig.json +9 -0
  79. package/templates/default/packages/types/tsup.config.ts +10 -0
  80. package/templates/default/packages/ui/package.json +38 -0
  81. package/templates/default/packages/ui/src/components/Button.tsx +58 -0
  82. package/templates/default/packages/ui/src/components/Card.tsx +85 -0
  83. package/templates/default/packages/ui/src/components/Input.tsx +45 -0
  84. package/templates/default/packages/ui/src/index.ts +15 -0
  85. package/templates/default/packages/ui/tsconfig.json +11 -0
  86. package/templates/default/packages/ui/tsup.config.ts +11 -0
  87. package/templates/default/packages/utils/package.json +30 -0
  88. package/templates/default/packages/utils/src/index.test.ts +130 -0
  89. package/templates/default/packages/utils/src/index.ts +154 -0
  90. package/templates/default/packages/utils/tsconfig.json +10 -0
  91. package/templates/default/packages/utils/tsup.config.ts +10 -0
  92. package/templates/default/pnpm-workspace.yaml +3 -0
  93. package/templates/default/scripts/audit.mjs +700 -0
  94. package/templates/default/scripts/deploy.mjs +40 -0
  95. package/templates/default/scripts/generate-app.mjs +808 -0
  96. package/templates/default/scripts/lib/package-manager.mjs +186 -0
  97. package/templates/default/scripts/setup.mjs +102 -0
  98. package/templates/default/services/.env.example +16 -0
  99. package/templates/default/services/docker-compose.yml +207 -0
  100. package/templates/default/services/grafana/provisioning/dashboards/dashboards.yml +11 -0
  101. package/templates/default/services/grafana/provisioning/datasources/datasources.yml +9 -0
  102. package/templates/default/services/postgres/init/gitkeep +2 -0
  103. package/templates/default/services/prometheus/prometheus.yml +13 -0
  104. package/templates/default/tsconfig.json +27 -0
  105. package/templates/default/turbo.json +40 -0
  106. package/templates/default/vitest.config.ts +15 -0
package/dist/index.js ADDED
@@ -0,0 +1,1192 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { readFileSync } from "fs";
5
+ import { dirname, join } from "path";
6
+ import { fileURLToPath as fileURLToPath4 } from "url";
7
+ import { Command } from "commander";
8
+
9
+ // src/commands/add.ts
10
+ import path2 from "path";
11
+ import { fileURLToPath } from "url";
12
+ import chalk2 from "chalk";
13
+ import fs2 from "fs-extra";
14
+ import inquirer from "inquirer";
15
+ import ora from "ora";
16
+
17
+ // src/utils/constants.ts
18
+ var TEMPLATE_DIRS = {
19
+ packages: "packages",
20
+ services: "services",
21
+ workflows: ".github/workflows",
22
+ actions: ".github/actions",
23
+ config: [
24
+ ".eslintrc.js",
25
+ ".eslintignore",
26
+ ".prettierrc",
27
+ ".prettierignore",
28
+ ".nexu-version",
29
+ "tsconfig.json",
30
+ "turbo.json",
31
+ "vitest.config.ts",
32
+ "commitlint.config.js"
33
+ ],
34
+ husky: ".husky",
35
+ vscode: ".vscode",
36
+ docker: "docker",
37
+ scripts: "scripts",
38
+ changeset: ".changeset"
39
+ };
40
+ var SHARED_PACKAGES = [
41
+ "auth",
42
+ "cache",
43
+ "config",
44
+ "constants",
45
+ "logger",
46
+ "result",
47
+ "types",
48
+ "ui",
49
+ "utils"
50
+ ];
51
+ var SERVICES = [
52
+ "postgres",
53
+ "redis",
54
+ "rabbitmq",
55
+ "kafka",
56
+ "prometheus",
57
+ "grafana",
58
+ "minio",
59
+ "elasticsearch"
60
+ ];
61
+
62
+ // src/utils/helpers.ts
63
+ import { execSync } from "child_process";
64
+ import path from "path";
65
+ import chalk from "chalk";
66
+ import fs from "fs-extra";
67
+ function isNexuProject(dir) {
68
+ const packageJsonPath = path.join(dir, "package.json");
69
+ if (!fs.existsSync(packageJsonPath)) return false;
70
+ try {
71
+ const packageJson2 = fs.readJsonSync(packageJsonPath);
72
+ return packageJson2.name === "nexu" || fs.existsSync(path.join(dir, "turbo.json"));
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+ function exec(command, cwd) {
78
+ try {
79
+ return execSync(command, {
80
+ cwd,
81
+ encoding: "utf-8",
82
+ stdio: ["pipe", "pipe", "pipe"]
83
+ });
84
+ } catch (error) {
85
+ throw new Error(`Command failed: ${command}`);
86
+ }
87
+ }
88
+ function execInherit(command, cwd) {
89
+ try {
90
+ execSync(command, {
91
+ cwd,
92
+ stdio: "inherit"
93
+ });
94
+ } catch (error) {
95
+ throw new Error(`Command failed: ${command}`);
96
+ }
97
+ }
98
+ function log(message, type = "info") {
99
+ const colors = {
100
+ info: chalk.blue,
101
+ success: chalk.green,
102
+ warn: chalk.yellow,
103
+ error: chalk.red
104
+ };
105
+ const icons = {
106
+ info: "i",
107
+ success: "\u2713",
108
+ warn: "!",
109
+ error: "\u2717"
110
+ };
111
+ console.log(`${colors[type](icons[type])} ${message}`);
112
+ }
113
+ function detectPackageManager(projectDir = process.cwd()) {
114
+ if (fs.existsSync(path.join(projectDir, "pnpm-lock.yaml"))) {
115
+ return "pnpm";
116
+ }
117
+ if (fs.existsSync(path.join(projectDir, "yarn.lock"))) {
118
+ return "yarn";
119
+ }
120
+ if (fs.existsSync(path.join(projectDir, "package-lock.json"))) {
121
+ return "npm";
122
+ }
123
+ const packageJsonPath = path.join(projectDir, "package.json");
124
+ if (fs.existsSync(packageJsonPath)) {
125
+ try {
126
+ const pkg = fs.readJsonSync(packageJsonPath);
127
+ if (pkg.packageManager) {
128
+ if (pkg.packageManager.startsWith("pnpm")) return "pnpm";
129
+ if (pkg.packageManager.startsWith("yarn")) return "yarn";
130
+ if (pkg.packageManager.startsWith("npm")) return "npm";
131
+ }
132
+ } catch {
133
+ }
134
+ }
135
+ const userAgent = process.env.npm_config_user_agent || "";
136
+ if (userAgent.includes("pnpm")) return "pnpm";
137
+ if (userAgent.includes("yarn")) return "yarn";
138
+ return "npm";
139
+ }
140
+ function getRunCommand(pm) {
141
+ switch (pm) {
142
+ case "pnpm":
143
+ return "pnpm";
144
+ case "yarn":
145
+ return "yarn";
146
+ case "npm":
147
+ default:
148
+ return "npm run";
149
+ }
150
+ }
151
+ function getPackageManagerVersion(pm) {
152
+ try {
153
+ const version = execSync(`${pm} --version`, {
154
+ encoding: "utf-8",
155
+ stdio: ["pipe", "pipe", "pipe"]
156
+ }).trim();
157
+ return version;
158
+ } catch {
159
+ return null;
160
+ }
161
+ }
162
+ function getPackageManagerField(pm) {
163
+ const version = getPackageManagerVersion(pm);
164
+ if (!version) return null;
165
+ return `${pm}@${version}`;
166
+ }
167
+ function getInstallCommand(pm) {
168
+ switch (pm) {
169
+ case "pnpm":
170
+ return "pnpm install";
171
+ case "yarn":
172
+ return "yarn install";
173
+ case "npm":
174
+ default:
175
+ return "npm install";
176
+ }
177
+ }
178
+
179
+ // src/commands/add.ts
180
+ var __filename = fileURLToPath(import.meta.url);
181
+ var __dirname = path2.dirname(__filename);
182
+ function getTemplateDir() {
183
+ const possiblePaths = [
184
+ path2.resolve(__dirname, "..", "..", "templates", "default"),
185
+ path2.resolve(__dirname, "..", "templates", "default")
186
+ ];
187
+ for (const templatePath of possiblePaths) {
188
+ if (fs2.existsSync(templatePath)) {
189
+ return templatePath;
190
+ }
191
+ }
192
+ throw new Error("Template directory not found. Please reinstall the package.");
193
+ }
194
+ async function add(component, options) {
195
+ console.log(chalk2.bold("\n\u2795 Add Component\n"));
196
+ const projectDir = process.cwd();
197
+ if (!isNexuProject(projectDir)) {
198
+ log("This does not appear to be a Nexu project.", "error");
199
+ process.exit(1);
200
+ }
201
+ const componentType = component.toLowerCase();
202
+ switch (componentType) {
203
+ case "package":
204
+ await addPackage(projectDir, options.name);
205
+ break;
206
+ case "service":
207
+ await addService(projectDir, options.name);
208
+ break;
209
+ default:
210
+ log(`Unknown component type: ${component}. Use 'package' or 'service'.`, "error");
211
+ process.exit(1);
212
+ }
213
+ }
214
+ async function addPackage(projectDir, packageName) {
215
+ const packagesDir = path2.join(projectDir, "packages");
216
+ const existingPackages = fs2.existsSync(packagesDir) ? fs2.readdirSync(packagesDir).filter((f) => fs2.statSync(path2.join(packagesDir, f)).isDirectory()) : [];
217
+ const availablePackages = SHARED_PACKAGES.filter((pkg) => !existingPackages.includes(pkg));
218
+ if (availablePackages.length === 0) {
219
+ log("All packages are already installed.", "info");
220
+ return;
221
+ }
222
+ let selectedPackages;
223
+ if (packageName && availablePackages.includes(packageName)) {
224
+ selectedPackages = [packageName];
225
+ } else {
226
+ const { packages } = await inquirer.prompt([
227
+ {
228
+ type: "checkbox",
229
+ name: "packages",
230
+ message: "Select packages to add:",
231
+ choices: availablePackages.map((pkg) => ({
232
+ name: pkg,
233
+ value: pkg
234
+ })),
235
+ validate: (input) => {
236
+ if (input.length === 0) {
237
+ return "Please select at least one package";
238
+ }
239
+ return true;
240
+ }
241
+ }
242
+ ]);
243
+ selectedPackages = packages;
244
+ }
245
+ const spinner = ora("Adding packages...").start();
246
+ try {
247
+ const templateDir = getTemplateDir();
248
+ for (const pkg of selectedPackages) {
249
+ const srcDir = path2.join(templateDir, "packages", pkg);
250
+ const destDir = path2.join(projectDir, "packages", pkg);
251
+ if (fs2.existsSync(srcDir)) {
252
+ fs2.copySync(srcDir, destDir);
253
+ }
254
+ }
255
+ spinner.succeed(`Added ${selectedPackages.length} package(s): ${selectedPackages.join(", ")}`);
256
+ console.log("\nRun " + chalk2.cyan("pnpm install") + " to install dependencies.");
257
+ } catch (error) {
258
+ spinner.fail("Failed to add packages");
259
+ console.error(error);
260
+ process.exit(1);
261
+ }
262
+ }
263
+ async function addService(projectDir, serviceName) {
264
+ const servicesDir = path2.join(projectDir, "services");
265
+ if (!fs2.existsSync(servicesDir)) {
266
+ const { createServices } = await inquirer.prompt([
267
+ {
268
+ type: "confirm",
269
+ name: "createServices",
270
+ message: "Services directory does not exist. Create it?",
271
+ default: true
272
+ }
273
+ ]);
274
+ if (!createServices) {
275
+ return;
276
+ }
277
+ }
278
+ let selectedServices;
279
+ if (serviceName && SERVICES.includes(serviceName)) {
280
+ selectedServices = [serviceName];
281
+ } else {
282
+ const { services } = await inquirer.prompt([
283
+ {
284
+ type: "checkbox",
285
+ name: "services",
286
+ message: "Select services to configure:",
287
+ choices: SERVICES.map((svc) => ({
288
+ name: svc,
289
+ value: svc
290
+ })),
291
+ validate: (input) => {
292
+ if (input.length === 0) {
293
+ return "Please select at least one service";
294
+ }
295
+ return true;
296
+ }
297
+ }
298
+ ]);
299
+ selectedServices = services;
300
+ }
301
+ const spinner = ora("Adding services configuration...").start();
302
+ try {
303
+ const templateDir = getTemplateDir();
304
+ const srcServicesDir = path2.join(templateDir, "services");
305
+ if (fs2.existsSync(srcServicesDir)) {
306
+ fs2.copySync(srcServicesDir, servicesDir, { overwrite: false });
307
+ }
308
+ spinner.succeed(`Services configuration added`);
309
+ console.log("\nAvailable services: " + chalk2.cyan(selectedServices.join(", ")));
310
+ console.log(
311
+ "Start with: " + chalk2.cyan("docker compose -f services/docker-compose.yml --profile <profile> up -d")
312
+ );
313
+ } catch (error) {
314
+ spinner.fail("Failed to add services");
315
+ console.error(error);
316
+ process.exit(1);
317
+ }
318
+ }
319
+
320
+ // src/commands/init.ts
321
+ import path3 from "path";
322
+ import { fileURLToPath as fileURLToPath2 } from "url";
323
+ import chalk3 from "chalk";
324
+ import fs3 from "fs-extra";
325
+ import inquirer2 from "inquirer";
326
+ import ora2 from "ora";
327
+ var __filename2 = fileURLToPath2(import.meta.url);
328
+ var __dirname2 = path3.dirname(__filename2);
329
+ function getTemplateDir2() {
330
+ const possiblePaths = [
331
+ path3.resolve(__dirname2, "..", "..", "templates", "default"),
332
+ path3.resolve(__dirname2, "..", "templates", "default")
333
+ ];
334
+ for (const templatePath of possiblePaths) {
335
+ if (fs3.existsSync(templatePath)) {
336
+ return templatePath;
337
+ }
338
+ }
339
+ throw new Error("Template directory not found. Please reinstall the package.");
340
+ }
341
+ async function init(projectName, options) {
342
+ console.log(chalk3.bold("\n\u{1F680} Create Nexu Monorepo\n"));
343
+ const useCurrentDir = projectName === ".";
344
+ let projectDir;
345
+ if (useCurrentDir) {
346
+ projectDir = process.cwd();
347
+ projectName = path3.basename(projectDir);
348
+ const files = fs3.readdirSync(projectDir).filter((f) => !f.startsWith("."));
349
+ if (files.length > 0) {
350
+ const { proceed } = await inquirer2.prompt([
351
+ {
352
+ type: "confirm",
353
+ name: "proceed",
354
+ message: `Current directory is not empty. Continue anyway?`,
355
+ default: false
356
+ }
357
+ ]);
358
+ if (!proceed) {
359
+ log("Aborted.", "warn");
360
+ process.exit(0);
361
+ }
362
+ }
363
+ } else {
364
+ if (!projectName) {
365
+ const answers = await inquirer2.prompt([
366
+ {
367
+ type: "input",
368
+ name: "projectName",
369
+ message: "Project name:",
370
+ default: "my-nexu-app",
371
+ validate: (input) => {
372
+ if (!/^[a-z0-9-]+$/.test(input)) {
373
+ return "Project name can only contain lowercase letters, numbers, and hyphens";
374
+ }
375
+ return true;
376
+ }
377
+ }
378
+ ]);
379
+ projectName = answers.projectName;
380
+ }
381
+ projectDir = path3.resolve(process.cwd(), projectName);
382
+ if (fs3.existsSync(projectDir)) {
383
+ const { overwrite } = await inquirer2.prompt([
384
+ {
385
+ type: "confirm",
386
+ name: "overwrite",
387
+ message: `Directory ${projectName} already exists. Overwrite?`,
388
+ default: false
389
+ }
390
+ ]);
391
+ if (!overwrite) {
392
+ log("Aborted.", "warn");
393
+ process.exit(0);
394
+ }
395
+ fs3.removeSync(projectDir);
396
+ }
397
+ }
398
+ let packageManager = options.packageManager;
399
+ if (!packageManager) {
400
+ const { pm } = await inquirer2.prompt([
401
+ {
402
+ type: "list",
403
+ name: "pm",
404
+ message: "Select package manager:",
405
+ choices: [
406
+ { name: "pnpm (recommended)", value: "pnpm" },
407
+ { name: "npm", value: "npm" },
408
+ { name: "yarn", value: "yarn" }
409
+ ],
410
+ default: "pnpm"
411
+ }
412
+ ]);
413
+ packageManager = pm;
414
+ }
415
+ const { selectedPackages } = await inquirer2.prompt([
416
+ {
417
+ type: "checkbox",
418
+ name: "selectedPackages",
419
+ message: "Select packages to include:",
420
+ choices: SHARED_PACKAGES.map((pkg) => ({
421
+ name: pkg,
422
+ value: pkg,
423
+ checked: true
424
+ }))
425
+ }
426
+ ]);
427
+ const { features } = await inquirer2.prompt([
428
+ {
429
+ type: "checkbox",
430
+ name: "features",
431
+ message: "Select additional features:",
432
+ choices: [
433
+ { name: "Docker services (PostgreSQL, Redis, etc.)", value: "services", checked: true },
434
+ { name: "GitHub Actions workflows", value: "workflows", checked: true },
435
+ { name: "Changesets (versioning)", value: "changesets", checked: true },
436
+ { name: "Husky (git hooks)", value: "husky", checked: true },
437
+ { name: "VSCode settings", value: "vscode", checked: true }
438
+ ]
439
+ }
440
+ ]);
441
+ const spinner = ora2("Copying template...").start();
442
+ try {
443
+ const templateDir = getTemplateDir2();
444
+ fs3.copySync(templateDir, projectDir);
445
+ const dotfilesToRename = [
446
+ { src: path3.join(projectDir, "gitignore"), dest: path3.join(projectDir, ".gitignore") },
447
+ {
448
+ src: path3.join(projectDir, "lintstagedrc.cjs"),
449
+ dest: path3.join(projectDir, ".lintstagedrc.cjs")
450
+ },
451
+ {
452
+ src: path3.join(projectDir, "apps", "gitkeep"),
453
+ dest: path3.join(projectDir, "apps", ".gitkeep")
454
+ },
455
+ {
456
+ src: path3.join(projectDir, "services", "postgres", "init", "gitkeep"),
457
+ dest: path3.join(projectDir, "services", "postgres", "init", ".gitkeep")
458
+ }
459
+ ];
460
+ for (const { src, dest } of dotfilesToRename) {
461
+ if (fs3.existsSync(src)) {
462
+ fs3.renameSync(src, dest);
463
+ }
464
+ }
465
+ spinner.succeed("Template copied");
466
+ } catch (error) {
467
+ spinner.fail("Failed to copy template");
468
+ console.error(error);
469
+ process.exit(1);
470
+ }
471
+ const packageJsonPath = path3.join(projectDir, "package.json");
472
+ const packageJson2 = fs3.readJsonSync(packageJsonPath);
473
+ packageJson2.name = projectName;
474
+ const pmField = getPackageManagerField(packageManager);
475
+ if (pmField) {
476
+ packageJson2.packageManager = pmField;
477
+ } else {
478
+ delete packageJson2.packageManager;
479
+ }
480
+ if (!features.includes("changesets")) {
481
+ delete packageJson2.scripts["changeset"];
482
+ delete packageJson2.scripts["version-packages"];
483
+ delete packageJson2.scripts["release"];
484
+ delete packageJson2.devDependencies["@changesets/cli"];
485
+ }
486
+ if (!features.includes("husky")) {
487
+ delete packageJson2.scripts["prepare"];
488
+ delete packageJson2.devDependencies["husky"];
489
+ delete packageJson2.devDependencies["lint-staged"];
490
+ delete packageJson2["lint-staged"];
491
+ }
492
+ fs3.writeJsonSync(packageJsonPath, packageJson2, { spaces: 2 });
493
+ const packagesToRemove = SHARED_PACKAGES.filter((pkg) => !selectedPackages.includes(pkg));
494
+ if (packagesToRemove.length > 0) {
495
+ const removeSpinner = ora2("Removing unselected packages...").start();
496
+ for (const pkg of packagesToRemove) {
497
+ const pkgDir = path3.join(projectDir, "packages", pkg);
498
+ if (fs3.existsSync(pkgDir)) {
499
+ fs3.removeSync(pkgDir);
500
+ }
501
+ }
502
+ removeSpinner.succeed("Removed unselected packages");
503
+ }
504
+ if (!features.includes("services")) {
505
+ fs3.removeSync(path3.join(projectDir, "services"));
506
+ }
507
+ if (!features.includes("workflows")) {
508
+ fs3.removeSync(path3.join(projectDir, ".github", "workflows"));
509
+ fs3.removeSync(path3.join(projectDir, ".github", "actions"));
510
+ }
511
+ if (!features.includes("changesets")) {
512
+ fs3.removeSync(path3.join(projectDir, ".changeset"));
513
+ }
514
+ if (!features.includes("husky")) {
515
+ fs3.removeSync(path3.join(projectDir, ".husky"));
516
+ }
517
+ if (!features.includes("vscode")) {
518
+ fs3.removeSync(path3.join(projectDir, ".vscode"));
519
+ }
520
+ if (packageManager === "yarn") {
521
+ fs3.removeSync(path3.join(projectDir, "pnpm-workspace.yaml"));
522
+ fs3.removeSync(path3.join(projectDir, ".npmrc"));
523
+ const updatedPkg = fs3.readJsonSync(packageJsonPath);
524
+ updatedPkg.private = true;
525
+ updatedPkg.workspaces = ["apps/*", "packages/*"];
526
+ fs3.writeJsonSync(packageJsonPath, updatedPkg, { spaces: 2 });
527
+ } else if (packageManager === "npm") {
528
+ fs3.removeSync(path3.join(projectDir, "pnpm-workspace.yaml"));
529
+ fs3.removeSync(path3.join(projectDir, ".npmrc"));
530
+ const updatedPkg = fs3.readJsonSync(packageJsonPath);
531
+ updatedPkg.private = true;
532
+ updatedPkg.workspaces = ["apps/*", "packages/*"];
533
+ fs3.writeJsonSync(packageJsonPath, updatedPkg, { spaces: 2 });
534
+ }
535
+ if (!options.skipGit) {
536
+ const gitSpinner = ora2("Initializing git repository...").start();
537
+ try {
538
+ exec("git init", projectDir);
539
+ exec("git add .", projectDir);
540
+ exec('git commit -m "Initial commit from nexu-app"', projectDir);
541
+ gitSpinner.succeed("Git repository initialized");
542
+ } catch {
543
+ gitSpinner.warn("Failed to initialize git repository");
544
+ }
545
+ }
546
+ const runCmd = getRunCommand(packageManager);
547
+ if (!options.skipInstall) {
548
+ console.log(chalk3.blue(`
549
+ \u{1F4E6} Installing dependencies with ${packageManager}...
550
+ `));
551
+ try {
552
+ execInherit(getInstallCommand(packageManager), projectDir);
553
+ console.log(chalk3.green("\n\u2713 Dependencies installed"));
554
+ } catch {
555
+ console.log(
556
+ chalk3.yellow(
557
+ `
558
+ ! Failed to install dependencies. Run "${getInstallCommand(packageManager)}" manually.`
559
+ )
560
+ );
561
+ }
562
+ }
563
+ console.log("\n" + chalk3.green.bold("\u2728 Project created successfully!\n"));
564
+ console.log("Next steps:\n");
565
+ if (!useCurrentDir) {
566
+ console.log(chalk3.cyan(` cd ${projectName}`));
567
+ }
568
+ if (options.skipInstall) {
569
+ console.log(chalk3.cyan(` ${getInstallCommand(packageManager)}`));
570
+ }
571
+ console.log(chalk3.cyan(` ${runCmd} dev`));
572
+ console.log("\nTo create an app:");
573
+ console.log(chalk3.cyan(` ${runCmd} generate:app <name> <port>`));
574
+ console.log("\nTo update with latest features:");
575
+ console.log(chalk3.cyan(" npx nexu-app update"));
576
+ console.log("");
577
+ }
578
+
579
+ // src/commands/update.ts
580
+ import path4 from "path";
581
+ import { fileURLToPath as fileURLToPath3 } from "url";
582
+ import chalk4 from "chalk";
583
+ import { diffLines } from "diff";
584
+ import fs4 from "fs-extra";
585
+ import inquirer3 from "inquirer";
586
+ import ora3 from "ora";
587
+ var __filename3 = fileURLToPath3(import.meta.url);
588
+ var __dirname3 = path4.dirname(__filename3);
589
+ function getTemplateDir3() {
590
+ const possiblePaths = [
591
+ path4.resolve(__dirname3, "..", "..", "templates", "default"),
592
+ path4.resolve(__dirname3, "..", "templates", "default")
593
+ ];
594
+ for (const templatePath of possiblePaths) {
595
+ if (fs4.existsSync(templatePath)) {
596
+ return templatePath;
597
+ }
598
+ }
599
+ throw new Error("Template directory not found. Please reinstall the package.");
600
+ }
601
+ function compareFiles(srcPath, destPath) {
602
+ if (!fs4.existsSync(destPath)) return false;
603
+ if (!fs4.existsSync(srcPath)) return false;
604
+ const srcStat = fs4.statSync(srcPath);
605
+ const destStat = fs4.statSync(destPath);
606
+ if (srcStat.isDirectory() || destStat.isDirectory()) return false;
607
+ const srcContent = fs4.readFileSync(srcPath, "utf-8");
608
+ const destContent = fs4.readFileSync(destPath, "utf-8");
609
+ return srcContent === destContent;
610
+ }
611
+ function collectFileChanges(srcDir, destDir, category, basePath = "", checkDeleted = true) {
612
+ const changes = [];
613
+ if (fs4.existsSync(srcDir)) {
614
+ const entries = fs4.readdirSync(srcDir, { withFileTypes: true });
615
+ for (const entry of entries) {
616
+ const srcPath = path4.join(srcDir, entry.name);
617
+ const destPath = path4.join(destDir, entry.name);
618
+ const relativePath = basePath ? path4.join(basePath, entry.name) : entry.name;
619
+ if (entry.isDirectory()) {
620
+ changes.push(
621
+ ...collectFileChanges(srcPath, destPath, category, relativePath, checkDeleted)
622
+ );
623
+ } else {
624
+ if (!fs4.existsSync(destPath)) {
625
+ changes.push({ type: "add", relativePath, srcPath, destPath, category });
626
+ } else if (!compareFiles(srcPath, destPath)) {
627
+ changes.push({ type: "modify", relativePath, srcPath, destPath, category });
628
+ }
629
+ }
630
+ }
631
+ }
632
+ if (checkDeleted && fs4.existsSync(destDir)) {
633
+ const destEntries = fs4.readdirSync(destDir, { withFileTypes: true });
634
+ for (const entry of destEntries) {
635
+ const srcPath = path4.join(srcDir, entry.name);
636
+ const destPath = path4.join(destDir, entry.name);
637
+ const relativePath = basePath ? path4.join(basePath, entry.name) : entry.name;
638
+ if (shouldExcludeFromDeletion(entry.name, relativePath)) {
639
+ continue;
640
+ }
641
+ if (fs4.existsSync(srcPath)) continue;
642
+ if (entry.isDirectory()) {
643
+ changes.push(...collectDeletedFiles(destPath, category, relativePath));
644
+ } else {
645
+ changes.push({ type: "delete", relativePath, srcPath, destPath, category });
646
+ }
647
+ }
648
+ }
649
+ return changes;
650
+ }
651
+ var EXCLUDE_FROM_DELETION = [
652
+ "node_modules",
653
+ ".git",
654
+ ".turbo",
655
+ "dist",
656
+ "build",
657
+ ".next",
658
+ "coverage",
659
+ ".husky/_",
660
+ // Husky internal files
661
+ ".DS_Store"
662
+ ];
663
+ function shouldExcludeFromDeletion(name, relativePath) {
664
+ if (EXCLUDE_FROM_DELETION.includes(name)) {
665
+ return true;
666
+ }
667
+ for (const pattern of EXCLUDE_FROM_DELETION) {
668
+ if (relativePath.startsWith(pattern + "/") || relativePath.startsWith(pattern + path4.sep)) {
669
+ return true;
670
+ }
671
+ if (relativePath === pattern) {
672
+ return true;
673
+ }
674
+ }
675
+ return false;
676
+ }
677
+ function collectDeletedFiles(destDir, category, basePath) {
678
+ const changes = [];
679
+ if (!fs4.existsSync(destDir)) return changes;
680
+ const entries = fs4.readdirSync(destDir, { withFileTypes: true });
681
+ for (const entry of entries) {
682
+ const destPath = path4.join(destDir, entry.name);
683
+ const relativePath = path4.join(basePath, entry.name);
684
+ if (shouldExcludeFromDeletion(entry.name, relativePath)) {
685
+ continue;
686
+ }
687
+ if (entry.isDirectory()) {
688
+ changes.push(...collectDeletedFiles(destPath, category, relativePath));
689
+ } else {
690
+ changes.push({ type: "delete", relativePath, srcPath: "", destPath, category });
691
+ }
692
+ }
693
+ return changes;
694
+ }
695
+ function showFileDiff(srcPath, destPath) {
696
+ if (!fs4.existsSync(destPath)) {
697
+ console.log(chalk4.green(" (new file)"));
698
+ return;
699
+ }
700
+ const srcContent = fs4.readFileSync(srcPath, "utf-8");
701
+ const destContent = fs4.readFileSync(destPath, "utf-8");
702
+ const differences = diffLines(destContent, srcContent);
703
+ let hasChanges2 = false;
704
+ for (const part of differences) {
705
+ if (part.added || part.removed) {
706
+ hasChanges2 = true;
707
+ const lines = part.value.split("\n").filter((l) => l.length > 0);
708
+ for (const line of lines.slice(0, 5)) {
709
+ if (part.added) {
710
+ console.log(chalk4.green(` + ${line.substring(0, 80)}`));
711
+ } else {
712
+ console.log(chalk4.red(` - ${line.substring(0, 80)}`));
713
+ }
714
+ }
715
+ if (lines.length > 5) {
716
+ console.log(chalk4.gray(` ... and ${lines.length - 5} more lines`));
717
+ }
718
+ }
719
+ }
720
+ if (!hasChanges2) {
721
+ console.log(chalk4.gray(" (no visible changes)"));
722
+ }
723
+ }
724
+ function collectDependencyChanges(templatePkgPath, projectPkgPath) {
725
+ if (!fs4.existsSync(templatePkgPath) || !fs4.existsSync(projectPkgPath)) {
726
+ return null;
727
+ }
728
+ const templatePkg = fs4.readJsonSync(templatePkgPath);
729
+ const projectPkg = fs4.readJsonSync(projectPkgPath);
730
+ const changes = {
731
+ type: "package.json",
732
+ changes: {
733
+ added: { dependencies: {}, devDependencies: {}, scripts: {} },
734
+ updated: { dependencies: {}, devDependencies: {}, scripts: {} }
735
+ }
736
+ };
737
+ for (const depType of ["dependencies", "devDependencies"]) {
738
+ if (templatePkg[depType]) {
739
+ for (const [pkg, version] of Object.entries(templatePkg[depType])) {
740
+ if (!projectPkg[depType]?.[pkg]) {
741
+ changes.changes.added[depType][pkg] = version;
742
+ } else if (projectPkg[depType][pkg] !== version) {
743
+ changes.changes.updated[depType][pkg] = {
744
+ from: projectPkg[depType][pkg],
745
+ to: version
746
+ };
747
+ }
748
+ }
749
+ }
750
+ }
751
+ if (templatePkg.scripts) {
752
+ for (const [script, cmd] of Object.entries(templatePkg.scripts)) {
753
+ if (!projectPkg.scripts?.[script]) {
754
+ changes.changes.added.scripts[script] = cmd;
755
+ } else if (projectPkg.scripts[script] !== cmd) {
756
+ changes.changes.updated.scripts[script] = {
757
+ from: projectPkg.scripts[script],
758
+ to: cmd
759
+ };
760
+ }
761
+ }
762
+ }
763
+ return changes;
764
+ }
765
+ function displayDependencyChanges(changes) {
766
+ const { added, updated } = changes.changes;
767
+ if (Object.keys(added.dependencies).length > 0) {
768
+ console.log(chalk4.cyan("\n New dependencies:"));
769
+ for (const [pkg, version] of Object.entries(added.dependencies)) {
770
+ console.log(chalk4.green(` + ${pkg}: ${version}`));
771
+ }
772
+ }
773
+ if (Object.keys(added.devDependencies).length > 0) {
774
+ console.log(chalk4.cyan("\n New devDependencies:"));
775
+ for (const [pkg, version] of Object.entries(added.devDependencies)) {
776
+ console.log(chalk4.green(` + ${pkg}: ${version}`));
777
+ }
778
+ }
779
+ if (Object.keys(updated.dependencies).length > 0) {
780
+ console.log(chalk4.cyan("\n Updated dependencies:"));
781
+ for (const [pkg, { from, to }] of Object.entries(updated.dependencies)) {
782
+ console.log(chalk4.yellow(` ~ ${pkg}: ${from} \u2192 ${to}`));
783
+ }
784
+ }
785
+ if (Object.keys(updated.devDependencies).length > 0) {
786
+ console.log(chalk4.cyan("\n Updated devDependencies:"));
787
+ for (const [pkg, { from, to }] of Object.entries(updated.devDependencies)) {
788
+ console.log(chalk4.yellow(` ~ ${pkg}: ${from} \u2192 ${to}`));
789
+ }
790
+ }
791
+ if (Object.keys(added.scripts).length > 0) {
792
+ console.log(chalk4.cyan("\n New scripts:"));
793
+ for (const [script, cmd] of Object.entries(added.scripts)) {
794
+ console.log(chalk4.green(` + ${script}: ${String(cmd).substring(0, 50)}...`));
795
+ }
796
+ }
797
+ if (Object.keys(updated.scripts).length > 0) {
798
+ console.log(chalk4.cyan("\n Updated scripts:"));
799
+ for (const [script, { from, to }] of Object.entries(updated.scripts)) {
800
+ console.log(chalk4.yellow(` ~ ${script}:`));
801
+ console.log(chalk4.red(` - ${from.substring(0, 60)}${from.length > 60 ? "..." : ""}`));
802
+ console.log(chalk4.green(` + ${to.substring(0, 60)}${to.length > 60 ? "..." : ""}`));
803
+ }
804
+ }
805
+ }
806
+ function hasChanges(changes) {
807
+ const { added, updated } = changes.changes;
808
+ return Object.keys(added.dependencies).length > 0 || Object.keys(added.devDependencies).length > 0 || Object.keys(added.scripts).length > 0 || Object.keys(updated.dependencies).length > 0 || Object.keys(updated.devDependencies).length > 0 || Object.keys(updated.scripts).length > 0;
809
+ }
810
+ async function update(options) {
811
+ console.log(chalk4.bold("\n\u{1F504} Update Nexu Project\n"));
812
+ const projectDir = process.cwd();
813
+ if (!isNexuProject(projectDir)) {
814
+ log(
815
+ "This does not appear to be a Nexu project. Run this command from the project root.",
816
+ "error"
817
+ );
818
+ process.exit(1);
819
+ }
820
+ const templateDir = getTemplateDir3();
821
+ const updateAll = options.all || !options.packages && !options.config && !options.workflows && !options.services && !options.scripts && !options.dependencies;
822
+ const updateTargets = {
823
+ packages: updateAll || options.packages,
824
+ config: updateAll || options.config,
825
+ workflows: updateAll || options.workflows,
826
+ services: updateAll || options.services,
827
+ scripts: updateAll || options.scripts,
828
+ dependencies: updateAll || options.dependencies
829
+ };
830
+ const spinner = ora3("Analyzing changes...").start();
831
+ const allFileChanges = [];
832
+ let dependencyChanges = null;
833
+ if (updateTargets.config) {
834
+ for (const file of TEMPLATE_DIRS.config) {
835
+ const srcFile = path4.join(templateDir, file);
836
+ const destFile = path4.join(projectDir, file);
837
+ if (fs4.existsSync(srcFile)) {
838
+ if (!fs4.existsSync(destFile)) {
839
+ allFileChanges.push({
840
+ type: "add",
841
+ relativePath: file,
842
+ srcPath: srcFile,
843
+ destPath: destFile,
844
+ category: "config"
845
+ });
846
+ } else if (!compareFiles(srcFile, destFile)) {
847
+ allFileChanges.push({
848
+ type: "modify",
849
+ relativePath: file,
850
+ srcPath: srcFile,
851
+ destPath: destFile,
852
+ category: "config"
853
+ });
854
+ }
855
+ }
856
+ }
857
+ allFileChanges.push(
858
+ ...collectFileChanges(
859
+ path4.join(templateDir, ".husky"),
860
+ path4.join(projectDir, ".husky"),
861
+ "config",
862
+ ".husky"
863
+ )
864
+ );
865
+ allFileChanges.push(
866
+ ...collectFileChanges(
867
+ path4.join(templateDir, ".vscode"),
868
+ path4.join(projectDir, ".vscode"),
869
+ "config",
870
+ ".vscode"
871
+ )
872
+ );
873
+ }
874
+ if (updateTargets.workflows) {
875
+ allFileChanges.push(
876
+ ...collectFileChanges(
877
+ path4.join(templateDir, ".github"),
878
+ path4.join(projectDir, ".github"),
879
+ "workflows",
880
+ ".github"
881
+ )
882
+ );
883
+ }
884
+ if (updateTargets.services) {
885
+ allFileChanges.push(
886
+ ...collectFileChanges(
887
+ path4.join(templateDir, "services"),
888
+ path4.join(projectDir, "services"),
889
+ "services",
890
+ "services"
891
+ )
892
+ );
893
+ allFileChanges.push(
894
+ ...collectFileChanges(
895
+ path4.join(templateDir, "docker"),
896
+ path4.join(projectDir, "docker"),
897
+ "services",
898
+ "docker"
899
+ )
900
+ );
901
+ }
902
+ if (updateTargets.scripts) {
903
+ allFileChanges.push(
904
+ ...collectFileChanges(
905
+ path4.join(templateDir, "scripts"),
906
+ path4.join(projectDir, "scripts"),
907
+ "scripts",
908
+ "scripts"
909
+ )
910
+ );
911
+ }
912
+ if (updateTargets.packages) {
913
+ const existingPackages = fs4.existsSync(path4.join(projectDir, "packages")) ? fs4.readdirSync(path4.join(projectDir, "packages")).filter((f) => fs4.statSync(path4.join(projectDir, "packages", f)).isDirectory()) : [];
914
+ for (const pkg of SHARED_PACKAGES) {
915
+ if (existingPackages.includes(pkg)) {
916
+ allFileChanges.push(
917
+ ...collectFileChanges(
918
+ path4.join(templateDir, "packages", pkg),
919
+ path4.join(projectDir, "packages", pkg),
920
+ "packages",
921
+ path4.join("packages", pkg)
922
+ )
923
+ );
924
+ }
925
+ }
926
+ }
927
+ if (updateTargets.dependencies) {
928
+ dependencyChanges = collectDependencyChanges(
929
+ path4.join(templateDir, "package.json"),
930
+ path4.join(projectDir, "package.json")
931
+ );
932
+ }
933
+ spinner.stop();
934
+ const addedFiles = allFileChanges.filter((c) => c.type === "add");
935
+ const modifiedFiles = allFileChanges.filter((c) => c.type === "modify");
936
+ const deletedFiles = allFileChanges.filter((c) => c.type === "delete");
937
+ if (addedFiles.length === 0 && modifiedFiles.length === 0 && deletedFiles.length === 0 && (!dependencyChanges || !hasChanges(dependencyChanges))) {
938
+ console.log(chalk4.green("\u2713 Your project is up to date! No changes needed.\n"));
939
+ return;
940
+ }
941
+ console.log(chalk4.bold("\u{1F4CB} Changes to apply:\n"));
942
+ const categories = [...new Set(allFileChanges.map((c) => c.category))];
943
+ for (const category of categories) {
944
+ const categoryChanges = allFileChanges.filter((c) => c.category === category);
945
+ if (categoryChanges.length === 0) continue;
946
+ const categoryNames = {
947
+ config: "Configuration files",
948
+ workflows: "GitHub workflows",
949
+ services: "Docker services",
950
+ scripts: "Scripts",
951
+ packages: "Shared packages"
952
+ };
953
+ console.log(chalk4.cyan.bold(`
954
+ ${categoryNames[category] || category}:`));
955
+ const added = categoryChanges.filter((c) => c.type === "add");
956
+ const modified = categoryChanges.filter((c) => c.type === "modify");
957
+ const deleted = categoryChanges.filter((c) => c.type === "delete");
958
+ if (added.length > 0) {
959
+ console.log(chalk4.green(` New files (${added.length}):`));
960
+ for (const change of added.slice(0, 10)) {
961
+ console.log(chalk4.green(` + ${change.relativePath}`));
962
+ }
963
+ if (added.length > 10) {
964
+ console.log(chalk4.gray(` ... and ${added.length - 10} more files`));
965
+ }
966
+ }
967
+ if (modified.length > 0) {
968
+ console.log(chalk4.yellow(` Modified files (${modified.length}):`));
969
+ for (const change of modified.slice(0, 10)) {
970
+ console.log(chalk4.yellow(` ~ ${change.relativePath}`));
971
+ }
972
+ if (modified.length > 10) {
973
+ console.log(chalk4.gray(` ... and ${modified.length - 10} more files`));
974
+ }
975
+ }
976
+ if (deleted.length > 0) {
977
+ console.log(chalk4.red(` Deleted files (${deleted.length}):`));
978
+ for (const change of deleted.slice(0, 10)) {
979
+ console.log(chalk4.red(` - ${change.relativePath}`));
980
+ }
981
+ if (deleted.length > 10) {
982
+ console.log(chalk4.gray(` ... and ${deleted.length - 10} more files`));
983
+ }
984
+ }
985
+ }
986
+ if (dependencyChanges && hasChanges(dependencyChanges)) {
987
+ console.log(chalk4.cyan.bold("\nPackage.json changes:"));
988
+ displayDependencyChanges(dependencyChanges);
989
+ }
990
+ console.log("");
991
+ if (options.preview) {
992
+ const { showDiff } = await inquirer3.prompt([
993
+ {
994
+ type: "confirm",
995
+ name: "showDiff",
996
+ message: "Show detailed file differences?",
997
+ default: false
998
+ }
999
+ ]);
1000
+ if (showDiff) {
1001
+ for (const change of modifiedFiles) {
1002
+ console.log(chalk4.bold(`
1003
+ ${change.relativePath}:`));
1004
+ showFileDiff(change.srcPath, change.destPath);
1005
+ }
1006
+ }
1007
+ }
1008
+ if (options.dryRun) {
1009
+ console.log(chalk4.blue("\ni Dry run mode - no changes were made.\n"));
1010
+ return;
1011
+ }
1012
+ const { selectedCategories } = await inquirer3.prompt([
1013
+ {
1014
+ type: "checkbox",
1015
+ name: "selectedCategories",
1016
+ message: "Select which changes to apply:",
1017
+ choices: [
1018
+ ...categories.map((cat) => {
1019
+ const count = allFileChanges.filter((c) => c.category === cat).length;
1020
+ const categoryNames = {
1021
+ config: "Configuration files",
1022
+ workflows: "GitHub workflows",
1023
+ services: "Docker services",
1024
+ scripts: "Scripts",
1025
+ packages: "Shared packages"
1026
+ };
1027
+ return {
1028
+ name: `${categoryNames[cat] || cat} (${count} files)`,
1029
+ value: cat,
1030
+ checked: true
1031
+ };
1032
+ }),
1033
+ ...dependencyChanges && hasChanges(dependencyChanges) ? [
1034
+ {
1035
+ name: "Package.json dependencies",
1036
+ value: "dependencies",
1037
+ checked: true
1038
+ }
1039
+ ] : []
1040
+ ]
1041
+ }
1042
+ ]);
1043
+ if (selectedCategories.length === 0) {
1044
+ log("No changes selected. Update cancelled.", "warn");
1045
+ return;
1046
+ }
1047
+ const { confirm } = await inquirer3.prompt([
1048
+ {
1049
+ type: "confirm",
1050
+ name: "confirm",
1051
+ message: `Apply ${selectedCategories.length} category(ies) of changes?`,
1052
+ default: true
1053
+ }
1054
+ ]);
1055
+ if (!confirm) {
1056
+ log("Update cancelled.", "warn");
1057
+ return;
1058
+ }
1059
+ const applySpinner = ora3("Applying changes...").start();
1060
+ let appliedFiles = 0;
1061
+ let deletedFilesCount = 0;
1062
+ for (const change of allFileChanges) {
1063
+ if (!selectedCategories.includes(change.category)) continue;
1064
+ if (change.type === "delete") {
1065
+ if (fs4.existsSync(change.destPath)) {
1066
+ fs4.removeSync(change.destPath);
1067
+ deletedFilesCount++;
1068
+ }
1069
+ } else {
1070
+ fs4.ensureDirSync(path4.dirname(change.destPath));
1071
+ fs4.copySync(change.srcPath, change.destPath, { overwrite: true });
1072
+ appliedFiles++;
1073
+ }
1074
+ }
1075
+ if (deletedFilesCount > 0) {
1076
+ cleanEmptyDirectories(projectDir);
1077
+ }
1078
+ const dotfilesToRename = [
1079
+ { src: path4.join(projectDir, "gitignore"), dest: path4.join(projectDir, ".gitignore") },
1080
+ {
1081
+ src: path4.join(projectDir, "lintstagedrc.cjs"),
1082
+ dest: path4.join(projectDir, ".lintstagedrc.cjs")
1083
+ },
1084
+ {
1085
+ src: path4.join(projectDir, "apps", "gitkeep"),
1086
+ dest: path4.join(projectDir, "apps", ".gitkeep")
1087
+ },
1088
+ {
1089
+ src: path4.join(projectDir, "services", "postgres", "init", "gitkeep"),
1090
+ dest: path4.join(projectDir, "services", "postgres", "init", ".gitkeep")
1091
+ }
1092
+ ];
1093
+ for (const { src, dest } of dotfilesToRename) {
1094
+ if (fs4.existsSync(src)) {
1095
+ fs4.renameSync(src, dest);
1096
+ }
1097
+ }
1098
+ if (selectedCategories.includes("dependencies") && dependencyChanges) {
1099
+ const templatePkgPath = path4.join(templateDir, "package.json");
1100
+ const projectPkgPath = path4.join(projectDir, "package.json");
1101
+ const templatePkg = fs4.readJsonSync(templatePkgPath);
1102
+ const projectPkg = fs4.readJsonSync(projectPkgPath);
1103
+ if (templatePkg.devDependencies) {
1104
+ projectPkg.devDependencies = {
1105
+ ...projectPkg.devDependencies,
1106
+ ...templatePkg.devDependencies
1107
+ };
1108
+ }
1109
+ if (templatePkg.dependencies) {
1110
+ projectPkg.dependencies = {
1111
+ ...projectPkg.dependencies,
1112
+ ...templatePkg.dependencies
1113
+ };
1114
+ }
1115
+ if (templatePkg.scripts) {
1116
+ projectPkg.scripts = {
1117
+ ...projectPkg.scripts,
1118
+ ...templatePkg.scripts
1119
+ };
1120
+ }
1121
+ if (projectPkg.dependencies) {
1122
+ projectPkg.dependencies = sortObject(projectPkg.dependencies);
1123
+ }
1124
+ if (projectPkg.devDependencies) {
1125
+ projectPkg.devDependencies = sortObject(projectPkg.devDependencies);
1126
+ }
1127
+ fs4.writeJsonSync(projectPkgPath, projectPkg, { spaces: 2 });
1128
+ }
1129
+ const summary = [];
1130
+ if (appliedFiles > 0) summary.push(`${appliedFiles} files updated`);
1131
+ if (deletedFilesCount > 0) summary.push(`${deletedFilesCount} files deleted`);
1132
+ applySpinner.succeed(summary.join(", ") || "No file changes applied");
1133
+ if (selectedCategories.includes("dependencies") && dependencyChanges && hasChanges(dependencyChanges)) {
1134
+ const pm = detectPackageManager(projectDir);
1135
+ const installCmd = getInstallCommand(pm);
1136
+ console.log(chalk4.blue(`
1137
+ \u{1F4E6} Installing dependencies with ${pm}...
1138
+ `));
1139
+ try {
1140
+ execInherit(installCmd, projectDir);
1141
+ console.log(chalk4.green("\n\u2713 Dependencies installed"));
1142
+ } catch {
1143
+ console.log(
1144
+ chalk4.yellow(`
1145
+ ! Failed to install dependencies. Run "${installCmd}" manually.`)
1146
+ );
1147
+ }
1148
+ }
1149
+ console.log("\n" + chalk4.green.bold("\u2728 Update complete!\n"));
1150
+ }
1151
+ function cleanEmptyDirectories(dir) {
1152
+ if (!fs4.existsSync(dir)) return;
1153
+ const entries = fs4.readdirSync(dir, { withFileTypes: true });
1154
+ for (const entry of entries) {
1155
+ if (entry.isDirectory()) {
1156
+ const fullPath = path4.join(dir, entry.name);
1157
+ cleanEmptyDirectories(fullPath);
1158
+ const remaining = fs4.readdirSync(fullPath);
1159
+ if (remaining.length === 0) {
1160
+ fs4.rmdirSync(fullPath);
1161
+ }
1162
+ }
1163
+ }
1164
+ }
1165
+ function sortObject(obj) {
1166
+ return Object.keys(obj).sort().reduce(
1167
+ (acc, key) => {
1168
+ acc[key] = obj[key];
1169
+ return acc;
1170
+ },
1171
+ {}
1172
+ );
1173
+ }
1174
+
1175
+ // src/index.ts
1176
+ var __dirname4 = dirname(fileURLToPath4(import.meta.url));
1177
+ var packageJson = JSON.parse(readFileSync(join(__dirname4, "../package.json"), "utf-8"));
1178
+ var program = new Command();
1179
+ program.name("nexu-app").description("CLI to create and update Nexu monorepo projects").version(packageJson.version);
1180
+ program.argument("[project-name]", "Name of the project to create").option("-t, --template <template>", "Template to use (default, minimal)", "default").option("--skip-install", "Skip dependency installation").option("--skip-git", "Skip git initialization").action((projectName, options) => {
1181
+ if (!projectName && process.argv.length <= 2) {
1182
+ program.help();
1183
+ } else {
1184
+ return init(projectName, options);
1185
+ }
1186
+ });
1187
+ program.command("init [project-name]").description("Initialize a new Nexu monorepo project").option("-t, --template <template>", "Template to use (default, minimal)", "default").option("--skip-install", "Skip dependency installation").option("--skip-git", "Skip git initialization").action((projectName, options) => {
1188
+ return init(projectName, options);
1189
+ });
1190
+ program.command("update").description("Update an existing Nexu project with latest features").option("-p, --packages", "Update only shared packages").option("-c, --config", "Update only configuration files").option("-w, --workflows", "Update only GitHub workflows").option("-s, --services", "Update only Docker services").option("--scripts", "Update only scripts").option("-d, --dependencies", "Update only package.json dependencies").option("--all", "Update everything (default)").option("--dry-run", "Show what would be updated without making changes").option("--preview", "Show detailed file differences before applying").action(update);
1191
+ program.command("add <component>").description("Add a component to your project (package, service)").option("-n, --name <name>", "Name for the component").action(add);
1192
+ program.parse();