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