create-next-pro-cli 0.1.26 → 0.1.28

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 (107) hide show
  1. package/README.md +281 -291
  2. package/create-next-pro-completion.sh +8 -38
  3. package/create-next-pro-completion.zsh +13 -0
  4. package/dist/bin.bun.js +2265 -794
  5. package/dist/bin.node.js +2425 -805
  6. package/dist/bin.node.js.map +1 -1
  7. package/package.json +50 -27
  8. package/templates/Projects/default/.env.example +17 -0
  9. package/templates/Projects/default/.github/workflows/quality.yml +24 -0
  10. package/templates/Projects/default/.gitignore.template +47 -0
  11. package/templates/Projects/default/.prettierignore +3 -0
  12. package/templates/Projects/default/README.md +52 -108
  13. package/templates/Projects/default/bun.lock +1152 -0
  14. package/templates/Projects/default/eslint.config.mjs +27 -11
  15. package/templates/Projects/default/messages/en/_global_ui.json +23 -10
  16. package/templates/Projects/default/messages/en.ts +17 -2
  17. package/templates/Projects/default/messages/fr/_global_ui.json +23 -10
  18. package/templates/Projects/default/messages/fr.ts +17 -2
  19. package/templates/Projects/default/next.config.ts +43 -3
  20. package/templates/Projects/default/package.json +42 -24
  21. package/templates/Projects/default/playwright.config.ts +26 -0
  22. package/templates/Projects/default/pnpm-workspace.yaml +5 -0
  23. package/templates/Projects/default/public/{cnp-logo.svg → logo.svg} +1 -1
  24. package/templates/Projects/default/src/app/[locale]/(public)/layout.tsx +8 -1
  25. package/templates/Projects/default/src/app/[locale]/(public)/login/page.tsx +8 -0
  26. package/templates/Projects/default/src/app/[locale]/(public)/register/page.tsx +8 -0
  27. package/templates/Projects/default/src/app/[locale]/(user)/dashboard/error.tsx +25 -0
  28. package/templates/Projects/default/src/app/[locale]/(user)/dashboard/loading.tsx +5 -0
  29. package/templates/Projects/default/src/app/[locale]/(user)/{Dashboard → dashboard}/page.tsx +1 -1
  30. package/templates/Projects/default/src/app/[locale]/(user)/layout.tsx +24 -2
  31. package/templates/Projects/default/src/app/[locale]/(user)/settings/loading.tsx +5 -0
  32. package/templates/Projects/default/src/app/[locale]/(user)/{Settings → settings}/page.tsx +1 -2
  33. package/templates/Projects/default/src/app/[locale]/(user)/userInfo/loading.tsx +5 -0
  34. package/templates/Projects/default/src/app/[locale]/(user)/{UserInfo → userInfo}/page.tsx +1 -2
  35. package/templates/Projects/default/src/app/[locale]/layout.tsx +34 -10
  36. package/templates/Projects/default/src/app/[locale]/loading.tsx +0 -9
  37. package/templates/Projects/default/src/app/[locale]/not-found.tsx +6 -15
  38. package/templates/Projects/default/src/app/[locale]/page.tsx +10 -1
  39. package/templates/Projects/default/src/app/api/auth/[...nextauth]/route.ts +8 -60
  40. package/templates/Projects/default/src/app/not-found.tsx +1 -1
  41. package/templates/Projects/default/src/app/sitemap.ts +2 -2
  42. package/templates/Projects/default/src/app/styles/globals.css +166 -113
  43. package/templates/Projects/default/src/auth.ts +20 -0
  44. package/templates/Projects/default/src/config.ts +3 -3
  45. package/templates/Projects/default/src/env.ts +65 -0
  46. package/templates/Projects/default/src/lib/i18n/messages.ts +8 -0
  47. package/templates/Projects/default/src/lib/i18n/request.ts +2 -16
  48. package/templates/Projects/default/src/lib/security/csp.ts +16 -0
  49. package/templates/Projects/default/src/lib/utils.ts +2 -1
  50. package/templates/Projects/default/src/proxy.ts +13 -0
  51. package/templates/Projects/default/src/ui/_global/BackButton.tsx +4 -2
  52. package/templates/Projects/default/src/ui/_global/Button.tsx +3 -8
  53. package/templates/Projects/default/src/ui/_global/GlobalHeader.tsx +10 -28
  54. package/templates/Projects/default/src/ui/_global/GlobalMain.tsx +1 -1
  55. package/templates/Projects/default/src/ui/_global/LocaleSwitcher.tsx +9 -10
  56. package/templates/Projects/default/src/ui/_global/PublicNav.tsx +51 -17
  57. package/templates/Projects/default/src/ui/_global/ThemeToggle.tsx +45 -39
  58. package/templates/Projects/default/src/ui/_global/UserNav.tsx +6 -6
  59. package/templates/Projects/default/src/ui/_home/page-ui.tsx +5 -7
  60. package/templates/Projects/default/src/ui/{Dashboard → dashboard}/LogoutButton.tsx +9 -7
  61. package/templates/Projects/default/src/ui/{Dashboard → dashboard}/StatsCard.tsx +4 -4
  62. package/templates/Projects/default/src/ui/{Dashboard → dashboard}/page-ui.tsx +7 -8
  63. package/templates/Projects/default/src/ui/login/page-ui.tsx +36 -0
  64. package/templates/Projects/default/src/ui/register/page-ui.tsx +38 -0
  65. package/templates/Projects/default/src/ui/settings/page-ui.tsx +15 -0
  66. package/templates/Projects/default/src/ui/userInfo/page-ui.tsx +15 -0
  67. package/templates/Projects/default/tailwind.config.ts +81 -1
  68. package/templates/Projects/default/tests/consumer/validate-template.ts +66 -0
  69. package/templates/Projects/default/tests/e2e/template-remediation.playwright.ts +106 -0
  70. package/templates/Projects/default/tests/rendering/verify-rendering.ts +56 -0
  71. package/templates/Projects/default/tests/unit/csp.test.ts +19 -0
  72. package/templates/Projects/default/tests/unit/env.test.ts +76 -0
  73. package/templates/Projects/default/tsconfig.json +6 -6
  74. package/templates/Projects/default/example.env +0 -8
  75. package/templates/Projects/default/messages/getMergedMessages.ts +0 -31
  76. package/templates/Projects/default/middleware.ts +0 -11
  77. package/templates/Projects/default/src/app/[locale]/(public)/Login/page.tsx +0 -6
  78. package/templates/Projects/default/src/app/[locale]/(public)/Register/page.tsx +0 -6
  79. package/templates/Projects/default/src/app/[locale]/(user)/Dashboard/error.tsx +0 -38
  80. package/templates/Projects/default/src/app/[locale]/(user)/Dashboard/loading.tsx +0 -10
  81. package/templates/Projects/default/src/app/[locale]/(user)/Settings/loading.tsx +0 -17
  82. package/templates/Projects/default/src/app/[locale]/(user)/UserInfo/loading.tsx +0 -17
  83. package/templates/Projects/default/src/app/api/auth/post-login/route.ts +0 -26
  84. package/templates/Projects/default/src/app/api/hello/route.ts +0 -5
  85. package/templates/Projects/default/src/app/layout.tsx +0 -11
  86. package/templates/Projects/default/src/app/page.tsx +0 -6
  87. package/templates/Projects/default/src/auth.config.ts +0 -0
  88. package/templates/Projects/default/src/lib/auth/disconnect.ts +0 -11
  89. package/templates/Projects/default/src/lib/auth/isConnected.ts +0 -18
  90. package/templates/Projects/default/src/lib/sample/example.ts +0 -3
  91. package/templates/Projects/default/src/lib/sample/index.ts +0 -3
  92. package/templates/Projects/default/src/ui/Login/page-ui.tsx +0 -22
  93. package/templates/Projects/default/src/ui/Register/page-ui.tsx +0 -26
  94. package/templates/Projects/default/src/ui/Settings/page-ui.tsx +0 -17
  95. package/templates/Projects/default/src/ui/UserInfo/page-ui.tsx +0 -17
  96. /package/templates/Projects/default/messages/en/{Dashboard.json → dashboard.json} +0 -0
  97. /package/templates/Projects/default/messages/en/{Login.json → login.json} +0 -0
  98. /package/templates/Projects/default/messages/en/{Register.json → register.json} +0 -0
  99. /package/templates/Projects/default/messages/en/{Settings.json → settings.json} +0 -0
  100. /package/templates/Projects/default/messages/en/{UserInfo.json → userInfo.json} +0 -0
  101. /package/templates/Projects/default/messages/fr/{Dashboard.json → dashboard.json} +0 -0
  102. /package/templates/Projects/default/messages/fr/{Login.json → login.json} +0 -0
  103. /package/templates/Projects/default/messages/fr/{Register.json → register.json} +0 -0
  104. /package/templates/Projects/default/messages/fr/{Settings.json → settings.json} +0 -0
  105. /package/templates/Projects/default/messages/fr/{UserInfo.json → userInfo.json} +0 -0
  106. /package/templates/Projects/default/public/{cnp-logo.png → logo.png} +0 -0
  107. /package/templates/Projects/default/src/ui/{Dashboard → dashboard}/WelcomeCard.tsx +0 -0
package/dist/bin.bun.js CHANGED
@@ -5,21 +5,35 @@ var __getProtoOf = Object.getPrototypeOf;
5
5
  var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ function __accessProp(key) {
9
+ return this[key];
10
+ }
11
+ var __toESMCache_node;
12
+ var __toESMCache_esm;
8
13
  var __toESM = (mod, isNodeMode, target) => {
14
+ var canCache = mod != null && typeof mod === "object";
15
+ if (canCache) {
16
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
+ var cached = cache.get(mod);
18
+ if (cached)
19
+ return cached;
20
+ }
9
21
  target = mod != null ? __create(__getProtoOf(mod)) : {};
10
22
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
23
  for (let key of __getOwnPropNames(mod))
12
24
  if (!__hasOwnProp.call(to, key))
13
25
  __defProp(to, key, {
14
- get: () => mod[key],
26
+ get: __accessProp.bind(mod, key),
15
27
  enumerable: true
16
28
  });
29
+ if (canCache)
30
+ cache.set(mod, to);
17
31
  return to;
18
32
  };
19
33
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
20
34
  var __require = import.meta.require;
21
35
 
22
- // node_modules/prompts/node_modules/kleur/index.js
36
+ // node_modules/kleur/index.js
23
37
  var require_kleur = __commonJS((exports, module) => {
24
38
  var { FORCE_COLOR, NODE_DISABLE_COLORS, TERM } = process.env;
25
39
  var $ = {
@@ -243,7 +257,7 @@ var require_clear = __commonJS((exports, module) => {
243
257
  if (it)
244
258
  o = it;
245
259
  var i = 0;
246
- var F = function F() {};
260
+ var F = function F2() {};
247
261
  return { s: F, n: function n() {
248
262
  if (i >= o.length)
249
263
  return { done: true };
@@ -523,7 +537,7 @@ var require_prompt = __commonJS((exports, module) => {
523
537
 
524
538
  // node_modules/prompts/dist/elements/text.js
525
539
  var require_text = __commonJS((exports, module) => {
526
- function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
540
+ function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
527
541
  try {
528
542
  var info = gen[key](arg);
529
543
  var value = info.value;
@@ -532,7 +546,7 @@ var require_text = __commonJS((exports, module) => {
532
546
  return;
533
547
  }
534
548
  if (info.done) {
535
- resolve(value);
549
+ resolve2(value);
536
550
  } else {
537
551
  Promise.resolve(value).then(_next, _throw);
538
552
  }
@@ -540,13 +554,13 @@ var require_text = __commonJS((exports, module) => {
540
554
  function _asyncToGenerator(fn) {
541
555
  return function() {
542
556
  var self = this, args = arguments;
543
- return new Promise(function(resolve, reject) {
557
+ return new Promise(function(resolve2, reject) {
544
558
  var gen = fn.apply(self, args);
545
559
  function _next(value) {
546
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
560
+ asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "next", value);
547
561
  }
548
562
  function _throw(err) {
549
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
563
+ asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "throw", err);
550
564
  }
551
565
  _next(undefined);
552
566
  });
@@ -1260,7 +1274,7 @@ var require_dateparts = __commonJS((exports, module) => {
1260
1274
 
1261
1275
  // node_modules/prompts/dist/elements/date.js
1262
1276
  var require_date = __commonJS((exports, module) => {
1263
- function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
1277
+ function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
1264
1278
  try {
1265
1279
  var info = gen[key](arg);
1266
1280
  var value = info.value;
@@ -1269,7 +1283,7 @@ var require_date = __commonJS((exports, module) => {
1269
1283
  return;
1270
1284
  }
1271
1285
  if (info.done) {
1272
- resolve(value);
1286
+ resolve2(value);
1273
1287
  } else {
1274
1288
  Promise.resolve(value).then(_next, _throw);
1275
1289
  }
@@ -1277,13 +1291,13 @@ var require_date = __commonJS((exports, module) => {
1277
1291
  function _asyncToGenerator(fn) {
1278
1292
  return function() {
1279
1293
  var self = this, args = arguments;
1280
- return new Promise(function(resolve, reject) {
1294
+ return new Promise(function(resolve2, reject) {
1281
1295
  var gen = fn.apply(self, args);
1282
1296
  function _next(value) {
1283
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
1297
+ asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "next", value);
1284
1298
  }
1285
1299
  function _throw(err) {
1286
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
1300
+ asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "throw", err);
1287
1301
  }
1288
1302
  _next(undefined);
1289
1303
  });
@@ -1487,7 +1501,7 @@ ${i ? ` ` : figures.pointerSmall} ${color.red().italic(l)}`, ``);
1487
1501
 
1488
1502
  // node_modules/prompts/dist/elements/number.js
1489
1503
  var require_number = __commonJS((exports, module) => {
1490
- function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
1504
+ function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
1491
1505
  try {
1492
1506
  var info = gen[key](arg);
1493
1507
  var value = info.value;
@@ -1496,7 +1510,7 @@ var require_number = __commonJS((exports, module) => {
1496
1510
  return;
1497
1511
  }
1498
1512
  if (info.done) {
1499
- resolve(value);
1513
+ resolve2(value);
1500
1514
  } else {
1501
1515
  Promise.resolve(value).then(_next, _throw);
1502
1516
  }
@@ -1504,13 +1518,13 @@ var require_number = __commonJS((exports, module) => {
1504
1518
  function _asyncToGenerator(fn) {
1505
1519
  return function() {
1506
1520
  var self = this, args = arguments;
1507
- return new Promise(function(resolve, reject) {
1521
+ return new Promise(function(resolve2, reject) {
1508
1522
  var gen = fn.apply(self, args);
1509
1523
  function _next(value) {
1510
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
1524
+ asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "next", value);
1511
1525
  }
1512
1526
  function _throw(err) {
1513
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
1527
+ asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "throw", err);
1514
1528
  }
1515
1529
  _next(undefined);
1516
1530
  });
@@ -1946,7 +1960,7 @@ Instructions:
1946
1960
 
1947
1961
  // node_modules/prompts/dist/elements/autocomplete.js
1948
1962
  var require_autocomplete = __commonJS((exports, module) => {
1949
- function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
1963
+ function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
1950
1964
  try {
1951
1965
  var info = gen[key](arg);
1952
1966
  var value = info.value;
@@ -1955,7 +1969,7 @@ var require_autocomplete = __commonJS((exports, module) => {
1955
1969
  return;
1956
1970
  }
1957
1971
  if (info.done) {
1958
- resolve(value);
1972
+ resolve2(value);
1959
1973
  } else {
1960
1974
  Promise.resolve(value).then(_next, _throw);
1961
1975
  }
@@ -1963,13 +1977,13 @@ var require_autocomplete = __commonJS((exports, module) => {
1963
1977
  function _asyncToGenerator(fn) {
1964
1978
  return function() {
1965
1979
  var self = this, args = arguments;
1966
- return new Promise(function(resolve, reject) {
1980
+ return new Promise(function(resolve2, reject) {
1967
1981
  var gen = fn.apply(self, args);
1968
1982
  function _next(value) {
1969
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
1983
+ asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "next", value);
1970
1984
  }
1971
1985
  function _throw(err) {
1972
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
1986
+ asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "throw", err);
1973
1987
  }
1974
1988
  _next(undefined);
1975
1989
  });
@@ -2575,7 +2589,7 @@ var require_dist = __commonJS((exports, module) => {
2575
2589
  if (it)
2576
2590
  o = it;
2577
2591
  var i = 0;
2578
- var F = function F() {};
2592
+ var F = function F2() {};
2579
2593
  return { s: F, n: function n() {
2580
2594
  if (i >= o.length)
2581
2595
  return { done: true };
@@ -2627,7 +2641,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
2627
2641
  arr2[i] = arr[i];
2628
2642
  return arr2;
2629
2643
  }
2630
- function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
2644
+ function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
2631
2645
  try {
2632
2646
  var info = gen[key](arg);
2633
2647
  var value = info.value;
@@ -2636,7 +2650,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
2636
2650
  return;
2637
2651
  }
2638
2652
  if (info.done) {
2639
- resolve(value);
2653
+ resolve2(value);
2640
2654
  } else {
2641
2655
  Promise.resolve(value).then(_next, _throw);
2642
2656
  }
@@ -2644,13 +2658,13 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
2644
2658
  function _asyncToGenerator(fn) {
2645
2659
  return function() {
2646
2660
  var self = this, args = arguments;
2647
- return new Promise(function(resolve, reject) {
2661
+ return new Promise(function(resolve2, reject) {
2648
2662
  var gen = fn.apply(self, args);
2649
2663
  function _next(value) {
2650
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
2664
+ asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "next", value);
2651
2665
  }
2652
2666
  function _throw(err) {
2653
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
2667
+ asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "throw", err);
2654
2668
  }
2655
2669
  _next(undefined);
2656
2670
  });
@@ -2678,7 +2692,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
2678
2692
  }
2679
2693
  return question2.format ? yield question2.format(answer2, answers) : answer2;
2680
2694
  });
2681
- return function getFormattedAnswer(_x, _x2) {
2695
+ return function getFormattedAnswer2(_x, _x2) {
2682
2696
  return _ref.apply(this, arguments);
2683
2697
  };
2684
2698
  }();
@@ -4937,31 +4951,515 @@ var require_prompts3 = __commonJS((exports, module) => {
4937
4951
  });
4938
4952
 
4939
4953
  // src/index.ts
4940
- var import_prompts8 = __toESM(require_prompts3(), 1);
4941
- import fs from "fs";
4942
- import os from "os";
4954
+ import path10 from "path";
4955
+
4956
+ // src/cli/completion.ts
4957
+ import path2 from "path";
4958
+
4959
+ // src/core/page-catalog.ts
4943
4960
  import path from "path";
4944
- import { fileURLToPath as fileURLToPath2 } from "url";
4945
- import { dirname, resolve } from "path";
4961
+ function isRouteGroup(segment) {
4962
+ return segment.startsWith("(") && segment.endsWith(")");
4963
+ }
4964
+ async function discoverPages(projectRoot, fs) {
4965
+ const appRoot = path.join(projectRoot, "src", "app", "[locale]");
4966
+ const candidates = [];
4967
+ async function visit(directory, relative = []) {
4968
+ let entries;
4969
+ try {
4970
+ entries = await fs.list(directory);
4971
+ } catch {
4972
+ return;
4973
+ }
4974
+ if (entries.some((entry) => entry.isFile && entry.name === "page.tsx")) {
4975
+ const routeSegments = relative.filter((segment) => !isRouteGroup(segment));
4976
+ if (routeSegments.length > 0 && !routeSegments.some((segment) => segment.startsWith("_") || segment.startsWith("["))) {
4977
+ const logicalName = routeSegments.join(".");
4978
+ candidates.push({
4979
+ logicalName,
4980
+ routeSegments,
4981
+ routeDirectory: directory,
4982
+ uiDirectory: path.join(projectRoot, "src", "ui", ...routeSegments),
4983
+ messageFile: path.join(projectRoot, "messages", "{locale}", `${routeSegments[0]}.json`),
4984
+ messageKey: routeSegments.length > 1 ? routeSegments.at(-1) : undefined
4985
+ });
4986
+ }
4987
+ }
4988
+ for (const entry of entries) {
4989
+ if (entry.isDirectory && !entry.name.startsWith(".")) {
4990
+ await visit(path.join(directory, entry.name), [
4991
+ ...relative,
4992
+ entry.name
4993
+ ]);
4994
+ }
4995
+ }
4996
+ }
4997
+ await visit(appRoot);
4998
+ return candidates.sort((left, right) => left.logicalName.localeCompare(right.logicalName));
4999
+ }
4946
5000
 
4947
- // src/lib/addComponent.ts
4948
- var import_prompts = __toESM(require_prompts3(), 1);
5001
+ // src/cli/completion.ts
5002
+ var PUBLIC_COMMANDS = [
5003
+ "addpage",
5004
+ "addcomponent",
5005
+ "addlib",
5006
+ "addapi",
5007
+ "addlanguage",
5008
+ "addtext",
5009
+ "rmpage",
5010
+ "--help",
5011
+ "--version",
5012
+ "--json",
5013
+ "--reconfigure"
5014
+ ];
5015
+ var OPTIONS = {
5016
+ addpage: [
5017
+ "--layout",
5018
+ "--page",
5019
+ "--loading",
5020
+ "--not-found",
5021
+ "--error",
5022
+ "--global-error",
5023
+ "--route",
5024
+ "--template",
5025
+ "--default"
5026
+ ],
5027
+ addcomponent: ["--page", "-P"]
5028
+ };
5029
+ async function directories(root, context) {
5030
+ try {
5031
+ return (await context.fs.list(root)).filter((entry) => entry.isDirectory && !entry.name.startsWith("_")).map((entry) => entry.name).sort();
5032
+ } catch {
5033
+ return [];
5034
+ }
5035
+ }
5036
+ async function completionCandidates(command, context) {
5037
+ if (!command)
5038
+ return [...PUBLIC_COMMANDS];
5039
+ if (command === "rmpage") {
5040
+ return (await discoverPages(context.cwd, context.fs)).map((candidate) => candidate.logicalName);
5041
+ }
5042
+ if (command === "addcomponent" || command === "addpage") {
5043
+ return [
5044
+ ...OPTIONS[command] ?? [],
5045
+ ...await directories(path2.join(context.cwd, "src", "ui"), context)
5046
+ ];
5047
+ }
5048
+ if (command === "addlanguage")
5049
+ return ["de", "en", "es", "fr", "it", "ja", "pt"];
5050
+ return OPTIONS[command] ?? [];
5051
+ }
5052
+
5053
+ // src/cli/onboarding.ts
5054
+ import path4 from "path";
5055
+
5056
+ // src/core/operations.ts
5057
+ import path3 from "path";
5058
+
5059
+ // src/core/contracts.ts
5060
+ class CliError extends Error {
5061
+ exitCode;
5062
+ code;
5063
+ hint;
5064
+ scope;
5065
+ path;
5066
+ constructor(message, options = {}) {
5067
+ super(message);
5068
+ this.name = "CliError";
5069
+ if (typeof options === "number") {
5070
+ this.exitCode = options;
5071
+ this.code = "FILESYSTEM_ERROR";
5072
+ } else {
5073
+ this.exitCode = options.exitCode ?? 1;
5074
+ this.code = options.code ?? "FILESYSTEM_ERROR";
5075
+ this.hint = options.hint;
5076
+ this.scope = options.scope;
5077
+ this.path = options.path;
5078
+ }
5079
+ }
5080
+ }
5081
+
5082
+ // src/core/operations.ts
5083
+ class OperationJournal {
5084
+ #events = [];
5085
+ record(event) {
5086
+ const detail = event.detail ? Object.fromEntries(Object.entries(event.detail).map(([key, value]) => [
5087
+ key,
5088
+ /(content|credential|env|password|secret|token|value)/i.test(key) ? "[REDACTED]" : value
5089
+ ])) : undefined;
5090
+ const recorded = {
5091
+ ...event,
5092
+ detail,
5093
+ sequence: this.#events.length + 1
5094
+ };
5095
+ this.#events.push(recorded);
5096
+ return recorded;
5097
+ }
5098
+ snapshot() {
5099
+ return this.#events.map((event) => ({ ...event }));
5100
+ }
5101
+ reset() {
5102
+ this.#events.length = 0;
5103
+ }
5104
+ }
5105
+ var MUTATIONS = new Set(["created", "copied", "updated", "deleted"]);
5106
+ function statusFromEvents(events) {
5107
+ if (events.some((event) => event.action === "cancelled"))
5108
+ return "cancelled";
5109
+ return events.some((event) => MUTATIONS.has(event.action)) ? "success" : "unchanged";
5110
+ }
5111
+ function commandResult(context, input) {
5112
+ const events = context.operations.snapshot();
5113
+ const status = input.status ?? statusFromEvents(events);
5114
+ return {
5115
+ exitCode: status === "failed" ? 1 : 0,
5116
+ status,
5117
+ command: input.command,
5118
+ summary: input.summary,
5119
+ projectRoot: input.projectRoot,
5120
+ configRoot: input.configRoot,
5121
+ homeRoot: input.homeRoot,
5122
+ events,
5123
+ nextSteps: input.nextSteps ?? [],
5124
+ error: null,
5125
+ data: input.data
5126
+ };
5127
+ }
5128
+ function failedResult(context, command, error, roots = {}) {
5129
+ const cliError = error instanceof CliError ? error : undefined;
5130
+ const normalized = {
5131
+ code: cliError?.code ?? "FILESYSTEM_ERROR",
5132
+ message: error instanceof Error ? error.message : String(error),
5133
+ hint: cliError?.hint,
5134
+ scope: cliError?.scope,
5135
+ path: cliError?.path
5136
+ };
5137
+ const nextSteps = normalized.code === "ONBOARDING_REQUIRED" ? [
5138
+ {
5139
+ kind: "rerun",
5140
+ required: true,
5141
+ message: "Run create-next-pro once in human mode, then rerun the JSON command.",
5142
+ paths: [{ scope: "config", path: "config.json" }],
5143
+ commands: ["create-next-pro"]
5144
+ }
5145
+ ] : [];
5146
+ context.operations.record({
5147
+ action: "failed",
5148
+ resource: "command",
5149
+ role: command,
5150
+ scope: normalized.scope ?? "project",
5151
+ path: normalized.path ?? ".",
5152
+ detail: { code: normalized.code }
5153
+ });
5154
+ return {
5155
+ exitCode: cliError?.exitCode ?? 1,
5156
+ status: "failed",
5157
+ command,
5158
+ summary: normalized.message,
5159
+ ...roots,
5160
+ events: context.operations.snapshot(),
5161
+ nextSteps,
5162
+ error: normalized
5163
+ };
5164
+ }
5165
+ function relativeResource(root, target) {
5166
+ const relative = path3.relative(path3.resolve(root), path3.resolve(target));
5167
+ return relative || ".";
5168
+ }
5169
+
5170
+ class MutationGateway {
5171
+ context;
5172
+ root;
5173
+ defaultScope;
5174
+ constructor(context, root, defaultScope = "project") {
5175
+ this.context = context;
5176
+ this.root = root;
5177
+ this.defaultScope = defaultScope;
5178
+ }
5179
+ path(target) {
5180
+ return relativeResource(this.root, target);
5181
+ }
5182
+ async mkdir(target, metadata, record = true) {
5183
+ if (this.context.fs.exists(target)) {
5184
+ if (record)
5185
+ this.record("unchanged", target, metadata);
5186
+ return "unchanged";
5187
+ }
5188
+ await this.context.fs.mkdir(target);
5189
+ if (record)
5190
+ this.record("created", target, metadata);
5191
+ return "created";
5192
+ }
5193
+ async write(target, content, metadata) {
5194
+ const exists = this.context.fs.exists(target);
5195
+ if (exists && metadata.preserveExisting) {
5196
+ this.record("unchanged", target, metadata);
5197
+ return "unchanged";
5198
+ }
5199
+ if (exists && await this.context.fs.readText(target) === content) {
5200
+ this.record("unchanged", target, metadata);
5201
+ return "unchanged";
5202
+ }
5203
+ await this.context.fs.mkdir(path3.dirname(target));
5204
+ await this.context.fs.writeText(target, content);
5205
+ const action = exists ? "updated" : "created";
5206
+ this.record(action, target, metadata);
5207
+ return action;
5208
+ }
5209
+ async copy(source, target, metadata) {
5210
+ if (this.context.fs.exists(target) && metadata.preserveExisting) {
5211
+ this.record("unchanged", target, metadata);
5212
+ return "unchanged";
5213
+ }
5214
+ await this.context.fs.mkdir(path3.dirname(target));
5215
+ await this.context.fs.copyFile(source, target);
5216
+ this.record("copied", target, {
5217
+ ...metadata,
5218
+ source: metadata.source ?? { path: source }
5219
+ });
5220
+ return "copied";
5221
+ }
5222
+ async remove(target, metadata, options = {}) {
5223
+ if (!this.context.fs.exists(target)) {
5224
+ this.record("unchanged", target, metadata);
5225
+ return "unchanged";
5226
+ }
5227
+ await this.context.fs.remove(target, options);
5228
+ this.record("deleted", target, metadata);
5229
+ return "deleted";
5230
+ }
5231
+ unchanged(target, metadata) {
5232
+ this.record("unchanged", target, metadata);
5233
+ }
5234
+ skipped(target, metadata) {
5235
+ this.record("skipped", target, metadata);
5236
+ }
5237
+ record(action, target, metadata) {
5238
+ this.context.operations.record({
5239
+ action,
5240
+ resource: metadata.resource ?? "file",
5241
+ role: metadata.role,
5242
+ scope: metadata.scope ?? this.defaultScope,
5243
+ path: this.path(target),
5244
+ source: metadata.source,
5245
+ detail: metadata.detail
5246
+ });
5247
+ }
5248
+ }
5249
+
5250
+ // src/cli/onboarding.ts
5251
+ function configDirectory(context) {
5252
+ return context.env.XDG_CONFIG_HOME ? path4.join(context.env.XDG_CONFIG_HOME, "create-next-pro") : path4.join(context.homeDir, ".config", "create-next-pro");
5253
+ }
5254
+ function configFile(context) {
5255
+ return path4.join(configDirectory(context), "config.json");
5256
+ }
5257
+ async function readConfig(context) {
5258
+ try {
5259
+ const config = JSON.parse(await context.fs.readText(configFile(context)));
5260
+ if (config.version !== 1 || config.shell !== "bash" && config.shell !== "zsh" || typeof config.completionInstalled !== "boolean" || typeof config.createdAt !== "string" || typeof config.updatedAt !== "string") {
5261
+ return null;
5262
+ }
5263
+ return config;
5264
+ } catch {
5265
+ return null;
5266
+ }
5267
+ }
5268
+ async function ensureLineInRc(context, target, line) {
5269
+ const current = context.fs.exists(target) ? await context.fs.readText(target) : "";
5270
+ const gateway = new MutationGateway(context, context.homeDir, "home");
5271
+ return gateway.write(target, current.includes(line) ? current : `${current}
5272
+ ${line}
5273
+ `, { role: "shell-profile", resource: "shell-profile" });
5274
+ }
5275
+ async function installCompletion(context, shell) {
5276
+ const directory = configDirectory(context);
5277
+ const source = path4.join(context.packageRoot, shell === "zsh" ? "create-next-pro-completion.zsh" : "create-next-pro-completion.sh");
5278
+ const target = path4.join(directory, `completion.${shell === "zsh" ? "zsh" : "sh"}`);
5279
+ const gateway = new MutationGateway(context, directory, "config");
5280
+ await gateway.write(target, await context.fs.readText(source), {
5281
+ role: "completion-script",
5282
+ source: { scope: "package", path: path4.basename(source) }
5283
+ });
5284
+ const rcFile = path4.join(context.homeDir, shell === "zsh" ? ".zshrc" : ".bashrc");
5285
+ await ensureLineInRc(context, rcFile, `source "${target}"`);
5286
+ }
5287
+ async function onboarding(context, version) {
5288
+ const response = await context.prompt([
5289
+ {
5290
+ type: "select",
5291
+ name: "shell",
5292
+ message: "Which shell do you use?",
5293
+ choices: [
5294
+ { title: "zsh", value: "zsh" },
5295
+ { title: "bash", value: "bash" }
5296
+ ],
5297
+ initial: context.env.SHELL?.includes("zsh") ? 0 : 1
5298
+ },
5299
+ {
5300
+ type: "toggle",
5301
+ name: "completion",
5302
+ message: "Install autocompletion?",
5303
+ initial: true,
5304
+ active: "Yes",
5305
+ inactive: "No"
5306
+ }
5307
+ ], { onCancel: () => false });
5308
+ if (response.shell !== "bash" && response.shell !== "zsh") {
5309
+ context.operations.record({
5310
+ action: "cancelled",
5311
+ resource: "command",
5312
+ role: "onboarding",
5313
+ scope: "config",
5314
+ path: "."
5315
+ });
5316
+ return commandResult(context, {
5317
+ command: "onboarding",
5318
+ summary: "Configuration was cancelled.",
5319
+ configRoot: configDirectory(context),
5320
+ homeRoot: context.homeDir,
5321
+ status: "cancelled"
5322
+ });
5323
+ }
5324
+ const previous = await readConfig(context);
5325
+ const now = new Date().toISOString();
5326
+ const config = {
5327
+ version: 1,
5328
+ shell: response.shell,
5329
+ completionInstalled: Boolean(response.completion),
5330
+ createdAt: previous?.createdAt ?? now,
5331
+ updatedAt: now
5332
+ };
5333
+ if (config.completionInstalled) {
5334
+ try {
5335
+ await installCompletion(context, config.shell);
5336
+ } catch (error) {
5337
+ context.operations.record({
5338
+ action: "skipped",
5339
+ resource: "file",
5340
+ role: "completion-installation",
5341
+ scope: "config",
5342
+ path: `completion.${config.shell === "zsh" ? "zsh" : "sh"}`,
5343
+ detail: {
5344
+ reason: error instanceof Error ? error.message : String(error)
5345
+ }
5346
+ });
5347
+ config.completionInstalled = false;
5348
+ }
5349
+ }
5350
+ const semanticallyUnchanged = previous?.shell === config.shell && previous.completionInstalled === config.completionInstalled;
5351
+ if (semanticallyUnchanged && previous)
5352
+ config.updatedAt = previous.updatedAt;
5353
+ const gateway = new MutationGateway(context, configDirectory(context), "config");
5354
+ await gateway.write(configFile(context), `${JSON.stringify(config, null, 2)}
5355
+ `, { role: "user-configuration", resource: "configuration" });
5356
+ return commandResult(context, {
5357
+ command: "onboarding",
5358
+ summary: `Configuration for create-next-pro v${version} is ready.`,
5359
+ configRoot: configDirectory(context),
5360
+ homeRoot: context.homeDir,
5361
+ nextSteps: [
5362
+ {
5363
+ kind: "rerun",
5364
+ required: true,
5365
+ message: "Run the original create-next-pro command again.",
5366
+ paths: []
5367
+ }
5368
+ ]
5369
+ });
5370
+ }
5371
+
5372
+ // src/cli/output.ts
5373
+ import path5 from "path";
5374
+ function rootFor(result, context, scope) {
5375
+ if (scope === "project")
5376
+ return result.projectRoot;
5377
+ if (scope === "config")
5378
+ return result.configRoot;
5379
+ if (scope === "home")
5380
+ return result.homeRoot;
5381
+ return context.packageRoot;
5382
+ }
5383
+ function displayPath(result, context, scope, target) {
5384
+ const root = rootFor(result, context, scope);
5385
+ return root ? path5.resolve(root, target) : target;
5386
+ }
5387
+ function renderEvent(result, context, event) {
5388
+ const source = event.source?.template ? ` from template ${event.source.template}` : event.source?.path ? ` from ${displayPath(result, context, event.source.scope ?? event.scope, event.source.path)}` : "";
5389
+ const detail = event.detail ? ` (${Object.entries(event.detail).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${JSON.stringify(value)}`).join(", ")})` : "";
5390
+ return `${event.action.toUpperCase()} ${event.role}: ${displayPath(result, context, event.scope, event.path)}${source}${detail}`;
5391
+ }
5392
+ function renderResult(result, context, mode) {
5393
+ if (mode === "json") {
5394
+ context.terminal.log(JSON.stringify({ schemaVersion: 1, ...result }));
5395
+ return;
5396
+ }
5397
+ if (result.command === "help" && typeof result.data?.help === "string") {
5398
+ context.terminal.log(result.data.help);
5399
+ return;
5400
+ }
5401
+ if (result.command === "version" && typeof result.data?.version === "string") {
5402
+ context.terminal.log(`v${result.data.version}`);
5403
+ return;
5404
+ }
5405
+ const copiedTemplateFiles = result.events.filter((event) => event.action === "copied" && event.role === "template-file");
5406
+ if (copiedTemplateFiles.length > 0) {
5407
+ context.terminal.log(`COPIED ${copiedTemplateFiles.length} template files to ${result.projectRoot}.`);
5408
+ }
5409
+ for (const event of result.events) {
5410
+ if (event.action === "copied" && event.role === "template-file")
5411
+ continue;
5412
+ const line = renderEvent(result, context, event);
5413
+ if (event.action === "failed")
5414
+ context.terminal.error(line);
5415
+ else if (event.action === "skipped")
5416
+ context.terminal.warn(line);
5417
+ else
5418
+ context.terminal.log(line);
5419
+ }
5420
+ if (result.error) {
5421
+ context.terminal.error(`ERROR [${result.error.code}]: ${result.error.message}`);
5422
+ if (result.error.hint)
5423
+ context.terminal.error(`HINT: ${result.error.hint}`);
5424
+ } else if (result.status === "success") {
5425
+ context.terminal.log(`SUCCESS: ${result.summary}`);
5426
+ } else if (result.status === "unchanged") {
5427
+ context.terminal.log(`NO CHANGES: ${result.summary}`);
5428
+ } else if (result.status === "cancelled") {
5429
+ context.terminal.log(`CANCELLED: ${result.summary}`);
5430
+ }
5431
+ for (const step of result.nextSteps) {
5432
+ context.terminal.log(`NEXT [${step.kind}]: ${step.message}`);
5433
+ for (const target of step.paths) {
5434
+ context.terminal.log(` ${displayPath(result, context, target.scope, target.path)}`);
5435
+ }
5436
+ for (const command of step.commands ?? []) {
5437
+ context.terminal.log(` ${command}`);
5438
+ }
5439
+ }
5440
+ }
5441
+
5442
+ // src/lib/addApi.ts
4949
5443
  import { join as join2 } from "path";
4950
- import { mkdir, readFile as readFile2, writeFile, readdir } from "fs/promises";
4951
5444
 
4952
5445
  // src/lib/utils.ts
4953
- import { readFile } from "fs/promises";
4954
- import { existsSync } from "fs";
4955
5446
  import { join } from "path";
4956
5447
  function capitalize(str) {
4957
5448
  return str.charAt(0).toUpperCase() + str.slice(1);
4958
5449
  }
4959
- async function loadConfig() {
4960
- const configPath = join(process.cwd(), "cnp.config.json");
4961
- if (!existsSync(configPath))
5450
+ function toIdentifier(value) {
5451
+ return value.replace(/[-_]+([A-Za-z0-9])/g, (_, character) => character.toUpperCase());
5452
+ }
5453
+ function configuredAliasPrefix(config) {
5454
+ const alias = config.importAlias ?? "@/*";
5455
+ return alias.endsWith("/*") ? alias.slice(0, -2) : "@";
5456
+ }
5457
+ async function loadConfig(context) {
5458
+ const configPath = join(context.cwd, "cnp.config.json");
5459
+ if (!context.fs.exists(configPath))
4962
5460
  return null;
4963
5461
  try {
4964
- const raw = await readFile(configPath, "utf-8");
5462
+ const raw = await context.fs.readText(configPath);
4965
5463
  return JSON.parse(raw);
4966
5464
  } catch {
4967
5465
  return null;
@@ -4992,533 +5490,1573 @@ function toFileName(key) {
4992
5490
  }
4993
5491
  }
4994
5492
 
4995
- // src/lib/addComponent.ts
4996
- import { existsSync as existsSync2, statSync } from "fs";
4997
- async function addComponent(args) {
4998
- let componentName = args[1];
4999
- let pageScope = null;
5000
- let pageIndex = args.findIndex((arg) => arg === "-P" || arg === "--page");
5001
- if (pageIndex !== -1 && args[pageIndex + 1]) {
5002
- pageScope = args[pageIndex + 1];
5493
+ // src/core/project-paths.ts
5494
+ import path6 from "path";
5495
+ var SAFE_SEGMENT = /^[A-Za-z][A-Za-z0-9_-]*$/;
5496
+ function hasControlCharacters(value) {
5497
+ return [...value].some((character) => {
5498
+ const code = character.charCodeAt(0);
5499
+ return code <= 31 || code === 127;
5500
+ });
5501
+ }
5502
+ function parseLogicalName(value, label = "name") {
5503
+ if (!value || hasControlCharacters(value)) {
5504
+ throw new CliError(`Invalid ${label}: a non-empty printable value is required.`, { code: "INVALID_ARGUMENT" });
5003
5505
  }
5004
- let nestedPath = null;
5005
- if (pageScope && pageScope.includes(".")) {
5006
- nestedPath = join2(...pageScope.split("."));
5506
+ if (path6.isAbsolute(value) || value.includes("/") || value.includes("\\")) {
5507
+ throw new CliError(`Invalid ${label}: paths and separators are not allowed.`, { code: "INVALID_ARGUMENT" });
5007
5508
  }
5008
- if (!componentName || componentName.startsWith("-")) {
5009
- const response = await import_prompts.default.prompt({
5010
- type: "text",
5011
- name: "componentName",
5012
- message: "\uD83E\uDDE9 Component name to add:",
5013
- validate: (name) => name ? true : "Component name is required"
5014
- });
5015
- componentName = response.componentName;
5509
+ const segments = value.split(".");
5510
+ if (segments.some((segment) => !SAFE_SEGMENT.test(segment))) {
5511
+ throw new CliError(`Invalid ${label}: use dot-separated alphanumeric segments beginning with a letter.`, { code: "INVALID_ARGUMENT" });
5016
5512
  }
5017
- const config = await loadConfig();
5018
- if (!config) {
5019
- console.error("\u274C Configuration file cnp.config.json not found. Run this command from the project root.");
5020
- return;
5513
+ return segments;
5514
+ }
5515
+ function validateProjectName(value) {
5516
+ if (!value || hasControlCharacters(value) || path6.isAbsolute(value) || value === "." || value === ".." || value.includes("/") || value.includes("\\") || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value)) {
5517
+ throw new CliError("Invalid project name: use letters, numbers, dots, dashes or underscores without path separators.", { code: "INVALID_ARGUMENT" });
5021
5518
  }
5022
- const useI18n = !!config.useI18n;
5023
- const componentNameUpper = capitalize(componentName);
5024
- const templatePath = join2(new URL("..", import.meta.url).pathname, "templates", "Component");
5025
- let messagesPath = null;
5026
- if (useI18n) {
5027
- messagesPath = join2(process.cwd(), "messages");
5028
- if (!existsSync2(messagesPath)) {
5029
- console.error("\u274C Messages directory missing. Ensure i18n was configured.");
5030
- return;
5031
- }
5519
+ return value;
5520
+ }
5521
+ function resolveInside(root, ...segments) {
5522
+ const absoluteRoot = path6.resolve(root);
5523
+ const target = path6.resolve(absoluteRoot, ...segments);
5524
+ if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${path6.sep}`)) {
5525
+ throw new CliError(`Refusing to access a path outside the project: ${target}`, { code: "UNSAFE_PATH" });
5032
5526
  }
5033
- let componentTargetPath;
5034
- let translationKey;
5035
- if (pageScope) {
5036
- if (nestedPath) {
5037
- componentTargetPath = join2(process.cwd(), "src", "ui", nestedPath);
5038
- translationKey = pageScope;
5039
- } else {
5040
- componentTargetPath = join2(process.cwd(), "src", "ui", pageScope);
5041
- translationKey = pageScope;
5042
- }
5043
- } else {
5044
- componentTargetPath = join2(process.cwd(), "src", "ui", "_global");
5045
- translationKey = "_global_ui";
5046
- }
5047
- if (!existsSync2(componentTargetPath)) {
5048
- await mkdir(componentTargetPath, { recursive: true });
5049
- }
5050
- const componentFile = join2(componentTargetPath, `${componentNameUpper}.tsx`);
5051
- const templateComponentPath = join2(templatePath, "Component.tsx");
5052
- if (existsSync2(templateComponentPath)) {
5053
- let content = await readFile2(templateComponentPath, "utf-8");
5054
- content = content.replace(/Component/g, componentNameUpper).replace(/componentPage/g, translationKey);
5055
- await writeFile(componentFile, content);
5056
- console.log(`\uD83D\uDCC4 File created: ${componentFile}`);
5057
- } else {
5058
- console.error("\u274C Template Component.tsx introuvable :", templateComponentPath);
5059
- }
5060
- if (useI18n && messagesPath) {
5061
- const entries = await readdir(messagesPath, { withFileTypes: true });
5062
- const langDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
5063
- const jsonTemplate = join2(templatePath, "component.json");
5064
- if (!existsSync2(jsonTemplate)) {
5065
- console.error("\u274C Template component.json not found:", jsonTemplate);
5066
- return;
5067
- }
5068
- const jsonContent = await readFile2(jsonTemplate, "utf-8");
5069
- const parsed = JSON.parse(jsonContent);
5070
- for (const locale of langDirs) {
5071
- const localeDir = join2(messagesPath, locale);
5072
- if (!existsSync2(localeDir) || !statSync(localeDir).isDirectory())
5073
- continue;
5074
- let jsonTarget;
5075
- if (pageScope) {
5076
- jsonTarget = join2(messagesPath, locale, `${pageScope}.json`);
5077
- } else {
5078
- jsonTarget = join2(messagesPath, locale, `_global_ui.json`);
5079
- }
5080
- let current = {};
5081
- if (existsSync2(jsonTarget)) {
5082
- const jsonFile = await readFile2(jsonTarget, "utf-8");
5083
- current = JSON.parse(jsonFile);
5084
- }
5085
- current[componentNameUpper] = parsed;
5086
- await writeFile(jsonTarget, JSON.stringify(current, null, 2));
5087
- console.log(`\uD83D\uDCC4 File updated: ${jsonTarget}`);
5527
+ return target;
5528
+ }
5529
+ async function assertSafeTarget(root, target, fs) {
5530
+ const safeTarget = resolveInside(root, path6.relative(root, target));
5531
+ const relativeSegments = path6.relative(path6.resolve(root), safeTarget).split(path6.sep);
5532
+ let current = path6.resolve(root);
5533
+ for (const segment of relativeSegments) {
5534
+ if (!segment)
5535
+ continue;
5536
+ current = path6.join(current, segment);
5537
+ const entry = await fs.inspect(current);
5538
+ if (entry?.isSymbolicLink) {
5539
+ throw new CliError(`Symbolic links are forbidden in project paths: ${current}`, { code: "UNSAFE_PATH" });
5088
5540
  }
5089
- } else {
5090
- console.log("\u2139\uFE0F Skipping translation entries; next-intl not enabled.");
5091
5541
  }
5092
- console.log(`\u2705 Component "${componentNameUpper}" added ${pageScope ? `to page ${pageScope}` : "globally"}${useI18n ? " with localized messages" : ""}.`);
5542
+ return safeTarget;
5543
+ }
5544
+ function normalizeImportAlias(value) {
5545
+ if (!/^[A-Za-z@~][A-Za-z0-9@~_-]*\/\*$/.test(value)) {
5546
+ throw new CliError('Invalid import alias: expected a prefix followed by "/*" (for example "@/*" or "@core/*").', { code: "INVALID_ARGUMENT" });
5547
+ }
5548
+ return value;
5093
5549
  }
5094
5550
 
5095
- // src/lib/addPage.ts
5096
- var import_prompts2 = __toESM(require_prompts3(), 1);
5097
- import { join as join3 } from "path";
5098
- import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2, readdir as readdir2 } from "fs/promises";
5099
- import { existsSync as existsSync3, statSync as statSync2 } from "fs";
5100
- async function addPage(args) {
5101
- let pageName = args[1];
5102
- if (!pageName || pageName.startsWith("-")) {
5103
- const response = await import_prompts2.default.prompt({
5551
+ // src/lib/addApi.ts
5552
+ var addApi = async (args, context) => {
5553
+ let apiName = args[1];
5554
+ if (!apiName || apiName.startsWith("-")) {
5555
+ if (context.outputMode === "json") {
5556
+ throw new CliError("API route name is required in JSON mode.", {
5557
+ code: "INTERACTIVE_INPUT_REQUIRED",
5558
+ hint: "Pass the API route name after addapi."
5559
+ });
5560
+ }
5561
+ const response = await context.prompt({
5104
5562
  type: "text",
5105
- name: "pageName",
5106
- message: "\uD83D\uDCDD Page name to add:",
5107
- validate: (name) => name ? true : "Page name is required"
5563
+ name: "apiName",
5564
+ message: "API route name to add:",
5565
+ validate: (name) => name ? true : "API route name is required"
5108
5566
  });
5109
- pageName = response.pageName;
5110
- }
5111
- let parentName = null;
5112
- let childName = null;
5113
- if (pageName.includes(".")) {
5114
- [parentName, childName] = pageName.split(".");
5115
- }
5116
- let shortFlags = args.find((arg) => /^-[A-Za-z]+$/.test(arg));
5117
- let longFlags = new Set(args.filter((a) => a.startsWith("--")));
5118
- const flags = new Set;
5119
- if (!shortFlags && Array.from(longFlags).length === 0) {
5120
- shortFlags = "-LPl";
5121
- }
5122
- if (shortFlags) {
5123
- for (const char of shortFlags.slice(1)) {
5124
- switch (char) {
5125
- case "L":
5126
- flags.add("layout");
5127
- break;
5128
- case "P":
5129
- flags.add("page");
5130
- break;
5131
- case "l":
5132
- flags.add("loading");
5133
- break;
5134
- case "n":
5135
- flags.add("not-found");
5136
- break;
5137
- case "e":
5138
- flags.add("error");
5139
- break;
5140
- case "g":
5141
- flags.add("global-error");
5142
- break;
5143
- case "r":
5144
- flags.add("route");
5145
- break;
5146
- case "t":
5147
- flags.add("template");
5148
- break;
5149
- case "d":
5150
- flags.add("default");
5151
- break;
5152
- }
5153
- }
5154
- }
5155
- for (const flag of [
5156
- "layout",
5157
- "page",
5158
- "loading",
5159
- "not-found",
5160
- "error",
5161
- "global-error",
5162
- "route",
5163
- "template",
5164
- "default"
5165
- ]) {
5166
- if (longFlags.has("--" + flag))
5167
- flags.add(flag);
5168
- }
5169
- const config = await loadConfig();
5170
- if (!config) {
5171
- console.error("\u274C Configuration file cnp.config.json not found. Run this command from the project root.");
5172
- return;
5173
- }
5174
- const useI18n = !!config.useI18n;
5175
- const srcSegments = ["src", "app"];
5176
- if (useI18n)
5177
- srcSegments.push("[locale]");
5178
- const srcPath = join3(process.cwd(), ...srcSegments);
5179
- if (!existsSync3(srcPath)) {
5180
- console.error(`\u274C Expected directory not found: ${srcPath}`);
5181
- return;
5182
- }
5183
- let messagesPath = null;
5184
- let locales = [];
5185
- if (useI18n) {
5186
- messagesPath = join3(process.cwd(), "messages");
5187
- if (!existsSync3(messagesPath)) {
5188
- console.error("\u274C Messages directory missing. Ensure i18n was configured.");
5189
- return;
5567
+ apiName = String(response.apiName ?? "");
5568
+ if (!apiName) {
5569
+ context.operations.record({
5570
+ action: "cancelled",
5571
+ resource: "command",
5572
+ role: "api-creation",
5573
+ scope: "project",
5574
+ path: "."
5575
+ });
5576
+ return commandResult(context, {
5577
+ command: "addapi",
5578
+ summary: "API route creation was cancelled.",
5579
+ projectRoot: context.cwd,
5580
+ status: "cancelled"
5581
+ });
5190
5582
  }
5191
- const entries = await readdir2(messagesPath, { withFileTypes: true });
5192
- locales = entries.filter((e) => e.isDirectory()).map((e) => e.name);
5193
- }
5194
- const templatePath = join3(new URL("..", import.meta.url).pathname, "templates", "Page");
5195
- let uiPageDir, localePagePath, jsonFileName;
5196
- if (parentName && childName) {
5197
- uiPageDir = join3(process.cwd(), "src", "ui", parentName, childName);
5198
- localePagePath = join3(srcPath, parentName, childName);
5199
- jsonFileName = parentName;
5200
- } else {
5201
- uiPageDir = join3(process.cwd(), "src", "ui", pageName);
5202
- localePagePath = join3(srcPath, pageName);
5203
- jsonFileName = pageName;
5204
- }
5205
- if (!existsSync3(uiPageDir)) {
5206
- await mkdir2(uiPageDir, { recursive: true });
5207
- }
5208
- const uiPageFile = join3(uiPageDir, "page-ui.tsx");
5209
- const uiPageTemplate = join3(templatePath, "page-ui.tsx");
5210
- if (existsSync3(uiPageTemplate)) {
5211
- let uiContent = await readFile3(uiPageTemplate, "utf-8");
5212
- uiContent = uiContent.replace(/template/g, childName || pageName).replace(/Template/g, capitalize(childName || pageName));
5213
- await writeFile2(uiPageFile, uiContent);
5214
- console.log(`\uD83D\uDCC4 File created: ${uiPageFile}`);
5215
- } else {
5216
- console.warn("\u26A0\uFE0F Missing template file: page-ui.tsx at path:", uiPageTemplate);
5217
5583
  }
5218
- if (!existsSync3(localePagePath)) {
5219
- await mkdir2(localePagePath, { recursive: true });
5584
+ const apiSegments = parseLogicalName(apiName, "API route name");
5585
+ const config = await loadConfig(context);
5586
+ if (!config) {
5587
+ throw new CliError("Configuration file cnp.config.json was not found.", {
5588
+ code: "CONFIG_NOT_FOUND",
5589
+ hint: "Run this command from the generated project root."
5590
+ });
5220
5591
  }
5221
- for (const flag of flags) {
5222
- const filename = toFileName(flag);
5223
- const src = join3(templatePath, filename);
5224
- const dst = join3(localePagePath, filename);
5225
- if (!existsSync3(src)) {
5226
- console.warn(`\u26A0\uFE0F Missing template file: ${filename} at path: ${src}`);
5227
- continue;
5228
- }
5229
- const content = await readFile3(src, "utf-8");
5230
- const replaced = content.replace(/template/g, childName || pageName).replace(/Template/g, capitalize(childName || pageName));
5231
- await writeFile2(dst, replaced);
5232
- console.log(`\uD83D\uDCC4 File created: ${dst}`);
5592
+ const apiDir = join2(context.cwd, "src", "app", "api", ...apiSegments);
5593
+ await assertSafeTarget(context.cwd, apiDir, context.fs);
5594
+ const gateway = new MutationGateway(context, context.cwd);
5595
+ const templateDir = join2(context.packageRoot, "templates", "Api");
5596
+ const routeTemplate = join2(templateDir, "route.ts");
5597
+ const routePath = join2(apiDir, "route.ts");
5598
+ if (!context.fs.exists(routeTemplate)) {
5599
+ throw new CliError("API template route.ts was not found.", {
5600
+ code: "TEMPLATE_MISSING",
5601
+ scope: "package",
5602
+ path: "templates/Api/route.ts"
5603
+ });
5233
5604
  }
5234
- if (useI18n && messagesPath) {
5235
- const jsonTemplate = join3(templatePath, "page.json");
5236
- if (!existsSync3(jsonTemplate)) {
5237
- console.warn("\u26A0\uFE0F Missing template: page.json at path:", jsonTemplate);
5238
- } else {
5239
- const content = await readFile3(jsonTemplate, "utf-8");
5240
- const replaced = content.replace(/template/g, childName || pageName).replace(/Template/g, capitalize(childName || pageName));
5241
- for (const locale of locales) {
5242
- const localeDir = join3(messagesPath, locale);
5243
- if (!existsSync3(localeDir) || !statSync2(localeDir).isDirectory())
5244
- continue;
5245
- const jsonTarget = join3(messagesPath, locale, `${jsonFileName}.json`);
5246
- let current = {};
5247
- if (existsSync3(jsonTarget)) {
5248
- const jsonFile = await readFile3(jsonTarget, "utf-8");
5249
- try {
5250
- current = JSON.parse(jsonFile);
5251
- } catch {
5252
- current = {};
5253
- }
5254
- }
5255
- if (parentName && childName) {
5256
- current[childName] = JSON.parse(replaced);
5257
- } else {
5258
- current = JSON.parse(replaced);
5259
- }
5260
- await writeFile2(jsonTarget, JSON.stringify(current, null, 2));
5261
- console.log(`\uD83D\uDCC4 File created: ${jsonTarget}`);
5605
+ await gateway.mkdir(apiDir, {
5606
+ role: "api-directory",
5607
+ resource: "directory"
5608
+ });
5609
+ const content = (await context.fs.readText(routeTemplate)).replace(/template/g, apiName);
5610
+ const action = await gateway.write(routePath, content, {
5611
+ role: "api-route",
5612
+ preserveExisting: true
5613
+ });
5614
+ return commandResult(context, {
5615
+ command: "addapi",
5616
+ summary: action === "unchanged" ? `API route "${apiName}" already exists and was preserved.` : `Added API route "${apiName}".`,
5617
+ projectRoot: context.cwd,
5618
+ nextSteps: action === "unchanged" ? [] : [
5619
+ {
5620
+ kind: "review",
5621
+ required: true,
5622
+ message: "Replace the example response and review validation and authentication.",
5623
+ paths: [{ scope: "project", path: gateway.path(routePath) }]
5624
+ },
5625
+ {
5626
+ kind: "run-checks",
5627
+ required: true,
5628
+ message: "Run the project checks.",
5629
+ paths: [],
5630
+ commands: ["bun run check", "npm run check", "pnpm run check"]
5262
5631
  }
5263
- }
5264
- } else {
5265
- console.log("\u2139\uFE0F Skipping translation templates; next-intl not enabled.");
5266
- }
5267
- console.log(`\u2705 Page "${pageName}" with templates added${useI18n ? " for each locale" : ""}.`);
5268
- }
5632
+ ]
5633
+ });
5634
+ };
5269
5635
 
5270
- // src/lib/rmPage.ts
5271
- var import_prompts3 = __toESM(require_prompts3(), 1);
5272
- import { join as join4 } from "path";
5273
- import { writeFile as writeFile3, readdir as readdir3 } from "fs/promises";
5274
- import { existsSync as existsSync4 } from "fs";
5275
- async function rmPage(args) {
5276
- let pageName = args[1];
5277
- if (!pageName || pageName.startsWith("-")) {
5278
- const response = await import_prompts3.default.prompt({
5279
- type: "text",
5280
- name: "pageName",
5281
- message: "\uD83D\uDDD1\uFE0F Page name to remove:",
5282
- validate: (name) => name ? true : "Page name is required"
5636
+ // src/lib/addComponent.ts
5637
+ import { join as join3 } from "path";
5638
+ var addComponent = async (args, context) => {
5639
+ let componentName = args[1];
5640
+ const pageIndex = args.findIndex((argument) => argument === "-P" || argument === "--page");
5641
+ const pageScope = pageIndex >= 0 ? args[pageIndex + 1] : undefined;
5642
+ if (pageIndex >= 0 && !pageScope) {
5643
+ throw new CliError("The --page option requires a page name.", {
5644
+ code: "INVALID_ARGUMENT"
5283
5645
  });
5284
- pageName = response.pageName;
5285
- }
5286
- const messagesPath = join4(process.cwd(), "messages");
5287
- const entries = await readdir3(messagesPath, { withFileTypes: true });
5288
- const langDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
5289
- for (const locale of langDirs) {
5290
- const jsonTarget = join4(messagesPath, locale, `${pageName}.json`);
5291
- if (existsSync4(jsonTarget)) {
5292
- await writeFile3(jsonTarget, "");
5293
- await import("child_process").then((cp) => cp.execSync(`rm -f '${jsonTarget}'`));
5294
- console.log(`\uD83D\uDDD1\uFE0F Deleted: ${jsonTarget}`);
5295
- }
5296
- }
5297
- const uiPageDir = join4(process.cwd(), "src", "ui", pageName);
5298
- if (existsSync4(uiPageDir)) {
5299
- await import("child_process").then((cp) => cp.execSync(`rm -rf '${uiPageDir}'`));
5300
- console.log(`\uD83D\uDDD1\uFE0F Deleted: ${uiPageDir}`);
5301
- }
5302
- const appLocaleDir = join4(process.cwd(), "src", "app", "[locale]", pageName);
5303
- if (existsSync4(appLocaleDir)) {
5304
- await import("child_process").then((cp) => cp.execSync(`rm -rf '${appLocaleDir}'`));
5305
- console.log(`\uD83D\uDDD1\uFE0F Deleted: ${appLocaleDir}`);
5306
- }
5307
- console.log(`\u2705 Page "${pageName}" deleted.`);
5308
- }
5309
-
5310
- // src/lib/addLib.ts
5311
- var import_prompts4 = __toESM(require_prompts3(), 1);
5312
- import { join as join5 } from "path";
5313
- import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
5314
- import { existsSync as existsSync5 } from "fs";
5315
- async function addLib(args) {
5316
- let libArg = args[1];
5317
- if (!libArg || libArg.startsWith("-")) {
5318
- const response = await import_prompts4.default.prompt({
5646
+ }
5647
+ if (!componentName || componentName.startsWith("-")) {
5648
+ if (context.outputMode === "json") {
5649
+ throw new CliError("Component name is required in JSON mode.", {
5650
+ code: "INTERACTIVE_INPUT_REQUIRED",
5651
+ hint: "Pass the component name after addcomponent."
5652
+ });
5653
+ }
5654
+ const response = await context.prompt({
5319
5655
  type: "text",
5320
- name: "libArg",
5321
- message: "\uD83D\uDCE6 Lib name to add:",
5322
- validate: (name) => name ? true : "Lib name is required"
5656
+ name: "componentName",
5657
+ message: "Component name to add:",
5658
+ validate: (name) => name ? true : "Component name is required"
5323
5659
  });
5324
- libArg = response.libArg;
5660
+ componentName = String(response.componentName ?? "");
5661
+ if (!componentName) {
5662
+ context.operations.record({
5663
+ action: "cancelled",
5664
+ resource: "command",
5665
+ role: "component-creation",
5666
+ scope: "project",
5667
+ path: "."
5668
+ });
5669
+ return commandResult(context, {
5670
+ command: "addcomponent",
5671
+ summary: "Component creation was cancelled.",
5672
+ projectRoot: context.cwd,
5673
+ status: "cancelled"
5674
+ });
5675
+ }
5325
5676
  }
5326
- let libName = libArg;
5327
- let fileName = null;
5328
- if (libArg.includes(".")) {
5329
- [libName, fileName] = libArg.split(".");
5677
+ const componentSegments = parseLogicalName(componentName, "component name");
5678
+ if (componentSegments.length !== 1) {
5679
+ throw new CliError("Component names must contain exactly one segment.", {
5680
+ code: "INVALID_ARGUMENT"
5681
+ });
5330
5682
  }
5331
- const config = await loadConfig();
5683
+ const pageSegments = pageScope ? parseLogicalName(pageScope, "page name") : [];
5684
+ const config = await loadConfig(context);
5332
5685
  if (!config) {
5333
- console.error("\u274C Configuration file cnp.config.json not found. Run this command from the project root.");
5334
- return;
5686
+ throw new CliError("Configuration file cnp.config.json was not found.", {
5687
+ code: "CONFIG_NOT_FOUND",
5688
+ hint: "Run this command from the generated project root."
5689
+ });
5335
5690
  }
5336
- const libDir = join5(process.cwd(), "src", "lib", libName);
5337
- if (!existsSync5(libDir)) {
5338
- await mkdir3(libDir, { recursive: true });
5691
+ const componentNameUpper = capitalize(toIdentifier(componentName));
5692
+ const templateRoot = join3(context.packageRoot, "templates", "Component");
5693
+ const componentTemplate = join3(templateRoot, "Component.tsx");
5694
+ const messagesTemplate = join3(templateRoot, "component.json");
5695
+ if (!context.fs.exists(componentTemplate) || config.useI18n && !context.fs.exists(messagesTemplate)) {
5696
+ throw new CliError("Required component template files were not found.", {
5697
+ code: "TEMPLATE_MISSING",
5698
+ scope: "package",
5699
+ path: "templates/Component"
5700
+ });
5339
5701
  }
5340
- const templateDir = join5(new URL("..", import.meta.url).pathname, "templates", "Lib");
5341
- const indexTemplate = join5(templateDir, "index.ts");
5342
- const fileTemplate = join5(templateDir, "item.ts");
5343
- const indexPath = join5(libDir, "index.ts");
5344
- if (!existsSync5(indexPath)) {
5345
- if (existsSync5(indexTemplate)) {
5346
- const content = await readFile4(indexTemplate, "utf-8");
5347
- await writeFile4(indexPath, content);
5348
- } else {
5349
- await writeFile4(indexPath, `export {}
5350
- `);
5702
+ const targetDirectory = pageScope ? join3(context.cwd, "src", "ui", ...pageSegments) : join3(context.cwd, "src", "ui", "_global");
5703
+ await assertSafeTarget(context.cwd, targetDirectory, context.fs);
5704
+ const componentFile = join3(targetDirectory, `${componentNameUpper}.tsx`);
5705
+ const translationNamespace = pageScope ?? "_global_ui";
5706
+ const componentContent = (await context.fs.readText(componentTemplate)).replace(/Component/g, componentNameUpper).replace(/componentPage/g, translationNamespace);
5707
+ const preparedMessages = [];
5708
+ if (config.useI18n) {
5709
+ const messagesRoot = join3(context.cwd, "messages");
5710
+ if (!context.fs.exists(messagesRoot)) {
5711
+ throw new CliError("The messages directory was not found.", {
5712
+ code: "CONFIG_NOT_FOUND",
5713
+ scope: "project",
5714
+ path: "messages"
5715
+ });
5351
5716
  }
5352
- console.log(`\uD83D\uDCC4 File created: ${indexPath}`);
5353
- }
5354
- if (fileName) {
5355
- const filePath = join5(libDir, `${fileName}.ts`);
5356
- if (!existsSync5(filePath)) {
5357
- if (existsSync5(fileTemplate)) {
5358
- let content = await readFile4(fileTemplate, "utf-8");
5359
- content = content.replace(/template/g, fileName).replace(/Template/g, capitalize(fileName));
5360
- await writeFile4(filePath, content);
5361
- } else {
5362
- await writeFile4(filePath, `export function ${fileName}() {
5363
- // TODO: implement
5364
- }
5365
- `);
5366
- }
5367
- console.log(`\uD83D\uDCC4 File created: ${filePath}`);
5717
+ const templateMessages = JSON.parse(await context.fs.readText(messagesTemplate));
5718
+ const locales = (await context.fs.list(messagesRoot)).filter((entry) => entry.isDirectory).map((entry) => entry.name).sort();
5719
+ if (locales.length === 0) {
5720
+ throw new CliError("No locale directories were found.", {
5721
+ code: "CONFIG_NOT_FOUND",
5722
+ scope: "project",
5723
+ path: "messages"
5724
+ });
5368
5725
  }
5369
- let indexContent = await readFile4(indexPath, "utf-8");
5370
- const importLine = `import { ${fileName} } from "./${fileName}";`;
5371
- const importRegex = new RegExp(`import\\s*{\\s*${fileName}\\s*}\\s*from\\s*"\\./${fileName}";`);
5372
- const exportRegex = /export\s*{([^}]*)}/m;
5373
- const imports = [];
5374
- const exportsSet = [];
5375
- for (const line of indexContent.split(`
5376
- `)) {
5377
- if (line.startsWith("import")) {
5378
- imports.push(line);
5379
- } else if (line.startsWith("export")) {
5380
- const match = line.match(exportRegex);
5381
- if (match && match[1]) {
5382
- exportsSet.push(...match[1].split(",").map((s) => s.trim()).filter(Boolean));
5726
+ for (const locale of locales) {
5727
+ const messageFile = pageScope ? pageSegments[0] : "_global_ui";
5728
+ const target = join3(messagesRoot, locale, `${messageFile}.json`);
5729
+ let data = {};
5730
+ if (context.fs.exists(target)) {
5731
+ try {
5732
+ data = JSON.parse(await context.fs.readText(target));
5733
+ } catch {
5734
+ throw new CliError(`Invalid JSON in messages/${locale}/${messageFile}.json.`, {
5735
+ code: "FILESYSTEM_ERROR",
5736
+ scope: "project",
5737
+ path: `messages/${locale}/${messageFile}.json`
5738
+ });
5383
5739
  }
5384
5740
  }
5741
+ let container = data;
5742
+ if (pageSegments.length > 1) {
5743
+ const child = pageSegments[1];
5744
+ const current = data[child];
5745
+ if (current !== undefined && (!current || typeof current !== "object" || Array.isArray(current))) {
5746
+ throw new CliError(`Translation namespace ${pageScope} is not an object in ${locale}.`, {
5747
+ code: "INVALID_ARGUMENT",
5748
+ scope: "project",
5749
+ path: `messages/${locale}/${messageFile}.json`
5750
+ });
5751
+ }
5752
+ if (!current)
5753
+ data[child] = {};
5754
+ container = data[child];
5755
+ }
5756
+ const exists = Object.hasOwn(container, componentNameUpper);
5757
+ if (!exists)
5758
+ container[componentNameUpper] = templateMessages;
5759
+ preparedMessages.push({
5760
+ locale,
5761
+ target,
5762
+ exists,
5763
+ content: exists ? undefined : `${JSON.stringify(data, null, 2)}
5764
+ `
5765
+ });
5385
5766
  }
5386
- if (!imports.some((l) => importRegex.test(l))) {
5387
- imports.push(importLine);
5388
- }
5389
- if (!exportsSet.includes(fileName)) {
5390
- exportsSet.push(fileName);
5767
+ }
5768
+ const gateway = new MutationGateway(context, context.cwd);
5769
+ await gateway.mkdir(targetDirectory, {
5770
+ role: "component-directory",
5771
+ resource: "directory"
5772
+ });
5773
+ await gateway.write(componentFile, componentContent, {
5774
+ role: "ui-component",
5775
+ preserveExisting: true
5776
+ });
5777
+ for (const item of preparedMessages) {
5778
+ if (item.exists) {
5779
+ gateway.unchanged(item.target, {
5780
+ role: "translation-messages",
5781
+ detail: {
5782
+ locale: item.locale,
5783
+ key: `${translationNamespace}.${componentNameUpper}`
5784
+ }
5785
+ });
5786
+ } else {
5787
+ await gateway.write(item.target, item.content, {
5788
+ role: "translation-messages",
5789
+ detail: {
5790
+ locale: item.locale,
5791
+ key: `${translationNamespace}.${componentNameUpper}`
5792
+ }
5793
+ });
5391
5794
  }
5392
- indexContent = imports.join(`
5393
- `) + `
5394
-
5395
- export { ` + exportsSet.join(", ") + ` };
5396
- `;
5397
- await writeFile4(indexPath, indexContent);
5398
- console.log(`\u270F\uFE0F Updated index: ${indexPath}`);
5399
5795
  }
5400
- console.log(`\u2705 Lib "${libName}"${fileName ? ` with module ${fileName}` : ""} added.`);
5401
- }
5796
+ const mutated = context.operations.snapshot().some((event) => event.action === "created" || event.action === "updated");
5797
+ return commandResult(context, {
5798
+ command: "addcomponent",
5799
+ summary: mutated ? `Added component "${componentNameUpper}" ${pageScope ? `to page "${pageScope}"` : "globally"}.` : `Component "${componentNameUpper}" already exists and was preserved.`,
5800
+ projectRoot: context.cwd,
5801
+ nextSteps: mutated ? [
5802
+ {
5803
+ kind: "review",
5804
+ required: true,
5805
+ message: "Review the generated component and its localized messages.",
5806
+ paths: [
5807
+ { scope: "project", path: gateway.path(componentFile) },
5808
+ ...preparedMessages.map((item) => ({
5809
+ scope: "project",
5810
+ path: gateway.path(item.target)
5811
+ }))
5812
+ ]
5813
+ },
5814
+ {
5815
+ kind: "run-checks",
5816
+ required: true,
5817
+ message: "Run the project checks.",
5818
+ paths: [],
5819
+ commands: ["bun run check", "npm run check", "pnpm run check"]
5820
+ }
5821
+ ] : []
5822
+ });
5823
+ };
5402
5824
 
5403
- // src/lib/addApi.ts
5404
- var import_prompts5 = __toESM(require_prompts3(), 1);
5405
- import { join as join6 } from "path";
5406
- import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
5407
- import { existsSync as existsSync6 } from "fs";
5408
- async function addApi(args) {
5409
- let apiName = args[1];
5410
- if (!apiName || apiName.startsWith("-")) {
5411
- const response = await import_prompts5.default.prompt({
5412
- type: "text",
5413
- name: "apiName",
5414
- message: "\uD83D\uDD0C API route name to add:",
5415
- validate: (name) => name ? true : "API route name is required"
5825
+ // src/lib/addLanguage.ts
5826
+ import { join as join4 } from "path";
5827
+ function generateLocales() {
5828
+ const names = new Intl.DisplayNames(["en"], { type: "language" });
5829
+ const locales = [];
5830
+ for (let first = 0;first < 26; first++) {
5831
+ for (let second = 0;second < 26; second++) {
5832
+ const code = String.fromCharCode(97 + first, 97 + second);
5833
+ try {
5834
+ const name = names.of(code);
5835
+ if (name && name.toLowerCase() !== code)
5836
+ locales.push(code);
5837
+ } catch {}
5838
+ }
5839
+ }
5840
+ return locales.sort();
5841
+ }
5842
+ async function listFiles(context, root) {
5843
+ const files = [];
5844
+ async function visit(directory, relative = "") {
5845
+ for (const entry of await context.fs.list(directory)) {
5846
+ const next = relative ? `${relative}/${entry.name}` : entry.name;
5847
+ if (entry.isSymbolicLink) {
5848
+ throw new CliError(`Symbolic link found in locale source: ${next}`, {
5849
+ code: "UNSAFE_PATH",
5850
+ scope: "project",
5851
+ path: next
5852
+ });
5853
+ }
5854
+ if (entry.isDirectory)
5855
+ await visit(join4(directory, entry.name), next);
5856
+ else if (entry.isFile)
5857
+ files.push(next);
5858
+ }
5859
+ }
5860
+ await visit(root);
5861
+ return files.sort();
5862
+ }
5863
+ var addLanguage = async (args, context) => {
5864
+ const config = await loadConfig(context);
5865
+ if (!config?.useI18n) {
5866
+ throw new CliError("Internationalization is not enabled in this project.", {
5867
+ code: "I18N_DISABLED"
5416
5868
  });
5417
- apiName = response.apiName;
5418
5869
  }
5419
- const config = await loadConfig();
5420
- if (!config) {
5421
- console.error("\u274C Configuration file cnp.config.json not found. Run this command from the project root.");
5422
- return;
5870
+ const messagesRoot = join4(context.cwd, "messages");
5871
+ if (!context.fs.exists(messagesRoot)) {
5872
+ throw new CliError("The messages directory was not found.", {
5873
+ code: "CONFIG_NOT_FOUND",
5874
+ scope: "project",
5875
+ path: "messages"
5876
+ });
5423
5877
  }
5424
- const apiDir = join6(process.cwd(), "src", "app", "api", apiName);
5425
- if (!existsSync6(apiDir)) {
5426
- await mkdir4(apiDir, { recursive: true });
5427
- }
5428
- const templateDir = join6(new URL("..", import.meta.url).pathname, "templates", "Api");
5429
- const routeTemplate = join6(templateDir, "route.ts");
5430
- const routePath = join6(apiDir, "route.ts");
5431
- if (!existsSync6(routePath)) {
5432
- if (existsSync6(routeTemplate)) {
5433
- let content = await readFile5(routeTemplate, "utf-8");
5434
- content = content.replace(/template/g, apiName);
5435
- await writeFile5(routePath, content);
5436
- } else {
5437
- await writeFile5(routePath, `import { NextResponse } from "next/server";
5878
+ const available = generateLocales();
5879
+ let locale = args[1];
5880
+ if (!locale) {
5881
+ if (context.outputMode === "json") {
5882
+ throw new CliError("Locale is required in JSON mode.", {
5883
+ code: "INTERACTIVE_INPUT_REQUIRED",
5884
+ hint: "Pass a two-letter locale after addlanguage."
5885
+ });
5886
+ }
5887
+ const response = await context.prompt({
5888
+ type: "autocomplete",
5889
+ name: "locale",
5890
+ message: "Locale to add:",
5891
+ choices: available.map((value) => ({ title: value, value }))
5892
+ });
5893
+ locale = String(response.locale ?? "");
5894
+ if (!locale) {
5895
+ context.operations.record({
5896
+ action: "cancelled",
5897
+ resource: "command",
5898
+ role: "locale-creation",
5899
+ scope: "project",
5900
+ path: "."
5901
+ });
5902
+ return commandResult(context, {
5903
+ command: "addlanguage",
5904
+ summary: "Locale creation was cancelled.",
5905
+ projectRoot: context.cwd,
5906
+ status: "cancelled"
5907
+ });
5908
+ }
5909
+ }
5910
+ parseLogicalName(locale, "locale");
5911
+ if (!available.includes(locale)) {
5912
+ throw new CliError(`Unsupported locale code: ${locale}.`, {
5913
+ code: "INVALID_ARGUMENT",
5914
+ hint: "Use a recognized two-letter language code."
5915
+ });
5916
+ }
5917
+ const routingFile = join4(context.cwd, "src", "lib", "i18n", "routing.ts");
5918
+ const registryFile = join4(context.cwd, "src", "lib", "i18n", "messages.ts");
5919
+ if (!context.fs.exists(routingFile) || !context.fs.exists(registryFile)) {
5920
+ throw new CliError("Required i18n routing or registry file was not found.", {
5921
+ code: "CONFIG_NOT_FOUND",
5922
+ scope: "project",
5923
+ path: "src/lib/i18n"
5924
+ });
5925
+ }
5926
+ const routing = await context.fs.readText(routingFile);
5927
+ const defaultLocale = routing.match(/defaultLocale:\s*["']([^"']+)["']/)?.[1];
5928
+ if (!defaultLocale) {
5929
+ throw new CliError("The default locale could not be resolved from routing.ts.", {
5930
+ code: "CONFIG_NOT_FOUND",
5931
+ scope: "project",
5932
+ path: "src/lib/i18n/routing.ts"
5933
+ });
5934
+ }
5935
+ const sourceDirectory = join4(messagesRoot, defaultLocale);
5936
+ const sourceAggregator = join4(messagesRoot, `${defaultLocale}.ts`);
5937
+ if (!context.fs.exists(sourceDirectory)) {
5938
+ throw new CliError(`Default locale directory ${defaultLocale} was not found.`, {
5939
+ code: "CONFIG_NOT_FOUND",
5940
+ scope: "project",
5941
+ path: `messages/${defaultLocale}`
5942
+ });
5943
+ }
5944
+ if (!context.fs.exists(sourceAggregator)) {
5945
+ throw new CliError("Default locale aggregator not found.", {
5946
+ code: "CONFIG_NOT_FOUND",
5947
+ scope: "project",
5948
+ path: `messages/${defaultLocale}.ts`
5949
+ });
5950
+ }
5951
+ const sourceFiles = await listFiles(context, sourceDirectory);
5952
+ if (sourceFiles.length === 0) {
5953
+ throw new CliError(`Default locale ${defaultLocale} contains no files.`, {
5954
+ code: "CONFIG_NOT_FOUND",
5955
+ scope: "project",
5956
+ path: `messages/${defaultLocale}`
5957
+ });
5958
+ }
5959
+ const defaultAggregator = await context.fs.readText(sourceAggregator);
5960
+ const importPrefix = `./${defaultLocale}/`;
5961
+ if (!defaultAggregator.includes(importPrefix)) {
5962
+ throw new CliError(`Default aggregator does not import from ${importPrefix}.`, {
5963
+ code: "CONFIG_NOT_FOUND",
5964
+ scope: "project",
5965
+ path: `messages/${defaultLocale}.ts`
5966
+ });
5967
+ }
5968
+ const localesMatch = routing.match(/locales:\s*\[([^\]]*)\]/);
5969
+ if (!localesMatch) {
5970
+ throw new CliError("Unable to locate routing locales.", {
5971
+ code: "CONFIG_NOT_FOUND",
5972
+ scope: "project",
5973
+ path: "src/lib/i18n/routing.ts"
5974
+ });
5975
+ }
5976
+ const routingLocales = localesMatch[1].split(",").map((value) => value.trim().replace(/["']/g, "")).filter(Boolean);
5977
+ const registry = await context.fs.readText(registryFile);
5978
+ const registryMatch = registry.match(/const messages = \{([^}]*)\} as const;/);
5979
+ if (!registryMatch) {
5980
+ throw new CliError("Unable to locate the typed messages registry.", {
5981
+ code: "CONFIG_NOT_FOUND",
5982
+ scope: "project",
5983
+ path: "src/lib/i18n/messages.ts"
5984
+ });
5985
+ }
5986
+ const registeredLocales = registryMatch[1].split(",").map((value) => value.trim()).filter(Boolean);
5987
+ const targetDirectory = join4(messagesRoot, locale);
5988
+ const targetAggregator = join4(messagesRoot, `${locale}.ts`);
5989
+ const targetAggregatorExists = context.fs.exists(targetAggregator);
5990
+ const targetAggregatorContent = targetAggregatorExists ? await context.fs.readText(targetAggregator) : "";
5991
+ const state = {
5992
+ directory: context.fs.exists(targetDirectory),
5993
+ aggregator: targetAggregatorExists,
5994
+ aggregatorImportsLocale: targetAggregatorContent.includes(`./${locale}/`),
5995
+ routing: routingLocales.includes(locale),
5996
+ registry: registeredLocales.includes(locale),
5997
+ registryImport: new RegExp(`import\\s+${locale}\\s+from\\s+["']\\.\\.\\/\\.\\.\\/\\.\\.\\/messages\\/${locale}["'];`).test(registry)
5998
+ };
5999
+ const present = Object.values(state).filter(Boolean).length;
6000
+ const gateway = new MutationGateway(context, context.cwd);
6001
+ const missingLocaleFiles = state.directory ? sourceFiles.filter((relative) => !context.fs.exists(join4(targetDirectory, relative))) : sourceFiles;
6002
+ if (present === Object.keys(state).length && missingLocaleFiles.length === 0) {
6003
+ gateway.unchanged(targetDirectory, {
6004
+ role: "locale-directory",
6005
+ resource: "directory"
6006
+ });
6007
+ for (const relative of sourceFiles) {
6008
+ gateway.unchanged(join4(targetDirectory, relative), {
6009
+ role: relative.endsWith(".json") ? "translation-messages" : "locale-file",
6010
+ detail: { locale, sourceLocale: defaultLocale }
6011
+ });
6012
+ }
6013
+ gateway.unchanged(targetAggregator, { role: "locale-aggregator" });
6014
+ gateway.unchanged(routingFile, { role: "i18n-routing" });
6015
+ gateway.unchanged(registryFile, { role: "messages-registry" });
6016
+ return commandResult(context, {
6017
+ command: "addlanguage",
6018
+ summary: `Locale "${locale}" is already fully configured.`,
6019
+ projectRoot: context.cwd
6020
+ });
6021
+ }
6022
+ if (present > 0 || missingLocaleFiles.length < sourceFiles.length) {
6023
+ throw new CliError(`Locale ${locale} is only partially configured.`, {
6024
+ code: "INCONSISTENT_LOCALE",
6025
+ scope: "project",
6026
+ path: `messages/${locale}`,
6027
+ hint: `Observed state: ${JSON.stringify({ ...state, missingLocaleFiles })}.`
6028
+ });
6029
+ }
6030
+ const nextAggregator = defaultAggregator.replaceAll(importPrefix, `./${locale}/`);
6031
+ const nextRouting = routing.replace(/locales:\s*\[[^\]]*\]/, `locales: [${[...routingLocales, locale].map((value) => `"${value}"`).join(", ")}]`);
6032
+ const declarationIndex = registry.indexOf("const messages =");
6033
+ const nextRegistry = registry.slice(0, declarationIndex) + `import ${locale} from "../../../messages/${locale}";
6034
+
6035
+ ` + registry.slice(declarationIndex).replace(/const messages = \{[^}]*\} as const;/, `const messages = { ${[...registeredLocales, locale].join(", ")} } as const;`);
6036
+ await assertSafeTarget(context.cwd, targetDirectory, context.fs);
6037
+ await gateway.mkdir(targetDirectory, {
6038
+ role: "locale-directory",
6039
+ resource: "directory",
6040
+ detail: { locale, sourceLocale: defaultLocale }
6041
+ });
6042
+ for (const relative of sourceFiles) {
6043
+ const source = join4(sourceDirectory, relative);
6044
+ const target = join4(targetDirectory, relative);
6045
+ await gateway.copy(source, target, {
6046
+ role: relative.endsWith(".json") ? "translation-messages" : "locale-file",
6047
+ source: { scope: "project", path: gateway.path(source) },
6048
+ detail: { locale, sourceLocale: defaultLocale }
6049
+ });
6050
+ }
6051
+ await gateway.write(targetAggregator, nextAggregator, {
6052
+ role: "locale-aggregator"
6053
+ });
6054
+ await gateway.write(registryFile, nextRegistry, {
6055
+ role: "messages-registry"
6056
+ });
6057
+ await gateway.write(routingFile, nextRouting, { role: "i18n-routing" });
6058
+ const translationPaths = sourceFiles.filter((relative) => relative.endsWith(".json")).map((relative) => ({
6059
+ scope: "project",
6060
+ path: gateway.path(join4(targetDirectory, relative))
6061
+ }));
6062
+ return commandResult(context, {
6063
+ command: "addlanguage",
6064
+ summary: `Added locale "${locale}" by copying ${sourceFiles.length} files from "${defaultLocale}".`,
6065
+ projectRoot: context.cwd,
6066
+ nextSteps: [
6067
+ {
6068
+ kind: "translate",
6069
+ required: true,
6070
+ message: `Translate every copied message from ${defaultLocale} to ${locale}; the copied text is not ready for delivery.`,
6071
+ paths: translationPaths
6072
+ },
6073
+ {
6074
+ kind: "run-checks",
6075
+ required: true,
6076
+ message: "Run the project checks.",
6077
+ paths: [],
6078
+ commands: ["bun run check", "npm run check", "pnpm run check"]
6079
+ }
6080
+ ]
6081
+ });
6082
+ };
6083
+
6084
+ // src/lib/addLib.ts
6085
+ import { join as join5 } from "path";
6086
+ var addLib = async (args, context) => {
6087
+ let libArg = args[1];
6088
+ if (!libArg || libArg.startsWith("-")) {
6089
+ if (context.outputMode === "json") {
6090
+ throw new CliError("Library name is required in JSON mode.", {
6091
+ code: "INTERACTIVE_INPUT_REQUIRED",
6092
+ hint: "Pass library or library.module after addlib."
6093
+ });
6094
+ }
6095
+ const response = await context.prompt({
6096
+ type: "text",
6097
+ name: "libArg",
6098
+ message: "Library name to add:",
6099
+ validate: (name) => name ? true : "Library name is required"
6100
+ });
6101
+ libArg = String(response.libArg ?? "");
6102
+ if (!libArg) {
6103
+ context.operations.record({
6104
+ action: "cancelled",
6105
+ resource: "command",
6106
+ role: "library-creation",
6107
+ scope: "project",
6108
+ path: "."
6109
+ });
6110
+ return commandResult(context, {
6111
+ command: "addlib",
6112
+ summary: "Library creation was cancelled.",
6113
+ projectRoot: context.cwd,
6114
+ status: "cancelled"
6115
+ });
6116
+ }
6117
+ }
6118
+ const segments = parseLogicalName(libArg, "library name");
6119
+ if (segments.length > 2) {
6120
+ throw new CliError("Libraries support exactly library or library.module.", {
6121
+ code: "INVALID_ARGUMENT"
6122
+ });
6123
+ }
6124
+ const [libName, fileName] = segments;
6125
+ if (!await loadConfig(context)) {
6126
+ throw new CliError("Configuration file cnp.config.json was not found.", {
6127
+ code: "CONFIG_NOT_FOUND",
6128
+ hint: "Run this command from the generated project root."
6129
+ });
6130
+ }
6131
+ const libDir = join5(context.cwd, "src", "lib", libName);
6132
+ await assertSafeTarget(context.cwd, libDir, context.fs);
6133
+ const gateway = new MutationGateway(context, context.cwd);
6134
+ const templateDir = join5(context.packageRoot, "templates", "Lib");
6135
+ const indexTemplate = join5(templateDir, "index.ts");
6136
+ const itemTemplate = join5(templateDir, "item.ts");
6137
+ if (!context.fs.exists(indexTemplate) || fileName && !context.fs.exists(itemTemplate)) {
6138
+ throw new CliError("Required library template files were not found.", {
6139
+ code: "TEMPLATE_MISSING",
6140
+ scope: "package",
6141
+ path: "templates/Lib"
6142
+ });
6143
+ }
6144
+ await gateway.mkdir(libDir, {
6145
+ role: "library-directory",
6146
+ resource: "directory"
6147
+ });
6148
+ const indexPath = join5(libDir, "index.ts");
6149
+ if (!context.fs.exists(indexPath)) {
6150
+ await gateway.write(indexPath, await context.fs.readText(indexTemplate), {
6151
+ role: "library-index"
6152
+ });
6153
+ }
6154
+ let moduleAction;
6155
+ if (fileName) {
6156
+ const moduleIdentifier = toIdentifier(fileName);
6157
+ const modulePath = join5(libDir, `${fileName}.ts`);
6158
+ const moduleContent = (await context.fs.readText(itemTemplate)).replace(/template/g, moduleIdentifier).replace(/Template/g, capitalize(moduleIdentifier));
6159
+ moduleAction = await gateway.write(modulePath, moduleContent, {
6160
+ role: "library-module",
6161
+ preserveExisting: true
6162
+ });
6163
+ const currentIndex = await context.fs.readText(indexPath);
6164
+ const importLine = `import { ${moduleIdentifier} } from "./${fileName}";`;
6165
+ const importRegex = new RegExp(`import\\s*{\\s*${moduleIdentifier}\\s*}\\s*from\\s*["']\\./${fileName}["'];`);
6166
+ const exportRegex = /export\s*{([^}]*)}/m;
6167
+ const imports = currentIndex.split(`
6168
+ `).filter((line) => line.startsWith("import"));
6169
+ const exportMatch = currentIndex.match(exportRegex);
6170
+ const exportsSet = (exportMatch?.[1] ?? "").split(",").map((item) => item.trim()).filter(Boolean);
6171
+ if (!imports.some((line) => importRegex.test(line)))
6172
+ imports.push(importLine);
6173
+ if (!exportsSet.includes(moduleIdentifier))
6174
+ exportsSet.push(moduleIdentifier);
6175
+ const nextIndex = `${imports.join(`
6176
+ `)}
6177
+
6178
+ export { ${exportsSet.join(", ")} };
6179
+ `;
6180
+ await gateway.write(indexPath, nextIndex, { role: "library-index" });
6181
+ } else if (context.fs.exists(indexPath)) {
6182
+ gateway.unchanged(indexPath, { role: "library-index" });
6183
+ }
6184
+ const mutated = context.operations.snapshot().some((event) => ["created", "updated", "copied", "deleted"].includes(event.action));
6185
+ const nextSteps = [];
6186
+ if (fileName && moduleAction !== "unchanged") {
6187
+ nextSteps.push({
6188
+ kind: "review",
6189
+ required: true,
6190
+ message: "Implement and review the generated library module.",
6191
+ paths: [
6192
+ {
6193
+ scope: "project",
6194
+ path: gateway.path(join5(libDir, `${fileName}.ts`))
6195
+ }
6196
+ ]
6197
+ });
6198
+ }
6199
+ if (mutated) {
6200
+ nextSteps.push({
6201
+ kind: "run-checks",
6202
+ required: true,
6203
+ message: "Run the project checks.",
6204
+ paths: [],
6205
+ commands: ["bun run check", "npm run check", "pnpm run check"]
6206
+ });
6207
+ }
6208
+ return commandResult(context, {
6209
+ command: "addlib",
6210
+ summary: mutated ? `Added library "${libName}"${fileName ? ` with module "${fileName}"` : ""}.` : `Library "${libName}"${fileName ? ` and module "${fileName}"` : ""} already exist and were preserved.`,
6211
+ projectRoot: context.cwd,
6212
+ nextSteps
6213
+ });
6214
+ };
5438
6215
 
5439
- export async function GET() {
5440
- return NextResponse.json({ message: "Hello from ${apiName}" });
6216
+ // src/lib/addPage.ts
6217
+ import { join as join6 } from "path";
6218
+ var LONG_FLAGS = [
6219
+ "layout",
6220
+ "page",
6221
+ "loading",
6222
+ "not-found",
6223
+ "error",
6224
+ "global-error",
6225
+ "route",
6226
+ "template",
6227
+ "default"
6228
+ ];
6229
+ var SHORT_FLAGS = {
6230
+ L: "layout",
6231
+ P: "page",
6232
+ l: "loading",
6233
+ n: "not-found",
6234
+ e: "error",
6235
+ g: "global-error",
6236
+ r: "route",
6237
+ t: "template",
6238
+ d: "default"
6239
+ };
6240
+ function registerMessagesFile(content, locale, fileName) {
6241
+ const importPath = `./${locale}/${fileName}.json`;
6242
+ if (content.includes(importPath))
6243
+ return content;
6244
+ const declarationIndex = content.indexOf("const messages =");
6245
+ const registryMatch = content.match(/const messages = \{([\s\S]*?)\n\};/);
6246
+ if (declarationIndex < 0 || !registryMatch) {
6247
+ throw new CliError(`Unable to register ${fileName}.json in messages/${locale}.ts.`, {
6248
+ code: "CONFIG_NOT_FOUND",
6249
+ scope: "project",
6250
+ path: `messages/${locale}.ts`
6251
+ });
6252
+ }
6253
+ const identifier = toIdentifier(fileName);
6254
+ const importStatement = `import ${identifier} from "${importPath}";
6255
+ `;
6256
+ const nextRegistry = registryMatch[0].replace(/\n\};$/, `
6257
+ ${identifier},
6258
+ };`);
6259
+ return content.slice(0, declarationIndex) + importStatement + content.slice(declarationIndex).replace(registryMatch[0], nextRegistry);
5441
6260
  }
5442
- `);
6261
+ function selectedFlags(args) {
6262
+ const selected = new Set;
6263
+ const optionArguments = args.slice(2).filter((argument) => argument.startsWith("-"));
6264
+ if (optionArguments.length === 0)
6265
+ return new Set(["layout", "page", "loading"]);
6266
+ for (const argument of optionArguments) {
6267
+ if (argument.startsWith("--")) {
6268
+ const flag = argument.slice(2);
6269
+ if (!LONG_FLAGS.includes(flag)) {
6270
+ throw new CliError(`Unknown addpage option: ${argument}.`, {
6271
+ code: "INVALID_ARGUMENT"
6272
+ });
6273
+ }
6274
+ selected.add(flag);
6275
+ continue;
6276
+ }
6277
+ for (const character of argument.slice(1)) {
6278
+ const flag = SHORT_FLAGS[character];
6279
+ if (!flag) {
6280
+ throw new CliError(`Unknown addpage short option: -${character}.`, {
6281
+ code: "INVALID_ARGUMENT"
6282
+ });
6283
+ }
6284
+ selected.add(flag);
6285
+ }
6286
+ }
6287
+ return selected;
6288
+ }
6289
+ var addPage = async (args, context) => {
6290
+ let logicalName = args[1];
6291
+ if (!logicalName || logicalName.startsWith("-")) {
6292
+ if (context.outputMode === "json") {
6293
+ throw new CliError("Page name is required in JSON mode.", {
6294
+ code: "INTERACTIVE_INPUT_REQUIRED",
6295
+ hint: "Pass a simple or Parent.Child page name after addpage."
6296
+ });
6297
+ }
6298
+ const response = await context.prompt({
6299
+ type: "text",
6300
+ name: "pageName",
6301
+ message: "Page name to add:",
6302
+ validate: (name) => name ? true : "Page name is required"
6303
+ });
6304
+ logicalName = String(response.pageName ?? "");
6305
+ if (!logicalName) {
6306
+ context.operations.record({
6307
+ action: "cancelled",
6308
+ resource: "command",
6309
+ role: "page-creation",
6310
+ scope: "project",
6311
+ path: "."
6312
+ });
6313
+ return commandResult(context, {
6314
+ command: "addpage",
6315
+ summary: "Page creation was cancelled.",
6316
+ projectRoot: context.cwd,
6317
+ status: "cancelled"
6318
+ });
6319
+ }
6320
+ }
6321
+ const pageSegments = parseLogicalName(logicalName, "page name");
6322
+ if (pageSegments.length > 2) {
6323
+ throw new CliError("Nested pages support exactly Parent.Child.", {
6324
+ code: "INVALID_ARGUMENT"
6325
+ });
6326
+ }
6327
+ const flags = selectedFlags(args);
6328
+ const config = await loadConfig(context);
6329
+ if (!config) {
6330
+ throw new CliError("Configuration file cnp.config.json was not found.", {
6331
+ code: "CONFIG_NOT_FOUND",
6332
+ hint: "Run this command from the generated project root."
6333
+ });
6334
+ }
6335
+ const useI18n = Boolean(config.useI18n);
6336
+ const aliasPrefix = configuredAliasPrefix(config);
6337
+ const appRoot = join6(context.cwd, "src", "app", ...useI18n ? ["[locale]"] : []);
6338
+ if (!context.fs.exists(appRoot)) {
6339
+ throw new CliError("The expected App Router directory was not found.", {
6340
+ code: "CONFIG_NOT_FOUND",
6341
+ scope: "project",
6342
+ path: useI18n ? "src/app/[locale]" : "src/app"
6343
+ });
6344
+ }
6345
+ const [parentName, childName] = pageSegments.length === 2 ? pageSegments : [undefined, undefined];
6346
+ const leafName = childName ?? pageSegments[0];
6347
+ const pageIdentifier = capitalize(toIdentifier(leafName));
6348
+ const jsonFileName = parentName ?? leafName;
6349
+ const uiDirectory = join6(context.cwd, "src", "ui", ...pageSegments);
6350
+ const routeDirectory = join6(appRoot, ...pageSegments);
6351
+ await assertSafeTarget(context.cwd, uiDirectory, context.fs);
6352
+ await assertSafeTarget(context.cwd, routeDirectory, context.fs);
6353
+ const templateRoot = join6(context.packageRoot, "templates", "Page");
6354
+ const uiTemplate = join6(templateRoot, "page-ui.tsx");
6355
+ const routeTemplates = [...flags].map((flag) => ({
6356
+ flag,
6357
+ source: join6(templateRoot, toFileName(flag))
6358
+ }));
6359
+ const messagesTemplate = join6(templateRoot, "page.json");
6360
+ const requiredTemplates = [
6361
+ uiTemplate,
6362
+ ...routeTemplates.map((entry) => entry.source)
6363
+ ];
6364
+ if (useI18n)
6365
+ requiredTemplates.push(messagesTemplate);
6366
+ const missing = requiredTemplates.find((target) => !context.fs.exists(target));
6367
+ if (missing) {
6368
+ throw new CliError(`Required page template was not found: ${missing}.`, {
6369
+ code: "TEMPLATE_MISSING",
6370
+ scope: "package",
6371
+ path: missing.replace(`${context.packageRoot}/`, "")
6372
+ });
6373
+ }
6374
+ const translationNamespace = parentName ? `${parentName}.${childName}` : leafName;
6375
+ const templateJson = useI18n ? JSON.parse((await context.fs.readText(messagesTemplate)).replace(/template/g, leafName).replace(/Template/g, capitalize(leafName))) : undefined;
6376
+ const preparedLocales = [];
6377
+ if (useI18n) {
6378
+ const messagesRoot = join6(context.cwd, "messages");
6379
+ if (!context.fs.exists(messagesRoot)) {
6380
+ throw new CliError("The messages directory was not found.", {
6381
+ code: "CONFIG_NOT_FOUND",
6382
+ scope: "project",
6383
+ path: "messages"
6384
+ });
6385
+ }
6386
+ const locales = (await context.fs.list(messagesRoot)).filter((entry) => entry.isDirectory).map((entry) => entry.name).sort();
6387
+ if (locales.length === 0) {
6388
+ throw new CliError("No locale directories were found.", {
6389
+ code: "CONFIG_NOT_FOUND",
6390
+ scope: "project",
6391
+ path: "messages"
6392
+ });
6393
+ }
6394
+ for (const locale of locales) {
6395
+ const target = join6(messagesRoot, locale, `${jsonFileName}.json`);
6396
+ const aggregator = join6(messagesRoot, `${locale}.ts`);
6397
+ if (!context.fs.exists(aggregator)) {
6398
+ throw new CliError(`Locale aggregator messages/${locale}.ts was not found.`, {
6399
+ code: "CONFIG_NOT_FOUND",
6400
+ scope: "project",
6401
+ path: `messages/${locale}.ts`
6402
+ });
6403
+ }
6404
+ let data = {};
6405
+ const targetExists = context.fs.exists(target);
6406
+ if (targetExists) {
6407
+ try {
6408
+ data = JSON.parse(await context.fs.readText(target));
6409
+ } catch {
6410
+ throw new CliError(`Invalid JSON in messages/${locale}/${jsonFileName}.json.`, {
6411
+ code: "FILESYSTEM_ERROR",
6412
+ scope: "project",
6413
+ path: `messages/${locale}/${jsonFileName}.json`
6414
+ });
6415
+ }
6416
+ }
6417
+ const messageExists = parentName ? Object.hasOwn(data, childName) : targetExists;
6418
+ if (!messageExists) {
6419
+ if (parentName)
6420
+ data[childName] = templateJson;
6421
+ else
6422
+ data = templateJson;
6423
+ }
6424
+ const aggregatorCurrent = await context.fs.readText(aggregator);
6425
+ preparedLocales.push({
6426
+ locale,
6427
+ target,
6428
+ targetContent: messageExists ? undefined : `${JSON.stringify(data, null, 2)}
6429
+ `,
6430
+ messageExists,
6431
+ aggregator,
6432
+ aggregatorContent: registerMessagesFile(aggregatorCurrent, locale, jsonFileName)
6433
+ });
6434
+ }
6435
+ }
6436
+ const gateway = new MutationGateway(context, context.cwd);
6437
+ await gateway.mkdir(uiDirectory, {
6438
+ role: "page-ui-directory",
6439
+ resource: "directory"
6440
+ });
6441
+ const uiFile = join6(uiDirectory, "page-ui.tsx");
6442
+ const uiContent = (await context.fs.readText(uiTemplate)).replace('useTranslations("template")', `useTranslations("${translationNamespace}")`).replaceAll('from "@/', `from "${aliasPrefix}/`).replace(/template/g, pageIdentifier).replace(/Template/g, pageIdentifier);
6443
+ await gateway.write(uiFile, uiContent, {
6444
+ role: "page-ui",
6445
+ preserveExisting: true
6446
+ });
6447
+ await gateway.mkdir(routeDirectory, {
6448
+ role: "page-route-directory",
6449
+ resource: "directory"
6450
+ });
6451
+ for (const template of routeTemplates) {
6452
+ const filename = toFileName(template.flag);
6453
+ const target = join6(routeDirectory, filename);
6454
+ const uiImportPath = pageSegments.join("/");
6455
+ const content = (await context.fs.readText(template.source)).replace("@/ui/template/", `@/ui/${uiImportPath}/`).replaceAll('from "@/', `from "${aliasPrefix}/`).replace(/template/g, pageIdentifier).replace(/Template/g, pageIdentifier);
6456
+ await gateway.write(target, content, {
6457
+ role: `page-${template.flag}`,
6458
+ preserveExisting: true
6459
+ });
6460
+ }
6461
+ if (useI18n) {
6462
+ for (const item of preparedLocales) {
6463
+ if (item.messageExists) {
6464
+ gateway.unchanged(item.target, {
6465
+ role: "translation-messages",
6466
+ detail: { locale: item.locale, namespace: translationNamespace }
6467
+ });
6468
+ } else {
6469
+ await gateway.write(item.target, item.targetContent, {
6470
+ role: "translation-messages",
6471
+ detail: { locale: item.locale, namespace: translationNamespace }
6472
+ });
6473
+ }
6474
+ await gateway.write(item.aggregator, item.aggregatorContent, {
6475
+ role: "locale-aggregator"
6476
+ });
5443
6477
  }
5444
- console.log(`\uD83D\uDCC4 File created: ${routePath}`);
5445
6478
  } else {
5446
- console.log(`\u2139\uFE0F File already exists: ${routePath}`);
6479
+ gateway.skipped(join6(context.cwd, "messages"), {
6480
+ role: "translation-messages",
6481
+ resource: "directory",
6482
+ detail: { reason: "Internationalization is disabled." }
6483
+ });
5447
6484
  }
5448
- console.log(`\u2705 API route "${apiName}" added.`);
6485
+ const mutated = context.operations.snapshot().some((event) => ["created", "updated", "copied", "deleted"].includes(event.action));
6486
+ const reviewPaths = [
6487
+ { scope: "project", path: gateway.path(uiFile) },
6488
+ ...routeTemplates.map((template) => ({
6489
+ scope: "project",
6490
+ path: gateway.path(join6(routeDirectory, toFileName(template.flag)))
6491
+ })),
6492
+ ...preparedLocales.map((item) => ({
6493
+ scope: "project",
6494
+ path: gateway.path(item.target)
6495
+ }))
6496
+ ];
6497
+ return commandResult(context, {
6498
+ command: "addpage",
6499
+ summary: mutated ? `Added page "${logicalName}" and its missing resources.` : `Page "${logicalName}" already exists and was preserved.`,
6500
+ projectRoot: context.cwd,
6501
+ nextSteps: mutated ? [
6502
+ {
6503
+ kind: "review",
6504
+ required: true,
6505
+ message: "Review the generated page UI, route files and localized messages.",
6506
+ paths: reviewPaths
6507
+ },
6508
+ {
6509
+ kind: "run-checks",
6510
+ required: true,
6511
+ message: "Run the project checks.",
6512
+ paths: [],
6513
+ commands: ["bun run check", "npm run check", "pnpm run check"]
6514
+ }
6515
+ ] : []
6516
+ });
6517
+ };
6518
+
6519
+ // src/lib/addText.ts
6520
+ import { join as join7 } from "path";
6521
+ function defaultText(key) {
6522
+ return key.replace(/_/g, " ").replace(/\b\w/g, (character) => character.toUpperCase());
6523
+ }
6524
+ var addText = async (args, context) => {
6525
+ const pathArg = args[1];
6526
+ if (!pathArg) {
6527
+ throw new CliError("Translation dot path is required.", {
6528
+ code: "INVALID_ARGUMENT",
6529
+ hint: "Use addtext domain.key followed by an optional value."
6530
+ });
6531
+ }
6532
+ const segments = parseLogicalName(pathArg, "translation path");
6533
+ if (segments.length < 2) {
6534
+ throw new CliError("Translation path must contain a file and a key.", {
6535
+ code: "INVALID_ARGUMENT"
6536
+ });
6537
+ }
6538
+ const providedText = args.slice(2).join(" ");
6539
+ const finalKey = segments.at(-1);
6540
+ const text = providedText || defaultText(finalKey);
6541
+ const config = await loadConfig(context);
6542
+ if (!config?.useI18n) {
6543
+ throw new CliError("Internationalization is not enabled in this project.", {
6544
+ code: "I18N_DISABLED"
6545
+ });
6546
+ }
6547
+ const messagesRoot = join7(context.cwd, "messages");
6548
+ if (!context.fs.exists(messagesRoot)) {
6549
+ throw new CliError("The messages directory was not found.", {
6550
+ code: "CONFIG_NOT_FOUND",
6551
+ scope: "project",
6552
+ path: "messages"
6553
+ });
6554
+ }
6555
+ const locales = (await context.fs.list(messagesRoot)).filter((entry) => entry.isDirectory).map((entry) => entry.name).sort();
6556
+ if (locales.length === 0) {
6557
+ throw new CliError("No locale directories were found.", {
6558
+ code: "CONFIG_NOT_FOUND",
6559
+ scope: "project",
6560
+ path: "messages"
6561
+ });
6562
+ }
6563
+ const [fileName, ...keySegments] = segments;
6564
+ const prepared = [];
6565
+ for (const locale of locales) {
6566
+ const file = join7(messagesRoot, locale, `${fileName}.json`);
6567
+ await assertSafeTarget(context.cwd, file, context.fs);
6568
+ const existed = context.fs.exists(file);
6569
+ let data = {};
6570
+ if (existed) {
6571
+ try {
6572
+ const parsed = JSON.parse(await context.fs.readText(file));
6573
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
6574
+ throw new Error("root value is not an object");
6575
+ }
6576
+ data = parsed;
6577
+ } catch (error) {
6578
+ throw new CliError(`Invalid JSON in messages/${locale}/${fileName}.json.`, {
6579
+ code: "FILESYSTEM_ERROR",
6580
+ scope: "project",
6581
+ path: `messages/${locale}/${fileName}.json`,
6582
+ hint: error instanceof Error ? error.message : undefined
6583
+ });
6584
+ }
6585
+ }
6586
+ let cursor = data;
6587
+ for (const segment of keySegments.slice(0, -1)) {
6588
+ const current = cursor[segment];
6589
+ if (current === undefined)
6590
+ cursor[segment] = {};
6591
+ else if (!current || typeof current !== "object" || Array.isArray(current)) {
6592
+ throw new CliError(`Translation segment "${segment}" is not an object in ${locale}.`, {
6593
+ code: "INVALID_ARGUMENT",
6594
+ scope: "project",
6595
+ path: `messages/${locale}/${fileName}.json`
6596
+ });
6597
+ }
6598
+ cursor = cursor[segment];
6599
+ }
6600
+ const previous = cursor[finalKey];
6601
+ cursor[finalKey] = text;
6602
+ prepared.push({
6603
+ locale,
6604
+ file,
6605
+ data,
6606
+ replaced: previous !== undefined && previous !== text
6607
+ });
6608
+ }
6609
+ const gateway = new MutationGateway(context, context.cwd);
6610
+ for (const item of prepared) {
6611
+ await gateway.write(item.file, `${JSON.stringify(item.data, null, 2)}
6612
+ `, {
6613
+ role: "translation-messages",
6614
+ detail: {
6615
+ locale: item.locale,
6616
+ key: pathArg,
6617
+ replaced: item.replaced
6618
+ }
6619
+ });
6620
+ }
6621
+ const mutated = context.operations.snapshot().some((event) => event.action === "created" || event.action === "updated");
6622
+ const nextSteps = mutated ? [
6623
+ ...locales.length > 1 ? [
6624
+ {
6625
+ kind: "translate",
6626
+ required: true,
6627
+ message: "Review the value in every locale; the same text was written to all locale files.",
6628
+ paths: prepared.map((item) => ({
6629
+ scope: "project",
6630
+ path: gateway.path(item.file)
6631
+ }))
6632
+ }
6633
+ ] : [],
6634
+ {
6635
+ kind: "run-checks",
6636
+ required: true,
6637
+ message: "Run the project checks.",
6638
+ paths: [],
6639
+ commands: ["bun run check", "npm run check", "pnpm run check"]
6640
+ }
6641
+ ] : [];
6642
+ return commandResult(context, {
6643
+ command: "addtext",
6644
+ summary: mutated ? `Set translation "${pathArg}" to "${text}" in ${locales.length} locale files.` : `Translation "${pathArg}" already has value "${text}" in every locale.`,
6645
+ projectRoot: context.cwd,
6646
+ nextSteps
6647
+ });
6648
+ };
6649
+
6650
+ // src/lib/rmPage.ts
6651
+ import path7 from "path";
6652
+ function escapeRegExp(value) {
6653
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6654
+ }
6655
+ function unregisterMessagesFile(content, locale, fileName) {
6656
+ const identifier = toIdentifier(fileName);
6657
+ const escapedIdentifier = escapeRegExp(identifier);
6658
+ const escapedPath = escapeRegExp(`./${locale}/${fileName}.json`);
6659
+ const importPattern = new RegExp(`^import\\s+${escapedIdentifier}\\s+from\\s+["']${escapedPath}["'];?\\r?\\n?`, "m");
6660
+ const propertyPattern = new RegExp(`^\\s*${escapedIdentifier},\\s*\\r?\\n?`, "m");
6661
+ const hasImport = importPattern.test(content);
6662
+ const hasProperty = propertyPattern.test(content);
6663
+ if (hasImport !== hasProperty) {
6664
+ throw new CliError(`Locale aggregator messages/${locale}.ts is inconsistent for ${fileName}.json.`, {
6665
+ code: "INCONSISTENT_LOCALE",
6666
+ scope: "project",
6667
+ path: `messages/${locale}.ts`
6668
+ });
6669
+ }
6670
+ if (!hasImport)
6671
+ return { content, changed: false };
6672
+ return {
6673
+ content: content.replace(importPattern, "").replace(propertyPattern, ""),
6674
+ changed: true
6675
+ };
6676
+ }
6677
+ var rmPage = async (args, context) => {
6678
+ const candidates = await discoverPages(context.cwd, context.fs);
6679
+ let logicalName = args[1];
6680
+ if (!logicalName || logicalName.startsWith("-")) {
6681
+ if (context.outputMode === "json") {
6682
+ throw new CliError("Page name is required in JSON mode.", {
6683
+ code: "INTERACTIVE_INPUT_REQUIRED",
6684
+ hint: "Pass a page name returned by completion after rmpage."
6685
+ });
6686
+ }
6687
+ const selected = await context.prompt([
6688
+ {
6689
+ type: "autocomplete",
6690
+ name: "page",
6691
+ message: "Page to remove:",
6692
+ choices: candidates.map((candidate2) => ({
6693
+ title: candidate2.logicalName.replaceAll(".", " > "),
6694
+ value: candidate2.logicalName
6695
+ }))
6696
+ },
6697
+ {
6698
+ type: (value) => value ? "confirm" : null,
6699
+ name: "confirm",
6700
+ message: "Confirm page deletion?",
6701
+ initial: false
6702
+ }
6703
+ ]);
6704
+ if (!selected.confirm) {
6705
+ context.operations.record({
6706
+ action: "cancelled",
6707
+ resource: "command",
6708
+ role: "page-removal",
6709
+ scope: "project",
6710
+ path: "."
6711
+ });
6712
+ return commandResult(context, {
6713
+ command: "rmpage",
6714
+ summary: "Page deletion was cancelled.",
6715
+ projectRoot: context.cwd,
6716
+ status: "cancelled"
6717
+ });
6718
+ }
6719
+ logicalName = String(selected.page ?? "");
6720
+ }
6721
+ parseLogicalName(logicalName, "page name");
6722
+ const candidate = candidates.find((entry) => entry.logicalName === logicalName);
6723
+ if (!candidate) {
6724
+ throw new CliError(`Page not found: ${logicalName}.`, {
6725
+ code: "TARGET_NOT_FOUND",
6726
+ scope: "project",
6727
+ path: logicalName.replaceAll(".", "/")
6728
+ });
6729
+ }
6730
+ const messagesRoot = resolveInside(context.cwd, "messages");
6731
+ const preparedMessages = [];
6732
+ const preparedAggregators = [];
6733
+ if (context.fs.exists(messagesRoot)) {
6734
+ const locales = (await context.fs.list(messagesRoot)).filter((entry) => entry.isDirectory).map((entry) => entry.name).sort();
6735
+ for (const locale of locales) {
6736
+ const target = resolveInside(context.cwd, "messages", locale, `${candidate.routeSegments[0]}.json`);
6737
+ if (!candidate.messageKey) {
6738
+ const aggregator = resolveInside(context.cwd, "messages", `${locale}.ts`);
6739
+ if (!context.fs.exists(aggregator)) {
6740
+ throw new CliError(`Locale aggregator messages/${locale}.ts was not found.`, {
6741
+ code: "CONFIG_NOT_FOUND",
6742
+ scope: "project",
6743
+ path: `messages/${locale}.ts`
6744
+ });
6745
+ }
6746
+ const nextAggregator = unregisterMessagesFile(await context.fs.readText(aggregator), locale, candidate.routeSegments[0]);
6747
+ preparedMessages.push({
6748
+ target,
6749
+ keyPresent: context.fs.exists(target)
6750
+ });
6751
+ preparedAggregators.push({
6752
+ target: aggregator,
6753
+ ...nextAggregator
6754
+ });
6755
+ continue;
6756
+ }
6757
+ if (!context.fs.exists(target))
6758
+ continue;
6759
+ let data;
6760
+ try {
6761
+ data = JSON.parse(await context.fs.readText(target));
6762
+ } catch {
6763
+ throw new CliError(`Invalid JSON prevents removal from ${path7.relative(context.cwd, target)}.`, {
6764
+ code: "FILESYSTEM_ERROR",
6765
+ scope: "project",
6766
+ path: path7.relative(context.cwd, target)
6767
+ });
6768
+ }
6769
+ const keyPresent = Object.hasOwn(data, candidate.messageKey);
6770
+ if (keyPresent)
6771
+ delete data[candidate.messageKey];
6772
+ preparedMessages.push({
6773
+ target,
6774
+ keyPresent,
6775
+ content: `${JSON.stringify(data, null, 2)}
6776
+ `
6777
+ });
6778
+ }
6779
+ }
6780
+ const gateway = new MutationGateway(context, context.cwd);
6781
+ for (const prepared of preparedMessages) {
6782
+ if (!candidate.messageKey) {
6783
+ await gateway.remove(prepared.target, { role: "translation-messages" });
6784
+ } else if (prepared.keyPresent) {
6785
+ await gateway.write(prepared.target, prepared.content, {
6786
+ role: "translation-messages",
6787
+ detail: { removedKey: candidate.messageKey }
6788
+ });
6789
+ } else {
6790
+ gateway.unchanged(prepared.target, {
6791
+ role: "translation-messages",
6792
+ detail: { missingKey: candidate.messageKey }
6793
+ });
6794
+ }
6795
+ }
6796
+ for (const prepared of preparedAggregators) {
6797
+ if (prepared.changed) {
6798
+ await gateway.write(prepared.target, prepared.content, {
6799
+ role: "locale-aggregator"
6800
+ });
6801
+ } else {
6802
+ gateway.unchanged(prepared.target, { role: "locale-aggregator" });
6803
+ }
6804
+ }
6805
+ await gateway.remove(candidate.uiDirectory, {
6806
+ role: "page-ui",
6807
+ resource: "directory"
6808
+ }, { recursive: true, force: false });
6809
+ await gateway.remove(candidate.routeDirectory, {
6810
+ role: "page-route",
6811
+ resource: "directory"
6812
+ }, { recursive: true, force: false });
6813
+ const mutated = context.operations.snapshot().some((event) => event.action === "deleted" || event.action === "updated");
6814
+ return commandResult(context, {
6815
+ command: "rmpage",
6816
+ summary: mutated ? `Deleted page "${logicalName}" and its associated resources.` : `No resources remained for page "${logicalName}".`,
6817
+ projectRoot: context.cwd,
6818
+ nextSteps: mutated ? [
6819
+ {
6820
+ kind: "run-checks",
6821
+ required: true,
6822
+ message: "Run the project checks after removing the page.",
6823
+ paths: [],
6824
+ commands: ["bun run check", "npm run check", "pnpm run check"]
6825
+ }
6826
+ ] : []
6827
+ });
6828
+ };
6829
+
6830
+ // src/cli/registry.ts
6831
+ function createCommandRegistry() {
6832
+ return new Map([
6833
+ ["addcomponent", addComponent],
6834
+ ["addpage", addPage],
6835
+ ["addlib", addLib],
6836
+ ["addapi", addApi],
6837
+ ["addlanguage", addLanguage],
6838
+ ["addtext", addText],
6839
+ ["rmpage", rmPage]
6840
+ ]);
5449
6841
  }
5450
6842
 
5451
6843
  // src/scaffold.ts
5452
- import { cp, mkdir as mkdir5, rm, writeFile as writeFile6, readFile as readFile6 } from "fs/promises";
5453
- import { join as join7 } from "path";
5454
- import { existsSync as existsSync7 } from "fs";
6844
+ import { join as join8, resolve } from "path";
5455
6845
  import { fileURLToPath } from "url";
5456
6846
 
5457
- // src/lib/helper/consoleColor.ts
5458
- var RED = "\x1B[31m";
5459
- var GREEN = "\x1B[32m";
5460
- var CYAN = "\x1B[36m";
5461
- var RESET = "\x1B[0m";
5462
- function red(text) {
5463
- return RED + text + RESET;
6847
+ // src/core/template-manifest.ts
6848
+ import path8 from "path";
6849
+ var TEMPLATE_DENY_NAMES = new Set([
6850
+ ".env",
6851
+ ".git",
6852
+ ".agent",
6853
+ ".cursor",
6854
+ ".next",
6855
+ "node_modules",
6856
+ "artifacts",
6857
+ "coverage",
6858
+ "playwright-report",
6859
+ "test-results"
6860
+ ]);
6861
+ function isDistributableTemplatePath(relativePath) {
6862
+ const segments = relativePath.split(path8.sep);
6863
+ return !segments.some((segment) => TEMPLATE_DENY_NAMES.has(segment) || segment.endsWith(".tsbuildinfo") || segment === ".DS_Store");
5464
6864
  }
5465
- function green(text) {
5466
- return GREEN + text + RESET;
6865
+ async function templateManifest(root, fs) {
6866
+ const files = [];
6867
+ async function visit(directory, relativeDirectory = "") {
6868
+ const entries = await fs.list(directory);
6869
+ for (const entry of entries) {
6870
+ const relative = path8.join(relativeDirectory, entry.name);
6871
+ if (!isDistributableTemplatePath(relative))
6872
+ continue;
6873
+ const source = path8.join(directory, entry.name);
6874
+ if (entry.isSymbolicLink) {
6875
+ throw new CliError(`Symbolic links are forbidden in templates: ${relative}`, { code: "UNSAFE_PATH", scope: "package", path: relative });
6876
+ }
6877
+ if (entry.isDirectory)
6878
+ await visit(source, relative);
6879
+ else if (entry.isFile)
6880
+ files.push(relative);
6881
+ else
6882
+ throw new CliError(`Unsupported template entry: ${relative}`, {
6883
+ code: "TEMPLATE_MISSING",
6884
+ scope: "package",
6885
+ path: relative
6886
+ });
6887
+ }
6888
+ }
6889
+ await visit(root);
6890
+ return files;
5467
6891
  }
5468
- function cyan(text) {
5469
- return CYAN + text + RESET;
6892
+ async function validateScaffoldTemplate(root, fs) {
6893
+ const manifest = await templateManifest(root, fs);
6894
+ for (const required of ["package.json", "tsconfig.json"]) {
6895
+ if (!manifest.includes(required)) {
6896
+ throw new CliError(`Required template file was not found: ${required}.`, {
6897
+ code: "TEMPLATE_MISSING",
6898
+ scope: "package",
6899
+ path: `templates/Projects/default/${required}`
6900
+ });
6901
+ }
6902
+ }
6903
+ try {
6904
+ JSON.parse(await fs.readText(path8.join(root, "package.json")));
6905
+ const tsconfig = JSON.parse(await fs.readText(path8.join(root, "tsconfig.json")));
6906
+ if (!tsconfig.compilerOptions?.paths?.["@/*"])
6907
+ throw new Error("alias");
6908
+ } catch {
6909
+ throw new CliError('The template manifests must be valid JSON and define the default "@/*" alias.', {
6910
+ code: "TEMPLATE_MISSING",
6911
+ scope: "package",
6912
+ path: "templates/Projects/default"
6913
+ });
6914
+ }
6915
+ return manifest;
6916
+ }
6917
+ async function copyTemplate(root, target, context, gateway, manifest) {
6918
+ const files = manifest ?? await templateManifest(root, context.fs);
6919
+ for (const relative of files) {
6920
+ const destination = path8.join(target, relative === ".gitignore.template" ? ".gitignore" : relative);
6921
+ const source = path8.join(root, relative);
6922
+ await gateway.copy(source, destination, {
6923
+ role: "template-file",
6924
+ source: { template: `Projects/default/${relative}` }
6925
+ });
6926
+ }
6927
+ return files;
6928
+ }
6929
+ async function customizeGeneratedProject(target, projectName, importAlias, context, gateway) {
6930
+ const packagePath = path8.join(target, "package.json");
6931
+ const packageJson = JSON.parse(await context.fs.readText(packagePath));
6932
+ packageJson.name = projectName.toLowerCase();
6933
+ delete packageJson.packageManager;
6934
+ await gateway.write(packagePath, `${JSON.stringify(packageJson, null, 2)}
6935
+ `, { role: "package-manifest" });
6936
+ const tsconfigPath = path8.join(target, "tsconfig.json");
6937
+ const tsconfigContent = await context.fs.readText(tsconfigPath);
6938
+ const tsconfig = JSON.parse(tsconfigContent);
6939
+ if (!tsconfig.compilerOptions?.paths?.["@/*"]) {
6940
+ throw new CliError('The template tsconfig must define the default "@/*" alias.', {
6941
+ code: "TEMPLATE_MISSING",
6942
+ scope: "package",
6943
+ path: "templates/Projects/default/tsconfig.json"
6944
+ });
6945
+ }
6946
+ await gateway.write(tsconfigPath, tsconfigContent.replace('"@/*"', `"${importAlias}"`), { role: "typescript-configuration" });
6947
+ if (importAlias !== "@/*") {
6948
+ const prefix = importAlias.slice(0, -2);
6949
+ for (const relative of await templateManifest(target, context.fs)) {
6950
+ if (!/\.[cm]?[jt]sx?$/.test(relative))
6951
+ continue;
6952
+ const file = path8.join(target, relative);
6953
+ const content = await context.fs.readText(file);
6954
+ const next = content.replaceAll('from "@/', `from "${prefix}/`).replaceAll("from '@/", `from '${prefix}/`);
6955
+ if (next !== content) {
6956
+ await gateway.write(file, next, { role: "import-alias-reference" });
6957
+ }
6958
+ }
6959
+ }
5470
6960
  }
5471
6961
 
5472
6962
  // src/scaffold.ts
5473
- async function scaffoldProject(options) {
5474
- const targetPath = join7(process.cwd(), options.projectName);
6963
+ async function scaffoldProject(options, runtime) {
6964
+ const requiredFeatures = [
6965
+ "useTypescript",
6966
+ "useEslint",
6967
+ "useTailwind",
6968
+ "useSrcDir",
6969
+ "useTurbopack",
6970
+ "useI18n"
6971
+ ];
6972
+ const unsupported = requiredFeatures.filter((feature) => options[feature] !== true);
6973
+ if (unsupported.length > 0) {
6974
+ throw new CliError(`The default Next.js 16 template requires: ${unsupported.join(", ")}.`, { code: "INVALID_ARGUMENT" });
6975
+ }
6976
+ const { context } = runtime;
6977
+ const cwd = context.cwd;
6978
+ const projectName = validateProjectName(options.projectName);
6979
+ const importAlias = normalizeImportAlias(options.customAlias === false ? "@/*" : options.importAlias || "@/*");
6980
+ const targetPath = join8(cwd, projectName);
5475
6981
  const __dirname2 = new URL(".", import.meta.url);
5476
- const templatePath = join7(fileURLToPath(__dirname2), "..", "templates", "Projects", "default");
5477
- if (existsSync7(targetPath)) {
6982
+ const templatePath = runtime.templatePath ?? join8(fileURLToPath(__dirname2), "..", "templates", "Projects", "default");
6983
+ const resolvedCwd = resolve(cwd);
6984
+ const resolvedTarget = resolve(targetPath);
6985
+ if (!resolvedTarget.startsWith(`${resolvedCwd}/`)) {
6986
+ throw new CliError("The project destination must be a child of the current directory.", { code: "UNSAFE_PATH" });
6987
+ }
6988
+ const gateway = new MutationGateway(context, targetPath);
6989
+ const manifest = await validateScaffoldTemplate(templatePath, context.fs);
6990
+ if (context.fs.exists(targetPath)) {
5478
6991
  if (options.force) {
5479
- console.warn("\u26A0\uFE0F Target directory already exists, removing...");
5480
- await rm(targetPath, { recursive: true, force: true });
6992
+ if (context.outputMode === "human") {
6993
+ context.terminal.warn(`WARNING: --force will remove the existing project at ${targetPath}.`);
6994
+ }
6995
+ await gateway.remove(targetPath, {
6996
+ role: "existing-project",
6997
+ resource: "project"
6998
+ }, { recursive: true, force: true });
5481
6999
  } else {
5482
- console.error(red("[X] Target directory already exists. Use --force to overwrite."));
5483
- process.exit(1);
7000
+ throw new CliError("Target directory already exists. Use --force to overwrite it.", {
7001
+ code: "TARGET_EXISTS",
7002
+ scope: "project",
7003
+ path: "."
7004
+ });
5484
7005
  }
5485
7006
  }
5486
- try {
5487
- console.log("Creating project directory...");
5488
- await mkdir5(targetPath, { recursive: true });
5489
- console.log("Copying files from template...");
5490
- await cp(templatePath, targetPath, { recursive: true });
5491
- const pkgPath = join7(targetPath, "package.json");
5492
- if (existsSync7(pkgPath)) {
5493
- const pkg = JSON.parse(await readFile6(pkgPath, "utf-8"));
5494
- pkg.dependencies = pkg.dependencies || {};
5495
- if (options.useI18n) {
5496
- pkg.dependencies["next-intl"] = pkg.dependencies["next-intl"] || "^4.3.5";
5497
- }
5498
- await writeFile6(pkgPath, JSON.stringify(pkg, null, 2));
5499
- }
5500
- await writeFile6(join7(targetPath, "cnp.config.json"), JSON.stringify(options, null, 2));
5501
- console.log("Project setup complete!");
5502
- console.log("");
5503
- console.log("To get started:");
5504
- console.log(" " + green(`cd ${options.projectName}`));
5505
- console.log("");
5506
- console.log("Then install dependencies and launch the dev server with your preferred tool:");
5507
- console.log(" " + green(`bun install && bun dev`));
5508
- console.log(" " + green(`npm install && npm run dev`));
5509
- console.log(" " + green(`pnpm install && pnpm run dev`));
5510
- console.log("");
5511
- console.log("Documentation and examples can be found at:");
5512
- console.log(" " + cyan("https://github.com/Rising-Corporation/create-next-pro-cli"));
5513
- console.log("_-`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`-_");
5514
- } catch (err) {
5515
- console.error(red("[X] Error during project creation:"), err);
5516
- process.exit(1);
5517
- }
7007
+ await gateway.mkdir(targetPath, {
7008
+ resource: "project",
7009
+ role: "project-root"
7010
+ });
7011
+ await copyTemplate(templatePath, targetPath, context, gateway, manifest);
7012
+ await customizeGeneratedProject(targetPath, projectName, importAlias, context, gateway);
7013
+ await gateway.write(join8(targetPath, "cnp.config.json"), `${JSON.stringify({ ...options, projectName, importAlias }, null, 2)}
7014
+ `, {
7015
+ role: "project-configuration",
7016
+ resource: "configuration",
7017
+ detail: { importAlias, packageManager: "user-selected" }
7018
+ });
7019
+ return { projectRoot: targetPath, copiedFiles: manifest.length, importAlias };
5518
7020
  }
5519
7021
 
5520
7022
  // src/lib/createProject.ts
5521
- async function createProject(nameArg, force) {
7023
+ async function createProjectFromOptions(options, context) {
7024
+ const { projectRoot, copiedFiles, importAlias } = await scaffoldProject(options, {
7025
+ context
7026
+ });
7027
+ return commandResult(context, {
7028
+ command: "create",
7029
+ summary: `Created project "${options.projectName}" with ${copiedFiles} template files and import alias ${importAlias}; no package manager was pinned.`,
7030
+ projectRoot,
7031
+ nextSteps: [
7032
+ {
7033
+ kind: "install",
7034
+ required: true,
7035
+ message: "Install dependencies with your preferred package manager.",
7036
+ paths: [],
7037
+ commands: [
7038
+ `cd ${options.projectName} && bun install`,
7039
+ `cd ${options.projectName} && npm install`,
7040
+ `cd ${options.projectName} && pnpm install`
7041
+ ]
7042
+ },
7043
+ {
7044
+ kind: "run-checks",
7045
+ required: true,
7046
+ message: "Run the generated project checks before development.",
7047
+ paths: [{ scope: "project", path: "package.json" }],
7048
+ commands: ["bun run check", "npm run check", "pnpm run check"]
7049
+ }
7050
+ ],
7051
+ data: {
7052
+ projectName: options.projectName,
7053
+ importAlias,
7054
+ packageManager: null,
7055
+ copiedFiles
7056
+ }
7057
+ });
7058
+ }
7059
+ async function createProject(nameArg, force, context) {
5522
7060
  const response = {
5523
7061
  projectName: nameArg,
5524
7062
  useTypescript: true,
@@ -5531,14 +7069,12 @@ async function createProject(nameArg, force) {
5531
7069
  importAlias: "@/*",
5532
7070
  force
5533
7071
  };
5534
- console.log(`Creating project "${response.projectName}"...`);
5535
- await scaffoldProject(response);
7072
+ return createProjectFromOptions(response, context);
5536
7073
  }
5537
7074
 
5538
7075
  // src/lib/createProjectWithPrompt.ts
5539
- var import_prompts6 = __toESM(require_prompts3(), 1);
5540
- async function createProjectWithPrompt() {
5541
- const response = await import_prompts6.default.prompt([
7076
+ async function createProjectWithPrompt(context) {
7077
+ const response = await context.prompt([
5542
7078
  {
5543
7079
  type: "text",
5544
7080
  name: "projectName",
@@ -5608,308 +7144,243 @@ async function createProjectWithPrompt() {
5608
7144
  initial: "@core/*"
5609
7145
  }
5610
7146
  ]);
5611
- console.log(`
5612
- Your choices:`);
5613
- console.log(response);
5614
- await scaffoldProject(response);
5615
- }
5616
-
5617
- // src/lib/addLanguage.ts
5618
- var import_prompts7 = __toESM(require_prompts3(), 1);
5619
- import { join as join8 } from "path";
5620
- import { existsSync as existsSync8 } from "fs";
5621
- import { cp as cp2, readFile as readFile7, writeFile as writeFile7 } from "fs/promises";
5622
- function generateLocales() {
5623
- const dn = new Intl.DisplayNames(["en"], { type: "language" });
5624
- const locales = [];
5625
- for (let i = 0;i < 26; i++) {
5626
- for (let j = 0;j < 26; j++) {
5627
- const code = String.fromCharCode(97 + i) + String.fromCharCode(97 + j);
5628
- try {
5629
- const name = dn.of(code);
5630
- if (name && name.toLowerCase() !== code) {
5631
- locales.push(code);
5632
- }
5633
- } catch {}
5634
- }
5635
- }
5636
- return locales.sort();
5637
- }
5638
- async function addLanguage(args) {
5639
- const config = await loadConfig();
5640
- if (!config?.useI18n) {
5641
- console.error("\u274C i18n is not enabled in this project.");
5642
- return;
5643
- }
5644
- const messagesPath = join8(process.cwd(), "messages");
5645
- if (!existsSync8(messagesPath)) {
5646
- console.error("\u274C Messages directory missing. Ensure i18n was configured.");
5647
- return;
5648
- }
5649
- const available = generateLocales();
5650
- let locale = args[1];
5651
- if (!locale || !available.includes(locale)) {
5652
- const response = await import_prompts7.default({
5653
- type: "autocomplete",
5654
- name: "locale",
5655
- message: "\uD83C\uDF10 Locale to add:",
5656
- choices: available.map((l) => ({ title: l, value: l }))
7147
+ if (!response.projectName) {
7148
+ context.operations.record({
7149
+ action: "cancelled",
7150
+ resource: "command",
7151
+ role: "project-creation",
7152
+ scope: "project",
7153
+ path: "."
7154
+ });
7155
+ return commandResult(context, {
7156
+ command: "create",
7157
+ summary: "Project creation was cancelled.",
7158
+ projectRoot: context.cwd,
7159
+ status: "cancelled"
5657
7160
  });
5658
- locale = response.locale;
5659
- }
5660
- if (!locale)
5661
- return;
5662
- if (existsSync8(join8(messagesPath, locale))) {
5663
- console.error(`\u274C Locale ${locale} already exists.`);
5664
- return;
5665
- }
5666
- const routingFile = join8(process.cwd(), "src", "lib", "i18n", "routing.ts");
5667
- if (!existsSync8(routingFile)) {
5668
- console.error("\u274C routing.ts not found. Are you in project root?");
5669
- return;
5670
- }
5671
- const routingContent = await readFile7(routingFile, "utf-8");
5672
- const defaultMatch = routingContent.match(/defaultLocale:\s*"([^"]+)"/);
5673
- const defaultLocale = defaultMatch ? defaultMatch[1] : null;
5674
- if (!defaultLocale || !existsSync8(join8(messagesPath, defaultLocale))) {
5675
- console.error("\u274C Default locale not found.");
5676
- return;
5677
- }
5678
- await cp2(join8(messagesPath, defaultLocale), join8(messagesPath, locale), {
5679
- recursive: true
5680
- });
5681
- console.log(`\uD83D\uDCC4 Directory created: ${join8(messagesPath, locale)}`);
5682
- const localesMatch = routingContent.match(/locales:\s*\[([^\]]*)\]/);
5683
- if (localesMatch) {
5684
- const localesArr = localesMatch[1].split(",").map((s) => s.trim().replace(/["']/g, "")).filter(Boolean);
5685
- if (!localesArr.includes(locale)) {
5686
- localesArr.push(locale);
5687
- const newLocales = `locales: [${localesArr.map((l) => `"${l}"`).join(", ")}]`;
5688
- const newContent = routingContent.replace(/locales:\s*\[[^\]]*\]/, newLocales);
5689
- await writeFile7(routingFile, newContent);
5690
- console.log(`\uD83D\uDCC4 File updated: ${routingFile}`);
5691
- }
5692
- }
5693
- console.log(`\u2705 Locale "${locale}" added and copied from default locale "${defaultLocale}".`);
5694
- }
5695
-
5696
- // src/lib/addText.ts
5697
- import { join as join9 } from "path";
5698
- import { existsSync as existsSync9 } from "fs";
5699
- import { readFile as readFile8, writeFile as writeFile8, readdir as readdir4 } from "fs/promises";
5700
- function defaultText(key) {
5701
- return key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
5702
- }
5703
- async function addText(args) {
5704
- const pathArg = args[1];
5705
- if (!pathArg) {
5706
- console.error("\u274C Dot path parameter is required.");
5707
- return;
5708
- }
5709
- const providedText = args.slice(2).join(" ");
5710
- const config = await loadConfig();
5711
- if (!config?.useI18n) {
5712
- console.error("\u274C i18n is not enabled in this project.");
5713
- return;
5714
- }
5715
- const messagesPath = join9(process.cwd(), "messages");
5716
- if (!existsSync9(messagesPath)) {
5717
- console.error("\u274C Messages directory missing. Ensure i18n was configured.");
5718
- return;
5719
- }
5720
- const entries = await readdir4(messagesPath, { withFileTypes: true });
5721
- const locales = entries.filter((e) => e.isDirectory()).map((e) => e.name);
5722
- const [fileName, ...segments] = pathArg.split(".");
5723
- if (!fileName || segments.length === 0) {
5724
- console.error("\u274C Invalid dot path provided.");
5725
- return;
5726
- }
5727
- const finalKey = segments[segments.length - 1];
5728
- const text = providedText || defaultText(finalKey);
5729
- for (const locale of locales) {
5730
- const filePath = join9(messagesPath, locale, `${fileName}.json`);
5731
- let data = {};
5732
- if (existsSync9(filePath)) {
5733
- const raw = await readFile8(filePath, "utf-8");
5734
- try {
5735
- data = JSON.parse(raw);
5736
- } catch {}
5737
- }
5738
- let cursor = data;
5739
- for (let i = 0;i < segments.length - 1; i++) {
5740
- const seg = segments[i];
5741
- if (!cursor[seg])
5742
- cursor[seg] = {};
5743
- cursor = cursor[seg];
5744
- }
5745
- cursor[finalKey] = text;
5746
- await writeFile8(filePath, JSON.stringify(data, null, 2));
5747
- console.log(`\uD83D\uDCC4 File updated: ${filePath}`);
5748
7161
  }
5749
- console.log(`\u2705 Text added at path "${pathArg}" with value "${text}".`);
7162
+ const options = response;
7163
+ return createProjectFromOptions(options, context);
5750
7164
  }
5751
7165
 
5752
- // src/index.ts
5753
- var CONFIG_DIR = process.env.XDG_CONFIG_HOME ? path.join(process.env.XDG_CONFIG_HOME, "create-next-pro") : path.join(os.homedir(), ".config", "create-next-pro");
5754
- var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
5755
- function readCfg() {
5756
- try {
5757
- return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
5758
- } catch {
5759
- return null;
7166
+ // src/runtime/node-context.ts
7167
+ var import_prompts = __toESM(require_prompts3(), 1);
7168
+ import { existsSync } from "fs";
7169
+ import {
7170
+ appendFile,
7171
+ copyFile,
7172
+ lstat,
7173
+ mkdir,
7174
+ readdir,
7175
+ readFile,
7176
+ rm,
7177
+ writeFile
7178
+ } from "fs/promises";
7179
+ import os from "os";
7180
+ import path9 from "path";
7181
+ import { fileURLToPath as fileURLToPath2 } from "url";
7182
+ function findPackageRoot(start) {
7183
+ let current = start;
7184
+ while (true) {
7185
+ const packagePath = path9.join(current, "package.json");
7186
+ if (existsSync(packagePath))
7187
+ return current;
7188
+ const parent = path9.dirname(current);
7189
+ if (parent === current) {
7190
+ throw new Error(`Unable to locate package.json from ${start}`);
7191
+ }
7192
+ current = parent;
5760
7193
  }
5761
7194
  }
5762
- function writeCfg(cfg) {
5763
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
5764
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
5765
- }
5766
- function rcFile(shell) {
5767
- return path.join(os.homedir(), shell === "zsh" ? ".zshrc" : ".bashrc");
5768
- }
5769
- function ensureLineInRc(file, line) {
5770
- try {
5771
- const cur = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
5772
- if (!cur.includes(line))
5773
- fs.appendFileSync(file, `
5774
- ${line}
5775
- `);
5776
- } catch {}
7195
+ function resolvePackageRoot(metaUrl = import.meta.url) {
7196
+ return findPackageRoot(path9.dirname(fileURLToPath2(metaUrl)));
5777
7197
  }
5778
- async function installCompletion(shell) {
5779
- const __dirname2 = path.dirname(fileURLToPath2(import.meta.url));
5780
- const completionSrc = path.resolve(__dirname2, "../create-next-pro-completion.sh");
5781
- const completionDst = path.join(CONFIG_DIR, "completion.sh");
5782
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
5783
- fs.copyFileSync(completionSrc, completionDst);
5784
- ensureLineInRc(rcFile(shell), `source "${completionDst}"`);
5785
- }
5786
- var __filename2 = fileURLToPath2(import.meta.url);
5787
- var __dirname2 = dirname(__filename2);
5788
- var packageJsonPath = resolve(__dirname2, "../package.json");
5789
- var packageJson = fs.readFileSync(packageJsonPath, "utf8");
5790
- async function onboarding() {
5791
- const pkg = JSON.parse(packageJson);
5792
- console.log(`\uD83D\uDE80 Welcome to create-next-pro v${pkg.version}
5793
- `);
5794
- const res = await import_prompts8.default([
5795
- {
5796
- type: "select",
5797
- name: "shell",
5798
- message: "Which shell do you use?",
5799
- choices: [
5800
- { title: "zsh", value: "zsh" },
5801
- { title: "bash", value: "bash" }
5802
- ],
5803
- initial: (os.userInfo().shell || "").includes("zsh") ? 0 : 1
7198
+ function createNodeContext(overrides = {}) {
7199
+ return {
7200
+ argv: process.argv.slice(2),
7201
+ cwd: process.cwd(),
7202
+ env: process.env,
7203
+ homeDir: os.homedir(),
7204
+ packageRoot: resolvePackageRoot(),
7205
+ terminal: console,
7206
+ prompt: import_prompts.default,
7207
+ fs: {
7208
+ exists: existsSync,
7209
+ readText: (target) => readFile(target, "utf8"),
7210
+ writeText: async (target, content) => {
7211
+ await writeFile(target, content);
7212
+ },
7213
+ mkdir: async (target) => {
7214
+ await mkdir(target, { recursive: true });
7215
+ },
7216
+ copyFile: async (source, target) => {
7217
+ await copyFile(source, target);
7218
+ },
7219
+ appendText: async (target, content) => {
7220
+ await appendFile(target, content);
7221
+ },
7222
+ remove: async (target, options) => {
7223
+ await rm(target, options);
7224
+ },
7225
+ inspect: async (target) => {
7226
+ try {
7227
+ const stats = await lstat(target);
7228
+ return {
7229
+ name: target,
7230
+ isFile: stats.isFile(),
7231
+ isDirectory: stats.isDirectory(),
7232
+ isSymbolicLink: stats.isSymbolicLink()
7233
+ };
7234
+ } catch (error) {
7235
+ if (error.code === "ENOENT")
7236
+ return null;
7237
+ throw error;
7238
+ }
7239
+ },
7240
+ list: async (target) => {
7241
+ const entries = await readdir(target, { withFileTypes: true });
7242
+ return Promise.all(entries.sort((left, right) => left.name.localeCompare(right.name)).map(async (entry) => {
7243
+ const stats = await lstat(path9.join(target, entry.name));
7244
+ return {
7245
+ name: entry.name,
7246
+ isFile: stats.isFile(),
7247
+ isDirectory: stats.isDirectory(),
7248
+ isSymbolicLink: stats.isSymbolicLink()
7249
+ };
7250
+ }));
7251
+ }
5804
7252
  },
5805
- {
5806
- type: "toggle",
5807
- name: "completion",
5808
- message: "Install autocompletion?",
5809
- initial: true,
5810
- active: "Yes",
5811
- inactive: "No"
5812
- }
5813
- ], { onCancel: () => process.exit(1) });
5814
- const cfg = {
5815
- version: 1,
5816
- shell: res.shell,
5817
- completionInstalled: !!res.completion,
5818
- createdAt: new Date().toISOString(),
5819
- updatedAt: new Date().toISOString()
7253
+ operations: new OperationJournal,
7254
+ outputMode: "human",
7255
+ ...overrides
5820
7256
  };
5821
- if (cfg.completionInstalled)
5822
- await installCompletion(cfg.shell);
5823
- writeCfg(cfg);
5824
- console.log(`
5825
- \u2705 Configuration saved.`);
5826
- console.log("you can now use the CLI ! ex : ");
5827
- console.log(" Without prompt (will change in future) :");
5828
- console.log(" create-next-pro my-next-project");
5829
- console.log(" With prompt :");
5830
- console.log(" create-next-pro");
5831
- console.log("For more information, visit: https://github.com/Rising-Corporation/create-next-pro-cli");
5832
- console.log("Happy coding! \uD83C\uDF89");
5833
- return cfg;
5834
7257
  }
5835
- function showHelp() {
5836
- console.log(`create-next-pro
7258
+
7259
+ // src/index.ts
7260
+ var HELP_TEXT = `create-next-pro
5837
7261
 
5838
7262
  Usage:
5839
- create-next-pro <project-name> [--force]
5840
- create-next-pro addpage [options]
5841
- create-next-pro addcomponent [options]
5842
- create-next-pro addlib [name]
5843
- create-next-pro addapi [name]
5844
- create-next-pro addlanguage [locale]
5845
- create-next-pro addtext <path> [text]
5846
- create-next-pro rmpage [options]
7263
+ create-next-pro <project-name> [--force] [--json]
7264
+ create-next-pro addpage [options] [--json]
7265
+ create-next-pro addcomponent [options] [--json]
7266
+ create-next-pro addlib [name] [--json]
7267
+ create-next-pro addapi [name] [--json]
7268
+ create-next-pro addlanguage [locale] [--json]
7269
+ create-next-pro addtext <path> [text] [--json]
7270
+ create-next-pro rmpage [options] [--json]
5847
7271
 
5848
7272
  Options:
5849
7273
  --help Show this help message
7274
+ --version Show the CLI version
7275
+ --json Emit one deterministic JSON document
5850
7276
  --reconfigure Run the configuration assistant again
5851
- `);
7277
+ `;
7278
+ function parseOptions(args) {
7279
+ return {
7280
+ force: args.includes("--force"),
7281
+ help: args.includes("--help"),
7282
+ version: args.includes("--version") || args.includes("-v"),
7283
+ reconfigure: args.includes("--reconfigure"),
7284
+ json: args.includes("--json")
7285
+ };
5852
7286
  }
5853
- function showVersion() {
5854
- const pkg = JSON.parse(packageJson);
5855
- console.log(`v${pkg.version}`);
7287
+ function withoutGlobalOptions(args) {
7288
+ return args.filter((argument) => argument !== "--json");
5856
7289
  }
5857
- async function main() {
5858
- let args;
5859
- if (typeof Bun !== "undefined") {
5860
- args = Bun.argv.slice(2);
5861
- } else if (typeof process !== "undefined" && process.argv) {
5862
- args = process.argv.slice(2);
5863
- } else {
5864
- args = [];
5865
- }
5866
- if (args.includes("--help"))
5867
- return showHelp();
5868
- if (args.includes("--version") || args.includes("-v"))
5869
- return showVersion();
5870
- if (args.includes("--reconfigure") || !readCfg()) {
5871
- await onboarding();
5872
- return;
5873
- }
5874
- const force = args.includes("--force");
5875
- if (args[0] === "addpage" && args.length === 1) {
5876
- args.push("-LPl");
5877
- }
5878
- if (args[0] === "addcomponent") {
5879
- addComponent(args);
5880
- return;
5881
- }
5882
- if (args[0] === "addpage") {
5883
- addPage(args);
5884
- return;
7290
+ async function packageVersion(context) {
7291
+ const packageJson = JSON.parse(await context.fs.readText(path10.join(context.packageRoot, "package.json")));
7292
+ return packageJson.version;
7293
+ }
7294
+ function interactiveInputError(message, hint) {
7295
+ throw new CliError(message, {
7296
+ code: "INTERACTIVE_INPUT_REQUIRED",
7297
+ hint
7298
+ });
7299
+ }
7300
+ async function executeCli(context) {
7301
+ const options = parseOptions(context.argv);
7302
+ context.outputMode = options.json ? "json" : "human";
7303
+ const args = withoutGlobalOptions(context.argv);
7304
+ const version = await packageVersion(context);
7305
+ if (options.help) {
7306
+ return commandResult(context, {
7307
+ command: "help",
7308
+ summary: "Displayed create-next-pro help.",
7309
+ projectRoot: context.cwd,
7310
+ status: "success",
7311
+ data: { help: HELP_TEXT }
7312
+ });
5885
7313
  }
5886
- if (args[0] === "addlib") {
5887
- addLib(args);
5888
- return;
7314
+ if (options.version) {
7315
+ return commandResult(context, {
7316
+ command: "version",
7317
+ summary: `create-next-pro version ${version}.`,
7318
+ projectRoot: context.cwd,
7319
+ status: "success",
7320
+ data: { version }
7321
+ });
5889
7322
  }
5890
- if (args[0] === "addapi") {
5891
- addApi(args);
5892
- return;
7323
+ const config = await readConfig(context);
7324
+ if (options.reconfigure) {
7325
+ if (options.json) {
7326
+ interactiveInputError("Reconfiguration is interactive and unavailable in JSON mode.", "Run create-next-pro --reconfigure without --json.");
7327
+ }
7328
+ return onboarding(context, version);
5893
7329
  }
5894
- if (args[0] === "addlanguage") {
5895
- await addLanguage(args);
5896
- return;
7330
+ if (!config) {
7331
+ if (options.json) {
7332
+ throw new CliError("Initial configuration is required.", {
7333
+ code: "ONBOARDING_REQUIRED",
7334
+ scope: "config",
7335
+ path: "config.json",
7336
+ hint: "Run create-next-pro once without --json, then rerun the command."
7337
+ });
7338
+ }
7339
+ return onboarding(context, version);
5897
7340
  }
5898
- if (args[0] === "addtext") {
5899
- await addText(args);
5900
- return;
7341
+ const command = args[0];
7342
+ const handler = command ? createCommandRegistry().get(command) : undefined;
7343
+ if (handler)
7344
+ return handler(args, context);
7345
+ const nameArg = args.find((argument) => !argument.startsWith("--") && argument !== "-v");
7346
+ if (nameArg)
7347
+ return createProject(nameArg, options.force, context);
7348
+ if (options.json) {
7349
+ interactiveInputError("A project name is required in JSON mode.", "Pass the project name as a positional argument.");
5901
7350
  }
5902
- if (args[0] === "rmpage") {
5903
- rmPage(args);
5904
- return;
7351
+ return createProjectWithPrompt(context);
7352
+ }
7353
+ async function runCompletion(context, args) {
7354
+ try {
7355
+ for (const candidate of await completionCandidates(args[1], context)) {
7356
+ context.terminal.log(candidate);
7357
+ }
7358
+ return 0;
7359
+ } catch {
7360
+ return 1;
5905
7361
  }
5906
- const nameArg = args.find((arg) => !arg.startsWith("--"));
5907
- if (nameArg) {
5908
- createProject(nameArg, force);
5909
- return;
7362
+ }
7363
+ async function main(context = createNodeContext()) {
7364
+ const completionArgs = withoutGlobalOptions(context.argv);
7365
+ if (completionArgs[0] === "__complete") {
7366
+ return runCompletion(context, completionArgs);
7367
+ }
7368
+ context.operations.reset();
7369
+ context.outputMode = parseOptions(context.argv).json ? "json" : "human";
7370
+ let result;
7371
+ try {
7372
+ result = await executeCli(context);
7373
+ } catch (error) {
7374
+ const command = completionArgs[0] ?? "create";
7375
+ result = failedResult(context, command, error, {
7376
+ projectRoot: context.cwd,
7377
+ configRoot: configDirectory(context),
7378
+ homeRoot: context.homeDir
7379
+ });
5910
7380
  }
5911
- await createProjectWithPrompt();
7381
+ renderResult(result, context, context.outputMode);
7382
+ return result.exitCode;
5912
7383
  }
5913
7384
 
5914
7385
  // bin.bun.ts
5915
- main();
7386
+ process.exitCode = await main();