create-next-pro-cli 0.1.27 → 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -6
- package/dist/bin.bun.js +1933 -947
- package/dist/bin.node.js +2079 -998
- package/dist/bin.node.js.map +1 -1
- package/package.json +3 -2
- package/templates/Projects/default/.env.example +8 -6
- package/templates/Projects/default/.github/workflows/quality.yml +40 -9
- package/templates/Projects/default/.gitignore.template +33 -15
- package/templates/Projects/default/README.md +38 -9
- package/templates/Projects/default/bun.lock +120 -134
- package/templates/Projects/default/messages/en/_home.json +9 -2
- package/templates/Projects/default/messages/en/settings.json +9 -1
- package/templates/Projects/default/messages/en/userInfo.json +6 -1
- package/templates/Projects/default/messages/fr/_home.json +9 -2
- package/templates/Projects/default/messages/fr/settings.json +9 -1
- package/templates/Projects/default/messages/fr/userInfo.json +6 -1
- package/templates/Projects/default/next.config.ts +11 -0
- package/templates/Projects/default/package.json +7 -1
- package/templates/Projects/default/playwright.config.ts +29 -6
- package/templates/Projects/default/pnpm-workspace.yaml +2 -0
- package/templates/Projects/default/scripts/audit.ts +4 -0
- package/templates/Projects/default/scripts/package-manager.ts +68 -0
- package/templates/Projects/default/src/app/[locale]/(public)/_home/page.tsx +2 -2
- package/templates/Projects/default/src/app/[locale]/(public)/layout.tsx +1 -2
- package/templates/Projects/default/src/app/[locale]/(public)/login/page.tsx +6 -1
- package/templates/Projects/default/src/app/[locale]/(public)/register/page.tsx +6 -1
- package/templates/Projects/default/src/app/[locale]/(user)/userInfo/page.tsx +5 -2
- package/templates/Projects/default/src/app/[locale]/page.tsx +1 -4
- package/templates/Projects/default/src/lib/github/repository.ts +68 -0
- package/templates/Projects/default/src/ui/_global/GlobalMain.tsx +10 -5
- package/templates/Projects/default/src/ui/_global/ThemeToggle.tsx +18 -8
- package/templates/Projects/default/src/ui/_global/UserNav.tsx +5 -1
- package/templates/Projects/default/src/ui/_home/GitHubActions.tsx +57 -0
- package/templates/Projects/default/src/ui/_home/page-shell.tsx +16 -0
- package/templates/Projects/default/src/ui/_home/page-ui.tsx +24 -13
- package/templates/Projects/default/src/ui/dashboard/LogoutButton.tsx +1 -1
- package/templates/Projects/default/src/ui/dashboard/StatsCard.tsx +2 -2
- package/templates/Projects/default/src/ui/dashboard/WelcomeCard.tsx +4 -2
- package/templates/Projects/default/src/ui/dashboard/page-ui.tsx +0 -2
- package/templates/Projects/default/src/ui/settings/page-ui.tsx +45 -6
- package/templates/Projects/default/src/ui/userInfo/page-ui.tsx +54 -7
- package/templates/Projects/default/tests/consumer/validate-template.ts +57 -48
- package/templates/Projects/default/tests/e2e/template-remediation.playwright.ts +47 -4
- package/templates/Projects/default/tests/unit/csp.test.ts +1 -1
- package/templates/Projects/default/tests/unit/github-repository.test.ts +45 -0
- package/templates/Projects/default/tests/unit/package-manager.test.ts +51 -0
- package/templates/Projects/default/tests/unit/template-baseline.test.ts +52 -0
- package/templates/Projects/default/tests/unit/user-nav.test.tsx +52 -0
- package/templates/Projects/default/tsconfig.json +2 -1
- package/templates/Projects/default/vitest.config.ts +10 -0
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
|
|
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
|
|
110
|
-
import path3 from "path";
|
|
7
|
+
import path2 from "path";
|
|
111
8
|
|
|
112
9
|
// src/core/page-catalog.ts
|
|
113
|
-
import
|
|
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 =
|
|
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
|
|
20
|
+
entries = await fs.list(directory);
|
|
125
21
|
} catch {
|
|
126
22
|
return;
|
|
127
23
|
}
|
|
128
|
-
if (entries.some((entry) => entry.isFile
|
|
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:
|
|
141
|
-
messageFile:
|
|
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
|
|
153
|
-
await visit(
|
|
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
|
|
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(
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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(
|
|
252
|
-
const configPath = join(cwd, "cnp.config.json");
|
|
253
|
-
if (!
|
|
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
|
|
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
|
|
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 (
|
|
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) ||
|
|
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 =
|
|
382
|
-
const target =
|
|
383
|
-
if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${
|
|
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,
|
|
392
|
-
const relativeSegments =
|
|
393
|
-
let current =
|
|
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 =
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
|
669
|
+
var addApi = async (args, context) => {
|
|
421
670
|
let apiName = args[1];
|
|
422
671
|
if (!apiName || apiName.startsWith("-")) {
|
|
423
|
-
|
|
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: "
|
|
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(
|
|
702
|
+
const config = await loadConfig(context);
|
|
433
703
|
if (!config) {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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 (!
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
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: "
|
|
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(
|
|
500
|
-
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
|
509
|
-
const
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
"
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
536
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
if (
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
current
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
591
|
-
|
|
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
|
-
|
|
597
|
-
|
|
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
|
|
956
|
+
const names = new Intl.DisplayNames(["en"], { type: "language" });
|
|
608
957
|
const locales = [];
|
|
609
|
-
for (let
|
|
610
|
-
for (let
|
|
611
|
-
const code = String.fromCharCode(97 +
|
|
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 =
|
|
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
|
|
624
|
-
const
|
|
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
|
-
|
|
627
|
-
|
|
992
|
+
throw new CliError("Internationalization is not enabled in this project.", {
|
|
993
|
+
code: "I18N_DISABLED"
|
|
994
|
+
});
|
|
628
995
|
}
|
|
629
|
-
const
|
|
630
|
-
if (!
|
|
631
|
-
|
|
632
|
-
|
|
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
|
|
637
|
-
|
|
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: "
|
|
641
|
-
choices: available.map((
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
|
653
|
-
if (!
|
|
654
|
-
throw new
|
|
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
|
-
|
|
657
|
-
|
|
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
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
|
670
|
-
if (
|
|
671
|
-
throw new
|
|
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
|
|
674
|
-
|
|
675
|
-
|
|
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
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
const registeredLocales = registryMatch[1].split(",").map((value) => value.trim()).filter(Boolean);
|
|
1127
|
+
const targetDirectory = join4(messagesRoot, locale);
|
|
1128
|
+
const targetAggregator = join4(messagesRoot, `${locale}.ts`);
|
|
1129
|
+
const targetAggregatorExists = context.fs.exists(targetAggregator);
|
|
1130
|
+
const targetAggregatorContent = targetAggregatorExists ? await context.fs.readText(targetAggregator) : "";
|
|
1131
|
+
const state = {
|
|
1132
|
+
directory: context.fs.exists(targetDirectory),
|
|
1133
|
+
aggregator: targetAggregatorExists,
|
|
1134
|
+
aggregatorImportsLocale: targetAggregatorContent.includes(`./${locale}/`),
|
|
1135
|
+
routing: routingLocales.includes(locale),
|
|
1136
|
+
registry: registeredLocales.includes(locale),
|
|
1137
|
+
registryImport: new RegExp(
|
|
1138
|
+
`import\\s+${locale}\\s+from\\s+["']\\.\\.\\/\\.\\.\\/\\.\\.\\/messages\\/${locale}["'];`
|
|
1139
|
+
).test(registry)
|
|
1140
|
+
};
|
|
1141
|
+
const present = Object.values(state).filter(Boolean).length;
|
|
1142
|
+
const gateway = new MutationGateway(context, context.cwd);
|
|
1143
|
+
const missingLocaleFiles = state.directory ? sourceFiles.filter(
|
|
1144
|
+
(relative) => !context.fs.exists(join4(targetDirectory, relative))
|
|
1145
|
+
) : sourceFiles;
|
|
1146
|
+
if (present === Object.keys(state).length && missingLocaleFiles.length === 0) {
|
|
1147
|
+
gateway.unchanged(targetDirectory, {
|
|
1148
|
+
role: "locale-directory",
|
|
1149
|
+
resource: "directory"
|
|
1150
|
+
});
|
|
1151
|
+
for (const relative of sourceFiles) {
|
|
1152
|
+
gateway.unchanged(join4(targetDirectory, relative), {
|
|
1153
|
+
role: relative.endsWith(".json") ? "translation-messages" : "locale-file",
|
|
1154
|
+
detail: { locale, sourceLocale: defaultLocale }
|
|
1155
|
+
});
|
|
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
|
+
});
|
|
691
1165
|
}
|
|
692
|
-
|
|
693
|
-
|
|
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
|
|
697
|
-
|
|
698
|
-
|
|
1178
|
+
const nextRouting = routing.replace(
|
|
1179
|
+
/locales:\s*\[[^\]]*\]/,
|
|
1180
|
+
`locales: [${[...routingLocales, locale].map((value) => `"${value}"`).join(", ")}]`
|
|
699
1181
|
);
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
` +
|
|
715
|
-
|
|
716
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
742
|
-
validate: (name) => name ? true : "
|
|
1251
|
+
message: "Library name to add:",
|
|
1252
|
+
validate: (name) => name ? true : "Library name is required"
|
|
743
1253
|
});
|
|
744
|
-
libArg = response.libArg;
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
|
755
|
-
if (
|
|
756
|
-
|
|
757
|
-
|
|
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
|
|
762
|
-
await
|
|
763
|
-
|
|
764
|
-
|
|
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
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
|
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 (!
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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*${
|
|
1319
|
+
`import\\s*{\\s*${moduleIdentifier}\\s*}\\s*from\\s*["']\\./${fileName}["'];`
|
|
805
1320
|
);
|
|
806
1321
|
const exportRegex = /export\s*{([^}]*)}/m;
|
|
807
|
-
const imports =
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
-
|
|
832
|
-
|
|
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
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
|
847
|
-
throw new
|
|
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
|
|
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
|
-
${
|
|
1416
|
+
${identifier},
|
|
857
1417
|
};`
|
|
858
1418
|
);
|
|
859
1419
|
return content.slice(0, declarationIndex) + importStatement + content.slice(declarationIndex).replace(registryMatch[0], nextRegistry);
|
|
860
1420
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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: "
|
|
1461
|
+
message: "Page name to add:",
|
|
868
1462
|
validate: (name) => name ? true : "Page name is required"
|
|
869
1463
|
});
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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
|
-
|
|
920
|
-
|
|
921
|
-
"
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
"error",
|
|
925
|
-
"global-error",
|
|
926
|
-
"route",
|
|
927
|
-
"template",
|
|
928
|
-
"default"
|
|
929
|
-
]) {
|
|
930
|
-
if (longFlags.has("--" + flag)) flags.add(flag);
|
|
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
|
+
});
|
|
931
1486
|
}
|
|
932
|
-
const
|
|
1487
|
+
const flags = selectedFlags(args);
|
|
1488
|
+
const config = await loadConfig(context);
|
|
933
1489
|
if (!config) {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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 =
|
|
1495
|
+
const useI18n = Boolean(config.useI18n);
|
|
940
1496
|
const aliasPrefix = configuredAliasPrefix(config);
|
|
941
|
-
const
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|
-
|
|
1000
|
-
|
|
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
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
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
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
-
|
|
1658
|
+
gateway.skipped(join6(context.cwd, "messages"), {
|
|
1659
|
+
role: "translation-messages",
|
|
1660
|
+
resource: "directory",
|
|
1661
|
+
detail: { reason: "Internationalization is disabled." }
|
|
1662
|
+
});
|
|
1063
1663
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
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, (
|
|
1703
|
+
return key.replace(/_/g, " ").replace(/\b\w/g, (character) => character.toUpperCase());
|
|
1075
1704
|
}
|
|
1076
|
-
async
|
|
1705
|
+
var addText = async (args, context) => {
|
|
1077
1706
|
const pathArg = args[1];
|
|
1078
1707
|
if (!pathArg) {
|
|
1079
|
-
|
|
1080
|
-
|
|
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
|
-
|
|
1084
|
-
const
|
|
1720
|
+
const finalKey = segments.at(-1);
|
|
1721
|
+
const text = providedText || defaultText(finalKey);
|
|
1722
|
+
const config = await loadConfig(context);
|
|
1085
1723
|
if (!config?.useI18n) {
|
|
1086
|
-
|
|
1087
|
-
|
|
1724
|
+
throw new CliError("Internationalization is not enabled in this project.", {
|
|
1725
|
+
code: "I18N_DISABLED"
|
|
1726
|
+
});
|
|
1088
1727
|
}
|
|
1089
|
-
const
|
|
1090
|
-
if (!
|
|
1091
|
-
|
|
1092
|
-
|
|
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
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
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
|
|
1102
|
-
const
|
|
1744
|
+
const [fileName, ...keySegments] = segments;
|
|
1745
|
+
const prepared = [];
|
|
1103
1746
|
for (const locale of locales) {
|
|
1104
|
-
const
|
|
1105
|
-
await assertSafeTarget(cwd,
|
|
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 (
|
|
1108
|
-
const raw = await readFile8(filePath, "utf-8");
|
|
1751
|
+
if (existed) {
|
|
1109
1752
|
try {
|
|
1110
|
-
|
|
1111
|
-
|
|
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 (
|
|
1116
|
-
const
|
|
1117
|
-
if (
|
|
1118
|
-
|
|
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
|
-
|
|
1122
|
-
|
|
1788
|
+
prepared.push({
|
|
1789
|
+
locale,
|
|
1790
|
+
file,
|
|
1791
|
+
data,
|
|
1792
|
+
replaced: previous !== void 0 && previous !== text
|
|
1793
|
+
});
|
|
1123
1794
|
}
|
|
1124
|
-
|
|
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
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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
|
-
|
|
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 (
|
|
1163
|
-
throw new CliError("
|
|
1164
|
-
|
|
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: "
|
|
1885
|
+
message: "Page to remove:",
|
|
1169
1886
|
choices: candidates.map((candidate2) => ({
|
|
1170
|
-
title: candidate2.logicalName.replaceAll(".", "
|
|
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
|
-
|
|
1183
|
-
|
|
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,77 +1916,161 @@ 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)
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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
|
-
|
|
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",
|
|
1213
|
-
["addpage",
|
|
1214
|
-
["addlib",
|
|
1215
|
-
["addapi",
|
|
1216
|
-
["addlanguage",
|
|
1217
|
-
["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 {
|
|
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",
|
|
2073
|
+
".gitignore",
|
|
1262
2074
|
".agent",
|
|
1263
2075
|
".cursor",
|
|
1264
2076
|
".next",
|
|
@@ -1269,80 +2081,142 @@ var TEMPLATE_DENY_NAMES = /* @__PURE__ */ new Set([
|
|
|
1269
2081
|
"test-results"
|
|
1270
2082
|
]);
|
|
1271
2083
|
function isDistributableTemplatePath(relativePath) {
|
|
1272
|
-
const segments = relativePath.split(
|
|
2084
|
+
const segments = relativePath.split(path8.sep);
|
|
1273
2085
|
return !segments.some(
|
|
1274
|
-
(segment) => TEMPLATE_DENY_NAMES.has(segment) || segment.endsWith(".tsbuildinfo") || segment === ".DS_Store"
|
|
2086
|
+
(segment) => TEMPLATE_DENY_NAMES.has(segment) || segment.startsWith(".env") && segment !== ".env.example" || segment.endsWith(".tsbuildinfo") || segment === ".DS_Store"
|
|
1275
2087
|
);
|
|
1276
2088
|
}
|
|
1277
|
-
async function templateManifest(root) {
|
|
2089
|
+
async function templateManifest(root, fs) {
|
|
1278
2090
|
const files = [];
|
|
1279
2091
|
async function visit(directory, relativeDirectory = "") {
|
|
1280
|
-
const entries = await
|
|
1281
|
-
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
2092
|
+
const entries = await fs.list(directory);
|
|
1282
2093
|
for (const entry of entries) {
|
|
1283
|
-
const relative =
|
|
2094
|
+
const relative = path8.join(relativeDirectory, entry.name);
|
|
1284
2095
|
if (!isDistributableTemplatePath(relative)) continue;
|
|
1285
|
-
const source =
|
|
1286
|
-
|
|
1287
|
-
if (stats.isSymbolicLink()) {
|
|
2096
|
+
const source = path8.join(directory, entry.name);
|
|
2097
|
+
if (entry.isSymbolicLink) {
|
|
1288
2098
|
throw new CliError(
|
|
1289
|
-
`Symbolic links are forbidden in templates: ${relative}
|
|
2099
|
+
`Symbolic links are forbidden in templates: ${relative}`,
|
|
2100
|
+
{ code: "UNSAFE_PATH", scope: "package", path: relative }
|
|
1290
2101
|
);
|
|
1291
2102
|
}
|
|
1292
|
-
if (
|
|
1293
|
-
else if (
|
|
1294
|
-
else
|
|
2103
|
+
if (entry.isDirectory) await visit(source, relative);
|
|
2104
|
+
else if (entry.isFile) files.push(relative);
|
|
2105
|
+
else
|
|
2106
|
+
throw new CliError(`Unsupported template entry: ${relative}`, {
|
|
2107
|
+
code: "TEMPLATE_MISSING",
|
|
2108
|
+
scope: "package",
|
|
2109
|
+
path: relative
|
|
2110
|
+
});
|
|
1295
2111
|
}
|
|
1296
2112
|
}
|
|
1297
2113
|
await visit(root);
|
|
1298
2114
|
return files;
|
|
1299
2115
|
}
|
|
1300
|
-
async function
|
|
1301
|
-
const manifest = await templateManifest(root);
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
2116
|
+
async function validateScaffoldTemplate(root, fs) {
|
|
2117
|
+
const manifest = await templateManifest(root, fs);
|
|
2118
|
+
for (const required of [
|
|
2119
|
+
"package.json",
|
|
2120
|
+
"tsconfig.json",
|
|
2121
|
+
".env.example",
|
|
2122
|
+
".gitignore.template",
|
|
2123
|
+
".prettierignore",
|
|
2124
|
+
"bun.lock",
|
|
2125
|
+
"pnpm-workspace.yaml",
|
|
2126
|
+
"vitest.config.ts",
|
|
2127
|
+
path8.join("scripts", "audit.ts"),
|
|
2128
|
+
path8.join("scripts", "package-manager.ts"),
|
|
2129
|
+
path8.join("tests", "consumer", "validate-template.ts"),
|
|
2130
|
+
path8.join(".github", "workflows", "quality.yml")
|
|
2131
|
+
]) {
|
|
2132
|
+
if (!manifest.includes(required)) {
|
|
2133
|
+
throw new CliError(`Required template file was not found: ${required}.`, {
|
|
2134
|
+
code: "TEMPLATE_MISSING",
|
|
2135
|
+
scope: "package",
|
|
2136
|
+
path: `templates/Projects/default/${required}`
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
try {
|
|
2141
|
+
JSON.parse(await fs.readText(path8.join(root, "package.json")));
|
|
2142
|
+
const tsconfig = JSON.parse(
|
|
2143
|
+
await fs.readText(path8.join(root, "tsconfig.json"))
|
|
2144
|
+
);
|
|
2145
|
+
if (!tsconfig.compilerOptions?.paths?.["@/*"]) throw new Error("alias");
|
|
2146
|
+
} catch {
|
|
2147
|
+
throw new CliError(
|
|
2148
|
+
'The template manifests must be valid JSON and define the default "@/*" alias.',
|
|
2149
|
+
{
|
|
2150
|
+
code: "TEMPLATE_MISSING",
|
|
2151
|
+
scope: "package",
|
|
2152
|
+
path: "templates/Projects/default"
|
|
2153
|
+
}
|
|
2154
|
+
);
|
|
2155
|
+
}
|
|
2156
|
+
return manifest;
|
|
2157
|
+
}
|
|
2158
|
+
async function copyTemplate(root, target, context, gateway, manifest) {
|
|
2159
|
+
const files = manifest ?? await templateManifest(root, context.fs);
|
|
2160
|
+
for (const relative of files) {
|
|
2161
|
+
const destination = path8.join(
|
|
1305
2162
|
target,
|
|
1306
2163
|
relative === ".gitignore.template" ? ".gitignore" : relative
|
|
1307
2164
|
);
|
|
1308
|
-
|
|
1309
|
-
await
|
|
2165
|
+
const source = path8.join(root, relative);
|
|
2166
|
+
await gateway.copy(source, destination, {
|
|
2167
|
+
role: "template-file",
|
|
2168
|
+
source: { template: `Projects/default/${relative}` }
|
|
2169
|
+
});
|
|
1310
2170
|
}
|
|
1311
|
-
return
|
|
2171
|
+
return files;
|
|
1312
2172
|
}
|
|
1313
|
-
async function customizeGeneratedProject(target, projectName, importAlias) {
|
|
1314
|
-
const packagePath =
|
|
1315
|
-
const packageJson = JSON.parse(
|
|
2173
|
+
async function customizeGeneratedProject(target, projectName, importAlias, context, gateway) {
|
|
2174
|
+
const packagePath = path8.join(target, "package.json");
|
|
2175
|
+
const packageJson = JSON.parse(
|
|
2176
|
+
await context.fs.readText(packagePath)
|
|
2177
|
+
);
|
|
1316
2178
|
packageJson.name = projectName.toLowerCase();
|
|
1317
2179
|
delete packageJson.packageManager;
|
|
1318
|
-
await
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
2180
|
+
await gateway.write(
|
|
2181
|
+
packagePath,
|
|
2182
|
+
`${JSON.stringify(packageJson, null, 2)}
|
|
2183
|
+
`,
|
|
2184
|
+
{ role: "package-manifest" }
|
|
2185
|
+
);
|
|
2186
|
+
const tsconfigPath = path8.join(target, "tsconfig.json");
|
|
2187
|
+
const tsconfigContent = await context.fs.readText(tsconfigPath);
|
|
1322
2188
|
const tsconfig = JSON.parse(tsconfigContent);
|
|
1323
2189
|
if (!tsconfig.compilerOptions?.paths?.["@/*"]) {
|
|
1324
2190
|
throw new CliError(
|
|
1325
|
-
'The template tsconfig must define the default "@/*" alias.'
|
|
2191
|
+
'The template tsconfig must define the default "@/*" alias.',
|
|
2192
|
+
{
|
|
2193
|
+
code: "TEMPLATE_MISSING",
|
|
2194
|
+
scope: "package",
|
|
2195
|
+
path: "templates/Projects/default/tsconfig.json"
|
|
2196
|
+
}
|
|
1326
2197
|
);
|
|
1327
2198
|
}
|
|
1328
|
-
await
|
|
2199
|
+
await gateway.write(
|
|
1329
2200
|
tsconfigPath,
|
|
1330
|
-
tsconfigContent.replace('"@/*"', `"${importAlias}"`)
|
|
2201
|
+
tsconfigContent.replace('"@/*"', `"${importAlias}"`),
|
|
2202
|
+
{ role: "typescript-configuration" }
|
|
1331
2203
|
);
|
|
1332
2204
|
if (importAlias !== "@/*") {
|
|
1333
2205
|
const prefix = importAlias.slice(0, -2);
|
|
1334
|
-
for (const relative of await templateManifest(target)) {
|
|
2206
|
+
for (const relative of await templateManifest(target, context.fs)) {
|
|
1335
2207
|
if (!/\.[cm]?[jt]sx?$/.test(relative)) continue;
|
|
1336
|
-
const file =
|
|
1337
|
-
const content = await
|
|
2208
|
+
const file = path8.join(target, relative);
|
|
2209
|
+
const content = await context.fs.readText(file);
|
|
1338
2210
|
const next = content.replaceAll('from "@/', `from "${prefix}/`).replaceAll("from '@/", `from '${prefix}/`);
|
|
1339
|
-
if (next !== content)
|
|
2211
|
+
if (next !== content) {
|
|
2212
|
+
await gateway.write(file, next, { role: "import-alias-reference" });
|
|
2213
|
+
}
|
|
1340
2214
|
}
|
|
1341
2215
|
}
|
|
1342
2216
|
}
|
|
1343
2217
|
|
|
1344
2218
|
// src/scaffold.ts
|
|
1345
|
-
async function scaffoldProject(options, runtime
|
|
2219
|
+
async function scaffoldProject(options, runtime) {
|
|
1346
2220
|
const requiredFeatures = [
|
|
1347
2221
|
"useTypescript",
|
|
1348
2222
|
"useEslint",
|
|
@@ -1356,75 +2230,120 @@ async function scaffoldProject(options, runtime = {}) {
|
|
|
1356
2230
|
);
|
|
1357
2231
|
if (unsupported.length > 0) {
|
|
1358
2232
|
throw new CliError(
|
|
1359
|
-
`The default Next.js 16 template requires: ${unsupported.join(", ")}
|
|
2233
|
+
`The default Next.js 16 template requires: ${unsupported.join(", ")}.`,
|
|
2234
|
+
{ code: "INVALID_ARGUMENT" }
|
|
1360
2235
|
);
|
|
1361
2236
|
}
|
|
1362
|
-
const
|
|
1363
|
-
const
|
|
2237
|
+
const { context } = runtime;
|
|
2238
|
+
const cwd = context.cwd;
|
|
1364
2239
|
const projectName = validateProjectName(options.projectName);
|
|
1365
2240
|
const importAlias = normalizeImportAlias(
|
|
1366
2241
|
options.customAlias === false ? "@/*" : options.importAlias || "@/*"
|
|
1367
2242
|
);
|
|
1368
2243
|
const targetPath = join8(cwd, projectName);
|
|
1369
2244
|
const __dirname = new URL(".", import.meta.url);
|
|
1370
|
-
const templatePath = runtime.templatePath ?? join8(
|
|
2245
|
+
const templatePath = runtime.templatePath ?? join8(fileURLToPath(__dirname), "..", "templates", "Projects", "default");
|
|
1371
2246
|
const resolvedCwd = resolve(cwd);
|
|
1372
2247
|
const resolvedTarget = resolve(targetPath);
|
|
1373
2248
|
if (!resolvedTarget.startsWith(`${resolvedCwd}/`)) {
|
|
1374
2249
|
throw new CliError(
|
|
1375
|
-
"The project destination must be a child of the current directory."
|
|
2250
|
+
"The project destination must be a child of the current directory.",
|
|
2251
|
+
{ code: "UNSAFE_PATH" }
|
|
1376
2252
|
);
|
|
1377
2253
|
}
|
|
1378
|
-
|
|
2254
|
+
const gateway = new MutationGateway(context, targetPath);
|
|
2255
|
+
const manifest = await validateScaffoldTemplate(templatePath, context.fs);
|
|
2256
|
+
if (context.fs.exists(targetPath)) {
|
|
1379
2257
|
if (options.force) {
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
2258
|
+
if (context.outputMode === "human") {
|
|
2259
|
+
context.terminal.warn(
|
|
2260
|
+
`WARNING: --force will remove the existing project at ${targetPath}.`
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
await gateway.remove(
|
|
2264
|
+
targetPath,
|
|
2265
|
+
{
|
|
2266
|
+
role: "existing-project",
|
|
2267
|
+
resource: "project"
|
|
2268
|
+
},
|
|
2269
|
+
{ recursive: true, force: true }
|
|
1385
2270
|
);
|
|
2271
|
+
} else {
|
|
1386
2272
|
throw new CliError(
|
|
1387
|
-
"
|
|
2273
|
+
"Target directory already exists. Use --force to overwrite it.",
|
|
2274
|
+
{
|
|
2275
|
+
code: "TARGET_EXISTS",
|
|
2276
|
+
scope: "project",
|
|
2277
|
+
path: "."
|
|
2278
|
+
}
|
|
1388
2279
|
);
|
|
1389
2280
|
}
|
|
1390
2281
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
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
|
-
}
|
|
2282
|
+
await gateway.mkdir(targetPath, {
|
|
2283
|
+
resource: "project",
|
|
2284
|
+
role: "project-root"
|
|
2285
|
+
});
|
|
2286
|
+
await copyTemplate(templatePath, targetPath, context, gateway, manifest);
|
|
2287
|
+
await customizeGeneratedProject(
|
|
2288
|
+
targetPath,
|
|
2289
|
+
projectName,
|
|
2290
|
+
importAlias,
|
|
2291
|
+
context,
|
|
2292
|
+
gateway
|
|
2293
|
+
);
|
|
2294
|
+
await gateway.write(
|
|
2295
|
+
join8(targetPath, "cnp.config.json"),
|
|
2296
|
+
`${JSON.stringify({ ...options, projectName, importAlias }, null, 2)}
|
|
2297
|
+
`,
|
|
2298
|
+
{
|
|
2299
|
+
role: "project-configuration",
|
|
2300
|
+
resource: "configuration",
|
|
2301
|
+
detail: { importAlias, packageManager: "user-selected" }
|
|
2302
|
+
}
|
|
2303
|
+
);
|
|
2304
|
+
return { projectRoot: targetPath, copiedFiles: manifest.length, importAlias };
|
|
1425
2305
|
}
|
|
1426
2306
|
|
|
1427
2307
|
// src/lib/createProject.ts
|
|
2308
|
+
async function createProjectFromOptions(options, context) {
|
|
2309
|
+
const { projectRoot, copiedFiles, importAlias } = await scaffoldProject(
|
|
2310
|
+
options,
|
|
2311
|
+
{
|
|
2312
|
+
context
|
|
2313
|
+
}
|
|
2314
|
+
);
|
|
2315
|
+
return commandResult(context, {
|
|
2316
|
+
command: "create",
|
|
2317
|
+
summary: `Created project "${options.projectName}" with ${copiedFiles} template files and import alias ${importAlias}; no package manager was pinned.`,
|
|
2318
|
+
projectRoot,
|
|
2319
|
+
nextSteps: [
|
|
2320
|
+
{
|
|
2321
|
+
kind: "install",
|
|
2322
|
+
required: true,
|
|
2323
|
+
message: "Install dependencies with your preferred package manager.",
|
|
2324
|
+
paths: [],
|
|
2325
|
+
commands: [
|
|
2326
|
+
`cd ${options.projectName} && bun install`,
|
|
2327
|
+
`cd ${options.projectName} && npm install`,
|
|
2328
|
+
`cd ${options.projectName} && pnpm install`
|
|
2329
|
+
]
|
|
2330
|
+
},
|
|
2331
|
+
{
|
|
2332
|
+
kind: "run-checks",
|
|
2333
|
+
required: true,
|
|
2334
|
+
message: "Run the generated project checks before development.",
|
|
2335
|
+
paths: [{ scope: "project", path: "package.json" }],
|
|
2336
|
+
commands: ["bun run check", "npm run check", "pnpm run check"]
|
|
2337
|
+
}
|
|
2338
|
+
],
|
|
2339
|
+
data: {
|
|
2340
|
+
projectName: options.projectName,
|
|
2341
|
+
importAlias,
|
|
2342
|
+
packageManager: null,
|
|
2343
|
+
copiedFiles
|
|
2344
|
+
}
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
1428
2347
|
async function createProject(nameArg, force, context) {
|
|
1429
2348
|
const response = {
|
|
1430
2349
|
projectName: nameArg,
|
|
@@ -1438,12 +2357,7 @@ async function createProject(nameArg, force, context) {
|
|
|
1438
2357
|
importAlias: "@/*",
|
|
1439
2358
|
force
|
|
1440
2359
|
};
|
|
1441
|
-
|
|
1442
|
-
terminal.log(`Creating project "${response.projectName}"...`);
|
|
1443
|
-
await scaffoldProject(response, {
|
|
1444
|
-
cwd: context?.cwd ?? process.cwd(),
|
|
1445
|
-
terminal
|
|
1446
|
-
});
|
|
2360
|
+
return createProjectFromOptions(response, context);
|
|
1447
2361
|
}
|
|
1448
2362
|
|
|
1449
2363
|
// src/lib/createProjectWithPrompt.ts
|
|
@@ -1518,31 +2432,135 @@ async function createProjectWithPrompt(context) {
|
|
|
1518
2432
|
initial: "@core/*"
|
|
1519
2433
|
}
|
|
1520
2434
|
]);
|
|
2435
|
+
if (!response.projectName) {
|
|
2436
|
+
context.operations.record({
|
|
2437
|
+
action: "cancelled",
|
|
2438
|
+
resource: "command",
|
|
2439
|
+
role: "project-creation",
|
|
2440
|
+
scope: "project",
|
|
2441
|
+
path: "."
|
|
2442
|
+
});
|
|
2443
|
+
return commandResult(context, {
|
|
2444
|
+
command: "create",
|
|
2445
|
+
summary: "Project creation was cancelled.",
|
|
2446
|
+
projectRoot: context.cwd,
|
|
2447
|
+
status: "cancelled"
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
1521
2450
|
const options = response;
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
2451
|
+
return createProjectFromOptions(options, context);
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
// src/runtime/node-context.ts
|
|
2455
|
+
import { existsSync } from "fs";
|
|
2456
|
+
import {
|
|
2457
|
+
appendFile,
|
|
2458
|
+
copyFile,
|
|
2459
|
+
lstat,
|
|
2460
|
+
mkdir,
|
|
2461
|
+
readdir,
|
|
2462
|
+
readFile,
|
|
2463
|
+
rm,
|
|
2464
|
+
writeFile
|
|
2465
|
+
} from "fs/promises";
|
|
2466
|
+
import os from "os";
|
|
2467
|
+
import path9 from "path";
|
|
2468
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2469
|
+
import prompts from "prompts";
|
|
2470
|
+
function findPackageRoot(start) {
|
|
2471
|
+
let current = start;
|
|
2472
|
+
while (true) {
|
|
2473
|
+
const packagePath = path9.join(current, "package.json");
|
|
2474
|
+
if (existsSync(packagePath)) return current;
|
|
2475
|
+
const parent = path9.dirname(current);
|
|
2476
|
+
if (parent === current) {
|
|
2477
|
+
throw new Error(`Unable to locate package.json from ${start}`);
|
|
2478
|
+
}
|
|
2479
|
+
current = parent;
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
function resolvePackageRoot(metaUrl = import.meta.url) {
|
|
2483
|
+
return findPackageRoot(path9.dirname(fileURLToPath2(metaUrl)));
|
|
2484
|
+
}
|
|
2485
|
+
function createNodeContext(overrides = {}) {
|
|
2486
|
+
return {
|
|
2487
|
+
argv: process.argv.slice(2),
|
|
2488
|
+
cwd: process.cwd(),
|
|
2489
|
+
env: process.env,
|
|
2490
|
+
homeDir: os.homedir(),
|
|
2491
|
+
packageRoot: resolvePackageRoot(),
|
|
2492
|
+
terminal: console,
|
|
2493
|
+
prompt: prompts,
|
|
2494
|
+
fs: {
|
|
2495
|
+
exists: existsSync,
|
|
2496
|
+
readText: (target) => readFile(target, "utf8"),
|
|
2497
|
+
writeText: async (target, content) => {
|
|
2498
|
+
await writeFile(target, content);
|
|
2499
|
+
},
|
|
2500
|
+
mkdir: async (target) => {
|
|
2501
|
+
await mkdir(target, { recursive: true });
|
|
2502
|
+
},
|
|
2503
|
+
copyFile: async (source, target) => {
|
|
2504
|
+
await copyFile(source, target);
|
|
2505
|
+
},
|
|
2506
|
+
appendText: async (target, content) => {
|
|
2507
|
+
await appendFile(target, content);
|
|
2508
|
+
},
|
|
2509
|
+
remove: async (target, options) => {
|
|
2510
|
+
await rm(target, options);
|
|
2511
|
+
},
|
|
2512
|
+
inspect: async (target) => {
|
|
2513
|
+
try {
|
|
2514
|
+
const stats = await lstat(target);
|
|
2515
|
+
return {
|
|
2516
|
+
name: target,
|
|
2517
|
+
isFile: stats.isFile(),
|
|
2518
|
+
isDirectory: stats.isDirectory(),
|
|
2519
|
+
isSymbolicLink: stats.isSymbolicLink()
|
|
2520
|
+
};
|
|
2521
|
+
} catch (error) {
|
|
2522
|
+
if (error.code === "ENOENT") return null;
|
|
2523
|
+
throw error;
|
|
2524
|
+
}
|
|
2525
|
+
},
|
|
2526
|
+
list: async (target) => {
|
|
2527
|
+
const entries = await readdir(target, { withFileTypes: true });
|
|
2528
|
+
return Promise.all(
|
|
2529
|
+
entries.sort((left, right) => left.name.localeCompare(right.name)).map(async (entry) => {
|
|
2530
|
+
const stats = await lstat(path9.join(target, entry.name));
|
|
2531
|
+
return {
|
|
2532
|
+
name: entry.name,
|
|
2533
|
+
isFile: stats.isFile(),
|
|
2534
|
+
isDirectory: stats.isDirectory(),
|
|
2535
|
+
isSymbolicLink: stats.isSymbolicLink()
|
|
2536
|
+
};
|
|
2537
|
+
})
|
|
2538
|
+
);
|
|
2539
|
+
}
|
|
2540
|
+
},
|
|
2541
|
+
operations: new OperationJournal(),
|
|
2542
|
+
outputMode: "human",
|
|
2543
|
+
...overrides
|
|
2544
|
+
};
|
|
1528
2545
|
}
|
|
1529
2546
|
|
|
1530
2547
|
// src/index.ts
|
|
1531
2548
|
var HELP_TEXT = `create-next-pro
|
|
1532
2549
|
|
|
1533
2550
|
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]
|
|
2551
|
+
create-next-pro <project-name> [--force] [--json]
|
|
2552
|
+
create-next-pro addpage [options] [--json]
|
|
2553
|
+
create-next-pro addcomponent [options] [--json]
|
|
2554
|
+
create-next-pro addlib [name] [--json]
|
|
2555
|
+
create-next-pro addapi [name] [--json]
|
|
2556
|
+
create-next-pro addlanguage [locale] [--json]
|
|
2557
|
+
create-next-pro addtext <path> [text] [--json]
|
|
2558
|
+
create-next-pro rmpage [options] [--json]
|
|
1542
2559
|
|
|
1543
2560
|
Options:
|
|
1544
2561
|
--help Show this help message
|
|
1545
2562
|
--version Show the CLI version
|
|
2563
|
+
--json Emit one deterministic JSON document
|
|
1546
2564
|
--reconfigure Run the configuration assistant again
|
|
1547
2565
|
`;
|
|
1548
2566
|
function parseOptions(args) {
|
|
@@ -1550,51 +2568,114 @@ function parseOptions(args) {
|
|
|
1550
2568
|
force: args.includes("--force"),
|
|
1551
2569
|
help: args.includes("--help"),
|
|
1552
2570
|
version: args.includes("--version") || args.includes("-v"),
|
|
1553
|
-
reconfigure: args.includes("--reconfigure")
|
|
2571
|
+
reconfigure: args.includes("--reconfigure"),
|
|
2572
|
+
json: args.includes("--json")
|
|
1554
2573
|
};
|
|
1555
2574
|
}
|
|
2575
|
+
function withoutGlobalOptions(args) {
|
|
2576
|
+
return args.filter((argument) => argument !== "--json");
|
|
2577
|
+
}
|
|
1556
2578
|
async function packageVersion(context) {
|
|
1557
2579
|
const packageJson = JSON.parse(
|
|
1558
|
-
await context.fs.readText(
|
|
2580
|
+
await context.fs.readText(path10.join(context.packageRoot, "package.json"))
|
|
1559
2581
|
);
|
|
1560
2582
|
return packageJson.version;
|
|
1561
2583
|
}
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
2584
|
+
function interactiveInputError(message, hint) {
|
|
2585
|
+
throw new CliError(message, {
|
|
2586
|
+
code: "INTERACTIVE_INPUT_REQUIRED",
|
|
2587
|
+
hint
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
async function executeCli(context) {
|
|
2591
|
+
const options = parseOptions(context.argv);
|
|
2592
|
+
context.outputMode = options.json ? "json" : "human";
|
|
2593
|
+
const args = withoutGlobalOptions(context.argv);
|
|
2594
|
+
const version = await packageVersion(context);
|
|
2595
|
+
if (options.help) {
|
|
2596
|
+
return commandResult(context, {
|
|
2597
|
+
command: "help",
|
|
2598
|
+
summary: "Displayed create-next-pro help.",
|
|
2599
|
+
projectRoot: context.cwd,
|
|
2600
|
+
status: "success",
|
|
2601
|
+
data: { help: HELP_TEXT }
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
if (options.version) {
|
|
2605
|
+
return commandResult(context, {
|
|
2606
|
+
command: "version",
|
|
2607
|
+
summary: `create-next-pro version ${version}.`,
|
|
2608
|
+
projectRoot: context.cwd,
|
|
2609
|
+
status: "success",
|
|
2610
|
+
data: { version }
|
|
2611
|
+
});
|
|
2612
|
+
}
|
|
2613
|
+
const config = await readConfig(context);
|
|
2614
|
+
if (options.reconfigure) {
|
|
2615
|
+
if (options.json) {
|
|
2616
|
+
interactiveInputError(
|
|
2617
|
+
"Reconfiguration is interactive and unavailable in JSON mode.",
|
|
2618
|
+
"Run create-next-pro --reconfigure without --json."
|
|
2619
|
+
);
|
|
1578
2620
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
2621
|
+
return onboarding(context, version);
|
|
2622
|
+
}
|
|
2623
|
+
if (!config) {
|
|
2624
|
+
if (options.json) {
|
|
2625
|
+
throw new CliError("Initial configuration is required.", {
|
|
2626
|
+
code: "ONBOARDING_REQUIRED",
|
|
2627
|
+
scope: "config",
|
|
2628
|
+
path: "config.json",
|
|
2629
|
+
hint: "Run create-next-pro once without --json, then rerun the command."
|
|
2630
|
+
});
|
|
1582
2631
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
2632
|
+
return onboarding(context, version);
|
|
2633
|
+
}
|
|
2634
|
+
const command = args[0];
|
|
2635
|
+
const handler = command ? createCommandRegistry().get(command) : void 0;
|
|
2636
|
+
if (handler) return handler(args, context);
|
|
2637
|
+
const nameArg = args.find(
|
|
2638
|
+
(argument) => !argument.startsWith("--") && argument !== "-v"
|
|
2639
|
+
);
|
|
2640
|
+
if (nameArg) return createProject(nameArg, options.force, context);
|
|
2641
|
+
if (options.json) {
|
|
2642
|
+
interactiveInputError(
|
|
2643
|
+
"A project name is required in JSON mode.",
|
|
2644
|
+
"Pass the project name as a positional argument."
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2647
|
+
return createProjectWithPrompt(context);
|
|
2648
|
+
}
|
|
2649
|
+
async function runCompletion(context, args) {
|
|
2650
|
+
try {
|
|
2651
|
+
for (const candidate of await completionCandidates(args[1], context)) {
|
|
2652
|
+
context.terminal.log(candidate);
|
|
1590
2653
|
}
|
|
1591
|
-
await createProjectWithPrompt(context);
|
|
1592
2654
|
return 0;
|
|
2655
|
+
} catch {
|
|
2656
|
+
return 1;
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
async function main(context = createNodeContext()) {
|
|
2660
|
+
const completionArgs = withoutGlobalOptions(context.argv);
|
|
2661
|
+
if (completionArgs[0] === "__complete") {
|
|
2662
|
+
return runCompletion(context, completionArgs);
|
|
2663
|
+
}
|
|
2664
|
+
context.operations.reset();
|
|
2665
|
+
context.outputMode = parseOptions(context.argv).json ? "json" : "human";
|
|
2666
|
+
let result;
|
|
2667
|
+
try {
|
|
2668
|
+
result = await executeCli(context);
|
|
1593
2669
|
} catch (error) {
|
|
1594
|
-
const
|
|
1595
|
-
context
|
|
1596
|
-
|
|
2670
|
+
const command = completionArgs[0] ?? "create";
|
|
2671
|
+
result = failedResult(context, command, error, {
|
|
2672
|
+
projectRoot: context.cwd,
|
|
2673
|
+
configRoot: configDirectory(context),
|
|
2674
|
+
homeRoot: context.homeDir
|
|
2675
|
+
});
|
|
1597
2676
|
}
|
|
2677
|
+
renderResult(result, context, context.outputMode);
|
|
2678
|
+
return result.exitCode;
|
|
1598
2679
|
}
|
|
1599
2680
|
|
|
1600
2681
|
// bin.node.ts
|