create-next-pro-cli 0.1.27 → 0.1.29

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 (50) hide show
  1. package/README.md +97 -6
  2. package/dist/bin.bun.js +1933 -947
  3. package/dist/bin.node.js +2079 -998
  4. package/dist/bin.node.js.map +1 -1
  5. package/package.json +3 -2
  6. package/templates/Projects/default/.env.example +8 -6
  7. package/templates/Projects/default/.github/workflows/quality.yml +40 -9
  8. package/templates/Projects/default/.gitignore.template +33 -15
  9. package/templates/Projects/default/README.md +38 -9
  10. package/templates/Projects/default/bun.lock +120 -134
  11. package/templates/Projects/default/messages/en/_home.json +9 -2
  12. package/templates/Projects/default/messages/en/settings.json +9 -1
  13. package/templates/Projects/default/messages/en/userInfo.json +6 -1
  14. package/templates/Projects/default/messages/fr/_home.json +9 -2
  15. package/templates/Projects/default/messages/fr/settings.json +9 -1
  16. package/templates/Projects/default/messages/fr/userInfo.json +6 -1
  17. package/templates/Projects/default/next.config.ts +11 -0
  18. package/templates/Projects/default/package.json +7 -1
  19. package/templates/Projects/default/playwright.config.ts +29 -6
  20. package/templates/Projects/default/pnpm-workspace.yaml +2 -0
  21. package/templates/Projects/default/scripts/audit.ts +4 -0
  22. package/templates/Projects/default/scripts/package-manager.ts +68 -0
  23. package/templates/Projects/default/src/app/[locale]/(public)/_home/page.tsx +2 -2
  24. package/templates/Projects/default/src/app/[locale]/(public)/layout.tsx +1 -2
  25. package/templates/Projects/default/src/app/[locale]/(public)/login/page.tsx +6 -1
  26. package/templates/Projects/default/src/app/[locale]/(public)/register/page.tsx +6 -1
  27. package/templates/Projects/default/src/app/[locale]/(user)/userInfo/page.tsx +5 -2
  28. package/templates/Projects/default/src/app/[locale]/page.tsx +1 -4
  29. package/templates/Projects/default/src/lib/github/repository.ts +68 -0
  30. package/templates/Projects/default/src/ui/_global/GlobalMain.tsx +10 -5
  31. package/templates/Projects/default/src/ui/_global/ThemeToggle.tsx +18 -8
  32. package/templates/Projects/default/src/ui/_global/UserNav.tsx +5 -1
  33. package/templates/Projects/default/src/ui/_home/GitHubActions.tsx +57 -0
  34. package/templates/Projects/default/src/ui/_home/page-shell.tsx +16 -0
  35. package/templates/Projects/default/src/ui/_home/page-ui.tsx +24 -13
  36. package/templates/Projects/default/src/ui/dashboard/LogoutButton.tsx +1 -1
  37. package/templates/Projects/default/src/ui/dashboard/StatsCard.tsx +2 -2
  38. package/templates/Projects/default/src/ui/dashboard/WelcomeCard.tsx +4 -2
  39. package/templates/Projects/default/src/ui/dashboard/page-ui.tsx +0 -2
  40. package/templates/Projects/default/src/ui/settings/page-ui.tsx +45 -6
  41. package/templates/Projects/default/src/ui/userInfo/page-ui.tsx +54 -7
  42. package/templates/Projects/default/tests/consumer/validate-template.ts +57 -48
  43. package/templates/Projects/default/tests/e2e/template-remediation.playwright.ts +47 -4
  44. package/templates/Projects/default/tests/unit/csp.test.ts +1 -1
  45. package/templates/Projects/default/tests/unit/github-repository.test.ts +45 -0
  46. package/templates/Projects/default/tests/unit/package-manager.test.ts +51 -0
  47. package/templates/Projects/default/tests/unit/template-baseline.test.ts +52 -0
  48. package/templates/Projects/default/tests/unit/user-nav.test.tsx +52 -0
  49. package/templates/Projects/default/tsconfig.json +2 -1
  50. package/templates/Projects/default/vitest.config.ts +10 -0
package/dist/bin.bun.js CHANGED
@@ -537,7 +537,7 @@ var require_prompt = __commonJS((exports, module) => {
537
537
 
538
538
  // node_modules/prompts/dist/elements/text.js
539
539
  var require_text = __commonJS((exports, module) => {
540
- function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
540
+ function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
541
541
  try {
542
542
  var info = gen[key](arg);
543
543
  var value = info.value;
@@ -546,7 +546,7 @@ var require_text = __commonJS((exports, module) => {
546
546
  return;
547
547
  }
548
548
  if (info.done) {
549
- resolve(value);
549
+ resolve2(value);
550
550
  } else {
551
551
  Promise.resolve(value).then(_next, _throw);
552
552
  }
@@ -554,13 +554,13 @@ var require_text = __commonJS((exports, module) => {
554
554
  function _asyncToGenerator(fn) {
555
555
  return function() {
556
556
  var self = this, args = arguments;
557
- return new Promise(function(resolve, reject) {
557
+ return new Promise(function(resolve2, reject) {
558
558
  var gen = fn.apply(self, args);
559
559
  function _next(value) {
560
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
560
+ asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "next", value);
561
561
  }
562
562
  function _throw(err) {
563
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
563
+ asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "throw", err);
564
564
  }
565
565
  _next(undefined);
566
566
  });
@@ -1274,7 +1274,7 @@ var require_dateparts = __commonJS((exports, module) => {
1274
1274
 
1275
1275
  // node_modules/prompts/dist/elements/date.js
1276
1276
  var require_date = __commonJS((exports, module) => {
1277
- function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
1277
+ function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
1278
1278
  try {
1279
1279
  var info = gen[key](arg);
1280
1280
  var value = info.value;
@@ -1283,7 +1283,7 @@ var require_date = __commonJS((exports, module) => {
1283
1283
  return;
1284
1284
  }
1285
1285
  if (info.done) {
1286
- resolve(value);
1286
+ resolve2(value);
1287
1287
  } else {
1288
1288
  Promise.resolve(value).then(_next, _throw);
1289
1289
  }
@@ -1291,13 +1291,13 @@ var require_date = __commonJS((exports, module) => {
1291
1291
  function _asyncToGenerator(fn) {
1292
1292
  return function() {
1293
1293
  var self = this, args = arguments;
1294
- return new Promise(function(resolve, reject) {
1294
+ return new Promise(function(resolve2, reject) {
1295
1295
  var gen = fn.apply(self, args);
1296
1296
  function _next(value) {
1297
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
1297
+ asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "next", value);
1298
1298
  }
1299
1299
  function _throw(err) {
1300
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
1300
+ asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "throw", err);
1301
1301
  }
1302
1302
  _next(undefined);
1303
1303
  });
@@ -1501,7 +1501,7 @@ ${i ? ` ` : figures.pointerSmall} ${color.red().italic(l)}`, ``);
1501
1501
 
1502
1502
  // node_modules/prompts/dist/elements/number.js
1503
1503
  var require_number = __commonJS((exports, module) => {
1504
- function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
1504
+ function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
1505
1505
  try {
1506
1506
  var info = gen[key](arg);
1507
1507
  var value = info.value;
@@ -1510,7 +1510,7 @@ var require_number = __commonJS((exports, module) => {
1510
1510
  return;
1511
1511
  }
1512
1512
  if (info.done) {
1513
- resolve(value);
1513
+ resolve2(value);
1514
1514
  } else {
1515
1515
  Promise.resolve(value).then(_next, _throw);
1516
1516
  }
@@ -1518,13 +1518,13 @@ var require_number = __commonJS((exports, module) => {
1518
1518
  function _asyncToGenerator(fn) {
1519
1519
  return function() {
1520
1520
  var self = this, args = arguments;
1521
- return new Promise(function(resolve, reject) {
1521
+ return new Promise(function(resolve2, reject) {
1522
1522
  var gen = fn.apply(self, args);
1523
1523
  function _next(value) {
1524
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
1524
+ asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "next", value);
1525
1525
  }
1526
1526
  function _throw(err) {
1527
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
1527
+ asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "throw", err);
1528
1528
  }
1529
1529
  _next(undefined);
1530
1530
  });
@@ -1960,7 +1960,7 @@ Instructions:
1960
1960
 
1961
1961
  // node_modules/prompts/dist/elements/autocomplete.js
1962
1962
  var require_autocomplete = __commonJS((exports, module) => {
1963
- function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
1963
+ function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
1964
1964
  try {
1965
1965
  var info = gen[key](arg);
1966
1966
  var value = info.value;
@@ -1969,7 +1969,7 @@ var require_autocomplete = __commonJS((exports, module) => {
1969
1969
  return;
1970
1970
  }
1971
1971
  if (info.done) {
1972
- resolve(value);
1972
+ resolve2(value);
1973
1973
  } else {
1974
1974
  Promise.resolve(value).then(_next, _throw);
1975
1975
  }
@@ -1977,13 +1977,13 @@ var require_autocomplete = __commonJS((exports, module) => {
1977
1977
  function _asyncToGenerator(fn) {
1978
1978
  return function() {
1979
1979
  var self = this, args = arguments;
1980
- return new Promise(function(resolve, reject) {
1980
+ return new Promise(function(resolve2, reject) {
1981
1981
  var gen = fn.apply(self, args);
1982
1982
  function _next(value) {
1983
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
1983
+ asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "next", value);
1984
1984
  }
1985
1985
  function _throw(err) {
1986
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
1986
+ asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "throw", err);
1987
1987
  }
1988
1988
  _next(undefined);
1989
1989
  });
@@ -2641,7 +2641,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
2641
2641
  arr2[i] = arr[i];
2642
2642
  return arr2;
2643
2643
  }
2644
- function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
2644
+ function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
2645
2645
  try {
2646
2646
  var info = gen[key](arg);
2647
2647
  var value = info.value;
@@ -2650,7 +2650,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
2650
2650
  return;
2651
2651
  }
2652
2652
  if (info.done) {
2653
- resolve(value);
2653
+ resolve2(value);
2654
2654
  } else {
2655
2655
  Promise.resolve(value).then(_next, _throw);
2656
2656
  }
@@ -2658,13 +2658,13 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
2658
2658
  function _asyncToGenerator(fn) {
2659
2659
  return function() {
2660
2660
  var self = this, args = arguments;
2661
- return new Promise(function(resolve, reject) {
2661
+ return new Promise(function(resolve2, reject) {
2662
2662
  var gen = fn.apply(self, args);
2663
2663
  function _next(value) {
2664
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
2664
+ asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "next", value);
2665
2665
  }
2666
2666
  function _throw(err) {
2667
- asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
2667
+ asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "throw", err);
2668
2668
  }
2669
2669
  _next(undefined);
2670
2670
  });
@@ -4951,112 +4951,27 @@ var require_prompts3 = __commonJS((exports, module) => {
4951
4951
  });
4952
4952
 
4953
4953
  // src/index.ts
4954
- import path8 from "path";
4955
-
4956
- // src/cli/onboarding.ts
4957
- import path from "path";
4958
- function configDirectory(context) {
4959
- return context.env.XDG_CONFIG_HOME ? path.join(context.env.XDG_CONFIG_HOME, "create-next-pro") : path.join(context.homeDir, ".config", "create-next-pro");
4960
- }
4961
- function configFile(context) {
4962
- return path.join(configDirectory(context), "config.json");
4963
- }
4964
- async function readConfig(context) {
4965
- try {
4966
- return JSON.parse(await context.fs.readText(configFile(context)));
4967
- } catch {
4968
- return null;
4969
- }
4970
- }
4971
- async function ensureLineInRc(context, target, line) {
4972
- try {
4973
- const current = context.fs.exists(target) ? await context.fs.readText(target) : "";
4974
- if (!current.includes(line))
4975
- await context.fs.appendText(target, `
4976
- ${line}
4977
- `);
4978
- } catch {}
4979
- }
4980
- async function installCompletion(context, shell) {
4981
- const directory = configDirectory(context);
4982
- const source = path.join(context.packageRoot, shell === "zsh" ? "create-next-pro-completion.zsh" : "create-next-pro-completion.sh");
4983
- const target = path.join(directory, `completion.${shell === "zsh" ? "zsh" : "sh"}`);
4984
- await context.fs.mkdir(directory);
4985
- await context.fs.copyFile(source, target);
4986
- const rcFile = path.join(context.homeDir, shell === "zsh" ? ".zshrc" : ".bashrc");
4987
- await ensureLineInRc(context, rcFile, `source "${target}"`);
4988
- }
4989
- async function onboarding(context, version) {
4990
- context.terminal.log(`\uD83D\uDE80 Welcome to create-next-pro v${version}
4991
- `);
4992
- const response = await context.prompt([
4993
- {
4994
- type: "select",
4995
- name: "shell",
4996
- message: "Which shell do you use?",
4997
- choices: [
4998
- { title: "zsh", value: "zsh" },
4999
- { title: "bash", value: "bash" }
5000
- ],
5001
- initial: context.env.SHELL?.includes("zsh") ? 0 : 1
5002
- },
5003
- {
5004
- type: "toggle",
5005
- name: "completion",
5006
- message: "Install autocompletion?",
5007
- initial: true,
5008
- active: "Yes",
5009
- inactive: "No"
5010
- }
5011
- ], { onCancel: () => false });
5012
- if (response.shell !== "bash" && response.shell !== "zsh") {
5013
- throw new Error("Configuration cancelled.");
5014
- }
5015
- const now = new Date().toISOString();
5016
- const config = {
5017
- version: 1,
5018
- shell: response.shell,
5019
- completionInstalled: Boolean(response.completion),
5020
- createdAt: now,
5021
- updatedAt: now
5022
- };
5023
- if (config.completionInstalled)
5024
- await installCompletion(context, config.shell);
5025
- await context.fs.mkdir(configDirectory(context));
5026
- await context.fs.writeText(configFile(context), JSON.stringify(config, null, 2));
5027
- context.terminal.log(`
5028
- \u2705 Configuration saved.`);
5029
- context.terminal.log("you can now use the CLI ! ex : ");
5030
- context.terminal.log(" Without prompt (will change in future) :");
5031
- context.terminal.log(" create-next-pro my-next-project");
5032
- context.terminal.log(" With prompt :");
5033
- context.terminal.log(" create-next-pro");
5034
- context.terminal.log("For more information, visit: https://github.com/Rising-Corporation/create-next-pro-cli");
5035
- context.terminal.log("Happy coding! \uD83C\uDF89");
5036
- return config;
5037
- }
4954
+ import path10 from "path";
5038
4955
 
5039
4956
  // src/cli/completion.ts
5040
- import { readdir as readdir2 } from "fs/promises";
5041
- import path3 from "path";
4957
+ import path2 from "path";
5042
4958
 
5043
4959
  // src/core/page-catalog.ts
5044
- import { readdir } from "fs/promises";
5045
- import path2 from "path";
4960
+ import path from "path";
5046
4961
  function isRouteGroup(segment) {
5047
4962
  return segment.startsWith("(") && segment.endsWith(")");
5048
4963
  }
5049
- async function discoverPages(projectRoot) {
5050
- const appRoot = path2.join(projectRoot, "src", "app", "[locale]");
4964
+ async function discoverPages(projectRoot, fs) {
4965
+ const appRoot = path.join(projectRoot, "src", "app", "[locale]");
5051
4966
  const candidates = [];
5052
4967
  async function visit(directory, relative = []) {
5053
4968
  let entries;
5054
4969
  try {
5055
- entries = await readdir(directory, { withFileTypes: true });
4970
+ entries = await fs.list(directory);
5056
4971
  } catch {
5057
4972
  return;
5058
4973
  }
5059
- if (entries.some((entry) => entry.isFile() && entry.name === "page.tsx")) {
4974
+ if (entries.some((entry) => entry.isFile && entry.name === "page.tsx")) {
5060
4975
  const routeSegments = relative.filter((segment) => !isRouteGroup(segment));
5061
4976
  if (routeSegments.length > 0 && !routeSegments.some((segment) => segment.startsWith("_") || segment.startsWith("["))) {
5062
4977
  const logicalName = routeSegments.join(".");
@@ -5064,15 +4979,15 @@ async function discoverPages(projectRoot) {
5064
4979
  logicalName,
5065
4980
  routeSegments,
5066
4981
  routeDirectory: directory,
5067
- uiDirectory: path2.join(projectRoot, "src", "ui", ...routeSegments),
5068
- messageFile: path2.join(projectRoot, "messages", "{locale}", `${routeSegments[0]}.json`),
4982
+ uiDirectory: path.join(projectRoot, "src", "ui", ...routeSegments),
4983
+ messageFile: path.join(projectRoot, "messages", "{locale}", `${routeSegments[0]}.json`),
5069
4984
  messageKey: routeSegments.length > 1 ? routeSegments.at(-1) : undefined
5070
4985
  });
5071
4986
  }
5072
4987
  }
5073
4988
  for (const entry of entries) {
5074
- if (entry.isDirectory() && !entry.name.startsWith(".")) {
5075
- await visit(path2.join(directory, entry.name), [
4989
+ if (entry.isDirectory && !entry.name.startsWith(".")) {
4990
+ await visit(path.join(directory, entry.name), [
5076
4991
  ...relative,
5077
4992
  entry.name
5078
4993
  ]);
@@ -5094,6 +5009,7 @@ var PUBLIC_COMMANDS = [
5094
5009
  "rmpage",
5095
5010
  "--help",
5096
5011
  "--version",
5012
+ "--json",
5097
5013
  "--reconfigure"
5098
5014
  ];
5099
5015
  var OPTIONS = {
@@ -5110,9 +5026,9 @@ var OPTIONS = {
5110
5026
  ],
5111
5027
  addcomponent: ["--page", "-P"]
5112
5028
  };
5113
- async function directories(root) {
5029
+ async function directories(root, context) {
5114
5030
  try {
5115
- return (await readdir2(root, { withFileTypes: true })).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_")).map((entry) => entry.name).sort();
5031
+ return (await context.fs.list(root)).filter((entry) => entry.isDirectory && !entry.name.startsWith("_")).map((entry) => entry.name).sort();
5116
5032
  } catch {
5117
5033
  return [];
5118
5034
  }
@@ -5121,59 +5037,429 @@ async function completionCandidates(command, context) {
5121
5037
  if (!command)
5122
5038
  return [...PUBLIC_COMMANDS];
5123
5039
  if (command === "rmpage") {
5124
- return (await discoverPages(context.cwd)).map((candidate) => candidate.logicalName);
5040
+ return (await discoverPages(context.cwd, context.fs)).map((candidate) => candidate.logicalName);
5125
5041
  }
5126
5042
  if (command === "addcomponent" || command === "addpage") {
5127
5043
  return [
5128
5044
  ...OPTIONS[command] ?? [],
5129
- ...await directories(path3.join(context.cwd, "src", "ui"))
5045
+ ...await directories(path2.join(context.cwd, "src", "ui"), context)
5130
5046
  ];
5131
5047
  }
5132
5048
  if (command === "addlanguage")
5133
5049
  return ["de", "en", "es", "fr", "it", "ja", "pt"];
5134
5050
  return OPTIONS[command] ?? [];
5135
5051
  }
5136
- async function printCompletions(args, context) {
5137
- for (const candidate of await completionCandidates(args[1], context)) {
5138
- context.terminal.log(candidate);
5139
- }
5140
- }
5141
5052
 
5142
- // src/core/contracts.ts
5143
- var success = () => ({ exitCode: 0 });
5053
+ // src/cli/onboarding.ts
5054
+ import path4 from "path";
5144
5055
 
5056
+ // src/core/operations.ts
5057
+ import path3 from "path";
5058
+
5059
+ // src/core/contracts.ts
5145
5060
  class CliError extends Error {
5146
5061
  exitCode;
5147
- constructor(message, exitCode = 1) {
5062
+ code;
5063
+ hint;
5064
+ scope;
5065
+ path;
5066
+ constructor(message, options = {}) {
5148
5067
  super(message);
5149
- this.exitCode = exitCode;
5150
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
+ }
5151
5439
  }
5152
5440
  }
5153
5441
 
5154
5442
  // src/lib/addApi.ts
5155
- var import_prompts2 = __toESM(require_prompts3(), 1);
5156
5443
  import { join as join2 } from "path";
5157
- import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
5158
- import { existsSync as existsSync3 } from "fs";
5159
5444
 
5160
5445
  // src/lib/utils.ts
5161
- import { readFile } from "fs/promises";
5162
- import { existsSync } from "fs";
5163
5446
  import { join } from "path";
5164
5447
  function capitalize(str) {
5165
5448
  return str.charAt(0).toUpperCase() + str.slice(1);
5166
5449
  }
5450
+ function toIdentifier(value) {
5451
+ return value.replace(/[-_]+([A-Za-z0-9])/g, (_, character) => character.toUpperCase());
5452
+ }
5167
5453
  function configuredAliasPrefix(config) {
5168
5454
  const alias = config.importAlias ?? "@/*";
5169
5455
  return alias.endsWith("/*") ? alias.slice(0, -2) : "@";
5170
5456
  }
5171
- async function loadConfig(cwd = process.cwd()) {
5172
- const configPath = join(cwd, "cnp.config.json");
5173
- if (!existsSync(configPath))
5457
+ async function loadConfig(context) {
5458
+ const configPath = join(context.cwd, "cnp.config.json");
5459
+ if (!context.fs.exists(configPath))
5174
5460
  return null;
5175
5461
  try {
5176
- const raw = await readFile(configPath, "utf-8");
5462
+ const raw = await context.fs.readText(configPath);
5177
5463
  return JSON.parse(raw);
5178
5464
  } catch {
5179
5465
  return null;
@@ -5204,67 +5490,8 @@ function toFileName(key) {
5204
5490
  }
5205
5491
  }
5206
5492
 
5207
- // src/runtime/node-context.ts
5208
- var import_prompts = __toESM(require_prompts3(), 1);
5209
- import { existsSync as existsSync2 } from "fs";
5210
- import {
5211
- appendFile,
5212
- copyFile,
5213
- mkdir,
5214
- readFile as readFile2,
5215
- writeFile
5216
- } from "fs/promises";
5217
- import os from "os";
5218
- import path4 from "path";
5219
- import { fileURLToPath } from "url";
5220
- function findPackageRoot(start) {
5221
- let current = start;
5222
- while (true) {
5223
- const packagePath = path4.join(current, "package.json");
5224
- if (existsSync2(packagePath))
5225
- return current;
5226
- const parent = path4.dirname(current);
5227
- if (parent === current) {
5228
- throw new Error(`Unable to locate package.json from ${start}`);
5229
- }
5230
- current = parent;
5231
- }
5232
- }
5233
- function resolvePackageRoot(metaUrl = import.meta.url) {
5234
- return findPackageRoot(path4.dirname(fileURLToPath(metaUrl)));
5235
- }
5236
- function createNodeContext(overrides = {}) {
5237
- return {
5238
- argv: process.argv.slice(2),
5239
- cwd: process.cwd(),
5240
- env: process.env,
5241
- homeDir: os.homedir(),
5242
- packageRoot: resolvePackageRoot(),
5243
- terminal: console,
5244
- prompt: import_prompts.default,
5245
- fs: {
5246
- exists: existsSync2,
5247
- readText: (target) => readFile2(target, "utf8"),
5248
- writeText: async (target, content) => {
5249
- await writeFile(target, content);
5250
- },
5251
- mkdir: async (target) => {
5252
- await mkdir(target, { recursive: true });
5253
- },
5254
- copyFile: async (source, target) => {
5255
- await copyFile(source, target);
5256
- },
5257
- appendText: async (target, content) => {
5258
- await appendFile(target, content);
5259
- }
5260
- },
5261
- ...overrides
5262
- };
5263
- }
5264
-
5265
5493
  // src/core/project-paths.ts
5266
- import path5 from "path";
5267
- import { lstat } from "fs/promises";
5494
+ import path6 from "path";
5268
5495
  var SAFE_SEGMENT = /^[A-Za-z][A-Za-z0-9_-]*$/;
5269
5496
  function hasControlCharacters(value) {
5270
5497
  return [...value].some((character) => {
@@ -5274,740 +5501,1196 @@ function hasControlCharacters(value) {
5274
5501
  }
5275
5502
  function parseLogicalName(value, label = "name") {
5276
5503
  if (!value || hasControlCharacters(value)) {
5277
- throw new CliError(`Invalid ${label}: a non-empty printable value is required.`);
5504
+ throw new CliError(`Invalid ${label}: a non-empty printable value is required.`, { code: "INVALID_ARGUMENT" });
5278
5505
  }
5279
- if (path5.isAbsolute(value) || value.includes("/") || value.includes("\\")) {
5280
- throw new CliError(`Invalid ${label}: paths and separators are not allowed.`);
5506
+ if (path6.isAbsolute(value) || value.includes("/") || value.includes("\\")) {
5507
+ throw new CliError(`Invalid ${label}: paths and separators are not allowed.`, { code: "INVALID_ARGUMENT" });
5281
5508
  }
5282
5509
  const segments = value.split(".");
5283
5510
  if (segments.some((segment) => !SAFE_SEGMENT.test(segment))) {
5284
- throw new CliError(`Invalid ${label}: use dot-separated alphanumeric segments beginning with a letter.`);
5511
+ throw new CliError(`Invalid ${label}: use dot-separated alphanumeric segments beginning with a letter.`, { code: "INVALID_ARGUMENT" });
5285
5512
  }
5286
5513
  return segments;
5287
5514
  }
5288
5515
  function validateProjectName(value) {
5289
- if (!value || hasControlCharacters(value) || path5.isAbsolute(value) || value === "." || value === ".." || value.includes("/") || value.includes("\\") || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value)) {
5290
- throw new CliError("Invalid project name: use letters, numbers, dots, dashes or underscores without path separators.");
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" });
5291
5518
  }
5292
5519
  return value;
5293
5520
  }
5294
5521
  function resolveInside(root, ...segments) {
5295
- const absoluteRoot = path5.resolve(root);
5296
- const target = path5.resolve(absoluteRoot, ...segments);
5297
- if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${path5.sep}`)) {
5298
- throw new CliError(`Refusing to access a path outside the project: ${target}`);
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" });
5299
5526
  }
5300
5527
  return target;
5301
5528
  }
5302
- async function assertSafeTarget(root, target) {
5303
- const safeTarget = resolveInside(root, path5.relative(root, target));
5304
- const relativeSegments = path5.relative(path5.resolve(root), safeTarget).split(path5.sep);
5305
- let current = path5.resolve(root);
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);
5306
5533
  for (const segment of relativeSegments) {
5307
5534
  if (!segment)
5308
5535
  continue;
5309
- current = path5.join(current, segment);
5310
- try {
5311
- if ((await lstat(current)).isSymbolicLink()) {
5312
- throw new CliError(`Symbolic links are forbidden in project paths: ${current}`);
5313
- }
5314
- } catch (error) {
5315
- if (error instanceof CliError)
5316
- throw error;
5317
- if (error.code !== "ENOENT")
5318
- throw error;
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" });
5319
5540
  }
5320
5541
  }
5321
5542
  return safeTarget;
5322
5543
  }
5323
5544
  function normalizeImportAlias(value) {
5324
5545
  if (!/^[A-Za-z@~][A-Za-z0-9@~_-]*\/\*$/.test(value)) {
5325
- throw new CliError('Invalid import alias: expected a prefix followed by "/*" (for example "@/*" or "@core/*").');
5546
+ throw new CliError('Invalid import alias: expected a prefix followed by "/*" (for example "@/*" or "@core/*").', { code: "INVALID_ARGUMENT" });
5326
5547
  }
5327
5548
  return value;
5328
5549
  }
5329
5550
 
5330
5551
  // src/lib/addApi.ts
5331
- async function addApi(args, cwd = process.cwd()) {
5552
+ var addApi = async (args, context) => {
5332
5553
  let apiName = args[1];
5333
5554
  if (!apiName || apiName.startsWith("-")) {
5334
- const response = await import_prompts2.default.prompt({
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({
5335
5562
  type: "text",
5336
5563
  name: "apiName",
5337
- message: "\uD83D\uDD0C API route name to add:",
5564
+ message: "API route name to add:",
5338
5565
  validate: (name) => name ? true : "API route name is required"
5339
5566
  });
5340
- apiName = response.apiName;
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
+ });
5582
+ }
5341
5583
  }
5342
5584
  const apiSegments = parseLogicalName(apiName, "API route name");
5343
- const config = await loadConfig(cwd);
5585
+ const config = await loadConfig(context);
5344
5586
  if (!config) {
5345
- console.error("\u274C Configuration file cnp.config.json not found. Run this command from the project root.");
5346
- return;
5347
- }
5348
- const apiDir = join2(cwd, "src", "app", "api", ...apiSegments);
5349
- await assertSafeTarget(cwd, apiDir);
5350
- if (!existsSync3(apiDir)) {
5351
- await mkdir2(apiDir, { recursive: true });
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
+ });
5352
5591
  }
5353
- const templateDir = join2(resolvePackageRoot(import.meta.url), "templates", "Api");
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");
5354
5596
  const routeTemplate = join2(templateDir, "route.ts");
5355
5597
  const routePath = join2(apiDir, "route.ts");
5356
- if (!existsSync3(routePath)) {
5357
- if (existsSync3(routeTemplate)) {
5358
- let content = await readFile3(routeTemplate, "utf-8");
5359
- content = content.replace(/template/g, apiName);
5360
- await writeFile2(routePath, content);
5361
- } else {
5362
- await writeFile2(routePath, `import { NextResponse } from "next/server";
5363
-
5364
- export async function GET() {
5365
- return NextResponse.json({ message: "Hello from ${apiName}" });
5366
- }
5367
- `);
5368
- }
5369
- console.log(`\uD83D\uDCC4 File created: ${routePath}`);
5370
- } else {
5371
- console.log(`\u2139\uFE0F File already exists: ${routePath}`);
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
+ });
5372
5604
  }
5373
- console.log(`\u2705 API route "${apiName}" added.`);
5374
- }
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"]
5631
+ }
5632
+ ]
5633
+ });
5634
+ };
5375
5635
 
5376
5636
  // src/lib/addComponent.ts
5377
- var import_prompts3 = __toESM(require_prompts3(), 1);
5378
5637
  import { join as join3 } from "path";
5379
- import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3, readdir as readdir3 } from "fs/promises";
5380
- import { existsSync as existsSync4, statSync } from "fs";
5381
- async function addComponent(args, cwd = process.cwd()) {
5638
+ var addComponent = async (args, context) => {
5382
5639
  let componentName = args[1];
5383
- let pageScope = null;
5384
- const pageIndex = args.findIndex((arg) => arg === "-P" || arg === "--page");
5385
- if (pageIndex !== -1 && args[pageIndex + 1]) {
5386
- pageScope = args[pageIndex + 1];
5387
- }
5388
- let nestedPath = null;
5389
- if (pageScope && pageScope.includes(".")) {
5390
- nestedPath = join3(...pageScope.split("."));
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"
5645
+ });
5391
5646
  }
5392
5647
  if (!componentName || componentName.startsWith("-")) {
5393
- const response = await import_prompts3.default.prompt({
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({
5394
5655
  type: "text",
5395
5656
  name: "componentName",
5396
- message: "\uD83E\uDDE9 Component name to add:",
5657
+ message: "Component name to add:",
5397
5658
  validate: (name) => name ? true : "Component name is required"
5398
5659
  });
5399
- componentName = response.componentName;
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
+ }
5400
5676
  }
5401
- parseLogicalName(componentName, "component name");
5402
- if (pageScope)
5403
- parseLogicalName(pageScope, "page name");
5404
- const config = await loadConfig(cwd);
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
+ });
5682
+ }
5683
+ const pageSegments = pageScope ? parseLogicalName(pageScope, "page name") : [];
5684
+ const config = await loadConfig(context);
5405
5685
  if (!config) {
5406
- console.error("\u274C Configuration file cnp.config.json not found. Run this command from the project root.");
5407
- 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
+ });
5408
5690
  }
5409
- const useI18n = !!config.useI18n;
5410
- const componentNameUpper = capitalize(componentName);
5411
- const templatePath = join3(resolvePackageRoot(import.meta.url), "templates", "Component");
5412
- let messagesPath = null;
5413
- if (useI18n) {
5414
- messagesPath = join3(cwd, "messages");
5415
- if (!existsSync4(messagesPath)) {
5416
- console.error("\u274C Messages directory missing. Ensure i18n was configured.");
5417
- return;
5418
- }
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
+ });
5419
5701
  }
5420
- let componentTargetPath;
5421
- let translationKey;
5422
- if (pageScope) {
5423
- if (nestedPath) {
5424
- componentTargetPath = join3(cwd, "src", "ui", nestedPath);
5425
- translationKey = pageScope;
5426
- } else {
5427
- componentTargetPath = join3(cwd, "src", "ui", pageScope);
5428
- translationKey = pageScope;
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
+ });
5429
5716
  }
5430
- } else {
5431
- componentTargetPath = join3(cwd, "src", "ui", "_global");
5432
- translationKey = "_global_ui";
5433
- }
5434
- await assertSafeTarget(cwd, componentTargetPath);
5435
- if (!existsSync4(componentTargetPath)) {
5436
- await mkdir3(componentTargetPath, { recursive: true });
5437
- }
5438
- const componentFile = join3(componentTargetPath, `${componentNameUpper}.tsx`);
5439
- const templateComponentPath = join3(templatePath, "Component.tsx");
5440
- if (existsSync4(templateComponentPath)) {
5441
- let content = await readFile4(templateComponentPath, "utf-8");
5442
- content = content.replace(/Component/g, componentNameUpper).replace(/componentPage/g, translationKey);
5443
- await writeFile3(componentFile, content);
5444
- console.log(`\uD83D\uDCC4 File created: ${componentFile}`);
5445
- } else {
5446
- console.error("\u274C Template Component.tsx introuvable :", templateComponentPath);
5447
- }
5448
- if (useI18n && messagesPath) {
5449
- const entries = await readdir3(messagesPath, { withFileTypes: true });
5450
- const langDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
5451
- const jsonTemplate = join3(templatePath, "component.json");
5452
- if (!existsSync4(jsonTemplate)) {
5453
- console.error("\u274C Template component.json not found:", jsonTemplate);
5454
- return;
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
+ });
5455
5725
  }
5456
- const jsonContent = await readFile4(jsonTemplate, "utf-8");
5457
- const parsed = JSON.parse(jsonContent);
5458
- for (const locale of langDirs) {
5459
- const localeDir = join3(messagesPath, locale);
5460
- if (!existsSync4(localeDir) || !statSync(localeDir).isDirectory())
5461
- continue;
5462
- let jsonTarget;
5463
- if (pageScope) {
5464
- const [messageFile] = pageScope.split(".");
5465
- jsonTarget = join3(messagesPath, locale, `${messageFile}.json`);
5466
- } else {
5467
- jsonTarget = join3(messagesPath, locale, `_global_ui.json`);
5468
- }
5469
- let current = {};
5470
- if (existsSync4(jsonTarget)) {
5471
- const jsonFile = await readFile4(jsonTarget, "utf-8");
5472
- current = JSON.parse(jsonFile);
5473
- }
5474
- if (pageScope?.includes(".")) {
5475
- const child = pageScope.split(".")[1];
5476
- const childMessages = current[child] && typeof current[child] === "object" ? current[child] : {};
5477
- childMessages[componentNameUpper] = parsed;
5478
- current[child] = childMessages;
5479
- } else {
5480
- current[componentNameUpper] = parsed;
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
+ });
5739
+ }
5481
5740
  }
5482
- await writeFile3(jsonTarget, JSON.stringify(current, null, 2));
5483
- console.log(`\uD83D\uDCC4 File updated: ${jsonTarget}`);
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
+ });
5484
5766
  }
5485
- } else {
5486
- console.log("\u2139\uFE0F Skipping translation entries; next-intl not enabled.");
5487
5767
  }
5488
- console.log(`\u2705 Component "${componentNameUpper}" added ${pageScope ? `to page ${pageScope}` : "globally"}${useI18n ? " with localized messages" : ""}.`);
5489
- }
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
+ });
5794
+ }
5795
+ }
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
+ };
5490
5824
 
5491
5825
  // src/lib/addLanguage.ts
5492
- var import_prompts4 = __toESM(require_prompts3(), 1);
5493
5826
  import { join as join4 } from "path";
5494
- import { existsSync as existsSync5 } from "fs";
5495
- import { cp, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
5496
5827
  function generateLocales() {
5497
- const dn = new Intl.DisplayNames(["en"], { type: "language" });
5828
+ const names = new Intl.DisplayNames(["en"], { type: "language" });
5498
5829
  const locales = [];
5499
- for (let i = 0;i < 26; i++) {
5500
- for (let j = 0;j < 26; j++) {
5501
- const code = String.fromCharCode(97 + i) + String.fromCharCode(97 + j);
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);
5502
5833
  try {
5503
- const name = dn.of(code);
5504
- if (name && name.toLowerCase() !== code) {
5834
+ const name = names.of(code);
5835
+ if (name && name.toLowerCase() !== code)
5505
5836
  locales.push(code);
5506
- }
5507
5837
  } catch {}
5508
5838
  }
5509
5839
  }
5510
5840
  return locales.sort();
5511
5841
  }
5512
- async function addLanguage(args, cwd = process.cwd()) {
5513
- const config = await loadConfig(cwd);
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);
5514
5865
  if (!config?.useI18n) {
5515
- console.error("\u274C i18n is not enabled in this project.");
5516
- return;
5866
+ throw new CliError("Internationalization is not enabled in this project.", {
5867
+ code: "I18N_DISABLED"
5868
+ });
5517
5869
  }
5518
- const messagesPath = join4(cwd, "messages");
5519
- if (!existsSync5(messagesPath)) {
5520
- console.error("\u274C Messages directory missing. Ensure i18n was configured.");
5521
- 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
+ });
5522
5877
  }
5523
5878
  const available = generateLocales();
5524
5879
  let locale = args[1];
5525
- if (!locale || !available.includes(locale)) {
5526
- const response = await import_prompts4.default({
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({
5527
5888
  type: "autocomplete",
5528
5889
  name: "locale",
5529
- message: "\uD83C\uDF10 Locale to add:",
5530
- choices: available.map((l) => ({ title: l, value: l }))
5890
+ message: "Locale to add:",
5891
+ choices: available.map((value) => ({ title: value, value }))
5531
5892
  });
5532
- locale = response.locale;
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
+ }
5533
5909
  }
5534
- if (!locale)
5535
- return;
5536
5910
  parseLogicalName(locale, "locale");
5537
- await assertSafeTarget(cwd, messagesPath);
5538
- if (existsSync5(join4(messagesPath, locale))) {
5539
- throw new Error(`Locale ${locale} already exists.`);
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
+ });
5540
5925
  }
5541
- const routingFile = join4(cwd, "src", "lib", "i18n", "routing.ts");
5542
- const messagesRegistryFile = join4(cwd, "src", "lib", "i18n", "messages.ts");
5543
- if (!existsSync5(routingFile)) {
5544
- throw new Error("routing.ts not found. Are you in project root?");
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
+ });
5545
5934
  }
5546
- if (!existsSync5(messagesRegistryFile)) {
5547
- throw new Error("messages.ts not found. Are you in project root?");
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
+ });
5548
5943
  }
5549
- const routingContent = await readFile5(routingFile, "utf-8");
5550
- const defaultMatch = routingContent.match(/defaultLocale:\s*"([^"]+)"/);
5551
- const defaultLocale = defaultMatch ? defaultMatch[1] : null;
5552
- if (!defaultLocale || !existsSync5(join4(messagesPath, defaultLocale))) {
5553
- throw new Error("Default locale not found.");
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
+ });
5554
5950
  }
5555
- const defaultAggregatorFile = join4(messagesPath, `${defaultLocale}.ts`);
5556
- if (!existsSync5(defaultAggregatorFile)) {
5557
- throw new Error(`Default locale aggregator not found: ${defaultLocale}.ts`);
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
+ });
5558
5967
  }
5559
- const localesMatch = routingContent.match(/locales:\s*\[([^\]]*)\]/);
5968
+ const localesMatch = routing.match(/locales:\s*\[([^\]]*)\]/);
5560
5969
  if (!localesMatch) {
5561
- throw new Error("Unable to locate routing locales.");
5562
- }
5563
- const localesArr = localesMatch[1].split(",").map((s) => s.trim().replace(/["']/g, "")).filter(Boolean);
5564
- if (localesArr.includes(locale)) {
5565
- throw new Error(`Locale ${locale} already exists in routing.`);
5566
- }
5567
- const newLocales = `locales: [${[...localesArr, locale].map((item) => `"${item}"`).join(", ")}]`;
5568
- const nextRoutingContent = routingContent.replace(/locales:\s*\[[^\]]*\]/, newLocales);
5569
- const defaultAggregatorContent = await readFile5(defaultAggregatorFile, "utf-8");
5570
- const defaultImportPrefix = `./${defaultLocale}/`;
5571
- if (!defaultAggregatorContent.includes(defaultImportPrefix)) {
5572
- throw new Error(`Default locale aggregator does not import from ${defaultImportPrefix}`);
5573
- }
5574
- const nextAggregatorContent = defaultAggregatorContent.replaceAll(defaultImportPrefix, `./${locale}/`);
5575
- const messagesRegistryContent = await readFile5(messagesRegistryFile, "utf-8");
5576
- const registryMatch = messagesRegistryContent.match(/const messages = \{([^}]*)\} as const;/);
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;/);
5577
5979
  if (!registryMatch) {
5578
- throw new Error("Unable to locate the typed messages registry.");
5579
- }
5580
- const registeredLocales = registryMatch[1].split(",").map((item) => item.trim()).filter(Boolean);
5581
- if (registeredLocales.includes(locale)) {
5582
- throw new Error(`Locale ${locale} already exists in messages registry.`);
5583
- }
5584
- const registryDeclaration = `const messages = { ${[
5585
- ...registeredLocales,
5586
- locale
5587
- ].join(", ")} } as const;`;
5588
- const declarationIndex = messagesRegistryContent.indexOf("const messages =");
5589
- const nextMessagesRegistryContent = messagesRegistryContent.slice(0, declarationIndex) + `import ${locale} from "../../../messages/${locale}";
5590
-
5591
- ` + messagesRegistryContent.slice(declarationIndex).replace(/const messages = \{[^}]*\} as const;/, registryDeclaration);
5592
- await cp(join4(messagesPath, defaultLocale), join4(messagesPath, locale), {
5593
- recursive: true
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 }
5594
6041
  });
5595
- console.log(`\uD83D\uDCC4 Directory created: ${join4(messagesPath, locale)}`);
5596
- await writeFile4(join4(messagesPath, `${locale}.ts`), nextAggregatorContent);
5597
- console.log(`\uD83D\uDCC4 File created: ${join4(messagesPath, `${locale}.ts`)}`);
5598
- await writeFile4(messagesRegistryFile, nextMessagesRegistryContent);
5599
- console.log(`\uD83D\uDCC4 File updated: ${messagesRegistryFile}`);
5600
- await writeFile4(routingFile, nextRoutingContent);
5601
- console.log(`\uD83D\uDCC4 File updated: ${routingFile}`);
5602
- console.log(`\u2705 Locale "${locale}" added and copied from default locale "${defaultLocale}".`);
5603
- }
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
+ };
5604
6083
 
5605
6084
  // src/lib/addLib.ts
5606
- var import_prompts5 = __toESM(require_prompts3(), 1);
5607
6085
  import { join as join5 } from "path";
5608
- import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
5609
- import { existsSync as existsSync6 } from "fs";
5610
- async function addLib(args, cwd = process.cwd()) {
6086
+ var addLib = async (args, context) => {
5611
6087
  let libArg = args[1];
5612
6088
  if (!libArg || libArg.startsWith("-")) {
5613
- const response = await import_prompts5.default.prompt({
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({
5614
6096
  type: "text",
5615
6097
  name: "libArg",
5616
- message: "\uD83D\uDCE6 Lib name to add:",
5617
- validate: (name) => name ? true : "Lib name is required"
6098
+ message: "Library name to add:",
6099
+ validate: (name) => name ? true : "Library name is required"
5618
6100
  });
5619
- libArg = response.libArg;
5620
- }
5621
- const libSegments = parseLogicalName(libArg, "library name");
5622
- let libName = libArg;
5623
- let fileName = null;
5624
- if (libSegments.length === 2) {
5625
- [libName, fileName] = libSegments;
5626
- } else if (libSegments.length > 2) {
5627
- throw new Error("Libraries currently support exactly library.module.");
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
+ }
5628
6117
  }
5629
- const config = await loadConfig(cwd);
5630
- if (!config) {
5631
- console.error("\u274C Configuration file cnp.config.json not found. Run this command from the project root.");
5632
- return;
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
+ });
5633
6123
  }
5634
- const libDir = join5(cwd, "src", "lib", libName);
5635
- await assertSafeTarget(cwd, libDir);
5636
- if (!existsSync6(libDir)) {
5637
- await mkdir4(libDir, { recursive: true });
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
+ });
5638
6130
  }
5639
- const templateDir = join5(resolvePackageRoot(import.meta.url), "templates", "Lib");
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");
5640
6135
  const indexTemplate = join5(templateDir, "index.ts");
5641
- const fileTemplate = join5(templateDir, "item.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
+ });
5642
6148
  const indexPath = join5(libDir, "index.ts");
5643
- if (!existsSync6(indexPath)) {
5644
- if (existsSync6(indexTemplate)) {
5645
- const content = await readFile6(indexTemplate, "utf-8");
5646
- await writeFile5(indexPath, content);
5647
- } else {
5648
- await writeFile5(indexPath, `export {}
5649
- `);
5650
- }
5651
- console.log(`\uD83D\uDCC4 File created: ${indexPath}`);
6149
+ if (!context.fs.exists(indexPath)) {
6150
+ await gateway.write(indexPath, await context.fs.readText(indexTemplate), {
6151
+ role: "library-index"
6152
+ });
5652
6153
  }
6154
+ let moduleAction;
5653
6155
  if (fileName) {
5654
- const filePath = join5(libDir, `${fileName}.ts`);
5655
- if (!existsSync6(filePath)) {
5656
- if (existsSync6(fileTemplate)) {
5657
- let content = await readFile6(fileTemplate, "utf-8");
5658
- content = content.replace(/template/g, fileName).replace(/Template/g, capitalize(fileName));
5659
- await writeFile5(filePath, content);
5660
- } else {
5661
- await writeFile5(filePath, `export function ${fileName}() {
5662
- // TODO: implement
5663
- }
5664
- `);
5665
- }
5666
- console.log(`\uD83D\uDCC4 File created: ${filePath}`);
5667
- }
5668
- let indexContent = await readFile6(indexPath, "utf-8");
5669
- const importLine = `import { ${fileName} } from "./${fileName}";`;
5670
- const importRegex = new RegExp(`import\\s*{\\s*${fileName}\\s*}\\s*from\\s*"\\./${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}["'];`);
5671
6166
  const exportRegex = /export\s*{([^}]*)}/m;
5672
- const imports = [];
5673
- const exportsSet = [];
5674
- for (const line of indexContent.split(`
5675
- `)) {
5676
- if (line.startsWith("import")) {
5677
- imports.push(line);
5678
- } else if (line.startsWith("export")) {
5679
- const match = line.match(exportRegex);
5680
- if (match && match[1]) {
5681
- exportsSet.push(...match[1].split(",").map((s) => s.trim()).filter(Boolean));
5682
- }
5683
- }
5684
- }
5685
- if (!imports.some((l) => importRegex.test(l))) {
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)))
5686
6172
  imports.push(importLine);
5687
- }
5688
- if (!exportsSet.includes(fileName)) {
5689
- exportsSet.push(fileName);
5690
- }
5691
- indexContent = imports.join(`
5692
- `) + `
6173
+ if (!exportsSet.includes(moduleIdentifier))
6174
+ exportsSet.push(moduleIdentifier);
6175
+ const nextIndex = `${imports.join(`
6176
+ `)}
5693
6177
 
5694
- export { ` + exportsSet.join(", ") + ` };
6178
+ export { ${exportsSet.join(", ")} };
5695
6179
  `;
5696
- await writeFile5(indexPath, indexContent);
5697
- console.log(`\u270F\uFE0F Updated index: ${indexPath}`);
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
+ });
5698
6198
  }
5699
- console.log(`\u2705 Lib "${libName}"${fileName ? ` with module ${fileName}` : ""} added.`);
5700
- }
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
+ };
5701
6215
 
5702
6216
  // src/lib/addPage.ts
5703
- var import_prompts6 = __toESM(require_prompts3(), 1);
5704
6217
  import { join as join6 } from "path";
5705
- import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile6, readdir as readdir4 } from "fs/promises";
5706
- import { existsSync as existsSync7, statSync as statSync2 } from "fs";
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
+ };
5707
6240
  function registerMessagesFile(content, locale, fileName) {
5708
6241
  const importPath = `./${locale}/${fileName}.json`;
5709
6242
  if (content.includes(importPath))
5710
6243
  return content;
5711
6244
  const declarationIndex = content.indexOf("const messages =");
5712
6245
  const registryMatch = content.match(/const messages = \{([\s\S]*?)\n\};/);
5713
- if (declarationIndex === -1 || !registryMatch) {
5714
- throw new Error(`Unable to register ${fileName}.json in messages/${locale}.ts`);
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
+ });
5715
6252
  }
5716
- const importStatement = `import ${fileName} from "${importPath}";
6253
+ const identifier = toIdentifier(fileName);
6254
+ const importStatement = `import ${identifier} from "${importPath}";
5717
6255
  `;
5718
6256
  const nextRegistry = registryMatch[0].replace(/\n\};$/, `
5719
- ${fileName},
6257
+ ${identifier},
5720
6258
  };`);
5721
6259
  return content.slice(0, declarationIndex) + importStatement + content.slice(declarationIndex).replace(registryMatch[0], nextRegistry);
5722
6260
  }
5723
- async function addPage(args, cwd = process.cwd()) {
5724
- let pageName = args[1];
5725
- if (!pageName || pageName.startsWith("-")) {
5726
- const response = await import_prompts6.default.prompt({
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({
5727
6299
  type: "text",
5728
6300
  name: "pageName",
5729
- message: "\uD83D\uDCDD Page name to add:",
6301
+ message: "Page name to add:",
5730
6302
  validate: (name) => name ? true : "Page name is required"
5731
6303
  });
5732
- pageName = response.pageName;
5733
- }
5734
- const pageSegments = parseLogicalName(pageName, "page name");
5735
- let parentName = null;
5736
- let childName = null;
5737
- if (pageSegments.length === 2) {
5738
- [parentName, childName] = pageSegments;
5739
- } else if (pageSegments.length > 2) {
5740
- throw new Error("Nested pages currently support exactly Parent.Child.");
5741
- }
5742
- let shortFlags = args.find((arg) => /^-[A-Za-z]+$/.test(arg));
5743
- const longFlags = new Set(args.filter((a) => a.startsWith("--")));
5744
- const flags = new Set;
5745
- if (!shortFlags && Array.from(longFlags).length === 0) {
5746
- shortFlags = "-LPl";
5747
- }
5748
- if (shortFlags) {
5749
- for (const char of shortFlags.slice(1)) {
5750
- switch (char) {
5751
- case "L":
5752
- flags.add("layout");
5753
- break;
5754
- case "P":
5755
- flags.add("page");
5756
- break;
5757
- case "l":
5758
- flags.add("loading");
5759
- break;
5760
- case "n":
5761
- flags.add("not-found");
5762
- break;
5763
- case "e":
5764
- flags.add("error");
5765
- break;
5766
- case "g":
5767
- flags.add("global-error");
5768
- break;
5769
- case "r":
5770
- flags.add("route");
5771
- break;
5772
- case "t":
5773
- flags.add("template");
5774
- break;
5775
- case "d":
5776
- flags.add("default");
5777
- break;
5778
- }
5779
- }
5780
- }
5781
- for (const flag of [
5782
- "layout",
5783
- "page",
5784
- "loading",
5785
- "not-found",
5786
- "error",
5787
- "global-error",
5788
- "route",
5789
- "template",
5790
- "default"
5791
- ]) {
5792
- if (longFlags.has("--" + flag))
5793
- flags.add(flag);
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
+ });
5794
6326
  }
5795
- const config = await loadConfig(cwd);
6327
+ const flags = selectedFlags(args);
6328
+ const config = await loadConfig(context);
5796
6329
  if (!config) {
5797
- console.error("\u274C Configuration file cnp.config.json not found. Run this command from the project root.");
5798
- return;
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
+ });
5799
6334
  }
5800
- const useI18n = !!config.useI18n;
6335
+ const useI18n = Boolean(config.useI18n);
5801
6336
  const aliasPrefix = configuredAliasPrefix(config);
5802
- const srcSegments = ["src", "app"];
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
+ ];
5803
6364
  if (useI18n)
5804
- srcSegments.push("[locale]");
5805
- const srcPath = join6(cwd, ...srcSegments);
5806
- if (!existsSync7(srcPath)) {
5807
- console.error(`\u274C Expected directory not found: ${srcPath}`);
5808
- return;
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
+ });
5809
6373
  }
5810
- let messagesPath = null;
5811
- let locales = [];
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 = [];
5812
6377
  if (useI18n) {
5813
- messagesPath = join6(cwd, "messages");
5814
- if (!existsSync7(messagesPath)) {
5815
- console.error("\u274C Messages directory missing. Ensure i18n was configured.");
5816
- return;
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
+ });
5817
6385
  }
5818
- const entries = await readdir4(messagesPath, { withFileTypes: true });
5819
- locales = entries.filter((e) => e.isDirectory()).map((e) => e.name);
5820
- }
5821
- const templatePath = join6(resolvePackageRoot(import.meta.url), "templates", "Page");
5822
- let uiPageDir, localePagePath, jsonFileName;
5823
- if (parentName && childName) {
5824
- uiPageDir = join6(cwd, "src", "ui", parentName, childName);
5825
- localePagePath = join6(srcPath, parentName, childName);
5826
- jsonFileName = parentName;
5827
- } else {
5828
- uiPageDir = join6(cwd, "src", "ui", pageName);
5829
- localePagePath = join6(srcPath, pageName);
5830
- jsonFileName = pageName;
5831
- }
5832
- await assertSafeTarget(cwd, uiPageDir);
5833
- await assertSafeTarget(cwd, localePagePath);
5834
- if (!existsSync7(uiPageDir)) {
5835
- await mkdir5(uiPageDir, { recursive: true });
5836
- }
5837
- const uiPageFile = join6(uiPageDir, "page-ui.tsx");
5838
- const uiPageTemplate = join6(templatePath, "page-ui.tsx");
5839
- if (existsSync7(uiPageTemplate)) {
5840
- let uiContent = await readFile7(uiPageTemplate, "utf-8");
5841
- const translationNamespace = parentName && childName ? `${parentName}.${childName}` : pageName;
5842
- uiContent = uiContent.replace('useTranslations("template")', `useTranslations("${translationNamespace}")`);
5843
- uiContent = uiContent.replaceAll('from "@/', `from "${aliasPrefix}/`).replace(/template/g, childName || pageName).replace(/Template/g, capitalize(childName || pageName));
5844
- await writeFile6(uiPageFile, uiContent);
5845
- console.log(`\uD83D\uDCC4 File created: ${uiPageFile}`);
5846
- } else {
5847
- console.warn("\u26A0\uFE0F Missing template file: page-ui.tsx at path:", uiPageTemplate);
5848
- }
5849
- if (!existsSync7(localePagePath)) {
5850
- await mkdir5(localePagePath, { recursive: true });
5851
- }
5852
- for (const flag of flags) {
5853
- const filename = toFileName(flag);
5854
- const src = join6(templatePath, filename);
5855
- const dst = join6(localePagePath, filename);
5856
- if (!existsSync7(src)) {
5857
- console.warn(`\u26A0\uFE0F Missing template file: ${filename} at path: ${src}`);
5858
- continue;
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
+ });
5859
6393
  }
5860
- const content = await readFile7(src, "utf-8");
5861
- const uiImportPath = parentName && childName ? `${parentName}/${childName}` : pageName;
5862
- const replaced = content.replace("@/ui/template/", `@/ui/${uiImportPath}/`).replaceAll('from "@/', `from "${aliasPrefix}/`).replace(/template/g, childName || pageName).replace(/Template/g, capitalize(childName || pageName));
5863
- await writeFile6(dst, replaced);
5864
- console.log(`\uD83D\uDCC4 File created: ${dst}`);
5865
- }
5866
- if (useI18n && messagesPath) {
5867
- const jsonTemplate = join6(templatePath, "page.json");
5868
- if (!existsSync7(jsonTemplate)) {
5869
- console.warn("\u26A0\uFE0F Missing template: page.json at path:", jsonTemplate);
5870
- } else {
5871
- const content = await readFile7(jsonTemplate, "utf-8");
5872
- const replaced = content.replace(/template/g, childName || pageName).replace(/Template/g, capitalize(childName || pageName));
5873
- for (const locale of locales) {
5874
- const localeDir = join6(messagesPath, locale);
5875
- if (!existsSync7(localeDir) || !statSync2(localeDir).isDirectory())
5876
- continue;
5877
- const jsonTarget = join6(messagesPath, locale, `${jsonFileName}.json`);
5878
- let current = {};
5879
- if (existsSync7(jsonTarget)) {
5880
- const jsonFile = await readFile7(jsonTarget, "utf-8");
5881
- try {
5882
- current = JSON.parse(jsonFile);
5883
- } catch {
5884
- current = {};
5885
- }
5886
- }
5887
- if (parentName && childName) {
5888
- current[childName] = JSON.parse(replaced);
5889
- } else {
5890
- current = JSON.parse(replaced);
5891
- }
5892
- await writeFile6(jsonTarget, `${JSON.stringify(current, null, 2)}
5893
- `);
5894
- console.log(`\uD83D\uDCC4 File created: ${jsonTarget}`);
5895
- const localeAggregator = join6(messagesPath, `${locale}.ts`);
5896
- if (!existsSync7(localeAggregator)) {
5897
- throw new Error(`Locale aggregator not found: messages/${locale}.ts`);
5898
- }
5899
- const aggregatorContent = await readFile7(localeAggregator, "utf-8");
5900
- const nextAggregatorContent = registerMessagesFile(aggregatorContent, locale, jsonFileName);
5901
- if (nextAggregatorContent !== aggregatorContent) {
5902
- await writeFile6(localeAggregator, nextAggregatorContent);
5903
- console.log(`\uD83D\uDCC4 File updated: ${localeAggregator}`);
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
+ });
5904
6415
  }
5905
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
+ });
5906
6477
  }
5907
6478
  } else {
5908
- console.log("\u2139\uFE0F Skipping translation templates; next-intl not enabled.");
6479
+ gateway.skipped(join6(context.cwd, "messages"), {
6480
+ role: "translation-messages",
6481
+ resource: "directory",
6482
+ detail: { reason: "Internationalization is disabled." }
6483
+ });
5909
6484
  }
5910
- console.log(`\u2705 Page "${pageName}" with templates added${useI18n ? " for each locale" : ""}.`);
5911
- }
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
+ };
5912
6518
 
5913
6519
  // src/lib/addText.ts
5914
6520
  import { join as join7 } from "path";
5915
- import { existsSync as existsSync8 } from "fs";
5916
- import { readFile as readFile8, writeFile as writeFile7, readdir as readdir5 } from "fs/promises";
5917
6521
  function defaultText(key) {
5918
- return key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
6522
+ return key.replace(/_/g, " ").replace(/\b\w/g, (character) => character.toUpperCase());
5919
6523
  }
5920
- async function addText(args, cwd = process.cwd()) {
6524
+ var addText = async (args, context) => {
5921
6525
  const pathArg = args[1];
5922
6526
  if (!pathArg) {
5923
- console.error("\u274C Dot path parameter is required.");
5924
- return;
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
+ });
5925
6537
  }
5926
6538
  const providedText = args.slice(2).join(" ");
5927
- parseLogicalName(pathArg, "translation path");
5928
- const config = await loadConfig(cwd);
6539
+ const finalKey = segments.at(-1);
6540
+ const text = providedText || defaultText(finalKey);
6541
+ const config = await loadConfig(context);
5929
6542
  if (!config?.useI18n) {
5930
- console.error("\u274C i18n is not enabled in this project.");
5931
- return;
6543
+ throw new CliError("Internationalization is not enabled in this project.", {
6544
+ code: "I18N_DISABLED"
6545
+ });
5932
6546
  }
5933
- const messagesPath = join7(cwd, "messages");
5934
- if (!existsSync8(messagesPath)) {
5935
- console.error("\u274C Messages directory missing. Ensure i18n was configured.");
5936
- return;
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
+ });
5937
6554
  }
5938
- const entries = await readdir5(messagesPath, { withFileTypes: true });
5939
- const locales = entries.filter((e) => e.isDirectory()).map((e) => e.name);
5940
- const [fileName, ...segments] = pathArg.split(".");
5941
- if (!fileName || segments.length === 0) {
5942
- console.error("\u274C Invalid dot path provided.");
5943
- return;
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
+ });
5944
6562
  }
5945
- const finalKey = segments[segments.length - 1];
5946
- const text = providedText || defaultText(finalKey);
6563
+ const [fileName, ...keySegments] = segments;
6564
+ const prepared = [];
5947
6565
  for (const locale of locales) {
5948
- const filePath = join7(messagesPath, locale, `${fileName}.json`);
5949
- await assertSafeTarget(cwd, filePath);
6566
+ const file = join7(messagesRoot, locale, `${fileName}.json`);
6567
+ await assertSafeTarget(context.cwd, file, context.fs);
6568
+ const existed = context.fs.exists(file);
5950
6569
  let data = {};
5951
- if (existsSync8(filePath)) {
5952
- const raw = await readFile8(filePath, "utf-8");
6570
+ if (existed) {
5953
6571
  try {
5954
- data = JSON.parse(raw);
5955
- } catch {}
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
+ }
5956
6585
  }
5957
6586
  let cursor = data;
5958
- for (let i = 0;i < segments.length - 1; i++) {
5959
- const seg = segments[i];
5960
- if (!cursor[seg])
5961
- cursor[seg] = {};
5962
- cursor = cursor[seg];
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];
5963
6599
  }
6600
+ const previous = cursor[finalKey];
5964
6601
  cursor[finalKey] = text;
5965
- await writeFile7(filePath, JSON.stringify(data, null, 2));
5966
- console.log(`\uD83D\uDCC4 File updated: ${filePath}`);
6602
+ prepared.push({
6603
+ locale,
6604
+ file,
6605
+ data,
6606
+ replaced: previous !== undefined && previous !== text
6607
+ });
5967
6608
  }
5968
- console.log(`\u2705 Text added at path "${pathArg}" with value "${text}".`);
5969
- }
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
+ };
5970
6649
 
5971
6650
  // src/lib/rmPage.ts
5972
- import { existsSync as existsSync9 } from "fs";
5973
- import { readFile as readFile9, readdir as readdir6, rm, writeFile as writeFile8 } from "fs/promises";
5974
- import path6 from "path";
5975
- async function removeMessages(projectRoot, candidate, terminal) {
5976
- const messagesRoot = resolveInside(projectRoot, "messages");
5977
- let locales;
5978
- try {
5979
- locales = (await readdir6(messagesRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
5980
- } catch {
5981
- return;
5982
- }
5983
- for (const locale of locales) {
5984
- const target = resolveInside(projectRoot, "messages", locale, `${candidate.routeSegments[0]}.json`);
5985
- if (!existsSync9(target))
5986
- continue;
5987
- if (candidate.messageKey) {
5988
- const data = JSON.parse(await readFile9(target, "utf8"));
5989
- delete data[candidate.messageKey];
5990
- await writeFile8(target, `${JSON.stringify(data, null, 2)}
5991
- `);
5992
- } else {
5993
- await rm(target);
5994
- }
5995
- terminal.log(`\uD83D\uDDD1\uFE0F Deleted messages for ${candidate.logicalName}: ${target}`);
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
+ });
5996
6669
  }
6670
+ if (!hasImport)
6671
+ return { content, changed: false };
6672
+ return {
6673
+ content: content.replace(importPattern, "").replace(propertyPattern, ""),
6674
+ changed: true
6675
+ };
5997
6676
  }
5998
- async function rmPage(args, cwd = process.cwd(), prompt, terminal = console) {
5999
- const candidates = await discoverPages(cwd);
6677
+ var rmPage = async (args, context) => {
6678
+ const candidates = await discoverPages(context.cwd, context.fs);
6000
6679
  let logicalName = args[1];
6001
6680
  if (!logicalName || logicalName.startsWith("-")) {
6002
- if (!prompt)
6003
- throw new CliError("A page name is required in non-interactive mode.");
6004
- const selected = await prompt([
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([
6005
6688
  {
6006
6689
  type: "autocomplete",
6007
6690
  name: "page",
6008
- message: "\uD83D\uDDD1\uFE0F Page to remove:",
6691
+ message: "Page to remove:",
6009
6692
  choices: candidates.map((candidate2) => ({
6010
- title: candidate2.logicalName.replaceAll(".", " \u203A "),
6693
+ title: candidate2.logicalName.replaceAll(".", " > "),
6011
6694
  value: candidate2.logicalName
6012
6695
  }))
6013
6696
  },
@@ -6019,85 +6702,154 @@ async function rmPage(args, cwd = process.cwd(), prompt, terminal = console) {
6019
6702
  }
6020
6703
  ]);
6021
6704
  if (!selected.confirm) {
6022
- terminal.log("Page deletion cancelled.");
6023
- return;
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
+ });
6024
6718
  }
6025
6719
  logicalName = String(selected.page ?? "");
6026
6720
  }
6027
6721
  parseLogicalName(logicalName, "page name");
6028
6722
  const candidate = candidates.find((entry) => entry.logicalName === logicalName);
6029
- if (!candidate)
6030
- throw new CliError(`Page not found: ${logicalName}`);
6031
- await removeMessages(cwd, candidate, terminal);
6032
- for (const target of [candidate.uiDirectory, candidate.routeDirectory]) {
6033
- const safeTarget = resolveInside(cwd, path6.relative(cwd, target));
6034
- if (existsSync9(safeTarget)) {
6035
- await rm(safeTarget, { recursive: true, force: false });
6036
- terminal.log(`\uD83D\uDDD1\uFE0F Deleted: ${safeTarget}`);
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
+ });
6037
6778
  }
6038
6779
  }
6039
- terminal.log(`\u2705 Page "${logicalName}" deleted.`);
6040
- }
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
+ };
6041
6829
 
6042
6830
  // src/cli/registry.ts
6043
- function legacyHandler(command) {
6044
- return async (args, context) => {
6045
- await command(args, context.cwd);
6046
- return success();
6047
- };
6048
- }
6049
6831
  function createCommandRegistry() {
6050
6832
  return new Map([
6051
- ["addcomponent", legacyHandler(addComponent)],
6052
- ["addpage", legacyHandler(addPage)],
6053
- ["addlib", legacyHandler(addLib)],
6054
- ["addapi", legacyHandler(addApi)],
6055
- ["addlanguage", legacyHandler(addLanguage)],
6056
- ["addtext", legacyHandler(addText)],
6057
- [
6058
- "rmpage",
6059
- async (args, context) => {
6060
- await rmPage(args, context.cwd, context.prompt, context.terminal);
6061
- return success();
6062
- }
6063
- ]
6833
+ ["addcomponent", addComponent],
6834
+ ["addpage", addPage],
6835
+ ["addlib", addLib],
6836
+ ["addapi", addApi],
6837
+ ["addlanguage", addLanguage],
6838
+ ["addtext", addText],
6839
+ ["rmpage", rmPage]
6064
6840
  ]);
6065
6841
  }
6066
6842
 
6067
6843
  // src/scaffold.ts
6068
- import { mkdir as mkdir7, rm as rm2, writeFile as writeFile10 } from "fs/promises";
6069
6844
  import { join as join8, resolve } from "path";
6070
- import { existsSync as existsSync10 } from "fs";
6071
- import { fileURLToPath as fileURLToPath2 } from "url";
6072
-
6073
- // src/lib/helper/consoleColor.ts
6074
- var RED = "\x1B[31m";
6075
- var GREEN = "\x1B[32m";
6076
- var CYAN = "\x1B[36m";
6077
- var RESET = "\x1B[0m";
6078
- function red(text) {
6079
- return RED + text + RESET;
6080
- }
6081
- function green(text) {
6082
- return GREEN + text + RESET;
6083
- }
6084
- function cyan(text) {
6085
- return CYAN + text + RESET;
6086
- }
6845
+ import { fileURLToPath } from "url";
6087
6846
 
6088
6847
  // src/core/template-manifest.ts
6089
- import {
6090
- lstat as lstat2,
6091
- mkdir as mkdir6,
6092
- readdir as readdir7,
6093
- readFile as readFile10,
6094
- writeFile as writeFile9,
6095
- copyFile as copyFile2
6096
- } from "fs/promises";
6097
- import path7 from "path";
6848
+ import path8 from "path";
6098
6849
  var TEMPLATE_DENY_NAMES = new Set([
6099
6850
  ".env",
6100
6851
  ".git",
6852
+ ".gitignore",
6101
6853
  ".agent",
6102
6854
  ".cursor",
6103
6855
  ".next",
@@ -6108,74 +6860,121 @@ var TEMPLATE_DENY_NAMES = new Set([
6108
6860
  "test-results"
6109
6861
  ]);
6110
6862
  function isDistributableTemplatePath(relativePath) {
6111
- const segments = relativePath.split(path7.sep);
6112
- return !segments.some((segment) => TEMPLATE_DENY_NAMES.has(segment) || segment.endsWith(".tsbuildinfo") || segment === ".DS_Store");
6863
+ const segments = relativePath.split(path8.sep);
6864
+ return !segments.some((segment) => TEMPLATE_DENY_NAMES.has(segment) || segment.startsWith(".env") && segment !== ".env.example" || segment.endsWith(".tsbuildinfo") || segment === ".DS_Store");
6113
6865
  }
6114
- async function templateManifest(root) {
6866
+ async function templateManifest(root, fs) {
6115
6867
  const files = [];
6116
6868
  async function visit(directory, relativeDirectory = "") {
6117
- const entries = await readdir7(directory, { withFileTypes: true });
6118
- entries.sort((left, right) => left.name.localeCompare(right.name));
6869
+ const entries = await fs.list(directory);
6119
6870
  for (const entry of entries) {
6120
- const relative = path7.join(relativeDirectory, entry.name);
6871
+ const relative = path8.join(relativeDirectory, entry.name);
6121
6872
  if (!isDistributableTemplatePath(relative))
6122
6873
  continue;
6123
- const source = path7.join(directory, entry.name);
6124
- const stats = await lstat2(source);
6125
- if (stats.isSymbolicLink()) {
6126
- throw new CliError(`Symbolic links are forbidden in templates: ${relative}`);
6874
+ const source = path8.join(directory, entry.name);
6875
+ if (entry.isSymbolicLink) {
6876
+ throw new CliError(`Symbolic links are forbidden in templates: ${relative}`, { code: "UNSAFE_PATH", scope: "package", path: relative });
6127
6877
  }
6128
- if (stats.isDirectory())
6878
+ if (entry.isDirectory)
6129
6879
  await visit(source, relative);
6130
- else if (stats.isFile())
6880
+ else if (entry.isFile)
6131
6881
  files.push(relative);
6132
6882
  else
6133
- throw new CliError(`Unsupported template entry: ${relative}`);
6883
+ throw new CliError(`Unsupported template entry: ${relative}`, {
6884
+ code: "TEMPLATE_MISSING",
6885
+ scope: "package",
6886
+ path: relative
6887
+ });
6134
6888
  }
6135
6889
  }
6136
6890
  await visit(root);
6137
6891
  return files;
6138
6892
  }
6139
- async function copyTemplate(root, target) {
6140
- const manifest = await templateManifest(root);
6141
- await mkdir6(target, { recursive: true });
6142
- for (const relative of manifest) {
6143
- const destination = path7.join(target, relative === ".gitignore.template" ? ".gitignore" : relative);
6144
- await mkdir6(path7.dirname(destination), { recursive: true });
6145
- await copyFile2(path7.join(root, relative), destination);
6893
+ async function validateScaffoldTemplate(root, fs) {
6894
+ const manifest = await templateManifest(root, fs);
6895
+ for (const required of [
6896
+ "package.json",
6897
+ "tsconfig.json",
6898
+ ".env.example",
6899
+ ".gitignore.template",
6900
+ ".prettierignore",
6901
+ "bun.lock",
6902
+ "pnpm-workspace.yaml",
6903
+ "vitest.config.ts",
6904
+ path8.join("scripts", "audit.ts"),
6905
+ path8.join("scripts", "package-manager.ts"),
6906
+ path8.join("tests", "consumer", "validate-template.ts"),
6907
+ path8.join(".github", "workflows", "quality.yml")
6908
+ ]) {
6909
+ if (!manifest.includes(required)) {
6910
+ throw new CliError(`Required template file was not found: ${required}.`, {
6911
+ code: "TEMPLATE_MISSING",
6912
+ scope: "package",
6913
+ path: `templates/Projects/default/${required}`
6914
+ });
6915
+ }
6916
+ }
6917
+ try {
6918
+ JSON.parse(await fs.readText(path8.join(root, "package.json")));
6919
+ const tsconfig = JSON.parse(await fs.readText(path8.join(root, "tsconfig.json")));
6920
+ if (!tsconfig.compilerOptions?.paths?.["@/*"])
6921
+ throw new Error("alias");
6922
+ } catch {
6923
+ throw new CliError('The template manifests must be valid JSON and define the default "@/*" alias.', {
6924
+ code: "TEMPLATE_MISSING",
6925
+ scope: "package",
6926
+ path: "templates/Projects/default"
6927
+ });
6146
6928
  }
6147
6929
  return manifest;
6148
6930
  }
6149
- async function customizeGeneratedProject(target, projectName, importAlias) {
6150
- const packagePath = path7.join(target, "package.json");
6151
- const packageJson = JSON.parse(await readFile10(packagePath, "utf8"));
6931
+ async function copyTemplate(root, target, context, gateway, manifest) {
6932
+ const files = manifest ?? await templateManifest(root, context.fs);
6933
+ for (const relative of files) {
6934
+ const destination = path8.join(target, relative === ".gitignore.template" ? ".gitignore" : relative);
6935
+ const source = path8.join(root, relative);
6936
+ await gateway.copy(source, destination, {
6937
+ role: "template-file",
6938
+ source: { template: `Projects/default/${relative}` }
6939
+ });
6940
+ }
6941
+ return files;
6942
+ }
6943
+ async function customizeGeneratedProject(target, projectName, importAlias, context, gateway) {
6944
+ const packagePath = path8.join(target, "package.json");
6945
+ const packageJson = JSON.parse(await context.fs.readText(packagePath));
6152
6946
  packageJson.name = projectName.toLowerCase();
6153
6947
  delete packageJson.packageManager;
6154
- await writeFile9(packagePath, `${JSON.stringify(packageJson, null, 2)}
6155
- `);
6156
- const tsconfigPath = path7.join(target, "tsconfig.json");
6157
- const tsconfigContent = await readFile10(tsconfigPath, "utf8");
6948
+ await gateway.write(packagePath, `${JSON.stringify(packageJson, null, 2)}
6949
+ `, { role: "package-manifest" });
6950
+ const tsconfigPath = path8.join(target, "tsconfig.json");
6951
+ const tsconfigContent = await context.fs.readText(tsconfigPath);
6158
6952
  const tsconfig = JSON.parse(tsconfigContent);
6159
6953
  if (!tsconfig.compilerOptions?.paths?.["@/*"]) {
6160
- throw new CliError('The template tsconfig must define the default "@/*" alias.');
6954
+ throw new CliError('The template tsconfig must define the default "@/*" alias.', {
6955
+ code: "TEMPLATE_MISSING",
6956
+ scope: "package",
6957
+ path: "templates/Projects/default/tsconfig.json"
6958
+ });
6161
6959
  }
6162
- await writeFile9(tsconfigPath, tsconfigContent.replace('"@/*"', `"${importAlias}"`));
6960
+ await gateway.write(tsconfigPath, tsconfigContent.replace('"@/*"', `"${importAlias}"`), { role: "typescript-configuration" });
6163
6961
  if (importAlias !== "@/*") {
6164
6962
  const prefix = importAlias.slice(0, -2);
6165
- for (const relative of await templateManifest(target)) {
6963
+ for (const relative of await templateManifest(target, context.fs)) {
6166
6964
  if (!/\.[cm]?[jt]sx?$/.test(relative))
6167
6965
  continue;
6168
- const file = path7.join(target, relative);
6169
- const content = await readFile10(file, "utf8");
6966
+ const file = path8.join(target, relative);
6967
+ const content = await context.fs.readText(file);
6170
6968
  const next = content.replaceAll('from "@/', `from "${prefix}/`).replaceAll("from '@/", `from '${prefix}/`);
6171
- if (next !== content)
6172
- await writeFile9(file, next);
6969
+ if (next !== content) {
6970
+ await gateway.write(file, next, { role: "import-alias-reference" });
6971
+ }
6173
6972
  }
6174
6973
  }
6175
6974
  }
6176
6975
 
6177
6976
  // src/scaffold.ts
6178
- async function scaffoldProject(options, runtime = {}) {
6977
+ async function scaffoldProject(options, runtime) {
6179
6978
  const requiredFeatures = [
6180
6979
  "useTypescript",
6181
6980
  "useEslint",
@@ -6186,57 +6985,91 @@ async function scaffoldProject(options, runtime = {}) {
6186
6985
  ];
6187
6986
  const unsupported = requiredFeatures.filter((feature) => options[feature] !== true);
6188
6987
  if (unsupported.length > 0) {
6189
- throw new CliError(`The default Next.js 16 template requires: ${unsupported.join(", ")}.`);
6988
+ throw new CliError(`The default Next.js 16 template requires: ${unsupported.join(", ")}.`, { code: "INVALID_ARGUMENT" });
6190
6989
  }
6191
- const cwd = runtime.cwd ?? process.cwd();
6192
- const terminal = runtime.terminal ?? console;
6990
+ const { context } = runtime;
6991
+ const cwd = context.cwd;
6193
6992
  const projectName = validateProjectName(options.projectName);
6194
6993
  const importAlias = normalizeImportAlias(options.customAlias === false ? "@/*" : options.importAlias || "@/*");
6195
6994
  const targetPath = join8(cwd, projectName);
6196
6995
  const __dirname2 = new URL(".", import.meta.url);
6197
- const templatePath = runtime.templatePath ?? join8(fileURLToPath2(__dirname2), "..", "templates", "Projects", "default");
6996
+ const templatePath = runtime.templatePath ?? join8(fileURLToPath(__dirname2), "..", "templates", "Projects", "default");
6198
6997
  const resolvedCwd = resolve(cwd);
6199
6998
  const resolvedTarget = resolve(targetPath);
6200
6999
  if (!resolvedTarget.startsWith(`${resolvedCwd}/`)) {
6201
- throw new CliError("The project destination must be a child of the current directory.");
7000
+ throw new CliError("The project destination must be a child of the current directory.", { code: "UNSAFE_PATH" });
6202
7001
  }
6203
- if (existsSync10(targetPath)) {
7002
+ const gateway = new MutationGateway(context, targetPath);
7003
+ const manifest = await validateScaffoldTemplate(templatePath, context.fs);
7004
+ if (context.fs.exists(targetPath)) {
6204
7005
  if (options.force) {
6205
- terminal.warn("\u26A0\uFE0F Target directory already exists, removing...");
6206
- await rm2(targetPath, { recursive: true, force: true });
7006
+ if (context.outputMode === "human") {
7007
+ context.terminal.warn(`WARNING: --force will remove the existing project at ${targetPath}.`);
7008
+ }
7009
+ await gateway.remove(targetPath, {
7010
+ role: "existing-project",
7011
+ resource: "project"
7012
+ }, { recursive: true, force: true });
6207
7013
  } else {
6208
- terminal.error(red("[X] Target directory already exists. Use --force to overwrite."));
6209
- throw new CliError("[X] Target directory already exists. Use --force to overwrite.");
7014
+ throw new CliError("Target directory already exists. Use --force to overwrite it.", {
7015
+ code: "TARGET_EXISTS",
7016
+ scope: "project",
7017
+ path: "."
7018
+ });
6210
7019
  }
6211
7020
  }
6212
- try {
6213
- terminal.log("Creating project directory...");
6214
- await mkdir7(targetPath, { recursive: true });
6215
- terminal.log("Copying files from template...");
6216
- await copyTemplate(templatePath, targetPath);
6217
- await customizeGeneratedProject(targetPath, projectName, importAlias);
6218
- await writeFile10(join8(targetPath, "cnp.config.json"), `${JSON.stringify({ ...options, projectName, importAlias }, null, 2)}
6219
- `);
6220
- terminal.log("Project setup complete!");
6221
- terminal.log("");
6222
- terminal.log("To get started:");
6223
- terminal.log(" " + green(`cd ${options.projectName}`));
6224
- terminal.log("");
6225
- terminal.log("Then install dependencies and launch the dev server with your preferred tool:");
6226
- terminal.log(" " + green(`bun install && bun dev`));
6227
- terminal.log(" " + green(`npm install && npm run dev`));
6228
- terminal.log(" " + green(`pnpm install && pnpm run dev`));
6229
- terminal.log("");
6230
- terminal.log("Documentation and examples can be found at:");
6231
- terminal.log(" " + cyan("https://github.com/Rising-Corporation/create-next-pro-cli"));
6232
- terminal.log("_-`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`-_");
6233
- } catch (err) {
6234
- terminal.error(red("[X] Error during project creation:"), err);
6235
- throw new CliError("[X] Error during project creation:");
6236
- }
7021
+ await gateway.mkdir(targetPath, {
7022
+ resource: "project",
7023
+ role: "project-root"
7024
+ });
7025
+ await copyTemplate(templatePath, targetPath, context, gateway, manifest);
7026
+ await customizeGeneratedProject(targetPath, projectName, importAlias, context, gateway);
7027
+ await gateway.write(join8(targetPath, "cnp.config.json"), `${JSON.stringify({ ...options, projectName, importAlias }, null, 2)}
7028
+ `, {
7029
+ role: "project-configuration",
7030
+ resource: "configuration",
7031
+ detail: { importAlias, packageManager: "user-selected" }
7032
+ });
7033
+ return { projectRoot: targetPath, copiedFiles: manifest.length, importAlias };
6237
7034
  }
6238
7035
 
6239
7036
  // src/lib/createProject.ts
7037
+ async function createProjectFromOptions(options, context) {
7038
+ const { projectRoot, copiedFiles, importAlias } = await scaffoldProject(options, {
7039
+ context
7040
+ });
7041
+ return commandResult(context, {
7042
+ command: "create",
7043
+ summary: `Created project "${options.projectName}" with ${copiedFiles} template files and import alias ${importAlias}; no package manager was pinned.`,
7044
+ projectRoot,
7045
+ nextSteps: [
7046
+ {
7047
+ kind: "install",
7048
+ required: true,
7049
+ message: "Install dependencies with your preferred package manager.",
7050
+ paths: [],
7051
+ commands: [
7052
+ `cd ${options.projectName} && bun install`,
7053
+ `cd ${options.projectName} && npm install`,
7054
+ `cd ${options.projectName} && pnpm install`
7055
+ ]
7056
+ },
7057
+ {
7058
+ kind: "run-checks",
7059
+ required: true,
7060
+ message: "Run the generated project checks before development.",
7061
+ paths: [{ scope: "project", path: "package.json" }],
7062
+ commands: ["bun run check", "npm run check", "pnpm run check"]
7063
+ }
7064
+ ],
7065
+ data: {
7066
+ projectName: options.projectName,
7067
+ importAlias,
7068
+ packageManager: null,
7069
+ copiedFiles
7070
+ }
7071
+ });
7072
+ }
6240
7073
  async function createProject(nameArg, force, context) {
6241
7074
  const response = {
6242
7075
  projectName: nameArg,
@@ -6250,12 +7083,7 @@ async function createProject(nameArg, force, context) {
6250
7083
  importAlias: "@/*",
6251
7084
  force
6252
7085
  };
6253
- const terminal = context?.terminal ?? console;
6254
- terminal.log(`Creating project "${response.projectName}"...`);
6255
- await scaffoldProject(response, {
6256
- cwd: context?.cwd ?? process.cwd(),
6257
- terminal
6258
- });
7086
+ return createProjectFromOptions(response, context);
6259
7087
  }
6260
7088
 
6261
7089
  // src/lib/createProjectWithPrompt.ts
@@ -6330,32 +7158,135 @@ async function createProjectWithPrompt(context) {
6330
7158
  initial: "@core/*"
6331
7159
  }
6332
7160
  ]);
7161
+ if (!response.projectName) {
7162
+ context.operations.record({
7163
+ action: "cancelled",
7164
+ resource: "command",
7165
+ role: "project-creation",
7166
+ scope: "project",
7167
+ path: "."
7168
+ });
7169
+ return commandResult(context, {
7170
+ command: "create",
7171
+ summary: "Project creation was cancelled.",
7172
+ projectRoot: context.cwd,
7173
+ status: "cancelled"
7174
+ });
7175
+ }
6333
7176
  const options = response;
6334
- context.terminal.log(`
6335
- Your choices:`);
6336
- context.terminal.log(options);
6337
- await scaffoldProject(options, {
6338
- cwd: context.cwd,
6339
- terminal: context.terminal
6340
- });
7177
+ return createProjectFromOptions(options, context);
7178
+ }
7179
+
7180
+ // src/runtime/node-context.ts
7181
+ var import_prompts = __toESM(require_prompts3(), 1);
7182
+ import { existsSync } from "fs";
7183
+ import {
7184
+ appendFile,
7185
+ copyFile,
7186
+ lstat,
7187
+ mkdir,
7188
+ readdir,
7189
+ readFile,
7190
+ rm,
7191
+ writeFile
7192
+ } from "fs/promises";
7193
+ import os from "os";
7194
+ import path9 from "path";
7195
+ import { fileURLToPath as fileURLToPath2 } from "url";
7196
+ function findPackageRoot(start) {
7197
+ let current = start;
7198
+ while (true) {
7199
+ const packagePath = path9.join(current, "package.json");
7200
+ if (existsSync(packagePath))
7201
+ return current;
7202
+ const parent = path9.dirname(current);
7203
+ if (parent === current) {
7204
+ throw new Error(`Unable to locate package.json from ${start}`);
7205
+ }
7206
+ current = parent;
7207
+ }
7208
+ }
7209
+ function resolvePackageRoot(metaUrl = import.meta.url) {
7210
+ return findPackageRoot(path9.dirname(fileURLToPath2(metaUrl)));
7211
+ }
7212
+ function createNodeContext(overrides = {}) {
7213
+ return {
7214
+ argv: process.argv.slice(2),
7215
+ cwd: process.cwd(),
7216
+ env: process.env,
7217
+ homeDir: os.homedir(),
7218
+ packageRoot: resolvePackageRoot(),
7219
+ terminal: console,
7220
+ prompt: import_prompts.default,
7221
+ fs: {
7222
+ exists: existsSync,
7223
+ readText: (target) => readFile(target, "utf8"),
7224
+ writeText: async (target, content) => {
7225
+ await writeFile(target, content);
7226
+ },
7227
+ mkdir: async (target) => {
7228
+ await mkdir(target, { recursive: true });
7229
+ },
7230
+ copyFile: async (source, target) => {
7231
+ await copyFile(source, target);
7232
+ },
7233
+ appendText: async (target, content) => {
7234
+ await appendFile(target, content);
7235
+ },
7236
+ remove: async (target, options) => {
7237
+ await rm(target, options);
7238
+ },
7239
+ inspect: async (target) => {
7240
+ try {
7241
+ const stats = await lstat(target);
7242
+ return {
7243
+ name: target,
7244
+ isFile: stats.isFile(),
7245
+ isDirectory: stats.isDirectory(),
7246
+ isSymbolicLink: stats.isSymbolicLink()
7247
+ };
7248
+ } catch (error) {
7249
+ if (error.code === "ENOENT")
7250
+ return null;
7251
+ throw error;
7252
+ }
7253
+ },
7254
+ list: async (target) => {
7255
+ const entries = await readdir(target, { withFileTypes: true });
7256
+ return Promise.all(entries.sort((left, right) => left.name.localeCompare(right.name)).map(async (entry) => {
7257
+ const stats = await lstat(path9.join(target, entry.name));
7258
+ return {
7259
+ name: entry.name,
7260
+ isFile: stats.isFile(),
7261
+ isDirectory: stats.isDirectory(),
7262
+ isSymbolicLink: stats.isSymbolicLink()
7263
+ };
7264
+ }));
7265
+ }
7266
+ },
7267
+ operations: new OperationJournal,
7268
+ outputMode: "human",
7269
+ ...overrides
7270
+ };
6341
7271
  }
6342
7272
 
6343
7273
  // src/index.ts
6344
7274
  var HELP_TEXT = `create-next-pro
6345
7275
 
6346
7276
  Usage:
6347
- create-next-pro <project-name> [--force]
6348
- create-next-pro addpage [options]
6349
- create-next-pro addcomponent [options]
6350
- create-next-pro addlib [name]
6351
- create-next-pro addapi [name]
6352
- create-next-pro addlanguage [locale]
6353
- create-next-pro addtext <path> [text]
6354
- create-next-pro rmpage [options]
7277
+ create-next-pro <project-name> [--force] [--json]
7278
+ create-next-pro addpage [options] [--json]
7279
+ create-next-pro addcomponent [options] [--json]
7280
+ create-next-pro addlib [name] [--json]
7281
+ create-next-pro addapi [name] [--json]
7282
+ create-next-pro addlanguage [locale] [--json]
7283
+ create-next-pro addtext <path> [text] [--json]
7284
+ create-next-pro rmpage [options] [--json]
6355
7285
 
6356
7286
  Options:
6357
7287
  --help Show this help message
6358
7288
  --version Show the CLI version
7289
+ --json Emit one deterministic JSON document
6359
7290
  --reconfigure Run the configuration assistant again
6360
7291
  `;
6361
7292
  function parseOptions(args) {
@@ -6363,51 +7294,106 @@ function parseOptions(args) {
6363
7294
  force: args.includes("--force"),
6364
7295
  help: args.includes("--help"),
6365
7296
  version: args.includes("--version") || args.includes("-v"),
6366
- reconfigure: args.includes("--reconfigure")
7297
+ reconfigure: args.includes("--reconfigure"),
7298
+ json: args.includes("--json")
6367
7299
  };
6368
7300
  }
7301
+ function withoutGlobalOptions(args) {
7302
+ return args.filter((argument) => argument !== "--json");
7303
+ }
6369
7304
  async function packageVersion(context) {
6370
- const packageJson = JSON.parse(await context.fs.readText(path8.join(context.packageRoot, "package.json")));
7305
+ const packageJson = JSON.parse(await context.fs.readText(path10.join(context.packageRoot, "package.json")));
6371
7306
  return packageJson.version;
6372
7307
  }
6373
- async function main(context = createNodeContext()) {
7308
+ function interactiveInputError(message, hint) {
7309
+ throw new CliError(message, {
7310
+ code: "INTERACTIVE_INPUT_REQUIRED",
7311
+ hint
7312
+ });
7313
+ }
7314
+ async function executeCli(context) {
7315
+ const options = parseOptions(context.argv);
7316
+ context.outputMode = options.json ? "json" : "human";
7317
+ const args = withoutGlobalOptions(context.argv);
7318
+ const version = await packageVersion(context);
7319
+ if (options.help) {
7320
+ return commandResult(context, {
7321
+ command: "help",
7322
+ summary: "Displayed create-next-pro help.",
7323
+ projectRoot: context.cwd,
7324
+ status: "success",
7325
+ data: { help: HELP_TEXT }
7326
+ });
7327
+ }
7328
+ if (options.version) {
7329
+ return commandResult(context, {
7330
+ command: "version",
7331
+ summary: `create-next-pro version ${version}.`,
7332
+ projectRoot: context.cwd,
7333
+ status: "success",
7334
+ data: { version }
7335
+ });
7336
+ }
7337
+ const config = await readConfig(context);
7338
+ if (options.reconfigure) {
7339
+ if (options.json) {
7340
+ interactiveInputError("Reconfiguration is interactive and unavailable in JSON mode.", "Run create-next-pro --reconfigure without --json.");
7341
+ }
7342
+ return onboarding(context, version);
7343
+ }
7344
+ if (!config) {
7345
+ if (options.json) {
7346
+ throw new CliError("Initial configuration is required.", {
7347
+ code: "ONBOARDING_REQUIRED",
7348
+ scope: "config",
7349
+ path: "config.json",
7350
+ hint: "Run create-next-pro once without --json, then rerun the command."
7351
+ });
7352
+ }
7353
+ return onboarding(context, version);
7354
+ }
7355
+ const command = args[0];
7356
+ const handler = command ? createCommandRegistry().get(command) : undefined;
7357
+ if (handler)
7358
+ return handler(args, context);
7359
+ const nameArg = args.find((argument) => !argument.startsWith("--") && argument !== "-v");
7360
+ if (nameArg)
7361
+ return createProject(nameArg, options.force, context);
7362
+ if (options.json) {
7363
+ interactiveInputError("A project name is required in JSON mode.", "Pass the project name as a positional argument.");
7364
+ }
7365
+ return createProjectWithPrompt(context);
7366
+ }
7367
+ async function runCompletion(context, args) {
6374
7368
  try {
6375
- const args = [...context.argv];
6376
- const options = parseOptions(args);
6377
- const version = await packageVersion(context);
6378
- if (args[0] === "__complete") {
6379
- await printCompletions(args, context);
6380
- return 0;
6381
- }
6382
- if (options.help) {
6383
- context.terminal.log(HELP_TEXT);
6384
- return 0;
6385
- }
6386
- if (options.version) {
6387
- context.terminal.log(`v${version}`);
6388
- return 0;
6389
- }
6390
- if (options.reconfigure || !await readConfig(context)) {
6391
- await onboarding(context, version);
6392
- return 0;
6393
- }
6394
- if (args[0] === "addpage" && args.length === 1)
6395
- args.push("-LPl");
6396
- const handler = args[0] ? createCommandRegistry().get(args[0]) : undefined;
6397
- if (handler)
6398
- return (await handler(args, context)).exitCode;
6399
- const nameArg = args.find((arg) => !arg.startsWith("--"));
6400
- if (nameArg) {
6401
- await createProject(nameArg, options.force, context);
6402
- return 0;
6403
- }
6404
- await createProjectWithPrompt(context);
7369
+ for (const candidate of await completionCandidates(args[1], context)) {
7370
+ context.terminal.log(candidate);
7371
+ }
6405
7372
  return 0;
7373
+ } catch {
7374
+ return 1;
7375
+ }
7376
+ }
7377
+ async function main(context = createNodeContext()) {
7378
+ const completionArgs = withoutGlobalOptions(context.argv);
7379
+ if (completionArgs[0] === "__complete") {
7380
+ return runCompletion(context, completionArgs);
7381
+ }
7382
+ context.operations.reset();
7383
+ context.outputMode = parseOptions(context.argv).json ? "json" : "human";
7384
+ let result;
7385
+ try {
7386
+ result = await executeCli(context);
6406
7387
  } catch (error) {
6407
- const message = error instanceof Error ? error.message : String(error);
6408
- context.terminal.error(message);
6409
- return error instanceof CliError ? error.exitCode : 1;
7388
+ const command = completionArgs[0] ?? "create";
7389
+ result = failedResult(context, command, error, {
7390
+ projectRoot: context.cwd,
7391
+ configRoot: configDirectory(context),
7392
+ homeRoot: context.homeDir
7393
+ });
6410
7394
  }
7395
+ renderResult(result, context, context.outputMode);
7396
+ return result.exitCode;
6411
7397
  }
6412
7398
 
6413
7399
  // bin.bun.ts