create-next-pro-cli 0.1.27 → 0.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -3
- package/dist/bin.bun.js +1919 -947
- package/dist/bin.node.js +2065 -998
- package/dist/bin.node.js.map +1 -1
- package/package.json +3 -2
package/dist/bin.bun.js
CHANGED
|
@@ -537,7 +537,7 @@ var require_prompt = __commonJS((exports, module) => {
|
|
|
537
537
|
|
|
538
538
|
// node_modules/prompts/dist/elements/text.js
|
|
539
539
|
var require_text = __commonJS((exports, module) => {
|
|
540
|
-
function asyncGeneratorStep(gen,
|
|
540
|
+
function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
|
|
541
541
|
try {
|
|
542
542
|
var info = gen[key](arg);
|
|
543
543
|
var value = info.value;
|
|
@@ -546,7 +546,7 @@ var require_text = __commonJS((exports, module) => {
|
|
|
546
546
|
return;
|
|
547
547
|
}
|
|
548
548
|
if (info.done) {
|
|
549
|
-
|
|
549
|
+
resolve2(value);
|
|
550
550
|
} else {
|
|
551
551
|
Promise.resolve(value).then(_next, _throw);
|
|
552
552
|
}
|
|
@@ -554,13 +554,13 @@ var require_text = __commonJS((exports, module) => {
|
|
|
554
554
|
function _asyncToGenerator(fn) {
|
|
555
555
|
return function() {
|
|
556
556
|
var self = this, args = arguments;
|
|
557
|
-
return new Promise(function(
|
|
557
|
+
return new Promise(function(resolve2, reject) {
|
|
558
558
|
var gen = fn.apply(self, args);
|
|
559
559
|
function _next(value) {
|
|
560
|
-
asyncGeneratorStep(gen,
|
|
560
|
+
asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "next", value);
|
|
561
561
|
}
|
|
562
562
|
function _throw(err) {
|
|
563
|
-
asyncGeneratorStep(gen,
|
|
563
|
+
asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "throw", err);
|
|
564
564
|
}
|
|
565
565
|
_next(undefined);
|
|
566
566
|
});
|
|
@@ -1274,7 +1274,7 @@ var require_dateparts = __commonJS((exports, module) => {
|
|
|
1274
1274
|
|
|
1275
1275
|
// node_modules/prompts/dist/elements/date.js
|
|
1276
1276
|
var require_date = __commonJS((exports, module) => {
|
|
1277
|
-
function asyncGeneratorStep(gen,
|
|
1277
|
+
function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
|
|
1278
1278
|
try {
|
|
1279
1279
|
var info = gen[key](arg);
|
|
1280
1280
|
var value = info.value;
|
|
@@ -1283,7 +1283,7 @@ var require_date = __commonJS((exports, module) => {
|
|
|
1283
1283
|
return;
|
|
1284
1284
|
}
|
|
1285
1285
|
if (info.done) {
|
|
1286
|
-
|
|
1286
|
+
resolve2(value);
|
|
1287
1287
|
} else {
|
|
1288
1288
|
Promise.resolve(value).then(_next, _throw);
|
|
1289
1289
|
}
|
|
@@ -1291,13 +1291,13 @@ var require_date = __commonJS((exports, module) => {
|
|
|
1291
1291
|
function _asyncToGenerator(fn) {
|
|
1292
1292
|
return function() {
|
|
1293
1293
|
var self = this, args = arguments;
|
|
1294
|
-
return new Promise(function(
|
|
1294
|
+
return new Promise(function(resolve2, reject) {
|
|
1295
1295
|
var gen = fn.apply(self, args);
|
|
1296
1296
|
function _next(value) {
|
|
1297
|
-
asyncGeneratorStep(gen,
|
|
1297
|
+
asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "next", value);
|
|
1298
1298
|
}
|
|
1299
1299
|
function _throw(err) {
|
|
1300
|
-
asyncGeneratorStep(gen,
|
|
1300
|
+
asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "throw", err);
|
|
1301
1301
|
}
|
|
1302
1302
|
_next(undefined);
|
|
1303
1303
|
});
|
|
@@ -1501,7 +1501,7 @@ ${i ? ` ` : figures.pointerSmall} ${color.red().italic(l)}`, ``);
|
|
|
1501
1501
|
|
|
1502
1502
|
// node_modules/prompts/dist/elements/number.js
|
|
1503
1503
|
var require_number = __commonJS((exports, module) => {
|
|
1504
|
-
function asyncGeneratorStep(gen,
|
|
1504
|
+
function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
|
|
1505
1505
|
try {
|
|
1506
1506
|
var info = gen[key](arg);
|
|
1507
1507
|
var value = info.value;
|
|
@@ -1510,7 +1510,7 @@ var require_number = __commonJS((exports, module) => {
|
|
|
1510
1510
|
return;
|
|
1511
1511
|
}
|
|
1512
1512
|
if (info.done) {
|
|
1513
|
-
|
|
1513
|
+
resolve2(value);
|
|
1514
1514
|
} else {
|
|
1515
1515
|
Promise.resolve(value).then(_next, _throw);
|
|
1516
1516
|
}
|
|
@@ -1518,13 +1518,13 @@ var require_number = __commonJS((exports, module) => {
|
|
|
1518
1518
|
function _asyncToGenerator(fn) {
|
|
1519
1519
|
return function() {
|
|
1520
1520
|
var self = this, args = arguments;
|
|
1521
|
-
return new Promise(function(
|
|
1521
|
+
return new Promise(function(resolve2, reject) {
|
|
1522
1522
|
var gen = fn.apply(self, args);
|
|
1523
1523
|
function _next(value) {
|
|
1524
|
-
asyncGeneratorStep(gen,
|
|
1524
|
+
asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "next", value);
|
|
1525
1525
|
}
|
|
1526
1526
|
function _throw(err) {
|
|
1527
|
-
asyncGeneratorStep(gen,
|
|
1527
|
+
asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "throw", err);
|
|
1528
1528
|
}
|
|
1529
1529
|
_next(undefined);
|
|
1530
1530
|
});
|
|
@@ -1960,7 +1960,7 @@ Instructions:
|
|
|
1960
1960
|
|
|
1961
1961
|
// node_modules/prompts/dist/elements/autocomplete.js
|
|
1962
1962
|
var require_autocomplete = __commonJS((exports, module) => {
|
|
1963
|
-
function asyncGeneratorStep(gen,
|
|
1963
|
+
function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
|
|
1964
1964
|
try {
|
|
1965
1965
|
var info = gen[key](arg);
|
|
1966
1966
|
var value = info.value;
|
|
@@ -1969,7 +1969,7 @@ var require_autocomplete = __commonJS((exports, module) => {
|
|
|
1969
1969
|
return;
|
|
1970
1970
|
}
|
|
1971
1971
|
if (info.done) {
|
|
1972
|
-
|
|
1972
|
+
resolve2(value);
|
|
1973
1973
|
} else {
|
|
1974
1974
|
Promise.resolve(value).then(_next, _throw);
|
|
1975
1975
|
}
|
|
@@ -1977,13 +1977,13 @@ var require_autocomplete = __commonJS((exports, module) => {
|
|
|
1977
1977
|
function _asyncToGenerator(fn) {
|
|
1978
1978
|
return function() {
|
|
1979
1979
|
var self = this, args = arguments;
|
|
1980
|
-
return new Promise(function(
|
|
1980
|
+
return new Promise(function(resolve2, reject) {
|
|
1981
1981
|
var gen = fn.apply(self, args);
|
|
1982
1982
|
function _next(value) {
|
|
1983
|
-
asyncGeneratorStep(gen,
|
|
1983
|
+
asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "next", value);
|
|
1984
1984
|
}
|
|
1985
1985
|
function _throw(err) {
|
|
1986
|
-
asyncGeneratorStep(gen,
|
|
1986
|
+
asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "throw", err);
|
|
1987
1987
|
}
|
|
1988
1988
|
_next(undefined);
|
|
1989
1989
|
});
|
|
@@ -2641,7 +2641,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
|
|
|
2641
2641
|
arr2[i] = arr[i];
|
|
2642
2642
|
return arr2;
|
|
2643
2643
|
}
|
|
2644
|
-
function asyncGeneratorStep(gen,
|
|
2644
|
+
function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
|
|
2645
2645
|
try {
|
|
2646
2646
|
var info = gen[key](arg);
|
|
2647
2647
|
var value = info.value;
|
|
@@ -2650,7 +2650,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
|
|
|
2650
2650
|
return;
|
|
2651
2651
|
}
|
|
2652
2652
|
if (info.done) {
|
|
2653
|
-
|
|
2653
|
+
resolve2(value);
|
|
2654
2654
|
} else {
|
|
2655
2655
|
Promise.resolve(value).then(_next, _throw);
|
|
2656
2656
|
}
|
|
@@ -2658,13 +2658,13 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
|
|
|
2658
2658
|
function _asyncToGenerator(fn) {
|
|
2659
2659
|
return function() {
|
|
2660
2660
|
var self = this, args = arguments;
|
|
2661
|
-
return new Promise(function(
|
|
2661
|
+
return new Promise(function(resolve2, reject) {
|
|
2662
2662
|
var gen = fn.apply(self, args);
|
|
2663
2663
|
function _next(value) {
|
|
2664
|
-
asyncGeneratorStep(gen,
|
|
2664
|
+
asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "next", value);
|
|
2665
2665
|
}
|
|
2666
2666
|
function _throw(err) {
|
|
2667
|
-
asyncGeneratorStep(gen,
|
|
2667
|
+
asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "throw", err);
|
|
2668
2668
|
}
|
|
2669
2669
|
_next(undefined);
|
|
2670
2670
|
});
|
|
@@ -4951,112 +4951,27 @@ var require_prompts3 = __commonJS((exports, module) => {
|
|
|
4951
4951
|
});
|
|
4952
4952
|
|
|
4953
4953
|
// src/index.ts
|
|
4954
|
-
import
|
|
4955
|
-
|
|
4956
|
-
// src/cli/onboarding.ts
|
|
4957
|
-
import path from "path";
|
|
4958
|
-
function configDirectory(context) {
|
|
4959
|
-
return context.env.XDG_CONFIG_HOME ? path.join(context.env.XDG_CONFIG_HOME, "create-next-pro") : path.join(context.homeDir, ".config", "create-next-pro");
|
|
4960
|
-
}
|
|
4961
|
-
function configFile(context) {
|
|
4962
|
-
return path.join(configDirectory(context), "config.json");
|
|
4963
|
-
}
|
|
4964
|
-
async function readConfig(context) {
|
|
4965
|
-
try {
|
|
4966
|
-
return JSON.parse(await context.fs.readText(configFile(context)));
|
|
4967
|
-
} catch {
|
|
4968
|
-
return null;
|
|
4969
|
-
}
|
|
4970
|
-
}
|
|
4971
|
-
async function ensureLineInRc(context, target, line) {
|
|
4972
|
-
try {
|
|
4973
|
-
const current = context.fs.exists(target) ? await context.fs.readText(target) : "";
|
|
4974
|
-
if (!current.includes(line))
|
|
4975
|
-
await context.fs.appendText(target, `
|
|
4976
|
-
${line}
|
|
4977
|
-
`);
|
|
4978
|
-
} catch {}
|
|
4979
|
-
}
|
|
4980
|
-
async function installCompletion(context, shell) {
|
|
4981
|
-
const directory = configDirectory(context);
|
|
4982
|
-
const source = path.join(context.packageRoot, shell === "zsh" ? "create-next-pro-completion.zsh" : "create-next-pro-completion.sh");
|
|
4983
|
-
const target = path.join(directory, `completion.${shell === "zsh" ? "zsh" : "sh"}`);
|
|
4984
|
-
await context.fs.mkdir(directory);
|
|
4985
|
-
await context.fs.copyFile(source, target);
|
|
4986
|
-
const rcFile = path.join(context.homeDir, shell === "zsh" ? ".zshrc" : ".bashrc");
|
|
4987
|
-
await ensureLineInRc(context, rcFile, `source "${target}"`);
|
|
4988
|
-
}
|
|
4989
|
-
async function onboarding(context, version) {
|
|
4990
|
-
context.terminal.log(`\uD83D\uDE80 Welcome to create-next-pro v${version}
|
|
4991
|
-
`);
|
|
4992
|
-
const response = await context.prompt([
|
|
4993
|
-
{
|
|
4994
|
-
type: "select",
|
|
4995
|
-
name: "shell",
|
|
4996
|
-
message: "Which shell do you use?",
|
|
4997
|
-
choices: [
|
|
4998
|
-
{ title: "zsh", value: "zsh" },
|
|
4999
|
-
{ title: "bash", value: "bash" }
|
|
5000
|
-
],
|
|
5001
|
-
initial: context.env.SHELL?.includes("zsh") ? 0 : 1
|
|
5002
|
-
},
|
|
5003
|
-
{
|
|
5004
|
-
type: "toggle",
|
|
5005
|
-
name: "completion",
|
|
5006
|
-
message: "Install autocompletion?",
|
|
5007
|
-
initial: true,
|
|
5008
|
-
active: "Yes",
|
|
5009
|
-
inactive: "No"
|
|
5010
|
-
}
|
|
5011
|
-
], { onCancel: () => false });
|
|
5012
|
-
if (response.shell !== "bash" && response.shell !== "zsh") {
|
|
5013
|
-
throw new Error("Configuration cancelled.");
|
|
5014
|
-
}
|
|
5015
|
-
const now = new Date().toISOString();
|
|
5016
|
-
const config = {
|
|
5017
|
-
version: 1,
|
|
5018
|
-
shell: response.shell,
|
|
5019
|
-
completionInstalled: Boolean(response.completion),
|
|
5020
|
-
createdAt: now,
|
|
5021
|
-
updatedAt: now
|
|
5022
|
-
};
|
|
5023
|
-
if (config.completionInstalled)
|
|
5024
|
-
await installCompletion(context, config.shell);
|
|
5025
|
-
await context.fs.mkdir(configDirectory(context));
|
|
5026
|
-
await context.fs.writeText(configFile(context), JSON.stringify(config, null, 2));
|
|
5027
|
-
context.terminal.log(`
|
|
5028
|
-
\u2705 Configuration saved.`);
|
|
5029
|
-
context.terminal.log("you can now use the CLI ! ex : ");
|
|
5030
|
-
context.terminal.log(" Without prompt (will change in future) :");
|
|
5031
|
-
context.terminal.log(" create-next-pro my-next-project");
|
|
5032
|
-
context.terminal.log(" With prompt :");
|
|
5033
|
-
context.terminal.log(" create-next-pro");
|
|
5034
|
-
context.terminal.log("For more information, visit: https://github.com/Rising-Corporation/create-next-pro-cli");
|
|
5035
|
-
context.terminal.log("Happy coding! \uD83C\uDF89");
|
|
5036
|
-
return config;
|
|
5037
|
-
}
|
|
4954
|
+
import path10 from "path";
|
|
5038
4955
|
|
|
5039
4956
|
// src/cli/completion.ts
|
|
5040
|
-
import
|
|
5041
|
-
import path3 from "path";
|
|
4957
|
+
import path2 from "path";
|
|
5042
4958
|
|
|
5043
4959
|
// src/core/page-catalog.ts
|
|
5044
|
-
import
|
|
5045
|
-
import path2 from "path";
|
|
4960
|
+
import path from "path";
|
|
5046
4961
|
function isRouteGroup(segment) {
|
|
5047
4962
|
return segment.startsWith("(") && segment.endsWith(")");
|
|
5048
4963
|
}
|
|
5049
|
-
async function discoverPages(projectRoot) {
|
|
5050
|
-
const appRoot =
|
|
4964
|
+
async function discoverPages(projectRoot, fs) {
|
|
4965
|
+
const appRoot = path.join(projectRoot, "src", "app", "[locale]");
|
|
5051
4966
|
const candidates = [];
|
|
5052
4967
|
async function visit(directory, relative = []) {
|
|
5053
4968
|
let entries;
|
|
5054
4969
|
try {
|
|
5055
|
-
entries = await
|
|
4970
|
+
entries = await fs.list(directory);
|
|
5056
4971
|
} catch {
|
|
5057
4972
|
return;
|
|
5058
4973
|
}
|
|
5059
|
-
if (entries.some((entry) => entry.isFile
|
|
4974
|
+
if (entries.some((entry) => entry.isFile && entry.name === "page.tsx")) {
|
|
5060
4975
|
const routeSegments = relative.filter((segment) => !isRouteGroup(segment));
|
|
5061
4976
|
if (routeSegments.length > 0 && !routeSegments.some((segment) => segment.startsWith("_") || segment.startsWith("["))) {
|
|
5062
4977
|
const logicalName = routeSegments.join(".");
|
|
@@ -5064,15 +4979,15 @@ async function discoverPages(projectRoot) {
|
|
|
5064
4979
|
logicalName,
|
|
5065
4980
|
routeSegments,
|
|
5066
4981
|
routeDirectory: directory,
|
|
5067
|
-
uiDirectory:
|
|
5068
|
-
messageFile:
|
|
4982
|
+
uiDirectory: path.join(projectRoot, "src", "ui", ...routeSegments),
|
|
4983
|
+
messageFile: path.join(projectRoot, "messages", "{locale}", `${routeSegments[0]}.json`),
|
|
5069
4984
|
messageKey: routeSegments.length > 1 ? routeSegments.at(-1) : undefined
|
|
5070
4985
|
});
|
|
5071
4986
|
}
|
|
5072
4987
|
}
|
|
5073
4988
|
for (const entry of entries) {
|
|
5074
|
-
if (entry.isDirectory
|
|
5075
|
-
await visit(
|
|
4989
|
+
if (entry.isDirectory && !entry.name.startsWith(".")) {
|
|
4990
|
+
await visit(path.join(directory, entry.name), [
|
|
5076
4991
|
...relative,
|
|
5077
4992
|
entry.name
|
|
5078
4993
|
]);
|
|
@@ -5094,6 +5009,7 @@ var PUBLIC_COMMANDS = [
|
|
|
5094
5009
|
"rmpage",
|
|
5095
5010
|
"--help",
|
|
5096
5011
|
"--version",
|
|
5012
|
+
"--json",
|
|
5097
5013
|
"--reconfigure"
|
|
5098
5014
|
];
|
|
5099
5015
|
var OPTIONS = {
|
|
@@ -5110,9 +5026,9 @@ var OPTIONS = {
|
|
|
5110
5026
|
],
|
|
5111
5027
|
addcomponent: ["--page", "-P"]
|
|
5112
5028
|
};
|
|
5113
|
-
async function directories(root) {
|
|
5029
|
+
async function directories(root, context) {
|
|
5114
5030
|
try {
|
|
5115
|
-
return (await
|
|
5031
|
+
return (await context.fs.list(root)).filter((entry) => entry.isDirectory && !entry.name.startsWith("_")).map((entry) => entry.name).sort();
|
|
5116
5032
|
} catch {
|
|
5117
5033
|
return [];
|
|
5118
5034
|
}
|
|
@@ -5121,59 +5037,429 @@ async function completionCandidates(command, context) {
|
|
|
5121
5037
|
if (!command)
|
|
5122
5038
|
return [...PUBLIC_COMMANDS];
|
|
5123
5039
|
if (command === "rmpage") {
|
|
5124
|
-
return (await discoverPages(context.cwd)).map((candidate) => candidate.logicalName);
|
|
5040
|
+
return (await discoverPages(context.cwd, context.fs)).map((candidate) => candidate.logicalName);
|
|
5125
5041
|
}
|
|
5126
5042
|
if (command === "addcomponent" || command === "addpage") {
|
|
5127
5043
|
return [
|
|
5128
5044
|
...OPTIONS[command] ?? [],
|
|
5129
|
-
...await directories(
|
|
5045
|
+
...await directories(path2.join(context.cwd, "src", "ui"), context)
|
|
5130
5046
|
];
|
|
5131
5047
|
}
|
|
5132
5048
|
if (command === "addlanguage")
|
|
5133
5049
|
return ["de", "en", "es", "fr", "it", "ja", "pt"];
|
|
5134
5050
|
return OPTIONS[command] ?? [];
|
|
5135
5051
|
}
|
|
5136
|
-
async function printCompletions(args, context) {
|
|
5137
|
-
for (const candidate of await completionCandidates(args[1], context)) {
|
|
5138
|
-
context.terminal.log(candidate);
|
|
5139
|
-
}
|
|
5140
|
-
}
|
|
5141
5052
|
|
|
5142
|
-
// src/
|
|
5143
|
-
|
|
5053
|
+
// src/cli/onboarding.ts
|
|
5054
|
+
import path4 from "path";
|
|
5144
5055
|
|
|
5056
|
+
// src/core/operations.ts
|
|
5057
|
+
import path3 from "path";
|
|
5058
|
+
|
|
5059
|
+
// src/core/contracts.ts
|
|
5145
5060
|
class CliError extends Error {
|
|
5146
5061
|
exitCode;
|
|
5147
|
-
|
|
5062
|
+
code;
|
|
5063
|
+
hint;
|
|
5064
|
+
scope;
|
|
5065
|
+
path;
|
|
5066
|
+
constructor(message, options = {}) {
|
|
5148
5067
|
super(message);
|
|
5149
|
-
this.exitCode = exitCode;
|
|
5150
5068
|
this.name = "CliError";
|
|
5069
|
+
if (typeof options === "number") {
|
|
5070
|
+
this.exitCode = options;
|
|
5071
|
+
this.code = "FILESYSTEM_ERROR";
|
|
5072
|
+
} else {
|
|
5073
|
+
this.exitCode = options.exitCode ?? 1;
|
|
5074
|
+
this.code = options.code ?? "FILESYSTEM_ERROR";
|
|
5075
|
+
this.hint = options.hint;
|
|
5076
|
+
this.scope = options.scope;
|
|
5077
|
+
this.path = options.path;
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
5081
|
+
|
|
5082
|
+
// src/core/operations.ts
|
|
5083
|
+
class OperationJournal {
|
|
5084
|
+
#events = [];
|
|
5085
|
+
record(event) {
|
|
5086
|
+
const detail = event.detail ? Object.fromEntries(Object.entries(event.detail).map(([key, value]) => [
|
|
5087
|
+
key,
|
|
5088
|
+
/(content|credential|env|password|secret|token|value)/i.test(key) ? "[REDACTED]" : value
|
|
5089
|
+
])) : undefined;
|
|
5090
|
+
const recorded = {
|
|
5091
|
+
...event,
|
|
5092
|
+
detail,
|
|
5093
|
+
sequence: this.#events.length + 1
|
|
5094
|
+
};
|
|
5095
|
+
this.#events.push(recorded);
|
|
5096
|
+
return recorded;
|
|
5097
|
+
}
|
|
5098
|
+
snapshot() {
|
|
5099
|
+
return this.#events.map((event) => ({ ...event }));
|
|
5100
|
+
}
|
|
5101
|
+
reset() {
|
|
5102
|
+
this.#events.length = 0;
|
|
5103
|
+
}
|
|
5104
|
+
}
|
|
5105
|
+
var MUTATIONS = new Set(["created", "copied", "updated", "deleted"]);
|
|
5106
|
+
function statusFromEvents(events) {
|
|
5107
|
+
if (events.some((event) => event.action === "cancelled"))
|
|
5108
|
+
return "cancelled";
|
|
5109
|
+
return events.some((event) => MUTATIONS.has(event.action)) ? "success" : "unchanged";
|
|
5110
|
+
}
|
|
5111
|
+
function commandResult(context, input) {
|
|
5112
|
+
const events = context.operations.snapshot();
|
|
5113
|
+
const status = input.status ?? statusFromEvents(events);
|
|
5114
|
+
return {
|
|
5115
|
+
exitCode: status === "failed" ? 1 : 0,
|
|
5116
|
+
status,
|
|
5117
|
+
command: input.command,
|
|
5118
|
+
summary: input.summary,
|
|
5119
|
+
projectRoot: input.projectRoot,
|
|
5120
|
+
configRoot: input.configRoot,
|
|
5121
|
+
homeRoot: input.homeRoot,
|
|
5122
|
+
events,
|
|
5123
|
+
nextSteps: input.nextSteps ?? [],
|
|
5124
|
+
error: null,
|
|
5125
|
+
data: input.data
|
|
5126
|
+
};
|
|
5127
|
+
}
|
|
5128
|
+
function failedResult(context, command, error, roots = {}) {
|
|
5129
|
+
const cliError = error instanceof CliError ? error : undefined;
|
|
5130
|
+
const normalized = {
|
|
5131
|
+
code: cliError?.code ?? "FILESYSTEM_ERROR",
|
|
5132
|
+
message: error instanceof Error ? error.message : String(error),
|
|
5133
|
+
hint: cliError?.hint,
|
|
5134
|
+
scope: cliError?.scope,
|
|
5135
|
+
path: cliError?.path
|
|
5136
|
+
};
|
|
5137
|
+
const nextSteps = normalized.code === "ONBOARDING_REQUIRED" ? [
|
|
5138
|
+
{
|
|
5139
|
+
kind: "rerun",
|
|
5140
|
+
required: true,
|
|
5141
|
+
message: "Run create-next-pro once in human mode, then rerun the JSON command.",
|
|
5142
|
+
paths: [{ scope: "config", path: "config.json" }],
|
|
5143
|
+
commands: ["create-next-pro"]
|
|
5144
|
+
}
|
|
5145
|
+
] : [];
|
|
5146
|
+
context.operations.record({
|
|
5147
|
+
action: "failed",
|
|
5148
|
+
resource: "command",
|
|
5149
|
+
role: command,
|
|
5150
|
+
scope: normalized.scope ?? "project",
|
|
5151
|
+
path: normalized.path ?? ".",
|
|
5152
|
+
detail: { code: normalized.code }
|
|
5153
|
+
});
|
|
5154
|
+
return {
|
|
5155
|
+
exitCode: cliError?.exitCode ?? 1,
|
|
5156
|
+
status: "failed",
|
|
5157
|
+
command,
|
|
5158
|
+
summary: normalized.message,
|
|
5159
|
+
...roots,
|
|
5160
|
+
events: context.operations.snapshot(),
|
|
5161
|
+
nextSteps,
|
|
5162
|
+
error: normalized
|
|
5163
|
+
};
|
|
5164
|
+
}
|
|
5165
|
+
function relativeResource(root, target) {
|
|
5166
|
+
const relative = path3.relative(path3.resolve(root), path3.resolve(target));
|
|
5167
|
+
return relative || ".";
|
|
5168
|
+
}
|
|
5169
|
+
|
|
5170
|
+
class MutationGateway {
|
|
5171
|
+
context;
|
|
5172
|
+
root;
|
|
5173
|
+
defaultScope;
|
|
5174
|
+
constructor(context, root, defaultScope = "project") {
|
|
5175
|
+
this.context = context;
|
|
5176
|
+
this.root = root;
|
|
5177
|
+
this.defaultScope = defaultScope;
|
|
5178
|
+
}
|
|
5179
|
+
path(target) {
|
|
5180
|
+
return relativeResource(this.root, target);
|
|
5181
|
+
}
|
|
5182
|
+
async mkdir(target, metadata, record = true) {
|
|
5183
|
+
if (this.context.fs.exists(target)) {
|
|
5184
|
+
if (record)
|
|
5185
|
+
this.record("unchanged", target, metadata);
|
|
5186
|
+
return "unchanged";
|
|
5187
|
+
}
|
|
5188
|
+
await this.context.fs.mkdir(target);
|
|
5189
|
+
if (record)
|
|
5190
|
+
this.record("created", target, metadata);
|
|
5191
|
+
return "created";
|
|
5192
|
+
}
|
|
5193
|
+
async write(target, content, metadata) {
|
|
5194
|
+
const exists = this.context.fs.exists(target);
|
|
5195
|
+
if (exists && metadata.preserveExisting) {
|
|
5196
|
+
this.record("unchanged", target, metadata);
|
|
5197
|
+
return "unchanged";
|
|
5198
|
+
}
|
|
5199
|
+
if (exists && await this.context.fs.readText(target) === content) {
|
|
5200
|
+
this.record("unchanged", target, metadata);
|
|
5201
|
+
return "unchanged";
|
|
5202
|
+
}
|
|
5203
|
+
await this.context.fs.mkdir(path3.dirname(target));
|
|
5204
|
+
await this.context.fs.writeText(target, content);
|
|
5205
|
+
const action = exists ? "updated" : "created";
|
|
5206
|
+
this.record(action, target, metadata);
|
|
5207
|
+
return action;
|
|
5208
|
+
}
|
|
5209
|
+
async copy(source, target, metadata) {
|
|
5210
|
+
if (this.context.fs.exists(target) && metadata.preserveExisting) {
|
|
5211
|
+
this.record("unchanged", target, metadata);
|
|
5212
|
+
return "unchanged";
|
|
5213
|
+
}
|
|
5214
|
+
await this.context.fs.mkdir(path3.dirname(target));
|
|
5215
|
+
await this.context.fs.copyFile(source, target);
|
|
5216
|
+
this.record("copied", target, {
|
|
5217
|
+
...metadata,
|
|
5218
|
+
source: metadata.source ?? { path: source }
|
|
5219
|
+
});
|
|
5220
|
+
return "copied";
|
|
5221
|
+
}
|
|
5222
|
+
async remove(target, metadata, options = {}) {
|
|
5223
|
+
if (!this.context.fs.exists(target)) {
|
|
5224
|
+
this.record("unchanged", target, metadata);
|
|
5225
|
+
return "unchanged";
|
|
5226
|
+
}
|
|
5227
|
+
await this.context.fs.remove(target, options);
|
|
5228
|
+
this.record("deleted", target, metadata);
|
|
5229
|
+
return "deleted";
|
|
5230
|
+
}
|
|
5231
|
+
unchanged(target, metadata) {
|
|
5232
|
+
this.record("unchanged", target, metadata);
|
|
5233
|
+
}
|
|
5234
|
+
skipped(target, metadata) {
|
|
5235
|
+
this.record("skipped", target, metadata);
|
|
5236
|
+
}
|
|
5237
|
+
record(action, target, metadata) {
|
|
5238
|
+
this.context.operations.record({
|
|
5239
|
+
action,
|
|
5240
|
+
resource: metadata.resource ?? "file",
|
|
5241
|
+
role: metadata.role,
|
|
5242
|
+
scope: metadata.scope ?? this.defaultScope,
|
|
5243
|
+
path: this.path(target),
|
|
5244
|
+
source: metadata.source,
|
|
5245
|
+
detail: metadata.detail
|
|
5246
|
+
});
|
|
5247
|
+
}
|
|
5248
|
+
}
|
|
5249
|
+
|
|
5250
|
+
// src/cli/onboarding.ts
|
|
5251
|
+
function configDirectory(context) {
|
|
5252
|
+
return context.env.XDG_CONFIG_HOME ? path4.join(context.env.XDG_CONFIG_HOME, "create-next-pro") : path4.join(context.homeDir, ".config", "create-next-pro");
|
|
5253
|
+
}
|
|
5254
|
+
function configFile(context) {
|
|
5255
|
+
return path4.join(configDirectory(context), "config.json");
|
|
5256
|
+
}
|
|
5257
|
+
async function readConfig(context) {
|
|
5258
|
+
try {
|
|
5259
|
+
const config = JSON.parse(await context.fs.readText(configFile(context)));
|
|
5260
|
+
if (config.version !== 1 || config.shell !== "bash" && config.shell !== "zsh" || typeof config.completionInstalled !== "boolean" || typeof config.createdAt !== "string" || typeof config.updatedAt !== "string") {
|
|
5261
|
+
return null;
|
|
5262
|
+
}
|
|
5263
|
+
return config;
|
|
5264
|
+
} catch {
|
|
5265
|
+
return null;
|
|
5266
|
+
}
|
|
5267
|
+
}
|
|
5268
|
+
async function ensureLineInRc(context, target, line) {
|
|
5269
|
+
const current = context.fs.exists(target) ? await context.fs.readText(target) : "";
|
|
5270
|
+
const gateway = new MutationGateway(context, context.homeDir, "home");
|
|
5271
|
+
return gateway.write(target, current.includes(line) ? current : `${current}
|
|
5272
|
+
${line}
|
|
5273
|
+
`, { role: "shell-profile", resource: "shell-profile" });
|
|
5274
|
+
}
|
|
5275
|
+
async function installCompletion(context, shell) {
|
|
5276
|
+
const directory = configDirectory(context);
|
|
5277
|
+
const source = path4.join(context.packageRoot, shell === "zsh" ? "create-next-pro-completion.zsh" : "create-next-pro-completion.sh");
|
|
5278
|
+
const target = path4.join(directory, `completion.${shell === "zsh" ? "zsh" : "sh"}`);
|
|
5279
|
+
const gateway = new MutationGateway(context, directory, "config");
|
|
5280
|
+
await gateway.write(target, await context.fs.readText(source), {
|
|
5281
|
+
role: "completion-script",
|
|
5282
|
+
source: { scope: "package", path: path4.basename(source) }
|
|
5283
|
+
});
|
|
5284
|
+
const rcFile = path4.join(context.homeDir, shell === "zsh" ? ".zshrc" : ".bashrc");
|
|
5285
|
+
await ensureLineInRc(context, rcFile, `source "${target}"`);
|
|
5286
|
+
}
|
|
5287
|
+
async function onboarding(context, version) {
|
|
5288
|
+
const response = await context.prompt([
|
|
5289
|
+
{
|
|
5290
|
+
type: "select",
|
|
5291
|
+
name: "shell",
|
|
5292
|
+
message: "Which shell do you use?",
|
|
5293
|
+
choices: [
|
|
5294
|
+
{ title: "zsh", value: "zsh" },
|
|
5295
|
+
{ title: "bash", value: "bash" }
|
|
5296
|
+
],
|
|
5297
|
+
initial: context.env.SHELL?.includes("zsh") ? 0 : 1
|
|
5298
|
+
},
|
|
5299
|
+
{
|
|
5300
|
+
type: "toggle",
|
|
5301
|
+
name: "completion",
|
|
5302
|
+
message: "Install autocompletion?",
|
|
5303
|
+
initial: true,
|
|
5304
|
+
active: "Yes",
|
|
5305
|
+
inactive: "No"
|
|
5306
|
+
}
|
|
5307
|
+
], { onCancel: () => false });
|
|
5308
|
+
if (response.shell !== "bash" && response.shell !== "zsh") {
|
|
5309
|
+
context.operations.record({
|
|
5310
|
+
action: "cancelled",
|
|
5311
|
+
resource: "command",
|
|
5312
|
+
role: "onboarding",
|
|
5313
|
+
scope: "config",
|
|
5314
|
+
path: "."
|
|
5315
|
+
});
|
|
5316
|
+
return commandResult(context, {
|
|
5317
|
+
command: "onboarding",
|
|
5318
|
+
summary: "Configuration was cancelled.",
|
|
5319
|
+
configRoot: configDirectory(context),
|
|
5320
|
+
homeRoot: context.homeDir,
|
|
5321
|
+
status: "cancelled"
|
|
5322
|
+
});
|
|
5323
|
+
}
|
|
5324
|
+
const previous = await readConfig(context);
|
|
5325
|
+
const now = new Date().toISOString();
|
|
5326
|
+
const config = {
|
|
5327
|
+
version: 1,
|
|
5328
|
+
shell: response.shell,
|
|
5329
|
+
completionInstalled: Boolean(response.completion),
|
|
5330
|
+
createdAt: previous?.createdAt ?? now,
|
|
5331
|
+
updatedAt: now
|
|
5332
|
+
};
|
|
5333
|
+
if (config.completionInstalled) {
|
|
5334
|
+
try {
|
|
5335
|
+
await installCompletion(context, config.shell);
|
|
5336
|
+
} catch (error) {
|
|
5337
|
+
context.operations.record({
|
|
5338
|
+
action: "skipped",
|
|
5339
|
+
resource: "file",
|
|
5340
|
+
role: "completion-installation",
|
|
5341
|
+
scope: "config",
|
|
5342
|
+
path: `completion.${config.shell === "zsh" ? "zsh" : "sh"}`,
|
|
5343
|
+
detail: {
|
|
5344
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
5345
|
+
}
|
|
5346
|
+
});
|
|
5347
|
+
config.completionInstalled = false;
|
|
5348
|
+
}
|
|
5349
|
+
}
|
|
5350
|
+
const semanticallyUnchanged = previous?.shell === config.shell && previous.completionInstalled === config.completionInstalled;
|
|
5351
|
+
if (semanticallyUnchanged && previous)
|
|
5352
|
+
config.updatedAt = previous.updatedAt;
|
|
5353
|
+
const gateway = new MutationGateway(context, configDirectory(context), "config");
|
|
5354
|
+
await gateway.write(configFile(context), `${JSON.stringify(config, null, 2)}
|
|
5355
|
+
`, { role: "user-configuration", resource: "configuration" });
|
|
5356
|
+
return commandResult(context, {
|
|
5357
|
+
command: "onboarding",
|
|
5358
|
+
summary: `Configuration for create-next-pro v${version} is ready.`,
|
|
5359
|
+
configRoot: configDirectory(context),
|
|
5360
|
+
homeRoot: context.homeDir,
|
|
5361
|
+
nextSteps: [
|
|
5362
|
+
{
|
|
5363
|
+
kind: "rerun",
|
|
5364
|
+
required: true,
|
|
5365
|
+
message: "Run the original create-next-pro command again.",
|
|
5366
|
+
paths: []
|
|
5367
|
+
}
|
|
5368
|
+
]
|
|
5369
|
+
});
|
|
5370
|
+
}
|
|
5371
|
+
|
|
5372
|
+
// src/cli/output.ts
|
|
5373
|
+
import path5 from "path";
|
|
5374
|
+
function rootFor(result, context, scope) {
|
|
5375
|
+
if (scope === "project")
|
|
5376
|
+
return result.projectRoot;
|
|
5377
|
+
if (scope === "config")
|
|
5378
|
+
return result.configRoot;
|
|
5379
|
+
if (scope === "home")
|
|
5380
|
+
return result.homeRoot;
|
|
5381
|
+
return context.packageRoot;
|
|
5382
|
+
}
|
|
5383
|
+
function displayPath(result, context, scope, target) {
|
|
5384
|
+
const root = rootFor(result, context, scope);
|
|
5385
|
+
return root ? path5.resolve(root, target) : target;
|
|
5386
|
+
}
|
|
5387
|
+
function renderEvent(result, context, event) {
|
|
5388
|
+
const source = event.source?.template ? ` from template ${event.source.template}` : event.source?.path ? ` from ${displayPath(result, context, event.source.scope ?? event.scope, event.source.path)}` : "";
|
|
5389
|
+
const detail = event.detail ? ` (${Object.entries(event.detail).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${JSON.stringify(value)}`).join(", ")})` : "";
|
|
5390
|
+
return `${event.action.toUpperCase()} ${event.role}: ${displayPath(result, context, event.scope, event.path)}${source}${detail}`;
|
|
5391
|
+
}
|
|
5392
|
+
function renderResult(result, context, mode) {
|
|
5393
|
+
if (mode === "json") {
|
|
5394
|
+
context.terminal.log(JSON.stringify({ schemaVersion: 1, ...result }));
|
|
5395
|
+
return;
|
|
5396
|
+
}
|
|
5397
|
+
if (result.command === "help" && typeof result.data?.help === "string") {
|
|
5398
|
+
context.terminal.log(result.data.help);
|
|
5399
|
+
return;
|
|
5400
|
+
}
|
|
5401
|
+
if (result.command === "version" && typeof result.data?.version === "string") {
|
|
5402
|
+
context.terminal.log(`v${result.data.version}`);
|
|
5403
|
+
return;
|
|
5404
|
+
}
|
|
5405
|
+
const copiedTemplateFiles = result.events.filter((event) => event.action === "copied" && event.role === "template-file");
|
|
5406
|
+
if (copiedTemplateFiles.length > 0) {
|
|
5407
|
+
context.terminal.log(`COPIED ${copiedTemplateFiles.length} template files to ${result.projectRoot}.`);
|
|
5408
|
+
}
|
|
5409
|
+
for (const event of result.events) {
|
|
5410
|
+
if (event.action === "copied" && event.role === "template-file")
|
|
5411
|
+
continue;
|
|
5412
|
+
const line = renderEvent(result, context, event);
|
|
5413
|
+
if (event.action === "failed")
|
|
5414
|
+
context.terminal.error(line);
|
|
5415
|
+
else if (event.action === "skipped")
|
|
5416
|
+
context.terminal.warn(line);
|
|
5417
|
+
else
|
|
5418
|
+
context.terminal.log(line);
|
|
5419
|
+
}
|
|
5420
|
+
if (result.error) {
|
|
5421
|
+
context.terminal.error(`ERROR [${result.error.code}]: ${result.error.message}`);
|
|
5422
|
+
if (result.error.hint)
|
|
5423
|
+
context.terminal.error(`HINT: ${result.error.hint}`);
|
|
5424
|
+
} else if (result.status === "success") {
|
|
5425
|
+
context.terminal.log(`SUCCESS: ${result.summary}`);
|
|
5426
|
+
} else if (result.status === "unchanged") {
|
|
5427
|
+
context.terminal.log(`NO CHANGES: ${result.summary}`);
|
|
5428
|
+
} else if (result.status === "cancelled") {
|
|
5429
|
+
context.terminal.log(`CANCELLED: ${result.summary}`);
|
|
5430
|
+
}
|
|
5431
|
+
for (const step of result.nextSteps) {
|
|
5432
|
+
context.terminal.log(`NEXT [${step.kind}]: ${step.message}`);
|
|
5433
|
+
for (const target of step.paths) {
|
|
5434
|
+
context.terminal.log(` ${displayPath(result, context, target.scope, target.path)}`);
|
|
5435
|
+
}
|
|
5436
|
+
for (const command of step.commands ?? []) {
|
|
5437
|
+
context.terminal.log(` ${command}`);
|
|
5438
|
+
}
|
|
5151
5439
|
}
|
|
5152
5440
|
}
|
|
5153
5441
|
|
|
5154
5442
|
// src/lib/addApi.ts
|
|
5155
|
-
var import_prompts2 = __toESM(require_prompts3(), 1);
|
|
5156
5443
|
import { join as join2 } from "path";
|
|
5157
|
-
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
5158
|
-
import { existsSync as existsSync3 } from "fs";
|
|
5159
5444
|
|
|
5160
5445
|
// src/lib/utils.ts
|
|
5161
|
-
import { readFile } from "fs/promises";
|
|
5162
|
-
import { existsSync } from "fs";
|
|
5163
5446
|
import { join } from "path";
|
|
5164
5447
|
function capitalize(str) {
|
|
5165
5448
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
5166
5449
|
}
|
|
5450
|
+
function toIdentifier(value) {
|
|
5451
|
+
return value.replace(/[-_]+([A-Za-z0-9])/g, (_, character) => character.toUpperCase());
|
|
5452
|
+
}
|
|
5167
5453
|
function configuredAliasPrefix(config) {
|
|
5168
5454
|
const alias = config.importAlias ?? "@/*";
|
|
5169
5455
|
return alias.endsWith("/*") ? alias.slice(0, -2) : "@";
|
|
5170
5456
|
}
|
|
5171
|
-
async function loadConfig(
|
|
5172
|
-
const configPath = join(cwd, "cnp.config.json");
|
|
5173
|
-
if (!
|
|
5457
|
+
async function loadConfig(context) {
|
|
5458
|
+
const configPath = join(context.cwd, "cnp.config.json");
|
|
5459
|
+
if (!context.fs.exists(configPath))
|
|
5174
5460
|
return null;
|
|
5175
5461
|
try {
|
|
5176
|
-
const raw = await
|
|
5462
|
+
const raw = await context.fs.readText(configPath);
|
|
5177
5463
|
return JSON.parse(raw);
|
|
5178
5464
|
} catch {
|
|
5179
5465
|
return null;
|
|
@@ -5204,67 +5490,8 @@ function toFileName(key) {
|
|
|
5204
5490
|
}
|
|
5205
5491
|
}
|
|
5206
5492
|
|
|
5207
|
-
// src/runtime/node-context.ts
|
|
5208
|
-
var import_prompts = __toESM(require_prompts3(), 1);
|
|
5209
|
-
import { existsSync as existsSync2 } from "fs";
|
|
5210
|
-
import {
|
|
5211
|
-
appendFile,
|
|
5212
|
-
copyFile,
|
|
5213
|
-
mkdir,
|
|
5214
|
-
readFile as readFile2,
|
|
5215
|
-
writeFile
|
|
5216
|
-
} from "fs/promises";
|
|
5217
|
-
import os from "os";
|
|
5218
|
-
import path4 from "path";
|
|
5219
|
-
import { fileURLToPath } from "url";
|
|
5220
|
-
function findPackageRoot(start) {
|
|
5221
|
-
let current = start;
|
|
5222
|
-
while (true) {
|
|
5223
|
-
const packagePath = path4.join(current, "package.json");
|
|
5224
|
-
if (existsSync2(packagePath))
|
|
5225
|
-
return current;
|
|
5226
|
-
const parent = path4.dirname(current);
|
|
5227
|
-
if (parent === current) {
|
|
5228
|
-
throw new Error(`Unable to locate package.json from ${start}`);
|
|
5229
|
-
}
|
|
5230
|
-
current = parent;
|
|
5231
|
-
}
|
|
5232
|
-
}
|
|
5233
|
-
function resolvePackageRoot(metaUrl = import.meta.url) {
|
|
5234
|
-
return findPackageRoot(path4.dirname(fileURLToPath(metaUrl)));
|
|
5235
|
-
}
|
|
5236
|
-
function createNodeContext(overrides = {}) {
|
|
5237
|
-
return {
|
|
5238
|
-
argv: process.argv.slice(2),
|
|
5239
|
-
cwd: process.cwd(),
|
|
5240
|
-
env: process.env,
|
|
5241
|
-
homeDir: os.homedir(),
|
|
5242
|
-
packageRoot: resolvePackageRoot(),
|
|
5243
|
-
terminal: console,
|
|
5244
|
-
prompt: import_prompts.default,
|
|
5245
|
-
fs: {
|
|
5246
|
-
exists: existsSync2,
|
|
5247
|
-
readText: (target) => readFile2(target, "utf8"),
|
|
5248
|
-
writeText: async (target, content) => {
|
|
5249
|
-
await writeFile(target, content);
|
|
5250
|
-
},
|
|
5251
|
-
mkdir: async (target) => {
|
|
5252
|
-
await mkdir(target, { recursive: true });
|
|
5253
|
-
},
|
|
5254
|
-
copyFile: async (source, target) => {
|
|
5255
|
-
await copyFile(source, target);
|
|
5256
|
-
},
|
|
5257
|
-
appendText: async (target, content) => {
|
|
5258
|
-
await appendFile(target, content);
|
|
5259
|
-
}
|
|
5260
|
-
},
|
|
5261
|
-
...overrides
|
|
5262
|
-
};
|
|
5263
|
-
}
|
|
5264
|
-
|
|
5265
5493
|
// src/core/project-paths.ts
|
|
5266
|
-
import
|
|
5267
|
-
import { lstat } from "fs/promises";
|
|
5494
|
+
import path6 from "path";
|
|
5268
5495
|
var SAFE_SEGMENT = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
5269
5496
|
function hasControlCharacters(value) {
|
|
5270
5497
|
return [...value].some((character) => {
|
|
@@ -5274,740 +5501,1196 @@ function hasControlCharacters(value) {
|
|
|
5274
5501
|
}
|
|
5275
5502
|
function parseLogicalName(value, label = "name") {
|
|
5276
5503
|
if (!value || hasControlCharacters(value)) {
|
|
5277
|
-
throw new CliError(`Invalid ${label}: a non-empty printable value is required
|
|
5504
|
+
throw new CliError(`Invalid ${label}: a non-empty printable value is required.`, { code: "INVALID_ARGUMENT" });
|
|
5278
5505
|
}
|
|
5279
|
-
if (
|
|
5280
|
-
throw new CliError(`Invalid ${label}: paths and separators are not allowed
|
|
5506
|
+
if (path6.isAbsolute(value) || value.includes("/") || value.includes("\\")) {
|
|
5507
|
+
throw new CliError(`Invalid ${label}: paths and separators are not allowed.`, { code: "INVALID_ARGUMENT" });
|
|
5281
5508
|
}
|
|
5282
5509
|
const segments = value.split(".");
|
|
5283
5510
|
if (segments.some((segment) => !SAFE_SEGMENT.test(segment))) {
|
|
5284
|
-
throw new CliError(`Invalid ${label}: use dot-separated alphanumeric segments beginning with a letter
|
|
5511
|
+
throw new CliError(`Invalid ${label}: use dot-separated alphanumeric segments beginning with a letter.`, { code: "INVALID_ARGUMENT" });
|
|
5285
5512
|
}
|
|
5286
5513
|
return segments;
|
|
5287
5514
|
}
|
|
5288
5515
|
function validateProjectName(value) {
|
|
5289
|
-
if (!value || hasControlCharacters(value) ||
|
|
5290
|
-
throw new CliError("Invalid project name: use letters, numbers, dots, dashes or underscores without path separators.");
|
|
5516
|
+
if (!value || hasControlCharacters(value) || path6.isAbsolute(value) || value === "." || value === ".." || value.includes("/") || value.includes("\\") || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value)) {
|
|
5517
|
+
throw new CliError("Invalid project name: use letters, numbers, dots, dashes or underscores without path separators.", { code: "INVALID_ARGUMENT" });
|
|
5291
5518
|
}
|
|
5292
5519
|
return value;
|
|
5293
5520
|
}
|
|
5294
5521
|
function resolveInside(root, ...segments) {
|
|
5295
|
-
const absoluteRoot =
|
|
5296
|
-
const target =
|
|
5297
|
-
if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${
|
|
5298
|
-
throw new CliError(`Refusing to access a path outside the project: ${target}
|
|
5522
|
+
const absoluteRoot = path6.resolve(root);
|
|
5523
|
+
const target = path6.resolve(absoluteRoot, ...segments);
|
|
5524
|
+
if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${path6.sep}`)) {
|
|
5525
|
+
throw new CliError(`Refusing to access a path outside the project: ${target}`, { code: "UNSAFE_PATH" });
|
|
5299
5526
|
}
|
|
5300
5527
|
return target;
|
|
5301
5528
|
}
|
|
5302
|
-
async function assertSafeTarget(root, target) {
|
|
5303
|
-
const safeTarget = resolveInside(root,
|
|
5304
|
-
const relativeSegments =
|
|
5305
|
-
let current =
|
|
5529
|
+
async function assertSafeTarget(root, target, fs) {
|
|
5530
|
+
const safeTarget = resolveInside(root, path6.relative(root, target));
|
|
5531
|
+
const relativeSegments = path6.relative(path6.resolve(root), safeTarget).split(path6.sep);
|
|
5532
|
+
let current = path6.resolve(root);
|
|
5306
5533
|
for (const segment of relativeSegments) {
|
|
5307
5534
|
if (!segment)
|
|
5308
5535
|
continue;
|
|
5309
|
-
current =
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
}
|
|
5314
|
-
} catch (error) {
|
|
5315
|
-
if (error instanceof CliError)
|
|
5316
|
-
throw error;
|
|
5317
|
-
if (error.code !== "ENOENT")
|
|
5318
|
-
throw error;
|
|
5536
|
+
current = path6.join(current, segment);
|
|
5537
|
+
const entry = await fs.inspect(current);
|
|
5538
|
+
if (entry?.isSymbolicLink) {
|
|
5539
|
+
throw new CliError(`Symbolic links are forbidden in project paths: ${current}`, { code: "UNSAFE_PATH" });
|
|
5319
5540
|
}
|
|
5320
5541
|
}
|
|
5321
5542
|
return safeTarget;
|
|
5322
5543
|
}
|
|
5323
5544
|
function normalizeImportAlias(value) {
|
|
5324
5545
|
if (!/^[A-Za-z@~][A-Za-z0-9@~_-]*\/\*$/.test(value)) {
|
|
5325
|
-
throw new CliError('Invalid import alias: expected a prefix followed by "/*" (for example "@/*" or "@core/*").');
|
|
5546
|
+
throw new CliError('Invalid import alias: expected a prefix followed by "/*" (for example "@/*" or "@core/*").', { code: "INVALID_ARGUMENT" });
|
|
5326
5547
|
}
|
|
5327
5548
|
return value;
|
|
5328
5549
|
}
|
|
5329
5550
|
|
|
5330
5551
|
// src/lib/addApi.ts
|
|
5331
|
-
async
|
|
5552
|
+
var addApi = async (args, context) => {
|
|
5332
5553
|
let apiName = args[1];
|
|
5333
5554
|
if (!apiName || apiName.startsWith("-")) {
|
|
5334
|
-
|
|
5555
|
+
if (context.outputMode === "json") {
|
|
5556
|
+
throw new CliError("API route name is required in JSON mode.", {
|
|
5557
|
+
code: "INTERACTIVE_INPUT_REQUIRED",
|
|
5558
|
+
hint: "Pass the API route name after addapi."
|
|
5559
|
+
});
|
|
5560
|
+
}
|
|
5561
|
+
const response = await context.prompt({
|
|
5335
5562
|
type: "text",
|
|
5336
5563
|
name: "apiName",
|
|
5337
|
-
message: "
|
|
5564
|
+
message: "API route name to add:",
|
|
5338
5565
|
validate: (name) => name ? true : "API route name is required"
|
|
5339
5566
|
});
|
|
5340
|
-
apiName = response.apiName;
|
|
5567
|
+
apiName = String(response.apiName ?? "");
|
|
5568
|
+
if (!apiName) {
|
|
5569
|
+
context.operations.record({
|
|
5570
|
+
action: "cancelled",
|
|
5571
|
+
resource: "command",
|
|
5572
|
+
role: "api-creation",
|
|
5573
|
+
scope: "project",
|
|
5574
|
+
path: "."
|
|
5575
|
+
});
|
|
5576
|
+
return commandResult(context, {
|
|
5577
|
+
command: "addapi",
|
|
5578
|
+
summary: "API route creation was cancelled.",
|
|
5579
|
+
projectRoot: context.cwd,
|
|
5580
|
+
status: "cancelled"
|
|
5581
|
+
});
|
|
5582
|
+
}
|
|
5341
5583
|
}
|
|
5342
5584
|
const apiSegments = parseLogicalName(apiName, "API route name");
|
|
5343
|
-
const config = await loadConfig(
|
|
5585
|
+
const config = await loadConfig(context);
|
|
5344
5586
|
if (!config) {
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
await assertSafeTarget(cwd, apiDir);
|
|
5350
|
-
if (!existsSync3(apiDir)) {
|
|
5351
|
-
await mkdir2(apiDir, { recursive: true });
|
|
5587
|
+
throw new CliError("Configuration file cnp.config.json was not found.", {
|
|
5588
|
+
code: "CONFIG_NOT_FOUND",
|
|
5589
|
+
hint: "Run this command from the generated project root."
|
|
5590
|
+
});
|
|
5352
5591
|
}
|
|
5353
|
-
const
|
|
5592
|
+
const apiDir = join2(context.cwd, "src", "app", "api", ...apiSegments);
|
|
5593
|
+
await assertSafeTarget(context.cwd, apiDir, context.fs);
|
|
5594
|
+
const gateway = new MutationGateway(context, context.cwd);
|
|
5595
|
+
const templateDir = join2(context.packageRoot, "templates", "Api");
|
|
5354
5596
|
const routeTemplate = join2(templateDir, "route.ts");
|
|
5355
5597
|
const routePath = join2(apiDir, "route.ts");
|
|
5356
|
-
if (!
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
}
|
|
5362
|
-
await writeFile2(routePath, `import { NextResponse } from "next/server";
|
|
5363
|
-
|
|
5364
|
-
export async function GET() {
|
|
5365
|
-
return NextResponse.json({ message: "Hello from ${apiName}" });
|
|
5366
|
-
}
|
|
5367
|
-
`);
|
|
5368
|
-
}
|
|
5369
|
-
console.log(`\uD83D\uDCC4 File created: ${routePath}`);
|
|
5370
|
-
} else {
|
|
5371
|
-
console.log(`\u2139\uFE0F File already exists: ${routePath}`);
|
|
5598
|
+
if (!context.fs.exists(routeTemplate)) {
|
|
5599
|
+
throw new CliError("API template route.ts was not found.", {
|
|
5600
|
+
code: "TEMPLATE_MISSING",
|
|
5601
|
+
scope: "package",
|
|
5602
|
+
path: "templates/Api/route.ts"
|
|
5603
|
+
});
|
|
5372
5604
|
}
|
|
5373
|
-
|
|
5374
|
-
|
|
5605
|
+
await gateway.mkdir(apiDir, {
|
|
5606
|
+
role: "api-directory",
|
|
5607
|
+
resource: "directory"
|
|
5608
|
+
});
|
|
5609
|
+
const content = (await context.fs.readText(routeTemplate)).replace(/template/g, apiName);
|
|
5610
|
+
const action = await gateway.write(routePath, content, {
|
|
5611
|
+
role: "api-route",
|
|
5612
|
+
preserveExisting: true
|
|
5613
|
+
});
|
|
5614
|
+
return commandResult(context, {
|
|
5615
|
+
command: "addapi",
|
|
5616
|
+
summary: action === "unchanged" ? `API route "${apiName}" already exists and was preserved.` : `Added API route "${apiName}".`,
|
|
5617
|
+
projectRoot: context.cwd,
|
|
5618
|
+
nextSteps: action === "unchanged" ? [] : [
|
|
5619
|
+
{
|
|
5620
|
+
kind: "review",
|
|
5621
|
+
required: true,
|
|
5622
|
+
message: "Replace the example response and review validation and authentication.",
|
|
5623
|
+
paths: [{ scope: "project", path: gateway.path(routePath) }]
|
|
5624
|
+
},
|
|
5625
|
+
{
|
|
5626
|
+
kind: "run-checks",
|
|
5627
|
+
required: true,
|
|
5628
|
+
message: "Run the project checks.",
|
|
5629
|
+
paths: [],
|
|
5630
|
+
commands: ["bun run check", "npm run check", "pnpm run check"]
|
|
5631
|
+
}
|
|
5632
|
+
]
|
|
5633
|
+
});
|
|
5634
|
+
};
|
|
5375
5635
|
|
|
5376
5636
|
// src/lib/addComponent.ts
|
|
5377
|
-
var import_prompts3 = __toESM(require_prompts3(), 1);
|
|
5378
5637
|
import { join as join3 } from "path";
|
|
5379
|
-
|
|
5380
|
-
import { existsSync as existsSync4, statSync } from "fs";
|
|
5381
|
-
async function addComponent(args, cwd = process.cwd()) {
|
|
5638
|
+
var addComponent = async (args, context) => {
|
|
5382
5639
|
let componentName = args[1];
|
|
5383
|
-
|
|
5384
|
-
const
|
|
5385
|
-
if (pageIndex
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
if (pageScope && pageScope.includes(".")) {
|
|
5390
|
-
nestedPath = join3(...pageScope.split("."));
|
|
5640
|
+
const pageIndex = args.findIndex((argument) => argument === "-P" || argument === "--page");
|
|
5641
|
+
const pageScope = pageIndex >= 0 ? args[pageIndex + 1] : undefined;
|
|
5642
|
+
if (pageIndex >= 0 && !pageScope) {
|
|
5643
|
+
throw new CliError("The --page option requires a page name.", {
|
|
5644
|
+
code: "INVALID_ARGUMENT"
|
|
5645
|
+
});
|
|
5391
5646
|
}
|
|
5392
5647
|
if (!componentName || componentName.startsWith("-")) {
|
|
5393
|
-
|
|
5648
|
+
if (context.outputMode === "json") {
|
|
5649
|
+
throw new CliError("Component name is required in JSON mode.", {
|
|
5650
|
+
code: "INTERACTIVE_INPUT_REQUIRED",
|
|
5651
|
+
hint: "Pass the component name after addcomponent."
|
|
5652
|
+
});
|
|
5653
|
+
}
|
|
5654
|
+
const response = await context.prompt({
|
|
5394
5655
|
type: "text",
|
|
5395
5656
|
name: "componentName",
|
|
5396
|
-
message: "
|
|
5657
|
+
message: "Component name to add:",
|
|
5397
5658
|
validate: (name) => name ? true : "Component name is required"
|
|
5398
5659
|
});
|
|
5399
|
-
componentName = response.componentName;
|
|
5660
|
+
componentName = String(response.componentName ?? "");
|
|
5661
|
+
if (!componentName) {
|
|
5662
|
+
context.operations.record({
|
|
5663
|
+
action: "cancelled",
|
|
5664
|
+
resource: "command",
|
|
5665
|
+
role: "component-creation",
|
|
5666
|
+
scope: "project",
|
|
5667
|
+
path: "."
|
|
5668
|
+
});
|
|
5669
|
+
return commandResult(context, {
|
|
5670
|
+
command: "addcomponent",
|
|
5671
|
+
summary: "Component creation was cancelled.",
|
|
5672
|
+
projectRoot: context.cwd,
|
|
5673
|
+
status: "cancelled"
|
|
5674
|
+
});
|
|
5675
|
+
}
|
|
5676
|
+
}
|
|
5677
|
+
const componentSegments = parseLogicalName(componentName, "component name");
|
|
5678
|
+
if (componentSegments.length !== 1) {
|
|
5679
|
+
throw new CliError("Component names must contain exactly one segment.", {
|
|
5680
|
+
code: "INVALID_ARGUMENT"
|
|
5681
|
+
});
|
|
5400
5682
|
}
|
|
5401
|
-
parseLogicalName(
|
|
5402
|
-
|
|
5403
|
-
parseLogicalName(pageScope, "page name");
|
|
5404
|
-
const config = await loadConfig(cwd);
|
|
5683
|
+
const pageSegments = pageScope ? parseLogicalName(pageScope, "page name") : [];
|
|
5684
|
+
const config = await loadConfig(context);
|
|
5405
5685
|
if (!config) {
|
|
5406
|
-
|
|
5407
|
-
|
|
5686
|
+
throw new CliError("Configuration file cnp.config.json was not found.", {
|
|
5687
|
+
code: "CONFIG_NOT_FOUND",
|
|
5688
|
+
hint: "Run this command from the generated project root."
|
|
5689
|
+
});
|
|
5408
5690
|
}
|
|
5409
|
-
const
|
|
5410
|
-
const
|
|
5411
|
-
const
|
|
5412
|
-
|
|
5413
|
-
if (useI18n) {
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
}
|
|
5691
|
+
const componentNameUpper = capitalize(toIdentifier(componentName));
|
|
5692
|
+
const templateRoot = join3(context.packageRoot, "templates", "Component");
|
|
5693
|
+
const componentTemplate = join3(templateRoot, "Component.tsx");
|
|
5694
|
+
const messagesTemplate = join3(templateRoot, "component.json");
|
|
5695
|
+
if (!context.fs.exists(componentTemplate) || config.useI18n && !context.fs.exists(messagesTemplate)) {
|
|
5696
|
+
throw new CliError("Required component template files were not found.", {
|
|
5697
|
+
code: "TEMPLATE_MISSING",
|
|
5698
|
+
scope: "package",
|
|
5699
|
+
path: "templates/Component"
|
|
5700
|
+
});
|
|
5419
5701
|
}
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5702
|
+
const targetDirectory = pageScope ? join3(context.cwd, "src", "ui", ...pageSegments) : join3(context.cwd, "src", "ui", "_global");
|
|
5703
|
+
await assertSafeTarget(context.cwd, targetDirectory, context.fs);
|
|
5704
|
+
const componentFile = join3(targetDirectory, `${componentNameUpper}.tsx`);
|
|
5705
|
+
const translationNamespace = pageScope ?? "_global_ui";
|
|
5706
|
+
const componentContent = (await context.fs.readText(componentTemplate)).replace(/Component/g, componentNameUpper).replace(/componentPage/g, translationNamespace);
|
|
5707
|
+
const preparedMessages = [];
|
|
5708
|
+
if (config.useI18n) {
|
|
5709
|
+
const messagesRoot = join3(context.cwd, "messages");
|
|
5710
|
+
if (!context.fs.exists(messagesRoot)) {
|
|
5711
|
+
throw new CliError("The messages directory was not found.", {
|
|
5712
|
+
code: "CONFIG_NOT_FOUND",
|
|
5713
|
+
scope: "project",
|
|
5714
|
+
path: "messages"
|
|
5715
|
+
});
|
|
5429
5716
|
}
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
const componentFile = join3(componentTargetPath, `${componentNameUpper}.tsx`);
|
|
5439
|
-
const templateComponentPath = join3(templatePath, "Component.tsx");
|
|
5440
|
-
if (existsSync4(templateComponentPath)) {
|
|
5441
|
-
let content = await readFile4(templateComponentPath, "utf-8");
|
|
5442
|
-
content = content.replace(/Component/g, componentNameUpper).replace(/componentPage/g, translationKey);
|
|
5443
|
-
await writeFile3(componentFile, content);
|
|
5444
|
-
console.log(`\uD83D\uDCC4 File created: ${componentFile}`);
|
|
5445
|
-
} else {
|
|
5446
|
-
console.error("\u274C Template Component.tsx introuvable :", templateComponentPath);
|
|
5447
|
-
}
|
|
5448
|
-
if (useI18n && messagesPath) {
|
|
5449
|
-
const entries = await readdir3(messagesPath, { withFileTypes: true });
|
|
5450
|
-
const langDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
5451
|
-
const jsonTemplate = join3(templatePath, "component.json");
|
|
5452
|
-
if (!existsSync4(jsonTemplate)) {
|
|
5453
|
-
console.error("\u274C Template component.json not found:", jsonTemplate);
|
|
5454
|
-
return;
|
|
5717
|
+
const templateMessages = JSON.parse(await context.fs.readText(messagesTemplate));
|
|
5718
|
+
const locales = (await context.fs.list(messagesRoot)).filter((entry) => entry.isDirectory).map((entry) => entry.name).sort();
|
|
5719
|
+
if (locales.length === 0) {
|
|
5720
|
+
throw new CliError("No locale directories were found.", {
|
|
5721
|
+
code: "CONFIG_NOT_FOUND",
|
|
5722
|
+
scope: "project",
|
|
5723
|
+
path: "messages"
|
|
5724
|
+
});
|
|
5455
5725
|
}
|
|
5456
|
-
const
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
if (
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
if (existsSync4(jsonTarget)) {
|
|
5471
|
-
const jsonFile = await readFile4(jsonTarget, "utf-8");
|
|
5472
|
-
current = JSON.parse(jsonFile);
|
|
5473
|
-
}
|
|
5474
|
-
if (pageScope?.includes(".")) {
|
|
5475
|
-
const child = pageScope.split(".")[1];
|
|
5476
|
-
const childMessages = current[child] && typeof current[child] === "object" ? current[child] : {};
|
|
5477
|
-
childMessages[componentNameUpper] = parsed;
|
|
5478
|
-
current[child] = childMessages;
|
|
5479
|
-
} else {
|
|
5480
|
-
current[componentNameUpper] = parsed;
|
|
5726
|
+
for (const locale of locales) {
|
|
5727
|
+
const messageFile = pageScope ? pageSegments[0] : "_global_ui";
|
|
5728
|
+
const target = join3(messagesRoot, locale, `${messageFile}.json`);
|
|
5729
|
+
let data = {};
|
|
5730
|
+
if (context.fs.exists(target)) {
|
|
5731
|
+
try {
|
|
5732
|
+
data = JSON.parse(await context.fs.readText(target));
|
|
5733
|
+
} catch {
|
|
5734
|
+
throw new CliError(`Invalid JSON in messages/${locale}/${messageFile}.json.`, {
|
|
5735
|
+
code: "FILESYSTEM_ERROR",
|
|
5736
|
+
scope: "project",
|
|
5737
|
+
path: `messages/${locale}/${messageFile}.json`
|
|
5738
|
+
});
|
|
5739
|
+
}
|
|
5481
5740
|
}
|
|
5482
|
-
|
|
5483
|
-
|
|
5741
|
+
let container = data;
|
|
5742
|
+
if (pageSegments.length > 1) {
|
|
5743
|
+
const child = pageSegments[1];
|
|
5744
|
+
const current = data[child];
|
|
5745
|
+
if (current !== undefined && (!current || typeof current !== "object" || Array.isArray(current))) {
|
|
5746
|
+
throw new CliError(`Translation namespace ${pageScope} is not an object in ${locale}.`, {
|
|
5747
|
+
code: "INVALID_ARGUMENT",
|
|
5748
|
+
scope: "project",
|
|
5749
|
+
path: `messages/${locale}/${messageFile}.json`
|
|
5750
|
+
});
|
|
5751
|
+
}
|
|
5752
|
+
if (!current)
|
|
5753
|
+
data[child] = {};
|
|
5754
|
+
container = data[child];
|
|
5755
|
+
}
|
|
5756
|
+
const exists = Object.hasOwn(container, componentNameUpper);
|
|
5757
|
+
if (!exists)
|
|
5758
|
+
container[componentNameUpper] = templateMessages;
|
|
5759
|
+
preparedMessages.push({
|
|
5760
|
+
locale,
|
|
5761
|
+
target,
|
|
5762
|
+
exists,
|
|
5763
|
+
content: exists ? undefined : `${JSON.stringify(data, null, 2)}
|
|
5764
|
+
`
|
|
5765
|
+
});
|
|
5484
5766
|
}
|
|
5485
|
-
} else {
|
|
5486
|
-
console.log("\u2139\uFE0F Skipping translation entries; next-intl not enabled.");
|
|
5487
5767
|
}
|
|
5488
|
-
|
|
5489
|
-
|
|
5768
|
+
const gateway = new MutationGateway(context, context.cwd);
|
|
5769
|
+
await gateway.mkdir(targetDirectory, {
|
|
5770
|
+
role: "component-directory",
|
|
5771
|
+
resource: "directory"
|
|
5772
|
+
});
|
|
5773
|
+
await gateway.write(componentFile, componentContent, {
|
|
5774
|
+
role: "ui-component",
|
|
5775
|
+
preserveExisting: true
|
|
5776
|
+
});
|
|
5777
|
+
for (const item of preparedMessages) {
|
|
5778
|
+
if (item.exists) {
|
|
5779
|
+
gateway.unchanged(item.target, {
|
|
5780
|
+
role: "translation-messages",
|
|
5781
|
+
detail: {
|
|
5782
|
+
locale: item.locale,
|
|
5783
|
+
key: `${translationNamespace}.${componentNameUpper}`
|
|
5784
|
+
}
|
|
5785
|
+
});
|
|
5786
|
+
} else {
|
|
5787
|
+
await gateway.write(item.target, item.content, {
|
|
5788
|
+
role: "translation-messages",
|
|
5789
|
+
detail: {
|
|
5790
|
+
locale: item.locale,
|
|
5791
|
+
key: `${translationNamespace}.${componentNameUpper}`
|
|
5792
|
+
}
|
|
5793
|
+
});
|
|
5794
|
+
}
|
|
5795
|
+
}
|
|
5796
|
+
const mutated = context.operations.snapshot().some((event) => event.action === "created" || event.action === "updated");
|
|
5797
|
+
return commandResult(context, {
|
|
5798
|
+
command: "addcomponent",
|
|
5799
|
+
summary: mutated ? `Added component "${componentNameUpper}" ${pageScope ? `to page "${pageScope}"` : "globally"}.` : `Component "${componentNameUpper}" already exists and was preserved.`,
|
|
5800
|
+
projectRoot: context.cwd,
|
|
5801
|
+
nextSteps: mutated ? [
|
|
5802
|
+
{
|
|
5803
|
+
kind: "review",
|
|
5804
|
+
required: true,
|
|
5805
|
+
message: "Review the generated component and its localized messages.",
|
|
5806
|
+
paths: [
|
|
5807
|
+
{ scope: "project", path: gateway.path(componentFile) },
|
|
5808
|
+
...preparedMessages.map((item) => ({
|
|
5809
|
+
scope: "project",
|
|
5810
|
+
path: gateway.path(item.target)
|
|
5811
|
+
}))
|
|
5812
|
+
]
|
|
5813
|
+
},
|
|
5814
|
+
{
|
|
5815
|
+
kind: "run-checks",
|
|
5816
|
+
required: true,
|
|
5817
|
+
message: "Run the project checks.",
|
|
5818
|
+
paths: [],
|
|
5819
|
+
commands: ["bun run check", "npm run check", "pnpm run check"]
|
|
5820
|
+
}
|
|
5821
|
+
] : []
|
|
5822
|
+
});
|
|
5823
|
+
};
|
|
5490
5824
|
|
|
5491
5825
|
// src/lib/addLanguage.ts
|
|
5492
|
-
var import_prompts4 = __toESM(require_prompts3(), 1);
|
|
5493
5826
|
import { join as join4 } from "path";
|
|
5494
|
-
import { existsSync as existsSync5 } from "fs";
|
|
5495
|
-
import { cp, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
5496
5827
|
function generateLocales() {
|
|
5497
|
-
const
|
|
5828
|
+
const names = new Intl.DisplayNames(["en"], { type: "language" });
|
|
5498
5829
|
const locales = [];
|
|
5499
|
-
for (let
|
|
5500
|
-
for (let
|
|
5501
|
-
const code = String.fromCharCode(97 +
|
|
5830
|
+
for (let first = 0;first < 26; first++) {
|
|
5831
|
+
for (let second = 0;second < 26; second++) {
|
|
5832
|
+
const code = String.fromCharCode(97 + first, 97 + second);
|
|
5502
5833
|
try {
|
|
5503
|
-
const name =
|
|
5504
|
-
if (name && name.toLowerCase() !== code)
|
|
5834
|
+
const name = names.of(code);
|
|
5835
|
+
if (name && name.toLowerCase() !== code)
|
|
5505
5836
|
locales.push(code);
|
|
5506
|
-
}
|
|
5507
5837
|
} catch {}
|
|
5508
5838
|
}
|
|
5509
5839
|
}
|
|
5510
5840
|
return locales.sort();
|
|
5511
5841
|
}
|
|
5512
|
-
async function
|
|
5513
|
-
const
|
|
5842
|
+
async function listFiles(context, root) {
|
|
5843
|
+
const files = [];
|
|
5844
|
+
async function visit(directory, relative = "") {
|
|
5845
|
+
for (const entry of await context.fs.list(directory)) {
|
|
5846
|
+
const next = relative ? `${relative}/${entry.name}` : entry.name;
|
|
5847
|
+
if (entry.isSymbolicLink) {
|
|
5848
|
+
throw new CliError(`Symbolic link found in locale source: ${next}`, {
|
|
5849
|
+
code: "UNSAFE_PATH",
|
|
5850
|
+
scope: "project",
|
|
5851
|
+
path: next
|
|
5852
|
+
});
|
|
5853
|
+
}
|
|
5854
|
+
if (entry.isDirectory)
|
|
5855
|
+
await visit(join4(directory, entry.name), next);
|
|
5856
|
+
else if (entry.isFile)
|
|
5857
|
+
files.push(next);
|
|
5858
|
+
}
|
|
5859
|
+
}
|
|
5860
|
+
await visit(root);
|
|
5861
|
+
return files.sort();
|
|
5862
|
+
}
|
|
5863
|
+
var addLanguage = async (args, context) => {
|
|
5864
|
+
const config = await loadConfig(context);
|
|
5514
5865
|
if (!config?.useI18n) {
|
|
5515
|
-
|
|
5516
|
-
|
|
5866
|
+
throw new CliError("Internationalization is not enabled in this project.", {
|
|
5867
|
+
code: "I18N_DISABLED"
|
|
5868
|
+
});
|
|
5517
5869
|
}
|
|
5518
|
-
const
|
|
5519
|
-
if (!
|
|
5520
|
-
|
|
5521
|
-
|
|
5870
|
+
const messagesRoot = join4(context.cwd, "messages");
|
|
5871
|
+
if (!context.fs.exists(messagesRoot)) {
|
|
5872
|
+
throw new CliError("The messages directory was not found.", {
|
|
5873
|
+
code: "CONFIG_NOT_FOUND",
|
|
5874
|
+
scope: "project",
|
|
5875
|
+
path: "messages"
|
|
5876
|
+
});
|
|
5522
5877
|
}
|
|
5523
5878
|
const available = generateLocales();
|
|
5524
5879
|
let locale = args[1];
|
|
5525
|
-
if (!locale
|
|
5526
|
-
|
|
5880
|
+
if (!locale) {
|
|
5881
|
+
if (context.outputMode === "json") {
|
|
5882
|
+
throw new CliError("Locale is required in JSON mode.", {
|
|
5883
|
+
code: "INTERACTIVE_INPUT_REQUIRED",
|
|
5884
|
+
hint: "Pass a two-letter locale after addlanguage."
|
|
5885
|
+
});
|
|
5886
|
+
}
|
|
5887
|
+
const response = await context.prompt({
|
|
5527
5888
|
type: "autocomplete",
|
|
5528
5889
|
name: "locale",
|
|
5529
|
-
message: "
|
|
5530
|
-
choices: available.map((
|
|
5890
|
+
message: "Locale to add:",
|
|
5891
|
+
choices: available.map((value) => ({ title: value, value }))
|
|
5531
5892
|
});
|
|
5532
|
-
locale = response.locale;
|
|
5893
|
+
locale = String(response.locale ?? "");
|
|
5894
|
+
if (!locale) {
|
|
5895
|
+
context.operations.record({
|
|
5896
|
+
action: "cancelled",
|
|
5897
|
+
resource: "command",
|
|
5898
|
+
role: "locale-creation",
|
|
5899
|
+
scope: "project",
|
|
5900
|
+
path: "."
|
|
5901
|
+
});
|
|
5902
|
+
return commandResult(context, {
|
|
5903
|
+
command: "addlanguage",
|
|
5904
|
+
summary: "Locale creation was cancelled.",
|
|
5905
|
+
projectRoot: context.cwd,
|
|
5906
|
+
status: "cancelled"
|
|
5907
|
+
});
|
|
5908
|
+
}
|
|
5533
5909
|
}
|
|
5534
|
-
if (!locale)
|
|
5535
|
-
return;
|
|
5536
5910
|
parseLogicalName(locale, "locale");
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5911
|
+
if (!available.includes(locale)) {
|
|
5912
|
+
throw new CliError(`Unsupported locale code: ${locale}.`, {
|
|
5913
|
+
code: "INVALID_ARGUMENT",
|
|
5914
|
+
hint: "Use a recognized two-letter language code."
|
|
5915
|
+
});
|
|
5540
5916
|
}
|
|
5541
|
-
const routingFile = join4(cwd, "src", "lib", "i18n", "routing.ts");
|
|
5542
|
-
const
|
|
5543
|
-
if (!
|
|
5544
|
-
throw new
|
|
5917
|
+
const routingFile = join4(context.cwd, "src", "lib", "i18n", "routing.ts");
|
|
5918
|
+
const registryFile = join4(context.cwd, "src", "lib", "i18n", "messages.ts");
|
|
5919
|
+
if (!context.fs.exists(routingFile) || !context.fs.exists(registryFile)) {
|
|
5920
|
+
throw new CliError("Required i18n routing or registry file was not found.", {
|
|
5921
|
+
code: "CONFIG_NOT_FOUND",
|
|
5922
|
+
scope: "project",
|
|
5923
|
+
path: "src/lib/i18n"
|
|
5924
|
+
});
|
|
5545
5925
|
}
|
|
5546
|
-
|
|
5547
|
-
|
|
5926
|
+
const routing = await context.fs.readText(routingFile);
|
|
5927
|
+
const defaultLocale = routing.match(/defaultLocale:\s*["']([^"']+)["']/)?.[1];
|
|
5928
|
+
if (!defaultLocale) {
|
|
5929
|
+
throw new CliError("The default locale could not be resolved from routing.ts.", {
|
|
5930
|
+
code: "CONFIG_NOT_FOUND",
|
|
5931
|
+
scope: "project",
|
|
5932
|
+
path: "src/lib/i18n/routing.ts"
|
|
5933
|
+
});
|
|
5548
5934
|
}
|
|
5549
|
-
const
|
|
5550
|
-
const
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5935
|
+
const sourceDirectory = join4(messagesRoot, defaultLocale);
|
|
5936
|
+
const sourceAggregator = join4(messagesRoot, `${defaultLocale}.ts`);
|
|
5937
|
+
if (!context.fs.exists(sourceDirectory)) {
|
|
5938
|
+
throw new CliError(`Default locale directory ${defaultLocale} was not found.`, {
|
|
5939
|
+
code: "CONFIG_NOT_FOUND",
|
|
5940
|
+
scope: "project",
|
|
5941
|
+
path: `messages/${defaultLocale}`
|
|
5942
|
+
});
|
|
5554
5943
|
}
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5944
|
+
if (!context.fs.exists(sourceAggregator)) {
|
|
5945
|
+
throw new CliError("Default locale aggregator not found.", {
|
|
5946
|
+
code: "CONFIG_NOT_FOUND",
|
|
5947
|
+
scope: "project",
|
|
5948
|
+
path: `messages/${defaultLocale}.ts`
|
|
5949
|
+
});
|
|
5558
5950
|
}
|
|
5559
|
-
const
|
|
5951
|
+
const sourceFiles = await listFiles(context, sourceDirectory);
|
|
5952
|
+
if (sourceFiles.length === 0) {
|
|
5953
|
+
throw new CliError(`Default locale ${defaultLocale} contains no files.`, {
|
|
5954
|
+
code: "CONFIG_NOT_FOUND",
|
|
5955
|
+
scope: "project",
|
|
5956
|
+
path: `messages/${defaultLocale}`
|
|
5957
|
+
});
|
|
5958
|
+
}
|
|
5959
|
+
const defaultAggregator = await context.fs.readText(sourceAggregator);
|
|
5960
|
+
const importPrefix = `./${defaultLocale}/`;
|
|
5961
|
+
if (!defaultAggregator.includes(importPrefix)) {
|
|
5962
|
+
throw new CliError(`Default aggregator does not import from ${importPrefix}.`, {
|
|
5963
|
+
code: "CONFIG_NOT_FOUND",
|
|
5964
|
+
scope: "project",
|
|
5965
|
+
path: `messages/${defaultLocale}.ts`
|
|
5966
|
+
});
|
|
5967
|
+
}
|
|
5968
|
+
const localesMatch = routing.match(/locales:\s*\[([^\]]*)\]/);
|
|
5560
5969
|
if (!localesMatch) {
|
|
5561
|
-
throw new
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
}
|
|
5567
|
-
const
|
|
5568
|
-
const
|
|
5569
|
-
const
|
|
5570
|
-
const defaultImportPrefix = `./${defaultLocale}/`;
|
|
5571
|
-
if (!defaultAggregatorContent.includes(defaultImportPrefix)) {
|
|
5572
|
-
throw new Error(`Default locale aggregator does not import from ${defaultImportPrefix}`);
|
|
5573
|
-
}
|
|
5574
|
-
const nextAggregatorContent = defaultAggregatorContent.replaceAll(defaultImportPrefix, `./${locale}/`);
|
|
5575
|
-
const messagesRegistryContent = await readFile5(messagesRegistryFile, "utf-8");
|
|
5576
|
-
const registryMatch = messagesRegistryContent.match(/const messages = \{([^}]*)\} as const;/);
|
|
5970
|
+
throw new CliError("Unable to locate routing locales.", {
|
|
5971
|
+
code: "CONFIG_NOT_FOUND",
|
|
5972
|
+
scope: "project",
|
|
5973
|
+
path: "src/lib/i18n/routing.ts"
|
|
5974
|
+
});
|
|
5975
|
+
}
|
|
5976
|
+
const routingLocales = localesMatch[1].split(",").map((value) => value.trim().replace(/["']/g, "")).filter(Boolean);
|
|
5977
|
+
const registry = await context.fs.readText(registryFile);
|
|
5978
|
+
const registryMatch = registry.match(/const messages = \{([^}]*)\} as const;/);
|
|
5577
5979
|
if (!registryMatch) {
|
|
5578
|
-
throw new
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
}
|
|
5584
|
-
const
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
const
|
|
5589
|
-
const
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5980
|
+
throw new CliError("Unable to locate the typed messages registry.", {
|
|
5981
|
+
code: "CONFIG_NOT_FOUND",
|
|
5982
|
+
scope: "project",
|
|
5983
|
+
path: "src/lib/i18n/messages.ts"
|
|
5984
|
+
});
|
|
5985
|
+
}
|
|
5986
|
+
const registeredLocales = registryMatch[1].split(",").map((value) => value.trim()).filter(Boolean);
|
|
5987
|
+
const targetDirectory = join4(messagesRoot, locale);
|
|
5988
|
+
const targetAggregator = join4(messagesRoot, `${locale}.ts`);
|
|
5989
|
+
const targetAggregatorExists = context.fs.exists(targetAggregator);
|
|
5990
|
+
const targetAggregatorContent = targetAggregatorExists ? await context.fs.readText(targetAggregator) : "";
|
|
5991
|
+
const state = {
|
|
5992
|
+
directory: context.fs.exists(targetDirectory),
|
|
5993
|
+
aggregator: targetAggregatorExists,
|
|
5994
|
+
aggregatorImportsLocale: targetAggregatorContent.includes(`./${locale}/`),
|
|
5995
|
+
routing: routingLocales.includes(locale),
|
|
5996
|
+
registry: registeredLocales.includes(locale),
|
|
5997
|
+
registryImport: new RegExp(`import\\s+${locale}\\s+from\\s+["']\\.\\.\\/\\.\\.\\/\\.\\.\\/messages\\/${locale}["'];`).test(registry)
|
|
5998
|
+
};
|
|
5999
|
+
const present = Object.values(state).filter(Boolean).length;
|
|
6000
|
+
const gateway = new MutationGateway(context, context.cwd);
|
|
6001
|
+
const missingLocaleFiles = state.directory ? sourceFiles.filter((relative) => !context.fs.exists(join4(targetDirectory, relative))) : sourceFiles;
|
|
6002
|
+
if (present === Object.keys(state).length && missingLocaleFiles.length === 0) {
|
|
6003
|
+
gateway.unchanged(targetDirectory, {
|
|
6004
|
+
role: "locale-directory",
|
|
6005
|
+
resource: "directory"
|
|
6006
|
+
});
|
|
6007
|
+
for (const relative of sourceFiles) {
|
|
6008
|
+
gateway.unchanged(join4(targetDirectory, relative), {
|
|
6009
|
+
role: relative.endsWith(".json") ? "translation-messages" : "locale-file",
|
|
6010
|
+
detail: { locale, sourceLocale: defaultLocale }
|
|
6011
|
+
});
|
|
6012
|
+
}
|
|
6013
|
+
gateway.unchanged(targetAggregator, { role: "locale-aggregator" });
|
|
6014
|
+
gateway.unchanged(routingFile, { role: "i18n-routing" });
|
|
6015
|
+
gateway.unchanged(registryFile, { role: "messages-registry" });
|
|
6016
|
+
return commandResult(context, {
|
|
6017
|
+
command: "addlanguage",
|
|
6018
|
+
summary: `Locale "${locale}" is already fully configured.`,
|
|
6019
|
+
projectRoot: context.cwd
|
|
6020
|
+
});
|
|
6021
|
+
}
|
|
6022
|
+
if (present > 0 || missingLocaleFiles.length < sourceFiles.length) {
|
|
6023
|
+
throw new CliError(`Locale ${locale} is only partially configured.`, {
|
|
6024
|
+
code: "INCONSISTENT_LOCALE",
|
|
6025
|
+
scope: "project",
|
|
6026
|
+
path: `messages/${locale}`,
|
|
6027
|
+
hint: `Observed state: ${JSON.stringify({ ...state, missingLocaleFiles })}.`
|
|
6028
|
+
});
|
|
6029
|
+
}
|
|
6030
|
+
const nextAggregator = defaultAggregator.replaceAll(importPrefix, `./${locale}/`);
|
|
6031
|
+
const nextRouting = routing.replace(/locales:\s*\[[^\]]*\]/, `locales: [${[...routingLocales, locale].map((value) => `"${value}"`).join(", ")}]`);
|
|
6032
|
+
const declarationIndex = registry.indexOf("const messages =");
|
|
6033
|
+
const nextRegistry = registry.slice(0, declarationIndex) + `import ${locale} from "../../../messages/${locale}";
|
|
6034
|
+
|
|
6035
|
+
` + registry.slice(declarationIndex).replace(/const messages = \{[^}]*\} as const;/, `const messages = { ${[...registeredLocales, locale].join(", ")} } as const;`);
|
|
6036
|
+
await assertSafeTarget(context.cwd, targetDirectory, context.fs);
|
|
6037
|
+
await gateway.mkdir(targetDirectory, {
|
|
6038
|
+
role: "locale-directory",
|
|
6039
|
+
resource: "directory",
|
|
6040
|
+
detail: { locale, sourceLocale: defaultLocale }
|
|
5594
6041
|
});
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
}
|
|
6042
|
+
for (const relative of sourceFiles) {
|
|
6043
|
+
const source = join4(sourceDirectory, relative);
|
|
6044
|
+
const target = join4(targetDirectory, relative);
|
|
6045
|
+
await gateway.copy(source, target, {
|
|
6046
|
+
role: relative.endsWith(".json") ? "translation-messages" : "locale-file",
|
|
6047
|
+
source: { scope: "project", path: gateway.path(source) },
|
|
6048
|
+
detail: { locale, sourceLocale: defaultLocale }
|
|
6049
|
+
});
|
|
6050
|
+
}
|
|
6051
|
+
await gateway.write(targetAggregator, nextAggregator, {
|
|
6052
|
+
role: "locale-aggregator"
|
|
6053
|
+
});
|
|
6054
|
+
await gateway.write(registryFile, nextRegistry, {
|
|
6055
|
+
role: "messages-registry"
|
|
6056
|
+
});
|
|
6057
|
+
await gateway.write(routingFile, nextRouting, { role: "i18n-routing" });
|
|
6058
|
+
const translationPaths = sourceFiles.filter((relative) => relative.endsWith(".json")).map((relative) => ({
|
|
6059
|
+
scope: "project",
|
|
6060
|
+
path: gateway.path(join4(targetDirectory, relative))
|
|
6061
|
+
}));
|
|
6062
|
+
return commandResult(context, {
|
|
6063
|
+
command: "addlanguage",
|
|
6064
|
+
summary: `Added locale "${locale}" by copying ${sourceFiles.length} files from "${defaultLocale}".`,
|
|
6065
|
+
projectRoot: context.cwd,
|
|
6066
|
+
nextSteps: [
|
|
6067
|
+
{
|
|
6068
|
+
kind: "translate",
|
|
6069
|
+
required: true,
|
|
6070
|
+
message: `Translate every copied message from ${defaultLocale} to ${locale}; the copied text is not ready for delivery.`,
|
|
6071
|
+
paths: translationPaths
|
|
6072
|
+
},
|
|
6073
|
+
{
|
|
6074
|
+
kind: "run-checks",
|
|
6075
|
+
required: true,
|
|
6076
|
+
message: "Run the project checks.",
|
|
6077
|
+
paths: [],
|
|
6078
|
+
commands: ["bun run check", "npm run check", "pnpm run check"]
|
|
6079
|
+
}
|
|
6080
|
+
]
|
|
6081
|
+
});
|
|
6082
|
+
};
|
|
5604
6083
|
|
|
5605
6084
|
// src/lib/addLib.ts
|
|
5606
|
-
var import_prompts5 = __toESM(require_prompts3(), 1);
|
|
5607
6085
|
import { join as join5 } from "path";
|
|
5608
|
-
|
|
5609
|
-
import { existsSync as existsSync6 } from "fs";
|
|
5610
|
-
async function addLib(args, cwd = process.cwd()) {
|
|
6086
|
+
var addLib = async (args, context) => {
|
|
5611
6087
|
let libArg = args[1];
|
|
5612
6088
|
if (!libArg || libArg.startsWith("-")) {
|
|
5613
|
-
|
|
6089
|
+
if (context.outputMode === "json") {
|
|
6090
|
+
throw new CliError("Library name is required in JSON mode.", {
|
|
6091
|
+
code: "INTERACTIVE_INPUT_REQUIRED",
|
|
6092
|
+
hint: "Pass library or library.module after addlib."
|
|
6093
|
+
});
|
|
6094
|
+
}
|
|
6095
|
+
const response = await context.prompt({
|
|
5614
6096
|
type: "text",
|
|
5615
6097
|
name: "libArg",
|
|
5616
|
-
message: "
|
|
5617
|
-
validate: (name) => name ? true : "
|
|
6098
|
+
message: "Library name to add:",
|
|
6099
|
+
validate: (name) => name ? true : "Library name is required"
|
|
5618
6100
|
});
|
|
5619
|
-
libArg = response.libArg;
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
6101
|
+
libArg = String(response.libArg ?? "");
|
|
6102
|
+
if (!libArg) {
|
|
6103
|
+
context.operations.record({
|
|
6104
|
+
action: "cancelled",
|
|
6105
|
+
resource: "command",
|
|
6106
|
+
role: "library-creation",
|
|
6107
|
+
scope: "project",
|
|
6108
|
+
path: "."
|
|
6109
|
+
});
|
|
6110
|
+
return commandResult(context, {
|
|
6111
|
+
command: "addlib",
|
|
6112
|
+
summary: "Library creation was cancelled.",
|
|
6113
|
+
projectRoot: context.cwd,
|
|
6114
|
+
status: "cancelled"
|
|
6115
|
+
});
|
|
6116
|
+
}
|
|
5628
6117
|
}
|
|
5629
|
-
const
|
|
5630
|
-
if (
|
|
5631
|
-
|
|
5632
|
-
|
|
6118
|
+
const segments = parseLogicalName(libArg, "library name");
|
|
6119
|
+
if (segments.length > 2) {
|
|
6120
|
+
throw new CliError("Libraries support exactly library or library.module.", {
|
|
6121
|
+
code: "INVALID_ARGUMENT"
|
|
6122
|
+
});
|
|
5633
6123
|
}
|
|
5634
|
-
const
|
|
5635
|
-
await
|
|
5636
|
-
|
|
5637
|
-
|
|
6124
|
+
const [libName, fileName] = segments;
|
|
6125
|
+
if (!await loadConfig(context)) {
|
|
6126
|
+
throw new CliError("Configuration file cnp.config.json was not found.", {
|
|
6127
|
+
code: "CONFIG_NOT_FOUND",
|
|
6128
|
+
hint: "Run this command from the generated project root."
|
|
6129
|
+
});
|
|
5638
6130
|
}
|
|
5639
|
-
const
|
|
6131
|
+
const libDir = join5(context.cwd, "src", "lib", libName);
|
|
6132
|
+
await assertSafeTarget(context.cwd, libDir, context.fs);
|
|
6133
|
+
const gateway = new MutationGateway(context, context.cwd);
|
|
6134
|
+
const templateDir = join5(context.packageRoot, "templates", "Lib");
|
|
5640
6135
|
const indexTemplate = join5(templateDir, "index.ts");
|
|
5641
|
-
const
|
|
6136
|
+
const itemTemplate = join5(templateDir, "item.ts");
|
|
6137
|
+
if (!context.fs.exists(indexTemplate) || fileName && !context.fs.exists(itemTemplate)) {
|
|
6138
|
+
throw new CliError("Required library template files were not found.", {
|
|
6139
|
+
code: "TEMPLATE_MISSING",
|
|
6140
|
+
scope: "package",
|
|
6141
|
+
path: "templates/Lib"
|
|
6142
|
+
});
|
|
6143
|
+
}
|
|
6144
|
+
await gateway.mkdir(libDir, {
|
|
6145
|
+
role: "library-directory",
|
|
6146
|
+
resource: "directory"
|
|
6147
|
+
});
|
|
5642
6148
|
const indexPath = join5(libDir, "index.ts");
|
|
5643
|
-
if (!
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
} else {
|
|
5648
|
-
await writeFile5(indexPath, `export {}
|
|
5649
|
-
`);
|
|
5650
|
-
}
|
|
5651
|
-
console.log(`\uD83D\uDCC4 File created: ${indexPath}`);
|
|
6149
|
+
if (!context.fs.exists(indexPath)) {
|
|
6150
|
+
await gateway.write(indexPath, await context.fs.readText(indexTemplate), {
|
|
6151
|
+
role: "library-index"
|
|
6152
|
+
});
|
|
5652
6153
|
}
|
|
6154
|
+
let moduleAction;
|
|
5653
6155
|
if (fileName) {
|
|
5654
|
-
const
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
}
|
|
5664
|
-
`);
|
|
5665
|
-
}
|
|
5666
|
-
console.log(`\uD83D\uDCC4 File created: ${filePath}`);
|
|
5667
|
-
}
|
|
5668
|
-
let indexContent = await readFile6(indexPath, "utf-8");
|
|
5669
|
-
const importLine = `import { ${fileName} } from "./${fileName}";`;
|
|
5670
|
-
const importRegex = new RegExp(`import\\s*{\\s*${fileName}\\s*}\\s*from\\s*"\\./${fileName}";`);
|
|
6156
|
+
const moduleIdentifier = toIdentifier(fileName);
|
|
6157
|
+
const modulePath = join5(libDir, `${fileName}.ts`);
|
|
6158
|
+
const moduleContent = (await context.fs.readText(itemTemplate)).replace(/template/g, moduleIdentifier).replace(/Template/g, capitalize(moduleIdentifier));
|
|
6159
|
+
moduleAction = await gateway.write(modulePath, moduleContent, {
|
|
6160
|
+
role: "library-module",
|
|
6161
|
+
preserveExisting: true
|
|
6162
|
+
});
|
|
6163
|
+
const currentIndex = await context.fs.readText(indexPath);
|
|
6164
|
+
const importLine = `import { ${moduleIdentifier} } from "./${fileName}";`;
|
|
6165
|
+
const importRegex = new RegExp(`import\\s*{\\s*${moduleIdentifier}\\s*}\\s*from\\s*["']\\./${fileName}["'];`);
|
|
5671
6166
|
const exportRegex = /export\s*{([^}]*)}/m;
|
|
5672
|
-
const imports =
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
imports.push(line);
|
|
5678
|
-
} else if (line.startsWith("export")) {
|
|
5679
|
-
const match = line.match(exportRegex);
|
|
5680
|
-
if (match && match[1]) {
|
|
5681
|
-
exportsSet.push(...match[1].split(",").map((s) => s.trim()).filter(Boolean));
|
|
5682
|
-
}
|
|
5683
|
-
}
|
|
5684
|
-
}
|
|
5685
|
-
if (!imports.some((l) => importRegex.test(l))) {
|
|
6167
|
+
const imports = currentIndex.split(`
|
|
6168
|
+
`).filter((line) => line.startsWith("import"));
|
|
6169
|
+
const exportMatch = currentIndex.match(exportRegex);
|
|
6170
|
+
const exportsSet = (exportMatch?.[1] ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
6171
|
+
if (!imports.some((line) => importRegex.test(line)))
|
|
5686
6172
|
imports.push(importLine);
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
indexContent = imports.join(`
|
|
5692
|
-
`) + `
|
|
6173
|
+
if (!exportsSet.includes(moduleIdentifier))
|
|
6174
|
+
exportsSet.push(moduleIdentifier);
|
|
6175
|
+
const nextIndex = `${imports.join(`
|
|
6176
|
+
`)}
|
|
5693
6177
|
|
|
5694
|
-
export {
|
|
6178
|
+
export { ${exportsSet.join(", ")} };
|
|
5695
6179
|
`;
|
|
5696
|
-
await
|
|
5697
|
-
|
|
6180
|
+
await gateway.write(indexPath, nextIndex, { role: "library-index" });
|
|
6181
|
+
} else if (context.fs.exists(indexPath)) {
|
|
6182
|
+
gateway.unchanged(indexPath, { role: "library-index" });
|
|
6183
|
+
}
|
|
6184
|
+
const mutated = context.operations.snapshot().some((event) => ["created", "updated", "copied", "deleted"].includes(event.action));
|
|
6185
|
+
const nextSteps = [];
|
|
6186
|
+
if (fileName && moduleAction !== "unchanged") {
|
|
6187
|
+
nextSteps.push({
|
|
6188
|
+
kind: "review",
|
|
6189
|
+
required: true,
|
|
6190
|
+
message: "Implement and review the generated library module.",
|
|
6191
|
+
paths: [
|
|
6192
|
+
{
|
|
6193
|
+
scope: "project",
|
|
6194
|
+
path: gateway.path(join5(libDir, `${fileName}.ts`))
|
|
6195
|
+
}
|
|
6196
|
+
]
|
|
6197
|
+
});
|
|
5698
6198
|
}
|
|
5699
|
-
|
|
5700
|
-
|
|
6199
|
+
if (mutated) {
|
|
6200
|
+
nextSteps.push({
|
|
6201
|
+
kind: "run-checks",
|
|
6202
|
+
required: true,
|
|
6203
|
+
message: "Run the project checks.",
|
|
6204
|
+
paths: [],
|
|
6205
|
+
commands: ["bun run check", "npm run check", "pnpm run check"]
|
|
6206
|
+
});
|
|
6207
|
+
}
|
|
6208
|
+
return commandResult(context, {
|
|
6209
|
+
command: "addlib",
|
|
6210
|
+
summary: mutated ? `Added library "${libName}"${fileName ? ` with module "${fileName}"` : ""}.` : `Library "${libName}"${fileName ? ` and module "${fileName}"` : ""} already exist and were preserved.`,
|
|
6211
|
+
projectRoot: context.cwd,
|
|
6212
|
+
nextSteps
|
|
6213
|
+
});
|
|
6214
|
+
};
|
|
5701
6215
|
|
|
5702
6216
|
// src/lib/addPage.ts
|
|
5703
|
-
var import_prompts6 = __toESM(require_prompts3(), 1);
|
|
5704
6217
|
import { join as join6 } from "path";
|
|
5705
|
-
|
|
5706
|
-
|
|
6218
|
+
var LONG_FLAGS = [
|
|
6219
|
+
"layout",
|
|
6220
|
+
"page",
|
|
6221
|
+
"loading",
|
|
6222
|
+
"not-found",
|
|
6223
|
+
"error",
|
|
6224
|
+
"global-error",
|
|
6225
|
+
"route",
|
|
6226
|
+
"template",
|
|
6227
|
+
"default"
|
|
6228
|
+
];
|
|
6229
|
+
var SHORT_FLAGS = {
|
|
6230
|
+
L: "layout",
|
|
6231
|
+
P: "page",
|
|
6232
|
+
l: "loading",
|
|
6233
|
+
n: "not-found",
|
|
6234
|
+
e: "error",
|
|
6235
|
+
g: "global-error",
|
|
6236
|
+
r: "route",
|
|
6237
|
+
t: "template",
|
|
6238
|
+
d: "default"
|
|
6239
|
+
};
|
|
5707
6240
|
function registerMessagesFile(content, locale, fileName) {
|
|
5708
6241
|
const importPath = `./${locale}/${fileName}.json`;
|
|
5709
6242
|
if (content.includes(importPath))
|
|
5710
6243
|
return content;
|
|
5711
6244
|
const declarationIndex = content.indexOf("const messages =");
|
|
5712
6245
|
const registryMatch = content.match(/const messages = \{([\s\S]*?)\n\};/);
|
|
5713
|
-
if (declarationIndex
|
|
5714
|
-
throw new
|
|
6246
|
+
if (declarationIndex < 0 || !registryMatch) {
|
|
6247
|
+
throw new CliError(`Unable to register ${fileName}.json in messages/${locale}.ts.`, {
|
|
6248
|
+
code: "CONFIG_NOT_FOUND",
|
|
6249
|
+
scope: "project",
|
|
6250
|
+
path: `messages/${locale}.ts`
|
|
6251
|
+
});
|
|
5715
6252
|
}
|
|
5716
|
-
const
|
|
6253
|
+
const identifier = toIdentifier(fileName);
|
|
6254
|
+
const importStatement = `import ${identifier} from "${importPath}";
|
|
5717
6255
|
`;
|
|
5718
6256
|
const nextRegistry = registryMatch[0].replace(/\n\};$/, `
|
|
5719
|
-
${
|
|
6257
|
+
${identifier},
|
|
5720
6258
|
};`);
|
|
5721
6259
|
return content.slice(0, declarationIndex) + importStatement + content.slice(declarationIndex).replace(registryMatch[0], nextRegistry);
|
|
5722
6260
|
}
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
6261
|
+
function selectedFlags(args) {
|
|
6262
|
+
const selected = new Set;
|
|
6263
|
+
const optionArguments = args.slice(2).filter((argument) => argument.startsWith("-"));
|
|
6264
|
+
if (optionArguments.length === 0)
|
|
6265
|
+
return new Set(["layout", "page", "loading"]);
|
|
6266
|
+
for (const argument of optionArguments) {
|
|
6267
|
+
if (argument.startsWith("--")) {
|
|
6268
|
+
const flag = argument.slice(2);
|
|
6269
|
+
if (!LONG_FLAGS.includes(flag)) {
|
|
6270
|
+
throw new CliError(`Unknown addpage option: ${argument}.`, {
|
|
6271
|
+
code: "INVALID_ARGUMENT"
|
|
6272
|
+
});
|
|
6273
|
+
}
|
|
6274
|
+
selected.add(flag);
|
|
6275
|
+
continue;
|
|
6276
|
+
}
|
|
6277
|
+
for (const character of argument.slice(1)) {
|
|
6278
|
+
const flag = SHORT_FLAGS[character];
|
|
6279
|
+
if (!flag) {
|
|
6280
|
+
throw new CliError(`Unknown addpage short option: -${character}.`, {
|
|
6281
|
+
code: "INVALID_ARGUMENT"
|
|
6282
|
+
});
|
|
6283
|
+
}
|
|
6284
|
+
selected.add(flag);
|
|
6285
|
+
}
|
|
6286
|
+
}
|
|
6287
|
+
return selected;
|
|
6288
|
+
}
|
|
6289
|
+
var addPage = async (args, context) => {
|
|
6290
|
+
let logicalName = args[1];
|
|
6291
|
+
if (!logicalName || logicalName.startsWith("-")) {
|
|
6292
|
+
if (context.outputMode === "json") {
|
|
6293
|
+
throw new CliError("Page name is required in JSON mode.", {
|
|
6294
|
+
code: "INTERACTIVE_INPUT_REQUIRED",
|
|
6295
|
+
hint: "Pass a simple or Parent.Child page name after addpage."
|
|
6296
|
+
});
|
|
6297
|
+
}
|
|
6298
|
+
const response = await context.prompt({
|
|
5727
6299
|
type: "text",
|
|
5728
6300
|
name: "pageName",
|
|
5729
|
-
message: "
|
|
6301
|
+
message: "Page name to add:",
|
|
5730
6302
|
validate: (name) => name ? true : "Page name is required"
|
|
5731
6303
|
});
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
case "l":
|
|
5758
|
-
flags.add("loading");
|
|
5759
|
-
break;
|
|
5760
|
-
case "n":
|
|
5761
|
-
flags.add("not-found");
|
|
5762
|
-
break;
|
|
5763
|
-
case "e":
|
|
5764
|
-
flags.add("error");
|
|
5765
|
-
break;
|
|
5766
|
-
case "g":
|
|
5767
|
-
flags.add("global-error");
|
|
5768
|
-
break;
|
|
5769
|
-
case "r":
|
|
5770
|
-
flags.add("route");
|
|
5771
|
-
break;
|
|
5772
|
-
case "t":
|
|
5773
|
-
flags.add("template");
|
|
5774
|
-
break;
|
|
5775
|
-
case "d":
|
|
5776
|
-
flags.add("default");
|
|
5777
|
-
break;
|
|
5778
|
-
}
|
|
5779
|
-
}
|
|
5780
|
-
}
|
|
5781
|
-
for (const flag of [
|
|
5782
|
-
"layout",
|
|
5783
|
-
"page",
|
|
5784
|
-
"loading",
|
|
5785
|
-
"not-found",
|
|
5786
|
-
"error",
|
|
5787
|
-
"global-error",
|
|
5788
|
-
"route",
|
|
5789
|
-
"template",
|
|
5790
|
-
"default"
|
|
5791
|
-
]) {
|
|
5792
|
-
if (longFlags.has("--" + flag))
|
|
5793
|
-
flags.add(flag);
|
|
5794
|
-
}
|
|
5795
|
-
const config = await loadConfig(cwd);
|
|
6304
|
+
logicalName = String(response.pageName ?? "");
|
|
6305
|
+
if (!logicalName) {
|
|
6306
|
+
context.operations.record({
|
|
6307
|
+
action: "cancelled",
|
|
6308
|
+
resource: "command",
|
|
6309
|
+
role: "page-creation",
|
|
6310
|
+
scope: "project",
|
|
6311
|
+
path: "."
|
|
6312
|
+
});
|
|
6313
|
+
return commandResult(context, {
|
|
6314
|
+
command: "addpage",
|
|
6315
|
+
summary: "Page creation was cancelled.",
|
|
6316
|
+
projectRoot: context.cwd,
|
|
6317
|
+
status: "cancelled"
|
|
6318
|
+
});
|
|
6319
|
+
}
|
|
6320
|
+
}
|
|
6321
|
+
const pageSegments = parseLogicalName(logicalName, "page name");
|
|
6322
|
+
if (pageSegments.length > 2) {
|
|
6323
|
+
throw new CliError("Nested pages support exactly Parent.Child.", {
|
|
6324
|
+
code: "INVALID_ARGUMENT"
|
|
6325
|
+
});
|
|
6326
|
+
}
|
|
6327
|
+
const flags = selectedFlags(args);
|
|
6328
|
+
const config = await loadConfig(context);
|
|
5796
6329
|
if (!config) {
|
|
5797
|
-
|
|
5798
|
-
|
|
6330
|
+
throw new CliError("Configuration file cnp.config.json was not found.", {
|
|
6331
|
+
code: "CONFIG_NOT_FOUND",
|
|
6332
|
+
hint: "Run this command from the generated project root."
|
|
6333
|
+
});
|
|
5799
6334
|
}
|
|
5800
|
-
const useI18n =
|
|
6335
|
+
const useI18n = Boolean(config.useI18n);
|
|
5801
6336
|
const aliasPrefix = configuredAliasPrefix(config);
|
|
5802
|
-
const
|
|
6337
|
+
const appRoot = join6(context.cwd, "src", "app", ...useI18n ? ["[locale]"] : []);
|
|
6338
|
+
if (!context.fs.exists(appRoot)) {
|
|
6339
|
+
throw new CliError("The expected App Router directory was not found.", {
|
|
6340
|
+
code: "CONFIG_NOT_FOUND",
|
|
6341
|
+
scope: "project",
|
|
6342
|
+
path: useI18n ? "src/app/[locale]" : "src/app"
|
|
6343
|
+
});
|
|
6344
|
+
}
|
|
6345
|
+
const [parentName, childName] = pageSegments.length === 2 ? pageSegments : [undefined, undefined];
|
|
6346
|
+
const leafName = childName ?? pageSegments[0];
|
|
6347
|
+
const pageIdentifier = capitalize(toIdentifier(leafName));
|
|
6348
|
+
const jsonFileName = parentName ?? leafName;
|
|
6349
|
+
const uiDirectory = join6(context.cwd, "src", "ui", ...pageSegments);
|
|
6350
|
+
const routeDirectory = join6(appRoot, ...pageSegments);
|
|
6351
|
+
await assertSafeTarget(context.cwd, uiDirectory, context.fs);
|
|
6352
|
+
await assertSafeTarget(context.cwd, routeDirectory, context.fs);
|
|
6353
|
+
const templateRoot = join6(context.packageRoot, "templates", "Page");
|
|
6354
|
+
const uiTemplate = join6(templateRoot, "page-ui.tsx");
|
|
6355
|
+
const routeTemplates = [...flags].map((flag) => ({
|
|
6356
|
+
flag,
|
|
6357
|
+
source: join6(templateRoot, toFileName(flag))
|
|
6358
|
+
}));
|
|
6359
|
+
const messagesTemplate = join6(templateRoot, "page.json");
|
|
6360
|
+
const requiredTemplates = [
|
|
6361
|
+
uiTemplate,
|
|
6362
|
+
...routeTemplates.map((entry) => entry.source)
|
|
6363
|
+
];
|
|
5803
6364
|
if (useI18n)
|
|
5804
|
-
|
|
5805
|
-
const
|
|
5806
|
-
if (
|
|
5807
|
-
|
|
5808
|
-
|
|
6365
|
+
requiredTemplates.push(messagesTemplate);
|
|
6366
|
+
const missing = requiredTemplates.find((target) => !context.fs.exists(target));
|
|
6367
|
+
if (missing) {
|
|
6368
|
+
throw new CliError(`Required page template was not found: ${missing}.`, {
|
|
6369
|
+
code: "TEMPLATE_MISSING",
|
|
6370
|
+
scope: "package",
|
|
6371
|
+
path: missing.replace(`${context.packageRoot}/`, "")
|
|
6372
|
+
});
|
|
5809
6373
|
}
|
|
5810
|
-
|
|
5811
|
-
|
|
6374
|
+
const translationNamespace = parentName ? `${parentName}.${childName}` : leafName;
|
|
6375
|
+
const templateJson = useI18n ? JSON.parse((await context.fs.readText(messagesTemplate)).replace(/template/g, leafName).replace(/Template/g, capitalize(leafName))) : undefined;
|
|
6376
|
+
const preparedLocales = [];
|
|
5812
6377
|
if (useI18n) {
|
|
5813
|
-
|
|
5814
|
-
if (!
|
|
5815
|
-
|
|
5816
|
-
|
|
6378
|
+
const messagesRoot = join6(context.cwd, "messages");
|
|
6379
|
+
if (!context.fs.exists(messagesRoot)) {
|
|
6380
|
+
throw new CliError("The messages directory was not found.", {
|
|
6381
|
+
code: "CONFIG_NOT_FOUND",
|
|
6382
|
+
scope: "project",
|
|
6383
|
+
path: "messages"
|
|
6384
|
+
});
|
|
5817
6385
|
}
|
|
5818
|
-
const
|
|
5819
|
-
locales
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
localePagePath = join6(srcPath, parentName, childName);
|
|
5826
|
-
jsonFileName = parentName;
|
|
5827
|
-
} else {
|
|
5828
|
-
uiPageDir = join6(cwd, "src", "ui", pageName);
|
|
5829
|
-
localePagePath = join6(srcPath, pageName);
|
|
5830
|
-
jsonFileName = pageName;
|
|
5831
|
-
}
|
|
5832
|
-
await assertSafeTarget(cwd, uiPageDir);
|
|
5833
|
-
await assertSafeTarget(cwd, localePagePath);
|
|
5834
|
-
if (!existsSync7(uiPageDir)) {
|
|
5835
|
-
await mkdir5(uiPageDir, { recursive: true });
|
|
5836
|
-
}
|
|
5837
|
-
const uiPageFile = join6(uiPageDir, "page-ui.tsx");
|
|
5838
|
-
const uiPageTemplate = join6(templatePath, "page-ui.tsx");
|
|
5839
|
-
if (existsSync7(uiPageTemplate)) {
|
|
5840
|
-
let uiContent = await readFile7(uiPageTemplate, "utf-8");
|
|
5841
|
-
const translationNamespace = parentName && childName ? `${parentName}.${childName}` : pageName;
|
|
5842
|
-
uiContent = uiContent.replace('useTranslations("template")', `useTranslations("${translationNamespace}")`);
|
|
5843
|
-
uiContent = uiContent.replaceAll('from "@/', `from "${aliasPrefix}/`).replace(/template/g, childName || pageName).replace(/Template/g, capitalize(childName || pageName));
|
|
5844
|
-
await writeFile6(uiPageFile, uiContent);
|
|
5845
|
-
console.log(`\uD83D\uDCC4 File created: ${uiPageFile}`);
|
|
5846
|
-
} else {
|
|
5847
|
-
console.warn("\u26A0\uFE0F Missing template file: page-ui.tsx at path:", uiPageTemplate);
|
|
5848
|
-
}
|
|
5849
|
-
if (!existsSync7(localePagePath)) {
|
|
5850
|
-
await mkdir5(localePagePath, { recursive: true });
|
|
5851
|
-
}
|
|
5852
|
-
for (const flag of flags) {
|
|
5853
|
-
const filename = toFileName(flag);
|
|
5854
|
-
const src = join6(templatePath, filename);
|
|
5855
|
-
const dst = join6(localePagePath, filename);
|
|
5856
|
-
if (!existsSync7(src)) {
|
|
5857
|
-
console.warn(`\u26A0\uFE0F Missing template file: ${filename} at path: ${src}`);
|
|
5858
|
-
continue;
|
|
6386
|
+
const locales = (await context.fs.list(messagesRoot)).filter((entry) => entry.isDirectory).map((entry) => entry.name).sort();
|
|
6387
|
+
if (locales.length === 0) {
|
|
6388
|
+
throw new CliError("No locale directories were found.", {
|
|
6389
|
+
code: "CONFIG_NOT_FOUND",
|
|
6390
|
+
scope: "project",
|
|
6391
|
+
path: "messages"
|
|
6392
|
+
});
|
|
5859
6393
|
}
|
|
5860
|
-
const
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
const
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
try {
|
|
5882
|
-
current = JSON.parse(jsonFile);
|
|
5883
|
-
} catch {
|
|
5884
|
-
current = {};
|
|
5885
|
-
}
|
|
5886
|
-
}
|
|
5887
|
-
if (parentName && childName) {
|
|
5888
|
-
current[childName] = JSON.parse(replaced);
|
|
5889
|
-
} else {
|
|
5890
|
-
current = JSON.parse(replaced);
|
|
5891
|
-
}
|
|
5892
|
-
await writeFile6(jsonTarget, `${JSON.stringify(current, null, 2)}
|
|
5893
|
-
`);
|
|
5894
|
-
console.log(`\uD83D\uDCC4 File created: ${jsonTarget}`);
|
|
5895
|
-
const localeAggregator = join6(messagesPath, `${locale}.ts`);
|
|
5896
|
-
if (!existsSync7(localeAggregator)) {
|
|
5897
|
-
throw new Error(`Locale aggregator not found: messages/${locale}.ts`);
|
|
5898
|
-
}
|
|
5899
|
-
const aggregatorContent = await readFile7(localeAggregator, "utf-8");
|
|
5900
|
-
const nextAggregatorContent = registerMessagesFile(aggregatorContent, locale, jsonFileName);
|
|
5901
|
-
if (nextAggregatorContent !== aggregatorContent) {
|
|
5902
|
-
await writeFile6(localeAggregator, nextAggregatorContent);
|
|
5903
|
-
console.log(`\uD83D\uDCC4 File updated: ${localeAggregator}`);
|
|
6394
|
+
for (const locale of locales) {
|
|
6395
|
+
const target = join6(messagesRoot, locale, `${jsonFileName}.json`);
|
|
6396
|
+
const aggregator = join6(messagesRoot, `${locale}.ts`);
|
|
6397
|
+
if (!context.fs.exists(aggregator)) {
|
|
6398
|
+
throw new CliError(`Locale aggregator messages/${locale}.ts was not found.`, {
|
|
6399
|
+
code: "CONFIG_NOT_FOUND",
|
|
6400
|
+
scope: "project",
|
|
6401
|
+
path: `messages/${locale}.ts`
|
|
6402
|
+
});
|
|
6403
|
+
}
|
|
6404
|
+
let data = {};
|
|
6405
|
+
const targetExists = context.fs.exists(target);
|
|
6406
|
+
if (targetExists) {
|
|
6407
|
+
try {
|
|
6408
|
+
data = JSON.parse(await context.fs.readText(target));
|
|
6409
|
+
} catch {
|
|
6410
|
+
throw new CliError(`Invalid JSON in messages/${locale}/${jsonFileName}.json.`, {
|
|
6411
|
+
code: "FILESYSTEM_ERROR",
|
|
6412
|
+
scope: "project",
|
|
6413
|
+
path: `messages/${locale}/${jsonFileName}.json`
|
|
6414
|
+
});
|
|
5904
6415
|
}
|
|
5905
6416
|
}
|
|
6417
|
+
const messageExists = parentName ? Object.hasOwn(data, childName) : targetExists;
|
|
6418
|
+
if (!messageExists) {
|
|
6419
|
+
if (parentName)
|
|
6420
|
+
data[childName] = templateJson;
|
|
6421
|
+
else
|
|
6422
|
+
data = templateJson;
|
|
6423
|
+
}
|
|
6424
|
+
const aggregatorCurrent = await context.fs.readText(aggregator);
|
|
6425
|
+
preparedLocales.push({
|
|
6426
|
+
locale,
|
|
6427
|
+
target,
|
|
6428
|
+
targetContent: messageExists ? undefined : `${JSON.stringify(data, null, 2)}
|
|
6429
|
+
`,
|
|
6430
|
+
messageExists,
|
|
6431
|
+
aggregator,
|
|
6432
|
+
aggregatorContent: registerMessagesFile(aggregatorCurrent, locale, jsonFileName)
|
|
6433
|
+
});
|
|
6434
|
+
}
|
|
6435
|
+
}
|
|
6436
|
+
const gateway = new MutationGateway(context, context.cwd);
|
|
6437
|
+
await gateway.mkdir(uiDirectory, {
|
|
6438
|
+
role: "page-ui-directory",
|
|
6439
|
+
resource: "directory"
|
|
6440
|
+
});
|
|
6441
|
+
const uiFile = join6(uiDirectory, "page-ui.tsx");
|
|
6442
|
+
const uiContent = (await context.fs.readText(uiTemplate)).replace('useTranslations("template")', `useTranslations("${translationNamespace}")`).replaceAll('from "@/', `from "${aliasPrefix}/`).replace(/template/g, pageIdentifier).replace(/Template/g, pageIdentifier);
|
|
6443
|
+
await gateway.write(uiFile, uiContent, {
|
|
6444
|
+
role: "page-ui",
|
|
6445
|
+
preserveExisting: true
|
|
6446
|
+
});
|
|
6447
|
+
await gateway.mkdir(routeDirectory, {
|
|
6448
|
+
role: "page-route-directory",
|
|
6449
|
+
resource: "directory"
|
|
6450
|
+
});
|
|
6451
|
+
for (const template of routeTemplates) {
|
|
6452
|
+
const filename = toFileName(template.flag);
|
|
6453
|
+
const target = join6(routeDirectory, filename);
|
|
6454
|
+
const uiImportPath = pageSegments.join("/");
|
|
6455
|
+
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);
|
|
6456
|
+
await gateway.write(target, content, {
|
|
6457
|
+
role: `page-${template.flag}`,
|
|
6458
|
+
preserveExisting: true
|
|
6459
|
+
});
|
|
6460
|
+
}
|
|
6461
|
+
if (useI18n) {
|
|
6462
|
+
for (const item of preparedLocales) {
|
|
6463
|
+
if (item.messageExists) {
|
|
6464
|
+
gateway.unchanged(item.target, {
|
|
6465
|
+
role: "translation-messages",
|
|
6466
|
+
detail: { locale: item.locale, namespace: translationNamespace }
|
|
6467
|
+
});
|
|
6468
|
+
} else {
|
|
6469
|
+
await gateway.write(item.target, item.targetContent, {
|
|
6470
|
+
role: "translation-messages",
|
|
6471
|
+
detail: { locale: item.locale, namespace: translationNamespace }
|
|
6472
|
+
});
|
|
6473
|
+
}
|
|
6474
|
+
await gateway.write(item.aggregator, item.aggregatorContent, {
|
|
6475
|
+
role: "locale-aggregator"
|
|
6476
|
+
});
|
|
5906
6477
|
}
|
|
5907
6478
|
} else {
|
|
5908
|
-
|
|
6479
|
+
gateway.skipped(join6(context.cwd, "messages"), {
|
|
6480
|
+
role: "translation-messages",
|
|
6481
|
+
resource: "directory",
|
|
6482
|
+
detail: { reason: "Internationalization is disabled." }
|
|
6483
|
+
});
|
|
5909
6484
|
}
|
|
5910
|
-
|
|
5911
|
-
|
|
6485
|
+
const mutated = context.operations.snapshot().some((event) => ["created", "updated", "copied", "deleted"].includes(event.action));
|
|
6486
|
+
const reviewPaths = [
|
|
6487
|
+
{ scope: "project", path: gateway.path(uiFile) },
|
|
6488
|
+
...routeTemplates.map((template) => ({
|
|
6489
|
+
scope: "project",
|
|
6490
|
+
path: gateway.path(join6(routeDirectory, toFileName(template.flag)))
|
|
6491
|
+
})),
|
|
6492
|
+
...preparedLocales.map((item) => ({
|
|
6493
|
+
scope: "project",
|
|
6494
|
+
path: gateway.path(item.target)
|
|
6495
|
+
}))
|
|
6496
|
+
];
|
|
6497
|
+
return commandResult(context, {
|
|
6498
|
+
command: "addpage",
|
|
6499
|
+
summary: mutated ? `Added page "${logicalName}" and its missing resources.` : `Page "${logicalName}" already exists and was preserved.`,
|
|
6500
|
+
projectRoot: context.cwd,
|
|
6501
|
+
nextSteps: mutated ? [
|
|
6502
|
+
{
|
|
6503
|
+
kind: "review",
|
|
6504
|
+
required: true,
|
|
6505
|
+
message: "Review the generated page UI, route files and localized messages.",
|
|
6506
|
+
paths: reviewPaths
|
|
6507
|
+
},
|
|
6508
|
+
{
|
|
6509
|
+
kind: "run-checks",
|
|
6510
|
+
required: true,
|
|
6511
|
+
message: "Run the project checks.",
|
|
6512
|
+
paths: [],
|
|
6513
|
+
commands: ["bun run check", "npm run check", "pnpm run check"]
|
|
6514
|
+
}
|
|
6515
|
+
] : []
|
|
6516
|
+
});
|
|
6517
|
+
};
|
|
5912
6518
|
|
|
5913
6519
|
// src/lib/addText.ts
|
|
5914
6520
|
import { join as join7 } from "path";
|
|
5915
|
-
import { existsSync as existsSync8 } from "fs";
|
|
5916
|
-
import { readFile as readFile8, writeFile as writeFile7, readdir as readdir5 } from "fs/promises";
|
|
5917
6521
|
function defaultText(key) {
|
|
5918
|
-
return key.replace(/_/g, " ").replace(/\b\w/g, (
|
|
6522
|
+
return key.replace(/_/g, " ").replace(/\b\w/g, (character) => character.toUpperCase());
|
|
5919
6523
|
}
|
|
5920
|
-
async
|
|
6524
|
+
var addText = async (args, context) => {
|
|
5921
6525
|
const pathArg = args[1];
|
|
5922
6526
|
if (!pathArg) {
|
|
5923
|
-
|
|
5924
|
-
|
|
6527
|
+
throw new CliError("Translation dot path is required.", {
|
|
6528
|
+
code: "INVALID_ARGUMENT",
|
|
6529
|
+
hint: "Use addtext domain.key followed by an optional value."
|
|
6530
|
+
});
|
|
6531
|
+
}
|
|
6532
|
+
const segments = parseLogicalName(pathArg, "translation path");
|
|
6533
|
+
if (segments.length < 2) {
|
|
6534
|
+
throw new CliError("Translation path must contain a file and a key.", {
|
|
6535
|
+
code: "INVALID_ARGUMENT"
|
|
6536
|
+
});
|
|
5925
6537
|
}
|
|
5926
6538
|
const providedText = args.slice(2).join(" ");
|
|
5927
|
-
|
|
5928
|
-
const
|
|
6539
|
+
const finalKey = segments.at(-1);
|
|
6540
|
+
const text = providedText || defaultText(finalKey);
|
|
6541
|
+
const config = await loadConfig(context);
|
|
5929
6542
|
if (!config?.useI18n) {
|
|
5930
|
-
|
|
5931
|
-
|
|
6543
|
+
throw new CliError("Internationalization is not enabled in this project.", {
|
|
6544
|
+
code: "I18N_DISABLED"
|
|
6545
|
+
});
|
|
5932
6546
|
}
|
|
5933
|
-
const
|
|
5934
|
-
if (!
|
|
5935
|
-
|
|
5936
|
-
|
|
6547
|
+
const messagesRoot = join7(context.cwd, "messages");
|
|
6548
|
+
if (!context.fs.exists(messagesRoot)) {
|
|
6549
|
+
throw new CliError("The messages directory was not found.", {
|
|
6550
|
+
code: "CONFIG_NOT_FOUND",
|
|
6551
|
+
scope: "project",
|
|
6552
|
+
path: "messages"
|
|
6553
|
+
});
|
|
5937
6554
|
}
|
|
5938
|
-
const
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
6555
|
+
const locales = (await context.fs.list(messagesRoot)).filter((entry) => entry.isDirectory).map((entry) => entry.name).sort();
|
|
6556
|
+
if (locales.length === 0) {
|
|
6557
|
+
throw new CliError("No locale directories were found.", {
|
|
6558
|
+
code: "CONFIG_NOT_FOUND",
|
|
6559
|
+
scope: "project",
|
|
6560
|
+
path: "messages"
|
|
6561
|
+
});
|
|
5944
6562
|
}
|
|
5945
|
-
const
|
|
5946
|
-
const
|
|
6563
|
+
const [fileName, ...keySegments] = segments;
|
|
6564
|
+
const prepared = [];
|
|
5947
6565
|
for (const locale of locales) {
|
|
5948
|
-
const
|
|
5949
|
-
await assertSafeTarget(cwd,
|
|
6566
|
+
const file = join7(messagesRoot, locale, `${fileName}.json`);
|
|
6567
|
+
await assertSafeTarget(context.cwd, file, context.fs);
|
|
6568
|
+
const existed = context.fs.exists(file);
|
|
5950
6569
|
let data = {};
|
|
5951
|
-
if (
|
|
5952
|
-
const raw = await readFile8(filePath, "utf-8");
|
|
6570
|
+
if (existed) {
|
|
5953
6571
|
try {
|
|
5954
|
-
|
|
5955
|
-
|
|
6572
|
+
const parsed = JSON.parse(await context.fs.readText(file));
|
|
6573
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
6574
|
+
throw new Error("root value is not an object");
|
|
6575
|
+
}
|
|
6576
|
+
data = parsed;
|
|
6577
|
+
} catch (error) {
|
|
6578
|
+
throw new CliError(`Invalid JSON in messages/${locale}/${fileName}.json.`, {
|
|
6579
|
+
code: "FILESYSTEM_ERROR",
|
|
6580
|
+
scope: "project",
|
|
6581
|
+
path: `messages/${locale}/${fileName}.json`,
|
|
6582
|
+
hint: error instanceof Error ? error.message : undefined
|
|
6583
|
+
});
|
|
6584
|
+
}
|
|
5956
6585
|
}
|
|
5957
6586
|
let cursor = data;
|
|
5958
|
-
for (
|
|
5959
|
-
const
|
|
5960
|
-
if (
|
|
5961
|
-
cursor[
|
|
5962
|
-
|
|
6587
|
+
for (const segment of keySegments.slice(0, -1)) {
|
|
6588
|
+
const current = cursor[segment];
|
|
6589
|
+
if (current === undefined)
|
|
6590
|
+
cursor[segment] = {};
|
|
6591
|
+
else if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
6592
|
+
throw new CliError(`Translation segment "${segment}" is not an object in ${locale}.`, {
|
|
6593
|
+
code: "INVALID_ARGUMENT",
|
|
6594
|
+
scope: "project",
|
|
6595
|
+
path: `messages/${locale}/${fileName}.json`
|
|
6596
|
+
});
|
|
6597
|
+
}
|
|
6598
|
+
cursor = cursor[segment];
|
|
5963
6599
|
}
|
|
6600
|
+
const previous = cursor[finalKey];
|
|
5964
6601
|
cursor[finalKey] = text;
|
|
5965
|
-
|
|
5966
|
-
|
|
6602
|
+
prepared.push({
|
|
6603
|
+
locale,
|
|
6604
|
+
file,
|
|
6605
|
+
data,
|
|
6606
|
+
replaced: previous !== undefined && previous !== text
|
|
6607
|
+
});
|
|
5967
6608
|
}
|
|
5968
|
-
|
|
5969
|
-
|
|
6609
|
+
const gateway = new MutationGateway(context, context.cwd);
|
|
6610
|
+
for (const item of prepared) {
|
|
6611
|
+
await gateway.write(item.file, `${JSON.stringify(item.data, null, 2)}
|
|
6612
|
+
`, {
|
|
6613
|
+
role: "translation-messages",
|
|
6614
|
+
detail: {
|
|
6615
|
+
locale: item.locale,
|
|
6616
|
+
key: pathArg,
|
|
6617
|
+
replaced: item.replaced
|
|
6618
|
+
}
|
|
6619
|
+
});
|
|
6620
|
+
}
|
|
6621
|
+
const mutated = context.operations.snapshot().some((event) => event.action === "created" || event.action === "updated");
|
|
6622
|
+
const nextSteps = mutated ? [
|
|
6623
|
+
...locales.length > 1 ? [
|
|
6624
|
+
{
|
|
6625
|
+
kind: "translate",
|
|
6626
|
+
required: true,
|
|
6627
|
+
message: "Review the value in every locale; the same text was written to all locale files.",
|
|
6628
|
+
paths: prepared.map((item) => ({
|
|
6629
|
+
scope: "project",
|
|
6630
|
+
path: gateway.path(item.file)
|
|
6631
|
+
}))
|
|
6632
|
+
}
|
|
6633
|
+
] : [],
|
|
6634
|
+
{
|
|
6635
|
+
kind: "run-checks",
|
|
6636
|
+
required: true,
|
|
6637
|
+
message: "Run the project checks.",
|
|
6638
|
+
paths: [],
|
|
6639
|
+
commands: ["bun run check", "npm run check", "pnpm run check"]
|
|
6640
|
+
}
|
|
6641
|
+
] : [];
|
|
6642
|
+
return commandResult(context, {
|
|
6643
|
+
command: "addtext",
|
|
6644
|
+
summary: mutated ? `Set translation "${pathArg}" to "${text}" in ${locales.length} locale files.` : `Translation "${pathArg}" already has value "${text}" in every locale.`,
|
|
6645
|
+
projectRoot: context.cwd,
|
|
6646
|
+
nextSteps
|
|
6647
|
+
});
|
|
6648
|
+
};
|
|
5970
6649
|
|
|
5971
6650
|
// src/lib/rmPage.ts
|
|
5972
|
-
import
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
await writeFile8(target, `${JSON.stringify(data, null, 2)}
|
|
5991
|
-
`);
|
|
5992
|
-
} else {
|
|
5993
|
-
await rm(target);
|
|
5994
|
-
}
|
|
5995
|
-
terminal.log(`\uD83D\uDDD1\uFE0F Deleted messages for ${candidate.logicalName}: ${target}`);
|
|
6651
|
+
import path7 from "path";
|
|
6652
|
+
function escapeRegExp(value) {
|
|
6653
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6654
|
+
}
|
|
6655
|
+
function unregisterMessagesFile(content, locale, fileName) {
|
|
6656
|
+
const identifier = toIdentifier(fileName);
|
|
6657
|
+
const escapedIdentifier = escapeRegExp(identifier);
|
|
6658
|
+
const escapedPath = escapeRegExp(`./${locale}/${fileName}.json`);
|
|
6659
|
+
const importPattern = new RegExp(`^import\\s+${escapedIdentifier}\\s+from\\s+["']${escapedPath}["'];?\\r?\\n?`, "m");
|
|
6660
|
+
const propertyPattern = new RegExp(`^\\s*${escapedIdentifier},\\s*\\r?\\n?`, "m");
|
|
6661
|
+
const hasImport = importPattern.test(content);
|
|
6662
|
+
const hasProperty = propertyPattern.test(content);
|
|
6663
|
+
if (hasImport !== hasProperty) {
|
|
6664
|
+
throw new CliError(`Locale aggregator messages/${locale}.ts is inconsistent for ${fileName}.json.`, {
|
|
6665
|
+
code: "INCONSISTENT_LOCALE",
|
|
6666
|
+
scope: "project",
|
|
6667
|
+
path: `messages/${locale}.ts`
|
|
6668
|
+
});
|
|
5996
6669
|
}
|
|
6670
|
+
if (!hasImport)
|
|
6671
|
+
return { content, changed: false };
|
|
6672
|
+
return {
|
|
6673
|
+
content: content.replace(importPattern, "").replace(propertyPattern, ""),
|
|
6674
|
+
changed: true
|
|
6675
|
+
};
|
|
5997
6676
|
}
|
|
5998
|
-
|
|
5999
|
-
const candidates = await discoverPages(cwd);
|
|
6677
|
+
var rmPage = async (args, context) => {
|
|
6678
|
+
const candidates = await discoverPages(context.cwd, context.fs);
|
|
6000
6679
|
let logicalName = args[1];
|
|
6001
6680
|
if (!logicalName || logicalName.startsWith("-")) {
|
|
6002
|
-
if (
|
|
6003
|
-
throw new CliError("
|
|
6004
|
-
|
|
6681
|
+
if (context.outputMode === "json") {
|
|
6682
|
+
throw new CliError("Page name is required in JSON mode.", {
|
|
6683
|
+
code: "INTERACTIVE_INPUT_REQUIRED",
|
|
6684
|
+
hint: "Pass a page name returned by completion after rmpage."
|
|
6685
|
+
});
|
|
6686
|
+
}
|
|
6687
|
+
const selected = await context.prompt([
|
|
6005
6688
|
{
|
|
6006
6689
|
type: "autocomplete",
|
|
6007
6690
|
name: "page",
|
|
6008
|
-
message: "
|
|
6691
|
+
message: "Page to remove:",
|
|
6009
6692
|
choices: candidates.map((candidate2) => ({
|
|
6010
|
-
title: candidate2.logicalName.replaceAll(".", "
|
|
6693
|
+
title: candidate2.logicalName.replaceAll(".", " > "),
|
|
6011
6694
|
value: candidate2.logicalName
|
|
6012
6695
|
}))
|
|
6013
6696
|
},
|
|
@@ -6019,82 +6702,150 @@ async function rmPage(args, cwd = process.cwd(), prompt, terminal = console) {
|
|
|
6019
6702
|
}
|
|
6020
6703
|
]);
|
|
6021
6704
|
if (!selected.confirm) {
|
|
6022
|
-
|
|
6023
|
-
|
|
6705
|
+
context.operations.record({
|
|
6706
|
+
action: "cancelled",
|
|
6707
|
+
resource: "command",
|
|
6708
|
+
role: "page-removal",
|
|
6709
|
+
scope: "project",
|
|
6710
|
+
path: "."
|
|
6711
|
+
});
|
|
6712
|
+
return commandResult(context, {
|
|
6713
|
+
command: "rmpage",
|
|
6714
|
+
summary: "Page deletion was cancelled.",
|
|
6715
|
+
projectRoot: context.cwd,
|
|
6716
|
+
status: "cancelled"
|
|
6717
|
+
});
|
|
6024
6718
|
}
|
|
6025
6719
|
logicalName = String(selected.page ?? "");
|
|
6026
6720
|
}
|
|
6027
6721
|
parseLogicalName(logicalName, "page name");
|
|
6028
6722
|
const candidate = candidates.find((entry) => entry.logicalName === logicalName);
|
|
6029
|
-
if (!candidate)
|
|
6030
|
-
throw new CliError(`Page not found: ${logicalName}
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6723
|
+
if (!candidate) {
|
|
6724
|
+
throw new CliError(`Page not found: ${logicalName}.`, {
|
|
6725
|
+
code: "TARGET_NOT_FOUND",
|
|
6726
|
+
scope: "project",
|
|
6727
|
+
path: logicalName.replaceAll(".", "/")
|
|
6728
|
+
});
|
|
6729
|
+
}
|
|
6730
|
+
const messagesRoot = resolveInside(context.cwd, "messages");
|
|
6731
|
+
const preparedMessages = [];
|
|
6732
|
+
const preparedAggregators = [];
|
|
6733
|
+
if (context.fs.exists(messagesRoot)) {
|
|
6734
|
+
const locales = (await context.fs.list(messagesRoot)).filter((entry) => entry.isDirectory).map((entry) => entry.name).sort();
|
|
6735
|
+
for (const locale of locales) {
|
|
6736
|
+
const target = resolveInside(context.cwd, "messages", locale, `${candidate.routeSegments[0]}.json`);
|
|
6737
|
+
if (!candidate.messageKey) {
|
|
6738
|
+
const aggregator = resolveInside(context.cwd, "messages", `${locale}.ts`);
|
|
6739
|
+
if (!context.fs.exists(aggregator)) {
|
|
6740
|
+
throw new CliError(`Locale aggregator messages/${locale}.ts was not found.`, {
|
|
6741
|
+
code: "CONFIG_NOT_FOUND",
|
|
6742
|
+
scope: "project",
|
|
6743
|
+
path: `messages/${locale}.ts`
|
|
6744
|
+
});
|
|
6745
|
+
}
|
|
6746
|
+
const nextAggregator = unregisterMessagesFile(await context.fs.readText(aggregator), locale, candidate.routeSegments[0]);
|
|
6747
|
+
preparedMessages.push({
|
|
6748
|
+
target,
|
|
6749
|
+
keyPresent: context.fs.exists(target)
|
|
6750
|
+
});
|
|
6751
|
+
preparedAggregators.push({
|
|
6752
|
+
target: aggregator,
|
|
6753
|
+
...nextAggregator
|
|
6754
|
+
});
|
|
6755
|
+
continue;
|
|
6756
|
+
}
|
|
6757
|
+
if (!context.fs.exists(target))
|
|
6758
|
+
continue;
|
|
6759
|
+
let data;
|
|
6760
|
+
try {
|
|
6761
|
+
data = JSON.parse(await context.fs.readText(target));
|
|
6762
|
+
} catch {
|
|
6763
|
+
throw new CliError(`Invalid JSON prevents removal from ${path7.relative(context.cwd, target)}.`, {
|
|
6764
|
+
code: "FILESYSTEM_ERROR",
|
|
6765
|
+
scope: "project",
|
|
6766
|
+
path: path7.relative(context.cwd, target)
|
|
6767
|
+
});
|
|
6768
|
+
}
|
|
6769
|
+
const keyPresent = Object.hasOwn(data, candidate.messageKey);
|
|
6770
|
+
if (keyPresent)
|
|
6771
|
+
delete data[candidate.messageKey];
|
|
6772
|
+
preparedMessages.push({
|
|
6773
|
+
target,
|
|
6774
|
+
keyPresent,
|
|
6775
|
+
content: `${JSON.stringify(data, null, 2)}
|
|
6776
|
+
`
|
|
6777
|
+
});
|
|
6037
6778
|
}
|
|
6038
6779
|
}
|
|
6039
|
-
|
|
6040
|
-
|
|
6780
|
+
const gateway = new MutationGateway(context, context.cwd);
|
|
6781
|
+
for (const prepared of preparedMessages) {
|
|
6782
|
+
if (!candidate.messageKey) {
|
|
6783
|
+
await gateway.remove(prepared.target, { role: "translation-messages" });
|
|
6784
|
+
} else if (prepared.keyPresent) {
|
|
6785
|
+
await gateway.write(prepared.target, prepared.content, {
|
|
6786
|
+
role: "translation-messages",
|
|
6787
|
+
detail: { removedKey: candidate.messageKey }
|
|
6788
|
+
});
|
|
6789
|
+
} else {
|
|
6790
|
+
gateway.unchanged(prepared.target, {
|
|
6791
|
+
role: "translation-messages",
|
|
6792
|
+
detail: { missingKey: candidate.messageKey }
|
|
6793
|
+
});
|
|
6794
|
+
}
|
|
6795
|
+
}
|
|
6796
|
+
for (const prepared of preparedAggregators) {
|
|
6797
|
+
if (prepared.changed) {
|
|
6798
|
+
await gateway.write(prepared.target, prepared.content, {
|
|
6799
|
+
role: "locale-aggregator"
|
|
6800
|
+
});
|
|
6801
|
+
} else {
|
|
6802
|
+
gateway.unchanged(prepared.target, { role: "locale-aggregator" });
|
|
6803
|
+
}
|
|
6804
|
+
}
|
|
6805
|
+
await gateway.remove(candidate.uiDirectory, {
|
|
6806
|
+
role: "page-ui",
|
|
6807
|
+
resource: "directory"
|
|
6808
|
+
}, { recursive: true, force: false });
|
|
6809
|
+
await gateway.remove(candidate.routeDirectory, {
|
|
6810
|
+
role: "page-route",
|
|
6811
|
+
resource: "directory"
|
|
6812
|
+
}, { recursive: true, force: false });
|
|
6813
|
+
const mutated = context.operations.snapshot().some((event) => event.action === "deleted" || event.action === "updated");
|
|
6814
|
+
return commandResult(context, {
|
|
6815
|
+
command: "rmpage",
|
|
6816
|
+
summary: mutated ? `Deleted page "${logicalName}" and its associated resources.` : `No resources remained for page "${logicalName}".`,
|
|
6817
|
+
projectRoot: context.cwd,
|
|
6818
|
+
nextSteps: mutated ? [
|
|
6819
|
+
{
|
|
6820
|
+
kind: "run-checks",
|
|
6821
|
+
required: true,
|
|
6822
|
+
message: "Run the project checks after removing the page.",
|
|
6823
|
+
paths: [],
|
|
6824
|
+
commands: ["bun run check", "npm run check", "pnpm run check"]
|
|
6825
|
+
}
|
|
6826
|
+
] : []
|
|
6827
|
+
});
|
|
6828
|
+
};
|
|
6041
6829
|
|
|
6042
6830
|
// src/cli/registry.ts
|
|
6043
|
-
function legacyHandler(command) {
|
|
6044
|
-
return async (args, context) => {
|
|
6045
|
-
await command(args, context.cwd);
|
|
6046
|
-
return success();
|
|
6047
|
-
};
|
|
6048
|
-
}
|
|
6049
6831
|
function createCommandRegistry() {
|
|
6050
6832
|
return new Map([
|
|
6051
|
-
["addcomponent",
|
|
6052
|
-
["addpage",
|
|
6053
|
-
["addlib",
|
|
6054
|
-
["addapi",
|
|
6055
|
-
["addlanguage",
|
|
6056
|
-
["addtext",
|
|
6057
|
-
[
|
|
6058
|
-
"rmpage",
|
|
6059
|
-
async (args, context) => {
|
|
6060
|
-
await rmPage(args, context.cwd, context.prompt, context.terminal);
|
|
6061
|
-
return success();
|
|
6062
|
-
}
|
|
6063
|
-
]
|
|
6833
|
+
["addcomponent", addComponent],
|
|
6834
|
+
["addpage", addPage],
|
|
6835
|
+
["addlib", addLib],
|
|
6836
|
+
["addapi", addApi],
|
|
6837
|
+
["addlanguage", addLanguage],
|
|
6838
|
+
["addtext", addText],
|
|
6839
|
+
["rmpage", rmPage]
|
|
6064
6840
|
]);
|
|
6065
6841
|
}
|
|
6066
6842
|
|
|
6067
6843
|
// src/scaffold.ts
|
|
6068
|
-
import { mkdir as mkdir7, rm as rm2, writeFile as writeFile10 } from "fs/promises";
|
|
6069
6844
|
import { join as join8, resolve } from "path";
|
|
6070
|
-
import {
|
|
6071
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6072
|
-
|
|
6073
|
-
// src/lib/helper/consoleColor.ts
|
|
6074
|
-
var RED = "\x1B[31m";
|
|
6075
|
-
var GREEN = "\x1B[32m";
|
|
6076
|
-
var CYAN = "\x1B[36m";
|
|
6077
|
-
var RESET = "\x1B[0m";
|
|
6078
|
-
function red(text) {
|
|
6079
|
-
return RED + text + RESET;
|
|
6080
|
-
}
|
|
6081
|
-
function green(text) {
|
|
6082
|
-
return GREEN + text + RESET;
|
|
6083
|
-
}
|
|
6084
|
-
function cyan(text) {
|
|
6085
|
-
return CYAN + text + RESET;
|
|
6086
|
-
}
|
|
6845
|
+
import { fileURLToPath } from "url";
|
|
6087
6846
|
|
|
6088
6847
|
// src/core/template-manifest.ts
|
|
6089
|
-
import
|
|
6090
|
-
lstat as lstat2,
|
|
6091
|
-
mkdir as mkdir6,
|
|
6092
|
-
readdir as readdir7,
|
|
6093
|
-
readFile as readFile10,
|
|
6094
|
-
writeFile as writeFile9,
|
|
6095
|
-
copyFile as copyFile2
|
|
6096
|
-
} from "fs/promises";
|
|
6097
|
-
import path7 from "path";
|
|
6848
|
+
import path8 from "path";
|
|
6098
6849
|
var TEMPLATE_DENY_NAMES = new Set([
|
|
6099
6850
|
".env",
|
|
6100
6851
|
".git",
|
|
@@ -6108,74 +6859,108 @@ var TEMPLATE_DENY_NAMES = new Set([
|
|
|
6108
6859
|
"test-results"
|
|
6109
6860
|
]);
|
|
6110
6861
|
function isDistributableTemplatePath(relativePath) {
|
|
6111
|
-
const segments = relativePath.split(
|
|
6862
|
+
const segments = relativePath.split(path8.sep);
|
|
6112
6863
|
return !segments.some((segment) => TEMPLATE_DENY_NAMES.has(segment) || segment.endsWith(".tsbuildinfo") || segment === ".DS_Store");
|
|
6113
6864
|
}
|
|
6114
|
-
async function templateManifest(root) {
|
|
6865
|
+
async function templateManifest(root, fs) {
|
|
6115
6866
|
const files = [];
|
|
6116
6867
|
async function visit(directory, relativeDirectory = "") {
|
|
6117
|
-
const entries = await
|
|
6118
|
-
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
6868
|
+
const entries = await fs.list(directory);
|
|
6119
6869
|
for (const entry of entries) {
|
|
6120
|
-
const relative =
|
|
6870
|
+
const relative = path8.join(relativeDirectory, entry.name);
|
|
6121
6871
|
if (!isDistributableTemplatePath(relative))
|
|
6122
6872
|
continue;
|
|
6123
|
-
const source =
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
throw new CliError(`Symbolic links are forbidden in templates: ${relative}`);
|
|
6873
|
+
const source = path8.join(directory, entry.name);
|
|
6874
|
+
if (entry.isSymbolicLink) {
|
|
6875
|
+
throw new CliError(`Symbolic links are forbidden in templates: ${relative}`, { code: "UNSAFE_PATH", scope: "package", path: relative });
|
|
6127
6876
|
}
|
|
6128
|
-
if (
|
|
6877
|
+
if (entry.isDirectory)
|
|
6129
6878
|
await visit(source, relative);
|
|
6130
|
-
else if (
|
|
6879
|
+
else if (entry.isFile)
|
|
6131
6880
|
files.push(relative);
|
|
6132
6881
|
else
|
|
6133
|
-
throw new CliError(`Unsupported template entry: ${relative}
|
|
6882
|
+
throw new CliError(`Unsupported template entry: ${relative}`, {
|
|
6883
|
+
code: "TEMPLATE_MISSING",
|
|
6884
|
+
scope: "package",
|
|
6885
|
+
path: relative
|
|
6886
|
+
});
|
|
6134
6887
|
}
|
|
6135
6888
|
}
|
|
6136
6889
|
await visit(root);
|
|
6137
6890
|
return files;
|
|
6138
6891
|
}
|
|
6139
|
-
async function
|
|
6140
|
-
const manifest = await templateManifest(root);
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6892
|
+
async function validateScaffoldTemplate(root, fs) {
|
|
6893
|
+
const manifest = await templateManifest(root, fs);
|
|
6894
|
+
for (const required of ["package.json", "tsconfig.json"]) {
|
|
6895
|
+
if (!manifest.includes(required)) {
|
|
6896
|
+
throw new CliError(`Required template file was not found: ${required}.`, {
|
|
6897
|
+
code: "TEMPLATE_MISSING",
|
|
6898
|
+
scope: "package",
|
|
6899
|
+
path: `templates/Projects/default/${required}`
|
|
6900
|
+
});
|
|
6901
|
+
}
|
|
6902
|
+
}
|
|
6903
|
+
try {
|
|
6904
|
+
JSON.parse(await fs.readText(path8.join(root, "package.json")));
|
|
6905
|
+
const tsconfig = JSON.parse(await fs.readText(path8.join(root, "tsconfig.json")));
|
|
6906
|
+
if (!tsconfig.compilerOptions?.paths?.["@/*"])
|
|
6907
|
+
throw new Error("alias");
|
|
6908
|
+
} catch {
|
|
6909
|
+
throw new CliError('The template manifests must be valid JSON and define the default "@/*" alias.', {
|
|
6910
|
+
code: "TEMPLATE_MISSING",
|
|
6911
|
+
scope: "package",
|
|
6912
|
+
path: "templates/Projects/default"
|
|
6913
|
+
});
|
|
6146
6914
|
}
|
|
6147
6915
|
return manifest;
|
|
6148
6916
|
}
|
|
6149
|
-
async function
|
|
6150
|
-
const
|
|
6151
|
-
const
|
|
6917
|
+
async function copyTemplate(root, target, context, gateway, manifest) {
|
|
6918
|
+
const files = manifest ?? await templateManifest(root, context.fs);
|
|
6919
|
+
for (const relative of files) {
|
|
6920
|
+
const destination = path8.join(target, relative === ".gitignore.template" ? ".gitignore" : relative);
|
|
6921
|
+
const source = path8.join(root, relative);
|
|
6922
|
+
await gateway.copy(source, destination, {
|
|
6923
|
+
role: "template-file",
|
|
6924
|
+
source: { template: `Projects/default/${relative}` }
|
|
6925
|
+
});
|
|
6926
|
+
}
|
|
6927
|
+
return files;
|
|
6928
|
+
}
|
|
6929
|
+
async function customizeGeneratedProject(target, projectName, importAlias, context, gateway) {
|
|
6930
|
+
const packagePath = path8.join(target, "package.json");
|
|
6931
|
+
const packageJson = JSON.parse(await context.fs.readText(packagePath));
|
|
6152
6932
|
packageJson.name = projectName.toLowerCase();
|
|
6153
6933
|
delete packageJson.packageManager;
|
|
6154
|
-
await
|
|
6155
|
-
|
|
6156
|
-
const tsconfigPath =
|
|
6157
|
-
const tsconfigContent = await
|
|
6934
|
+
await gateway.write(packagePath, `${JSON.stringify(packageJson, null, 2)}
|
|
6935
|
+
`, { role: "package-manifest" });
|
|
6936
|
+
const tsconfigPath = path8.join(target, "tsconfig.json");
|
|
6937
|
+
const tsconfigContent = await context.fs.readText(tsconfigPath);
|
|
6158
6938
|
const tsconfig = JSON.parse(tsconfigContent);
|
|
6159
6939
|
if (!tsconfig.compilerOptions?.paths?.["@/*"]) {
|
|
6160
|
-
throw new CliError('The template tsconfig must define the default "@/*" alias.'
|
|
6940
|
+
throw new CliError('The template tsconfig must define the default "@/*" alias.', {
|
|
6941
|
+
code: "TEMPLATE_MISSING",
|
|
6942
|
+
scope: "package",
|
|
6943
|
+
path: "templates/Projects/default/tsconfig.json"
|
|
6944
|
+
});
|
|
6161
6945
|
}
|
|
6162
|
-
await
|
|
6946
|
+
await gateway.write(tsconfigPath, tsconfigContent.replace('"@/*"', `"${importAlias}"`), { role: "typescript-configuration" });
|
|
6163
6947
|
if (importAlias !== "@/*") {
|
|
6164
6948
|
const prefix = importAlias.slice(0, -2);
|
|
6165
|
-
for (const relative of await templateManifest(target)) {
|
|
6949
|
+
for (const relative of await templateManifest(target, context.fs)) {
|
|
6166
6950
|
if (!/\.[cm]?[jt]sx?$/.test(relative))
|
|
6167
6951
|
continue;
|
|
6168
|
-
const file =
|
|
6169
|
-
const content = await
|
|
6952
|
+
const file = path8.join(target, relative);
|
|
6953
|
+
const content = await context.fs.readText(file);
|
|
6170
6954
|
const next = content.replaceAll('from "@/', `from "${prefix}/`).replaceAll("from '@/", `from '${prefix}/`);
|
|
6171
|
-
if (next !== content)
|
|
6172
|
-
await
|
|
6955
|
+
if (next !== content) {
|
|
6956
|
+
await gateway.write(file, next, { role: "import-alias-reference" });
|
|
6957
|
+
}
|
|
6173
6958
|
}
|
|
6174
6959
|
}
|
|
6175
6960
|
}
|
|
6176
6961
|
|
|
6177
6962
|
// src/scaffold.ts
|
|
6178
|
-
async function scaffoldProject(options, runtime
|
|
6963
|
+
async function scaffoldProject(options, runtime) {
|
|
6179
6964
|
const requiredFeatures = [
|
|
6180
6965
|
"useTypescript",
|
|
6181
6966
|
"useEslint",
|
|
@@ -6186,57 +6971,91 @@ async function scaffoldProject(options, runtime = {}) {
|
|
|
6186
6971
|
];
|
|
6187
6972
|
const unsupported = requiredFeatures.filter((feature) => options[feature] !== true);
|
|
6188
6973
|
if (unsupported.length > 0) {
|
|
6189
|
-
throw new CliError(`The default Next.js 16 template requires: ${unsupported.join(", ")}
|
|
6974
|
+
throw new CliError(`The default Next.js 16 template requires: ${unsupported.join(", ")}.`, { code: "INVALID_ARGUMENT" });
|
|
6190
6975
|
}
|
|
6191
|
-
const
|
|
6192
|
-
const
|
|
6976
|
+
const { context } = runtime;
|
|
6977
|
+
const cwd = context.cwd;
|
|
6193
6978
|
const projectName = validateProjectName(options.projectName);
|
|
6194
6979
|
const importAlias = normalizeImportAlias(options.customAlias === false ? "@/*" : options.importAlias || "@/*");
|
|
6195
6980
|
const targetPath = join8(cwd, projectName);
|
|
6196
6981
|
const __dirname2 = new URL(".", import.meta.url);
|
|
6197
|
-
const templatePath = runtime.templatePath ?? join8(
|
|
6982
|
+
const templatePath = runtime.templatePath ?? join8(fileURLToPath(__dirname2), "..", "templates", "Projects", "default");
|
|
6198
6983
|
const resolvedCwd = resolve(cwd);
|
|
6199
6984
|
const resolvedTarget = resolve(targetPath);
|
|
6200
6985
|
if (!resolvedTarget.startsWith(`${resolvedCwd}/`)) {
|
|
6201
|
-
throw new CliError("The project destination must be a child of the current directory.");
|
|
6986
|
+
throw new CliError("The project destination must be a child of the current directory.", { code: "UNSAFE_PATH" });
|
|
6202
6987
|
}
|
|
6203
|
-
|
|
6988
|
+
const gateway = new MutationGateway(context, targetPath);
|
|
6989
|
+
const manifest = await validateScaffoldTemplate(templatePath, context.fs);
|
|
6990
|
+
if (context.fs.exists(targetPath)) {
|
|
6204
6991
|
if (options.force) {
|
|
6205
|
-
|
|
6206
|
-
|
|
6992
|
+
if (context.outputMode === "human") {
|
|
6993
|
+
context.terminal.warn(`WARNING: --force will remove the existing project at ${targetPath}.`);
|
|
6994
|
+
}
|
|
6995
|
+
await gateway.remove(targetPath, {
|
|
6996
|
+
role: "existing-project",
|
|
6997
|
+
resource: "project"
|
|
6998
|
+
}, { recursive: true, force: true });
|
|
6207
6999
|
} else {
|
|
6208
|
-
|
|
6209
|
-
|
|
7000
|
+
throw new CliError("Target directory already exists. Use --force to overwrite it.", {
|
|
7001
|
+
code: "TARGET_EXISTS",
|
|
7002
|
+
scope: "project",
|
|
7003
|
+
path: "."
|
|
7004
|
+
});
|
|
6210
7005
|
}
|
|
6211
7006
|
}
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
terminal.log("Then install dependencies and launch the dev server with your preferred tool:");
|
|
6226
|
-
terminal.log(" " + green(`bun install && bun dev`));
|
|
6227
|
-
terminal.log(" " + green(`npm install && npm run dev`));
|
|
6228
|
-
terminal.log(" " + green(`pnpm install && pnpm run dev`));
|
|
6229
|
-
terminal.log("");
|
|
6230
|
-
terminal.log("Documentation and examples can be found at:");
|
|
6231
|
-
terminal.log(" " + cyan("https://github.com/Rising-Corporation/create-next-pro-cli"));
|
|
6232
|
-
terminal.log("_-`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`-_");
|
|
6233
|
-
} catch (err) {
|
|
6234
|
-
terminal.error(red("[X] Error during project creation:"), err);
|
|
6235
|
-
throw new CliError("[X] Error during project creation:");
|
|
6236
|
-
}
|
|
7007
|
+
await gateway.mkdir(targetPath, {
|
|
7008
|
+
resource: "project",
|
|
7009
|
+
role: "project-root"
|
|
7010
|
+
});
|
|
7011
|
+
await copyTemplate(templatePath, targetPath, context, gateway, manifest);
|
|
7012
|
+
await customizeGeneratedProject(targetPath, projectName, importAlias, context, gateway);
|
|
7013
|
+
await gateway.write(join8(targetPath, "cnp.config.json"), `${JSON.stringify({ ...options, projectName, importAlias }, null, 2)}
|
|
7014
|
+
`, {
|
|
7015
|
+
role: "project-configuration",
|
|
7016
|
+
resource: "configuration",
|
|
7017
|
+
detail: { importAlias, packageManager: "user-selected" }
|
|
7018
|
+
});
|
|
7019
|
+
return { projectRoot: targetPath, copiedFiles: manifest.length, importAlias };
|
|
6237
7020
|
}
|
|
6238
7021
|
|
|
6239
7022
|
// src/lib/createProject.ts
|
|
7023
|
+
async function createProjectFromOptions(options, context) {
|
|
7024
|
+
const { projectRoot, copiedFiles, importAlias } = await scaffoldProject(options, {
|
|
7025
|
+
context
|
|
7026
|
+
});
|
|
7027
|
+
return commandResult(context, {
|
|
7028
|
+
command: "create",
|
|
7029
|
+
summary: `Created project "${options.projectName}" with ${copiedFiles} template files and import alias ${importAlias}; no package manager was pinned.`,
|
|
7030
|
+
projectRoot,
|
|
7031
|
+
nextSteps: [
|
|
7032
|
+
{
|
|
7033
|
+
kind: "install",
|
|
7034
|
+
required: true,
|
|
7035
|
+
message: "Install dependencies with your preferred package manager.",
|
|
7036
|
+
paths: [],
|
|
7037
|
+
commands: [
|
|
7038
|
+
`cd ${options.projectName} && bun install`,
|
|
7039
|
+
`cd ${options.projectName} && npm install`,
|
|
7040
|
+
`cd ${options.projectName} && pnpm install`
|
|
7041
|
+
]
|
|
7042
|
+
},
|
|
7043
|
+
{
|
|
7044
|
+
kind: "run-checks",
|
|
7045
|
+
required: true,
|
|
7046
|
+
message: "Run the generated project checks before development.",
|
|
7047
|
+
paths: [{ scope: "project", path: "package.json" }],
|
|
7048
|
+
commands: ["bun run check", "npm run check", "pnpm run check"]
|
|
7049
|
+
}
|
|
7050
|
+
],
|
|
7051
|
+
data: {
|
|
7052
|
+
projectName: options.projectName,
|
|
7053
|
+
importAlias,
|
|
7054
|
+
packageManager: null,
|
|
7055
|
+
copiedFiles
|
|
7056
|
+
}
|
|
7057
|
+
});
|
|
7058
|
+
}
|
|
6240
7059
|
async function createProject(nameArg, force, context) {
|
|
6241
7060
|
const response = {
|
|
6242
7061
|
projectName: nameArg,
|
|
@@ -6250,12 +7069,7 @@ async function createProject(nameArg, force, context) {
|
|
|
6250
7069
|
importAlias: "@/*",
|
|
6251
7070
|
force
|
|
6252
7071
|
};
|
|
6253
|
-
|
|
6254
|
-
terminal.log(`Creating project "${response.projectName}"...`);
|
|
6255
|
-
await scaffoldProject(response, {
|
|
6256
|
-
cwd: context?.cwd ?? process.cwd(),
|
|
6257
|
-
terminal
|
|
6258
|
-
});
|
|
7072
|
+
return createProjectFromOptions(response, context);
|
|
6259
7073
|
}
|
|
6260
7074
|
|
|
6261
7075
|
// src/lib/createProjectWithPrompt.ts
|
|
@@ -6330,32 +7144,135 @@ async function createProjectWithPrompt(context) {
|
|
|
6330
7144
|
initial: "@core/*"
|
|
6331
7145
|
}
|
|
6332
7146
|
]);
|
|
7147
|
+
if (!response.projectName) {
|
|
7148
|
+
context.operations.record({
|
|
7149
|
+
action: "cancelled",
|
|
7150
|
+
resource: "command",
|
|
7151
|
+
role: "project-creation",
|
|
7152
|
+
scope: "project",
|
|
7153
|
+
path: "."
|
|
7154
|
+
});
|
|
7155
|
+
return commandResult(context, {
|
|
7156
|
+
command: "create",
|
|
7157
|
+
summary: "Project creation was cancelled.",
|
|
7158
|
+
projectRoot: context.cwd,
|
|
7159
|
+
status: "cancelled"
|
|
7160
|
+
});
|
|
7161
|
+
}
|
|
6333
7162
|
const options = response;
|
|
6334
|
-
context
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
7163
|
+
return createProjectFromOptions(options, context);
|
|
7164
|
+
}
|
|
7165
|
+
|
|
7166
|
+
// src/runtime/node-context.ts
|
|
7167
|
+
var import_prompts = __toESM(require_prompts3(), 1);
|
|
7168
|
+
import { existsSync } from "fs";
|
|
7169
|
+
import {
|
|
7170
|
+
appendFile,
|
|
7171
|
+
copyFile,
|
|
7172
|
+
lstat,
|
|
7173
|
+
mkdir,
|
|
7174
|
+
readdir,
|
|
7175
|
+
readFile,
|
|
7176
|
+
rm,
|
|
7177
|
+
writeFile
|
|
7178
|
+
} from "fs/promises";
|
|
7179
|
+
import os from "os";
|
|
7180
|
+
import path9 from "path";
|
|
7181
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7182
|
+
function findPackageRoot(start) {
|
|
7183
|
+
let current = start;
|
|
7184
|
+
while (true) {
|
|
7185
|
+
const packagePath = path9.join(current, "package.json");
|
|
7186
|
+
if (existsSync(packagePath))
|
|
7187
|
+
return current;
|
|
7188
|
+
const parent = path9.dirname(current);
|
|
7189
|
+
if (parent === current) {
|
|
7190
|
+
throw new Error(`Unable to locate package.json from ${start}`);
|
|
7191
|
+
}
|
|
7192
|
+
current = parent;
|
|
7193
|
+
}
|
|
7194
|
+
}
|
|
7195
|
+
function resolvePackageRoot(metaUrl = import.meta.url) {
|
|
7196
|
+
return findPackageRoot(path9.dirname(fileURLToPath2(metaUrl)));
|
|
7197
|
+
}
|
|
7198
|
+
function createNodeContext(overrides = {}) {
|
|
7199
|
+
return {
|
|
7200
|
+
argv: process.argv.slice(2),
|
|
7201
|
+
cwd: process.cwd(),
|
|
7202
|
+
env: process.env,
|
|
7203
|
+
homeDir: os.homedir(),
|
|
7204
|
+
packageRoot: resolvePackageRoot(),
|
|
7205
|
+
terminal: console,
|
|
7206
|
+
prompt: import_prompts.default,
|
|
7207
|
+
fs: {
|
|
7208
|
+
exists: existsSync,
|
|
7209
|
+
readText: (target) => readFile(target, "utf8"),
|
|
7210
|
+
writeText: async (target, content) => {
|
|
7211
|
+
await writeFile(target, content);
|
|
7212
|
+
},
|
|
7213
|
+
mkdir: async (target) => {
|
|
7214
|
+
await mkdir(target, { recursive: true });
|
|
7215
|
+
},
|
|
7216
|
+
copyFile: async (source, target) => {
|
|
7217
|
+
await copyFile(source, target);
|
|
7218
|
+
},
|
|
7219
|
+
appendText: async (target, content) => {
|
|
7220
|
+
await appendFile(target, content);
|
|
7221
|
+
},
|
|
7222
|
+
remove: async (target, options) => {
|
|
7223
|
+
await rm(target, options);
|
|
7224
|
+
},
|
|
7225
|
+
inspect: async (target) => {
|
|
7226
|
+
try {
|
|
7227
|
+
const stats = await lstat(target);
|
|
7228
|
+
return {
|
|
7229
|
+
name: target,
|
|
7230
|
+
isFile: stats.isFile(),
|
|
7231
|
+
isDirectory: stats.isDirectory(),
|
|
7232
|
+
isSymbolicLink: stats.isSymbolicLink()
|
|
7233
|
+
};
|
|
7234
|
+
} catch (error) {
|
|
7235
|
+
if (error.code === "ENOENT")
|
|
7236
|
+
return null;
|
|
7237
|
+
throw error;
|
|
7238
|
+
}
|
|
7239
|
+
},
|
|
7240
|
+
list: async (target) => {
|
|
7241
|
+
const entries = await readdir(target, { withFileTypes: true });
|
|
7242
|
+
return Promise.all(entries.sort((left, right) => left.name.localeCompare(right.name)).map(async (entry) => {
|
|
7243
|
+
const stats = await lstat(path9.join(target, entry.name));
|
|
7244
|
+
return {
|
|
7245
|
+
name: entry.name,
|
|
7246
|
+
isFile: stats.isFile(),
|
|
7247
|
+
isDirectory: stats.isDirectory(),
|
|
7248
|
+
isSymbolicLink: stats.isSymbolicLink()
|
|
7249
|
+
};
|
|
7250
|
+
}));
|
|
7251
|
+
}
|
|
7252
|
+
},
|
|
7253
|
+
operations: new OperationJournal,
|
|
7254
|
+
outputMode: "human",
|
|
7255
|
+
...overrides
|
|
7256
|
+
};
|
|
6341
7257
|
}
|
|
6342
7258
|
|
|
6343
7259
|
// src/index.ts
|
|
6344
7260
|
var HELP_TEXT = `create-next-pro
|
|
6345
7261
|
|
|
6346
7262
|
Usage:
|
|
6347
|
-
create-next-pro <project-name> [--force]
|
|
6348
|
-
create-next-pro addpage [options]
|
|
6349
|
-
create-next-pro addcomponent [options]
|
|
6350
|
-
create-next-pro addlib [name]
|
|
6351
|
-
create-next-pro addapi [name]
|
|
6352
|
-
create-next-pro addlanguage [locale]
|
|
6353
|
-
create-next-pro addtext <path> [text]
|
|
6354
|
-
create-next-pro rmpage [options]
|
|
7263
|
+
create-next-pro <project-name> [--force] [--json]
|
|
7264
|
+
create-next-pro addpage [options] [--json]
|
|
7265
|
+
create-next-pro addcomponent [options] [--json]
|
|
7266
|
+
create-next-pro addlib [name] [--json]
|
|
7267
|
+
create-next-pro addapi [name] [--json]
|
|
7268
|
+
create-next-pro addlanguage [locale] [--json]
|
|
7269
|
+
create-next-pro addtext <path> [text] [--json]
|
|
7270
|
+
create-next-pro rmpage [options] [--json]
|
|
6355
7271
|
|
|
6356
7272
|
Options:
|
|
6357
7273
|
--help Show this help message
|
|
6358
7274
|
--version Show the CLI version
|
|
7275
|
+
--json Emit one deterministic JSON document
|
|
6359
7276
|
--reconfigure Run the configuration assistant again
|
|
6360
7277
|
`;
|
|
6361
7278
|
function parseOptions(args) {
|
|
@@ -6363,51 +7280,106 @@ function parseOptions(args) {
|
|
|
6363
7280
|
force: args.includes("--force"),
|
|
6364
7281
|
help: args.includes("--help"),
|
|
6365
7282
|
version: args.includes("--version") || args.includes("-v"),
|
|
6366
|
-
reconfigure: args.includes("--reconfigure")
|
|
7283
|
+
reconfigure: args.includes("--reconfigure"),
|
|
7284
|
+
json: args.includes("--json")
|
|
6367
7285
|
};
|
|
6368
7286
|
}
|
|
7287
|
+
function withoutGlobalOptions(args) {
|
|
7288
|
+
return args.filter((argument) => argument !== "--json");
|
|
7289
|
+
}
|
|
6369
7290
|
async function packageVersion(context) {
|
|
6370
|
-
const packageJson = JSON.parse(await context.fs.readText(
|
|
7291
|
+
const packageJson = JSON.parse(await context.fs.readText(path10.join(context.packageRoot, "package.json")));
|
|
6371
7292
|
return packageJson.version;
|
|
6372
7293
|
}
|
|
6373
|
-
|
|
7294
|
+
function interactiveInputError(message, hint) {
|
|
7295
|
+
throw new CliError(message, {
|
|
7296
|
+
code: "INTERACTIVE_INPUT_REQUIRED",
|
|
7297
|
+
hint
|
|
7298
|
+
});
|
|
7299
|
+
}
|
|
7300
|
+
async function executeCli(context) {
|
|
7301
|
+
const options = parseOptions(context.argv);
|
|
7302
|
+
context.outputMode = options.json ? "json" : "human";
|
|
7303
|
+
const args = withoutGlobalOptions(context.argv);
|
|
7304
|
+
const version = await packageVersion(context);
|
|
7305
|
+
if (options.help) {
|
|
7306
|
+
return commandResult(context, {
|
|
7307
|
+
command: "help",
|
|
7308
|
+
summary: "Displayed create-next-pro help.",
|
|
7309
|
+
projectRoot: context.cwd,
|
|
7310
|
+
status: "success",
|
|
7311
|
+
data: { help: HELP_TEXT }
|
|
7312
|
+
});
|
|
7313
|
+
}
|
|
7314
|
+
if (options.version) {
|
|
7315
|
+
return commandResult(context, {
|
|
7316
|
+
command: "version",
|
|
7317
|
+
summary: `create-next-pro version ${version}.`,
|
|
7318
|
+
projectRoot: context.cwd,
|
|
7319
|
+
status: "success",
|
|
7320
|
+
data: { version }
|
|
7321
|
+
});
|
|
7322
|
+
}
|
|
7323
|
+
const config = await readConfig(context);
|
|
7324
|
+
if (options.reconfigure) {
|
|
7325
|
+
if (options.json) {
|
|
7326
|
+
interactiveInputError("Reconfiguration is interactive and unavailable in JSON mode.", "Run create-next-pro --reconfigure without --json.");
|
|
7327
|
+
}
|
|
7328
|
+
return onboarding(context, version);
|
|
7329
|
+
}
|
|
7330
|
+
if (!config) {
|
|
7331
|
+
if (options.json) {
|
|
7332
|
+
throw new CliError("Initial configuration is required.", {
|
|
7333
|
+
code: "ONBOARDING_REQUIRED",
|
|
7334
|
+
scope: "config",
|
|
7335
|
+
path: "config.json",
|
|
7336
|
+
hint: "Run create-next-pro once without --json, then rerun the command."
|
|
7337
|
+
});
|
|
7338
|
+
}
|
|
7339
|
+
return onboarding(context, version);
|
|
7340
|
+
}
|
|
7341
|
+
const command = args[0];
|
|
7342
|
+
const handler = command ? createCommandRegistry().get(command) : undefined;
|
|
7343
|
+
if (handler)
|
|
7344
|
+
return handler(args, context);
|
|
7345
|
+
const nameArg = args.find((argument) => !argument.startsWith("--") && argument !== "-v");
|
|
7346
|
+
if (nameArg)
|
|
7347
|
+
return createProject(nameArg, options.force, context);
|
|
7348
|
+
if (options.json) {
|
|
7349
|
+
interactiveInputError("A project name is required in JSON mode.", "Pass the project name as a positional argument.");
|
|
7350
|
+
}
|
|
7351
|
+
return createProjectWithPrompt(context);
|
|
7352
|
+
}
|
|
7353
|
+
async function runCompletion(context, args) {
|
|
6374
7354
|
try {
|
|
6375
|
-
const
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
if (args[0] === "__complete") {
|
|
6379
|
-
await printCompletions(args, context);
|
|
6380
|
-
return 0;
|
|
6381
|
-
}
|
|
6382
|
-
if (options.help) {
|
|
6383
|
-
context.terminal.log(HELP_TEXT);
|
|
6384
|
-
return 0;
|
|
6385
|
-
}
|
|
6386
|
-
if (options.version) {
|
|
6387
|
-
context.terminal.log(`v${version}`);
|
|
6388
|
-
return 0;
|
|
6389
|
-
}
|
|
6390
|
-
if (options.reconfigure || !await readConfig(context)) {
|
|
6391
|
-
await onboarding(context, version);
|
|
6392
|
-
return 0;
|
|
6393
|
-
}
|
|
6394
|
-
if (args[0] === "addpage" && args.length === 1)
|
|
6395
|
-
args.push("-LPl");
|
|
6396
|
-
const handler = args[0] ? createCommandRegistry().get(args[0]) : undefined;
|
|
6397
|
-
if (handler)
|
|
6398
|
-
return (await handler(args, context)).exitCode;
|
|
6399
|
-
const nameArg = args.find((arg) => !arg.startsWith("--"));
|
|
6400
|
-
if (nameArg) {
|
|
6401
|
-
await createProject(nameArg, options.force, context);
|
|
6402
|
-
return 0;
|
|
6403
|
-
}
|
|
6404
|
-
await createProjectWithPrompt(context);
|
|
7355
|
+
for (const candidate of await completionCandidates(args[1], context)) {
|
|
7356
|
+
context.terminal.log(candidate);
|
|
7357
|
+
}
|
|
6405
7358
|
return 0;
|
|
7359
|
+
} catch {
|
|
7360
|
+
return 1;
|
|
7361
|
+
}
|
|
7362
|
+
}
|
|
7363
|
+
async function main(context = createNodeContext()) {
|
|
7364
|
+
const completionArgs = withoutGlobalOptions(context.argv);
|
|
7365
|
+
if (completionArgs[0] === "__complete") {
|
|
7366
|
+
return runCompletion(context, completionArgs);
|
|
7367
|
+
}
|
|
7368
|
+
context.operations.reset();
|
|
7369
|
+
context.outputMode = parseOptions(context.argv).json ? "json" : "human";
|
|
7370
|
+
let result;
|
|
7371
|
+
try {
|
|
7372
|
+
result = await executeCli(context);
|
|
6406
7373
|
} catch (error) {
|
|
6407
|
-
const
|
|
6408
|
-
context
|
|
6409
|
-
|
|
7374
|
+
const command = completionArgs[0] ?? "create";
|
|
7375
|
+
result = failedResult(context, command, error, {
|
|
7376
|
+
projectRoot: context.cwd,
|
|
7377
|
+
configRoot: configDirectory(context),
|
|
7378
|
+
homeRoot: context.homeDir
|
|
7379
|
+
});
|
|
6410
7380
|
}
|
|
7381
|
+
renderResult(result, context, context.outputMode);
|
|
7382
|
+
return result.exitCode;
|
|
6411
7383
|
}
|
|
6412
7384
|
|
|
6413
7385
|
// bin.bun.ts
|