create-next-pro-cli 0.1.26 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +281 -291
  2. package/create-next-pro-completion.sh +8 -38
  3. package/create-next-pro-completion.zsh +13 -0
  4. package/dist/bin.bun.js +2265 -794
  5. package/dist/bin.node.js +2425 -805
  6. package/dist/bin.node.js.map +1 -1
  7. package/package.json +50 -27
  8. package/templates/Projects/default/.env.example +17 -0
  9. package/templates/Projects/default/.github/workflows/quality.yml +24 -0
  10. package/templates/Projects/default/.gitignore.template +47 -0
  11. package/templates/Projects/default/.prettierignore +3 -0
  12. package/templates/Projects/default/README.md +52 -108
  13. package/templates/Projects/default/bun.lock +1152 -0
  14. package/templates/Projects/default/eslint.config.mjs +27 -11
  15. package/templates/Projects/default/messages/en/_global_ui.json +23 -10
  16. package/templates/Projects/default/messages/en.ts +17 -2
  17. package/templates/Projects/default/messages/fr/_global_ui.json +23 -10
  18. package/templates/Projects/default/messages/fr.ts +17 -2
  19. package/templates/Projects/default/next.config.ts +43 -3
  20. package/templates/Projects/default/package.json +42 -24
  21. package/templates/Projects/default/playwright.config.ts +26 -0
  22. package/templates/Projects/default/pnpm-workspace.yaml +5 -0
  23. package/templates/Projects/default/public/{cnp-logo.svg → logo.svg} +1 -1
  24. package/templates/Projects/default/src/app/[locale]/(public)/layout.tsx +8 -1
  25. package/templates/Projects/default/src/app/[locale]/(public)/login/page.tsx +8 -0
  26. package/templates/Projects/default/src/app/[locale]/(public)/register/page.tsx +8 -0
  27. package/templates/Projects/default/src/app/[locale]/(user)/dashboard/error.tsx +25 -0
  28. package/templates/Projects/default/src/app/[locale]/(user)/dashboard/loading.tsx +5 -0
  29. package/templates/Projects/default/src/app/[locale]/(user)/{Dashboard → dashboard}/page.tsx +1 -1
  30. package/templates/Projects/default/src/app/[locale]/(user)/layout.tsx +24 -2
  31. package/templates/Projects/default/src/app/[locale]/(user)/settings/loading.tsx +5 -0
  32. package/templates/Projects/default/src/app/[locale]/(user)/{Settings → settings}/page.tsx +1 -2
  33. package/templates/Projects/default/src/app/[locale]/(user)/userInfo/loading.tsx +5 -0
  34. package/templates/Projects/default/src/app/[locale]/(user)/{UserInfo → userInfo}/page.tsx +1 -2
  35. package/templates/Projects/default/src/app/[locale]/layout.tsx +34 -10
  36. package/templates/Projects/default/src/app/[locale]/loading.tsx +0 -9
  37. package/templates/Projects/default/src/app/[locale]/not-found.tsx +6 -15
  38. package/templates/Projects/default/src/app/[locale]/page.tsx +10 -1
  39. package/templates/Projects/default/src/app/api/auth/[...nextauth]/route.ts +8 -60
  40. package/templates/Projects/default/src/app/not-found.tsx +1 -1
  41. package/templates/Projects/default/src/app/sitemap.ts +2 -2
  42. package/templates/Projects/default/src/app/styles/globals.css +166 -113
  43. package/templates/Projects/default/src/auth.ts +20 -0
  44. package/templates/Projects/default/src/config.ts +3 -3
  45. package/templates/Projects/default/src/env.ts +65 -0
  46. package/templates/Projects/default/src/lib/i18n/messages.ts +8 -0
  47. package/templates/Projects/default/src/lib/i18n/request.ts +2 -16
  48. package/templates/Projects/default/src/lib/security/csp.ts +16 -0
  49. package/templates/Projects/default/src/lib/utils.ts +2 -1
  50. package/templates/Projects/default/src/proxy.ts +13 -0
  51. package/templates/Projects/default/src/ui/_global/BackButton.tsx +4 -2
  52. package/templates/Projects/default/src/ui/_global/Button.tsx +3 -8
  53. package/templates/Projects/default/src/ui/_global/GlobalHeader.tsx +10 -28
  54. package/templates/Projects/default/src/ui/_global/GlobalMain.tsx +1 -1
  55. package/templates/Projects/default/src/ui/_global/LocaleSwitcher.tsx +9 -10
  56. package/templates/Projects/default/src/ui/_global/PublicNav.tsx +51 -17
  57. package/templates/Projects/default/src/ui/_global/ThemeToggle.tsx +45 -39
  58. package/templates/Projects/default/src/ui/_global/UserNav.tsx +6 -6
  59. package/templates/Projects/default/src/ui/_home/page-ui.tsx +5 -7
  60. package/templates/Projects/default/src/ui/{Dashboard → dashboard}/LogoutButton.tsx +9 -7
  61. package/templates/Projects/default/src/ui/{Dashboard → dashboard}/StatsCard.tsx +4 -4
  62. package/templates/Projects/default/src/ui/{Dashboard → dashboard}/page-ui.tsx +7 -8
  63. package/templates/Projects/default/src/ui/login/page-ui.tsx +36 -0
  64. package/templates/Projects/default/src/ui/register/page-ui.tsx +38 -0
  65. package/templates/Projects/default/src/ui/settings/page-ui.tsx +15 -0
  66. package/templates/Projects/default/src/ui/userInfo/page-ui.tsx +15 -0
  67. package/templates/Projects/default/tailwind.config.ts +81 -1
  68. package/templates/Projects/default/tests/consumer/validate-template.ts +66 -0
  69. package/templates/Projects/default/tests/e2e/template-remediation.playwright.ts +106 -0
  70. package/templates/Projects/default/tests/rendering/verify-rendering.ts +56 -0
  71. package/templates/Projects/default/tests/unit/csp.test.ts +19 -0
  72. package/templates/Projects/default/tests/unit/env.test.ts +76 -0
  73. package/templates/Projects/default/tsconfig.json +6 -6
  74. package/templates/Projects/default/example.env +0 -8
  75. package/templates/Projects/default/messages/getMergedMessages.ts +0 -31
  76. package/templates/Projects/default/middleware.ts +0 -11
  77. package/templates/Projects/default/src/app/[locale]/(public)/Login/page.tsx +0 -6
  78. package/templates/Projects/default/src/app/[locale]/(public)/Register/page.tsx +0 -6
  79. package/templates/Projects/default/src/app/[locale]/(user)/Dashboard/error.tsx +0 -38
  80. package/templates/Projects/default/src/app/[locale]/(user)/Dashboard/loading.tsx +0 -10
  81. package/templates/Projects/default/src/app/[locale]/(user)/Settings/loading.tsx +0 -17
  82. package/templates/Projects/default/src/app/[locale]/(user)/UserInfo/loading.tsx +0 -17
  83. package/templates/Projects/default/src/app/api/auth/post-login/route.ts +0 -26
  84. package/templates/Projects/default/src/app/api/hello/route.ts +0 -5
  85. package/templates/Projects/default/src/app/layout.tsx +0 -11
  86. package/templates/Projects/default/src/app/page.tsx +0 -6
  87. package/templates/Projects/default/src/auth.config.ts +0 -0
  88. package/templates/Projects/default/src/lib/auth/disconnect.ts +0 -11
  89. package/templates/Projects/default/src/lib/auth/isConnected.ts +0 -18
  90. package/templates/Projects/default/src/lib/sample/example.ts +0 -3
  91. package/templates/Projects/default/src/lib/sample/index.ts +0 -3
  92. package/templates/Projects/default/src/ui/Login/page-ui.tsx +0 -22
  93. package/templates/Projects/default/src/ui/Register/page-ui.tsx +0 -26
  94. package/templates/Projects/default/src/ui/Settings/page-ui.tsx +0 -17
  95. package/templates/Projects/default/src/ui/UserInfo/page-ui.tsx +0 -17
  96. /package/templates/Projects/default/messages/en/{Dashboard.json → dashboard.json} +0 -0
  97. /package/templates/Projects/default/messages/en/{Login.json → login.json} +0 -0
  98. /package/templates/Projects/default/messages/en/{Register.json → register.json} +0 -0
  99. /package/templates/Projects/default/messages/en/{Settings.json → settings.json} +0 -0
  100. /package/templates/Projects/default/messages/en/{UserInfo.json → userInfo.json} +0 -0
  101. /package/templates/Projects/default/messages/fr/{Dashboard.json → dashboard.json} +0 -0
  102. /package/templates/Projects/default/messages/fr/{Login.json → login.json} +0 -0
  103. /package/templates/Projects/default/messages/fr/{Register.json → register.json} +0 -0
  104. /package/templates/Projects/default/messages/fr/{Settings.json → settings.json} +0 -0
  105. /package/templates/Projects/default/messages/fr/{UserInfo.json → userInfo.json} +0 -0
  106. /package/templates/Projects/default/public/{cnp-logo.png → logo.png} +0 -0
  107. /package/templates/Projects/default/src/ui/{Dashboard → dashboard}/WelcomeCard.tsx +0 -0
package/dist/bin.node.js CHANGED
@@ -1,30 +1,562 @@
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 path10 from "path";
5
+
6
+ // src/cli/completion.ts
7
+ import path2 from "path";
8
+
9
+ // src/core/page-catalog.ts
6
10
  import path from "path";
7
- import prompts8 from "prompts";
8
- import { fileURLToPath as fileURLToPath2 } from "url";
9
- import { dirname, resolve } from "path";
11
+ function isRouteGroup(segment) {
12
+ return segment.startsWith("(") && segment.endsWith(")");
13
+ }
14
+ async function discoverPages(projectRoot, fs) {
15
+ const appRoot = path.join(projectRoot, "src", "app", "[locale]");
16
+ const candidates = [];
17
+ async function visit(directory, relative = []) {
18
+ let entries;
19
+ try {
20
+ entries = await fs.list(directory);
21
+ } catch {
22
+ return;
23
+ }
24
+ if (entries.some((entry) => entry.isFile && entry.name === "page.tsx")) {
25
+ const routeSegments = relative.filter(
26
+ (segment) => !isRouteGroup(segment)
27
+ );
28
+ if (routeSegments.length > 0 && !routeSegments.some(
29
+ (segment) => segment.startsWith("_") || segment.startsWith("[")
30
+ )) {
31
+ const logicalName = routeSegments.join(".");
32
+ candidates.push({
33
+ logicalName,
34
+ routeSegments,
35
+ routeDirectory: directory,
36
+ uiDirectory: path.join(projectRoot, "src", "ui", ...routeSegments),
37
+ messageFile: path.join(
38
+ projectRoot,
39
+ "messages",
40
+ "{locale}",
41
+ `${routeSegments[0]}.json`
42
+ ),
43
+ messageKey: routeSegments.length > 1 ? routeSegments.at(-1) : void 0
44
+ });
45
+ }
46
+ }
47
+ for (const entry of entries) {
48
+ if (entry.isDirectory && !entry.name.startsWith(".")) {
49
+ await visit(path.join(directory, entry.name), [
50
+ ...relative,
51
+ entry.name
52
+ ]);
53
+ }
54
+ }
55
+ }
56
+ await visit(appRoot);
57
+ return candidates.sort(
58
+ (left, right) => left.logicalName.localeCompare(right.logicalName)
59
+ );
60
+ }
10
61
 
11
- // src/lib/addComponent.ts
62
+ // src/cli/completion.ts
63
+ var PUBLIC_COMMANDS = [
64
+ "addpage",
65
+ "addcomponent",
66
+ "addlib",
67
+ "addapi",
68
+ "addlanguage",
69
+ "addtext",
70
+ "rmpage",
71
+ "--help",
72
+ "--version",
73
+ "--json",
74
+ "--reconfigure"
75
+ ];
76
+ var OPTIONS = {
77
+ addpage: [
78
+ "--layout",
79
+ "--page",
80
+ "--loading",
81
+ "--not-found",
82
+ "--error",
83
+ "--global-error",
84
+ "--route",
85
+ "--template",
86
+ "--default"
87
+ ],
88
+ addcomponent: ["--page", "-P"]
89
+ };
90
+ async function directories(root, context) {
91
+ try {
92
+ return (await context.fs.list(root)).filter((entry) => entry.isDirectory && !entry.name.startsWith("_")).map((entry) => entry.name).sort();
93
+ } catch {
94
+ return [];
95
+ }
96
+ }
97
+ async function completionCandidates(command, context) {
98
+ if (!command) return [...PUBLIC_COMMANDS];
99
+ if (command === "rmpage") {
100
+ return (await discoverPages(context.cwd, context.fs)).map(
101
+ (candidate) => candidate.logicalName
102
+ );
103
+ }
104
+ if (command === "addcomponent" || command === "addpage") {
105
+ return [
106
+ ...OPTIONS[command] ?? [],
107
+ ...await directories(path2.join(context.cwd, "src", "ui"), context)
108
+ ];
109
+ }
110
+ if (command === "addlanguage")
111
+ return ["de", "en", "es", "fr", "it", "ja", "pt"];
112
+ return OPTIONS[command] ?? [];
113
+ }
114
+
115
+ // src/cli/onboarding.ts
116
+ import path4 from "path";
117
+
118
+ // src/core/operations.ts
119
+ import path3 from "path";
120
+
121
+ // src/core/contracts.ts
122
+ var CliError = class extends Error {
123
+ exitCode;
124
+ code;
125
+ hint;
126
+ scope;
127
+ path;
128
+ constructor(message, options = {}) {
129
+ super(message);
130
+ this.name = "CliError";
131
+ if (typeof options === "number") {
132
+ this.exitCode = options;
133
+ this.code = "FILESYSTEM_ERROR";
134
+ } else {
135
+ this.exitCode = options.exitCode ?? 1;
136
+ this.code = options.code ?? "FILESYSTEM_ERROR";
137
+ this.hint = options.hint;
138
+ this.scope = options.scope;
139
+ this.path = options.path;
140
+ }
141
+ }
142
+ };
143
+
144
+ // src/core/operations.ts
145
+ var OperationJournal = class {
146
+ #events = [];
147
+ record(event) {
148
+ const detail = event.detail ? Object.fromEntries(
149
+ Object.entries(event.detail).map(([key, value]) => [
150
+ key,
151
+ /(content|credential|env|password|secret|token|value)/i.test(key) ? "[REDACTED]" : value
152
+ ])
153
+ ) : void 0;
154
+ const recorded = {
155
+ ...event,
156
+ detail,
157
+ sequence: this.#events.length + 1
158
+ };
159
+ this.#events.push(recorded);
160
+ return recorded;
161
+ }
162
+ snapshot() {
163
+ return this.#events.map((event) => ({ ...event }));
164
+ }
165
+ reset() {
166
+ this.#events.length = 0;
167
+ }
168
+ };
169
+ var MUTATIONS = /* @__PURE__ */ new Set(["created", "copied", "updated", "deleted"]);
170
+ function statusFromEvents(events) {
171
+ if (events.some((event) => event.action === "cancelled")) return "cancelled";
172
+ return events.some((event) => MUTATIONS.has(event.action)) ? "success" : "unchanged";
173
+ }
174
+ function commandResult(context, input) {
175
+ const events = context.operations.snapshot();
176
+ const status = input.status ?? statusFromEvents(events);
177
+ return {
178
+ exitCode: status === "failed" ? 1 : 0,
179
+ status,
180
+ command: input.command,
181
+ summary: input.summary,
182
+ projectRoot: input.projectRoot,
183
+ configRoot: input.configRoot,
184
+ homeRoot: input.homeRoot,
185
+ events,
186
+ nextSteps: input.nextSteps ?? [],
187
+ error: null,
188
+ data: input.data
189
+ };
190
+ }
191
+ function failedResult(context, command, error, roots = {}) {
192
+ const cliError = error instanceof CliError ? error : void 0;
193
+ const normalized = {
194
+ code: cliError?.code ?? "FILESYSTEM_ERROR",
195
+ message: error instanceof Error ? error.message : String(error),
196
+ hint: cliError?.hint,
197
+ scope: cliError?.scope,
198
+ path: cliError?.path
199
+ };
200
+ const nextSteps = normalized.code === "ONBOARDING_REQUIRED" ? [
201
+ {
202
+ kind: "rerun",
203
+ required: true,
204
+ message: "Run create-next-pro once in human mode, then rerun the JSON command.",
205
+ paths: [{ scope: "config", path: "config.json" }],
206
+ commands: ["create-next-pro"]
207
+ }
208
+ ] : [];
209
+ context.operations.record({
210
+ action: "failed",
211
+ resource: "command",
212
+ role: command,
213
+ scope: normalized.scope ?? "project",
214
+ path: normalized.path ?? ".",
215
+ detail: { code: normalized.code }
216
+ });
217
+ return {
218
+ exitCode: cliError?.exitCode ?? 1,
219
+ status: "failed",
220
+ command,
221
+ summary: normalized.message,
222
+ ...roots,
223
+ events: context.operations.snapshot(),
224
+ nextSteps,
225
+ error: normalized
226
+ };
227
+ }
228
+ function relativeResource(root, target) {
229
+ const relative = path3.relative(path3.resolve(root), path3.resolve(target));
230
+ return relative || ".";
231
+ }
232
+ var MutationGateway = class {
233
+ constructor(context, root, defaultScope = "project") {
234
+ this.context = context;
235
+ this.root = root;
236
+ this.defaultScope = defaultScope;
237
+ }
238
+ context;
239
+ root;
240
+ defaultScope;
241
+ path(target) {
242
+ return relativeResource(this.root, target);
243
+ }
244
+ async mkdir(target, metadata, record = true) {
245
+ if (this.context.fs.exists(target)) {
246
+ if (record) this.record("unchanged", target, metadata);
247
+ return "unchanged";
248
+ }
249
+ await this.context.fs.mkdir(target);
250
+ if (record) this.record("created", target, metadata);
251
+ return "created";
252
+ }
253
+ async write(target, content, metadata) {
254
+ const exists = this.context.fs.exists(target);
255
+ if (exists && metadata.preserveExisting) {
256
+ this.record("unchanged", target, metadata);
257
+ return "unchanged";
258
+ }
259
+ if (exists && await this.context.fs.readText(target) === content) {
260
+ this.record("unchanged", target, metadata);
261
+ return "unchanged";
262
+ }
263
+ await this.context.fs.mkdir(path3.dirname(target));
264
+ await this.context.fs.writeText(target, content);
265
+ const action = exists ? "updated" : "created";
266
+ this.record(action, target, metadata);
267
+ return action;
268
+ }
269
+ async copy(source, target, metadata) {
270
+ if (this.context.fs.exists(target) && metadata.preserveExisting) {
271
+ this.record("unchanged", target, metadata);
272
+ return "unchanged";
273
+ }
274
+ await this.context.fs.mkdir(path3.dirname(target));
275
+ await this.context.fs.copyFile(source, target);
276
+ this.record("copied", target, {
277
+ ...metadata,
278
+ source: metadata.source ?? { path: source }
279
+ });
280
+ return "copied";
281
+ }
282
+ async remove(target, metadata, options = {}) {
283
+ if (!this.context.fs.exists(target)) {
284
+ this.record("unchanged", target, metadata);
285
+ return "unchanged";
286
+ }
287
+ await this.context.fs.remove(target, options);
288
+ this.record("deleted", target, metadata);
289
+ return "deleted";
290
+ }
291
+ unchanged(target, metadata) {
292
+ this.record("unchanged", target, metadata);
293
+ }
294
+ skipped(target, metadata) {
295
+ this.record("skipped", target, metadata);
296
+ }
297
+ record(action, target, metadata) {
298
+ this.context.operations.record({
299
+ action,
300
+ resource: metadata.resource ?? "file",
301
+ role: metadata.role,
302
+ scope: metadata.scope ?? this.defaultScope,
303
+ path: this.path(target),
304
+ source: metadata.source,
305
+ detail: metadata.detail
306
+ });
307
+ }
308
+ };
309
+
310
+ // src/cli/onboarding.ts
311
+ function configDirectory(context) {
312
+ return context.env.XDG_CONFIG_HOME ? path4.join(context.env.XDG_CONFIG_HOME, "create-next-pro") : path4.join(context.homeDir, ".config", "create-next-pro");
313
+ }
314
+ function configFile(context) {
315
+ return path4.join(configDirectory(context), "config.json");
316
+ }
317
+ async function readConfig(context) {
318
+ try {
319
+ const config = JSON.parse(
320
+ await context.fs.readText(configFile(context))
321
+ );
322
+ if (config.version !== 1 || config.shell !== "bash" && config.shell !== "zsh" || typeof config.completionInstalled !== "boolean" || typeof config.createdAt !== "string" || typeof config.updatedAt !== "string") {
323
+ return null;
324
+ }
325
+ return config;
326
+ } catch {
327
+ return null;
328
+ }
329
+ }
330
+ async function ensureLineInRc(context, target, line) {
331
+ const current = context.fs.exists(target) ? await context.fs.readText(target) : "";
332
+ const gateway = new MutationGateway(context, context.homeDir, "home");
333
+ return gateway.write(
334
+ target,
335
+ current.includes(line) ? current : `${current}
336
+ ${line}
337
+ `,
338
+ { role: "shell-profile", resource: "shell-profile" }
339
+ );
340
+ }
341
+ async function installCompletion(context, shell) {
342
+ const directory = configDirectory(context);
343
+ const source = path4.join(
344
+ context.packageRoot,
345
+ shell === "zsh" ? "create-next-pro-completion.zsh" : "create-next-pro-completion.sh"
346
+ );
347
+ const target = path4.join(
348
+ directory,
349
+ `completion.${shell === "zsh" ? "zsh" : "sh"}`
350
+ );
351
+ const gateway = new MutationGateway(context, directory, "config");
352
+ await gateway.write(target, await context.fs.readText(source), {
353
+ role: "completion-script",
354
+ source: { scope: "package", path: path4.basename(source) }
355
+ });
356
+ const rcFile = path4.join(
357
+ context.homeDir,
358
+ shell === "zsh" ? ".zshrc" : ".bashrc"
359
+ );
360
+ await ensureLineInRc(context, rcFile, `source "${target}"`);
361
+ }
362
+ async function onboarding(context, version) {
363
+ const response = await context.prompt(
364
+ [
365
+ {
366
+ type: "select",
367
+ name: "shell",
368
+ message: "Which shell do you use?",
369
+ choices: [
370
+ { title: "zsh", value: "zsh" },
371
+ { title: "bash", value: "bash" }
372
+ ],
373
+ initial: context.env.SHELL?.includes("zsh") ? 0 : 1
374
+ },
375
+ {
376
+ type: "toggle",
377
+ name: "completion",
378
+ message: "Install autocompletion?",
379
+ initial: true,
380
+ active: "Yes",
381
+ inactive: "No"
382
+ }
383
+ ],
384
+ { onCancel: () => false }
385
+ );
386
+ if (response.shell !== "bash" && response.shell !== "zsh") {
387
+ context.operations.record({
388
+ action: "cancelled",
389
+ resource: "command",
390
+ role: "onboarding",
391
+ scope: "config",
392
+ path: "."
393
+ });
394
+ return commandResult(context, {
395
+ command: "onboarding",
396
+ summary: "Configuration was cancelled.",
397
+ configRoot: configDirectory(context),
398
+ homeRoot: context.homeDir,
399
+ status: "cancelled"
400
+ });
401
+ }
402
+ const previous = await readConfig(context);
403
+ const now = (/* @__PURE__ */ new Date()).toISOString();
404
+ const config = {
405
+ version: 1,
406
+ shell: response.shell,
407
+ completionInstalled: Boolean(response.completion),
408
+ createdAt: previous?.createdAt ?? now,
409
+ updatedAt: now
410
+ };
411
+ if (config.completionInstalled) {
412
+ try {
413
+ await installCompletion(context, config.shell);
414
+ } catch (error) {
415
+ context.operations.record({
416
+ action: "skipped",
417
+ resource: "file",
418
+ role: "completion-installation",
419
+ scope: "config",
420
+ path: `completion.${config.shell === "zsh" ? "zsh" : "sh"}`,
421
+ detail: {
422
+ reason: error instanceof Error ? error.message : String(error)
423
+ }
424
+ });
425
+ config.completionInstalled = false;
426
+ }
427
+ }
428
+ const semanticallyUnchanged = previous?.shell === config.shell && previous.completionInstalled === config.completionInstalled;
429
+ if (semanticallyUnchanged && previous) config.updatedAt = previous.updatedAt;
430
+ const gateway = new MutationGateway(
431
+ context,
432
+ configDirectory(context),
433
+ "config"
434
+ );
435
+ await gateway.write(
436
+ configFile(context),
437
+ `${JSON.stringify(config, null, 2)}
438
+ `,
439
+ { role: "user-configuration", resource: "configuration" }
440
+ );
441
+ return commandResult(context, {
442
+ command: "onboarding",
443
+ summary: `Configuration for create-next-pro v${version} is ready.`,
444
+ configRoot: configDirectory(context),
445
+ homeRoot: context.homeDir,
446
+ nextSteps: [
447
+ {
448
+ kind: "rerun",
449
+ required: true,
450
+ message: "Run the original create-next-pro command again.",
451
+ paths: []
452
+ }
453
+ ]
454
+ });
455
+ }
456
+
457
+ // src/cli/output.ts
458
+ import path5 from "path";
459
+ function rootFor(result, context, scope) {
460
+ if (scope === "project") return result.projectRoot;
461
+ if (scope === "config") return result.configRoot;
462
+ if (scope === "home") return result.homeRoot;
463
+ return context.packageRoot;
464
+ }
465
+ function displayPath(result, context, scope, target) {
466
+ const root = rootFor(result, context, scope);
467
+ return root ? path5.resolve(root, target) : target;
468
+ }
469
+ function renderEvent(result, context, event) {
470
+ const source = event.source?.template ? ` from template ${event.source.template}` : event.source?.path ? ` from ${displayPath(
471
+ result,
472
+ context,
473
+ event.source.scope ?? event.scope,
474
+ event.source.path
475
+ )}` : "";
476
+ const detail = event.detail ? ` (${Object.entries(event.detail).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${JSON.stringify(value)}`).join(", ")})` : "";
477
+ return `${event.action.toUpperCase()} ${event.role}: ${displayPath(
478
+ result,
479
+ context,
480
+ event.scope,
481
+ event.path
482
+ )}${source}${detail}`;
483
+ }
484
+ function renderResult(result, context, mode) {
485
+ if (mode === "json") {
486
+ context.terminal.log(JSON.stringify({ schemaVersion: 1, ...result }));
487
+ return;
488
+ }
489
+ if (result.command === "help" && typeof result.data?.help === "string") {
490
+ context.terminal.log(result.data.help);
491
+ return;
492
+ }
493
+ if (result.command === "version" && typeof result.data?.version === "string") {
494
+ context.terminal.log(`v${result.data.version}`);
495
+ return;
496
+ }
497
+ const copiedTemplateFiles = result.events.filter(
498
+ (event) => event.action === "copied" && event.role === "template-file"
499
+ );
500
+ if (copiedTemplateFiles.length > 0) {
501
+ context.terminal.log(
502
+ `COPIED ${copiedTemplateFiles.length} template files to ${result.projectRoot}.`
503
+ );
504
+ }
505
+ for (const event of result.events) {
506
+ if (event.action === "copied" && event.role === "template-file") continue;
507
+ const line = renderEvent(result, context, event);
508
+ if (event.action === "failed") context.terminal.error(line);
509
+ else if (event.action === "skipped") context.terminal.warn(line);
510
+ else context.terminal.log(line);
511
+ }
512
+ if (result.error) {
513
+ context.terminal.error(
514
+ `ERROR [${result.error.code}]: ${result.error.message}`
515
+ );
516
+ if (result.error.hint) context.terminal.error(`HINT: ${result.error.hint}`);
517
+ } else if (result.status === "success") {
518
+ context.terminal.log(`SUCCESS: ${result.summary}`);
519
+ } else if (result.status === "unchanged") {
520
+ context.terminal.log(`NO CHANGES: ${result.summary}`);
521
+ } else if (result.status === "cancelled") {
522
+ context.terminal.log(`CANCELLED: ${result.summary}`);
523
+ }
524
+ for (const step of result.nextSteps) {
525
+ context.terminal.log(`NEXT [${step.kind}]: ${step.message}`);
526
+ for (const target of step.paths) {
527
+ context.terminal.log(
528
+ ` ${displayPath(result, context, target.scope, target.path)}`
529
+ );
530
+ }
531
+ for (const command of step.commands ?? []) {
532
+ context.terminal.log(` ${command}`);
533
+ }
534
+ }
535
+ }
536
+
537
+ // src/lib/addApi.ts
12
538
  import { join as join2 } from "path";
13
- import { mkdir, readFile as readFile2, writeFile, readdir } from "fs/promises";
14
- import prompts from "prompts";
15
539
 
16
540
  // src/lib/utils.ts
17
- import { readFile } from "fs/promises";
18
- import { existsSync } from "fs";
19
541
  import { join } from "path";
20
542
  function capitalize(str) {
21
543
  return str.charAt(0).toUpperCase() + str.slice(1);
22
544
  }
23
- async function loadConfig() {
24
- const configPath = join(process.cwd(), "cnp.config.json");
25
- if (!existsSync(configPath)) return null;
545
+ function toIdentifier(value) {
546
+ return value.replace(
547
+ /[-_]+([A-Za-z0-9])/g,
548
+ (_, character) => character.toUpperCase()
549
+ );
550
+ }
551
+ function configuredAliasPrefix(config) {
552
+ const alias = config.importAlias ?? "@/*";
553
+ return alias.endsWith("/*") ? alias.slice(0, -2) : "@";
554
+ }
555
+ async function loadConfig(context) {
556
+ const configPath = join(context.cwd, "cnp.config.json");
557
+ if (!context.fs.exists(configPath)) return null;
26
558
  try {
27
- const raw = await readFile(configPath, "utf-8");
559
+ const raw = await context.fs.readText(configPath);
28
560
  return JSON.parse(raw);
29
561
  } catch {
30
562
  return null;
@@ -55,598 +587,1750 @@ function toFileName(key) {
55
587
  }
56
588
  }
57
589
 
590
+ // src/core/project-paths.ts
591
+ import path6 from "path";
592
+ var SAFE_SEGMENT = /^[A-Za-z][A-Za-z0-9_-]*$/;
593
+ function hasControlCharacters(value) {
594
+ return [...value].some((character) => {
595
+ const code = character.charCodeAt(0);
596
+ return code <= 31 || code === 127;
597
+ });
598
+ }
599
+ function parseLogicalName(value, label = "name") {
600
+ if (!value || hasControlCharacters(value)) {
601
+ throw new CliError(
602
+ `Invalid ${label}: a non-empty printable value is required.`,
603
+ { code: "INVALID_ARGUMENT" }
604
+ );
605
+ }
606
+ if (path6.isAbsolute(value) || value.includes("/") || value.includes("\\")) {
607
+ throw new CliError(
608
+ `Invalid ${label}: paths and separators are not allowed.`,
609
+ { code: "INVALID_ARGUMENT" }
610
+ );
611
+ }
612
+ const segments = value.split(".");
613
+ if (segments.some((segment) => !SAFE_SEGMENT.test(segment))) {
614
+ throw new CliError(
615
+ `Invalid ${label}: use dot-separated alphanumeric segments beginning with a letter.`,
616
+ { code: "INVALID_ARGUMENT" }
617
+ );
618
+ }
619
+ return segments;
620
+ }
621
+ function validateProjectName(value) {
622
+ if (!value || hasControlCharacters(value) || path6.isAbsolute(value) || value === "." || value === ".." || value.includes("/") || value.includes("\\") || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value)) {
623
+ throw new CliError(
624
+ "Invalid project name: use letters, numbers, dots, dashes or underscores without path separators.",
625
+ { code: "INVALID_ARGUMENT" }
626
+ );
627
+ }
628
+ return value;
629
+ }
630
+ function resolveInside(root, ...segments) {
631
+ const absoluteRoot = path6.resolve(root);
632
+ const target = path6.resolve(absoluteRoot, ...segments);
633
+ if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${path6.sep}`)) {
634
+ throw new CliError(
635
+ `Refusing to access a path outside the project: ${target}`,
636
+ { code: "UNSAFE_PATH" }
637
+ );
638
+ }
639
+ return target;
640
+ }
641
+ async function assertSafeTarget(root, target, fs) {
642
+ const safeTarget = resolveInside(root, path6.relative(root, target));
643
+ const relativeSegments = path6.relative(path6.resolve(root), safeTarget).split(path6.sep);
644
+ let current = path6.resolve(root);
645
+ for (const segment of relativeSegments) {
646
+ if (!segment) continue;
647
+ current = path6.join(current, segment);
648
+ const entry = await fs.inspect(current);
649
+ if (entry?.isSymbolicLink) {
650
+ throw new CliError(
651
+ `Symbolic links are forbidden in project paths: ${current}`,
652
+ { code: "UNSAFE_PATH" }
653
+ );
654
+ }
655
+ }
656
+ return safeTarget;
657
+ }
658
+ function normalizeImportAlias(value) {
659
+ if (!/^[A-Za-z@~][A-Za-z0-9@~_-]*\/\*$/.test(value)) {
660
+ throw new CliError(
661
+ 'Invalid import alias: expected a prefix followed by "/*" (for example "@/*" or "@core/*").',
662
+ { code: "INVALID_ARGUMENT" }
663
+ );
664
+ }
665
+ return value;
666
+ }
667
+
668
+ // src/lib/addApi.ts
669
+ var addApi = async (args, context) => {
670
+ let apiName = args[1];
671
+ if (!apiName || apiName.startsWith("-")) {
672
+ if (context.outputMode === "json") {
673
+ throw new CliError("API route name is required in JSON mode.", {
674
+ code: "INTERACTIVE_INPUT_REQUIRED",
675
+ hint: "Pass the API route name after addapi."
676
+ });
677
+ }
678
+ const response = await context.prompt({
679
+ type: "text",
680
+ name: "apiName",
681
+ message: "API route name to add:",
682
+ validate: (name) => name ? true : "API route name is required"
683
+ });
684
+ apiName = String(response.apiName ?? "");
685
+ if (!apiName) {
686
+ context.operations.record({
687
+ action: "cancelled",
688
+ resource: "command",
689
+ role: "api-creation",
690
+ scope: "project",
691
+ path: "."
692
+ });
693
+ return commandResult(context, {
694
+ command: "addapi",
695
+ summary: "API route creation was cancelled.",
696
+ projectRoot: context.cwd,
697
+ status: "cancelled"
698
+ });
699
+ }
700
+ }
701
+ const apiSegments = parseLogicalName(apiName, "API route name");
702
+ const config = await loadConfig(context);
703
+ if (!config) {
704
+ throw new CliError("Configuration file cnp.config.json was not found.", {
705
+ code: "CONFIG_NOT_FOUND",
706
+ hint: "Run this command from the generated project root."
707
+ });
708
+ }
709
+ const apiDir = join2(context.cwd, "src", "app", "api", ...apiSegments);
710
+ await assertSafeTarget(context.cwd, apiDir, context.fs);
711
+ const gateway = new MutationGateway(context, context.cwd);
712
+ const templateDir = join2(context.packageRoot, "templates", "Api");
713
+ const routeTemplate = join2(templateDir, "route.ts");
714
+ const routePath = join2(apiDir, "route.ts");
715
+ if (!context.fs.exists(routeTemplate)) {
716
+ throw new CliError("API template route.ts was not found.", {
717
+ code: "TEMPLATE_MISSING",
718
+ scope: "package",
719
+ path: "templates/Api/route.ts"
720
+ });
721
+ }
722
+ await gateway.mkdir(apiDir, {
723
+ role: "api-directory",
724
+ resource: "directory"
725
+ });
726
+ const content = (await context.fs.readText(routeTemplate)).replace(
727
+ /template/g,
728
+ apiName
729
+ );
730
+ const action = await gateway.write(routePath, content, {
731
+ role: "api-route",
732
+ preserveExisting: true
733
+ });
734
+ return commandResult(context, {
735
+ command: "addapi",
736
+ summary: action === "unchanged" ? `API route "${apiName}" already exists and was preserved.` : `Added API route "${apiName}".`,
737
+ projectRoot: context.cwd,
738
+ nextSteps: action === "unchanged" ? [] : [
739
+ {
740
+ kind: "review",
741
+ required: true,
742
+ message: "Replace the example response and review validation and authentication.",
743
+ paths: [{ scope: "project", path: gateway.path(routePath) }]
744
+ },
745
+ {
746
+ kind: "run-checks",
747
+ required: true,
748
+ message: "Run the project checks.",
749
+ paths: [],
750
+ commands: ["bun run check", "npm run check", "pnpm run check"]
751
+ }
752
+ ]
753
+ });
754
+ };
755
+
58
756
  // src/lib/addComponent.ts
59
- import { existsSync as existsSync2, statSync } from "fs";
60
- async function addComponent(args) {
757
+ import { join as join3 } from "path";
758
+ var addComponent = async (args, context) => {
61
759
  let componentName = args[1];
62
- let pageScope = null;
63
- let pageIndex = args.findIndex((arg) => arg === "-P" || arg === "--page");
64
- if (pageIndex !== -1 && args[pageIndex + 1]) {
65
- pageScope = args[pageIndex + 1];
66
- }
67
- let nestedPath = null;
68
- if (pageScope && pageScope.includes(".")) {
69
- nestedPath = join2(...pageScope.split("."));
760
+ const pageIndex = args.findIndex(
761
+ (argument) => argument === "-P" || argument === "--page"
762
+ );
763
+ const pageScope = pageIndex >= 0 ? args[pageIndex + 1] : void 0;
764
+ if (pageIndex >= 0 && !pageScope) {
765
+ throw new CliError("The --page option requires a page name.", {
766
+ code: "INVALID_ARGUMENT"
767
+ });
70
768
  }
71
769
  if (!componentName || componentName.startsWith("-")) {
72
- const response = await prompts.prompt({
770
+ if (context.outputMode === "json") {
771
+ throw new CliError("Component name is required in JSON mode.", {
772
+ code: "INTERACTIVE_INPUT_REQUIRED",
773
+ hint: "Pass the component name after addcomponent."
774
+ });
775
+ }
776
+ const response = await context.prompt({
73
777
  type: "text",
74
778
  name: "componentName",
75
- message: "\u{1F9E9} Component name to add:",
779
+ message: "Component name to add:",
76
780
  validate: (name) => name ? true : "Component name is required"
77
781
  });
78
- componentName = response.componentName;
782
+ componentName = String(response.componentName ?? "");
783
+ if (!componentName) {
784
+ context.operations.record({
785
+ action: "cancelled",
786
+ resource: "command",
787
+ role: "component-creation",
788
+ scope: "project",
789
+ path: "."
790
+ });
791
+ return commandResult(context, {
792
+ command: "addcomponent",
793
+ summary: "Component creation was cancelled.",
794
+ projectRoot: context.cwd,
795
+ status: "cancelled"
796
+ });
797
+ }
79
798
  }
80
- const config = await loadConfig();
799
+ const componentSegments = parseLogicalName(componentName, "component name");
800
+ if (componentSegments.length !== 1) {
801
+ throw new CliError("Component names must contain exactly one segment.", {
802
+ code: "INVALID_ARGUMENT"
803
+ });
804
+ }
805
+ const pageSegments = pageScope ? parseLogicalName(pageScope, "page name") : [];
806
+ const config = await loadConfig(context);
81
807
  if (!config) {
82
- console.error(
83
- "\u274C Configuration file cnp.config.json not found. Run this command from the project root."
84
- );
85
- return;
808
+ throw new CliError("Configuration file cnp.config.json was not found.", {
809
+ code: "CONFIG_NOT_FOUND",
810
+ hint: "Run this command from the generated project root."
811
+ });
86
812
  }
87
- const useI18n = !!config.useI18n;
88
- const componentNameUpper = capitalize(componentName);
89
- const templatePath = join2(
90
- new URL("..", import.meta.url).pathname,
91
- "templates",
92
- "Component"
93
- );
94
- let messagesPath = null;
95
- if (useI18n) {
96
- messagesPath = join2(process.cwd(), "messages");
97
- if (!existsSync2(messagesPath)) {
98
- console.error(
99
- "\u274C Messages directory missing. Ensure i18n was configured."
100
- );
101
- return;
813
+ const componentNameUpper = capitalize(toIdentifier(componentName));
814
+ const templateRoot = join3(context.packageRoot, "templates", "Component");
815
+ const componentTemplate = join3(templateRoot, "Component.tsx");
816
+ const messagesTemplate = join3(templateRoot, "component.json");
817
+ if (!context.fs.exists(componentTemplate) || config.useI18n && !context.fs.exists(messagesTemplate)) {
818
+ throw new CliError("Required component template files were not found.", {
819
+ code: "TEMPLATE_MISSING",
820
+ scope: "package",
821
+ path: "templates/Component"
822
+ });
823
+ }
824
+ const targetDirectory = pageScope ? join3(context.cwd, "src", "ui", ...pageSegments) : join3(context.cwd, "src", "ui", "_global");
825
+ await assertSafeTarget(context.cwd, targetDirectory, context.fs);
826
+ const componentFile = join3(targetDirectory, `${componentNameUpper}.tsx`);
827
+ const translationNamespace = pageScope ?? "_global_ui";
828
+ const componentContent = (await context.fs.readText(componentTemplate)).replace(/Component/g, componentNameUpper).replace(/componentPage/g, translationNamespace);
829
+ const preparedMessages = [];
830
+ if (config.useI18n) {
831
+ const messagesRoot = join3(context.cwd, "messages");
832
+ if (!context.fs.exists(messagesRoot)) {
833
+ throw new CliError("The messages directory was not found.", {
834
+ code: "CONFIG_NOT_FOUND",
835
+ scope: "project",
836
+ path: "messages"
837
+ });
838
+ }
839
+ const templateMessages = JSON.parse(
840
+ await context.fs.readText(messagesTemplate)
841
+ );
842
+ const locales = (await context.fs.list(messagesRoot)).filter((entry) => entry.isDirectory).map((entry) => entry.name).sort();
843
+ if (locales.length === 0) {
844
+ throw new CliError("No locale directories were found.", {
845
+ code: "CONFIG_NOT_FOUND",
846
+ scope: "project",
847
+ path: "messages"
848
+ });
849
+ }
850
+ for (const locale of locales) {
851
+ const messageFile = pageScope ? pageSegments[0] : "_global_ui";
852
+ const target = join3(messagesRoot, locale, `${messageFile}.json`);
853
+ let data = {};
854
+ if (context.fs.exists(target)) {
855
+ try {
856
+ data = JSON.parse(await context.fs.readText(target));
857
+ } catch {
858
+ throw new CliError(
859
+ `Invalid JSON in messages/${locale}/${messageFile}.json.`,
860
+ {
861
+ code: "FILESYSTEM_ERROR",
862
+ scope: "project",
863
+ path: `messages/${locale}/${messageFile}.json`
864
+ }
865
+ );
866
+ }
867
+ }
868
+ let container = data;
869
+ if (pageSegments.length > 1) {
870
+ const child = pageSegments[1];
871
+ const current = data[child];
872
+ if (current !== void 0 && (!current || typeof current !== "object" || Array.isArray(current))) {
873
+ throw new CliError(
874
+ `Translation namespace ${pageScope} is not an object in ${locale}.`,
875
+ {
876
+ code: "INVALID_ARGUMENT",
877
+ scope: "project",
878
+ path: `messages/${locale}/${messageFile}.json`
879
+ }
880
+ );
881
+ }
882
+ if (!current) data[child] = {};
883
+ container = data[child];
884
+ }
885
+ const exists = Object.hasOwn(container, componentNameUpper);
886
+ if (!exists) container[componentNameUpper] = templateMessages;
887
+ preparedMessages.push({
888
+ locale,
889
+ target,
890
+ exists,
891
+ content: exists ? void 0 : `${JSON.stringify(data, null, 2)}
892
+ `
893
+ });
102
894
  }
103
895
  }
104
- let componentTargetPath;
105
- let translationKey;
106
- if (pageScope) {
107
- if (nestedPath) {
108
- componentTargetPath = join2(process.cwd(), "src", "ui", nestedPath);
109
- translationKey = pageScope;
896
+ const gateway = new MutationGateway(context, context.cwd);
897
+ await gateway.mkdir(targetDirectory, {
898
+ role: "component-directory",
899
+ resource: "directory"
900
+ });
901
+ await gateway.write(componentFile, componentContent, {
902
+ role: "ui-component",
903
+ preserveExisting: true
904
+ });
905
+ for (const item of preparedMessages) {
906
+ if (item.exists) {
907
+ gateway.unchanged(item.target, {
908
+ role: "translation-messages",
909
+ detail: {
910
+ locale: item.locale,
911
+ key: `${translationNamespace}.${componentNameUpper}`
912
+ }
913
+ });
110
914
  } else {
111
- componentTargetPath = join2(process.cwd(), "src", "ui", pageScope);
112
- translationKey = pageScope;
915
+ await gateway.write(item.target, item.content, {
916
+ role: "translation-messages",
917
+ detail: {
918
+ locale: item.locale,
919
+ key: `${translationNamespace}.${componentNameUpper}`
920
+ }
921
+ });
113
922
  }
114
- } else {
115
- componentTargetPath = join2(process.cwd(), "src", "ui", "_global");
116
- translationKey = "_global_ui";
117
- }
118
- if (!existsSync2(componentTargetPath)) {
119
- await mkdir(componentTargetPath, { recursive: true });
120
- }
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");
125
- content = content.replace(/Component/g, componentNameUpper).replace(/componentPage/g, translationKey);
126
- await writeFile(componentFile, content);
127
- console.log(`\u{1F4C4} File created: ${componentFile}`);
128
- } else {
129
- console.error(
130
- "\u274C Template Component.tsx introuvable :",
131
- templateComponentPath
132
- );
133
923
  }
134
- if (useI18n && messagesPath) {
135
- const entries = await readdir(messagesPath, { withFileTypes: true });
136
- const langDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
137
- const jsonTemplate = join2(templatePath, "component.json");
138
- if (!existsSync2(jsonTemplate)) {
139
- console.error("\u274C Template component.json not found:", jsonTemplate);
140
- return;
924
+ const mutated = context.operations.snapshot().some((event) => event.action === "created" || event.action === "updated");
925
+ return commandResult(context, {
926
+ command: "addcomponent",
927
+ summary: mutated ? `Added component "${componentNameUpper}" ${pageScope ? `to page "${pageScope}"` : "globally"}.` : `Component "${componentNameUpper}" already exists and was preserved.`,
928
+ projectRoot: context.cwd,
929
+ nextSteps: mutated ? [
930
+ {
931
+ kind: "review",
932
+ required: true,
933
+ message: "Review the generated component and its localized messages.",
934
+ paths: [
935
+ { scope: "project", path: gateway.path(componentFile) },
936
+ ...preparedMessages.map((item) => ({
937
+ scope: "project",
938
+ path: gateway.path(item.target)
939
+ }))
940
+ ]
941
+ },
942
+ {
943
+ kind: "run-checks",
944
+ required: true,
945
+ message: "Run the project checks.",
946
+ paths: [],
947
+ commands: ["bun run check", "npm run check", "pnpm run check"]
948
+ }
949
+ ] : []
950
+ });
951
+ };
952
+
953
+ // src/lib/addLanguage.ts
954
+ import { join as join4 } from "path";
955
+ function generateLocales() {
956
+ const names = new Intl.DisplayNames(["en"], { type: "language" });
957
+ const locales = [];
958
+ for (let first = 0; first < 26; first++) {
959
+ for (let second = 0; second < 26; second++) {
960
+ const code = String.fromCharCode(97 + first, 97 + second);
961
+ try {
962
+ const name = names.of(code);
963
+ if (name && name.toLowerCase() !== code) locales.push(code);
964
+ } catch {
965
+ }
141
966
  }
142
- const jsonContent = await readFile2(jsonTemplate, "utf-8");
143
- const parsed = JSON.parse(jsonContent);
144
- for (const locale of langDirs) {
145
- const localeDir = join2(messagesPath, locale);
146
- if (!existsSync2(localeDir) || !statSync(localeDir).isDirectory())
147
- continue;
148
- let jsonTarget;
149
- if (pageScope) {
150
- jsonTarget = join2(messagesPath, locale, `${pageScope}.json`);
151
- } else {
152
- jsonTarget = join2(messagesPath, locale, `_global_ui.json`);
967
+ }
968
+ return locales.sort();
969
+ }
970
+ async function listFiles(context, root) {
971
+ const files = [];
972
+ async function visit(directory, relative = "") {
973
+ for (const entry of await context.fs.list(directory)) {
974
+ const next = relative ? `${relative}/${entry.name}` : entry.name;
975
+ if (entry.isSymbolicLink) {
976
+ throw new CliError(`Symbolic link found in locale source: ${next}`, {
977
+ code: "UNSAFE_PATH",
978
+ scope: "project",
979
+ path: next
980
+ });
981
+ }
982
+ if (entry.isDirectory) await visit(join4(directory, entry.name), next);
983
+ else if (entry.isFile) files.push(next);
984
+ }
985
+ }
986
+ await visit(root);
987
+ return files.sort();
988
+ }
989
+ var addLanguage = async (args, context) => {
990
+ const config = await loadConfig(context);
991
+ if (!config?.useI18n) {
992
+ throw new CliError("Internationalization is not enabled in this project.", {
993
+ code: "I18N_DISABLED"
994
+ });
995
+ }
996
+ const messagesRoot = join4(context.cwd, "messages");
997
+ if (!context.fs.exists(messagesRoot)) {
998
+ throw new CliError("The messages directory was not found.", {
999
+ code: "CONFIG_NOT_FOUND",
1000
+ scope: "project",
1001
+ path: "messages"
1002
+ });
1003
+ }
1004
+ const available = generateLocales();
1005
+ let locale = args[1];
1006
+ if (!locale) {
1007
+ if (context.outputMode === "json") {
1008
+ throw new CliError("Locale is required in JSON mode.", {
1009
+ code: "INTERACTIVE_INPUT_REQUIRED",
1010
+ hint: "Pass a two-letter locale after addlanguage."
1011
+ });
1012
+ }
1013
+ const response = await context.prompt({
1014
+ type: "autocomplete",
1015
+ name: "locale",
1016
+ message: "Locale to add:",
1017
+ choices: available.map((value) => ({ title: value, value }))
1018
+ });
1019
+ locale = String(response.locale ?? "");
1020
+ if (!locale) {
1021
+ context.operations.record({
1022
+ action: "cancelled",
1023
+ resource: "command",
1024
+ role: "locale-creation",
1025
+ scope: "project",
1026
+ path: "."
1027
+ });
1028
+ return commandResult(context, {
1029
+ command: "addlanguage",
1030
+ summary: "Locale creation was cancelled.",
1031
+ projectRoot: context.cwd,
1032
+ status: "cancelled"
1033
+ });
1034
+ }
1035
+ }
1036
+ parseLogicalName(locale, "locale");
1037
+ if (!available.includes(locale)) {
1038
+ throw new CliError(`Unsupported locale code: ${locale}.`, {
1039
+ code: "INVALID_ARGUMENT",
1040
+ hint: "Use a recognized two-letter language code."
1041
+ });
1042
+ }
1043
+ const routingFile = join4(context.cwd, "src", "lib", "i18n", "routing.ts");
1044
+ const registryFile = join4(context.cwd, "src", "lib", "i18n", "messages.ts");
1045
+ if (!context.fs.exists(routingFile) || !context.fs.exists(registryFile)) {
1046
+ throw new CliError(
1047
+ "Required i18n routing or registry file was not found.",
1048
+ {
1049
+ code: "CONFIG_NOT_FOUND",
1050
+ scope: "project",
1051
+ path: "src/lib/i18n"
1052
+ }
1053
+ );
1054
+ }
1055
+ const routing = await context.fs.readText(routingFile);
1056
+ const defaultLocale = routing.match(/defaultLocale:\s*["']([^"']+)["']/)?.[1];
1057
+ if (!defaultLocale) {
1058
+ throw new CliError(
1059
+ "The default locale could not be resolved from routing.ts.",
1060
+ {
1061
+ code: "CONFIG_NOT_FOUND",
1062
+ scope: "project",
1063
+ path: "src/lib/i18n/routing.ts"
1064
+ }
1065
+ );
1066
+ }
1067
+ const sourceDirectory = join4(messagesRoot, defaultLocale);
1068
+ const sourceAggregator = join4(messagesRoot, `${defaultLocale}.ts`);
1069
+ if (!context.fs.exists(sourceDirectory)) {
1070
+ throw new CliError(
1071
+ `Default locale directory ${defaultLocale} was not found.`,
1072
+ {
1073
+ code: "CONFIG_NOT_FOUND",
1074
+ scope: "project",
1075
+ path: `messages/${defaultLocale}`
153
1076
  }
154
- let current = {};
155
- if (existsSync2(jsonTarget)) {
156
- const jsonFile = await readFile2(jsonTarget, "utf-8");
157
- current = JSON.parse(jsonFile);
1077
+ );
1078
+ }
1079
+ if (!context.fs.exists(sourceAggregator)) {
1080
+ throw new CliError("Default locale aggregator not found.", {
1081
+ code: "CONFIG_NOT_FOUND",
1082
+ scope: "project",
1083
+ path: `messages/${defaultLocale}.ts`
1084
+ });
1085
+ }
1086
+ const sourceFiles = await listFiles(context, sourceDirectory);
1087
+ if (sourceFiles.length === 0) {
1088
+ throw new CliError(`Default locale ${defaultLocale} contains no files.`, {
1089
+ code: "CONFIG_NOT_FOUND",
1090
+ scope: "project",
1091
+ path: `messages/${defaultLocale}`
1092
+ });
1093
+ }
1094
+ const defaultAggregator = await context.fs.readText(sourceAggregator);
1095
+ const importPrefix = `./${defaultLocale}/`;
1096
+ if (!defaultAggregator.includes(importPrefix)) {
1097
+ throw new CliError(
1098
+ `Default aggregator does not import from ${importPrefix}.`,
1099
+ {
1100
+ code: "CONFIG_NOT_FOUND",
1101
+ scope: "project",
1102
+ path: `messages/${defaultLocale}.ts`
158
1103
  }
159
- current[componentNameUpper] = parsed;
160
- await writeFile(jsonTarget, JSON.stringify(current, null, 2));
161
- console.log(`\u{1F4C4} File updated: ${jsonTarget}`);
1104
+ );
1105
+ }
1106
+ const localesMatch = routing.match(/locales:\s*\[([^\]]*)\]/);
1107
+ if (!localesMatch) {
1108
+ throw new CliError("Unable to locate routing locales.", {
1109
+ code: "CONFIG_NOT_FOUND",
1110
+ scope: "project",
1111
+ path: "src/lib/i18n/routing.ts"
1112
+ });
1113
+ }
1114
+ const routingLocales = localesMatch[1].split(",").map((value) => value.trim().replace(/["']/g, "")).filter(Boolean);
1115
+ const registry = await context.fs.readText(registryFile);
1116
+ const registryMatch = registry.match(
1117
+ /const messages = \{([^}]*)\} as const;/
1118
+ );
1119
+ if (!registryMatch) {
1120
+ throw new CliError("Unable to locate the typed messages registry.", {
1121
+ code: "CONFIG_NOT_FOUND",
1122
+ scope: "project",
1123
+ path: "src/lib/i18n/messages.ts"
1124
+ });
1125
+ }
1126
+ const registeredLocales = registryMatch[1].split(",").map((value) => value.trim()).filter(Boolean);
1127
+ const targetDirectory = join4(messagesRoot, locale);
1128
+ const targetAggregator = join4(messagesRoot, `${locale}.ts`);
1129
+ const targetAggregatorExists = context.fs.exists(targetAggregator);
1130
+ const targetAggregatorContent = targetAggregatorExists ? await context.fs.readText(targetAggregator) : "";
1131
+ const state = {
1132
+ directory: context.fs.exists(targetDirectory),
1133
+ aggregator: targetAggregatorExists,
1134
+ aggregatorImportsLocale: targetAggregatorContent.includes(`./${locale}/`),
1135
+ routing: routingLocales.includes(locale),
1136
+ registry: registeredLocales.includes(locale),
1137
+ registryImport: new RegExp(
1138
+ `import\\s+${locale}\\s+from\\s+["']\\.\\.\\/\\.\\.\\/\\.\\.\\/messages\\/${locale}["'];`
1139
+ ).test(registry)
1140
+ };
1141
+ const present = Object.values(state).filter(Boolean).length;
1142
+ const gateway = new MutationGateway(context, context.cwd);
1143
+ const missingLocaleFiles = state.directory ? sourceFiles.filter(
1144
+ (relative) => !context.fs.exists(join4(targetDirectory, relative))
1145
+ ) : sourceFiles;
1146
+ if (present === Object.keys(state).length && missingLocaleFiles.length === 0) {
1147
+ gateway.unchanged(targetDirectory, {
1148
+ role: "locale-directory",
1149
+ resource: "directory"
1150
+ });
1151
+ for (const relative of sourceFiles) {
1152
+ gateway.unchanged(join4(targetDirectory, relative), {
1153
+ role: relative.endsWith(".json") ? "translation-messages" : "locale-file",
1154
+ detail: { locale, sourceLocale: defaultLocale }
1155
+ });
162
1156
  }
163
- } else {
164
- console.log("\u2139\uFE0F Skipping translation entries; next-intl not enabled.");
1157
+ gateway.unchanged(targetAggregator, { role: "locale-aggregator" });
1158
+ gateway.unchanged(routingFile, { role: "i18n-routing" });
1159
+ gateway.unchanged(registryFile, { role: "messages-registry" });
1160
+ return commandResult(context, {
1161
+ command: "addlanguage",
1162
+ summary: `Locale "${locale}" is already fully configured.`,
1163
+ projectRoot: context.cwd
1164
+ });
165
1165
  }
166
- console.log(
167
- `\u2705 Component "${componentNameUpper}" added ${pageScope ? `to page ${pageScope}` : "globally"}${useI18n ? " with localized messages" : ""}.`
1166
+ if (present > 0 || missingLocaleFiles.length < sourceFiles.length) {
1167
+ throw new CliError(`Locale ${locale} is only partially configured.`, {
1168
+ code: "INCONSISTENT_LOCALE",
1169
+ scope: "project",
1170
+ path: `messages/${locale}`,
1171
+ hint: `Observed state: ${JSON.stringify({ ...state, missingLocaleFiles })}.`
1172
+ });
1173
+ }
1174
+ const nextAggregator = defaultAggregator.replaceAll(
1175
+ importPrefix,
1176
+ `./${locale}/`
168
1177
  );
169
- }
1178
+ const nextRouting = routing.replace(
1179
+ /locales:\s*\[[^\]]*\]/,
1180
+ `locales: [${[...routingLocales, locale].map((value) => `"${value}"`).join(", ")}]`
1181
+ );
1182
+ const declarationIndex = registry.indexOf("const messages =");
1183
+ const nextRegistry = registry.slice(0, declarationIndex) + `import ${locale} from "../../../messages/${locale}";
170
1184
 
171
- // 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) {
177
- let pageName = args[1];
178
- if (!pageName || pageName.startsWith("-")) {
179
- const response = await prompts2.prompt({
180
- type: "text",
181
- name: "pageName",
182
- message: "\u{1F4DD} Page name to add:",
183
- validate: (name) => name ? true : "Page name is required"
1185
+ ` + registry.slice(declarationIndex).replace(
1186
+ /const messages = \{[^}]*\} as const;/,
1187
+ `const messages = { ${[...registeredLocales, locale].join(", ")} } as const;`
1188
+ );
1189
+ await assertSafeTarget(context.cwd, targetDirectory, context.fs);
1190
+ await gateway.mkdir(targetDirectory, {
1191
+ role: "locale-directory",
1192
+ resource: "directory",
1193
+ detail: { locale, sourceLocale: defaultLocale }
1194
+ });
1195
+ for (const relative of sourceFiles) {
1196
+ const source = join4(sourceDirectory, relative);
1197
+ const target = join4(targetDirectory, relative);
1198
+ await gateway.copy(source, target, {
1199
+ role: relative.endsWith(".json") ? "translation-messages" : "locale-file",
1200
+ source: { scope: "project", path: gateway.path(source) },
1201
+ detail: { locale, sourceLocale: defaultLocale }
184
1202
  });
185
- pageName = response.pageName;
186
- }
187
- let parentName = null;
188
- let childName = null;
189
- if (pageName.includes(".")) {
190
- [parentName, childName] = pageName.split(".");
191
- }
192
- let shortFlags = args.find((arg) => /^-[A-Za-z]+$/.test(arg));
193
- let longFlags = new Set(args.filter((a) => a.startsWith("--")));
194
- const flags = /* @__PURE__ */ new Set();
195
- if (!shortFlags && Array.from(longFlags).length === 0) {
196
- shortFlags = "-LPl";
197
- }
198
- if (shortFlags) {
199
- for (const char of shortFlags.slice(1)) {
200
- switch (char) {
201
- case "L":
202
- flags.add("layout");
203
- break;
204
- case "P":
205
- flags.add("page");
206
- break;
207
- case "l":
208
- flags.add("loading");
209
- break;
210
- case "n":
211
- flags.add("not-found");
212
- break;
213
- case "e":
214
- flags.add("error");
215
- break;
216
- case "g":
217
- flags.add("global-error");
218
- break;
219
- case "r":
220
- flags.add("route");
221
- break;
222
- case "t":
223
- flags.add("template");
224
- break;
225
- case "d":
226
- flags.add("default");
227
- break;
1203
+ }
1204
+ await gateway.write(targetAggregator, nextAggregator, {
1205
+ role: "locale-aggregator"
1206
+ });
1207
+ await gateway.write(registryFile, nextRegistry, {
1208
+ role: "messages-registry"
1209
+ });
1210
+ await gateway.write(routingFile, nextRouting, { role: "i18n-routing" });
1211
+ const translationPaths = sourceFiles.filter((relative) => relative.endsWith(".json")).map((relative) => ({
1212
+ scope: "project",
1213
+ path: gateway.path(join4(targetDirectory, relative))
1214
+ }));
1215
+ return commandResult(context, {
1216
+ command: "addlanguage",
1217
+ summary: `Added locale "${locale}" by copying ${sourceFiles.length} files from "${defaultLocale}".`,
1218
+ projectRoot: context.cwd,
1219
+ nextSteps: [
1220
+ {
1221
+ kind: "translate",
1222
+ required: true,
1223
+ message: `Translate every copied message from ${defaultLocale} to ${locale}; the copied text is not ready for delivery.`,
1224
+ paths: translationPaths
1225
+ },
1226
+ {
1227
+ kind: "run-checks",
1228
+ required: true,
1229
+ message: "Run the project checks.",
1230
+ paths: [],
1231
+ commands: ["bun run check", "npm run check", "pnpm run check"]
228
1232
  }
1233
+ ]
1234
+ });
1235
+ };
1236
+
1237
+ // src/lib/addLib.ts
1238
+ import { join as join5 } from "path";
1239
+ var addLib = async (args, context) => {
1240
+ let libArg = args[1];
1241
+ if (!libArg || libArg.startsWith("-")) {
1242
+ if (context.outputMode === "json") {
1243
+ throw new CliError("Library name is required in JSON mode.", {
1244
+ code: "INTERACTIVE_INPUT_REQUIRED",
1245
+ hint: "Pass library or library.module after addlib."
1246
+ });
1247
+ }
1248
+ const response = await context.prompt({
1249
+ type: "text",
1250
+ name: "libArg",
1251
+ message: "Library name to add:",
1252
+ validate: (name) => name ? true : "Library name is required"
1253
+ });
1254
+ libArg = String(response.libArg ?? "");
1255
+ if (!libArg) {
1256
+ context.operations.record({
1257
+ action: "cancelled",
1258
+ resource: "command",
1259
+ role: "library-creation",
1260
+ scope: "project",
1261
+ path: "."
1262
+ });
1263
+ return commandResult(context, {
1264
+ command: "addlib",
1265
+ summary: "Library creation was cancelled.",
1266
+ projectRoot: context.cwd,
1267
+ status: "cancelled"
1268
+ });
229
1269
  }
230
1270
  }
231
- for (const flag of [
232
- "layout",
233
- "page",
234
- "loading",
235
- "not-found",
236
- "error",
237
- "global-error",
238
- "route",
239
- "template",
240
- "default"
241
- ]) {
242
- if (longFlags.has("--" + flag)) flags.add(flag);
243
- }
244
- const config = await loadConfig();
245
- if (!config) {
246
- console.error(
247
- "\u274C Configuration file cnp.config.json not found. Run this command from the project root."
1271
+ const segments = parseLogicalName(libArg, "library name");
1272
+ if (segments.length > 2) {
1273
+ throw new CliError("Libraries support exactly library or library.module.", {
1274
+ code: "INVALID_ARGUMENT"
1275
+ });
1276
+ }
1277
+ const [libName, fileName] = segments;
1278
+ if (!await loadConfig(context)) {
1279
+ throw new CliError("Configuration file cnp.config.json was not found.", {
1280
+ code: "CONFIG_NOT_FOUND",
1281
+ hint: "Run this command from the generated project root."
1282
+ });
1283
+ }
1284
+ const libDir = join5(context.cwd, "src", "lib", libName);
1285
+ await assertSafeTarget(context.cwd, libDir, context.fs);
1286
+ const gateway = new MutationGateway(context, context.cwd);
1287
+ const templateDir = join5(context.packageRoot, "templates", "Lib");
1288
+ const indexTemplate = join5(templateDir, "index.ts");
1289
+ const itemTemplate = join5(templateDir, "item.ts");
1290
+ if (!context.fs.exists(indexTemplate) || fileName && !context.fs.exists(itemTemplate)) {
1291
+ throw new CliError("Required library template files were not found.", {
1292
+ code: "TEMPLATE_MISSING",
1293
+ scope: "package",
1294
+ path: "templates/Lib"
1295
+ });
1296
+ }
1297
+ await gateway.mkdir(libDir, {
1298
+ role: "library-directory",
1299
+ resource: "directory"
1300
+ });
1301
+ const indexPath = join5(libDir, "index.ts");
1302
+ if (!context.fs.exists(indexPath)) {
1303
+ await gateway.write(indexPath, await context.fs.readText(indexTemplate), {
1304
+ role: "library-index"
1305
+ });
1306
+ }
1307
+ let moduleAction;
1308
+ if (fileName) {
1309
+ const moduleIdentifier = toIdentifier(fileName);
1310
+ const modulePath = join5(libDir, `${fileName}.ts`);
1311
+ const moduleContent = (await context.fs.readText(itemTemplate)).replace(/template/g, moduleIdentifier).replace(/Template/g, capitalize(moduleIdentifier));
1312
+ moduleAction = await gateway.write(modulePath, moduleContent, {
1313
+ role: "library-module",
1314
+ preserveExisting: true
1315
+ });
1316
+ const currentIndex = await context.fs.readText(indexPath);
1317
+ const importLine = `import { ${moduleIdentifier} } from "./${fileName}";`;
1318
+ const importRegex = new RegExp(
1319
+ `import\\s*{\\s*${moduleIdentifier}\\s*}\\s*from\\s*["']\\./${fileName}["'];`
248
1320
  );
249
- return;
1321
+ const exportRegex = /export\s*{([^}]*)}/m;
1322
+ const imports = currentIndex.split("\n").filter((line) => line.startsWith("import"));
1323
+ const exportMatch = currentIndex.match(exportRegex);
1324
+ const exportsSet = (exportMatch?.[1] ?? "").split(",").map((item) => item.trim()).filter(Boolean);
1325
+ if (!imports.some((line) => importRegex.test(line)))
1326
+ imports.push(importLine);
1327
+ if (!exportsSet.includes(moduleIdentifier))
1328
+ exportsSet.push(moduleIdentifier);
1329
+ const nextIndex = `${imports.join("\n")}
1330
+
1331
+ export { ${exportsSet.join(", ")} };
1332
+ `;
1333
+ await gateway.write(indexPath, nextIndex, { role: "library-index" });
1334
+ } else if (context.fs.exists(indexPath)) {
1335
+ gateway.unchanged(indexPath, { role: "library-index" });
1336
+ }
1337
+ const mutated = context.operations.snapshot().some(
1338
+ (event) => ["created", "updated", "copied", "deleted"].includes(event.action)
1339
+ );
1340
+ const nextSteps = [];
1341
+ if (fileName && moduleAction !== "unchanged") {
1342
+ nextSteps.push({
1343
+ kind: "review",
1344
+ required: true,
1345
+ message: "Implement and review the generated library module.",
1346
+ paths: [
1347
+ {
1348
+ scope: "project",
1349
+ path: gateway.path(join5(libDir, `${fileName}.ts`))
1350
+ }
1351
+ ]
1352
+ });
250
1353
  }
251
- const useI18n = !!config.useI18n;
252
- const srcSegments = ["src", "app"];
253
- if (useI18n) srcSegments.push("[locale]");
254
- const srcPath = join3(process.cwd(), ...srcSegments);
255
- if (!existsSync3(srcPath)) {
256
- console.error(`\u274C Expected directory not found: ${srcPath}`);
257
- return;
1354
+ if (mutated) {
1355
+ nextSteps.push({
1356
+ kind: "run-checks",
1357
+ required: true,
1358
+ message: "Run the project checks.",
1359
+ paths: [],
1360
+ commands: ["bun run check", "npm run check", "pnpm run check"]
1361
+ });
258
1362
  }
259
- let messagesPath = null;
260
- let locales = [];
261
- if (useI18n) {
262
- messagesPath = join3(process.cwd(), "messages");
263
- if (!existsSync3(messagesPath)) {
264
- console.error(
265
- "\u274C Messages directory missing. Ensure i18n was configured."
266
- );
267
- return;
1363
+ return commandResult(context, {
1364
+ command: "addlib",
1365
+ summary: mutated ? `Added library "${libName}"${fileName ? ` with module "${fileName}"` : ""}.` : `Library "${libName}"${fileName ? ` and module "${fileName}"` : ""} already exist and were preserved.`,
1366
+ projectRoot: context.cwd,
1367
+ nextSteps
1368
+ });
1369
+ };
1370
+
1371
+ // src/lib/addPage.ts
1372
+ import { join as join6 } from "path";
1373
+ var LONG_FLAGS = [
1374
+ "layout",
1375
+ "page",
1376
+ "loading",
1377
+ "not-found",
1378
+ "error",
1379
+ "global-error",
1380
+ "route",
1381
+ "template",
1382
+ "default"
1383
+ ];
1384
+ var SHORT_FLAGS = {
1385
+ L: "layout",
1386
+ P: "page",
1387
+ l: "loading",
1388
+ n: "not-found",
1389
+ e: "error",
1390
+ g: "global-error",
1391
+ r: "route",
1392
+ t: "template",
1393
+ d: "default"
1394
+ };
1395
+ function registerMessagesFile(content, locale, fileName) {
1396
+ const importPath = `./${locale}/${fileName}.json`;
1397
+ if (content.includes(importPath)) return content;
1398
+ const declarationIndex = content.indexOf("const messages =");
1399
+ const registryMatch = content.match(/const messages = \{([\s\S]*?)\n\};/);
1400
+ if (declarationIndex < 0 || !registryMatch) {
1401
+ throw new CliError(
1402
+ `Unable to register ${fileName}.json in messages/${locale}.ts.`,
1403
+ {
1404
+ code: "CONFIG_NOT_FOUND",
1405
+ scope: "project",
1406
+ path: `messages/${locale}.ts`
1407
+ }
1408
+ );
1409
+ }
1410
+ const identifier = toIdentifier(fileName);
1411
+ const importStatement = `import ${identifier} from "${importPath}";
1412
+ `;
1413
+ const nextRegistry = registryMatch[0].replace(
1414
+ /\n\};$/,
1415
+ `
1416
+ ${identifier},
1417
+ };`
1418
+ );
1419
+ return content.slice(0, declarationIndex) + importStatement + content.slice(declarationIndex).replace(registryMatch[0], nextRegistry);
1420
+ }
1421
+ function selectedFlags(args) {
1422
+ const selected = /* @__PURE__ */ new Set();
1423
+ const optionArguments = args.slice(2).filter((argument) => argument.startsWith("-"));
1424
+ if (optionArguments.length === 0)
1425
+ return /* @__PURE__ */ new Set(["layout", "page", "loading"]);
1426
+ for (const argument of optionArguments) {
1427
+ if (argument.startsWith("--")) {
1428
+ const flag = argument.slice(2);
1429
+ if (!LONG_FLAGS.includes(flag)) {
1430
+ throw new CliError(`Unknown addpage option: ${argument}.`, {
1431
+ code: "INVALID_ARGUMENT"
1432
+ });
1433
+ }
1434
+ selected.add(flag);
1435
+ continue;
1436
+ }
1437
+ for (const character of argument.slice(1)) {
1438
+ const flag = SHORT_FLAGS[character];
1439
+ if (!flag) {
1440
+ throw new CliError(`Unknown addpage short option: -${character}.`, {
1441
+ code: "INVALID_ARGUMENT"
1442
+ });
1443
+ }
1444
+ selected.add(flag);
1445
+ }
1446
+ }
1447
+ return selected;
1448
+ }
1449
+ var addPage = async (args, context) => {
1450
+ let logicalName = args[1];
1451
+ if (!logicalName || logicalName.startsWith("-")) {
1452
+ if (context.outputMode === "json") {
1453
+ throw new CliError("Page name is required in JSON mode.", {
1454
+ code: "INTERACTIVE_INPUT_REQUIRED",
1455
+ hint: "Pass a simple or Parent.Child page name after addpage."
1456
+ });
1457
+ }
1458
+ const response = await context.prompt({
1459
+ type: "text",
1460
+ name: "pageName",
1461
+ message: "Page name to add:",
1462
+ validate: (name) => name ? true : "Page name is required"
1463
+ });
1464
+ logicalName = String(response.pageName ?? "");
1465
+ if (!logicalName) {
1466
+ context.operations.record({
1467
+ action: "cancelled",
1468
+ resource: "command",
1469
+ role: "page-creation",
1470
+ scope: "project",
1471
+ path: "."
1472
+ });
1473
+ return commandResult(context, {
1474
+ command: "addpage",
1475
+ summary: "Page creation was cancelled.",
1476
+ projectRoot: context.cwd,
1477
+ status: "cancelled"
1478
+ });
268
1479
  }
269
- const entries = await readdir2(messagesPath, { withFileTypes: true });
270
- locales = entries.filter((e) => e.isDirectory()).map((e) => e.name);
271
1480
  }
272
- const templatePath = join3(
273
- new URL("..", import.meta.url).pathname,
274
- "templates",
275
- "Page"
1481
+ const pageSegments = parseLogicalName(logicalName, "page name");
1482
+ if (pageSegments.length > 2) {
1483
+ throw new CliError("Nested pages support exactly Parent.Child.", {
1484
+ code: "INVALID_ARGUMENT"
1485
+ });
1486
+ }
1487
+ const flags = selectedFlags(args);
1488
+ const config = await loadConfig(context);
1489
+ if (!config) {
1490
+ throw new CliError("Configuration file cnp.config.json was not found.", {
1491
+ code: "CONFIG_NOT_FOUND",
1492
+ hint: "Run this command from the generated project root."
1493
+ });
1494
+ }
1495
+ const useI18n = Boolean(config.useI18n);
1496
+ const aliasPrefix = configuredAliasPrefix(config);
1497
+ const appRoot = join6(
1498
+ context.cwd,
1499
+ "src",
1500
+ "app",
1501
+ ...useI18n ? ["[locale]"] : []
276
1502
  );
277
- let uiPageDir, localePagePath, jsonFileName;
278
- if (parentName && childName) {
279
- uiPageDir = join3(process.cwd(), "src", "ui", parentName, childName);
280
- localePagePath = join3(srcPath, parentName, childName);
281
- jsonFileName = parentName;
282
- } else {
283
- uiPageDir = join3(process.cwd(), "src", "ui", pageName);
284
- localePagePath = join3(srcPath, pageName);
285
- jsonFileName = pageName;
286
- }
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);
296
- console.log(`\u{1F4C4} File created: ${uiPageFile}`);
297
- } else {
298
- console.warn(
299
- "\u26A0\uFE0F Missing template file: page-ui.tsx at path:",
300
- uiPageTemplate
301
- );
1503
+ if (!context.fs.exists(appRoot)) {
1504
+ throw new CliError("The expected App Router directory was not found.", {
1505
+ code: "CONFIG_NOT_FOUND",
1506
+ scope: "project",
1507
+ path: useI18n ? "src/app/[locale]" : "src/app"
1508
+ });
302
1509
  }
303
- if (!existsSync3(localePagePath)) {
304
- await mkdir2(localePagePath, { recursive: true });
1510
+ const [parentName, childName] = pageSegments.length === 2 ? pageSegments : [void 0, void 0];
1511
+ const leafName = childName ?? pageSegments[0];
1512
+ const pageIdentifier = capitalize(toIdentifier(leafName));
1513
+ const jsonFileName = parentName ?? leafName;
1514
+ const uiDirectory = join6(context.cwd, "src", "ui", ...pageSegments);
1515
+ const routeDirectory = join6(appRoot, ...pageSegments);
1516
+ await assertSafeTarget(context.cwd, uiDirectory, context.fs);
1517
+ await assertSafeTarget(context.cwd, routeDirectory, context.fs);
1518
+ const templateRoot = join6(context.packageRoot, "templates", "Page");
1519
+ const uiTemplate = join6(templateRoot, "page-ui.tsx");
1520
+ const routeTemplates = [...flags].map((flag) => ({
1521
+ flag,
1522
+ source: join6(templateRoot, toFileName(flag))
1523
+ }));
1524
+ const messagesTemplate = join6(templateRoot, "page.json");
1525
+ const requiredTemplates = [
1526
+ uiTemplate,
1527
+ ...routeTemplates.map((entry) => entry.source)
1528
+ ];
1529
+ if (useI18n) requiredTemplates.push(messagesTemplate);
1530
+ const missing = requiredTemplates.find(
1531
+ (target) => !context.fs.exists(target)
1532
+ );
1533
+ if (missing) {
1534
+ throw new CliError(`Required page template was not found: ${missing}.`, {
1535
+ code: "TEMPLATE_MISSING",
1536
+ scope: "package",
1537
+ path: missing.replace(`${context.packageRoot}/`, "")
1538
+ });
305
1539
  }
306
- for (const flag of flags) {
307
- const filename = toFileName(flag);
308
- const src = join3(templatePath, filename);
309
- const dst = join3(localePagePath, filename);
310
- if (!existsSync3(src)) {
311
- console.warn(`\u26A0\uFE0F Missing template file: ${filename} at path: ${src}`);
312
- continue;
1540
+ const translationNamespace = parentName ? `${parentName}.${childName}` : leafName;
1541
+ const templateJson = useI18n ? JSON.parse(
1542
+ (await context.fs.readText(messagesTemplate)).replace(/template/g, leafName).replace(/Template/g, capitalize(leafName))
1543
+ ) : void 0;
1544
+ const preparedLocales = [];
1545
+ if (useI18n) {
1546
+ const messagesRoot = join6(context.cwd, "messages");
1547
+ if (!context.fs.exists(messagesRoot)) {
1548
+ throw new CliError("The messages directory was not found.", {
1549
+ code: "CONFIG_NOT_FOUND",
1550
+ scope: "project",
1551
+ path: "messages"
1552
+ });
313
1553
  }
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);
317
- console.log(`\u{1F4C4} File created: ${dst}`);
318
- }
319
- if (useI18n && messagesPath) {
320
- const jsonTemplate = join3(templatePath, "page.json");
321
- if (!existsSync3(jsonTemplate)) {
322
- console.warn("\u26A0\uFE0F Missing template: page.json at path:", jsonTemplate);
323
- } else {
324
- const content = await readFile3(jsonTemplate, "utf-8");
325
- const replaced = content.replace(/template/g, childName || pageName).replace(/Template/g, capitalize(childName || pageName));
326
- for (const locale of locales) {
327
- const localeDir = join3(messagesPath, locale);
328
- if (!existsSync3(localeDir) || !statSync2(localeDir).isDirectory())
329
- continue;
330
- const jsonTarget = join3(messagesPath, locale, `${jsonFileName}.json`);
331
- let current = {};
332
- if (existsSync3(jsonTarget)) {
333
- const jsonFile = await readFile3(jsonTarget, "utf-8");
334
- try {
335
- current = JSON.parse(jsonFile);
336
- } catch {
337
- current = {};
1554
+ const locales = (await context.fs.list(messagesRoot)).filter((entry) => entry.isDirectory).map((entry) => entry.name).sort();
1555
+ if (locales.length === 0) {
1556
+ throw new CliError("No locale directories were found.", {
1557
+ code: "CONFIG_NOT_FOUND",
1558
+ scope: "project",
1559
+ path: "messages"
1560
+ });
1561
+ }
1562
+ for (const locale of locales) {
1563
+ const target = join6(messagesRoot, locale, `${jsonFileName}.json`);
1564
+ const aggregator = join6(messagesRoot, `${locale}.ts`);
1565
+ if (!context.fs.exists(aggregator)) {
1566
+ throw new CliError(
1567
+ `Locale aggregator messages/${locale}.ts was not found.`,
1568
+ {
1569
+ code: "CONFIG_NOT_FOUND",
1570
+ scope: "project",
1571
+ path: `messages/${locale}.ts`
338
1572
  }
1573
+ );
1574
+ }
1575
+ let data = {};
1576
+ const targetExists = context.fs.exists(target);
1577
+ if (targetExists) {
1578
+ try {
1579
+ data = JSON.parse(await context.fs.readText(target));
1580
+ } catch {
1581
+ throw new CliError(
1582
+ `Invalid JSON in messages/${locale}/${jsonFileName}.json.`,
1583
+ {
1584
+ code: "FILESYSTEM_ERROR",
1585
+ scope: "project",
1586
+ path: `messages/${locale}/${jsonFileName}.json`
1587
+ }
1588
+ );
339
1589
  }
340
- if (parentName && childName) {
341
- current[childName] = JSON.parse(replaced);
342
- } else {
343
- current = JSON.parse(replaced);
344
- }
345
- await writeFile2(jsonTarget, JSON.stringify(current, null, 2));
346
- console.log(`\u{1F4C4} File created: ${jsonTarget}`);
347
1590
  }
1591
+ const messageExists = parentName ? Object.hasOwn(data, childName) : targetExists;
1592
+ if (!messageExists) {
1593
+ if (parentName) data[childName] = templateJson;
1594
+ else data = templateJson;
1595
+ }
1596
+ const aggregatorCurrent = await context.fs.readText(aggregator);
1597
+ preparedLocales.push({
1598
+ locale,
1599
+ target,
1600
+ targetContent: messageExists ? void 0 : `${JSON.stringify(data, null, 2)}
1601
+ `,
1602
+ messageExists,
1603
+ aggregator,
1604
+ aggregatorContent: registerMessagesFile(
1605
+ aggregatorCurrent,
1606
+ locale,
1607
+ jsonFileName
1608
+ )
1609
+ });
1610
+ }
1611
+ }
1612
+ const gateway = new MutationGateway(context, context.cwd);
1613
+ await gateway.mkdir(uiDirectory, {
1614
+ role: "page-ui-directory",
1615
+ resource: "directory"
1616
+ });
1617
+ const uiFile = join6(uiDirectory, "page-ui.tsx");
1618
+ const uiContent = (await context.fs.readText(uiTemplate)).replace(
1619
+ 'useTranslations("template")',
1620
+ `useTranslations("${translationNamespace}")`
1621
+ ).replaceAll('from "@/', `from "${aliasPrefix}/`).replace(/template/g, pageIdentifier).replace(/Template/g, pageIdentifier);
1622
+ await gateway.write(uiFile, uiContent, {
1623
+ role: "page-ui",
1624
+ preserveExisting: true
1625
+ });
1626
+ await gateway.mkdir(routeDirectory, {
1627
+ role: "page-route-directory",
1628
+ resource: "directory"
1629
+ });
1630
+ for (const template of routeTemplates) {
1631
+ const filename = toFileName(template.flag);
1632
+ const target = join6(routeDirectory, filename);
1633
+ const uiImportPath = pageSegments.join("/");
1634
+ 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);
1635
+ await gateway.write(target, content, {
1636
+ role: `page-${template.flag}`,
1637
+ preserveExisting: true
1638
+ });
1639
+ }
1640
+ if (useI18n) {
1641
+ for (const item of preparedLocales) {
1642
+ if (item.messageExists) {
1643
+ gateway.unchanged(item.target, {
1644
+ role: "translation-messages",
1645
+ detail: { locale: item.locale, namespace: translationNamespace }
1646
+ });
1647
+ } else {
1648
+ await gateway.write(item.target, item.targetContent, {
1649
+ role: "translation-messages",
1650
+ detail: { locale: item.locale, namespace: translationNamespace }
1651
+ });
1652
+ }
1653
+ await gateway.write(item.aggregator, item.aggregatorContent, {
1654
+ role: "locale-aggregator"
1655
+ });
348
1656
  }
349
1657
  } else {
350
- console.log("\u2139\uFE0F Skipping translation templates; next-intl not enabled.");
1658
+ gateway.skipped(join6(context.cwd, "messages"), {
1659
+ role: "translation-messages",
1660
+ resource: "directory",
1661
+ detail: { reason: "Internationalization is disabled." }
1662
+ });
351
1663
  }
352
- console.log(
353
- `\u2705 Page "${pageName}" with templates added${useI18n ? " for each locale" : ""}.`
1664
+ const mutated = context.operations.snapshot().some(
1665
+ (event) => ["created", "updated", "copied", "deleted"].includes(event.action)
354
1666
  );
355
- }
1667
+ const reviewPaths = [
1668
+ { scope: "project", path: gateway.path(uiFile) },
1669
+ ...routeTemplates.map((template) => ({
1670
+ scope: "project",
1671
+ path: gateway.path(join6(routeDirectory, toFileName(template.flag)))
1672
+ })),
1673
+ ...preparedLocales.map((item) => ({
1674
+ scope: "project",
1675
+ path: gateway.path(item.target)
1676
+ }))
1677
+ ];
1678
+ return commandResult(context, {
1679
+ command: "addpage",
1680
+ summary: mutated ? `Added page "${logicalName}" and its missing resources.` : `Page "${logicalName}" already exists and was preserved.`,
1681
+ projectRoot: context.cwd,
1682
+ nextSteps: mutated ? [
1683
+ {
1684
+ kind: "review",
1685
+ required: true,
1686
+ message: "Review the generated page UI, route files and localized messages.",
1687
+ paths: reviewPaths
1688
+ },
1689
+ {
1690
+ kind: "run-checks",
1691
+ required: true,
1692
+ message: "Run the project checks.",
1693
+ paths: [],
1694
+ commands: ["bun run check", "npm run check", "pnpm run check"]
1695
+ }
1696
+ ] : []
1697
+ });
1698
+ };
356
1699
 
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"
1700
+ // src/lib/addText.ts
1701
+ import { join as join7 } from "path";
1702
+ function defaultText(key) {
1703
+ return key.replace(/_/g, " ").replace(/\b\w/g, (character) => character.toUpperCase());
1704
+ }
1705
+ var addText = async (args, context) => {
1706
+ const pathArg = args[1];
1707
+ if (!pathArg) {
1708
+ throw new CliError("Translation dot path is required.", {
1709
+ code: "INVALID_ARGUMENT",
1710
+ hint: "Use addtext domain.key followed by an optional value."
370
1711
  });
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}`);
1712
+ }
1713
+ const segments = parseLogicalName(pathArg, "translation path");
1714
+ if (segments.length < 2) {
1715
+ throw new CliError("Translation path must contain a file and a key.", {
1716
+ code: "INVALID_ARGUMENT"
1717
+ });
1718
+ }
1719
+ const providedText = args.slice(2).join(" ");
1720
+ const finalKey = segments.at(-1);
1721
+ const text = providedText || defaultText(finalKey);
1722
+ const config = await loadConfig(context);
1723
+ if (!config?.useI18n) {
1724
+ throw new CliError("Internationalization is not enabled in this project.", {
1725
+ code: "I18N_DISABLED"
1726
+ });
1727
+ }
1728
+ const messagesRoot = join7(context.cwd, "messages");
1729
+ if (!context.fs.exists(messagesRoot)) {
1730
+ throw new CliError("The messages directory was not found.", {
1731
+ code: "CONFIG_NOT_FOUND",
1732
+ scope: "project",
1733
+ path: "messages"
1734
+ });
1735
+ }
1736
+ const locales = (await context.fs.list(messagesRoot)).filter((entry) => entry.isDirectory).map((entry) => entry.name).sort();
1737
+ if (locales.length === 0) {
1738
+ throw new CliError("No locale directories were found.", {
1739
+ code: "CONFIG_NOT_FOUND",
1740
+ scope: "project",
1741
+ path: "messages"
1742
+ });
1743
+ }
1744
+ const [fileName, ...keySegments] = segments;
1745
+ const prepared = [];
1746
+ for (const locale of locales) {
1747
+ const file = join7(messagesRoot, locale, `${fileName}.json`);
1748
+ await assertSafeTarget(context.cwd, file, context.fs);
1749
+ const existed = context.fs.exists(file);
1750
+ let data = {};
1751
+ if (existed) {
1752
+ try {
1753
+ const parsed = JSON.parse(await context.fs.readText(file));
1754
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1755
+ throw new Error("root value is not an object");
1756
+ }
1757
+ data = parsed;
1758
+ } catch (error) {
1759
+ throw new CliError(
1760
+ `Invalid JSON in messages/${locale}/${fileName}.json.`,
1761
+ {
1762
+ code: "FILESYSTEM_ERROR",
1763
+ scope: "project",
1764
+ path: `messages/${locale}/${fileName}.json`,
1765
+ hint: error instanceof Error ? error.message : void 0
1766
+ }
1767
+ );
1768
+ }
384
1769
  }
1770
+ let cursor = data;
1771
+ for (const segment of keySegments.slice(0, -1)) {
1772
+ const current = cursor[segment];
1773
+ if (current === void 0) cursor[segment] = {};
1774
+ else if (!current || typeof current !== "object" || Array.isArray(current)) {
1775
+ throw new CliError(
1776
+ `Translation segment "${segment}" is not an object in ${locale}.`,
1777
+ {
1778
+ code: "INVALID_ARGUMENT",
1779
+ scope: "project",
1780
+ path: `messages/${locale}/${fileName}.json`
1781
+ }
1782
+ );
1783
+ }
1784
+ cursor = cursor[segment];
1785
+ }
1786
+ const previous = cursor[finalKey];
1787
+ cursor[finalKey] = text;
1788
+ prepared.push({
1789
+ locale,
1790
+ file,
1791
+ data,
1792
+ replaced: previous !== void 0 && previous !== text
1793
+ });
385
1794
  }
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}`);
1795
+ const gateway = new MutationGateway(context, context.cwd);
1796
+ for (const item of prepared) {
1797
+ await gateway.write(item.file, `${JSON.stringify(item.data, null, 2)}
1798
+ `, {
1799
+ role: "translation-messages",
1800
+ detail: {
1801
+ locale: item.locale,
1802
+ key: pathArg,
1803
+ replaced: item.replaced
1804
+ }
1805
+ });
392
1806
  }
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}'`)
1807
+ const mutated = context.operations.snapshot().some((event) => event.action === "created" || event.action === "updated");
1808
+ const nextSteps = mutated ? [
1809
+ ...locales.length > 1 ? [
1810
+ {
1811
+ kind: "translate",
1812
+ required: true,
1813
+ message: "Review the value in every locale; the same text was written to all locale files.",
1814
+ paths: prepared.map((item) => ({
1815
+ scope: "project",
1816
+ path: gateway.path(item.file)
1817
+ }))
1818
+ }
1819
+ ] : [],
1820
+ {
1821
+ kind: "run-checks",
1822
+ required: true,
1823
+ message: "Run the project checks.",
1824
+ paths: [],
1825
+ commands: ["bun run check", "npm run check", "pnpm run check"]
1826
+ }
1827
+ ] : [];
1828
+ return commandResult(context, {
1829
+ command: "addtext",
1830
+ summary: mutated ? `Set translation "${pathArg}" to "${text}" in ${locales.length} locale files.` : `Translation "${pathArg}" already has value "${text}" in every locale.`,
1831
+ projectRoot: context.cwd,
1832
+ nextSteps
1833
+ });
1834
+ };
1835
+
1836
+ // src/lib/rmPage.ts
1837
+ import path7 from "path";
1838
+ function escapeRegExp(value) {
1839
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1840
+ }
1841
+ function unregisterMessagesFile(content, locale, fileName) {
1842
+ const identifier = toIdentifier(fileName);
1843
+ const escapedIdentifier = escapeRegExp(identifier);
1844
+ const escapedPath = escapeRegExp(`./${locale}/${fileName}.json`);
1845
+ const importPattern = new RegExp(
1846
+ `^import\\s+${escapedIdentifier}\\s+from\\s+["']${escapedPath}["'];?\\r?\\n?`,
1847
+ "m"
1848
+ );
1849
+ const propertyPattern = new RegExp(
1850
+ `^\\s*${escapedIdentifier},\\s*\\r?\\n?`,
1851
+ "m"
1852
+ );
1853
+ const hasImport = importPattern.test(content);
1854
+ const hasProperty = propertyPattern.test(content);
1855
+ if (hasImport !== hasProperty) {
1856
+ throw new CliError(
1857
+ `Locale aggregator messages/${locale}.ts is inconsistent for ${fileName}.json.`,
1858
+ {
1859
+ code: "INCONSISTENT_LOCALE",
1860
+ scope: "project",
1861
+ path: `messages/${locale}.ts`
1862
+ }
397
1863
  );
398
- console.log(`\u{1F5D1}\uFE0F Deleted: ${appLocaleDir}`);
399
1864
  }
400
- console.log(`\u2705 Page "${pageName}" deleted.`);
1865
+ if (!hasImport) return { content, changed: false };
1866
+ return {
1867
+ content: content.replace(importPattern, "").replace(propertyPattern, ""),
1868
+ changed: true
1869
+ };
401
1870
  }
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;
1871
+ var rmPage = async (args, context) => {
1872
+ const candidates = await discoverPages(context.cwd, context.fs);
1873
+ let logicalName = args[1];
1874
+ if (!logicalName || logicalName.startsWith("-")) {
1875
+ if (context.outputMode === "json") {
1876
+ throw new CliError("Page name is required in JSON mode.", {
1877
+ code: "INTERACTIVE_INPUT_REQUIRED",
1878
+ hint: "Pass a page name returned by completion after rmpage."
1879
+ });
1880
+ }
1881
+ const selected = await context.prompt([
1882
+ {
1883
+ type: "autocomplete",
1884
+ name: "page",
1885
+ message: "Page to remove:",
1886
+ choices: candidates.map((candidate2) => ({
1887
+ title: candidate2.logicalName.replaceAll(".", " > "),
1888
+ value: candidate2.logicalName
1889
+ }))
1890
+ },
1891
+ {
1892
+ type: (value) => value ? "confirm" : null,
1893
+ name: "confirm",
1894
+ message: "Confirm page deletion?",
1895
+ initial: false
1896
+ }
1897
+ ]);
1898
+ if (!selected.confirm) {
1899
+ context.operations.record({
1900
+ action: "cancelled",
1901
+ resource: "command",
1902
+ role: "page-removal",
1903
+ scope: "project",
1904
+ path: "."
1905
+ });
1906
+ return commandResult(context, {
1907
+ command: "rmpage",
1908
+ summary: "Page deletion was cancelled.",
1909
+ projectRoot: context.cwd,
1910
+ status: "cancelled"
1911
+ });
1912
+ }
1913
+ logicalName = String(selected.page ?? "");
418
1914
  }
419
- let libName = libArg;
420
- let fileName = null;
421
- if (libArg.includes(".")) {
422
- [libName, fileName] = libArg.split(".");
1915
+ parseLogicalName(logicalName, "page name");
1916
+ const candidate = candidates.find(
1917
+ (entry) => entry.logicalName === logicalName
1918
+ );
1919
+ if (!candidate) {
1920
+ throw new CliError(`Page not found: ${logicalName}.`, {
1921
+ code: "TARGET_NOT_FOUND",
1922
+ scope: "project",
1923
+ path: logicalName.replaceAll(".", "/")
1924
+ });
423
1925
  }
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
- );
429
- return;
1926
+ const messagesRoot = resolveInside(context.cwd, "messages");
1927
+ const preparedMessages = [];
1928
+ const preparedAggregators = [];
1929
+ if (context.fs.exists(messagesRoot)) {
1930
+ const locales = (await context.fs.list(messagesRoot)).filter((entry) => entry.isDirectory).map((entry) => entry.name).sort();
1931
+ for (const locale of locales) {
1932
+ const target = resolveInside(
1933
+ context.cwd,
1934
+ "messages",
1935
+ locale,
1936
+ `${candidate.routeSegments[0]}.json`
1937
+ );
1938
+ if (!candidate.messageKey) {
1939
+ const aggregator = resolveInside(
1940
+ context.cwd,
1941
+ "messages",
1942
+ `${locale}.ts`
1943
+ );
1944
+ if (!context.fs.exists(aggregator)) {
1945
+ throw new CliError(
1946
+ `Locale aggregator messages/${locale}.ts was not found.`,
1947
+ {
1948
+ code: "CONFIG_NOT_FOUND",
1949
+ scope: "project",
1950
+ path: `messages/${locale}.ts`
1951
+ }
1952
+ );
1953
+ }
1954
+ const nextAggregator = unregisterMessagesFile(
1955
+ await context.fs.readText(aggregator),
1956
+ locale,
1957
+ candidate.routeSegments[0]
1958
+ );
1959
+ preparedMessages.push({
1960
+ target,
1961
+ keyPresent: context.fs.exists(target)
1962
+ });
1963
+ preparedAggregators.push({
1964
+ target: aggregator,
1965
+ ...nextAggregator
1966
+ });
1967
+ continue;
1968
+ }
1969
+ if (!context.fs.exists(target)) continue;
1970
+ let data;
1971
+ try {
1972
+ data = JSON.parse(await context.fs.readText(target));
1973
+ } catch {
1974
+ throw new CliError(
1975
+ `Invalid JSON prevents removal from ${path7.relative(context.cwd, target)}.`,
1976
+ {
1977
+ code: "FILESYSTEM_ERROR",
1978
+ scope: "project",
1979
+ path: path7.relative(context.cwd, target)
1980
+ }
1981
+ );
1982
+ }
1983
+ const keyPresent = Object.hasOwn(data, candidate.messageKey);
1984
+ if (keyPresent) delete data[candidate.messageKey];
1985
+ preparedMessages.push({
1986
+ target,
1987
+ keyPresent,
1988
+ content: `${JSON.stringify(data, null, 2)}
1989
+ `
1990
+ });
1991
+ }
430
1992
  }
431
- const libDir = join5(process.cwd(), "src", "lib", libName);
432
- if (!existsSync5(libDir)) {
433
- await mkdir3(libDir, { recursive: true });
1993
+ const gateway = new MutationGateway(context, context.cwd);
1994
+ for (const prepared of preparedMessages) {
1995
+ if (!candidate.messageKey) {
1996
+ await gateway.remove(prepared.target, { role: "translation-messages" });
1997
+ } else if (prepared.keyPresent) {
1998
+ await gateway.write(prepared.target, prepared.content, {
1999
+ role: "translation-messages",
2000
+ detail: { removedKey: candidate.messageKey }
2001
+ });
2002
+ } else {
2003
+ gateway.unchanged(prepared.target, {
2004
+ role: "translation-messages",
2005
+ detail: { missingKey: candidate.messageKey }
2006
+ });
2007
+ }
434
2008
  }
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);
2009
+ for (const prepared of preparedAggregators) {
2010
+ if (prepared.changed) {
2011
+ await gateway.write(prepared.target, prepared.content, {
2012
+ role: "locale-aggregator"
2013
+ });
447
2014
  } else {
448
- await writeFile4(indexPath, "export {}\n");
2015
+ gateway.unchanged(prepared.target, { role: "locale-aggregator" });
449
2016
  }
450
- console.log(`\u{1F4C4} File created: ${indexPath}`);
451
2017
  }
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
2018
+ await gateway.remove(
2019
+ candidate.uiDirectory,
2020
+ {
2021
+ role: "page-ui",
2022
+ resource: "directory"
2023
+ },
2024
+ { recursive: true, force: false }
2025
+ );
2026
+ await gateway.remove(
2027
+ candidate.routeDirectory,
2028
+ {
2029
+ role: "page-route",
2030
+ resource: "directory"
2031
+ },
2032
+ { recursive: true, force: false }
2033
+ );
2034
+ const mutated = context.operations.snapshot().some((event) => event.action === "deleted" || event.action === "updated");
2035
+ return commandResult(context, {
2036
+ command: "rmpage",
2037
+ summary: mutated ? `Deleted page "${logicalName}" and its associated resources.` : `No resources remained for page "${logicalName}".`,
2038
+ projectRoot: context.cwd,
2039
+ nextSteps: mutated ? [
2040
+ {
2041
+ kind: "run-checks",
2042
+ required: true,
2043
+ message: "Run the project checks after removing the page.",
2044
+ paths: [],
2045
+ commands: ["bun run check", "npm run check", "pnpm run check"]
2046
+ }
2047
+ ] : []
2048
+ });
2049
+ };
2050
+
2051
+ // src/cli/registry.ts
2052
+ function createCommandRegistry() {
2053
+ return /* @__PURE__ */ new Map([
2054
+ ["addcomponent", addComponent],
2055
+ ["addpage", addPage],
2056
+ ["addlib", addLib],
2057
+ ["addapi", addApi],
2058
+ ["addlanguage", addLanguage],
2059
+ ["addtext", addText],
2060
+ ["rmpage", rmPage]
2061
+ ]);
464
2062
  }
465
- `
2063
+
2064
+ // src/scaffold.ts
2065
+ import { join as join8, resolve } from "path";
2066
+ import { fileURLToPath } from "url";
2067
+
2068
+ // src/core/template-manifest.ts
2069
+ import path8 from "path";
2070
+ var TEMPLATE_DENY_NAMES = /* @__PURE__ */ new Set([
2071
+ ".env",
2072
+ ".git",
2073
+ ".agent",
2074
+ ".cursor",
2075
+ ".next",
2076
+ "node_modules",
2077
+ "artifacts",
2078
+ "coverage",
2079
+ "playwright-report",
2080
+ "test-results"
2081
+ ]);
2082
+ function isDistributableTemplatePath(relativePath) {
2083
+ const segments = relativePath.split(path8.sep);
2084
+ return !segments.some(
2085
+ (segment) => TEMPLATE_DENY_NAMES.has(segment) || segment.endsWith(".tsbuildinfo") || segment === ".DS_Store"
2086
+ );
2087
+ }
2088
+ async function templateManifest(root, fs) {
2089
+ const files = [];
2090
+ async function visit(directory, relativeDirectory = "") {
2091
+ const entries = await fs.list(directory);
2092
+ for (const entry of entries) {
2093
+ const relative = path8.join(relativeDirectory, entry.name);
2094
+ if (!isDistributableTemplatePath(relative)) continue;
2095
+ const source = path8.join(directory, entry.name);
2096
+ if (entry.isSymbolicLink) {
2097
+ throw new CliError(
2098
+ `Symbolic links are forbidden in templates: ${relative}`,
2099
+ { code: "UNSAFE_PATH", scope: "package", path: relative }
466
2100
  );
467
2101
  }
468
- console.log(`\u{1F4C4} File created: ${filePath}`);
2102
+ if (entry.isDirectory) await visit(source, relative);
2103
+ else if (entry.isFile) files.push(relative);
2104
+ else
2105
+ throw new CliError(`Unsupported template entry: ${relative}`, {
2106
+ code: "TEMPLATE_MISSING",
2107
+ scope: "package",
2108
+ path: relative
2109
+ });
469
2110
  }
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}";`
2111
+ }
2112
+ await visit(root);
2113
+ return files;
2114
+ }
2115
+ async function validateScaffoldTemplate(root, fs) {
2116
+ const manifest = await templateManifest(root, fs);
2117
+ for (const required of ["package.json", "tsconfig.json"]) {
2118
+ if (!manifest.includes(required)) {
2119
+ throw new CliError(`Required template file was not found: ${required}.`, {
2120
+ code: "TEMPLATE_MISSING",
2121
+ scope: "package",
2122
+ path: `templates/Projects/default/${required}`
2123
+ });
2124
+ }
2125
+ }
2126
+ try {
2127
+ JSON.parse(await fs.readText(path8.join(root, "package.json")));
2128
+ const tsconfig = JSON.parse(
2129
+ await fs.readText(path8.join(root, "tsconfig.json"))
474
2130
  );
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
- }
2131
+ if (!tsconfig.compilerOptions?.paths?.["@/*"]) throw new Error("alias");
2132
+ } catch {
2133
+ throw new CliError(
2134
+ 'The template manifests must be valid JSON and define the default "@/*" alias.',
2135
+ {
2136
+ code: "TEMPLATE_MISSING",
2137
+ scope: "package",
2138
+ path: "templates/Projects/default"
488
2139
  }
489
- }
490
- if (!imports.some((l) => importRegex.test(l))) {
491
- imports.push(importLine);
492
- }
493
- if (!exportsSet.includes(fileName)) {
494
- exportsSet.push(fileName);
495
- }
496
- indexContent = imports.join("\n") + "\n\nexport { " + exportsSet.join(", ") + " };\n";
497
- await writeFile4(indexPath, indexContent);
498
- console.log(`\u270F\uFE0F Updated index: ${indexPath}`);
2140
+ );
499
2141
  }
500
- console.log(
501
- `\u2705 Lib "${libName}"${fileName ? ` with module ${fileName}` : ""} added.`
502
- );
2142
+ return manifest;
503
2143
  }
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"
2144
+ async function copyTemplate(root, target, context, gateway, manifest) {
2145
+ const files = manifest ?? await templateManifest(root, context.fs);
2146
+ for (const relative of files) {
2147
+ const destination = path8.join(
2148
+ target,
2149
+ relative === ".gitignore.template" ? ".gitignore" : relative
2150
+ );
2151
+ const source = path8.join(root, relative);
2152
+ await gateway.copy(source, destination, {
2153
+ role: "template-file",
2154
+ source: { template: `Projects/default/${relative}` }
518
2155
  });
519
- apiName = response.apiName;
520
2156
  }
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."
2157
+ return files;
2158
+ }
2159
+ async function customizeGeneratedProject(target, projectName, importAlias, context, gateway) {
2160
+ const packagePath = path8.join(target, "package.json");
2161
+ const packageJson = JSON.parse(
2162
+ await context.fs.readText(packagePath)
2163
+ );
2164
+ packageJson.name = projectName.toLowerCase();
2165
+ delete packageJson.packageManager;
2166
+ await gateway.write(
2167
+ packagePath,
2168
+ `${JSON.stringify(packageJson, null, 2)}
2169
+ `,
2170
+ { role: "package-manifest" }
2171
+ );
2172
+ const tsconfigPath = path8.join(target, "tsconfig.json");
2173
+ const tsconfigContent = await context.fs.readText(tsconfigPath);
2174
+ const tsconfig = JSON.parse(tsconfigContent);
2175
+ if (!tsconfig.compilerOptions?.paths?.["@/*"]) {
2176
+ throw new CliError(
2177
+ 'The template tsconfig must define the default "@/*" alias.',
2178
+ {
2179
+ code: "TEMPLATE_MISSING",
2180
+ scope: "package",
2181
+ path: "templates/Projects/default/tsconfig.json"
2182
+ }
525
2183
  );
526
- return;
527
- }
528
- const apiDir = join6(process.cwd(), "src", "app", "api", apiName);
529
- if (!existsSync6(apiDir)) {
530
- await mkdir4(apiDir, { recursive: true });
531
2184
  }
532
- const templateDir = join6(
533
- new URL("..", import.meta.url).pathname,
534
- "templates",
535
- "Api"
2185
+ await gateway.write(
2186
+ tsconfigPath,
2187
+ tsconfigContent.replace('"@/*"', `"${importAlias}"`),
2188
+ { role: "typescript-configuration" }
536
2189
  );
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);
544
- } 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}" });
551
- }
552
- `
553
- );
2190
+ if (importAlias !== "@/*") {
2191
+ const prefix = importAlias.slice(0, -2);
2192
+ for (const relative of await templateManifest(target, context.fs)) {
2193
+ if (!/\.[cm]?[jt]sx?$/.test(relative)) continue;
2194
+ const file = path8.join(target, relative);
2195
+ const content = await context.fs.readText(file);
2196
+ const next = content.replaceAll('from "@/', `from "${prefix}/`).replaceAll("from '@/", `from '${prefix}/`);
2197
+ if (next !== content) {
2198
+ await gateway.write(file, next, { role: "import-alias-reference" });
2199
+ }
554
2200
  }
555
- console.log(`\u{1F4C4} File created: ${routePath}`);
556
- } else {
557
- console.log(`\u2139\uFE0F File already exists: ${routePath}`);
558
2201
  }
559
- console.log(`\u2705 API route "${apiName}" added.`);
560
- }
561
-
562
- // 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";
567
-
568
- // src/lib/helper/consoleColor.ts
569
- var RED = "\x1B[31m";
570
- var GREEN = "\x1B[32m";
571
- var CYAN = "\x1B[36m";
572
- var RESET = "\x1B[0m";
573
- function red(text) {
574
- return RED + text + RESET;
575
- }
576
- function green(text) {
577
- return GREEN + text + RESET;
578
- }
579
- function cyan(text) {
580
- return CYAN + text + RESET;
581
2202
  }
582
2203
 
583
2204
  // 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"
2205
+ async function scaffoldProject(options, runtime) {
2206
+ const requiredFeatures = [
2207
+ "useTypescript",
2208
+ "useEslint",
2209
+ "useTailwind",
2210
+ "useSrcDir",
2211
+ "useTurbopack",
2212
+ "useI18n"
2213
+ ];
2214
+ const unsupported = requiredFeatures.filter(
2215
+ (feature) => options[feature] !== true
2216
+ );
2217
+ if (unsupported.length > 0) {
2218
+ throw new CliError(
2219
+ `The default Next.js 16 template requires: ${unsupported.join(", ")}.`,
2220
+ { code: "INVALID_ARGUMENT" }
2221
+ );
2222
+ }
2223
+ const { context } = runtime;
2224
+ const cwd = context.cwd;
2225
+ const projectName = validateProjectName(options.projectName);
2226
+ const importAlias = normalizeImportAlias(
2227
+ options.customAlias === false ? "@/*" : options.importAlias || "@/*"
593
2228
  );
594
- if (existsSync7(targetPath)) {
2229
+ const targetPath = join8(cwd, projectName);
2230
+ const __dirname = new URL(".", import.meta.url);
2231
+ const templatePath = runtime.templatePath ?? join8(fileURLToPath(__dirname), "..", "templates", "Projects", "default");
2232
+ const resolvedCwd = resolve(cwd);
2233
+ const resolvedTarget = resolve(targetPath);
2234
+ if (!resolvedTarget.startsWith(`${resolvedCwd}/`)) {
2235
+ throw new CliError(
2236
+ "The project destination must be a child of the current directory.",
2237
+ { code: "UNSAFE_PATH" }
2238
+ );
2239
+ }
2240
+ const gateway = new MutationGateway(context, targetPath);
2241
+ const manifest = await validateScaffoldTemplate(templatePath, context.fs);
2242
+ if (context.fs.exists(targetPath)) {
595
2243
  if (options.force) {
596
- console.warn("\u26A0\uFE0F Target directory already exists, removing...");
597
- await rm(targetPath, { recursive: true, force: true });
2244
+ if (context.outputMode === "human") {
2245
+ context.terminal.warn(
2246
+ `WARNING: --force will remove the existing project at ${targetPath}.`
2247
+ );
2248
+ }
2249
+ await gateway.remove(
2250
+ targetPath,
2251
+ {
2252
+ role: "existing-project",
2253
+ resource: "project"
2254
+ },
2255
+ { recursive: true, force: true }
2256
+ );
598
2257
  } else {
599
- console.error(
600
- red("[X] Target directory already exists. Use --force to overwrite.")
2258
+ throw new CliError(
2259
+ "Target directory already exists. Use --force to overwrite it.",
2260
+ {
2261
+ code: "TARGET_EXISTS",
2262
+ scope: "project",
2263
+ path: "."
2264
+ }
601
2265
  );
602
- process.exit(1);
603
2266
  }
604
2267
  }
605
- 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));
2268
+ await gateway.mkdir(targetPath, {
2269
+ resource: "project",
2270
+ role: "project-root"
2271
+ });
2272
+ await copyTemplate(templatePath, targetPath, context, gateway, manifest);
2273
+ await customizeGeneratedProject(
2274
+ targetPath,
2275
+ projectName,
2276
+ importAlias,
2277
+ context,
2278
+ gateway
2279
+ );
2280
+ await gateway.write(
2281
+ join8(targetPath, "cnp.config.json"),
2282
+ `${JSON.stringify({ ...options, projectName, importAlias }, null, 2)}
2283
+ `,
2284
+ {
2285
+ role: "project-configuration",
2286
+ resource: "configuration",
2287
+ detail: { importAlias, packageManager: "user-selected" }
618
2288
  }
619
- await writeFile6(
620
- join7(targetPath, "cnp.config.json"),
621
- JSON.stringify(options, null, 2)
622
- );
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(
629
- "Then install dependencies and launch the dev server with your preferred tool:"
630
- );
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(
637
- " " + cyan("https://github.com/Rising-Corporation/create-next-pro-cli")
638
- );
639
- console.log(
640
- "_-`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`-_"
641
- );
642
- } catch (err) {
643
- console.error(red("[X] Error during project creation:"), err);
644
- process.exit(1);
645
- }
2289
+ );
2290
+ return { projectRoot: targetPath, copiedFiles: manifest.length, importAlias };
646
2291
  }
647
2292
 
648
2293
  // src/lib/createProject.ts
649
- async function createProject(nameArg, force) {
2294
+ async function createProjectFromOptions(options, context) {
2295
+ const { projectRoot, copiedFiles, importAlias } = await scaffoldProject(
2296
+ options,
2297
+ {
2298
+ context
2299
+ }
2300
+ );
2301
+ return commandResult(context, {
2302
+ command: "create",
2303
+ summary: `Created project "${options.projectName}" with ${copiedFiles} template files and import alias ${importAlias}; no package manager was pinned.`,
2304
+ projectRoot,
2305
+ nextSteps: [
2306
+ {
2307
+ kind: "install",
2308
+ required: true,
2309
+ message: "Install dependencies with your preferred package manager.",
2310
+ paths: [],
2311
+ commands: [
2312
+ `cd ${options.projectName} && bun install`,
2313
+ `cd ${options.projectName} && npm install`,
2314
+ `cd ${options.projectName} && pnpm install`
2315
+ ]
2316
+ },
2317
+ {
2318
+ kind: "run-checks",
2319
+ required: true,
2320
+ message: "Run the generated project checks before development.",
2321
+ paths: [{ scope: "project", path: "package.json" }],
2322
+ commands: ["bun run check", "npm run check", "pnpm run check"]
2323
+ }
2324
+ ],
2325
+ data: {
2326
+ projectName: options.projectName,
2327
+ importAlias,
2328
+ packageManager: null,
2329
+ copiedFiles
2330
+ }
2331
+ });
2332
+ }
2333
+ async function createProject(nameArg, force, context) {
650
2334
  const response = {
651
2335
  projectName: nameArg,
652
2336
  useTypescript: true,
@@ -659,14 +2343,12 @@ async function createProject(nameArg, force) {
659
2343
  importAlias: "@/*",
660
2344
  force
661
2345
  };
662
- console.log(`Creating project "${response.projectName}"...`);
663
- await scaffoldProject(response);
2346
+ return createProjectFromOptions(response, context);
664
2347
  }
665
2348
 
666
2349
  // src/lib/createProjectWithPrompt.ts
667
- import prompts6 from "prompts";
668
- async function createProjectWithPrompt() {
669
- const response = await prompts6.prompt([
2350
+ async function createProjectWithPrompt(context) {
2351
+ const response = await context.prompt([
670
2352
  {
671
2353
  type: "text",
672
2354
  name: "projectName",
@@ -736,314 +2418,252 @@ async function createProjectWithPrompt() {
736
2418
  initial: "@core/*"
737
2419
  }
738
2420
  ]);
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 }))
2421
+ if (!response.projectName) {
2422
+ context.operations.record({
2423
+ action: "cancelled",
2424
+ resource: "command",
2425
+ role: "project-creation",
2426
+ scope: "project",
2427
+ path: "."
2428
+ });
2429
+ return commandResult(context, {
2430
+ command: "create",
2431
+ summary: "Project creation was cancelled.",
2432
+ projectRoot: context.cwd,
2433
+ status: "cancelled"
785
2434
  });
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
807
- });
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
2435
  }
820
- console.log(
821
- `\u2705 Locale "${locale}" added and copied from default locale "${defaultLocale}".`
822
- );
2436
+ const options = response;
2437
+ return createProjectFromOptions(options, context);
823
2438
  }
824
2439
 
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];
2440
+ // src/runtime/node-context.ts
2441
+ import { existsSync } from "fs";
2442
+ import {
2443
+ appendFile,
2444
+ copyFile,
2445
+ lstat,
2446
+ mkdir,
2447
+ readdir,
2448
+ readFile,
2449
+ rm,
2450
+ writeFile
2451
+ } from "fs/promises";
2452
+ import os from "os";
2453
+ import path9 from "path";
2454
+ import { fileURLToPath as fileURLToPath2 } from "url";
2455
+ import prompts from "prompts";
2456
+ function findPackageRoot(start) {
2457
+ let current = start;
2458
+ while (true) {
2459
+ const packagePath = path9.join(current, "package.json");
2460
+ if (existsSync(packagePath)) return current;
2461
+ const parent = path9.dirname(current);
2462
+ if (parent === current) {
2463
+ throw new Error(`Unable to locate package.json from ${start}`);
873
2464
  }
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
- }
880
-
881
- // 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;
2465
+ current = parent;
889
2466
  }
890
2467
  }
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}"`);
2468
+ function resolvePackageRoot(metaUrl = import.meta.url) {
2469
+ return findPackageRoot(path9.dirname(fileURLToPath2(metaUrl)));
917
2470
  }
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
2471
+ function createNodeContext(overrides = {}) {
2472
+ return {
2473
+ argv: process.argv.slice(2),
2474
+ cwd: process.cwd(),
2475
+ env: process.env,
2476
+ homeDir: os.homedir(),
2477
+ packageRoot: resolvePackageRoot(),
2478
+ terminal: console,
2479
+ prompt: prompts,
2480
+ fs: {
2481
+ exists: existsSync,
2482
+ readText: (target) => readFile(target, "utf8"),
2483
+ writeText: async (target, content) => {
2484
+ await writeFile(target, content);
937
2485
  },
938
- {
939
- type: "toggle",
940
- name: "completion",
941
- message: "Install autocompletion?",
942
- initial: true,
943
- active: "Yes",
944
- inactive: "No"
2486
+ mkdir: async (target) => {
2487
+ await mkdir(target, { recursive: true });
2488
+ },
2489
+ copyFile: async (source, target) => {
2490
+ await copyFile(source, target);
2491
+ },
2492
+ appendText: async (target, content) => {
2493
+ await appendFile(target, content);
2494
+ },
2495
+ remove: async (target, options) => {
2496
+ await rm(target, options);
2497
+ },
2498
+ inspect: async (target) => {
2499
+ try {
2500
+ const stats = await lstat(target);
2501
+ return {
2502
+ name: target,
2503
+ isFile: stats.isFile(),
2504
+ isDirectory: stats.isDirectory(),
2505
+ isSymbolicLink: stats.isSymbolicLink()
2506
+ };
2507
+ } catch (error) {
2508
+ if (error.code === "ENOENT") return null;
2509
+ throw error;
2510
+ }
2511
+ },
2512
+ list: async (target) => {
2513
+ const entries = await readdir(target, { withFileTypes: true });
2514
+ return Promise.all(
2515
+ entries.sort((left, right) => left.name.localeCompare(right.name)).map(async (entry) => {
2516
+ const stats = await lstat(path9.join(target, entry.name));
2517
+ return {
2518
+ name: entry.name,
2519
+ isFile: stats.isFile(),
2520
+ isDirectory: stats.isDirectory(),
2521
+ isSymbolicLink: stats.isSymbolicLink()
2522
+ };
2523
+ })
2524
+ );
945
2525
  }
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()
2526
+ },
2527
+ operations: new OperationJournal(),
2528
+ outputMode: "human",
2529
+ ...overrides
955
2530
  };
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
2531
  }
970
- function showHelp() {
971
- console.log(`create-next-pro
2532
+
2533
+ // src/index.ts
2534
+ var HELP_TEXT = `create-next-pro
972
2535
 
973
2536
  Usage:
974
- create-next-pro <project-name> [--force]
975
- create-next-pro addpage [options]
976
- create-next-pro addcomponent [options]
977
- create-next-pro addlib [name]
978
- create-next-pro addapi [name]
979
- create-next-pro addlanguage [locale]
980
- create-next-pro addtext <path> [text]
981
- create-next-pro rmpage [options]
2537
+ create-next-pro <project-name> [--force] [--json]
2538
+ create-next-pro addpage [options] [--json]
2539
+ create-next-pro addcomponent [options] [--json]
2540
+ create-next-pro addlib [name] [--json]
2541
+ create-next-pro addapi [name] [--json]
2542
+ create-next-pro addlanguage [locale] [--json]
2543
+ create-next-pro addtext <path> [text] [--json]
2544
+ create-next-pro rmpage [options] [--json]
982
2545
 
983
2546
  Options:
984
2547
  --help Show this help message
2548
+ --version Show the CLI version
2549
+ --json Emit one deterministic JSON document
985
2550
  --reconfigure Run the configuration assistant again
986
- `);
2551
+ `;
2552
+ function parseOptions(args) {
2553
+ return {
2554
+ force: args.includes("--force"),
2555
+ help: args.includes("--help"),
2556
+ version: args.includes("--version") || args.includes("-v"),
2557
+ reconfigure: args.includes("--reconfigure"),
2558
+ json: args.includes("--json")
2559
+ };
987
2560
  }
988
- function showVersion() {
989
- const pkg = JSON.parse(packageJson);
990
- console.log(`v${pkg.version}`);
2561
+ function withoutGlobalOptions(args) {
2562
+ return args.filter((argument) => argument !== "--json");
991
2563
  }
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;
2564
+ async function packageVersion(context) {
2565
+ const packageJson = JSON.parse(
2566
+ await context.fs.readText(path10.join(context.packageRoot, "package.json"))
2567
+ );
2568
+ return packageJson.version;
2569
+ }
2570
+ function interactiveInputError(message, hint) {
2571
+ throw new CliError(message, {
2572
+ code: "INTERACTIVE_INPUT_REQUIRED",
2573
+ hint
2574
+ });
2575
+ }
2576
+ async function executeCli(context) {
2577
+ const options = parseOptions(context.argv);
2578
+ context.outputMode = options.json ? "json" : "human";
2579
+ const args = withoutGlobalOptions(context.argv);
2580
+ const version = await packageVersion(context);
2581
+ if (options.help) {
2582
+ return commandResult(context, {
2583
+ command: "help",
2584
+ summary: "Displayed create-next-pro help.",
2585
+ projectRoot: context.cwd,
2586
+ status: "success",
2587
+ data: { help: HELP_TEXT }
2588
+ });
1018
2589
  }
1019
- if (args[0] === "addlib") {
1020
- addLib(args);
1021
- return;
2590
+ if (options.version) {
2591
+ return commandResult(context, {
2592
+ command: "version",
2593
+ summary: `create-next-pro version ${version}.`,
2594
+ projectRoot: context.cwd,
2595
+ status: "success",
2596
+ data: { version }
2597
+ });
1022
2598
  }
1023
- if (args[0] === "addapi") {
1024
- addApi(args);
1025
- return;
2599
+ const config = await readConfig(context);
2600
+ if (options.reconfigure) {
2601
+ if (options.json) {
2602
+ interactiveInputError(
2603
+ "Reconfiguration is interactive and unavailable in JSON mode.",
2604
+ "Run create-next-pro --reconfigure without --json."
2605
+ );
2606
+ }
2607
+ return onboarding(context, version);
1026
2608
  }
1027
- if (args[0] === "addlanguage") {
1028
- await addLanguage(args);
1029
- return;
2609
+ if (!config) {
2610
+ if (options.json) {
2611
+ throw new CliError("Initial configuration is required.", {
2612
+ code: "ONBOARDING_REQUIRED",
2613
+ scope: "config",
2614
+ path: "config.json",
2615
+ hint: "Run create-next-pro once without --json, then rerun the command."
2616
+ });
2617
+ }
2618
+ return onboarding(context, version);
1030
2619
  }
1031
- if (args[0] === "addtext") {
1032
- await addText(args);
1033
- return;
2620
+ const command = args[0];
2621
+ const handler = command ? createCommandRegistry().get(command) : void 0;
2622
+ if (handler) return handler(args, context);
2623
+ const nameArg = args.find(
2624
+ (argument) => !argument.startsWith("--") && argument !== "-v"
2625
+ );
2626
+ if (nameArg) return createProject(nameArg, options.force, context);
2627
+ if (options.json) {
2628
+ interactiveInputError(
2629
+ "A project name is required in JSON mode.",
2630
+ "Pass the project name as a positional argument."
2631
+ );
1034
2632
  }
1035
- if (args[0] === "rmpage") {
1036
- rmPage(args);
1037
- return;
2633
+ return createProjectWithPrompt(context);
2634
+ }
2635
+ async function runCompletion(context, args) {
2636
+ try {
2637
+ for (const candidate of await completionCandidates(args[1], context)) {
2638
+ context.terminal.log(candidate);
2639
+ }
2640
+ return 0;
2641
+ } catch {
2642
+ return 1;
1038
2643
  }
1039
- const nameArg = args.find((arg) => !arg.startsWith("--"));
1040
- if (nameArg) {
1041
- createProject(nameArg, force);
1042
- return;
2644
+ }
2645
+ async function main(context = createNodeContext()) {
2646
+ const completionArgs = withoutGlobalOptions(context.argv);
2647
+ if (completionArgs[0] === "__complete") {
2648
+ return runCompletion(context, completionArgs);
2649
+ }
2650
+ context.operations.reset();
2651
+ context.outputMode = parseOptions(context.argv).json ? "json" : "human";
2652
+ let result;
2653
+ try {
2654
+ result = await executeCli(context);
2655
+ } catch (error) {
2656
+ const command = completionArgs[0] ?? "create";
2657
+ result = failedResult(context, command, error, {
2658
+ projectRoot: context.cwd,
2659
+ configRoot: configDirectory(context),
2660
+ homeRoot: context.homeDir
2661
+ });
1043
2662
  }
1044
- await createProjectWithPrompt();
2663
+ renderResult(result, context, context.outputMode);
2664
+ return result.exitCode;
1045
2665
  }
1046
2666
 
1047
2667
  // bin.node.ts
1048
- main();
2668
+ process.exitCode = await main();
1049
2669
  //# sourceMappingURL=bin.node.js.map