create-next-pro-cli 0.1.25 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +205 -292
  2. package/create-next-pro-completion.sh +8 -38
  3. package/create-next-pro-completion.zsh +13 -0
  4. package/dist/bin.bun.js +1127 -628
  5. package/dist/bin.node.js +1170 -617
  6. package/dist/bin.node.js.map +1 -1
  7. package/dist/create-next-pro +35 -6
  8. package/package.json +49 -27
  9. package/templates/Projects/default/.env.example +17 -0
  10. package/templates/Projects/default/.github/workflows/quality.yml +24 -0
  11. package/templates/Projects/default/.gitignore.template +47 -0
  12. package/templates/Projects/default/.prettierignore +3 -0
  13. package/templates/Projects/default/README.md +66 -21
  14. package/templates/Projects/default/bun.lock +1152 -0
  15. package/templates/Projects/default/eslint.config.mjs +27 -11
  16. package/templates/Projects/default/messages/en/_global_ui.json +23 -10
  17. package/templates/Projects/default/messages/en.ts +17 -2
  18. package/templates/Projects/default/messages/fr/_global_ui.json +23 -10
  19. package/templates/Projects/default/messages/fr.ts +17 -2
  20. package/templates/Projects/default/next.config.ts +43 -3
  21. package/templates/Projects/default/package.json +42 -24
  22. package/templates/Projects/default/playwright.config.ts +26 -0
  23. package/templates/Projects/default/pnpm-workspace.yaml +5 -0
  24. package/templates/Projects/default/public/{cnp-logo.svg → logo.svg} +1 -1
  25. package/templates/Projects/default/src/app/[locale]/(public)/layout.tsx +8 -1
  26. package/templates/Projects/default/src/app/[locale]/(public)/login/page.tsx +8 -0
  27. package/templates/Projects/default/src/app/[locale]/(public)/register/page.tsx +8 -0
  28. package/templates/Projects/default/src/app/[locale]/(user)/dashboard/error.tsx +25 -0
  29. package/templates/Projects/default/src/app/[locale]/(user)/dashboard/loading.tsx +5 -0
  30. package/templates/Projects/default/src/app/[locale]/(user)/{Dashboard → dashboard}/page.tsx +1 -1
  31. package/templates/Projects/default/src/app/[locale]/(user)/layout.tsx +24 -2
  32. package/templates/Projects/default/src/app/[locale]/(user)/settings/loading.tsx +5 -0
  33. package/templates/Projects/default/src/app/[locale]/(user)/{Settings → settings}/page.tsx +1 -2
  34. package/templates/Projects/default/src/app/[locale]/(user)/userInfo/loading.tsx +5 -0
  35. package/templates/Projects/default/src/app/[locale]/(user)/{UserInfo → userInfo}/page.tsx +1 -2
  36. package/templates/Projects/default/src/app/[locale]/layout.tsx +34 -10
  37. package/templates/Projects/default/src/app/[locale]/loading.tsx +0 -9
  38. package/templates/Projects/default/src/app/[locale]/not-found.tsx +6 -15
  39. package/templates/Projects/default/src/app/[locale]/page.tsx +10 -1
  40. package/templates/Projects/default/src/app/api/auth/[...nextauth]/route.ts +8 -60
  41. package/templates/Projects/default/src/app/not-found.tsx +1 -1
  42. package/templates/Projects/default/src/app/sitemap.ts +2 -2
  43. package/templates/Projects/default/src/app/styles/globals.css +166 -113
  44. package/templates/Projects/default/src/auth.ts +20 -0
  45. package/templates/Projects/default/src/config.ts +3 -3
  46. package/templates/Projects/default/src/env.ts +65 -0
  47. package/templates/Projects/default/src/lib/i18n/messages.ts +8 -0
  48. package/templates/Projects/default/src/lib/i18n/request.ts +2 -16
  49. package/templates/Projects/default/src/lib/security/csp.ts +16 -0
  50. package/templates/Projects/default/src/lib/utils.ts +2 -1
  51. package/templates/Projects/default/src/proxy.ts +13 -0
  52. package/templates/Projects/default/src/ui/_global/BackButton.tsx +4 -2
  53. package/templates/Projects/default/src/ui/_global/Button.tsx +3 -8
  54. package/templates/Projects/default/src/ui/_global/GlobalHeader.tsx +10 -28
  55. package/templates/Projects/default/src/ui/_global/GlobalMain.tsx +1 -1
  56. package/templates/Projects/default/src/ui/_global/LocaleSwitcher.tsx +9 -10
  57. package/templates/Projects/default/src/ui/_global/PublicNav.tsx +51 -17
  58. package/templates/Projects/default/src/ui/_global/ThemeToggle.tsx +45 -39
  59. package/templates/Projects/default/src/ui/_global/UserNav.tsx +6 -6
  60. package/templates/Projects/default/src/ui/_home/page-ui.tsx +5 -7
  61. package/templates/Projects/default/src/ui/{Dashboard → dashboard}/LogoutButton.tsx +9 -7
  62. package/templates/Projects/default/src/ui/{Dashboard → dashboard}/StatsCard.tsx +4 -4
  63. package/templates/Projects/default/src/ui/{Dashboard → dashboard}/page-ui.tsx +7 -8
  64. package/templates/Projects/default/src/ui/login/page-ui.tsx +36 -0
  65. package/templates/Projects/default/src/ui/register/page-ui.tsx +38 -0
  66. package/templates/Projects/default/src/ui/settings/page-ui.tsx +15 -0
  67. package/templates/Projects/default/src/ui/userInfo/page-ui.tsx +15 -0
  68. package/templates/Projects/default/tailwind.config.ts +81 -1
  69. package/templates/Projects/default/tests/consumer/validate-template.ts +66 -0
  70. package/templates/Projects/default/tests/e2e/template-remediation.playwright.ts +106 -0
  71. package/templates/Projects/default/tests/rendering/verify-rendering.ts +56 -0
  72. package/templates/Projects/default/tests/unit/csp.test.ts +19 -0
  73. package/templates/Projects/default/tests/unit/env.test.ts +76 -0
  74. package/templates/Projects/default/tsconfig.json +6 -6
  75. package/templates/Projects/default/messages/getMergedMessages.ts +0 -31
  76. package/templates/Projects/default/middleware.ts +0 -11
  77. package/templates/Projects/default/src/app/[locale]/(public)/Login/page.tsx +0 -6
  78. package/templates/Projects/default/src/app/[locale]/(public)/Register/page.tsx +0 -6
  79. package/templates/Projects/default/src/app/[locale]/(user)/Dashboard/error.tsx +0 -38
  80. package/templates/Projects/default/src/app/[locale]/(user)/Dashboard/loading.tsx +0 -10
  81. package/templates/Projects/default/src/app/[locale]/(user)/Settings/loading.tsx +0 -17
  82. package/templates/Projects/default/src/app/[locale]/(user)/UserInfo/loading.tsx +0 -17
  83. package/templates/Projects/default/src/app/api/auth/post-login/route.ts +0 -26
  84. package/templates/Projects/default/src/app/api/hello/route.ts +0 -5
  85. package/templates/Projects/default/src/app/layout.tsx +0 -11
  86. package/templates/Projects/default/src/app/page.tsx +0 -6
  87. package/templates/Projects/default/src/auth.config.ts +0 -0
  88. package/templates/Projects/default/src/lib/auth/disconnect.ts +0 -11
  89. package/templates/Projects/default/src/lib/auth/isConnected.ts +0 -18
  90. package/templates/Projects/default/src/lib/sample/example.ts +0 -3
  91. package/templates/Projects/default/src/lib/sample/index.ts +0 -3
  92. package/templates/Projects/default/src/ui/Login/page-ui.tsx +0 -22
  93. package/templates/Projects/default/src/ui/Register/page-ui.tsx +0 -26
  94. package/templates/Projects/default/src/ui/Settings/page-ui.tsx +0 -17
  95. package/templates/Projects/default/src/ui/UserInfo/page-ui.tsx +0 -17
  96. /package/templates/Projects/default/messages/en/{Dashboard.json → dashboard.json} +0 -0
  97. /package/templates/Projects/default/messages/en/{Login.json → login.json} +0 -0
  98. /package/templates/Projects/default/messages/en/{Register.json → register.json} +0 -0
  99. /package/templates/Projects/default/messages/en/{Settings.json → settings.json} +0 -0
  100. /package/templates/Projects/default/messages/en/{UserInfo.json → userInfo.json} +0 -0
  101. /package/templates/Projects/default/messages/fr/{Dashboard.json → dashboard.json} +0 -0
  102. /package/templates/Projects/default/messages/fr/{Login.json → login.json} +0 -0
  103. /package/templates/Projects/default/messages/fr/{Register.json → register.json} +0 -0
  104. /package/templates/Projects/default/messages/fr/{Settings.json → settings.json} +0 -0
  105. /package/templates/Projects/default/messages/fr/{UserInfo.json → userInfo.json} +0 -0
  106. /package/templates/Projects/default/public/{cnp-logo.png → logo.png} +0 -0
  107. /package/templates/Projects/default/src/ui/{Dashboard → dashboard}/WelcomeCard.tsx +0 -0
package/dist/bin.node.js CHANGED
@@ -1,17 +1,241 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import fs from "fs";
5
- import os from "os";
4
+ import path8 from "path";
5
+
6
+ // src/cli/onboarding.ts
6
7
  import path from "path";
7
- import prompts8 from "prompts";
8
- import { fileURLToPath as fileURLToPath2 } from "url";
9
- import { dirname, resolve } from "path";
8
+ function configDirectory(context) {
9
+ return context.env.XDG_CONFIG_HOME ? path.join(context.env.XDG_CONFIG_HOME, "create-next-pro") : path.join(context.homeDir, ".config", "create-next-pro");
10
+ }
11
+ function configFile(context) {
12
+ return path.join(configDirectory(context), "config.json");
13
+ }
14
+ async function readConfig(context) {
15
+ try {
16
+ return JSON.parse(
17
+ await context.fs.readText(configFile(context))
18
+ );
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+ async function ensureLineInRc(context, target, line) {
24
+ try {
25
+ const current = context.fs.exists(target) ? await context.fs.readText(target) : "";
26
+ if (!current.includes(line))
27
+ await context.fs.appendText(target, `
28
+ ${line}
29
+ `);
30
+ } catch {
31
+ }
32
+ }
33
+ async function installCompletion(context, shell) {
34
+ const directory = configDirectory(context);
35
+ const source = path.join(
36
+ context.packageRoot,
37
+ shell === "zsh" ? "create-next-pro-completion.zsh" : "create-next-pro-completion.sh"
38
+ );
39
+ const target = path.join(
40
+ directory,
41
+ `completion.${shell === "zsh" ? "zsh" : "sh"}`
42
+ );
43
+ await context.fs.mkdir(directory);
44
+ await context.fs.copyFile(source, target);
45
+ const rcFile = path.join(
46
+ context.homeDir,
47
+ shell === "zsh" ? ".zshrc" : ".bashrc"
48
+ );
49
+ await ensureLineInRc(context, rcFile, `source "${target}"`);
50
+ }
51
+ async function onboarding(context, version) {
52
+ context.terminal.log(`\u{1F680} Welcome to create-next-pro v${version}
53
+ `);
54
+ const response = await context.prompt(
55
+ [
56
+ {
57
+ type: "select",
58
+ name: "shell",
59
+ message: "Which shell do you use?",
60
+ choices: [
61
+ { title: "zsh", value: "zsh" },
62
+ { title: "bash", value: "bash" }
63
+ ],
64
+ initial: context.env.SHELL?.includes("zsh") ? 0 : 1
65
+ },
66
+ {
67
+ type: "toggle",
68
+ name: "completion",
69
+ message: "Install autocompletion?",
70
+ initial: true,
71
+ active: "Yes",
72
+ inactive: "No"
73
+ }
74
+ ],
75
+ { onCancel: () => false }
76
+ );
77
+ if (response.shell !== "bash" && response.shell !== "zsh") {
78
+ throw new Error("Configuration cancelled.");
79
+ }
80
+ const now = (/* @__PURE__ */ new Date()).toISOString();
81
+ const config = {
82
+ version: 1,
83
+ shell: response.shell,
84
+ completionInstalled: Boolean(response.completion),
85
+ createdAt: now,
86
+ updatedAt: now
87
+ };
88
+ if (config.completionInstalled)
89
+ await installCompletion(context, config.shell);
90
+ await context.fs.mkdir(configDirectory(context));
91
+ await context.fs.writeText(
92
+ configFile(context),
93
+ JSON.stringify(config, null, 2)
94
+ );
95
+ context.terminal.log("\n\u2705 Configuration saved.");
96
+ context.terminal.log("you can now use the CLI ! ex : ");
97
+ context.terminal.log(" Without prompt (will change in future) :");
98
+ context.terminal.log(" create-next-pro my-next-project");
99
+ context.terminal.log(" With prompt :");
100
+ context.terminal.log(" create-next-pro");
101
+ context.terminal.log(
102
+ "For more information, visit: https://github.com/Rising-Corporation/create-next-pro-cli"
103
+ );
104
+ context.terminal.log("Happy coding! \u{1F389}");
105
+ return config;
106
+ }
10
107
 
11
- // src/lib/addComponent.ts
108
+ // src/cli/completion.ts
109
+ import { readdir as readdir2 } from "fs/promises";
110
+ import path3 from "path";
111
+
112
+ // src/core/page-catalog.ts
113
+ import { readdir } from "fs/promises";
114
+ import path2 from "path";
115
+ function isRouteGroup(segment) {
116
+ return segment.startsWith("(") && segment.endsWith(")");
117
+ }
118
+ async function discoverPages(projectRoot) {
119
+ const appRoot = path2.join(projectRoot, "src", "app", "[locale]");
120
+ const candidates = [];
121
+ async function visit(directory, relative = []) {
122
+ let entries;
123
+ try {
124
+ entries = await readdir(directory, { withFileTypes: true });
125
+ } catch {
126
+ return;
127
+ }
128
+ if (entries.some((entry) => entry.isFile() && entry.name === "page.tsx")) {
129
+ const routeSegments = relative.filter(
130
+ (segment) => !isRouteGroup(segment)
131
+ );
132
+ if (routeSegments.length > 0 && !routeSegments.some(
133
+ (segment) => segment.startsWith("_") || segment.startsWith("[")
134
+ )) {
135
+ const logicalName = routeSegments.join(".");
136
+ candidates.push({
137
+ logicalName,
138
+ routeSegments,
139
+ routeDirectory: directory,
140
+ uiDirectory: path2.join(projectRoot, "src", "ui", ...routeSegments),
141
+ messageFile: path2.join(
142
+ projectRoot,
143
+ "messages",
144
+ "{locale}",
145
+ `${routeSegments[0]}.json`
146
+ ),
147
+ messageKey: routeSegments.length > 1 ? routeSegments.at(-1) : void 0
148
+ });
149
+ }
150
+ }
151
+ for (const entry of entries) {
152
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
153
+ await visit(path2.join(directory, entry.name), [
154
+ ...relative,
155
+ entry.name
156
+ ]);
157
+ }
158
+ }
159
+ }
160
+ await visit(appRoot);
161
+ return candidates.sort(
162
+ (left, right) => left.logicalName.localeCompare(right.logicalName)
163
+ );
164
+ }
165
+
166
+ // src/cli/completion.ts
167
+ var PUBLIC_COMMANDS = [
168
+ "addpage",
169
+ "addcomponent",
170
+ "addlib",
171
+ "addapi",
172
+ "addlanguage",
173
+ "addtext",
174
+ "rmpage",
175
+ "--help",
176
+ "--version",
177
+ "--reconfigure"
178
+ ];
179
+ var OPTIONS = {
180
+ addpage: [
181
+ "--layout",
182
+ "--page",
183
+ "--loading",
184
+ "--not-found",
185
+ "--error",
186
+ "--global-error",
187
+ "--route",
188
+ "--template",
189
+ "--default"
190
+ ],
191
+ addcomponent: ["--page", "-P"]
192
+ };
193
+ async function directories(root) {
194
+ try {
195
+ return (await readdir2(root, { withFileTypes: true })).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_")).map((entry) => entry.name).sort();
196
+ } catch {
197
+ return [];
198
+ }
199
+ }
200
+ async function completionCandidates(command, context) {
201
+ if (!command) return [...PUBLIC_COMMANDS];
202
+ if (command === "rmpage") {
203
+ return (await discoverPages(context.cwd)).map(
204
+ (candidate) => candidate.logicalName
205
+ );
206
+ }
207
+ if (command === "addcomponent" || command === "addpage") {
208
+ return [
209
+ ...OPTIONS[command] ?? [],
210
+ ...await directories(path3.join(context.cwd, "src", "ui"))
211
+ ];
212
+ }
213
+ if (command === "addlanguage")
214
+ return ["de", "en", "es", "fr", "it", "ja", "pt"];
215
+ return OPTIONS[command] ?? [];
216
+ }
217
+ async function printCompletions(args, context) {
218
+ for (const candidate of await completionCandidates(args[1], context)) {
219
+ context.terminal.log(candidate);
220
+ }
221
+ }
222
+
223
+ // src/core/contracts.ts
224
+ var success = () => ({ exitCode: 0 });
225
+ var CliError = class extends Error {
226
+ constructor(message, exitCode = 1) {
227
+ super(message);
228
+ this.exitCode = exitCode;
229
+ this.name = "CliError";
230
+ }
231
+ exitCode;
232
+ };
233
+
234
+ // src/lib/addApi.ts
12
235
  import { join as join2 } from "path";
13
- import { mkdir, readFile as readFile2, writeFile, readdir } from "fs/promises";
14
- import prompts from "prompts";
236
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
237
+ import prompts2 from "prompts";
238
+ import { existsSync as existsSync3 } from "fs";
15
239
 
16
240
  // src/lib/utils.ts
17
241
  import { readFile } from "fs/promises";
@@ -20,8 +244,12 @@ import { join } from "path";
20
244
  function capitalize(str) {
21
245
  return str.charAt(0).toUpperCase() + str.slice(1);
22
246
  }
23
- async function loadConfig() {
24
- const configPath = join(process.cwd(), "cnp.config.json");
247
+ function configuredAliasPrefix(config) {
248
+ const alias = config.importAlias ?? "@/*";
249
+ return alias.endsWith("/*") ? alias.slice(0, -2) : "@";
250
+ }
251
+ async function loadConfig(cwd = process.cwd()) {
252
+ const configPath = join(cwd, "cnp.config.json");
25
253
  if (!existsSync(configPath)) return null;
26
254
  try {
27
255
  const raw = await readFile(configPath, "utf-8");
@@ -55,21 +283,212 @@ function toFileName(key) {
55
283
  }
56
284
  }
57
285
 
286
+ // src/runtime/node-context.ts
287
+ import { existsSync as existsSync2 } from "fs";
288
+ import {
289
+ appendFile,
290
+ copyFile,
291
+ mkdir,
292
+ readFile as readFile2,
293
+ writeFile
294
+ } from "fs/promises";
295
+ import os from "os";
296
+ import path4 from "path";
297
+ import { fileURLToPath } from "url";
298
+ import prompts from "prompts";
299
+ function findPackageRoot(start) {
300
+ let current = start;
301
+ while (true) {
302
+ const packagePath = path4.join(current, "package.json");
303
+ if (existsSync2(packagePath)) return current;
304
+ const parent = path4.dirname(current);
305
+ if (parent === current) {
306
+ throw new Error(`Unable to locate package.json from ${start}`);
307
+ }
308
+ current = parent;
309
+ }
310
+ }
311
+ function resolvePackageRoot(metaUrl = import.meta.url) {
312
+ return findPackageRoot(path4.dirname(fileURLToPath(metaUrl)));
313
+ }
314
+ function createNodeContext(overrides = {}) {
315
+ return {
316
+ argv: process.argv.slice(2),
317
+ cwd: process.cwd(),
318
+ env: process.env,
319
+ homeDir: os.homedir(),
320
+ packageRoot: resolvePackageRoot(),
321
+ terminal: console,
322
+ prompt: prompts,
323
+ fs: {
324
+ exists: existsSync2,
325
+ readText: (target) => readFile2(target, "utf8"),
326
+ writeText: async (target, content) => {
327
+ await writeFile(target, content);
328
+ },
329
+ mkdir: async (target) => {
330
+ await mkdir(target, { recursive: true });
331
+ },
332
+ copyFile: async (source, target) => {
333
+ await copyFile(source, target);
334
+ },
335
+ appendText: async (target, content) => {
336
+ await appendFile(target, content);
337
+ }
338
+ },
339
+ ...overrides
340
+ };
341
+ }
342
+
343
+ // src/core/project-paths.ts
344
+ import path5 from "path";
345
+ import { lstat } from "fs/promises";
346
+ var SAFE_SEGMENT = /^[A-Za-z][A-Za-z0-9_-]*$/;
347
+ function hasControlCharacters(value) {
348
+ return [...value].some((character) => {
349
+ const code = character.charCodeAt(0);
350
+ return code <= 31 || code === 127;
351
+ });
352
+ }
353
+ function parseLogicalName(value, label = "name") {
354
+ if (!value || hasControlCharacters(value)) {
355
+ throw new CliError(
356
+ `Invalid ${label}: a non-empty printable value is required.`
357
+ );
358
+ }
359
+ if (path5.isAbsolute(value) || value.includes("/") || value.includes("\\")) {
360
+ throw new CliError(
361
+ `Invalid ${label}: paths and separators are not allowed.`
362
+ );
363
+ }
364
+ const segments = value.split(".");
365
+ if (segments.some((segment) => !SAFE_SEGMENT.test(segment))) {
366
+ throw new CliError(
367
+ `Invalid ${label}: use dot-separated alphanumeric segments beginning with a letter.`
368
+ );
369
+ }
370
+ return segments;
371
+ }
372
+ function validateProjectName(value) {
373
+ if (!value || hasControlCharacters(value) || path5.isAbsolute(value) || value === "." || value === ".." || value.includes("/") || value.includes("\\") || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value)) {
374
+ throw new CliError(
375
+ "Invalid project name: use letters, numbers, dots, dashes or underscores without path separators."
376
+ );
377
+ }
378
+ return value;
379
+ }
380
+ function resolveInside(root, ...segments) {
381
+ const absoluteRoot = path5.resolve(root);
382
+ const target = path5.resolve(absoluteRoot, ...segments);
383
+ if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${path5.sep}`)) {
384
+ throw new CliError(
385
+ `Refusing to access a path outside the project: ${target}`
386
+ );
387
+ }
388
+ return target;
389
+ }
390
+ async function assertSafeTarget(root, target) {
391
+ const safeTarget = resolveInside(root, path5.relative(root, target));
392
+ const relativeSegments = path5.relative(path5.resolve(root), safeTarget).split(path5.sep);
393
+ let current = path5.resolve(root);
394
+ for (const segment of relativeSegments) {
395
+ if (!segment) continue;
396
+ current = path5.join(current, segment);
397
+ try {
398
+ if ((await lstat(current)).isSymbolicLink()) {
399
+ throw new CliError(
400
+ `Symbolic links are forbidden in project paths: ${current}`
401
+ );
402
+ }
403
+ } catch (error) {
404
+ if (error instanceof CliError) throw error;
405
+ if (error.code !== "ENOENT") throw error;
406
+ }
407
+ }
408
+ return safeTarget;
409
+ }
410
+ function normalizeImportAlias(value) {
411
+ if (!/^[A-Za-z@~][A-Za-z0-9@~_-]*\/\*$/.test(value)) {
412
+ throw new CliError(
413
+ 'Invalid import alias: expected a prefix followed by "/*" (for example "@/*" or "@core/*").'
414
+ );
415
+ }
416
+ return value;
417
+ }
418
+
419
+ // src/lib/addApi.ts
420
+ async function addApi(args, cwd = process.cwd()) {
421
+ let apiName = args[1];
422
+ if (!apiName || apiName.startsWith("-")) {
423
+ const response = await prompts2.prompt({
424
+ type: "text",
425
+ name: "apiName",
426
+ message: "\u{1F50C} API route name to add:",
427
+ validate: (name) => name ? true : "API route name is required"
428
+ });
429
+ apiName = response.apiName;
430
+ }
431
+ const apiSegments = parseLogicalName(apiName, "API route name");
432
+ const config = await loadConfig(cwd);
433
+ if (!config) {
434
+ console.error(
435
+ "\u274C Configuration file cnp.config.json not found. Run this command from the project root."
436
+ );
437
+ return;
438
+ }
439
+ const apiDir = join2(cwd, "src", "app", "api", ...apiSegments);
440
+ await assertSafeTarget(cwd, apiDir);
441
+ if (!existsSync3(apiDir)) {
442
+ await mkdir2(apiDir, { recursive: true });
443
+ }
444
+ const templateDir = join2(
445
+ resolvePackageRoot(import.meta.url),
446
+ "templates",
447
+ "Api"
448
+ );
449
+ const routeTemplate = join2(templateDir, "route.ts");
450
+ const routePath = join2(apiDir, "route.ts");
451
+ if (!existsSync3(routePath)) {
452
+ if (existsSync3(routeTemplate)) {
453
+ let content = await readFile3(routeTemplate, "utf-8");
454
+ content = content.replace(/template/g, apiName);
455
+ await writeFile2(routePath, content);
456
+ } else {
457
+ await writeFile2(
458
+ routePath,
459
+ `import { NextResponse } from "next/server";
460
+
461
+ export async function GET() {
462
+ return NextResponse.json({ message: "Hello from ${apiName}" });
463
+ }
464
+ `
465
+ );
466
+ }
467
+ console.log(`\u{1F4C4} File created: ${routePath}`);
468
+ } else {
469
+ console.log(`\u2139\uFE0F File already exists: ${routePath}`);
470
+ }
471
+ console.log(`\u2705 API route "${apiName}" added.`);
472
+ }
473
+
58
474
  // src/lib/addComponent.ts
59
- import { existsSync as existsSync2, statSync } from "fs";
60
- async function addComponent(args) {
475
+ import { join as join3 } from "path";
476
+ import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3, readdir as readdir3 } from "fs/promises";
477
+ import prompts3 from "prompts";
478
+ import { existsSync as existsSync4, statSync } from "fs";
479
+ async function addComponent(args, cwd = process.cwd()) {
61
480
  let componentName = args[1];
62
481
  let pageScope = null;
63
- let pageIndex = args.findIndex((arg) => arg === "-P" || arg === "--page");
482
+ const pageIndex = args.findIndex((arg) => arg === "-P" || arg === "--page");
64
483
  if (pageIndex !== -1 && args[pageIndex + 1]) {
65
484
  pageScope = args[pageIndex + 1];
66
485
  }
67
486
  let nestedPath = null;
68
487
  if (pageScope && pageScope.includes(".")) {
69
- nestedPath = join2(...pageScope.split("."));
488
+ nestedPath = join3(...pageScope.split("."));
70
489
  }
71
490
  if (!componentName || componentName.startsWith("-")) {
72
- const response = await prompts.prompt({
491
+ const response = await prompts3.prompt({
73
492
  type: "text",
74
493
  name: "componentName",
75
494
  message: "\u{1F9E9} Component name to add:",
@@ -77,7 +496,9 @@ async function addComponent(args) {
77
496
  });
78
497
  componentName = response.componentName;
79
498
  }
80
- const config = await loadConfig();
499
+ parseLogicalName(componentName, "component name");
500
+ if (pageScope) parseLogicalName(pageScope, "page name");
501
+ const config = await loadConfig(cwd);
81
502
  if (!config) {
82
503
  console.error(
83
504
  "\u274C Configuration file cnp.config.json not found. Run this command from the project root."
@@ -86,15 +507,15 @@ async function addComponent(args) {
86
507
  }
87
508
  const useI18n = !!config.useI18n;
88
509
  const componentNameUpper = capitalize(componentName);
89
- const templatePath = join2(
90
- new URL("..", import.meta.url).pathname,
510
+ const templatePath = join3(
511
+ resolvePackageRoot(import.meta.url),
91
512
  "templates",
92
513
  "Component"
93
514
  );
94
515
  let messagesPath = null;
95
516
  if (useI18n) {
96
- messagesPath = join2(process.cwd(), "messages");
97
- if (!existsSync2(messagesPath)) {
517
+ messagesPath = join3(cwd, "messages");
518
+ if (!existsSync4(messagesPath)) {
98
519
  console.error(
99
520
  "\u274C Messages directory missing. Ensure i18n was configured."
100
521
  );
@@ -105,25 +526,26 @@ async function addComponent(args) {
105
526
  let translationKey;
106
527
  if (pageScope) {
107
528
  if (nestedPath) {
108
- componentTargetPath = join2(process.cwd(), "src", "ui", nestedPath);
529
+ componentTargetPath = join3(cwd, "src", "ui", nestedPath);
109
530
  translationKey = pageScope;
110
531
  } else {
111
- componentTargetPath = join2(process.cwd(), "src", "ui", pageScope);
532
+ componentTargetPath = join3(cwd, "src", "ui", pageScope);
112
533
  translationKey = pageScope;
113
534
  }
114
535
  } else {
115
- componentTargetPath = join2(process.cwd(), "src", "ui", "_global");
536
+ componentTargetPath = join3(cwd, "src", "ui", "_global");
116
537
  translationKey = "_global_ui";
117
538
  }
118
- if (!existsSync2(componentTargetPath)) {
119
- await mkdir(componentTargetPath, { recursive: true });
539
+ await assertSafeTarget(cwd, componentTargetPath);
540
+ if (!existsSync4(componentTargetPath)) {
541
+ await mkdir3(componentTargetPath, { recursive: true });
120
542
  }
121
- const componentFile = join2(componentTargetPath, `${componentNameUpper}.tsx`);
122
- const templateComponentPath = join2(templatePath, "Component.tsx");
123
- if (existsSync2(templateComponentPath)) {
124
- let content = await readFile2(templateComponentPath, "utf-8");
543
+ const componentFile = join3(componentTargetPath, `${componentNameUpper}.tsx`);
544
+ const templateComponentPath = join3(templatePath, "Component.tsx");
545
+ if (existsSync4(templateComponentPath)) {
546
+ let content = await readFile4(templateComponentPath, "utf-8");
125
547
  content = content.replace(/Component/g, componentNameUpper).replace(/componentPage/g, translationKey);
126
- await writeFile(componentFile, content);
548
+ await writeFile3(componentFile, content);
127
549
  console.log(`\u{1F4C4} File created: ${componentFile}`);
128
550
  } else {
129
551
  console.error(
@@ -132,32 +554,40 @@ async function addComponent(args) {
132
554
  );
133
555
  }
134
556
  if (useI18n && messagesPath) {
135
- const entries = await readdir(messagesPath, { withFileTypes: true });
557
+ const entries = await readdir3(messagesPath, { withFileTypes: true });
136
558
  const langDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
137
- const jsonTemplate = join2(templatePath, "component.json");
138
- if (!existsSync2(jsonTemplate)) {
559
+ const jsonTemplate = join3(templatePath, "component.json");
560
+ if (!existsSync4(jsonTemplate)) {
139
561
  console.error("\u274C Template component.json not found:", jsonTemplate);
140
562
  return;
141
563
  }
142
- const jsonContent = await readFile2(jsonTemplate, "utf-8");
564
+ const jsonContent = await readFile4(jsonTemplate, "utf-8");
143
565
  const parsed = JSON.parse(jsonContent);
144
566
  for (const locale of langDirs) {
145
- const localeDir = join2(messagesPath, locale);
146
- if (!existsSync2(localeDir) || !statSync(localeDir).isDirectory())
567
+ const localeDir = join3(messagesPath, locale);
568
+ if (!existsSync4(localeDir) || !statSync(localeDir).isDirectory())
147
569
  continue;
148
570
  let jsonTarget;
149
571
  if (pageScope) {
150
- jsonTarget = join2(messagesPath, locale, `${pageScope}.json`);
572
+ const [messageFile] = pageScope.split(".");
573
+ jsonTarget = join3(messagesPath, locale, `${messageFile}.json`);
151
574
  } else {
152
- jsonTarget = join2(messagesPath, locale, `_global_ui.json`);
575
+ jsonTarget = join3(messagesPath, locale, `_global_ui.json`);
153
576
  }
154
577
  let current = {};
155
- if (existsSync2(jsonTarget)) {
156
- const jsonFile = await readFile2(jsonTarget, "utf-8");
578
+ if (existsSync4(jsonTarget)) {
579
+ const jsonFile = await readFile4(jsonTarget, "utf-8");
157
580
  current = JSON.parse(jsonFile);
158
581
  }
159
- current[componentNameUpper] = parsed;
160
- await writeFile(jsonTarget, JSON.stringify(current, null, 2));
582
+ if (pageScope?.includes(".")) {
583
+ const child = pageScope.split(".")[1];
584
+ const childMessages = current[child] && typeof current[child] === "object" ? current[child] : {};
585
+ childMessages[componentNameUpper] = parsed;
586
+ current[child] = childMessages;
587
+ } else {
588
+ current[componentNameUpper] = parsed;
589
+ }
590
+ await writeFile3(jsonTarget, JSON.stringify(current, null, 2));
161
591
  console.log(`\u{1F4C4} File updated: ${jsonTarget}`);
162
592
  }
163
593
  } else {
@@ -168,15 +598,270 @@ async function addComponent(args) {
168
598
  );
169
599
  }
170
600
 
601
+ // src/lib/addLanguage.ts
602
+ import { join as join4 } from "path";
603
+ import { existsSync as existsSync5 } from "fs";
604
+ import { cp, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
605
+ import prompts4 from "prompts";
606
+ function generateLocales() {
607
+ const dn = new Intl.DisplayNames(["en"], { type: "language" });
608
+ const locales = [];
609
+ for (let i = 0; i < 26; i++) {
610
+ for (let j = 0; j < 26; j++) {
611
+ const code = String.fromCharCode(97 + i) + String.fromCharCode(97 + j);
612
+ try {
613
+ const name = dn.of(code);
614
+ if (name && name.toLowerCase() !== code) {
615
+ locales.push(code);
616
+ }
617
+ } catch {
618
+ }
619
+ }
620
+ }
621
+ return locales.sort();
622
+ }
623
+ async function addLanguage(args, cwd = process.cwd()) {
624
+ const config = await loadConfig(cwd);
625
+ if (!config?.useI18n) {
626
+ console.error("\u274C i18n is not enabled in this project.");
627
+ return;
628
+ }
629
+ const messagesPath = join4(cwd, "messages");
630
+ if (!existsSync5(messagesPath)) {
631
+ console.error("\u274C Messages directory missing. Ensure i18n was configured.");
632
+ return;
633
+ }
634
+ const available = generateLocales();
635
+ let locale = args[1];
636
+ if (!locale || !available.includes(locale)) {
637
+ const response = await prompts4({
638
+ type: "autocomplete",
639
+ name: "locale",
640
+ message: "\u{1F310} Locale to add:",
641
+ choices: available.map((l) => ({ title: l, value: l }))
642
+ });
643
+ locale = response.locale;
644
+ }
645
+ if (!locale) return;
646
+ parseLogicalName(locale, "locale");
647
+ await assertSafeTarget(cwd, messagesPath);
648
+ if (existsSync5(join4(messagesPath, locale))) {
649
+ throw new Error(`Locale ${locale} already exists.`);
650
+ }
651
+ const routingFile = join4(cwd, "src", "lib", "i18n", "routing.ts");
652
+ const messagesRegistryFile = join4(cwd, "src", "lib", "i18n", "messages.ts");
653
+ if (!existsSync5(routingFile)) {
654
+ throw new Error("routing.ts not found. Are you in project root?");
655
+ }
656
+ if (!existsSync5(messagesRegistryFile)) {
657
+ throw new Error("messages.ts not found. Are you in project root?");
658
+ }
659
+ const routingContent = await readFile5(routingFile, "utf-8");
660
+ const defaultMatch = routingContent.match(/defaultLocale:\s*"([^"]+)"/);
661
+ const defaultLocale = defaultMatch ? defaultMatch[1] : null;
662
+ if (!defaultLocale || !existsSync5(join4(messagesPath, defaultLocale))) {
663
+ throw new Error("Default locale not found.");
664
+ }
665
+ const defaultAggregatorFile = join4(messagesPath, `${defaultLocale}.ts`);
666
+ if (!existsSync5(defaultAggregatorFile)) {
667
+ throw new Error(`Default locale aggregator not found: ${defaultLocale}.ts`);
668
+ }
669
+ const localesMatch = routingContent.match(/locales:\s*\[([^\]]*)\]/);
670
+ if (!localesMatch) {
671
+ throw new Error("Unable to locate routing locales.");
672
+ }
673
+ const localesArr = localesMatch[1].split(",").map((s) => s.trim().replace(/["']/g, "")).filter(Boolean);
674
+ if (localesArr.includes(locale)) {
675
+ throw new Error(`Locale ${locale} already exists in routing.`);
676
+ }
677
+ const newLocales = `locales: [${[...localesArr, locale].map((item) => `"${item}"`).join(", ")}]`;
678
+ const nextRoutingContent = routingContent.replace(
679
+ /locales:\s*\[[^\]]*\]/,
680
+ newLocales
681
+ );
682
+ const defaultAggregatorContent = await readFile5(
683
+ defaultAggregatorFile,
684
+ "utf-8"
685
+ );
686
+ const defaultImportPrefix = `./${defaultLocale}/`;
687
+ if (!defaultAggregatorContent.includes(defaultImportPrefix)) {
688
+ throw new Error(
689
+ `Default locale aggregator does not import from ${defaultImportPrefix}`
690
+ );
691
+ }
692
+ const nextAggregatorContent = defaultAggregatorContent.replaceAll(
693
+ defaultImportPrefix,
694
+ `./${locale}/`
695
+ );
696
+ const messagesRegistryContent = await readFile5(messagesRegistryFile, "utf-8");
697
+ const registryMatch = messagesRegistryContent.match(
698
+ /const messages = \{([^}]*)\} as const;/
699
+ );
700
+ if (!registryMatch) {
701
+ throw new Error("Unable to locate the typed messages registry.");
702
+ }
703
+ const registeredLocales = registryMatch[1].split(",").map((item) => item.trim()).filter(Boolean);
704
+ if (registeredLocales.includes(locale)) {
705
+ throw new Error(`Locale ${locale} already exists in messages registry.`);
706
+ }
707
+ const registryDeclaration = `const messages = { ${[
708
+ ...registeredLocales,
709
+ locale
710
+ ].join(", ")} } as const;`;
711
+ const declarationIndex = messagesRegistryContent.indexOf("const messages =");
712
+ const nextMessagesRegistryContent = messagesRegistryContent.slice(0, declarationIndex) + `import ${locale} from "../../../messages/${locale}";
713
+
714
+ ` + messagesRegistryContent.slice(declarationIndex).replace(/const messages = \{[^}]*\} as const;/, registryDeclaration);
715
+ await cp(join4(messagesPath, defaultLocale), join4(messagesPath, locale), {
716
+ recursive: true
717
+ });
718
+ console.log(`\u{1F4C4} Directory created: ${join4(messagesPath, locale)}`);
719
+ await writeFile4(join4(messagesPath, `${locale}.ts`), nextAggregatorContent);
720
+ console.log(`\u{1F4C4} File created: ${join4(messagesPath, `${locale}.ts`)}`);
721
+ await writeFile4(messagesRegistryFile, nextMessagesRegistryContent);
722
+ console.log(`\u{1F4C4} File updated: ${messagesRegistryFile}`);
723
+ await writeFile4(routingFile, nextRoutingContent);
724
+ console.log(`\u{1F4C4} File updated: ${routingFile}`);
725
+ console.log(
726
+ `\u2705 Locale "${locale}" added and copied from default locale "${defaultLocale}".`
727
+ );
728
+ }
729
+
730
+ // src/lib/addLib.ts
731
+ import { join as join5 } from "path";
732
+ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
733
+ import prompts5 from "prompts";
734
+ import { existsSync as existsSync6 } from "fs";
735
+ async function addLib(args, cwd = process.cwd()) {
736
+ let libArg = args[1];
737
+ if (!libArg || libArg.startsWith("-")) {
738
+ const response = await prompts5.prompt({
739
+ type: "text",
740
+ name: "libArg",
741
+ message: "\u{1F4E6} Lib name to add:",
742
+ validate: (name) => name ? true : "Lib name is required"
743
+ });
744
+ libArg = response.libArg;
745
+ }
746
+ const libSegments = parseLogicalName(libArg, "library name");
747
+ let libName = libArg;
748
+ let fileName = null;
749
+ if (libSegments.length === 2) {
750
+ [libName, fileName] = libSegments;
751
+ } else if (libSegments.length > 2) {
752
+ throw new Error("Libraries currently support exactly library.module.");
753
+ }
754
+ const config = await loadConfig(cwd);
755
+ if (!config) {
756
+ console.error(
757
+ "\u274C Configuration file cnp.config.json not found. Run this command from the project root."
758
+ );
759
+ return;
760
+ }
761
+ const libDir = join5(cwd, "src", "lib", libName);
762
+ await assertSafeTarget(cwd, libDir);
763
+ if (!existsSync6(libDir)) {
764
+ await mkdir4(libDir, { recursive: true });
765
+ }
766
+ const templateDir = join5(
767
+ resolvePackageRoot(import.meta.url),
768
+ "templates",
769
+ "Lib"
770
+ );
771
+ const indexTemplate = join5(templateDir, "index.ts");
772
+ const fileTemplate = join5(templateDir, "item.ts");
773
+ const indexPath = join5(libDir, "index.ts");
774
+ if (!existsSync6(indexPath)) {
775
+ if (existsSync6(indexTemplate)) {
776
+ const content = await readFile6(indexTemplate, "utf-8");
777
+ await writeFile5(indexPath, content);
778
+ } else {
779
+ await writeFile5(indexPath, "export {}\n");
780
+ }
781
+ console.log(`\u{1F4C4} File created: ${indexPath}`);
782
+ }
783
+ if (fileName) {
784
+ const filePath = join5(libDir, `${fileName}.ts`);
785
+ if (!existsSync6(filePath)) {
786
+ if (existsSync6(fileTemplate)) {
787
+ let content = await readFile6(fileTemplate, "utf-8");
788
+ content = content.replace(/template/g, fileName).replace(/Template/g, capitalize(fileName));
789
+ await writeFile5(filePath, content);
790
+ } else {
791
+ await writeFile5(
792
+ filePath,
793
+ `export function ${fileName}() {
794
+ // TODO: implement
795
+ }
796
+ `
797
+ );
798
+ }
799
+ console.log(`\u{1F4C4} File created: ${filePath}`);
800
+ }
801
+ let indexContent = await readFile6(indexPath, "utf-8");
802
+ const importLine = `import { ${fileName} } from "./${fileName}";`;
803
+ const importRegex = new RegExp(
804
+ `import\\s*{\\s*${fileName}\\s*}\\s*from\\s*"\\./${fileName}";`
805
+ );
806
+ const exportRegex = /export\s*{([^}]*)}/m;
807
+ const imports = [];
808
+ const exportsSet = [];
809
+ for (const line of indexContent.split("\n")) {
810
+ if (line.startsWith("import")) {
811
+ imports.push(line);
812
+ } else if (line.startsWith("export")) {
813
+ const match = line.match(exportRegex);
814
+ if (match && match[1]) {
815
+ exportsSet.push(
816
+ ...match[1].split(",").map((s) => s.trim()).filter(Boolean)
817
+ );
818
+ }
819
+ }
820
+ }
821
+ if (!imports.some((l) => importRegex.test(l))) {
822
+ imports.push(importLine);
823
+ }
824
+ if (!exportsSet.includes(fileName)) {
825
+ exportsSet.push(fileName);
826
+ }
827
+ indexContent = imports.join("\n") + "\n\nexport { " + exportsSet.join(", ") + " };\n";
828
+ await writeFile5(indexPath, indexContent);
829
+ console.log(`\u270F\uFE0F Updated index: ${indexPath}`);
830
+ }
831
+ console.log(
832
+ `\u2705 Lib "${libName}"${fileName ? ` with module ${fileName}` : ""} added.`
833
+ );
834
+ }
835
+
171
836
  // src/lib/addPage.ts
172
- import { join as join3 } from "path";
173
- import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2, readdir as readdir2 } from "fs/promises";
174
- import prompts2 from "prompts";
175
- import { existsSync as existsSync3, statSync as statSync2 } from "fs";
176
- async function addPage(args) {
837
+ import { join as join6 } from "path";
838
+ import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile6, readdir as readdir4 } from "fs/promises";
839
+ import prompts6 from "prompts";
840
+ import { existsSync as existsSync7, statSync as statSync2 } from "fs";
841
+ function registerMessagesFile(content, locale, fileName) {
842
+ const importPath = `./${locale}/${fileName}.json`;
843
+ if (content.includes(importPath)) return content;
844
+ const declarationIndex = content.indexOf("const messages =");
845
+ const registryMatch = content.match(/const messages = \{([\s\S]*?)\n\};/);
846
+ if (declarationIndex === -1 || !registryMatch) {
847
+ throw new Error(
848
+ `Unable to register ${fileName}.json in messages/${locale}.ts`
849
+ );
850
+ }
851
+ const importStatement = `import ${fileName} from "${importPath}";
852
+ `;
853
+ const nextRegistry = registryMatch[0].replace(
854
+ /\n\};$/,
855
+ `
856
+ ${fileName},
857
+ };`
858
+ );
859
+ return content.slice(0, declarationIndex) + importStatement + content.slice(declarationIndex).replace(registryMatch[0], nextRegistry);
860
+ }
861
+ async function addPage(args, cwd = process.cwd()) {
177
862
  let pageName = args[1];
178
863
  if (!pageName || pageName.startsWith("-")) {
179
- const response = await prompts2.prompt({
864
+ const response = await prompts6.prompt({
180
865
  type: "text",
181
866
  name: "pageName",
182
867
  message: "\u{1F4DD} Page name to add:",
@@ -184,13 +869,16 @@ async function addPage(args) {
184
869
  });
185
870
  pageName = response.pageName;
186
871
  }
872
+ const pageSegments = parseLogicalName(pageName, "page name");
187
873
  let parentName = null;
188
874
  let childName = null;
189
- if (pageName.includes(".")) {
190
- [parentName, childName] = pageName.split(".");
875
+ if (pageSegments.length === 2) {
876
+ [parentName, childName] = pageSegments;
877
+ } else if (pageSegments.length > 2) {
878
+ throw new Error("Nested pages currently support exactly Parent.Child.");
191
879
  }
192
880
  let shortFlags = args.find((arg) => /^-[A-Za-z]+$/.test(arg));
193
- let longFlags = new Set(args.filter((a) => a.startsWith("--")));
881
+ const longFlags = new Set(args.filter((a) => a.startsWith("--")));
194
882
  const flags = /* @__PURE__ */ new Set();
195
883
  if (!shortFlags && Array.from(longFlags).length === 0) {
196
884
  shortFlags = "-LPl";
@@ -241,7 +929,7 @@ async function addPage(args) {
241
929
  ]) {
242
930
  if (longFlags.has("--" + flag)) flags.add(flag);
243
931
  }
244
- const config = await loadConfig();
932
+ const config = await loadConfig(cwd);
245
933
  if (!config) {
246
934
  console.error(
247
935
  "\u274C Configuration file cnp.config.json not found. Run this command from the project root."
@@ -249,50 +937,58 @@ async function addPage(args) {
249
937
  return;
250
938
  }
251
939
  const useI18n = !!config.useI18n;
940
+ const aliasPrefix = configuredAliasPrefix(config);
252
941
  const srcSegments = ["src", "app"];
253
942
  if (useI18n) srcSegments.push("[locale]");
254
- const srcPath = join3(process.cwd(), ...srcSegments);
255
- if (!existsSync3(srcPath)) {
943
+ const srcPath = join6(cwd, ...srcSegments);
944
+ if (!existsSync7(srcPath)) {
256
945
  console.error(`\u274C Expected directory not found: ${srcPath}`);
257
946
  return;
258
947
  }
259
948
  let messagesPath = null;
260
949
  let locales = [];
261
950
  if (useI18n) {
262
- messagesPath = join3(process.cwd(), "messages");
263
- if (!existsSync3(messagesPath)) {
951
+ messagesPath = join6(cwd, "messages");
952
+ if (!existsSync7(messagesPath)) {
264
953
  console.error(
265
954
  "\u274C Messages directory missing. Ensure i18n was configured."
266
955
  );
267
956
  return;
268
957
  }
269
- const entries = await readdir2(messagesPath, { withFileTypes: true });
958
+ const entries = await readdir4(messagesPath, { withFileTypes: true });
270
959
  locales = entries.filter((e) => e.isDirectory()).map((e) => e.name);
271
960
  }
272
- const templatePath = join3(
273
- new URL("..", import.meta.url).pathname,
961
+ const templatePath = join6(
962
+ resolvePackageRoot(import.meta.url),
274
963
  "templates",
275
964
  "Page"
276
965
  );
277
966
  let uiPageDir, localePagePath, jsonFileName;
278
967
  if (parentName && childName) {
279
- uiPageDir = join3(process.cwd(), "src", "ui", parentName, childName);
280
- localePagePath = join3(srcPath, parentName, childName);
968
+ uiPageDir = join6(cwd, "src", "ui", parentName, childName);
969
+ localePagePath = join6(srcPath, parentName, childName);
281
970
  jsonFileName = parentName;
282
971
  } else {
283
- uiPageDir = join3(process.cwd(), "src", "ui", pageName);
284
- localePagePath = join3(srcPath, pageName);
972
+ uiPageDir = join6(cwd, "src", "ui", pageName);
973
+ localePagePath = join6(srcPath, pageName);
285
974
  jsonFileName = pageName;
286
975
  }
287
- if (!existsSync3(uiPageDir)) {
288
- await mkdir2(uiPageDir, { recursive: true });
289
- }
290
- const uiPageFile = join3(uiPageDir, "page-ui.tsx");
291
- const uiPageTemplate = join3(templatePath, "page-ui.tsx");
292
- if (existsSync3(uiPageTemplate)) {
293
- let uiContent = await readFile3(uiPageTemplate, "utf-8");
294
- uiContent = uiContent.replace(/template/g, childName || pageName).replace(/Template/g, capitalize(childName || pageName));
295
- await writeFile2(uiPageFile, uiContent);
976
+ await assertSafeTarget(cwd, uiPageDir);
977
+ await assertSafeTarget(cwd, localePagePath);
978
+ if (!existsSync7(uiPageDir)) {
979
+ await mkdir5(uiPageDir, { recursive: true });
980
+ }
981
+ const uiPageFile = join6(uiPageDir, "page-ui.tsx");
982
+ const uiPageTemplate = join6(templatePath, "page-ui.tsx");
983
+ if (existsSync7(uiPageTemplate)) {
984
+ let uiContent = await readFile7(uiPageTemplate, "utf-8");
985
+ const translationNamespace = parentName && childName ? `${parentName}.${childName}` : pageName;
986
+ uiContent = uiContent.replace(
987
+ 'useTranslations("template")',
988
+ `useTranslations("${translationNamespace}")`
989
+ );
990
+ uiContent = uiContent.replaceAll('from "@/', `from "${aliasPrefix}/`).replace(/template/g, childName || pageName).replace(/Template/g, capitalize(childName || pageName));
991
+ await writeFile6(uiPageFile, uiContent);
296
992
  console.log(`\u{1F4C4} File created: ${uiPageFile}`);
297
993
  } else {
298
994
  console.warn(
@@ -300,37 +996,38 @@ async function addPage(args) {
300
996
  uiPageTemplate
301
997
  );
302
998
  }
303
- if (!existsSync3(localePagePath)) {
304
- await mkdir2(localePagePath, { recursive: true });
999
+ if (!existsSync7(localePagePath)) {
1000
+ await mkdir5(localePagePath, { recursive: true });
305
1001
  }
306
1002
  for (const flag of flags) {
307
1003
  const filename = toFileName(flag);
308
- const src = join3(templatePath, filename);
309
- const dst = join3(localePagePath, filename);
310
- if (!existsSync3(src)) {
1004
+ const src = join6(templatePath, filename);
1005
+ const dst = join6(localePagePath, filename);
1006
+ if (!existsSync7(src)) {
311
1007
  console.warn(`\u26A0\uFE0F Missing template file: ${filename} at path: ${src}`);
312
1008
  continue;
313
1009
  }
314
- const content = await readFile3(src, "utf-8");
315
- const replaced = content.replace(/template/g, childName || pageName).replace(/Template/g, capitalize(childName || pageName));
316
- await writeFile2(dst, replaced);
1010
+ const content = await readFile7(src, "utf-8");
1011
+ const uiImportPath = parentName && childName ? `${parentName}/${childName}` : pageName;
1012
+ const replaced = content.replace("@/ui/template/", `@/ui/${uiImportPath}/`).replaceAll('from "@/', `from "${aliasPrefix}/`).replace(/template/g, childName || pageName).replace(/Template/g, capitalize(childName || pageName));
1013
+ await writeFile6(dst, replaced);
317
1014
  console.log(`\u{1F4C4} File created: ${dst}`);
318
1015
  }
319
1016
  if (useI18n && messagesPath) {
320
- const jsonTemplate = join3(templatePath, "page.json");
321
- if (!existsSync3(jsonTemplate)) {
1017
+ const jsonTemplate = join6(templatePath, "page.json");
1018
+ if (!existsSync7(jsonTemplate)) {
322
1019
  console.warn("\u26A0\uFE0F Missing template: page.json at path:", jsonTemplate);
323
1020
  } else {
324
- const content = await readFile3(jsonTemplate, "utf-8");
1021
+ const content = await readFile7(jsonTemplate, "utf-8");
325
1022
  const replaced = content.replace(/template/g, childName || pageName).replace(/Template/g, capitalize(childName || pageName));
326
1023
  for (const locale of locales) {
327
- const localeDir = join3(messagesPath, locale);
328
- if (!existsSync3(localeDir) || !statSync2(localeDir).isDirectory())
1024
+ const localeDir = join6(messagesPath, locale);
1025
+ if (!existsSync7(localeDir) || !statSync2(localeDir).isDirectory())
329
1026
  continue;
330
- const jsonTarget = join3(messagesPath, locale, `${jsonFileName}.json`);
1027
+ const jsonTarget = join6(messagesPath, locale, `${jsonFileName}.json`);
331
1028
  let current = {};
332
- if (existsSync3(jsonTarget)) {
333
- const jsonFile = await readFile3(jsonTarget, "utf-8");
1029
+ if (existsSync7(jsonTarget)) {
1030
+ const jsonFile = await readFile7(jsonTarget, "utf-8");
334
1031
  try {
335
1032
  current = JSON.parse(jsonFile);
336
1033
  } catch {
@@ -342,8 +1039,23 @@ async function addPage(args) {
342
1039
  } else {
343
1040
  current = JSON.parse(replaced);
344
1041
  }
345
- await writeFile2(jsonTarget, JSON.stringify(current, null, 2));
1042
+ await writeFile6(jsonTarget, `${JSON.stringify(current, null, 2)}
1043
+ `);
346
1044
  console.log(`\u{1F4C4} File created: ${jsonTarget}`);
1045
+ const localeAggregator = join6(messagesPath, `${locale}.ts`);
1046
+ if (!existsSync7(localeAggregator)) {
1047
+ throw new Error(`Locale aggregator not found: messages/${locale}.ts`);
1048
+ }
1049
+ const aggregatorContent = await readFile7(localeAggregator, "utf-8");
1050
+ const nextAggregatorContent = registerMessagesFile(
1051
+ aggregatorContent,
1052
+ locale,
1053
+ jsonFileName
1054
+ );
1055
+ if (nextAggregatorContent !== aggregatorContent) {
1056
+ await writeFile6(localeAggregator, nextAggregatorContent);
1057
+ console.log(`\u{1F4C4} File updated: ${localeAggregator}`);
1058
+ }
347
1059
  }
348
1060
  }
349
1061
  } else {
@@ -354,216 +1066,170 @@ async function addPage(args) {
354
1066
  );
355
1067
  }
356
1068
 
357
- // src/lib/rmPage.ts
358
- import { join as join4 } from "path";
359
- import { writeFile as writeFile3, readdir as readdir3 } from "fs/promises";
360
- import prompts3 from "prompts";
361
- import { existsSync as existsSync4 } from "fs";
362
- async function rmPage(args) {
363
- let pageName = args[1];
364
- if (!pageName || pageName.startsWith("-")) {
365
- const response = await prompts3.prompt({
366
- type: "text",
367
- name: "pageName",
368
- message: "\u{1F5D1}\uFE0F Page name to remove:",
369
- validate: (name) => name ? true : "Page name is required"
370
- });
371
- pageName = response.pageName;
372
- }
373
- const messagesPath = join4(process.cwd(), "messages");
374
- const entries = await readdir3(messagesPath, { withFileTypes: true });
375
- const langDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
376
- for (const locale of langDirs) {
377
- const jsonTarget = join4(messagesPath, locale, `${pageName}.json`);
378
- if (existsSync4(jsonTarget)) {
379
- await writeFile3(jsonTarget, "");
380
- await import("child_process").then(
381
- (cp3) => cp3.execSync(`rm -f '${jsonTarget}'`)
382
- );
383
- console.log(`\u{1F5D1}\uFE0F Deleted: ${jsonTarget}`);
384
- }
385
- }
386
- const uiPageDir = join4(process.cwd(), "src", "ui", pageName);
387
- if (existsSync4(uiPageDir)) {
388
- await import("child_process").then(
389
- (cp3) => cp3.execSync(`rm -rf '${uiPageDir}'`)
390
- );
391
- console.log(`\u{1F5D1}\uFE0F Deleted: ${uiPageDir}`);
392
- }
393
- const appLocaleDir = join4(process.cwd(), "src", "app", "[locale]", pageName);
394
- if (existsSync4(appLocaleDir)) {
395
- await import("child_process").then(
396
- (cp3) => cp3.execSync(`rm -rf '${appLocaleDir}'`)
397
- );
398
- console.log(`\u{1F5D1}\uFE0F Deleted: ${appLocaleDir}`);
399
- }
400
- console.log(`\u2705 Page "${pageName}" deleted.`);
1069
+ // src/lib/addText.ts
1070
+ import { join as join7 } from "path";
1071
+ import { existsSync as existsSync8 } from "fs";
1072
+ import { readFile as readFile8, writeFile as writeFile7, readdir as readdir5 } from "fs/promises";
1073
+ function defaultText(key) {
1074
+ return key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
401
1075
  }
402
-
403
- // src/lib/addLib.ts
404
- import { join as join5 } from "path";
405
- import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
406
- import prompts4 from "prompts";
407
- import { existsSync as existsSync5 } from "fs";
408
- async function addLib(args) {
409
- let libArg = args[1];
410
- if (!libArg || libArg.startsWith("-")) {
411
- const response = await prompts4.prompt({
412
- type: "text",
413
- name: "libArg",
414
- message: "\u{1F4E6} Lib name to add:",
415
- validate: (name) => name ? true : "Lib name is required"
416
- });
417
- libArg = response.libArg;
418
- }
419
- let libName = libArg;
420
- let fileName = null;
421
- if (libArg.includes(".")) {
422
- [libName, fileName] = libArg.split(".");
1076
+ async function addText(args, cwd = process.cwd()) {
1077
+ const pathArg = args[1];
1078
+ if (!pathArg) {
1079
+ console.error("\u274C Dot path parameter is required.");
1080
+ return;
423
1081
  }
424
- const config = await loadConfig();
425
- if (!config) {
426
- console.error(
427
- "\u274C Configuration file cnp.config.json not found. Run this command from the project root."
428
- );
1082
+ const providedText = args.slice(2).join(" ");
1083
+ parseLogicalName(pathArg, "translation path");
1084
+ const config = await loadConfig(cwd);
1085
+ if (!config?.useI18n) {
1086
+ console.error("\u274C i18n is not enabled in this project.");
429
1087
  return;
430
1088
  }
431
- const libDir = join5(process.cwd(), "src", "lib", libName);
432
- if (!existsSync5(libDir)) {
433
- await mkdir3(libDir, { recursive: true });
1089
+ const messagesPath = join7(cwd, "messages");
1090
+ if (!existsSync8(messagesPath)) {
1091
+ console.error("\u274C Messages directory missing. Ensure i18n was configured.");
1092
+ return;
434
1093
  }
435
- const templateDir = join5(
436
- new URL("..", import.meta.url).pathname,
437
- "templates",
438
- "Lib"
439
- );
440
- const indexTemplate = join5(templateDir, "index.ts");
441
- const fileTemplate = join5(templateDir, "item.ts");
442
- const indexPath = join5(libDir, "index.ts");
443
- if (!existsSync5(indexPath)) {
444
- if (existsSync5(indexTemplate)) {
445
- const content = await readFile4(indexTemplate, "utf-8");
446
- await writeFile4(indexPath, content);
447
- } else {
448
- await writeFile4(indexPath, "export {}\n");
449
- }
450
- console.log(`\u{1F4C4} File created: ${indexPath}`);
1094
+ const entries = await readdir5(messagesPath, { withFileTypes: true });
1095
+ const locales = entries.filter((e) => e.isDirectory()).map((e) => e.name);
1096
+ const [fileName, ...segments] = pathArg.split(".");
1097
+ if (!fileName || segments.length === 0) {
1098
+ console.error("\u274C Invalid dot path provided.");
1099
+ return;
451
1100
  }
452
- if (fileName) {
453
- const filePath = join5(libDir, `${fileName}.ts`);
454
- if (!existsSync5(filePath)) {
455
- if (existsSync5(fileTemplate)) {
456
- let content = await readFile4(fileTemplate, "utf-8");
457
- content = content.replace(/template/g, fileName).replace(/Template/g, capitalize(fileName));
458
- await writeFile4(filePath, content);
459
- } else {
460
- await writeFile4(
461
- filePath,
462
- `export function ${fileName}() {
463
- // TODO: implement
464
- }
465
- `
466
- );
467
- }
468
- console.log(`\u{1F4C4} File created: ${filePath}`);
469
- }
470
- let indexContent = await readFile4(indexPath, "utf-8");
471
- const importLine = `import { ${fileName} } from "./${fileName}";`;
472
- const importRegex = new RegExp(
473
- `import\\s*{\\s*${fileName}\\s*}\\s*from\\s*"\\./${fileName}";`
474
- );
475
- const exportRegex = /export\s*{([^}]*)}/m;
476
- const imports = [];
477
- const exportsSet = [];
478
- for (const line of indexContent.split("\n")) {
479
- if (line.startsWith("import")) {
480
- imports.push(line);
481
- } else if (line.startsWith("export")) {
482
- const match = line.match(exportRegex);
483
- if (match && match[1]) {
484
- exportsSet.push(
485
- ...match[1].split(",").map((s) => s.trim()).filter(Boolean)
486
- );
487
- }
1101
+ const finalKey = segments[segments.length - 1];
1102
+ const text = providedText || defaultText(finalKey);
1103
+ for (const locale of locales) {
1104
+ const filePath = join7(messagesPath, locale, `${fileName}.json`);
1105
+ await assertSafeTarget(cwd, filePath);
1106
+ let data = {};
1107
+ if (existsSync8(filePath)) {
1108
+ const raw = await readFile8(filePath, "utf-8");
1109
+ try {
1110
+ data = JSON.parse(raw);
1111
+ } catch {
488
1112
  }
489
1113
  }
490
- if (!imports.some((l) => importRegex.test(l))) {
491
- imports.push(importLine);
492
- }
493
- if (!exportsSet.includes(fileName)) {
494
- exportsSet.push(fileName);
1114
+ let cursor = data;
1115
+ for (let i = 0; i < segments.length - 1; i++) {
1116
+ const seg = segments[i];
1117
+ if (!cursor[seg]) cursor[seg] = {};
1118
+ cursor = cursor[seg];
495
1119
  }
496
- indexContent = imports.join("\n") + "\n\nexport { " + exportsSet.join(", ") + " };\n";
497
- await writeFile4(indexPath, indexContent);
498
- console.log(`\u270F\uFE0F Updated index: ${indexPath}`);
499
- }
500
- console.log(
501
- `\u2705 Lib "${libName}"${fileName ? ` with module ${fileName}` : ""} added.`
502
- );
503
- }
504
-
505
- // src/lib/addApi.ts
506
- import { join as join6 } from "path";
507
- import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
508
- import prompts5 from "prompts";
509
- import { existsSync as existsSync6 } from "fs";
510
- async function addApi(args) {
511
- let apiName = args[1];
512
- if (!apiName || apiName.startsWith("-")) {
513
- const response = await prompts5.prompt({
514
- type: "text",
515
- name: "apiName",
516
- message: "\u{1F50C} API route name to add:",
517
- validate: (name) => name ? true : "API route name is required"
518
- });
519
- apiName = response.apiName;
1120
+ cursor[finalKey] = text;
1121
+ await writeFile7(filePath, JSON.stringify(data, null, 2));
1122
+ console.log(`\u{1F4C4} File updated: ${filePath}`);
520
1123
  }
521
- const config = await loadConfig();
522
- if (!config) {
523
- console.error(
524
- "\u274C Configuration file cnp.config.json not found. Run this command from the project root."
525
- );
1124
+ console.log(`\u2705 Text added at path "${pathArg}" with value "${text}".`);
1125
+ }
1126
+
1127
+ // src/lib/rmPage.ts
1128
+ import { existsSync as existsSync9 } from "fs";
1129
+ import { readFile as readFile9, readdir as readdir6, rm, writeFile as writeFile8 } from "fs/promises";
1130
+ import path6 from "path";
1131
+ async function removeMessages(projectRoot, candidate, terminal) {
1132
+ const messagesRoot = resolveInside(projectRoot, "messages");
1133
+ let locales;
1134
+ try {
1135
+ locales = (await readdir6(messagesRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
1136
+ } catch {
526
1137
  return;
527
1138
  }
528
- const apiDir = join6(process.cwd(), "src", "app", "api", apiName);
529
- if (!existsSync6(apiDir)) {
530
- await mkdir4(apiDir, { recursive: true });
531
- }
532
- const templateDir = join6(
533
- new URL("..", import.meta.url).pathname,
534
- "templates",
535
- "Api"
536
- );
537
- const routeTemplate = join6(templateDir, "route.ts");
538
- const routePath = join6(apiDir, "route.ts");
539
- if (!existsSync6(routePath)) {
540
- if (existsSync6(routeTemplate)) {
541
- let content = await readFile5(routeTemplate, "utf-8");
542
- content = content.replace(/template/g, apiName);
543
- await writeFile5(routePath, content);
1139
+ for (const locale of locales) {
1140
+ const target = resolveInside(
1141
+ projectRoot,
1142
+ "messages",
1143
+ locale,
1144
+ `${candidate.routeSegments[0]}.json`
1145
+ );
1146
+ if (!existsSync9(target)) continue;
1147
+ if (candidate.messageKey) {
1148
+ const data = JSON.parse(await readFile9(target, "utf8"));
1149
+ delete data[candidate.messageKey];
1150
+ await writeFile8(target, `${JSON.stringify(data, null, 2)}
1151
+ `);
544
1152
  } else {
545
- await writeFile5(
546
- routePath,
547
- `import { NextResponse } from "next/server";
548
-
549
- export async function GET() {
550
- return NextResponse.json({ message: "Hello from ${apiName}" });
1153
+ await rm(target);
1154
+ }
1155
+ terminal.log(`\u{1F5D1}\uFE0F Deleted messages for ${candidate.logicalName}: ${target}`);
1156
+ }
551
1157
  }
552
- `
553
- );
1158
+ async function rmPage(args, cwd = process.cwd(), prompt, terminal = console) {
1159
+ const candidates = await discoverPages(cwd);
1160
+ let logicalName = args[1];
1161
+ if (!logicalName || logicalName.startsWith("-")) {
1162
+ if (!prompt)
1163
+ throw new CliError("A page name is required in non-interactive mode.");
1164
+ const selected = await prompt([
1165
+ {
1166
+ type: "autocomplete",
1167
+ name: "page",
1168
+ message: "\u{1F5D1}\uFE0F Page to remove:",
1169
+ choices: candidates.map((candidate2) => ({
1170
+ title: candidate2.logicalName.replaceAll(".", " \u203A "),
1171
+ value: candidate2.logicalName
1172
+ }))
1173
+ },
1174
+ {
1175
+ type: (value) => value ? "confirm" : null,
1176
+ name: "confirm",
1177
+ message: "Confirm page deletion?",
1178
+ initial: false
1179
+ }
1180
+ ]);
1181
+ if (!selected.confirm) {
1182
+ terminal.log("Page deletion cancelled.");
1183
+ return;
554
1184
  }
555
- console.log(`\u{1F4C4} File created: ${routePath}`);
556
- } else {
557
- console.log(`\u2139\uFE0F File already exists: ${routePath}`);
1185
+ logicalName = String(selected.page ?? "");
558
1186
  }
559
- console.log(`\u2705 API route "${apiName}" added.`);
1187
+ parseLogicalName(logicalName, "page name");
1188
+ const candidate = candidates.find(
1189
+ (entry) => entry.logicalName === logicalName
1190
+ );
1191
+ if (!candidate) throw new CliError(`Page not found: ${logicalName}`);
1192
+ await removeMessages(cwd, candidate, terminal);
1193
+ for (const target of [candidate.uiDirectory, candidate.routeDirectory]) {
1194
+ const safeTarget = resolveInside(cwd, path6.relative(cwd, target));
1195
+ if (existsSync9(safeTarget)) {
1196
+ await rm(safeTarget, { recursive: true, force: false });
1197
+ terminal.log(`\u{1F5D1}\uFE0F Deleted: ${safeTarget}`);
1198
+ }
1199
+ }
1200
+ terminal.log(`\u2705 Page "${logicalName}" deleted.`);
1201
+ }
1202
+
1203
+ // src/cli/registry.ts
1204
+ function legacyHandler(command) {
1205
+ return async (args, context) => {
1206
+ await command(args, context.cwd);
1207
+ return success();
1208
+ };
1209
+ }
1210
+ function createCommandRegistry() {
1211
+ return /* @__PURE__ */ new Map([
1212
+ ["addcomponent", legacyHandler(addComponent)],
1213
+ ["addpage", legacyHandler(addPage)],
1214
+ ["addlib", legacyHandler(addLib)],
1215
+ ["addapi", legacyHandler(addApi)],
1216
+ ["addlanguage", legacyHandler(addLanguage)],
1217
+ ["addtext", legacyHandler(addText)],
1218
+ [
1219
+ "rmpage",
1220
+ async (args, context) => {
1221
+ await rmPage(args, context.cwd, context.prompt, context.terminal);
1222
+ return success();
1223
+ }
1224
+ ]
1225
+ ]);
560
1226
  }
561
1227
 
562
1228
  // src/scaffold.ts
563
- import { cp, mkdir as mkdir5, rm, writeFile as writeFile6, readFile as readFile6 } from "fs/promises";
564
- import { join as join7 } from "path";
565
- import { existsSync as existsSync7 } from "fs";
566
- import { fileURLToPath } from "url";
1229
+ import { mkdir as mkdir7, rm as rm2, writeFile as writeFile10 } from "fs/promises";
1230
+ import { join as join8, resolve } from "path";
1231
+ import { existsSync as existsSync10 } from "fs";
1232
+ import { fileURLToPath as fileURLToPath2 } from "url";
567
1233
 
568
1234
  // src/lib/helper/consoleColor.ts
569
1235
  var RED = "\x1B[31m";
@@ -580,73 +1246,186 @@ function cyan(text) {
580
1246
  return CYAN + text + RESET;
581
1247
  }
582
1248
 
1249
+ // src/core/template-manifest.ts
1250
+ import {
1251
+ lstat as lstat2,
1252
+ mkdir as mkdir6,
1253
+ readdir as readdir7,
1254
+ readFile as readFile10,
1255
+ writeFile as writeFile9,
1256
+ copyFile as copyFile2
1257
+ } from "fs/promises";
1258
+ import path7 from "path";
1259
+ var TEMPLATE_DENY_NAMES = /* @__PURE__ */ new Set([
1260
+ ".env",
1261
+ ".git",
1262
+ ".agent",
1263
+ ".cursor",
1264
+ ".next",
1265
+ "node_modules",
1266
+ "artifacts",
1267
+ "coverage",
1268
+ "playwright-report",
1269
+ "test-results"
1270
+ ]);
1271
+ function isDistributableTemplatePath(relativePath) {
1272
+ const segments = relativePath.split(path7.sep);
1273
+ return !segments.some(
1274
+ (segment) => TEMPLATE_DENY_NAMES.has(segment) || segment.endsWith(".tsbuildinfo") || segment === ".DS_Store"
1275
+ );
1276
+ }
1277
+ async function templateManifest(root) {
1278
+ const files = [];
1279
+ async function visit(directory, relativeDirectory = "") {
1280
+ const entries = await readdir7(directory, { withFileTypes: true });
1281
+ entries.sort((left, right) => left.name.localeCompare(right.name));
1282
+ for (const entry of entries) {
1283
+ const relative = path7.join(relativeDirectory, entry.name);
1284
+ if (!isDistributableTemplatePath(relative)) continue;
1285
+ const source = path7.join(directory, entry.name);
1286
+ const stats = await lstat2(source);
1287
+ if (stats.isSymbolicLink()) {
1288
+ throw new CliError(
1289
+ `Symbolic links are forbidden in templates: ${relative}`
1290
+ );
1291
+ }
1292
+ if (stats.isDirectory()) await visit(source, relative);
1293
+ else if (stats.isFile()) files.push(relative);
1294
+ else throw new CliError(`Unsupported template entry: ${relative}`);
1295
+ }
1296
+ }
1297
+ await visit(root);
1298
+ return files;
1299
+ }
1300
+ async function copyTemplate(root, target) {
1301
+ const manifest = await templateManifest(root);
1302
+ await mkdir6(target, { recursive: true });
1303
+ for (const relative of manifest) {
1304
+ const destination = path7.join(
1305
+ target,
1306
+ relative === ".gitignore.template" ? ".gitignore" : relative
1307
+ );
1308
+ await mkdir6(path7.dirname(destination), { recursive: true });
1309
+ await copyFile2(path7.join(root, relative), destination);
1310
+ }
1311
+ return manifest;
1312
+ }
1313
+ async function customizeGeneratedProject(target, projectName, importAlias) {
1314
+ const packagePath = path7.join(target, "package.json");
1315
+ const packageJson = JSON.parse(await readFile10(packagePath, "utf8"));
1316
+ packageJson.name = projectName.toLowerCase();
1317
+ delete packageJson.packageManager;
1318
+ await writeFile9(packagePath, `${JSON.stringify(packageJson, null, 2)}
1319
+ `);
1320
+ const tsconfigPath = path7.join(target, "tsconfig.json");
1321
+ const tsconfigContent = await readFile10(tsconfigPath, "utf8");
1322
+ const tsconfig = JSON.parse(tsconfigContent);
1323
+ if (!tsconfig.compilerOptions?.paths?.["@/*"]) {
1324
+ throw new CliError(
1325
+ 'The template tsconfig must define the default "@/*" alias.'
1326
+ );
1327
+ }
1328
+ await writeFile9(
1329
+ tsconfigPath,
1330
+ tsconfigContent.replace('"@/*"', `"${importAlias}"`)
1331
+ );
1332
+ if (importAlias !== "@/*") {
1333
+ const prefix = importAlias.slice(0, -2);
1334
+ for (const relative of await templateManifest(target)) {
1335
+ if (!/\.[cm]?[jt]sx?$/.test(relative)) continue;
1336
+ const file = path7.join(target, relative);
1337
+ const content = await readFile10(file, "utf8");
1338
+ const next = content.replaceAll('from "@/', `from "${prefix}/`).replaceAll("from '@/", `from '${prefix}/`);
1339
+ if (next !== content) await writeFile9(file, next);
1340
+ }
1341
+ }
1342
+ }
1343
+
583
1344
  // src/scaffold.ts
584
- async function scaffoldProject(options) {
585
- const targetPath = join7(process.cwd(), options.projectName);
586
- const __dirname2 = new URL(".", import.meta.url);
587
- const templatePath = join7(
588
- fileURLToPath(__dirname2),
589
- "..",
590
- "templates",
591
- "Projects",
592
- "default"
1345
+ async function scaffoldProject(options, runtime = {}) {
1346
+ const requiredFeatures = [
1347
+ "useTypescript",
1348
+ "useEslint",
1349
+ "useTailwind",
1350
+ "useSrcDir",
1351
+ "useTurbopack",
1352
+ "useI18n"
1353
+ ];
1354
+ const unsupported = requiredFeatures.filter(
1355
+ (feature) => options[feature] !== true
593
1356
  );
594
- if (existsSync7(targetPath)) {
1357
+ if (unsupported.length > 0) {
1358
+ throw new CliError(
1359
+ `The default Next.js 16 template requires: ${unsupported.join(", ")}.`
1360
+ );
1361
+ }
1362
+ const cwd = runtime.cwd ?? process.cwd();
1363
+ const terminal = runtime.terminal ?? console;
1364
+ const projectName = validateProjectName(options.projectName);
1365
+ const importAlias = normalizeImportAlias(
1366
+ options.customAlias === false ? "@/*" : options.importAlias || "@/*"
1367
+ );
1368
+ const targetPath = join8(cwd, projectName);
1369
+ const __dirname = new URL(".", import.meta.url);
1370
+ const templatePath = runtime.templatePath ?? join8(fileURLToPath2(__dirname), "..", "templates", "Projects", "default");
1371
+ const resolvedCwd = resolve(cwd);
1372
+ const resolvedTarget = resolve(targetPath);
1373
+ if (!resolvedTarget.startsWith(`${resolvedCwd}/`)) {
1374
+ throw new CliError(
1375
+ "The project destination must be a child of the current directory."
1376
+ );
1377
+ }
1378
+ if (existsSync10(targetPath)) {
595
1379
  if (options.force) {
596
- console.warn("\u26A0\uFE0F Target directory already exists, removing...");
597
- await rm(targetPath, { recursive: true, force: true });
1380
+ terminal.warn("\u26A0\uFE0F Target directory already exists, removing...");
1381
+ await rm2(targetPath, { recursive: true, force: true });
598
1382
  } else {
599
- console.error(
1383
+ terminal.error(
600
1384
  red("[X] Target directory already exists. Use --force to overwrite.")
601
1385
  );
602
- process.exit(1);
1386
+ throw new CliError(
1387
+ "[X] Target directory already exists. Use --force to overwrite."
1388
+ );
603
1389
  }
604
1390
  }
605
1391
  try {
606
- console.log("Creating project directory...");
607
- await mkdir5(targetPath, { recursive: true });
608
- console.log("Copying files from template...");
609
- await cp(templatePath, targetPath, { recursive: true });
610
- const pkgPath = join7(targetPath, "package.json");
611
- if (existsSync7(pkgPath)) {
612
- const pkg = JSON.parse(await readFile6(pkgPath, "utf-8"));
613
- pkg.dependencies = pkg.dependencies || {};
614
- if (options.useI18n) {
615
- pkg.dependencies["next-intl"] = pkg.dependencies["next-intl"] || "^4.3.5";
616
- }
617
- await writeFile6(pkgPath, JSON.stringify(pkg, null, 2));
618
- }
619
- await writeFile6(
620
- join7(targetPath, "cnp.config.json"),
621
- JSON.stringify(options, null, 2)
1392
+ terminal.log("Creating project directory...");
1393
+ await mkdir7(targetPath, { recursive: true });
1394
+ terminal.log("Copying files from template...");
1395
+ await copyTemplate(templatePath, targetPath);
1396
+ await customizeGeneratedProject(targetPath, projectName, importAlias);
1397
+ await writeFile10(
1398
+ join8(targetPath, "cnp.config.json"),
1399
+ `${JSON.stringify({ ...options, projectName, importAlias }, null, 2)}
1400
+ `
622
1401
  );
623
- console.log("Project setup complete!");
624
- console.log("");
625
- console.log("To get started:");
626
- console.log(" " + green(`cd ${options.projectName}`));
627
- console.log("");
628
- console.log(
1402
+ terminal.log("Project setup complete!");
1403
+ terminal.log("");
1404
+ terminal.log("To get started:");
1405
+ terminal.log(" " + green(`cd ${options.projectName}`));
1406
+ terminal.log("");
1407
+ terminal.log(
629
1408
  "Then install dependencies and launch the dev server with your preferred tool:"
630
1409
  );
631
- console.log(" " + green(`bun install && bun dev`));
632
- console.log(" " + green(`npm install && npm run dev`));
633
- console.log(" " + green(`pnpm install && pnpm run dev`));
634
- console.log("");
635
- console.log("Documentation and examples can be found at:");
636
- console.log(
1410
+ terminal.log(" " + green(`bun install && bun dev`));
1411
+ terminal.log(" " + green(`npm install && npm run dev`));
1412
+ terminal.log(" " + green(`pnpm install && pnpm run dev`));
1413
+ terminal.log("");
1414
+ terminal.log("Documentation and examples can be found at:");
1415
+ terminal.log(
637
1416
  " " + cyan("https://github.com/Rising-Corporation/create-next-pro-cli")
638
1417
  );
639
- console.log(
1418
+ terminal.log(
640
1419
  "_-`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`-_"
641
1420
  );
642
1421
  } catch (err) {
643
- console.error(red("[X] Error during project creation:"), err);
644
- process.exit(1);
1422
+ terminal.error(red("[X] Error during project creation:"), err);
1423
+ throw new CliError("[X] Error during project creation:");
645
1424
  }
646
1425
  }
647
1426
 
648
1427
  // src/lib/createProject.ts
649
- async function createProject(nameArg, force) {
1428
+ async function createProject(nameArg, force, context) {
650
1429
  const response = {
651
1430
  projectName: nameArg,
652
1431
  useTypescript: true,
@@ -659,14 +1438,17 @@ async function createProject(nameArg, force) {
659
1438
  importAlias: "@/*",
660
1439
  force
661
1440
  };
662
- console.log(`Creating project "${response.projectName}"...`);
663
- await scaffoldProject(response);
1441
+ const terminal = context?.terminal ?? console;
1442
+ terminal.log(`Creating project "${response.projectName}"...`);
1443
+ await scaffoldProject(response, {
1444
+ cwd: context?.cwd ?? process.cwd(),
1445
+ terminal
1446
+ });
664
1447
  }
665
1448
 
666
1449
  // src/lib/createProjectWithPrompt.ts
667
- import prompts6 from "prompts";
668
- async function createProjectWithPrompt() {
669
- const response = await prompts6.prompt([
1450
+ async function createProjectWithPrompt(context) {
1451
+ const response = await context.prompt([
670
1452
  {
671
1453
  type: "text",
672
1454
  name: "projectName",
@@ -736,239 +1518,17 @@ async function createProjectWithPrompt() {
736
1518
  initial: "@core/*"
737
1519
  }
738
1520
  ]);
739
- console.log("\nYour choices:");
740
- console.log(response);
741
- await scaffoldProject(response);
742
- }
743
-
744
- // src/lib/addLanguage.ts
745
- import { join as join8 } from "path";
746
- import { existsSync as existsSync8 } from "fs";
747
- import { cp as cp2, readFile as readFile7, writeFile as writeFile7 } from "fs/promises";
748
- import prompts7 from "prompts";
749
- function generateLocales() {
750
- const dn = new Intl.DisplayNames(["en"], { type: "language" });
751
- const locales = [];
752
- for (let i = 0; i < 26; i++) {
753
- for (let j = 0; j < 26; j++) {
754
- const code = String.fromCharCode(97 + i) + String.fromCharCode(97 + j);
755
- try {
756
- const name = dn.of(code);
757
- if (name && name.toLowerCase() !== code) {
758
- locales.push(code);
759
- }
760
- } catch {
761
- }
762
- }
763
- }
764
- return locales.sort();
765
- }
766
- async function addLanguage(args) {
767
- const config = await loadConfig();
768
- if (!config?.useI18n) {
769
- console.error("\u274C i18n is not enabled in this project.");
770
- return;
771
- }
772
- const messagesPath = join8(process.cwd(), "messages");
773
- if (!existsSync8(messagesPath)) {
774
- console.error("\u274C Messages directory missing. Ensure i18n was configured.");
775
- return;
776
- }
777
- const available = generateLocales();
778
- let locale = args[1];
779
- if (!locale || !available.includes(locale)) {
780
- const response = await prompts7({
781
- type: "autocomplete",
782
- name: "locale",
783
- message: "\u{1F310} Locale to add:",
784
- choices: available.map((l) => ({ title: l, value: l }))
785
- });
786
- locale = response.locale;
787
- }
788
- if (!locale) return;
789
- if (existsSync8(join8(messagesPath, locale))) {
790
- console.error(`\u274C Locale ${locale} already exists.`);
791
- return;
792
- }
793
- const routingFile = join8(process.cwd(), "src", "lib", "i18n", "routing.ts");
794
- if (!existsSync8(routingFile)) {
795
- console.error("\u274C routing.ts not found. Are you in project root?");
796
- return;
797
- }
798
- const routingContent = await readFile7(routingFile, "utf-8");
799
- const defaultMatch = routingContent.match(/defaultLocale:\s*"([^"]+)"/);
800
- const defaultLocale = defaultMatch ? defaultMatch[1] : null;
801
- if (!defaultLocale || !existsSync8(join8(messagesPath, defaultLocale))) {
802
- console.error("\u274C Default locale not found.");
803
- return;
804
- }
805
- await cp2(join8(messagesPath, defaultLocale), join8(messagesPath, locale), {
806
- recursive: true
1521
+ const options = response;
1522
+ context.terminal.log("\nYour choices:");
1523
+ context.terminal.log(options);
1524
+ await scaffoldProject(options, {
1525
+ cwd: context.cwd,
1526
+ terminal: context.terminal
807
1527
  });
808
- console.log(`\u{1F4C4} Directory created: ${join8(messagesPath, locale)}`);
809
- const localesMatch = routingContent.match(/locales:\s*\[([^\]]*)\]/);
810
- if (localesMatch) {
811
- const localesArr = localesMatch[1].split(",").map((s) => s.trim().replace(/["']/g, "")).filter(Boolean);
812
- if (!localesArr.includes(locale)) {
813
- localesArr.push(locale);
814
- const newLocales = `locales: [${localesArr.map((l) => `"${l}"`).join(", ")}]`;
815
- const newContent = routingContent.replace(/locales:\s*\[[^\]]*\]/, newLocales);
816
- await writeFile7(routingFile, newContent);
817
- console.log(`\u{1F4C4} File updated: ${routingFile}`);
818
- }
819
- }
820
- console.log(
821
- `\u2705 Locale "${locale}" added and copied from default locale "${defaultLocale}".`
822
- );
823
- }
824
-
825
- // src/lib/addText.ts
826
- import { join as join9 } from "path";
827
- import { existsSync as existsSync9 } from "fs";
828
- import { readFile as readFile8, writeFile as writeFile8, readdir as readdir4 } from "fs/promises";
829
- function defaultText(key) {
830
- return key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
831
- }
832
- async function addText(args) {
833
- const pathArg = args[1];
834
- if (!pathArg) {
835
- console.error("\u274C Dot path parameter is required.");
836
- return;
837
- }
838
- const providedText = args.slice(2).join(" ");
839
- const config = await loadConfig();
840
- if (!config?.useI18n) {
841
- console.error("\u274C i18n is not enabled in this project.");
842
- return;
843
- }
844
- const messagesPath = join9(process.cwd(), "messages");
845
- if (!existsSync9(messagesPath)) {
846
- console.error("\u274C Messages directory missing. Ensure i18n was configured.");
847
- return;
848
- }
849
- const entries = await readdir4(messagesPath, { withFileTypes: true });
850
- const locales = entries.filter((e) => e.isDirectory()).map((e) => e.name);
851
- const [fileName, ...segments] = pathArg.split(".");
852
- if (!fileName || segments.length === 0) {
853
- console.error("\u274C Invalid dot path provided.");
854
- return;
855
- }
856
- const finalKey = segments[segments.length - 1];
857
- const text = providedText || defaultText(finalKey);
858
- for (const locale of locales) {
859
- const filePath = join9(messagesPath, locale, `${fileName}.json`);
860
- let data = {};
861
- if (existsSync9(filePath)) {
862
- const raw = await readFile8(filePath, "utf-8");
863
- try {
864
- data = JSON.parse(raw);
865
- } catch {
866
- }
867
- }
868
- let cursor = data;
869
- for (let i = 0; i < segments.length - 1; i++) {
870
- const seg = segments[i];
871
- if (!cursor[seg]) cursor[seg] = {};
872
- cursor = cursor[seg];
873
- }
874
- cursor[finalKey] = text;
875
- await writeFile8(filePath, JSON.stringify(data, null, 2));
876
- console.log(`\u{1F4C4} File updated: ${filePath}`);
877
- }
878
- console.log(`\u2705 Text added at path "${pathArg}" with value "${text}".`);
879
1528
  }
880
1529
 
881
1530
  // src/index.ts
882
- var CONFIG_DIR = process.env.XDG_CONFIG_HOME ? path.join(process.env.XDG_CONFIG_HOME, "create-next-pro") : path.join(os.homedir(), ".config", "create-next-pro");
883
- var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
884
- function readCfg() {
885
- try {
886
- return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
887
- } catch {
888
- return null;
889
- }
890
- }
891
- function writeCfg(cfg) {
892
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
893
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
894
- }
895
- function rcFile(shell) {
896
- return path.join(os.homedir(), shell === "zsh" ? ".zshrc" : ".bashrc");
897
- }
898
- function ensureLineInRc(file, line) {
899
- try {
900
- const cur = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
901
- if (!cur.includes(line)) fs.appendFileSync(file, `
902
- ${line}
903
- `);
904
- } catch {
905
- }
906
- }
907
- async function installCompletion(shell) {
908
- const __dirname2 = path.dirname(fileURLToPath2(import.meta.url));
909
- const completionSrc = path.resolve(
910
- __dirname2,
911
- "../create-next-pro-completion.sh"
912
- );
913
- const completionDst = path.join(CONFIG_DIR, "completion.sh");
914
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
915
- fs.copyFileSync(completionSrc, completionDst);
916
- ensureLineInRc(rcFile(shell), `source "${completionDst}"`);
917
- }
918
- var __filename = fileURLToPath2(import.meta.url);
919
- var __dirname = dirname(__filename);
920
- var packageJsonPath = resolve(__dirname, "../package.json");
921
- var packageJson = fs.readFileSync(packageJsonPath, "utf8");
922
- async function onboarding() {
923
- const pkg = JSON.parse(packageJson);
924
- console.log(`\u{1F680} Welcome to create-next-pro v${pkg.version}
925
- `);
926
- const res = await prompts8(
927
- [
928
- {
929
- type: "select",
930
- name: "shell",
931
- message: "Which shell do you use?",
932
- choices: [
933
- { title: "zsh", value: "zsh" },
934
- { title: "bash", value: "bash" }
935
- ],
936
- initial: (os.userInfo().shell || "").includes("zsh") ? 0 : 1
937
- },
938
- {
939
- type: "toggle",
940
- name: "completion",
941
- message: "Install autocompletion?",
942
- initial: true,
943
- active: "Yes",
944
- inactive: "No"
945
- }
946
- ],
947
- { onCancel: () => process.exit(1) }
948
- );
949
- const cfg = {
950
- version: 1,
951
- shell: res.shell,
952
- completionInstalled: !!res.completion,
953
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
954
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
955
- };
956
- if (cfg.completionInstalled) await installCompletion(cfg.shell);
957
- writeCfg(cfg);
958
- console.log("\n\u2705 Configuration saved.");
959
- console.log("you can now use the CLI ! ex : ");
960
- console.log(" Without prompt (will change in future) :");
961
- console.log(" create-next-pro my-next-project");
962
- console.log(" With prompt :");
963
- console.log(" create-next-pro");
964
- console.log(
965
- "For more information, visit: https://github.com/Rising-Corporation/create-next-pro-cli"
966
- );
967
- console.log("Happy coding! \u{1F389}");
968
- return cfg;
969
- }
970
- function showHelp() {
971
- console.log(`create-next-pro
1531
+ var HELP_TEXT = `create-next-pro
972
1532
 
973
1533
  Usage:
974
1534
  create-next-pro <project-name> [--force]
@@ -982,68 +1542,61 @@ Usage:
982
1542
 
983
1543
  Options:
984
1544
  --help Show this help message
1545
+ --version Show the CLI version
985
1546
  --reconfigure Run the configuration assistant again
986
- `);
1547
+ `;
1548
+ function parseOptions(args) {
1549
+ return {
1550
+ force: args.includes("--force"),
1551
+ help: args.includes("--help"),
1552
+ version: args.includes("--version") || args.includes("-v"),
1553
+ reconfigure: args.includes("--reconfigure")
1554
+ };
987
1555
  }
988
- function showVersion() {
989
- const pkg = JSON.parse(packageJson);
990
- console.log(`v${pkg.version}`);
1556
+ async function packageVersion(context) {
1557
+ const packageJson = JSON.parse(
1558
+ await context.fs.readText(path8.join(context.packageRoot, "package.json"))
1559
+ );
1560
+ return packageJson.version;
991
1561
  }
992
- async function main() {
993
- let args;
994
- if (typeof Bun !== "undefined") {
995
- args = Bun.argv.slice(2);
996
- } else if (typeof process !== "undefined" && process.argv) {
997
- args = process.argv.slice(2);
998
- } else {
999
- args = [];
1000
- }
1001
- if (args.includes("--help")) return showHelp();
1002
- if (args.includes("--version") || args.includes("-v")) return showVersion();
1003
- if (args.includes("--reconfigure") || !readCfg()) {
1004
- await onboarding();
1005
- return;
1006
- }
1007
- const force = args.includes("--force");
1008
- if (args[0] === "addpage" && args.length === 1) {
1009
- args.push("-LPl");
1010
- }
1011
- if (args[0] === "addcomponent") {
1012
- addComponent(args);
1013
- return;
1014
- }
1015
- if (args[0] === "addpage") {
1016
- addPage(args);
1017
- return;
1018
- }
1019
- if (args[0] === "addlib") {
1020
- addLib(args);
1021
- return;
1022
- }
1023
- if (args[0] === "addapi") {
1024
- addApi(args);
1025
- return;
1026
- }
1027
- if (args[0] === "addlanguage") {
1028
- await addLanguage(args);
1029
- return;
1030
- }
1031
- if (args[0] === "addtext") {
1032
- await addText(args);
1033
- return;
1034
- }
1035
- if (args[0] === "rmpage") {
1036
- rmPage(args);
1037
- return;
1038
- }
1039
- const nameArg = args.find((arg) => !arg.startsWith("--"));
1040
- if (nameArg) {
1041
- createProject(nameArg, force);
1042
- return;
1562
+ async function main(context = createNodeContext()) {
1563
+ try {
1564
+ const args = [...context.argv];
1565
+ const options = parseOptions(args);
1566
+ const version = await packageVersion(context);
1567
+ if (args[0] === "__complete") {
1568
+ await printCompletions(args, context);
1569
+ return 0;
1570
+ }
1571
+ if (options.help) {
1572
+ context.terminal.log(HELP_TEXT);
1573
+ return 0;
1574
+ }
1575
+ if (options.version) {
1576
+ context.terminal.log(`v${version}`);
1577
+ return 0;
1578
+ }
1579
+ if (options.reconfigure || !await readConfig(context)) {
1580
+ await onboarding(context, version);
1581
+ return 0;
1582
+ }
1583
+ if (args[0] === "addpage" && args.length === 1) args.push("-LPl");
1584
+ const handler = args[0] ? createCommandRegistry().get(args[0]) : void 0;
1585
+ if (handler) return (await handler(args, context)).exitCode;
1586
+ const nameArg = args.find((arg) => !arg.startsWith("--"));
1587
+ if (nameArg) {
1588
+ await createProject(nameArg, options.force, context);
1589
+ return 0;
1590
+ }
1591
+ await createProjectWithPrompt(context);
1592
+ return 0;
1593
+ } catch (error) {
1594
+ const message = error instanceof Error ? error.message : String(error);
1595
+ context.terminal.error(message);
1596
+ return error instanceof CliError ? error.exitCode : 1;
1043
1597
  }
1044
- await createProjectWithPrompt();
1045
1598
  }
1046
1599
 
1047
1600
  // bin.node.ts
1048
- main();
1601
+ process.exitCode = await main();
1049
1602
  //# sourceMappingURL=bin.node.js.map