create-next-pro-cli 0.1.27 → 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.
package/dist/bin.node.js CHANGED
@@ -1,131 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import path8 from "path";
5
-
6
- // src/cli/onboarding.ts
7
- import path from "path";
8
- function configDirectory(context) {
9
- return context.env.XDG_CONFIG_HOME ? path.join(context.env.XDG_CONFIG_HOME, "create-next-pro") : path.join(context.homeDir, ".config", "create-next-pro");
10
- }
11
- function configFile(context) {
12
- return path.join(configDirectory(context), "config.json");
13
- }
14
- async function readConfig(context) {
15
- try {
16
- return JSON.parse(
17
- await context.fs.readText(configFile(context))
18
- );
19
- } catch {
20
- return null;
21
- }
22
- }
23
- async function ensureLineInRc(context, target, line) {
24
- try {
25
- const current = context.fs.exists(target) ? await context.fs.readText(target) : "";
26
- if (!current.includes(line))
27
- await context.fs.appendText(target, `
28
- ${line}
29
- `);
30
- } catch {
31
- }
32
- }
33
- async function installCompletion(context, shell) {
34
- const directory = configDirectory(context);
35
- const source = path.join(
36
- context.packageRoot,
37
- shell === "zsh" ? "create-next-pro-completion.zsh" : "create-next-pro-completion.sh"
38
- );
39
- const target = path.join(
40
- directory,
41
- `completion.${shell === "zsh" ? "zsh" : "sh"}`
42
- );
43
- await context.fs.mkdir(directory);
44
- await context.fs.copyFile(source, target);
45
- const rcFile = path.join(
46
- context.homeDir,
47
- shell === "zsh" ? ".zshrc" : ".bashrc"
48
- );
49
- await ensureLineInRc(context, rcFile, `source "${target}"`);
50
- }
51
- async function onboarding(context, version) {
52
- context.terminal.log(`\u{1F680} Welcome to create-next-pro v${version}
53
- `);
54
- const response = await context.prompt(
55
- [
56
- {
57
- type: "select",
58
- name: "shell",
59
- message: "Which shell do you use?",
60
- choices: [
61
- { title: "zsh", value: "zsh" },
62
- { title: "bash", value: "bash" }
63
- ],
64
- initial: context.env.SHELL?.includes("zsh") ? 0 : 1
65
- },
66
- {
67
- type: "toggle",
68
- name: "completion",
69
- message: "Install autocompletion?",
70
- initial: true,
71
- active: "Yes",
72
- inactive: "No"
73
- }
74
- ],
75
- { onCancel: () => false }
76
- );
77
- if (response.shell !== "bash" && response.shell !== "zsh") {
78
- throw new Error("Configuration cancelled.");
79
- }
80
- const now = (/* @__PURE__ */ new Date()).toISOString();
81
- const config = {
82
- version: 1,
83
- shell: response.shell,
84
- completionInstalled: Boolean(response.completion),
85
- createdAt: now,
86
- updatedAt: now
87
- };
88
- if (config.completionInstalled)
89
- await installCompletion(context, config.shell);
90
- await context.fs.mkdir(configDirectory(context));
91
- await context.fs.writeText(
92
- configFile(context),
93
- JSON.stringify(config, null, 2)
94
- );
95
- context.terminal.log("\n\u2705 Configuration saved.");
96
- context.terminal.log("you can now use the CLI ! ex : ");
97
- context.terminal.log(" Without prompt (will change in future) :");
98
- context.terminal.log(" create-next-pro my-next-project");
99
- context.terminal.log(" With prompt :");
100
- context.terminal.log(" create-next-pro");
101
- context.terminal.log(
102
- "For more information, visit: https://github.com/Rising-Corporation/create-next-pro-cli"
103
- );
104
- context.terminal.log("Happy coding! \u{1F389}");
105
- return config;
106
- }
4
+ import path10 from "path";
107
5
 
108
6
  // src/cli/completion.ts
109
- import { readdir as readdir2 } from "fs/promises";
110
- import path3 from "path";
7
+ import path2 from "path";
111
8
 
112
9
  // src/core/page-catalog.ts
113
- import { readdir } from "fs/promises";
114
- import path2 from "path";
10
+ import path from "path";
115
11
  function isRouteGroup(segment) {
116
12
  return segment.startsWith("(") && segment.endsWith(")");
117
13
  }
118
- async function discoverPages(projectRoot) {
119
- const appRoot = path2.join(projectRoot, "src", "app", "[locale]");
14
+ async function discoverPages(projectRoot, fs) {
15
+ const appRoot = path.join(projectRoot, "src", "app", "[locale]");
120
16
  const candidates = [];
121
17
  async function visit(directory, relative = []) {
122
18
  let entries;
123
19
  try {
124
- entries = await readdir(directory, { withFileTypes: true });
20
+ entries = await fs.list(directory);
125
21
  } catch {
126
22
  return;
127
23
  }
128
- if (entries.some((entry) => entry.isFile() && entry.name === "page.tsx")) {
24
+ if (entries.some((entry) => entry.isFile && entry.name === "page.tsx")) {
129
25
  const routeSegments = relative.filter(
130
26
  (segment) => !isRouteGroup(segment)
131
27
  );
@@ -137,8 +33,8 @@ async function discoverPages(projectRoot) {
137
33
  logicalName,
138
34
  routeSegments,
139
35
  routeDirectory: directory,
140
- uiDirectory: path2.join(projectRoot, "src", "ui", ...routeSegments),
141
- messageFile: path2.join(
36
+ uiDirectory: path.join(projectRoot, "src", "ui", ...routeSegments),
37
+ messageFile: path.join(
142
38
  projectRoot,
143
39
  "messages",
144
40
  "{locale}",
@@ -149,8 +45,8 @@ async function discoverPages(projectRoot) {
149
45
  }
150
46
  }
151
47
  for (const entry of entries) {
152
- if (entry.isDirectory() && !entry.name.startsWith(".")) {
153
- await visit(path2.join(directory, entry.name), [
48
+ if (entry.isDirectory && !entry.name.startsWith(".")) {
49
+ await visit(path.join(directory, entry.name), [
154
50
  ...relative,
155
51
  entry.name
156
52
  ]);
@@ -174,6 +70,7 @@ var PUBLIC_COMMANDS = [
174
70
  "rmpage",
175
71
  "--help",
176
72
  "--version",
73
+ "--json",
177
74
  "--reconfigure"
178
75
  ];
179
76
  var OPTIONS = {
@@ -190,9 +87,9 @@ var OPTIONS = {
190
87
  ],
191
88
  addcomponent: ["--page", "-P"]
192
89
  };
193
- async function directories(root) {
90
+ async function directories(root, context) {
194
91
  try {
195
- return (await readdir2(root, { withFileTypes: true })).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_")).map((entry) => entry.name).sort();
92
+ return (await context.fs.list(root)).filter((entry) => entry.isDirectory && !entry.name.startsWith("_")).map((entry) => entry.name).sort();
196
93
  } catch {
197
94
  return [];
198
95
  }
@@ -200,59 +97,466 @@ async function directories(root) {
200
97
  async function completionCandidates(command, context) {
201
98
  if (!command) return [...PUBLIC_COMMANDS];
202
99
  if (command === "rmpage") {
203
- return (await discoverPages(context.cwd)).map(
100
+ return (await discoverPages(context.cwd, context.fs)).map(
204
101
  (candidate) => candidate.logicalName
205
102
  );
206
103
  }
207
104
  if (command === "addcomponent" || command === "addpage") {
208
105
  return [
209
106
  ...OPTIONS[command] ?? [],
210
- ...await directories(path3.join(context.cwd, "src", "ui"))
107
+ ...await directories(path2.join(context.cwd, "src", "ui"), context)
211
108
  ];
212
109
  }
213
110
  if (command === "addlanguage")
214
111
  return ["de", "en", "es", "fr", "it", "ja", "pt"];
215
112
  return OPTIONS[command] ?? [];
216
113
  }
217
- async function printCompletions(args, context) {
218
- for (const candidate of await completionCandidates(args[1], context)) {
219
- context.terminal.log(candidate);
220
- }
221
- }
114
+
115
+ // src/cli/onboarding.ts
116
+ import path4 from "path";
117
+
118
+ // src/core/operations.ts
119
+ import path3 from "path";
222
120
 
223
121
  // src/core/contracts.ts
224
- var success = () => ({ exitCode: 0 });
225
122
  var CliError = class extends Error {
226
- constructor(message, exitCode = 1) {
123
+ exitCode;
124
+ code;
125
+ hint;
126
+ scope;
127
+ path;
128
+ constructor(message, options = {}) {
227
129
  super(message);
228
- this.exitCode = exitCode;
229
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
+ });
230
307
  }
231
- exitCode;
232
308
  };
233
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
+
234
537
  // src/lib/addApi.ts
235
538
  import { join as join2 } from "path";
236
- import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
237
- import prompts2 from "prompts";
238
- import { existsSync as existsSync3 } from "fs";
239
539
 
240
540
  // src/lib/utils.ts
241
- import { readFile } from "fs/promises";
242
- import { existsSync } from "fs";
243
541
  import { join } from "path";
244
542
  function capitalize(str) {
245
543
  return str.charAt(0).toUpperCase() + str.slice(1);
246
544
  }
545
+ function toIdentifier(value) {
546
+ return value.replace(
547
+ /[-_]+([A-Za-z0-9])/g,
548
+ (_, character) => character.toUpperCase()
549
+ );
550
+ }
247
551
  function configuredAliasPrefix(config) {
248
552
  const alias = config.importAlias ?? "@/*";
249
553
  return alias.endsWith("/*") ? alias.slice(0, -2) : "@";
250
554
  }
251
- async function loadConfig(cwd = process.cwd()) {
252
- const configPath = join(cwd, "cnp.config.json");
253
- if (!existsSync(configPath)) return null;
555
+ async function loadConfig(context) {
556
+ const configPath = join(context.cwd, "cnp.config.json");
557
+ if (!context.fs.exists(configPath)) return null;
254
558
  try {
255
- const raw = await readFile(configPath, "utf-8");
559
+ const raw = await context.fs.readText(configPath);
256
560
  return JSON.parse(raw);
257
561
  } catch {
258
562
  return null;
@@ -283,66 +587,8 @@ function toFileName(key) {
283
587
  }
284
588
  }
285
589
 
286
- // src/runtime/node-context.ts
287
- import { existsSync as existsSync2 } from "fs";
288
- import {
289
- appendFile,
290
- copyFile,
291
- mkdir,
292
- readFile as readFile2,
293
- writeFile
294
- } from "fs/promises";
295
- import os from "os";
296
- import path4 from "path";
297
- import { fileURLToPath } from "url";
298
- import prompts from "prompts";
299
- function findPackageRoot(start) {
300
- let current = start;
301
- while (true) {
302
- const packagePath = path4.join(current, "package.json");
303
- if (existsSync2(packagePath)) return current;
304
- const parent = path4.dirname(current);
305
- if (parent === current) {
306
- throw new Error(`Unable to locate package.json from ${start}`);
307
- }
308
- current = parent;
309
- }
310
- }
311
- function resolvePackageRoot(metaUrl = import.meta.url) {
312
- return findPackageRoot(path4.dirname(fileURLToPath(metaUrl)));
313
- }
314
- function createNodeContext(overrides = {}) {
315
- return {
316
- argv: process.argv.slice(2),
317
- cwd: process.cwd(),
318
- env: process.env,
319
- homeDir: os.homedir(),
320
- packageRoot: resolvePackageRoot(),
321
- terminal: console,
322
- prompt: prompts,
323
- fs: {
324
- exists: existsSync2,
325
- readText: (target) => readFile2(target, "utf8"),
326
- writeText: async (target, content) => {
327
- await writeFile(target, content);
328
- },
329
- mkdir: async (target) => {
330
- await mkdir(target, { recursive: true });
331
- },
332
- copyFile: async (source, target) => {
333
- await copyFile(source, target);
334
- },
335
- appendText: async (target, content) => {
336
- await appendFile(target, content);
337
- }
338
- },
339
- ...overrides
340
- };
341
- }
342
-
343
590
  // src/core/project-paths.ts
344
- import path5 from "path";
345
- import { lstat } from "fs/promises";
591
+ import path6 from "path";
346
592
  var SAFE_SEGMENT = /^[A-Za-z][A-Za-z0-9_-]*$/;
347
593
  function hasControlCharacters(value) {
348
594
  return [...value].some((character) => {
@@ -353,56 +599,58 @@ function hasControlCharacters(value) {
353
599
  function parseLogicalName(value, label = "name") {
354
600
  if (!value || hasControlCharacters(value)) {
355
601
  throw new CliError(
356
- `Invalid ${label}: a non-empty printable value is required.`
602
+ `Invalid ${label}: a non-empty printable value is required.`,
603
+ { code: "INVALID_ARGUMENT" }
357
604
  );
358
605
  }
359
- if (path5.isAbsolute(value) || value.includes("/") || value.includes("\\")) {
606
+ if (path6.isAbsolute(value) || value.includes("/") || value.includes("\\")) {
360
607
  throw new CliError(
361
- `Invalid ${label}: paths and separators are not allowed.`
608
+ `Invalid ${label}: paths and separators are not allowed.`,
609
+ { code: "INVALID_ARGUMENT" }
362
610
  );
363
611
  }
364
612
  const segments = value.split(".");
365
613
  if (segments.some((segment) => !SAFE_SEGMENT.test(segment))) {
366
614
  throw new CliError(
367
- `Invalid ${label}: use dot-separated alphanumeric segments beginning with a letter.`
615
+ `Invalid ${label}: use dot-separated alphanumeric segments beginning with a letter.`,
616
+ { code: "INVALID_ARGUMENT" }
368
617
  );
369
618
  }
370
619
  return segments;
371
620
  }
372
621
  function validateProjectName(value) {
373
- if (!value || hasControlCharacters(value) || path5.isAbsolute(value) || value === "." || value === ".." || value.includes("/") || value.includes("\\") || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value)) {
622
+ if (!value || hasControlCharacters(value) || path6.isAbsolute(value) || value === "." || value === ".." || value.includes("/") || value.includes("\\") || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value)) {
374
623
  throw new CliError(
375
- "Invalid project name: use letters, numbers, dots, dashes or underscores without path separators."
624
+ "Invalid project name: use letters, numbers, dots, dashes or underscores without path separators.",
625
+ { code: "INVALID_ARGUMENT" }
376
626
  );
377
627
  }
378
628
  return value;
379
629
  }
380
630
  function resolveInside(root, ...segments) {
381
- const absoluteRoot = path5.resolve(root);
382
- const target = path5.resolve(absoluteRoot, ...segments);
383
- if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${path5.sep}`)) {
631
+ const absoluteRoot = path6.resolve(root);
632
+ const target = path6.resolve(absoluteRoot, ...segments);
633
+ if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${path6.sep}`)) {
384
634
  throw new CliError(
385
- `Refusing to access a path outside the project: ${target}`
635
+ `Refusing to access a path outside the project: ${target}`,
636
+ { code: "UNSAFE_PATH" }
386
637
  );
387
638
  }
388
639
  return target;
389
640
  }
390
- async function assertSafeTarget(root, target) {
391
- const safeTarget = resolveInside(root, path5.relative(root, target));
392
- const relativeSegments = path5.relative(path5.resolve(root), safeTarget).split(path5.sep);
393
- let current = path5.resolve(root);
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);
394
645
  for (const segment of relativeSegments) {
395
646
  if (!segment) continue;
396
- current = path5.join(current, segment);
397
- try {
398
- if ((await lstat(current)).isSymbolicLink()) {
399
- throw new CliError(
400
- `Symbolic links are forbidden in project paths: ${current}`
401
- );
402
- }
403
- } catch (error) {
404
- if (error instanceof CliError) throw error;
405
- if (error.code !== "ENOENT") throw error;
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
+ );
406
654
  }
407
655
  }
408
656
  return safeTarget;
@@ -410,764 +658,1233 @@ async function assertSafeTarget(root, target) {
410
658
  function normalizeImportAlias(value) {
411
659
  if (!/^[A-Za-z@~][A-Za-z0-9@~_-]*\/\*$/.test(value)) {
412
660
  throw new CliError(
413
- 'Invalid import alias: expected a prefix followed by "/*" (for example "@/*" or "@core/*").'
661
+ 'Invalid import alias: expected a prefix followed by "/*" (for example "@/*" or "@core/*").',
662
+ { code: "INVALID_ARGUMENT" }
414
663
  );
415
664
  }
416
665
  return value;
417
666
  }
418
667
 
419
668
  // src/lib/addApi.ts
420
- async function addApi(args, cwd = process.cwd()) {
669
+ var addApi = async (args, context) => {
421
670
  let apiName = args[1];
422
671
  if (!apiName || apiName.startsWith("-")) {
423
- const response = await prompts2.prompt({
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({
424
679
  type: "text",
425
680
  name: "apiName",
426
- message: "\u{1F50C} API route name to add:",
681
+ message: "API route name to add:",
427
682
  validate: (name) => name ? true : "API route name is required"
428
683
  });
429
- apiName = response.apiName;
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
+ }
430
700
  }
431
701
  const apiSegments = parseLogicalName(apiName, "API route name");
432
- const config = await loadConfig(cwd);
702
+ const config = await loadConfig(context);
433
703
  if (!config) {
434
- console.error(
435
- "\u274C Configuration file cnp.config.json not found. Run this command from the project root."
436
- );
437
- return;
438
- }
439
- const apiDir = join2(cwd, "src", "app", "api", ...apiSegments);
440
- await assertSafeTarget(cwd, apiDir);
441
- if (!existsSync3(apiDir)) {
442
- await mkdir2(apiDir, { recursive: true });
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
+ });
443
708
  }
444
- const templateDir = join2(
445
- resolvePackageRoot(import.meta.url),
446
- "templates",
447
- "Api"
448
- );
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");
449
713
  const routeTemplate = join2(templateDir, "route.ts");
450
714
  const routePath = join2(apiDir, "route.ts");
451
- if (!existsSync3(routePath)) {
452
- if (existsSync3(routeTemplate)) {
453
- let content = await readFile3(routeTemplate, "utf-8");
454
- content = content.replace(/template/g, apiName);
455
- await writeFile2(routePath, content);
456
- } else {
457
- await writeFile2(
458
- routePath,
459
- `import { NextResponse } from "next/server";
460
-
461
- export async function GET() {
462
- return NextResponse.json({ message: "Hello from ${apiName}" });
463
- }
464
- `
465
- );
466
- }
467
- console.log(`\u{1F4C4} File created: ${routePath}`);
468
- } else {
469
- console.log(`\u2139\uFE0F File already exists: ${routePath}`);
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
+ });
470
721
  }
471
- console.log(`\u2705 API route "${apiName}" added.`);
472
- }
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
+ };
473
755
 
474
756
  // src/lib/addComponent.ts
475
757
  import { join as join3 } from "path";
476
- import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3, readdir as readdir3 } from "fs/promises";
477
- import prompts3 from "prompts";
478
- import { existsSync as existsSync4, statSync } from "fs";
479
- async function addComponent(args, cwd = process.cwd()) {
758
+ var addComponent = async (args, context) => {
480
759
  let componentName = args[1];
481
- let pageScope = null;
482
- const pageIndex = args.findIndex((arg) => arg === "-P" || arg === "--page");
483
- if (pageIndex !== -1 && args[pageIndex + 1]) {
484
- pageScope = args[pageIndex + 1];
485
- }
486
- let nestedPath = null;
487
- if (pageScope && pageScope.includes(".")) {
488
- nestedPath = join3(...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
+ });
489
768
  }
490
769
  if (!componentName || componentName.startsWith("-")) {
491
- const response = await prompts3.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({
492
777
  type: "text",
493
778
  name: "componentName",
494
- message: "\u{1F9E9} Component name to add:",
779
+ message: "Component name to add:",
495
780
  validate: (name) => name ? true : "Component name is required"
496
781
  });
497
- 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
+ }
798
+ }
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
+ });
498
804
  }
499
- parseLogicalName(componentName, "component name");
500
- if (pageScope) parseLogicalName(pageScope, "page name");
501
- const config = await loadConfig(cwd);
805
+ const pageSegments = pageScope ? parseLogicalName(pageScope, "page name") : [];
806
+ const config = await loadConfig(context);
502
807
  if (!config) {
503
- console.error(
504
- "\u274C Configuration file cnp.config.json not found. Run this command from the project root."
505
- );
506
- 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
+ });
507
812
  }
508
- const useI18n = !!config.useI18n;
509
- const componentNameUpper = capitalize(componentName);
510
- const templatePath = join3(
511
- resolvePackageRoot(import.meta.url),
512
- "templates",
513
- "Component"
514
- );
515
- let messagesPath = null;
516
- if (useI18n) {
517
- messagesPath = join3(cwd, "messages");
518
- if (!existsSync4(messagesPath)) {
519
- console.error(
520
- "\u274C Messages directory missing. Ensure i18n was configured."
521
- );
522
- return;
523
- }
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
+ });
524
823
  }
525
- let componentTargetPath;
526
- let translationKey;
527
- if (pageScope) {
528
- if (nestedPath) {
529
- componentTargetPath = join3(cwd, "src", "ui", nestedPath);
530
- translationKey = pageScope;
531
- } else {
532
- componentTargetPath = join3(cwd, "src", "ui", pageScope);
533
- translationKey = pageScope;
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
+ });
534
838
  }
535
- } else {
536
- componentTargetPath = join3(cwd, "src", "ui", "_global");
537
- translationKey = "_global_ui";
538
- }
539
- await assertSafeTarget(cwd, componentTargetPath);
540
- if (!existsSync4(componentTargetPath)) {
541
- await mkdir3(componentTargetPath, { recursive: true });
542
- }
543
- const componentFile = join3(componentTargetPath, `${componentNameUpper}.tsx`);
544
- const templateComponentPath = join3(templatePath, "Component.tsx");
545
- if (existsSync4(templateComponentPath)) {
546
- let content = await readFile4(templateComponentPath, "utf-8");
547
- content = content.replace(/Component/g, componentNameUpper).replace(/componentPage/g, translationKey);
548
- await writeFile3(componentFile, content);
549
- console.log(`\u{1F4C4} File created: ${componentFile}`);
550
- } else {
551
- console.error(
552
- "\u274C Template Component.tsx introuvable :",
553
- templateComponentPath
839
+ const templateMessages = JSON.parse(
840
+ await context.fs.readText(messagesTemplate)
554
841
  );
555
- }
556
- if (useI18n && messagesPath) {
557
- const entries = await readdir3(messagesPath, { withFileTypes: true });
558
- const langDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
559
- const jsonTemplate = join3(templatePath, "component.json");
560
- if (!existsSync4(jsonTemplate)) {
561
- console.error("\u274C Template component.json not found:", jsonTemplate);
562
- return;
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
+ });
563
849
  }
564
- const jsonContent = await readFile4(jsonTemplate, "utf-8");
565
- const parsed = JSON.parse(jsonContent);
566
- for (const locale of langDirs) {
567
- const localeDir = join3(messagesPath, locale);
568
- if (!existsSync4(localeDir) || !statSync(localeDir).isDirectory())
569
- continue;
570
- let jsonTarget;
571
- if (pageScope) {
572
- const [messageFile] = pageScope.split(".");
573
- jsonTarget = join3(messagesPath, locale, `${messageFile}.json`);
574
- } else {
575
- jsonTarget = join3(messagesPath, locale, `_global_ui.json`);
576
- }
577
- let current = {};
578
- if (existsSync4(jsonTarget)) {
579
- const jsonFile = await readFile4(jsonTarget, "utf-8");
580
- current = JSON.parse(jsonFile);
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
+ }
581
867
  }
582
- if (pageScope?.includes(".")) {
583
- const child = pageScope.split(".")[1];
584
- const childMessages = current[child] && typeof current[child] === "object" ? current[child] : {};
585
- childMessages[componentNameUpper] = parsed;
586
- current[child] = childMessages;
587
- } else {
588
- current[componentNameUpper] = parsed;
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];
589
884
  }
590
- await writeFile3(jsonTarget, JSON.stringify(current, null, 2));
591
- console.log(`\u{1F4C4} File updated: ${jsonTarget}`);
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
+ });
592
894
  }
593
- } else {
594
- console.log("\u2139\uFE0F Skipping translation entries; next-intl not enabled.");
595
895
  }
596
- console.log(
597
- `\u2705 Component "${componentNameUpper}" added ${pageScope ? `to page ${pageScope}` : "globally"}${useI18n ? " with localized messages" : ""}.`
598
- );
599
- }
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
+ });
914
+ } else {
915
+ await gateway.write(item.target, item.content, {
916
+ role: "translation-messages",
917
+ detail: {
918
+ locale: item.locale,
919
+ key: `${translationNamespace}.${componentNameUpper}`
920
+ }
921
+ });
922
+ }
923
+ }
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
+ };
600
952
 
601
953
  // src/lib/addLanguage.ts
602
954
  import { join as join4 } from "path";
603
- import { existsSync as existsSync5 } from "fs";
604
- import { cp, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
605
- import prompts4 from "prompts";
606
955
  function generateLocales() {
607
- const dn = new Intl.DisplayNames(["en"], { type: "language" });
956
+ const names = new Intl.DisplayNames(["en"], { type: "language" });
608
957
  const locales = [];
609
- for (let i = 0; i < 26; i++) {
610
- for (let j = 0; j < 26; j++) {
611
- const code = String.fromCharCode(97 + i) + String.fromCharCode(97 + j);
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);
612
961
  try {
613
- const name = dn.of(code);
614
- if (name && name.toLowerCase() !== code) {
615
- locales.push(code);
616
- }
962
+ const name = names.of(code);
963
+ if (name && name.toLowerCase() !== code) locales.push(code);
617
964
  } catch {
618
965
  }
619
966
  }
620
967
  }
621
968
  return locales.sort();
622
969
  }
623
- async function addLanguage(args, cwd = process.cwd()) {
624
- const config = await loadConfig(cwd);
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);
625
991
  if (!config?.useI18n) {
626
- console.error("\u274C i18n is not enabled in this project.");
627
- return;
992
+ throw new CliError("Internationalization is not enabled in this project.", {
993
+ code: "I18N_DISABLED"
994
+ });
628
995
  }
629
- const messagesPath = join4(cwd, "messages");
630
- if (!existsSync5(messagesPath)) {
631
- console.error("\u274C Messages directory missing. Ensure i18n was configured.");
632
- return;
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
+ });
633
1003
  }
634
1004
  const available = generateLocales();
635
1005
  let locale = args[1];
636
- if (!locale || !available.includes(locale)) {
637
- const response = await prompts4({
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({
638
1014
  type: "autocomplete",
639
1015
  name: "locale",
640
- message: "\u{1F310} Locale to add:",
641
- choices: available.map((l) => ({ title: l, value: l }))
1016
+ message: "Locale to add:",
1017
+ choices: available.map((value) => ({ title: value, value }))
642
1018
  });
643
- locale = response.locale;
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
+ }
644
1035
  }
645
- if (!locale) return;
646
1036
  parseLogicalName(locale, "locale");
647
- await assertSafeTarget(cwd, messagesPath);
648
- if (existsSync5(join4(messagesPath, locale))) {
649
- throw new Error(`Locale ${locale} already exists.`);
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
+ });
650
1042
  }
651
- const routingFile = join4(cwd, "src", "lib", "i18n", "routing.ts");
652
- const messagesRegistryFile = join4(cwd, "src", "lib", "i18n", "messages.ts");
653
- if (!existsSync5(routingFile)) {
654
- throw new Error("routing.ts not found. Are you in project root?");
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
+ );
655
1054
  }
656
- if (!existsSync5(messagesRegistryFile)) {
657
- throw new Error("messages.ts not found. Are you in project root?");
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
+ );
658
1066
  }
659
- const routingContent = await readFile5(routingFile, "utf-8");
660
- const defaultMatch = routingContent.match(/defaultLocale:\s*"([^"]+)"/);
661
- const defaultLocale = defaultMatch ? defaultMatch[1] : null;
662
- if (!defaultLocale || !existsSync5(join4(messagesPath, defaultLocale))) {
663
- throw new Error("Default locale not found.");
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}`
1076
+ }
1077
+ );
664
1078
  }
665
- const defaultAggregatorFile = join4(messagesPath, `${defaultLocale}.ts`);
666
- if (!existsSync5(defaultAggregatorFile)) {
667
- throw new Error(`Default locale aggregator not found: ${defaultLocale}.ts`);
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
+ });
668
1085
  }
669
- const localesMatch = routingContent.match(/locales:\s*\[([^\]]*)\]/);
670
- if (!localesMatch) {
671
- throw new Error("Unable to locate routing locales.");
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
+ });
672
1093
  }
673
- const localesArr = localesMatch[1].split(",").map((s) => s.trim().replace(/["']/g, "")).filter(Boolean);
674
- if (localesArr.includes(locale)) {
675
- throw new Error(`Locale ${locale} already exists in routing.`);
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`
1103
+ }
1104
+ );
676
1105
  }
677
- const newLocales = `locales: [${[...localesArr, locale].map((item) => `"${item}"`).join(", ")}]`;
678
- const nextRoutingContent = routingContent.replace(
679
- /locales:\s*\[[^\]]*\]/,
680
- newLocales
681
- );
682
- const defaultAggregatorContent = await readFile5(
683
- defaultAggregatorFile,
684
- "utf-8"
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;/
685
1118
  );
686
- const defaultImportPrefix = `./${defaultLocale}/`;
687
- if (!defaultAggregatorContent.includes(defaultImportPrefix)) {
688
- throw new Error(
689
- `Default locale aggregator does not import from ${defaultImportPrefix}`
690
- );
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
+ });
691
1125
  }
692
- const nextAggregatorContent = defaultAggregatorContent.replaceAll(
693
- defaultImportPrefix,
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
+ });
1156
+ }
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
+ });
1165
+ }
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,
694
1176
  `./${locale}/`
695
1177
  );
696
- const messagesRegistryContent = await readFile5(messagesRegistryFile, "utf-8");
697
- const registryMatch = messagesRegistryContent.match(
698
- /const messages = \{([^}]*)\} as const;/
1178
+ const nextRouting = routing.replace(
1179
+ /locales:\s*\[[^\]]*\]/,
1180
+ `locales: [${[...routingLocales, locale].map((value) => `"${value}"`).join(", ")}]`
699
1181
  );
700
- if (!registryMatch) {
701
- throw new Error("Unable to locate the typed messages registry.");
702
- }
703
- const registeredLocales = registryMatch[1].split(",").map((item) => item.trim()).filter(Boolean);
704
- if (registeredLocales.includes(locale)) {
705
- throw new Error(`Locale ${locale} already exists in messages registry.`);
706
- }
707
- const registryDeclaration = `const messages = { ${[
708
- ...registeredLocales,
709
- locale
710
- ].join(", ")} } as const;`;
711
- const declarationIndex = messagesRegistryContent.indexOf("const messages =");
712
- const nextMessagesRegistryContent = messagesRegistryContent.slice(0, declarationIndex) + `import ${locale} from "../../../messages/${locale}";
1182
+ const declarationIndex = registry.indexOf("const messages =");
1183
+ const nextRegistry = registry.slice(0, declarationIndex) + `import ${locale} from "../../../messages/${locale}";
713
1184
 
714
- ` + messagesRegistryContent.slice(declarationIndex).replace(/const messages = \{[^}]*\} as const;/, registryDeclaration);
715
- await cp(join4(messagesPath, defaultLocale), join4(messagesPath, locale), {
716
- recursive: true
717
- });
718
- console.log(`\u{1F4C4} Directory created: ${join4(messagesPath, locale)}`);
719
- await writeFile4(join4(messagesPath, `${locale}.ts`), nextAggregatorContent);
720
- console.log(`\u{1F4C4} File created: ${join4(messagesPath, `${locale}.ts`)}`);
721
- await writeFile4(messagesRegistryFile, nextMessagesRegistryContent);
722
- console.log(`\u{1F4C4} File updated: ${messagesRegistryFile}`);
723
- await writeFile4(routingFile, nextRoutingContent);
724
- console.log(`\u{1F4C4} File updated: ${routingFile}`);
725
- console.log(
726
- `\u2705 Locale "${locale}" added and copied from default locale "${defaultLocale}".`
1185
+ ` + registry.slice(declarationIndex).replace(
1186
+ /const messages = \{[^}]*\} as const;/,
1187
+ `const messages = { ${[...registeredLocales, locale].join(", ")} } as const;`
727
1188
  );
728
- }
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 }
1202
+ });
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"]
1232
+ }
1233
+ ]
1234
+ });
1235
+ };
729
1236
 
730
1237
  // src/lib/addLib.ts
731
1238
  import { join as join5 } from "path";
732
- import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
733
- import prompts5 from "prompts";
734
- import { existsSync as existsSync6 } from "fs";
735
- async function addLib(args, cwd = process.cwd()) {
1239
+ var addLib = async (args, context) => {
736
1240
  let libArg = args[1];
737
1241
  if (!libArg || libArg.startsWith("-")) {
738
- const response = await prompts5.prompt({
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({
739
1249
  type: "text",
740
1250
  name: "libArg",
741
- message: "\u{1F4E6} Lib name to add:",
742
- validate: (name) => name ? true : "Lib name is required"
1251
+ message: "Library name to add:",
1252
+ validate: (name) => name ? true : "Library name is required"
743
1253
  });
744
- libArg = response.libArg;
745
- }
746
- const libSegments = parseLogicalName(libArg, "library name");
747
- let libName = libArg;
748
- let fileName = null;
749
- if (libSegments.length === 2) {
750
- [libName, fileName] = libSegments;
751
- } else if (libSegments.length > 2) {
752
- throw new Error("Libraries currently support exactly library.module.");
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
+ });
1269
+ }
753
1270
  }
754
- const config = await loadConfig(cwd);
755
- if (!config) {
756
- console.error(
757
- "\u274C Configuration file cnp.config.json not found. Run this command from the project root."
758
- );
759
- return;
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
+ });
760
1276
  }
761
- const libDir = join5(cwd, "src", "lib", libName);
762
- await assertSafeTarget(cwd, libDir);
763
- if (!existsSync6(libDir)) {
764
- await mkdir4(libDir, { recursive: true });
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
+ });
765
1283
  }
766
- const templateDir = join5(
767
- resolvePackageRoot(import.meta.url),
768
- "templates",
769
- "Lib"
770
- );
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");
771
1288
  const indexTemplate = join5(templateDir, "index.ts");
772
- const fileTemplate = join5(templateDir, "item.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
+ });
773
1301
  const indexPath = join5(libDir, "index.ts");
774
- if (!existsSync6(indexPath)) {
775
- if (existsSync6(indexTemplate)) {
776
- const content = await readFile6(indexTemplate, "utf-8");
777
- await writeFile5(indexPath, content);
778
- } else {
779
- await writeFile5(indexPath, "export {}\n");
780
- }
781
- console.log(`\u{1F4C4} File created: ${indexPath}`);
1302
+ if (!context.fs.exists(indexPath)) {
1303
+ await gateway.write(indexPath, await context.fs.readText(indexTemplate), {
1304
+ role: "library-index"
1305
+ });
782
1306
  }
1307
+ let moduleAction;
783
1308
  if (fileName) {
784
- const filePath = join5(libDir, `${fileName}.ts`);
785
- if (!existsSync6(filePath)) {
786
- if (existsSync6(fileTemplate)) {
787
- let content = await readFile6(fileTemplate, "utf-8");
788
- content = content.replace(/template/g, fileName).replace(/Template/g, capitalize(fileName));
789
- await writeFile5(filePath, content);
790
- } else {
791
- await writeFile5(
792
- filePath,
793
- `export function ${fileName}() {
794
- // TODO: implement
795
- }
796
- `
797
- );
798
- }
799
- console.log(`\u{1F4C4} File created: ${filePath}`);
800
- }
801
- let indexContent = await readFile6(indexPath, "utf-8");
802
- const importLine = `import { ${fileName} } from "./${fileName}";`;
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}";`;
803
1318
  const importRegex = new RegExp(
804
- `import\\s*{\\s*${fileName}\\s*}\\s*from\\s*"\\./${fileName}";`
1319
+ `import\\s*{\\s*${moduleIdentifier}\\s*}\\s*from\\s*["']\\./${fileName}["'];`
805
1320
  );
806
1321
  const exportRegex = /export\s*{([^}]*)}/m;
807
- const imports = [];
808
- const exportsSet = [];
809
- for (const line of indexContent.split("\n")) {
810
- if (line.startsWith("import")) {
811
- imports.push(line);
812
- } else if (line.startsWith("export")) {
813
- const match = line.match(exportRegex);
814
- if (match && match[1]) {
815
- exportsSet.push(
816
- ...match[1].split(",").map((s) => s.trim()).filter(Boolean)
817
- );
818
- }
819
- }
820
- }
821
- if (!imports.some((l) => importRegex.test(l))) {
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)))
822
1326
  imports.push(importLine);
823
- }
824
- if (!exportsSet.includes(fileName)) {
825
- exportsSet.push(fileName);
826
- }
827
- indexContent = imports.join("\n") + "\n\nexport { " + exportsSet.join(", ") + " };\n";
828
- await writeFile5(indexPath, indexContent);
829
- console.log(`\u270F\uFE0F Updated index: ${indexPath}`);
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" });
830
1336
  }
831
- console.log(
832
- `\u2705 Lib "${libName}"${fileName ? ` with module ${fileName}` : ""} added.`
1337
+ const mutated = context.operations.snapshot().some(
1338
+ (event) => ["created", "updated", "copied", "deleted"].includes(event.action)
833
1339
  );
834
- }
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
+ });
1353
+ }
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
+ });
1362
+ }
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
+ };
835
1370
 
836
1371
  // src/lib/addPage.ts
837
1372
  import { join as join6 } from "path";
838
- import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile6, readdir as readdir4 } from "fs/promises";
839
- import prompts6 from "prompts";
840
- import { existsSync as existsSync7, statSync as statSync2 } from "fs";
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
+ };
841
1395
  function registerMessagesFile(content, locale, fileName) {
842
1396
  const importPath = `./${locale}/${fileName}.json`;
843
1397
  if (content.includes(importPath)) return content;
844
1398
  const declarationIndex = content.indexOf("const messages =");
845
1399
  const registryMatch = content.match(/const messages = \{([\s\S]*?)\n\};/);
846
- if (declarationIndex === -1 || !registryMatch) {
847
- throw new Error(
848
- `Unable to register ${fileName}.json in messages/${locale}.ts`
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
+ }
849
1408
  );
850
1409
  }
851
- const importStatement = `import ${fileName} from "${importPath}";
1410
+ const identifier = toIdentifier(fileName);
1411
+ const importStatement = `import ${identifier} from "${importPath}";
852
1412
  `;
853
1413
  const nextRegistry = registryMatch[0].replace(
854
1414
  /\n\};$/,
855
1415
  `
856
- ${fileName},
1416
+ ${identifier},
857
1417
  };`
858
1418
  );
859
1419
  return content.slice(0, declarationIndex) + importStatement + content.slice(declarationIndex).replace(registryMatch[0], nextRegistry);
860
1420
  }
861
- async function addPage(args, cwd = process.cwd()) {
862
- let pageName = args[1];
863
- if (!pageName || pageName.startsWith("-")) {
864
- const response = await prompts6.prompt({
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({
865
1459
  type: "text",
866
1460
  name: "pageName",
867
- message: "\u{1F4DD} Page name to add:",
1461
+ message: "Page name to add:",
868
1462
  validate: (name) => name ? true : "Page name is required"
869
1463
  });
870
- pageName = response.pageName;
871
- }
872
- const pageSegments = parseLogicalName(pageName, "page name");
873
- let parentName = null;
874
- let childName = null;
875
- if (pageSegments.length === 2) {
876
- [parentName, childName] = pageSegments;
877
- } else if (pageSegments.length > 2) {
878
- throw new Error("Nested pages currently support exactly Parent.Child.");
879
- }
880
- let shortFlags = args.find((arg) => /^-[A-Za-z]+$/.test(arg));
881
- const longFlags = new Set(args.filter((a) => a.startsWith("--")));
882
- const flags = /* @__PURE__ */ new Set();
883
- if (!shortFlags && Array.from(longFlags).length === 0) {
884
- shortFlags = "-LPl";
885
- }
886
- if (shortFlags) {
887
- for (const char of shortFlags.slice(1)) {
888
- switch (char) {
889
- case "L":
890
- flags.add("layout");
891
- break;
892
- case "P":
893
- flags.add("page");
894
- break;
895
- case "l":
896
- flags.add("loading");
897
- break;
898
- case "n":
899
- flags.add("not-found");
900
- break;
901
- case "e":
902
- flags.add("error");
903
- break;
904
- case "g":
905
- flags.add("global-error");
906
- break;
907
- case "r":
908
- flags.add("route");
909
- break;
910
- case "t":
911
- flags.add("template");
912
- break;
913
- case "d":
914
- flags.add("default");
915
- break;
916
- }
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
+ });
917
1479
  }
918
1480
  }
919
- for (const flag of [
920
- "layout",
921
- "page",
922
- "loading",
923
- "not-found",
924
- "error",
925
- "global-error",
926
- "route",
927
- "template",
928
- "default"
929
- ]) {
930
- if (longFlags.has("--" + flag)) flags.add(flag);
931
- }
932
- const config = await loadConfig(cwd);
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);
933
1489
  if (!config) {
934
- console.error(
935
- "\u274C Configuration file cnp.config.json not found. Run this command from the project root."
936
- );
937
- return;
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
+ });
938
1494
  }
939
- const useI18n = !!config.useI18n;
1495
+ const useI18n = Boolean(config.useI18n);
940
1496
  const aliasPrefix = configuredAliasPrefix(config);
941
- const srcSegments = ["src", "app"];
942
- if (useI18n) srcSegments.push("[locale]");
943
- const srcPath = join6(cwd, ...srcSegments);
944
- if (!existsSync7(srcPath)) {
945
- console.error(`\u274C Expected directory not found: ${srcPath}`);
946
- return;
947
- }
948
- let messagesPath = null;
949
- let locales = [];
950
- if (useI18n) {
951
- messagesPath = join6(cwd, "messages");
952
- if (!existsSync7(messagesPath)) {
953
- console.error(
954
- "\u274C Messages directory missing. Ensure i18n was configured."
955
- );
956
- return;
957
- }
958
- const entries = await readdir4(messagesPath, { withFileTypes: true });
959
- locales = entries.filter((e) => e.isDirectory()).map((e) => e.name);
960
- }
961
- const templatePath = join6(
962
- resolvePackageRoot(import.meta.url),
963
- "templates",
964
- "Page"
1497
+ const appRoot = join6(
1498
+ context.cwd,
1499
+ "src",
1500
+ "app",
1501
+ ...useI18n ? ["[locale]"] : []
965
1502
  );
966
- let uiPageDir, localePagePath, jsonFileName;
967
- if (parentName && childName) {
968
- uiPageDir = join6(cwd, "src", "ui", parentName, childName);
969
- localePagePath = join6(srcPath, parentName, childName);
970
- jsonFileName = parentName;
971
- } else {
972
- uiPageDir = join6(cwd, "src", "ui", pageName);
973
- localePagePath = join6(srcPath, pageName);
974
- jsonFileName = pageName;
975
- }
976
- await assertSafeTarget(cwd, uiPageDir);
977
- await assertSafeTarget(cwd, localePagePath);
978
- if (!existsSync7(uiPageDir)) {
979
- await mkdir5(uiPageDir, { recursive: true });
980
- }
981
- const uiPageFile = join6(uiPageDir, "page-ui.tsx");
982
- const uiPageTemplate = join6(templatePath, "page-ui.tsx");
983
- if (existsSync7(uiPageTemplate)) {
984
- let uiContent = await readFile7(uiPageTemplate, "utf-8");
985
- const translationNamespace = parentName && childName ? `${parentName}.${childName}` : pageName;
986
- uiContent = uiContent.replace(
987
- 'useTranslations("template")',
988
- `useTranslations("${translationNamespace}")`
989
- );
990
- uiContent = uiContent.replaceAll('from "@/', `from "${aliasPrefix}/`).replace(/template/g, childName || pageName).replace(/Template/g, capitalize(childName || pageName));
991
- await writeFile6(uiPageFile, uiContent);
992
- console.log(`\u{1F4C4} File created: ${uiPageFile}`);
993
- } else {
994
- console.warn(
995
- "\u26A0\uFE0F Missing template file: page-ui.tsx at path:",
996
- uiPageTemplate
997
- );
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
+ });
998
1509
  }
999
- if (!existsSync7(localePagePath)) {
1000
- await mkdir5(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
+ });
1001
1539
  }
1002
- for (const flag of flags) {
1003
- const filename = toFileName(flag);
1004
- const src = join6(templatePath, filename);
1005
- const dst = join6(localePagePath, filename);
1006
- if (!existsSync7(src)) {
1007
- console.warn(`\u26A0\uFE0F Missing template file: ${filename} at path: ${src}`);
1008
- 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
+ });
1009
1553
  }
1010
- const content = await readFile7(src, "utf-8");
1011
- const uiImportPath = parentName && childName ? `${parentName}/${childName}` : pageName;
1012
- const replaced = content.replace("@/ui/template/", `@/ui/${uiImportPath}/`).replaceAll('from "@/', `from "${aliasPrefix}/`).replace(/template/g, childName || pageName).replace(/Template/g, capitalize(childName || pageName));
1013
- await writeFile6(dst, replaced);
1014
- console.log(`\u{1F4C4} File created: ${dst}`);
1015
- }
1016
- if (useI18n && messagesPath) {
1017
- const jsonTemplate = join6(templatePath, "page.json");
1018
- if (!existsSync7(jsonTemplate)) {
1019
- console.warn("\u26A0\uFE0F Missing template: page.json at path:", jsonTemplate);
1020
- } else {
1021
- const content = await readFile7(jsonTemplate, "utf-8");
1022
- const replaced = content.replace(/template/g, childName || pageName).replace(/Template/g, capitalize(childName || pageName));
1023
- for (const locale of locales) {
1024
- const localeDir = join6(messagesPath, locale);
1025
- if (!existsSync7(localeDir) || !statSync2(localeDir).isDirectory())
1026
- continue;
1027
- const jsonTarget = join6(messagesPath, locale, `${jsonFileName}.json`);
1028
- let current = {};
1029
- if (existsSync7(jsonTarget)) {
1030
- const jsonFile = await readFile7(jsonTarget, "utf-8");
1031
- try {
1032
- current = JSON.parse(jsonFile);
1033
- } catch {
1034
- 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`
1035
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
+ );
1036
1589
  }
1037
- if (parentName && childName) {
1038
- current[childName] = JSON.parse(replaced);
1039
- } else {
1040
- current = JSON.parse(replaced);
1041
- }
1042
- await writeFile6(jsonTarget, `${JSON.stringify(current, null, 2)}
1043
- `);
1044
- console.log(`\u{1F4C4} File created: ${jsonTarget}`);
1045
- const localeAggregator = join6(messagesPath, `${locale}.ts`);
1046
- if (!existsSync7(localeAggregator)) {
1047
- throw new Error(`Locale aggregator not found: messages/${locale}.ts`);
1048
- }
1049
- const aggregatorContent = await readFile7(localeAggregator, "utf-8");
1050
- const nextAggregatorContent = registerMessagesFile(
1051
- aggregatorContent,
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,
1052
1606
  locale,
1053
1607
  jsonFileName
1054
- );
1055
- if (nextAggregatorContent !== aggregatorContent) {
1056
- await writeFile6(localeAggregator, nextAggregatorContent);
1057
- console.log(`\u{1F4C4} File updated: ${localeAggregator}`);
1058
- }
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
+ });
1059
1652
  }
1653
+ await gateway.write(item.aggregator, item.aggregatorContent, {
1654
+ role: "locale-aggregator"
1655
+ });
1060
1656
  }
1061
1657
  } else {
1062
- 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
+ });
1063
1663
  }
1064
- console.log(
1065
- `\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)
1066
1666
  );
1067
- }
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
+ };
1068
1699
 
1069
1700
  // src/lib/addText.ts
1070
1701
  import { join as join7 } from "path";
1071
- import { existsSync as existsSync8 } from "fs";
1072
- import { readFile as readFile8, writeFile as writeFile7, readdir as readdir5 } from "fs/promises";
1073
1702
  function defaultText(key) {
1074
- return key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1703
+ return key.replace(/_/g, " ").replace(/\b\w/g, (character) => character.toUpperCase());
1075
1704
  }
1076
- async function addText(args, cwd = process.cwd()) {
1705
+ var addText = async (args, context) => {
1077
1706
  const pathArg = args[1];
1078
1707
  if (!pathArg) {
1079
- console.error("\u274C Dot path parameter is required.");
1080
- return;
1708
+ throw new CliError("Translation dot path is required.", {
1709
+ code: "INVALID_ARGUMENT",
1710
+ hint: "Use addtext domain.key followed by an optional value."
1711
+ });
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
+ });
1081
1718
  }
1082
1719
  const providedText = args.slice(2).join(" ");
1083
- parseLogicalName(pathArg, "translation path");
1084
- const config = await loadConfig(cwd);
1720
+ const finalKey = segments.at(-1);
1721
+ const text = providedText || defaultText(finalKey);
1722
+ const config = await loadConfig(context);
1085
1723
  if (!config?.useI18n) {
1086
- console.error("\u274C i18n is not enabled in this project.");
1087
- return;
1724
+ throw new CliError("Internationalization is not enabled in this project.", {
1725
+ code: "I18N_DISABLED"
1726
+ });
1088
1727
  }
1089
- const messagesPath = join7(cwd, "messages");
1090
- if (!existsSync8(messagesPath)) {
1091
- console.error("\u274C Messages directory missing. Ensure i18n was configured.");
1092
- return;
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
+ });
1093
1735
  }
1094
- const entries = await readdir5(messagesPath, { withFileTypes: true });
1095
- const locales = entries.filter((e) => e.isDirectory()).map((e) => e.name);
1096
- const [fileName, ...segments] = pathArg.split(".");
1097
- if (!fileName || segments.length === 0) {
1098
- console.error("\u274C Invalid dot path provided.");
1099
- return;
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
+ });
1100
1743
  }
1101
- const finalKey = segments[segments.length - 1];
1102
- const text = providedText || defaultText(finalKey);
1744
+ const [fileName, ...keySegments] = segments;
1745
+ const prepared = [];
1103
1746
  for (const locale of locales) {
1104
- const filePath = join7(messagesPath, locale, `${fileName}.json`);
1105
- await assertSafeTarget(cwd, filePath);
1747
+ const file = join7(messagesRoot, locale, `${fileName}.json`);
1748
+ await assertSafeTarget(context.cwd, file, context.fs);
1749
+ const existed = context.fs.exists(file);
1106
1750
  let data = {};
1107
- if (existsSync8(filePath)) {
1108
- const raw = await readFile8(filePath, "utf-8");
1751
+ if (existed) {
1109
1752
  try {
1110
- data = JSON.parse(raw);
1111
- } catch {
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
+ );
1112
1768
  }
1113
1769
  }
1114
1770
  let cursor = data;
1115
- for (let i = 0; i < segments.length - 1; i++) {
1116
- const seg = segments[i];
1117
- if (!cursor[seg]) cursor[seg] = {};
1118
- cursor = cursor[seg];
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];
1119
1785
  }
1786
+ const previous = cursor[finalKey];
1120
1787
  cursor[finalKey] = text;
1121
- await writeFile7(filePath, JSON.stringify(data, null, 2));
1122
- console.log(`\u{1F4C4} File updated: ${filePath}`);
1788
+ prepared.push({
1789
+ locale,
1790
+ file,
1791
+ data,
1792
+ replaced: previous !== void 0 && previous !== text
1793
+ });
1123
1794
  }
1124
- console.log(`\u2705 Text added at path "${pathArg}" with value "${text}".`);
1125
- }
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
+ });
1806
+ }
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
+ };
1126
1835
 
1127
1836
  // src/lib/rmPage.ts
1128
- import { existsSync as existsSync9 } from "fs";
1129
- import { readFile as readFile9, readdir as readdir6, rm, writeFile as writeFile8 } from "fs/promises";
1130
- import path6 from "path";
1131
- async function removeMessages(projectRoot, candidate, terminal) {
1132
- const messagesRoot = resolveInside(projectRoot, "messages");
1133
- let locales;
1134
- try {
1135
- locales = (await readdir6(messagesRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
1136
- } catch {
1137
- return;
1138
- }
1139
- for (const locale of locales) {
1140
- const target = resolveInside(
1141
- projectRoot,
1142
- "messages",
1143
- locale,
1144
- `${candidate.routeSegments[0]}.json`
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
+ }
1145
1863
  );
1146
- if (!existsSync9(target)) continue;
1147
- if (candidate.messageKey) {
1148
- const data = JSON.parse(await readFile9(target, "utf8"));
1149
- delete data[candidate.messageKey];
1150
- await writeFile8(target, `${JSON.stringify(data, null, 2)}
1151
- `);
1152
- } else {
1153
- await rm(target);
1154
- }
1155
- terminal.log(`\u{1F5D1}\uFE0F Deleted messages for ${candidate.logicalName}: ${target}`);
1156
1864
  }
1865
+ if (!hasImport) return { content, changed: false };
1866
+ return {
1867
+ content: content.replace(importPattern, "").replace(propertyPattern, ""),
1868
+ changed: true
1869
+ };
1157
1870
  }
1158
- async function rmPage(args, cwd = process.cwd(), prompt, terminal = console) {
1159
- const candidates = await discoverPages(cwd);
1871
+ var rmPage = async (args, context) => {
1872
+ const candidates = await discoverPages(context.cwd, context.fs);
1160
1873
  let logicalName = args[1];
1161
1874
  if (!logicalName || logicalName.startsWith("-")) {
1162
- if (!prompt)
1163
- throw new CliError("A page name is required in non-interactive mode.");
1164
- const selected = await prompt([
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([
1165
1882
  {
1166
1883
  type: "autocomplete",
1167
1884
  name: "page",
1168
- message: "\u{1F5D1}\uFE0F Page to remove:",
1885
+ message: "Page to remove:",
1169
1886
  choices: candidates.map((candidate2) => ({
1170
- title: candidate2.logicalName.replaceAll(".", " \u203A "),
1887
+ title: candidate2.logicalName.replaceAll(".", " > "),
1171
1888
  value: candidate2.logicalName
1172
1889
  }))
1173
1890
  },
@@ -1179,8 +1896,19 @@ async function rmPage(args, cwd = process.cwd(), prompt, terminal = console) {
1179
1896
  }
1180
1897
  ]);
1181
1898
  if (!selected.confirm) {
1182
- terminal.log("Page deletion cancelled.");
1183
- return;
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
+ });
1184
1912
  }
1185
1913
  logicalName = String(selected.page ?? "");
1186
1914
  }
@@ -1188,74 +1916,157 @@ async function rmPage(args, cwd = process.cwd(), prompt, terminal = console) {
1188
1916
  const candidate = candidates.find(
1189
1917
  (entry) => entry.logicalName === logicalName
1190
1918
  );
1191
- if (!candidate) throw new CliError(`Page not found: ${logicalName}`);
1192
- await removeMessages(cwd, candidate, terminal);
1193
- for (const target of [candidate.uiDirectory, candidate.routeDirectory]) {
1194
- const safeTarget = resolveInside(cwd, path6.relative(cwd, target));
1195
- if (existsSync9(safeTarget)) {
1196
- await rm(safeTarget, { recursive: true, force: false });
1197
- terminal.log(`\u{1F5D1}\uFE0F Deleted: ${safeTarget}`);
1919
+ if (!candidate) {
1920
+ throw new CliError(`Page not found: ${logicalName}.`, {
1921
+ code: "TARGET_NOT_FOUND",
1922
+ scope: "project",
1923
+ path: logicalName.replaceAll(".", "/")
1924
+ });
1925
+ }
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
+ });
1198
1991
  }
1199
1992
  }
1200
- terminal.log(`\u2705 Page "${logicalName}" deleted.`);
1201
- }
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
+ }
2008
+ }
2009
+ for (const prepared of preparedAggregators) {
2010
+ if (prepared.changed) {
2011
+ await gateway.write(prepared.target, prepared.content, {
2012
+ role: "locale-aggregator"
2013
+ });
2014
+ } else {
2015
+ gateway.unchanged(prepared.target, { role: "locale-aggregator" });
2016
+ }
2017
+ }
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
+ };
1202
2050
 
1203
2051
  // src/cli/registry.ts
1204
- function legacyHandler(command) {
1205
- return async (args, context) => {
1206
- await command(args, context.cwd);
1207
- return success();
1208
- };
1209
- }
1210
2052
  function createCommandRegistry() {
1211
2053
  return /* @__PURE__ */ new Map([
1212
- ["addcomponent", legacyHandler(addComponent)],
1213
- ["addpage", legacyHandler(addPage)],
1214
- ["addlib", legacyHandler(addLib)],
1215
- ["addapi", legacyHandler(addApi)],
1216
- ["addlanguage", legacyHandler(addLanguage)],
1217
- ["addtext", legacyHandler(addText)],
1218
- [
1219
- "rmpage",
1220
- async (args, context) => {
1221
- await rmPage(args, context.cwd, context.prompt, context.terminal);
1222
- return success();
1223
- }
1224
- ]
2054
+ ["addcomponent", addComponent],
2055
+ ["addpage", addPage],
2056
+ ["addlib", addLib],
2057
+ ["addapi", addApi],
2058
+ ["addlanguage", addLanguage],
2059
+ ["addtext", addText],
2060
+ ["rmpage", rmPage]
1225
2061
  ]);
1226
2062
  }
1227
2063
 
1228
2064
  // src/scaffold.ts
1229
- import { mkdir as mkdir7, rm as rm2, writeFile as writeFile10 } from "fs/promises";
1230
2065
  import { join as join8, resolve } from "path";
1231
- import { existsSync as existsSync10 } from "fs";
1232
- import { fileURLToPath as fileURLToPath2 } from "url";
1233
-
1234
- // src/lib/helper/consoleColor.ts
1235
- var RED = "\x1B[31m";
1236
- var GREEN = "\x1B[32m";
1237
- var CYAN = "\x1B[36m";
1238
- var RESET = "\x1B[0m";
1239
- function red(text) {
1240
- return RED + text + RESET;
1241
- }
1242
- function green(text) {
1243
- return GREEN + text + RESET;
1244
- }
1245
- function cyan(text) {
1246
- return CYAN + text + RESET;
1247
- }
2066
+ import { fileURLToPath } from "url";
1248
2067
 
1249
2068
  // src/core/template-manifest.ts
1250
- import {
1251
- lstat as lstat2,
1252
- mkdir as mkdir6,
1253
- readdir as readdir7,
1254
- readFile as readFile10,
1255
- writeFile as writeFile9,
1256
- copyFile as copyFile2
1257
- } from "fs/promises";
1258
- import path7 from "path";
2069
+ import path8 from "path";
1259
2070
  var TEMPLATE_DENY_NAMES = /* @__PURE__ */ new Set([
1260
2071
  ".env",
1261
2072
  ".git",
@@ -1269,80 +2080,129 @@ var TEMPLATE_DENY_NAMES = /* @__PURE__ */ new Set([
1269
2080
  "test-results"
1270
2081
  ]);
1271
2082
  function isDistributableTemplatePath(relativePath) {
1272
- const segments = relativePath.split(path7.sep);
2083
+ const segments = relativePath.split(path8.sep);
1273
2084
  return !segments.some(
1274
2085
  (segment) => TEMPLATE_DENY_NAMES.has(segment) || segment.endsWith(".tsbuildinfo") || segment === ".DS_Store"
1275
2086
  );
1276
2087
  }
1277
- async function templateManifest(root) {
2088
+ async function templateManifest(root, fs) {
1278
2089
  const files = [];
1279
2090
  async function visit(directory, relativeDirectory = "") {
1280
- const entries = await readdir7(directory, { withFileTypes: true });
1281
- entries.sort((left, right) => left.name.localeCompare(right.name));
2091
+ const entries = await fs.list(directory);
1282
2092
  for (const entry of entries) {
1283
- const relative = path7.join(relativeDirectory, entry.name);
2093
+ const relative = path8.join(relativeDirectory, entry.name);
1284
2094
  if (!isDistributableTemplatePath(relative)) continue;
1285
- const source = path7.join(directory, entry.name);
1286
- const stats = await lstat2(source);
1287
- if (stats.isSymbolicLink()) {
2095
+ const source = path8.join(directory, entry.name);
2096
+ if (entry.isSymbolicLink) {
1288
2097
  throw new CliError(
1289
- `Symbolic links are forbidden in templates: ${relative}`
2098
+ `Symbolic links are forbidden in templates: ${relative}`,
2099
+ { code: "UNSAFE_PATH", scope: "package", path: relative }
1290
2100
  );
1291
2101
  }
1292
- if (stats.isDirectory()) await visit(source, relative);
1293
- else if (stats.isFile()) files.push(relative);
1294
- else throw new CliError(`Unsupported template entry: ${relative}`);
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
+ });
1295
2110
  }
1296
2111
  }
1297
2112
  await visit(root);
1298
2113
  return files;
1299
2114
  }
1300
- async function copyTemplate(root, target) {
1301
- const manifest = await templateManifest(root);
1302
- await mkdir6(target, { recursive: true });
1303
- for (const relative of manifest) {
1304
- const destination = path7.join(
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"))
2130
+ );
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"
2139
+ }
2140
+ );
2141
+ }
2142
+ return manifest;
2143
+ }
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(
1305
2148
  target,
1306
2149
  relative === ".gitignore.template" ? ".gitignore" : relative
1307
2150
  );
1308
- await mkdir6(path7.dirname(destination), { recursive: true });
1309
- await copyFile2(path7.join(root, relative), destination);
2151
+ const source = path8.join(root, relative);
2152
+ await gateway.copy(source, destination, {
2153
+ role: "template-file",
2154
+ source: { template: `Projects/default/${relative}` }
2155
+ });
1310
2156
  }
1311
- return manifest;
2157
+ return files;
1312
2158
  }
1313
- async function customizeGeneratedProject(target, projectName, importAlias) {
1314
- const packagePath = path7.join(target, "package.json");
1315
- const packageJson = JSON.parse(await readFile10(packagePath, "utf8"));
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
+ );
1316
2164
  packageJson.name = projectName.toLowerCase();
1317
2165
  delete packageJson.packageManager;
1318
- await writeFile9(packagePath, `${JSON.stringify(packageJson, null, 2)}
1319
- `);
1320
- const tsconfigPath = path7.join(target, "tsconfig.json");
1321
- const tsconfigContent = await readFile10(tsconfigPath, "utf8");
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);
1322
2174
  const tsconfig = JSON.parse(tsconfigContent);
1323
2175
  if (!tsconfig.compilerOptions?.paths?.["@/*"]) {
1324
2176
  throw new CliError(
1325
- 'The template tsconfig must define the default "@/*" alias.'
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
+ }
1326
2183
  );
1327
2184
  }
1328
- await writeFile9(
2185
+ await gateway.write(
1329
2186
  tsconfigPath,
1330
- tsconfigContent.replace('"@/*"', `"${importAlias}"`)
2187
+ tsconfigContent.replace('"@/*"', `"${importAlias}"`),
2188
+ { role: "typescript-configuration" }
1331
2189
  );
1332
2190
  if (importAlias !== "@/*") {
1333
2191
  const prefix = importAlias.slice(0, -2);
1334
- for (const relative of await templateManifest(target)) {
2192
+ for (const relative of await templateManifest(target, context.fs)) {
1335
2193
  if (!/\.[cm]?[jt]sx?$/.test(relative)) continue;
1336
- const file = path7.join(target, relative);
1337
- const content = await readFile10(file, "utf8");
2194
+ const file = path8.join(target, relative);
2195
+ const content = await context.fs.readText(file);
1338
2196
  const next = content.replaceAll('from "@/', `from "${prefix}/`).replaceAll("from '@/", `from '${prefix}/`);
1339
- if (next !== content) await writeFile9(file, next);
2197
+ if (next !== content) {
2198
+ await gateway.write(file, next, { role: "import-alias-reference" });
2199
+ }
1340
2200
  }
1341
2201
  }
1342
2202
  }
1343
2203
 
1344
2204
  // src/scaffold.ts
1345
- async function scaffoldProject(options, runtime = {}) {
2205
+ async function scaffoldProject(options, runtime) {
1346
2206
  const requiredFeatures = [
1347
2207
  "useTypescript",
1348
2208
  "useEslint",
@@ -1356,75 +2216,120 @@ async function scaffoldProject(options, runtime = {}) {
1356
2216
  );
1357
2217
  if (unsupported.length > 0) {
1358
2218
  throw new CliError(
1359
- `The default Next.js 16 template requires: ${unsupported.join(", ")}.`
2219
+ `The default Next.js 16 template requires: ${unsupported.join(", ")}.`,
2220
+ { code: "INVALID_ARGUMENT" }
1360
2221
  );
1361
2222
  }
1362
- const cwd = runtime.cwd ?? process.cwd();
1363
- const terminal = runtime.terminal ?? console;
2223
+ const { context } = runtime;
2224
+ const cwd = context.cwd;
1364
2225
  const projectName = validateProjectName(options.projectName);
1365
2226
  const importAlias = normalizeImportAlias(
1366
2227
  options.customAlias === false ? "@/*" : options.importAlias || "@/*"
1367
2228
  );
1368
2229
  const targetPath = join8(cwd, projectName);
1369
2230
  const __dirname = new URL(".", import.meta.url);
1370
- const templatePath = runtime.templatePath ?? join8(fileURLToPath2(__dirname), "..", "templates", "Projects", "default");
2231
+ const templatePath = runtime.templatePath ?? join8(fileURLToPath(__dirname), "..", "templates", "Projects", "default");
1371
2232
  const resolvedCwd = resolve(cwd);
1372
2233
  const resolvedTarget = resolve(targetPath);
1373
2234
  if (!resolvedTarget.startsWith(`${resolvedCwd}/`)) {
1374
2235
  throw new CliError(
1375
- "The project destination must be a child of the current directory."
2236
+ "The project destination must be a child of the current directory.",
2237
+ { code: "UNSAFE_PATH" }
1376
2238
  );
1377
2239
  }
1378
- if (existsSync10(targetPath)) {
2240
+ const gateway = new MutationGateway(context, targetPath);
2241
+ const manifest = await validateScaffoldTemplate(templatePath, context.fs);
2242
+ if (context.fs.exists(targetPath)) {
1379
2243
  if (options.force) {
1380
- terminal.warn("\u26A0\uFE0F Target directory already exists, removing...");
1381
- await rm2(targetPath, { recursive: true, force: true });
1382
- } else {
1383
- terminal.error(
1384
- red("[X] Target directory already exists. Use --force to overwrite.")
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 }
1385
2256
  );
2257
+ } else {
1386
2258
  throw new CliError(
1387
- "[X] Target directory already exists. Use --force to overwrite."
2259
+ "Target directory already exists. Use --force to overwrite it.",
2260
+ {
2261
+ code: "TARGET_EXISTS",
2262
+ scope: "project",
2263
+ path: "."
2264
+ }
1388
2265
  );
1389
2266
  }
1390
2267
  }
1391
- try {
1392
- terminal.log("Creating project directory...");
1393
- await mkdir7(targetPath, { recursive: true });
1394
- terminal.log("Copying files from template...");
1395
- await copyTemplate(templatePath, targetPath);
1396
- await customizeGeneratedProject(targetPath, projectName, importAlias);
1397
- await writeFile10(
1398
- join8(targetPath, "cnp.config.json"),
1399
- `${JSON.stringify({ ...options, projectName, importAlias }, null, 2)}
1400
- `
1401
- );
1402
- terminal.log("Project setup complete!");
1403
- terminal.log("");
1404
- terminal.log("To get started:");
1405
- terminal.log(" " + green(`cd ${options.projectName}`));
1406
- terminal.log("");
1407
- terminal.log(
1408
- "Then install dependencies and launch the dev server with your preferred tool:"
1409
- );
1410
- terminal.log(" " + green(`bun install && bun dev`));
1411
- terminal.log(" " + green(`npm install && npm run dev`));
1412
- terminal.log(" " + green(`pnpm install && pnpm run dev`));
1413
- terminal.log("");
1414
- terminal.log("Documentation and examples can be found at:");
1415
- terminal.log(
1416
- " " + cyan("https://github.com/Rising-Corporation/create-next-pro-cli")
1417
- );
1418
- terminal.log(
1419
- "_-`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`-_"
1420
- );
1421
- } catch (err) {
1422
- terminal.error(red("[X] Error during project creation:"), err);
1423
- throw new CliError("[X] Error during project creation:");
1424
- }
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" }
2288
+ }
2289
+ );
2290
+ return { projectRoot: targetPath, copiedFiles: manifest.length, importAlias };
1425
2291
  }
1426
2292
 
1427
2293
  // src/lib/createProject.ts
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
+ }
1428
2333
  async function createProject(nameArg, force, context) {
1429
2334
  const response = {
1430
2335
  projectName: nameArg,
@@ -1438,12 +2343,7 @@ async function createProject(nameArg, force, context) {
1438
2343
  importAlias: "@/*",
1439
2344
  force
1440
2345
  };
1441
- const terminal = context?.terminal ?? console;
1442
- terminal.log(`Creating project "${response.projectName}"...`);
1443
- await scaffoldProject(response, {
1444
- cwd: context?.cwd ?? process.cwd(),
1445
- terminal
1446
- });
2346
+ return createProjectFromOptions(response, context);
1447
2347
  }
1448
2348
 
1449
2349
  // src/lib/createProjectWithPrompt.ts
@@ -1518,31 +2418,135 @@ async function createProjectWithPrompt(context) {
1518
2418
  initial: "@core/*"
1519
2419
  }
1520
2420
  ]);
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"
2434
+ });
2435
+ }
1521
2436
  const options = response;
1522
- context.terminal.log("\nYour choices:");
1523
- context.terminal.log(options);
1524
- await scaffoldProject(options, {
1525
- cwd: context.cwd,
1526
- terminal: context.terminal
1527
- });
2437
+ return createProjectFromOptions(options, context);
2438
+ }
2439
+
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}`);
2464
+ }
2465
+ current = parent;
2466
+ }
2467
+ }
2468
+ function resolvePackageRoot(metaUrl = import.meta.url) {
2469
+ return findPackageRoot(path9.dirname(fileURLToPath2(metaUrl)));
2470
+ }
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);
2485
+ },
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
+ );
2525
+ }
2526
+ },
2527
+ operations: new OperationJournal(),
2528
+ outputMode: "human",
2529
+ ...overrides
2530
+ };
1528
2531
  }
1529
2532
 
1530
2533
  // src/index.ts
1531
2534
  var HELP_TEXT = `create-next-pro
1532
2535
 
1533
2536
  Usage:
1534
- create-next-pro <project-name> [--force]
1535
- create-next-pro addpage [options]
1536
- create-next-pro addcomponent [options]
1537
- create-next-pro addlib [name]
1538
- create-next-pro addapi [name]
1539
- create-next-pro addlanguage [locale]
1540
- create-next-pro addtext <path> [text]
1541
- 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]
1542
2545
 
1543
2546
  Options:
1544
2547
  --help Show this help message
1545
2548
  --version Show the CLI version
2549
+ --json Emit one deterministic JSON document
1546
2550
  --reconfigure Run the configuration assistant again
1547
2551
  `;
1548
2552
  function parseOptions(args) {
@@ -1550,51 +2554,114 @@ function parseOptions(args) {
1550
2554
  force: args.includes("--force"),
1551
2555
  help: args.includes("--help"),
1552
2556
  version: args.includes("--version") || args.includes("-v"),
1553
- reconfigure: args.includes("--reconfigure")
2557
+ reconfigure: args.includes("--reconfigure"),
2558
+ json: args.includes("--json")
1554
2559
  };
1555
2560
  }
2561
+ function withoutGlobalOptions(args) {
2562
+ return args.filter((argument) => argument !== "--json");
2563
+ }
1556
2564
  async function packageVersion(context) {
1557
2565
  const packageJson = JSON.parse(
1558
- await context.fs.readText(path8.join(context.packageRoot, "package.json"))
2566
+ await context.fs.readText(path10.join(context.packageRoot, "package.json"))
1559
2567
  );
1560
2568
  return packageJson.version;
1561
2569
  }
1562
- async function main(context = createNodeContext()) {
1563
- try {
1564
- const args = [...context.argv];
1565
- const options = parseOptions(args);
1566
- const version = await packageVersion(context);
1567
- if (args[0] === "__complete") {
1568
- await printCompletions(args, context);
1569
- return 0;
1570
- }
1571
- if (options.help) {
1572
- context.terminal.log(HELP_TEXT);
1573
- return 0;
1574
- }
1575
- if (options.version) {
1576
- context.terminal.log(`v${version}`);
1577
- return 0;
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
+ });
2589
+ }
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
+ });
2598
+ }
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
+ );
1578
2606
  }
1579
- if (options.reconfigure || !await readConfig(context)) {
1580
- await onboarding(context, version);
1581
- return 0;
2607
+ return onboarding(context, version);
2608
+ }
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
+ });
1582
2617
  }
1583
- if (args[0] === "addpage" && args.length === 1) args.push("-LPl");
1584
- const handler = args[0] ? createCommandRegistry().get(args[0]) : void 0;
1585
- if (handler) return (await handler(args, context)).exitCode;
1586
- const nameArg = args.find((arg) => !arg.startsWith("--"));
1587
- if (nameArg) {
1588
- await createProject(nameArg, options.force, context);
1589
- return 0;
2618
+ return onboarding(context, version);
2619
+ }
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
+ );
2632
+ }
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);
1590
2639
  }
1591
- await createProjectWithPrompt(context);
1592
2640
  return 0;
2641
+ } catch {
2642
+ return 1;
2643
+ }
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);
1593
2655
  } catch (error) {
1594
- const message = error instanceof Error ? error.message : String(error);
1595
- context.terminal.error(message);
1596
- return error instanceof CliError ? error.exitCode : 1;
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
+ });
1597
2662
  }
2663
+ renderResult(result, context, context.outputMode);
2664
+ return result.exitCode;
1598
2665
  }
1599
2666
 
1600
2667
  // bin.node.ts