create-next-pro-cli 0.1.30 → 0.1.32
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 +16 -6
- package/create-next-pro-completion.sh +4 -3
- package/create-next-pro-completion.zsh +3 -3
- package/dist/bin.bun.js +514 -118
- package/dist/bin.node.js +579 -135
- package/dist/bin.node.js.map +1 -1
- package/package.json +6 -5
- package/templates/Page/page-ui.tsx +2 -2
- package/templates/Page/page-ui.user.tsx +17 -0
- package/templates/Projects/default/.agents/skills/create-next-pro-addapi/SKILL.md +61 -0
- package/templates/Projects/default/.agents/skills/create-next-pro-addcomponent/SKILL.md +67 -0
- package/templates/Projects/default/.agents/skills/create-next-pro-addlanguage/SKILL.md +68 -0
- package/templates/Projects/default/.agents/skills/create-next-pro-addlib/SKILL.md +65 -0
- package/templates/Projects/default/.agents/skills/create-next-pro-addpage/SKILL.md +89 -0
- package/templates/Projects/default/.agents/skills/create-next-pro-addtext/SKILL.md +63 -0
- package/templates/Projects/default/.agents/skills/create-next-pro-create-project/SKILL.md +76 -0
- package/templates/Projects/default/.agents/skills/create-next-pro-rmpage/SKILL.md +67 -0
- package/templates/Projects/default/.github/workflows/quality.yml +2 -1
- package/templates/Projects/default/AGENTS.md +91 -0
- package/templates/Projects/default/README.md +29 -9
- package/templates/Projects/default/bun.lock +114 -106
- package/templates/Projects/default/package.json +7 -5
- package/templates/Projects/default/pnpm-workspace.yaml +2 -1
- package/templates/Projects/default/scripts/audit-policy.ts +376 -0
- package/templates/Projects/default/scripts/audit.ts +72 -3
- package/templates/Projects/default/scripts/package-manager.ts +37 -0
- package/templates/Projects/default/tests/consumer/validate-template.ts +2 -0
- package/templates/Projects/default/tests/unit/audit-policy.test.ts +152 -0
- package/templates/Projects/default/tests/unit/template-baseline.test.ts +9 -0
package/dist/bin.bun.js
CHANGED
|
@@ -4956,14 +4956,120 @@ import path10 from "path";
|
|
|
4956
4956
|
// src/cli/completion.ts
|
|
4957
4957
|
import path2 from "path";
|
|
4958
4958
|
|
|
4959
|
+
// src/core/contracts.ts
|
|
4960
|
+
class CliError extends Error {
|
|
4961
|
+
exitCode;
|
|
4962
|
+
code;
|
|
4963
|
+
hint;
|
|
4964
|
+
scope;
|
|
4965
|
+
path;
|
|
4966
|
+
constructor(message, options = {}) {
|
|
4967
|
+
super(message);
|
|
4968
|
+
this.name = "CliError";
|
|
4969
|
+
if (typeof options === "number") {
|
|
4970
|
+
this.exitCode = options;
|
|
4971
|
+
this.code = "FILESYSTEM_ERROR";
|
|
4972
|
+
} else {
|
|
4973
|
+
this.exitCode = options.exitCode ?? 1;
|
|
4974
|
+
this.code = options.code ?? "FILESYSTEM_ERROR";
|
|
4975
|
+
this.hint = options.hint;
|
|
4976
|
+
this.scope = options.scope;
|
|
4977
|
+
this.path = options.path;
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
}
|
|
4981
|
+
|
|
4982
|
+
// src/core/page-area.ts
|
|
4983
|
+
var PAGE_AREAS = ["public", "user"];
|
|
4984
|
+
function isPageArea(value) {
|
|
4985
|
+
return PAGE_AREAS.includes(value);
|
|
4986
|
+
}
|
|
4987
|
+
function parseAreaOption(args) {
|
|
4988
|
+
const remaining = args.length > 0 ? [args[0]] : [];
|
|
4989
|
+
let area;
|
|
4990
|
+
for (let index = 1;index < args.length; index += 1) {
|
|
4991
|
+
const argument = args[index];
|
|
4992
|
+
if (argument.startsWith("--area=")) {
|
|
4993
|
+
throw new CliError("The --area option must use a separate public or user value.", {
|
|
4994
|
+
code: "INVALID_ARGUMENT",
|
|
4995
|
+
hint: "Use --area public or --area user."
|
|
4996
|
+
});
|
|
4997
|
+
}
|
|
4998
|
+
if (argument !== "--area") {
|
|
4999
|
+
remaining.push(argument);
|
|
5000
|
+
continue;
|
|
5001
|
+
}
|
|
5002
|
+
if (area) {
|
|
5003
|
+
throw new CliError("The --area option can only be provided once.", {
|
|
5004
|
+
code: "INVALID_ARGUMENT"
|
|
5005
|
+
});
|
|
5006
|
+
}
|
|
5007
|
+
const value = args[index + 1];
|
|
5008
|
+
if (!value || value.startsWith("-")) {
|
|
5009
|
+
throw new CliError("The --area option requires public or user.", {
|
|
5010
|
+
code: "INVALID_ARGUMENT",
|
|
5011
|
+
hint: "Use --area public or --area user."
|
|
5012
|
+
});
|
|
5013
|
+
}
|
|
5014
|
+
if (!isPageArea(value)) {
|
|
5015
|
+
throw new CliError(`Unsupported page area: ${value}.`, {
|
|
5016
|
+
code: "INVALID_ARGUMENT",
|
|
5017
|
+
hint: "Use --area public or --area user."
|
|
5018
|
+
});
|
|
5019
|
+
}
|
|
5020
|
+
area = value;
|
|
5021
|
+
index += 1;
|
|
5022
|
+
}
|
|
5023
|
+
return { area, args: remaining };
|
|
5024
|
+
}
|
|
5025
|
+
function requirePageArea(area, command) {
|
|
5026
|
+
if (area)
|
|
5027
|
+
return area;
|
|
5028
|
+
throw new CliError(`The ${command} command requires --area public or user.`, {
|
|
5029
|
+
code: "INVALID_ARGUMENT",
|
|
5030
|
+
hint: `Run ${command} with --area public or --area user.`
|
|
5031
|
+
});
|
|
5032
|
+
}
|
|
5033
|
+
function areaRouteGroup(area) {
|
|
5034
|
+
return `(${area})`;
|
|
5035
|
+
}
|
|
5036
|
+
|
|
4959
5037
|
// src/core/page-catalog.ts
|
|
4960
5038
|
import path from "path";
|
|
4961
5039
|
function isRouteGroup(segment) {
|
|
4962
5040
|
return segment.startsWith("(") && segment.endsWith(")");
|
|
4963
5041
|
}
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
5042
|
+
function logicalSegments(relative) {
|
|
5043
|
+
return relative.filter((segment) => !isRouteGroup(segment));
|
|
5044
|
+
}
|
|
5045
|
+
function issueForRoute(projectRoot, directory, relative) {
|
|
5046
|
+
const segments = logicalSegments(relative);
|
|
5047
|
+
if (segments.length === 0 || segments.some((segment) => segment.startsWith("_") || segment.startsWith("["))) {
|
|
5048
|
+
return;
|
|
5049
|
+
}
|
|
5050
|
+
const first = relative[0];
|
|
5051
|
+
if (!first || !isRouteGroup(first)) {
|
|
5052
|
+
return {
|
|
5053
|
+
logicalName: segments.join("."),
|
|
5054
|
+
reason: "ungrouped",
|
|
5055
|
+
routeDirectories: [path.relative(projectRoot, directory)]
|
|
5056
|
+
};
|
|
5057
|
+
}
|
|
5058
|
+
const groupName = first.slice(1, -1);
|
|
5059
|
+
if (!isPageArea(groupName) || relative.slice(1).some(isRouteGroup)) {
|
|
5060
|
+
return {
|
|
5061
|
+
logicalName: segments.join("."),
|
|
5062
|
+
reason: "unsupported-route-group",
|
|
5063
|
+
routeDirectories: [path.relative(projectRoot, directory)]
|
|
5064
|
+
};
|
|
5065
|
+
}
|
|
5066
|
+
return;
|
|
5067
|
+
}
|
|
5068
|
+
async function discoverPageCatalog(projectRoot, fs) {
|
|
5069
|
+
const localizedRoot = path.join(projectRoot, "src", "app", "[locale]");
|
|
5070
|
+
const appRoot = fs.exists(localizedRoot) ? localizedRoot : path.join(projectRoot, "src", "app");
|
|
5071
|
+
const rawCandidates = [];
|
|
5072
|
+
const issues = [];
|
|
4967
5073
|
async function visit(directory, relative = []) {
|
|
4968
5074
|
let entries;
|
|
4969
5075
|
try {
|
|
@@ -4972,17 +5078,25 @@ async function discoverPages(projectRoot, fs) {
|
|
|
4972
5078
|
return;
|
|
4973
5079
|
}
|
|
4974
5080
|
if (entries.some((entry) => entry.isFile && entry.name === "page.tsx")) {
|
|
4975
|
-
const
|
|
4976
|
-
if (
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
5081
|
+
const issue = issueForRoute(projectRoot, directory, relative);
|
|
5082
|
+
if (issue) {
|
|
5083
|
+
issues.push(issue);
|
|
5084
|
+
} else if (relative.length > 1) {
|
|
5085
|
+
const area = relative[0].slice(1, -1);
|
|
5086
|
+
const routeSegments = relative.slice(1);
|
|
5087
|
+
if (routeSegments.length > 0 && !routeSegments.some((segment) => segment.startsWith("_") || segment.startsWith("["))) {
|
|
5088
|
+
const logicalName = routeSegments.join(".");
|
|
5089
|
+
rawCandidates.push({
|
|
5090
|
+
id: `${area}:${logicalName}`,
|
|
5091
|
+
area,
|
|
5092
|
+
logicalName,
|
|
5093
|
+
routeSegments,
|
|
5094
|
+
routeDirectory: directory,
|
|
5095
|
+
uiDirectory: path.join(projectRoot, "src", "ui", ...routeSegments),
|
|
5096
|
+
messageFile: path.join(projectRoot, "messages", "{locale}", `${routeSegments[0]}.json`),
|
|
5097
|
+
messageKey: routeSegments.length > 1 ? routeSegments.at(-1) : undefined
|
|
5098
|
+
});
|
|
5099
|
+
}
|
|
4986
5100
|
}
|
|
4987
5101
|
}
|
|
4988
5102
|
for (const entry of entries) {
|
|
@@ -4995,7 +5109,52 @@ async function discoverPages(projectRoot, fs) {
|
|
|
4995
5109
|
}
|
|
4996
5110
|
}
|
|
4997
5111
|
await visit(appRoot);
|
|
4998
|
-
|
|
5112
|
+
const candidatesByName = new Map;
|
|
5113
|
+
for (const candidate of rawCandidates) {
|
|
5114
|
+
const entries = candidatesByName.get(candidate.logicalName) ?? [];
|
|
5115
|
+
entries.push(candidate);
|
|
5116
|
+
candidatesByName.set(candidate.logicalName, entries);
|
|
5117
|
+
}
|
|
5118
|
+
for (const [logicalName, candidates2] of candidatesByName) {
|
|
5119
|
+
if (candidates2.length < 2)
|
|
5120
|
+
continue;
|
|
5121
|
+
issues.push({
|
|
5122
|
+
logicalName,
|
|
5123
|
+
reason: "duplicate-logical-route",
|
|
5124
|
+
routeDirectories: candidates2.map((candidate) => path.relative(projectRoot, candidate.routeDirectory))
|
|
5125
|
+
});
|
|
5126
|
+
}
|
|
5127
|
+
const invalidNames = new Set(issues.map((issue) => issue.logicalName));
|
|
5128
|
+
const candidates = rawCandidates.filter((candidate) => !invalidNames.has(candidate.logicalName)).sort((left, right) => left.area.localeCompare(right.area) || left.logicalName.localeCompare(right.logicalName));
|
|
5129
|
+
issues.sort((left, right) => left.logicalName.localeCompare(right.logicalName));
|
|
5130
|
+
return { candidates, issues };
|
|
5131
|
+
}
|
|
5132
|
+
function routeIssue(catalog, logicalName) {
|
|
5133
|
+
return catalog.issues.find((issue) => issue.logicalName === logicalName);
|
|
5134
|
+
}
|
|
5135
|
+
function assertConsistentLogicalRoute(catalog, logicalName) {
|
|
5136
|
+
const issue = routeIssue(catalog, logicalName);
|
|
5137
|
+
if (!issue)
|
|
5138
|
+
return;
|
|
5139
|
+
throw new CliError(`Page route "${logicalName}" is inconsistent.`, {
|
|
5140
|
+
code: "INCONSISTENT_ROUTE",
|
|
5141
|
+
scope: "project",
|
|
5142
|
+
path: issue.routeDirectories.join(", "),
|
|
5143
|
+
hint: "Move the route into exactly one of the (public) or (user) areas before retrying."
|
|
5144
|
+
});
|
|
5145
|
+
}
|
|
5146
|
+
function resolvePageCandidate(catalog, logicalName, area) {
|
|
5147
|
+
assertConsistentLogicalRoute(catalog, logicalName);
|
|
5148
|
+
const candidate = catalog.candidates.find((entry) => entry.logicalName === logicalName && entry.area === area);
|
|
5149
|
+
if (candidate)
|
|
5150
|
+
return candidate;
|
|
5151
|
+
const otherArea = catalog.candidates.find((entry) => entry.logicalName === logicalName);
|
|
5152
|
+
throw new CliError(`Page not found in the ${area} area: ${logicalName}.`, {
|
|
5153
|
+
code: "TARGET_NOT_FOUND",
|
|
5154
|
+
scope: "project",
|
|
5155
|
+
path: logicalName.replaceAll(".", "/"),
|
|
5156
|
+
hint: otherArea ? `The page exists in the ${otherArea.area} area.` : undefined
|
|
5157
|
+
});
|
|
4999
5158
|
}
|
|
5000
5159
|
|
|
5001
5160
|
// src/cli/completion.ts
|
|
@@ -5014,6 +5173,7 @@ var PUBLIC_COMMANDS = [
|
|
|
5014
5173
|
];
|
|
5015
5174
|
var OPTIONS = {
|
|
5016
5175
|
addpage: [
|
|
5176
|
+
"--area",
|
|
5017
5177
|
"--layout",
|
|
5018
5178
|
"--page",
|
|
5019
5179
|
"--loading",
|
|
@@ -5033,21 +5193,51 @@ async function directories(root, context) {
|
|
|
5033
5193
|
return [];
|
|
5034
5194
|
}
|
|
5035
5195
|
}
|
|
5036
|
-
|
|
5037
|
-
|
|
5196
|
+
function selectedArea(args) {
|
|
5197
|
+
const index = args.lastIndexOf("--area");
|
|
5198
|
+
if (index < 0)
|
|
5199
|
+
return;
|
|
5200
|
+
const value = args[index + 1];
|
|
5201
|
+
return value && isPageArea(value) ? value : undefined;
|
|
5202
|
+
}
|
|
5203
|
+
function uniqueSorted(candidates) {
|
|
5204
|
+
return [...new Set(candidates)].sort((left, right) => left.localeCompare(right));
|
|
5205
|
+
}
|
|
5206
|
+
async function completionCandidates(words, context) {
|
|
5207
|
+
if (words.length === 0)
|
|
5038
5208
|
return [...PUBLIC_COMMANDS];
|
|
5209
|
+
const [command, ...args] = words;
|
|
5210
|
+
const previous = args.at(-1);
|
|
5211
|
+
if (previous === "--area")
|
|
5212
|
+
return [...PAGE_AREAS];
|
|
5213
|
+
const area = selectedArea(args);
|
|
5214
|
+
const hasArea = args.includes("--area");
|
|
5039
5215
|
if (command === "rmpage") {
|
|
5040
|
-
|
|
5216
|
+
if (!area)
|
|
5217
|
+
return hasArea ? [] : ["--area"];
|
|
5218
|
+
const catalog = await discoverPageCatalog(context.cwd, context.fs);
|
|
5219
|
+
return uniqueSorted(catalog.candidates.filter((candidate) => candidate.area === area).map((candidate) => candidate.logicalName));
|
|
5220
|
+
}
|
|
5221
|
+
if (command === "addcomponent") {
|
|
5222
|
+
const pageOptionIndex = args.findIndex((argument) => argument === "--page" || argument === "-P");
|
|
5223
|
+
if (previous === "--page" || previous === "-P") {
|
|
5224
|
+
const catalog = await discoverPageCatalog(context.cwd, context.fs);
|
|
5225
|
+
return uniqueSorted(catalog.candidates.filter((candidate) => !area || candidate.area === area).map((candidate) => candidate.logicalName));
|
|
5226
|
+
}
|
|
5227
|
+
return uniqueSorted([
|
|
5228
|
+
...OPTIONS.addcomponent ?? [],
|
|
5229
|
+
...pageOptionIndex >= 0 && !hasArea ? ["--area"] : []
|
|
5230
|
+
]);
|
|
5041
5231
|
}
|
|
5042
|
-
if (command === "
|
|
5043
|
-
return [
|
|
5044
|
-
...OPTIONS
|
|
5232
|
+
if (command === "addpage") {
|
|
5233
|
+
return uniqueSorted([
|
|
5234
|
+
...(OPTIONS.addpage ?? []).filter((candidate) => candidate !== "--area" || !hasArea),
|
|
5045
5235
|
...await directories(path2.join(context.cwd, "src", "ui"), context)
|
|
5046
|
-
];
|
|
5236
|
+
]);
|
|
5047
5237
|
}
|
|
5048
5238
|
if (command === "addlanguage")
|
|
5049
5239
|
return ["de", "en", "es", "fr", "it", "ja", "pt"];
|
|
5050
|
-
return OPTIONS[command] ?? [];
|
|
5240
|
+
return uniqueSorted(OPTIONS[command] ?? []);
|
|
5051
5241
|
}
|
|
5052
5242
|
|
|
5053
5243
|
// src/cli/onboarding.ts
|
|
@@ -5055,31 +5245,6 @@ import path4 from "path";
|
|
|
5055
5245
|
|
|
5056
5246
|
// src/core/operations.ts
|
|
5057
5247
|
import path3 from "path";
|
|
5058
|
-
|
|
5059
|
-
// src/core/contracts.ts
|
|
5060
|
-
class CliError extends Error {
|
|
5061
|
-
exitCode;
|
|
5062
|
-
code;
|
|
5063
|
-
hint;
|
|
5064
|
-
scope;
|
|
5065
|
-
path;
|
|
5066
|
-
constructor(message, options = {}) {
|
|
5067
|
-
super(message);
|
|
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
5248
|
class OperationJournal {
|
|
5084
5249
|
#events = [];
|
|
5085
5250
|
record(event) {
|
|
@@ -5635,16 +5800,58 @@ var addApi = async (args, context) => {
|
|
|
5635
5800
|
|
|
5636
5801
|
// src/lib/addComponent.ts
|
|
5637
5802
|
import { join as join3 } from "path";
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5803
|
+
function formatGeneratedTranslations(content) {
|
|
5804
|
+
return content.replace(/^(\s*)(<(?:h2|p)\b[^>]*>)\{t\("([^"]+)"\)\}(<\/(?:h2|p)>)$/gm, (line, indent, opening, key, closing) => line.length <= 80 ? line : `${indent}${opening}
|
|
5805
|
+
${indent} {t("${key}")}
|
|
5806
|
+
${indent}${closing}`);
|
|
5807
|
+
}
|
|
5808
|
+
function parseAddComponentArguments(args) {
|
|
5809
|
+
const parsedArea = parseAreaOption(args);
|
|
5810
|
+
let componentName;
|
|
5811
|
+
let pageScope;
|
|
5812
|
+
for (let index = 1;index < parsedArea.args.length; index += 1) {
|
|
5813
|
+
const argument = parsedArea.args[index];
|
|
5814
|
+
if (argument === "-P" || argument === "--page") {
|
|
5815
|
+
if (pageScope) {
|
|
5816
|
+
throw new CliError("The --page option can only be provided once.", {
|
|
5817
|
+
code: "INVALID_ARGUMENT"
|
|
5818
|
+
});
|
|
5819
|
+
}
|
|
5820
|
+
const value = parsedArea.args[index + 1];
|
|
5821
|
+
if (!value || value.startsWith("-")) {
|
|
5822
|
+
throw new CliError("The --page option requires a page name.", {
|
|
5823
|
+
code: "INVALID_ARGUMENT"
|
|
5824
|
+
});
|
|
5825
|
+
}
|
|
5826
|
+
pageScope = value;
|
|
5827
|
+
index += 1;
|
|
5828
|
+
continue;
|
|
5829
|
+
}
|
|
5830
|
+
if (argument.startsWith("-")) {
|
|
5831
|
+
throw new CliError(`Unknown addcomponent option: ${argument}.`, {
|
|
5832
|
+
code: "INVALID_ARGUMENT"
|
|
5833
|
+
});
|
|
5834
|
+
}
|
|
5835
|
+
if (componentName) {
|
|
5836
|
+
throw new CliError(`Unexpected addcomponent argument: ${argument}.`, {
|
|
5837
|
+
code: "INVALID_ARGUMENT"
|
|
5838
|
+
});
|
|
5839
|
+
}
|
|
5840
|
+
componentName = argument;
|
|
5841
|
+
}
|
|
5842
|
+
if (pageScope && !parsedArea.area) {
|
|
5843
|
+
requirePageArea(parsedArea.area, "addcomponent --page");
|
|
5646
5844
|
}
|
|
5647
|
-
if (!
|
|
5845
|
+
if (!pageScope && parsedArea.area) {
|
|
5846
|
+
throw new CliError("The --area option is only valid with addcomponent --page.", { code: "INVALID_ARGUMENT" });
|
|
5847
|
+
}
|
|
5848
|
+
return { area: parsedArea.area, componentName, pageScope };
|
|
5849
|
+
}
|
|
5850
|
+
var addComponent = async (args, context) => {
|
|
5851
|
+
const parsed = parseAddComponentArguments(args);
|
|
5852
|
+
let { componentName } = parsed;
|
|
5853
|
+
const { area, pageScope } = parsed;
|
|
5854
|
+
if (!componentName) {
|
|
5648
5855
|
if (context.outputMode === "json") {
|
|
5649
5856
|
throw new CliError("Component name is required in JSON mode.", {
|
|
5650
5857
|
code: "INTERACTIVE_INPUT_REQUIRED",
|
|
@@ -5688,6 +5895,10 @@ var addComponent = async (args, context) => {
|
|
|
5688
5895
|
hint: "Run this command from the generated project root."
|
|
5689
5896
|
});
|
|
5690
5897
|
}
|
|
5898
|
+
if (pageScope) {
|
|
5899
|
+
const catalog = await discoverPageCatalog(context.cwd, context.fs);
|
|
5900
|
+
resolvePageCandidate(catalog, pageScope, area);
|
|
5901
|
+
}
|
|
5691
5902
|
const componentNameUpper = capitalize(toIdentifier(componentName));
|
|
5692
5903
|
const templateRoot = join3(context.packageRoot, "templates", "Component");
|
|
5693
5904
|
const componentTemplate = join3(templateRoot, "Component.tsx");
|
|
@@ -5703,7 +5914,7 @@ var addComponent = async (args, context) => {
|
|
|
5703
5914
|
await assertSafeTarget(context.cwd, targetDirectory, context.fs);
|
|
5704
5915
|
const componentFile = join3(targetDirectory, `${componentNameUpper}.tsx`);
|
|
5705
5916
|
const translationNamespace = pageScope ?? "_global_ui";
|
|
5706
|
-
const componentContent = (await context.fs.readText(componentTemplate)).replace(/Component/g, componentNameUpper).replace(/componentPage/g, translationNamespace);
|
|
5917
|
+
const componentContent = formatGeneratedTranslations((await context.fs.readText(componentTemplate)).replace(/Component/g, componentNameUpper).replace(/componentPage/g, translationNamespace));
|
|
5707
5918
|
const preparedMessages = [];
|
|
5708
5919
|
if (config.useI18n) {
|
|
5709
5920
|
const messagesRoot = join3(context.cwd, "messages");
|
|
@@ -5714,6 +5925,7 @@ var addComponent = async (args, context) => {
|
|
|
5714
5925
|
path: "messages"
|
|
5715
5926
|
});
|
|
5716
5927
|
}
|
|
5928
|
+
await assertSafeTarget(context.cwd, messagesRoot, context.fs);
|
|
5717
5929
|
const templateMessages = JSON.parse(await context.fs.readText(messagesTemplate));
|
|
5718
5930
|
const locales = (await context.fs.list(messagesRoot)).filter((entry) => entry.isDirectory).map((entry) => entry.name).sort();
|
|
5719
5931
|
if (locales.length === 0) {
|
|
@@ -5726,6 +5938,7 @@ var addComponent = async (args, context) => {
|
|
|
5726
5938
|
for (const locale of locales) {
|
|
5727
5939
|
const messageFile = pageScope ? pageSegments[0] : "_global_ui";
|
|
5728
5940
|
const target = join3(messagesRoot, locale, `${messageFile}.json`);
|
|
5941
|
+
await assertSafeTarget(context.cwd, target, context.fs);
|
|
5729
5942
|
let data = {};
|
|
5730
5943
|
if (context.fs.exists(target)) {
|
|
5731
5944
|
try {
|
|
@@ -5768,17 +5981,20 @@ var addComponent = async (args, context) => {
|
|
|
5768
5981
|
const gateway = new MutationGateway(context, context.cwd);
|
|
5769
5982
|
await gateway.mkdir(targetDirectory, {
|
|
5770
5983
|
role: "component-directory",
|
|
5771
|
-
resource: "directory"
|
|
5984
|
+
resource: "directory",
|
|
5985
|
+
detail: area ? { area } : undefined
|
|
5772
5986
|
});
|
|
5773
5987
|
await gateway.write(componentFile, componentContent, {
|
|
5774
5988
|
role: "ui-component",
|
|
5775
|
-
preserveExisting: true
|
|
5989
|
+
preserveExisting: true,
|
|
5990
|
+
detail: area ? { area } : undefined
|
|
5776
5991
|
});
|
|
5777
5992
|
for (const item of preparedMessages) {
|
|
5778
5993
|
if (item.exists) {
|
|
5779
5994
|
gateway.unchanged(item.target, {
|
|
5780
5995
|
role: "translation-messages",
|
|
5781
5996
|
detail: {
|
|
5997
|
+
...area ? { area } : {},
|
|
5782
5998
|
locale: item.locale,
|
|
5783
5999
|
key: `${translationNamespace}.${componentNameUpper}`
|
|
5784
6000
|
}
|
|
@@ -5787,6 +6003,7 @@ var addComponent = async (args, context) => {
|
|
|
5787
6003
|
await gateway.write(item.target, item.content, {
|
|
5788
6004
|
role: "translation-messages",
|
|
5789
6005
|
detail: {
|
|
6006
|
+
...area ? { area } : {},
|
|
5790
6007
|
locale: item.locale,
|
|
5791
6008
|
key: `${translationNamespace}.${componentNameUpper}`
|
|
5792
6009
|
}
|
|
@@ -5796,8 +6013,9 @@ var addComponent = async (args, context) => {
|
|
|
5796
6013
|
const mutated = context.operations.snapshot().some((event) => event.action === "created" || event.action === "updated");
|
|
5797
6014
|
return commandResult(context, {
|
|
5798
6015
|
command: "addcomponent",
|
|
5799
|
-
summary: mutated ? `Added component "${componentNameUpper}" ${pageScope ? `to page "${pageScope}"` : "globally"}.` : `Component "${componentNameUpper}" already exists and was preserved.`,
|
|
6016
|
+
summary: mutated ? `Added component "${componentNameUpper}" ${pageScope ? `to ${area} page "${pageScope}"` : "globally"}.` : pageScope ? `Component "${componentNameUpper}" already exists on ${area} page "${pageScope}" and was preserved.` : `Component "${componentNameUpper}" already exists and was preserved.`,
|
|
5800
6017
|
projectRoot: context.cwd,
|
|
6018
|
+
data: pageScope ? { area, componentName: componentNameUpper, page: pageScope } : { componentName: componentNameUpper },
|
|
5801
6019
|
nextSteps: mutated ? [
|
|
5802
6020
|
{
|
|
5803
6021
|
kind: "review",
|
|
@@ -6239,8 +6457,6 @@ var SHORT_FLAGS = {
|
|
|
6239
6457
|
};
|
|
6240
6458
|
function registerMessagesFile(content, locale, fileName) {
|
|
6241
6459
|
const importPath = `./${locale}/${fileName}.json`;
|
|
6242
|
-
if (content.includes(importPath))
|
|
6243
|
-
return content;
|
|
6244
6460
|
const declarationIndex = content.indexOf("const messages =");
|
|
6245
6461
|
const registryMatch = content.match(/const messages = \{([\s\S]*?)\n\};/);
|
|
6246
6462
|
if (declarationIndex < 0 || !registryMatch) {
|
|
@@ -6251,16 +6467,28 @@ function registerMessagesFile(content, locale, fileName) {
|
|
|
6251
6467
|
});
|
|
6252
6468
|
}
|
|
6253
6469
|
const identifier = toIdentifier(fileName);
|
|
6470
|
+
const property = identifier === fileName ? ` ${identifier},` : ` ${JSON.stringify(fileName)}: ${identifier},`;
|
|
6471
|
+
if (content.includes(importPath)) {
|
|
6472
|
+
if (registryMatch[1].split(/\r?\n/).includes(property))
|
|
6473
|
+
return content;
|
|
6474
|
+
const legacyProperty = new RegExp(`^\\s*${identifier},\\s*$`, "m");
|
|
6475
|
+
if (legacyProperty.test(registryMatch[1])) {
|
|
6476
|
+
return content.replace(legacyProperty, property);
|
|
6477
|
+
}
|
|
6478
|
+
const nextRegistry2 = registryMatch[0].replace(/\n\};$/, `
|
|
6479
|
+
${property}
|
|
6480
|
+
};`);
|
|
6481
|
+
return content.replace(registryMatch[0], nextRegistry2);
|
|
6482
|
+
}
|
|
6254
6483
|
const importStatement = `import ${identifier} from "${importPath}";
|
|
6255
6484
|
`;
|
|
6256
6485
|
const nextRegistry = registryMatch[0].replace(/\n\};$/, `
|
|
6257
|
-
|
|
6486
|
+
${property}
|
|
6258
6487
|
};`);
|
|
6259
6488
|
return content.slice(0, declarationIndex) + importStatement + content.slice(declarationIndex).replace(registryMatch[0], nextRegistry);
|
|
6260
6489
|
}
|
|
6261
|
-
function selectedFlags(
|
|
6490
|
+
function selectedFlags(optionArguments) {
|
|
6262
6491
|
const selected = new Set;
|
|
6263
|
-
const optionArguments = args.slice(2).filter((argument) => argument.startsWith("-"));
|
|
6264
6492
|
if (optionArguments.length === 0)
|
|
6265
6493
|
return new Set(["layout", "page", "loading"]);
|
|
6266
6494
|
for (const argument of optionArguments) {
|
|
@@ -6286,23 +6514,61 @@ function selectedFlags(args) {
|
|
|
6286
6514
|
}
|
|
6287
6515
|
return selected;
|
|
6288
6516
|
}
|
|
6517
|
+
function parseAddPageArguments(args) {
|
|
6518
|
+
const parsedArea = parseAreaOption(args);
|
|
6519
|
+
let logicalName;
|
|
6520
|
+
const optionArguments = [];
|
|
6521
|
+
for (const argument of parsedArea.args.slice(1)) {
|
|
6522
|
+
if (argument.startsWith("-")) {
|
|
6523
|
+
optionArguments.push(argument);
|
|
6524
|
+
continue;
|
|
6525
|
+
}
|
|
6526
|
+
if (logicalName) {
|
|
6527
|
+
throw new CliError(`Unexpected addpage argument: ${argument}.`, {
|
|
6528
|
+
code: "INVALID_ARGUMENT"
|
|
6529
|
+
});
|
|
6530
|
+
}
|
|
6531
|
+
logicalName = argument;
|
|
6532
|
+
}
|
|
6533
|
+
return {
|
|
6534
|
+
area: parsedArea.area,
|
|
6535
|
+
logicalName,
|
|
6536
|
+
flags: selectedFlags(optionArguments)
|
|
6537
|
+
};
|
|
6538
|
+
}
|
|
6289
6539
|
var addPage = async (args, context) => {
|
|
6290
|
-
|
|
6291
|
-
|
|
6540
|
+
const parsed = parseAddPageArguments(args);
|
|
6541
|
+
let { area, logicalName } = parsed;
|
|
6542
|
+
if (!logicalName) {
|
|
6292
6543
|
if (context.outputMode === "json") {
|
|
6293
6544
|
throw new CliError("Page name is required in JSON mode.", {
|
|
6294
6545
|
code: "INTERACTIVE_INPUT_REQUIRED",
|
|
6295
6546
|
hint: "Pass a simple or Parent.Child page name after addpage."
|
|
6296
6547
|
});
|
|
6297
6548
|
}
|
|
6298
|
-
const
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6549
|
+
const questions = [
|
|
6550
|
+
{
|
|
6551
|
+
type: "text",
|
|
6552
|
+
name: "pageName",
|
|
6553
|
+
message: "Page name to add:",
|
|
6554
|
+
validate: (name) => name ? true : "Page name is required"
|
|
6555
|
+
},
|
|
6556
|
+
...!area ? [
|
|
6557
|
+
{
|
|
6558
|
+
type: "select",
|
|
6559
|
+
name: "area",
|
|
6560
|
+
message: "Page area:",
|
|
6561
|
+
choices: [
|
|
6562
|
+
{ title: "public", value: "public" },
|
|
6563
|
+
{ title: "user", value: "user" }
|
|
6564
|
+
]
|
|
6565
|
+
}
|
|
6566
|
+
] : []
|
|
6567
|
+
];
|
|
6568
|
+
const response = await context.prompt(questions);
|
|
6304
6569
|
logicalName = String(response.pageName ?? "");
|
|
6305
|
-
|
|
6570
|
+
area = area ?? response.area;
|
|
6571
|
+
if (!logicalName || !area) {
|
|
6306
6572
|
context.operations.record({
|
|
6307
6573
|
action: "cancelled",
|
|
6308
6574
|
resource: "command",
|
|
@@ -6317,14 +6583,17 @@ var addPage = async (args, context) => {
|
|
|
6317
6583
|
status: "cancelled"
|
|
6318
6584
|
});
|
|
6319
6585
|
}
|
|
6586
|
+
} else {
|
|
6587
|
+
area = requirePageArea(area, "addpage");
|
|
6320
6588
|
}
|
|
6589
|
+
area = requirePageArea(area, "addpage");
|
|
6321
6590
|
const pageSegments = parseLogicalName(logicalName, "page name");
|
|
6322
6591
|
if (pageSegments.length > 2) {
|
|
6323
6592
|
throw new CliError("Nested pages support exactly Parent.Child.", {
|
|
6324
6593
|
code: "INVALID_ARGUMENT"
|
|
6325
6594
|
});
|
|
6326
6595
|
}
|
|
6327
|
-
const flags =
|
|
6596
|
+
const flags = parsed.flags;
|
|
6328
6597
|
const config = await loadConfig(context);
|
|
6329
6598
|
if (!config) {
|
|
6330
6599
|
throw new CliError("Configuration file cnp.config.json was not found.", {
|
|
@@ -6342,16 +6611,45 @@ var addPage = async (args, context) => {
|
|
|
6342
6611
|
path: useI18n ? "src/app/[locale]" : "src/app"
|
|
6343
6612
|
});
|
|
6344
6613
|
}
|
|
6614
|
+
const areaRoot = join6(appRoot, areaRouteGroup(area));
|
|
6615
|
+
if (!context.fs.exists(areaRoot)) {
|
|
6616
|
+
throw new CliError(`The ${area} page area was not found.`, {
|
|
6617
|
+
code: "CONFIG_NOT_FOUND",
|
|
6618
|
+
scope: "project",
|
|
6619
|
+
path: join6(useI18n ? "src/app/[locale]" : "src/app", areaRouteGroup(area))
|
|
6620
|
+
});
|
|
6621
|
+
}
|
|
6345
6622
|
const [parentName, childName] = pageSegments.length === 2 ? pageSegments : [undefined, undefined];
|
|
6346
6623
|
const leafName = childName ?? pageSegments[0];
|
|
6347
6624
|
const pageIdentifier = capitalize(toIdentifier(leafName));
|
|
6348
6625
|
const jsonFileName = parentName ?? leafName;
|
|
6349
6626
|
const uiDirectory = join6(context.cwd, "src", "ui", ...pageSegments);
|
|
6350
|
-
const routeDirectory = join6(
|
|
6627
|
+
const routeDirectory = join6(areaRoot, ...pageSegments);
|
|
6628
|
+
const catalog = await discoverPageCatalog(context.cwd, context.fs);
|
|
6629
|
+
assertConsistentLogicalRoute(catalog, logicalName);
|
|
6630
|
+
const existingOtherArea = catalog.candidates.find((candidate) => candidate.logicalName === logicalName && candidate.area !== area);
|
|
6631
|
+
const otherArea = area === "public" ? "user" : "public";
|
|
6632
|
+
const otherAreaDirectory = join6(appRoot, areaRouteGroup(otherArea), ...pageSegments);
|
|
6633
|
+
if (existingOtherArea || context.fs.exists(otherAreaDirectory)) {
|
|
6634
|
+
throw new CliError(`Page "${logicalName}" already belongs to the ${otherArea} area.`, {
|
|
6635
|
+
code: "TARGET_EXISTS",
|
|
6636
|
+
scope: "project",
|
|
6637
|
+
path: join6(useI18n ? "src/app/[locale]" : "src/app", areaRouteGroup(otherArea), ...pageSegments)
|
|
6638
|
+
});
|
|
6639
|
+
}
|
|
6640
|
+
const legacyDirectory = join6(appRoot, ...pageSegments);
|
|
6641
|
+
if (context.fs.exists(legacyDirectory)) {
|
|
6642
|
+
throw new CliError(`Page route "${logicalName}" is ungrouped.`, {
|
|
6643
|
+
code: "INCONSISTENT_ROUTE",
|
|
6644
|
+
scope: "project",
|
|
6645
|
+
path: join6(useI18n ? "src/app/[locale]" : "src/app", ...pageSegments),
|
|
6646
|
+
hint: "Move the route into exactly one of the (public) or (user) areas before retrying."
|
|
6647
|
+
});
|
|
6648
|
+
}
|
|
6351
6649
|
await assertSafeTarget(context.cwd, uiDirectory, context.fs);
|
|
6352
6650
|
await assertSafeTarget(context.cwd, routeDirectory, context.fs);
|
|
6353
6651
|
const templateRoot = join6(context.packageRoot, "templates", "Page");
|
|
6354
|
-
const uiTemplate = join6(templateRoot, "page-ui.tsx");
|
|
6652
|
+
const uiTemplate = join6(templateRoot, area === "user" ? "page-ui.user.tsx" : "page-ui.tsx");
|
|
6355
6653
|
const routeTemplates = [...flags].map((flag) => ({
|
|
6356
6654
|
flag,
|
|
6357
6655
|
source: join6(templateRoot, toFileName(flag))
|
|
@@ -6383,6 +6681,7 @@ var addPage = async (args, context) => {
|
|
|
6383
6681
|
path: "messages"
|
|
6384
6682
|
});
|
|
6385
6683
|
}
|
|
6684
|
+
await assertSafeTarget(context.cwd, messagesRoot, context.fs);
|
|
6386
6685
|
const locales = (await context.fs.list(messagesRoot)).filter((entry) => entry.isDirectory).map((entry) => entry.name).sort();
|
|
6387
6686
|
if (locales.length === 0) {
|
|
6388
6687
|
throw new CliError("No locale directories were found.", {
|
|
@@ -6394,6 +6693,8 @@ var addPage = async (args, context) => {
|
|
|
6394
6693
|
for (const locale of locales) {
|
|
6395
6694
|
const target = join6(messagesRoot, locale, `${jsonFileName}.json`);
|
|
6396
6695
|
const aggregator = join6(messagesRoot, `${locale}.ts`);
|
|
6696
|
+
await assertSafeTarget(context.cwd, target, context.fs);
|
|
6697
|
+
await assertSafeTarget(context.cwd, aggregator, context.fs);
|
|
6397
6698
|
if (!context.fs.exists(aggregator)) {
|
|
6398
6699
|
throw new CliError(`Locale aggregator messages/${locale}.ts was not found.`, {
|
|
6399
6700
|
code: "CONFIG_NOT_FOUND",
|
|
@@ -6436,17 +6737,20 @@ var addPage = async (args, context) => {
|
|
|
6436
6737
|
const gateway = new MutationGateway(context, context.cwd);
|
|
6437
6738
|
await gateway.mkdir(uiDirectory, {
|
|
6438
6739
|
role: "page-ui-directory",
|
|
6439
|
-
resource: "directory"
|
|
6740
|
+
resource: "directory",
|
|
6741
|
+
detail: { area }
|
|
6440
6742
|
});
|
|
6441
6743
|
const uiFile = join6(uiDirectory, "page-ui.tsx");
|
|
6442
6744
|
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
6745
|
await gateway.write(uiFile, uiContent, {
|
|
6444
6746
|
role: "page-ui",
|
|
6445
|
-
preserveExisting: true
|
|
6747
|
+
preserveExisting: true,
|
|
6748
|
+
detail: { area }
|
|
6446
6749
|
});
|
|
6447
6750
|
await gateway.mkdir(routeDirectory, {
|
|
6448
6751
|
role: "page-route-directory",
|
|
6449
|
-
resource: "directory"
|
|
6752
|
+
resource: "directory",
|
|
6753
|
+
detail: { area }
|
|
6450
6754
|
});
|
|
6451
6755
|
for (const template of routeTemplates) {
|
|
6452
6756
|
const filename = toFileName(template.flag);
|
|
@@ -6455,7 +6759,8 @@ var addPage = async (args, context) => {
|
|
|
6455
6759
|
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
6760
|
await gateway.write(target, content, {
|
|
6457
6761
|
role: `page-${template.flag}`,
|
|
6458
|
-
preserveExisting: true
|
|
6762
|
+
preserveExisting: true,
|
|
6763
|
+
detail: { area }
|
|
6459
6764
|
});
|
|
6460
6765
|
}
|
|
6461
6766
|
if (useI18n) {
|
|
@@ -6463,23 +6768,32 @@ var addPage = async (args, context) => {
|
|
|
6463
6768
|
if (item.messageExists) {
|
|
6464
6769
|
gateway.unchanged(item.target, {
|
|
6465
6770
|
role: "translation-messages",
|
|
6466
|
-
detail: {
|
|
6771
|
+
detail: {
|
|
6772
|
+
area,
|
|
6773
|
+
locale: item.locale,
|
|
6774
|
+
namespace: translationNamespace
|
|
6775
|
+
}
|
|
6467
6776
|
});
|
|
6468
6777
|
} else {
|
|
6469
6778
|
await gateway.write(item.target, item.targetContent, {
|
|
6470
6779
|
role: "translation-messages",
|
|
6471
|
-
detail: {
|
|
6780
|
+
detail: {
|
|
6781
|
+
area,
|
|
6782
|
+
locale: item.locale,
|
|
6783
|
+
namespace: translationNamespace
|
|
6784
|
+
}
|
|
6472
6785
|
});
|
|
6473
6786
|
}
|
|
6474
6787
|
await gateway.write(item.aggregator, item.aggregatorContent, {
|
|
6475
|
-
role: "locale-aggregator"
|
|
6788
|
+
role: "locale-aggregator",
|
|
6789
|
+
detail: { area }
|
|
6476
6790
|
});
|
|
6477
6791
|
}
|
|
6478
6792
|
} else {
|
|
6479
6793
|
gateway.skipped(join6(context.cwd, "messages"), {
|
|
6480
6794
|
role: "translation-messages",
|
|
6481
6795
|
resource: "directory",
|
|
6482
|
-
detail: { reason: "Internationalization is disabled." }
|
|
6796
|
+
detail: { area, reason: "Internationalization is disabled." }
|
|
6483
6797
|
});
|
|
6484
6798
|
}
|
|
6485
6799
|
const mutated = context.operations.snapshot().some((event) => ["created", "updated", "copied", "deleted"].includes(event.action));
|
|
@@ -6496,8 +6810,9 @@ var addPage = async (args, context) => {
|
|
|
6496
6810
|
];
|
|
6497
6811
|
return commandResult(context, {
|
|
6498
6812
|
command: "addpage",
|
|
6499
|
-
summary: mutated ? `Added page "${logicalName}" and its missing resources.` :
|
|
6813
|
+
summary: mutated ? `Added ${area} page "${logicalName}" and its missing resources.` : `${area[0].toUpperCase()}${area.slice(1)} page "${logicalName}" already exists and was preserved.`,
|
|
6500
6814
|
projectRoot: context.cwd,
|
|
6815
|
+
data: { area, logicalName },
|
|
6501
6816
|
nextSteps: mutated ? [
|
|
6502
6817
|
{
|
|
6503
6818
|
kind: "review",
|
|
@@ -6655,9 +6970,11 @@ function escapeRegExp(value) {
|
|
|
6655
6970
|
function unregisterMessagesFile(content, locale, fileName) {
|
|
6656
6971
|
const identifier = toIdentifier(fileName);
|
|
6657
6972
|
const escapedIdentifier = escapeRegExp(identifier);
|
|
6973
|
+
const escapedFileName = escapeRegExp(fileName);
|
|
6658
6974
|
const escapedPath = escapeRegExp(`./${locale}/${fileName}.json`);
|
|
6659
6975
|
const importPattern = new RegExp(`^import\\s+${escapedIdentifier}\\s+from\\s+["']${escapedPath}["'];?\\r?\\n?`, "m");
|
|
6660
|
-
const
|
|
6976
|
+
const propertyExpression = identifier === fileName ? escapedIdentifier : `(?:["']${escapedFileName}["']\\s*:\\s*)?${escapedIdentifier}`;
|
|
6977
|
+
const propertyPattern = new RegExp(`^\\s*${propertyExpression},\\s*\\r?\\n?`, "m");
|
|
6661
6978
|
const hasImport = importPattern.test(content);
|
|
6662
6979
|
const hasProperty = propertyPattern.test(content);
|
|
6663
6980
|
if (hasImport !== hasProperty) {
|
|
@@ -6674,24 +6991,57 @@ function unregisterMessagesFile(content, locale, fileName) {
|
|
|
6674
6991
|
changed: true
|
|
6675
6992
|
};
|
|
6676
6993
|
}
|
|
6994
|
+
function parseRmPageArguments(args) {
|
|
6995
|
+
const parsedArea = parseAreaOption(args);
|
|
6996
|
+
let logicalName;
|
|
6997
|
+
for (const argument of parsedArea.args.slice(1)) {
|
|
6998
|
+
if (argument.startsWith("-")) {
|
|
6999
|
+
throw new CliError(`Unknown rmpage option: ${argument}.`, {
|
|
7000
|
+
code: "INVALID_ARGUMENT"
|
|
7001
|
+
});
|
|
7002
|
+
}
|
|
7003
|
+
if (logicalName) {
|
|
7004
|
+
throw new CliError(`Unexpected rmpage argument: ${argument}.`, {
|
|
7005
|
+
code: "INVALID_ARGUMENT"
|
|
7006
|
+
});
|
|
7007
|
+
}
|
|
7008
|
+
logicalName = argument;
|
|
7009
|
+
}
|
|
7010
|
+
return { area: parsedArea.area, logicalName };
|
|
7011
|
+
}
|
|
6677
7012
|
var rmPage = async (args, context) => {
|
|
6678
|
-
const
|
|
6679
|
-
|
|
6680
|
-
|
|
7013
|
+
const parsed = parseRmPageArguments(args);
|
|
7014
|
+
const catalog = await discoverPageCatalog(context.cwd, context.fs);
|
|
7015
|
+
let { area, logicalName } = parsed;
|
|
7016
|
+
let candidate;
|
|
7017
|
+
if (!logicalName) {
|
|
6681
7018
|
if (context.outputMode === "json") {
|
|
6682
7019
|
throw new CliError("Page name is required in JSON mode.", {
|
|
6683
7020
|
code: "INTERACTIVE_INPUT_REQUIRED",
|
|
6684
7021
|
hint: "Pass a page name returned by completion after rmpage."
|
|
6685
7022
|
});
|
|
6686
7023
|
}
|
|
7024
|
+
const candidates = area ? catalog.candidates.filter((entry) => entry.area === area) : catalog.candidates;
|
|
7025
|
+
if (candidates.length === 0) {
|
|
7026
|
+
const issue = catalog.issues[0];
|
|
7027
|
+
if (issue) {
|
|
7028
|
+
throw new CliError(`Page route "${issue.logicalName}" is inconsistent.`, {
|
|
7029
|
+
code: "INCONSISTENT_ROUTE",
|
|
7030
|
+
scope: "project",
|
|
7031
|
+
path: issue.routeDirectories.join(", "),
|
|
7032
|
+
hint: "Move the route into exactly one of the (public) or (user) areas before retrying."
|
|
7033
|
+
});
|
|
7034
|
+
}
|
|
7035
|
+
throw new CliError(area ? `No pages were found in the ${area} area.` : "No removable pages were found.", { code: "TARGET_NOT_FOUND", scope: "project", path: "." });
|
|
7036
|
+
}
|
|
6687
7037
|
const selected = await context.prompt([
|
|
6688
7038
|
{
|
|
6689
7039
|
type: "autocomplete",
|
|
6690
7040
|
name: "page",
|
|
6691
7041
|
message: "Page to remove:",
|
|
6692
7042
|
choices: candidates.map((candidate2) => ({
|
|
6693
|
-
title: candidate2.logicalName.replaceAll(".", " > ")
|
|
6694
|
-
value: candidate2.
|
|
7043
|
+
title: `${candidate2.area[0].toUpperCase()}${candidate2.area.slice(1)} > ${candidate2.logicalName.replaceAll(".", " > ")}`,
|
|
7044
|
+
value: candidate2.id
|
|
6695
7045
|
}))
|
|
6696
7046
|
},
|
|
6697
7047
|
{
|
|
@@ -6701,41 +7051,56 @@ var rmPage = async (args, context) => {
|
|
|
6701
7051
|
initial: false
|
|
6702
7052
|
}
|
|
6703
7053
|
]);
|
|
7054
|
+
const selectedId = String(selected.page ?? "");
|
|
7055
|
+
const selectedCandidate = candidates.find((entry) => entry.id === selectedId);
|
|
6704
7056
|
if (!selected.confirm) {
|
|
6705
7057
|
context.operations.record({
|
|
6706
7058
|
action: "cancelled",
|
|
6707
7059
|
resource: "command",
|
|
6708
7060
|
role: "page-removal",
|
|
6709
7061
|
scope: "project",
|
|
6710
|
-
path: "."
|
|
7062
|
+
path: ".",
|
|
7063
|
+
detail: selectedCandidate ? { area: selectedCandidate.area } : area ? { area } : undefined
|
|
6711
7064
|
});
|
|
6712
7065
|
return commandResult(context, {
|
|
6713
7066
|
command: "rmpage",
|
|
6714
|
-
summary: "Page deletion was cancelled.",
|
|
7067
|
+
summary: selectedCandidate ? `Deletion of ${selectedCandidate.area} page "${selectedCandidate.logicalName}" was cancelled.` : area ? `Page deletion in the ${area} area was cancelled.` : "Page deletion was cancelled.",
|
|
6715
7068
|
projectRoot: context.cwd,
|
|
6716
|
-
status: "cancelled"
|
|
7069
|
+
status: "cancelled",
|
|
7070
|
+
data: selectedCandidate ? {
|
|
7071
|
+
area: selectedCandidate.area,
|
|
7072
|
+
logicalName: selectedCandidate.logicalName
|
|
7073
|
+
} : area ? { area } : undefined
|
|
6717
7074
|
});
|
|
6718
7075
|
}
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
7076
|
+
if (!selectedCandidate) {
|
|
7077
|
+
throw new CliError("The selected page is not in the page catalog.", {
|
|
7078
|
+
code: "INVALID_ARGUMENT"
|
|
7079
|
+
});
|
|
7080
|
+
}
|
|
7081
|
+
candidate = selectedCandidate;
|
|
7082
|
+
logicalName = candidate.logicalName;
|
|
7083
|
+
area = candidate.area;
|
|
7084
|
+
} else {
|
|
7085
|
+
area = requirePageArea(area, "rmpage");
|
|
7086
|
+
parseLogicalName(logicalName, "page name");
|
|
7087
|
+
candidate = resolvePageCandidate(catalog, logicalName, area);
|
|
6729
7088
|
}
|
|
7089
|
+
area = requirePageArea(area, "rmpage");
|
|
6730
7090
|
const messagesRoot = resolveInside(context.cwd, "messages");
|
|
7091
|
+
await assertSafeTarget(context.cwd, candidate.uiDirectory, context.fs);
|
|
7092
|
+
await assertSafeTarget(context.cwd, candidate.routeDirectory, context.fs);
|
|
6731
7093
|
const preparedMessages = [];
|
|
6732
7094
|
const preparedAggregators = [];
|
|
6733
7095
|
if (context.fs.exists(messagesRoot)) {
|
|
7096
|
+
await assertSafeTarget(context.cwd, messagesRoot, context.fs);
|
|
6734
7097
|
const locales = (await context.fs.list(messagesRoot)).filter((entry) => entry.isDirectory).map((entry) => entry.name).sort();
|
|
6735
7098
|
for (const locale of locales) {
|
|
6736
7099
|
const target = resolveInside(context.cwd, "messages", locale, `${candidate.routeSegments[0]}.json`);
|
|
7100
|
+
await assertSafeTarget(context.cwd, target, context.fs);
|
|
6737
7101
|
if (!candidate.messageKey) {
|
|
6738
7102
|
const aggregator = resolveInside(context.cwd, "messages", `${locale}.ts`);
|
|
7103
|
+
await assertSafeTarget(context.cwd, aggregator, context.fs);
|
|
6739
7104
|
if (!context.fs.exists(aggregator)) {
|
|
6740
7105
|
throw new CliError(`Locale aggregator messages/${locale}.ts was not found.`, {
|
|
6741
7106
|
code: "CONFIG_NOT_FOUND",
|
|
@@ -6780,41 +7145,51 @@ var rmPage = async (args, context) => {
|
|
|
6780
7145
|
const gateway = new MutationGateway(context, context.cwd);
|
|
6781
7146
|
for (const prepared of preparedMessages) {
|
|
6782
7147
|
if (!candidate.messageKey) {
|
|
6783
|
-
await gateway.remove(prepared.target, {
|
|
7148
|
+
await gateway.remove(prepared.target, {
|
|
7149
|
+
role: "translation-messages",
|
|
7150
|
+
detail: { area }
|
|
7151
|
+
});
|
|
6784
7152
|
} else if (prepared.keyPresent) {
|
|
6785
7153
|
await gateway.write(prepared.target, prepared.content, {
|
|
6786
7154
|
role: "translation-messages",
|
|
6787
|
-
detail: { removedKey: candidate.messageKey }
|
|
7155
|
+
detail: { area, removedKey: candidate.messageKey }
|
|
6788
7156
|
});
|
|
6789
7157
|
} else {
|
|
6790
7158
|
gateway.unchanged(prepared.target, {
|
|
6791
7159
|
role: "translation-messages",
|
|
6792
|
-
detail: { missingKey: candidate.messageKey }
|
|
7160
|
+
detail: { area, missingKey: candidate.messageKey }
|
|
6793
7161
|
});
|
|
6794
7162
|
}
|
|
6795
7163
|
}
|
|
6796
7164
|
for (const prepared of preparedAggregators) {
|
|
6797
7165
|
if (prepared.changed) {
|
|
6798
7166
|
await gateway.write(prepared.target, prepared.content, {
|
|
6799
|
-
role: "locale-aggregator"
|
|
7167
|
+
role: "locale-aggregator",
|
|
7168
|
+
detail: { area }
|
|
6800
7169
|
});
|
|
6801
7170
|
} else {
|
|
6802
|
-
gateway.unchanged(prepared.target, {
|
|
7171
|
+
gateway.unchanged(prepared.target, {
|
|
7172
|
+
role: "locale-aggregator",
|
|
7173
|
+
detail: { area }
|
|
7174
|
+
});
|
|
6803
7175
|
}
|
|
6804
7176
|
}
|
|
6805
7177
|
await gateway.remove(candidate.uiDirectory, {
|
|
6806
7178
|
role: "page-ui",
|
|
6807
|
-
resource: "directory"
|
|
7179
|
+
resource: "directory",
|
|
7180
|
+
detail: { area }
|
|
6808
7181
|
}, { recursive: true, force: false });
|
|
6809
7182
|
await gateway.remove(candidate.routeDirectory, {
|
|
6810
7183
|
role: "page-route",
|
|
6811
|
-
resource: "directory"
|
|
7184
|
+
resource: "directory",
|
|
7185
|
+
detail: { area }
|
|
6812
7186
|
}, { recursive: true, force: false });
|
|
6813
7187
|
const mutated = context.operations.snapshot().some((event) => event.action === "deleted" || event.action === "updated");
|
|
6814
7188
|
return commandResult(context, {
|
|
6815
7189
|
command: "rmpage",
|
|
6816
|
-
summary: mutated ? `Deleted page "${logicalName}" and its associated resources.` : `No resources remained for page "${logicalName}".`,
|
|
7190
|
+
summary: mutated ? `Deleted ${area} page "${logicalName}" and its associated resources.` : `No resources remained for ${area} page "${logicalName}".`,
|
|
6817
7191
|
projectRoot: context.cwd,
|
|
7192
|
+
data: { area, logicalName },
|
|
6818
7193
|
nextSteps: mutated ? [
|
|
6819
7194
|
{
|
|
6820
7195
|
kind: "run-checks",
|
|
@@ -6846,6 +7221,24 @@ import { fileURLToPath } from "url";
|
|
|
6846
7221
|
|
|
6847
7222
|
// src/core/template-manifest.ts
|
|
6848
7223
|
import path8 from "path";
|
|
7224
|
+
|
|
7225
|
+
// src/core/agent-guidance.ts
|
|
7226
|
+
var PROJECT_SKILL_NAMES = [
|
|
7227
|
+
"create-next-pro-create-project",
|
|
7228
|
+
"create-next-pro-addcomponent",
|
|
7229
|
+
"create-next-pro-addpage",
|
|
7230
|
+
"create-next-pro-addlib",
|
|
7231
|
+
"create-next-pro-addapi",
|
|
7232
|
+
"create-next-pro-addlanguage",
|
|
7233
|
+
"create-next-pro-addtext",
|
|
7234
|
+
"create-next-pro-rmpage"
|
|
7235
|
+
];
|
|
7236
|
+
var PROJECT_AGENT_GUIDANCE_FILES = [
|
|
7237
|
+
"AGENTS.md",
|
|
7238
|
+
...PROJECT_SKILL_NAMES.map((name) => `.agents/skills/${name}/SKILL.md`)
|
|
7239
|
+
];
|
|
7240
|
+
|
|
7241
|
+
// src/core/template-manifest.ts
|
|
6849
7242
|
var TEMPLATE_DENY_NAMES = new Set([
|
|
6850
7243
|
".env",
|
|
6851
7244
|
".git",
|
|
@@ -6893,6 +7286,7 @@ async function templateManifest(root, fs) {
|
|
|
6893
7286
|
async function validateScaffoldTemplate(root, fs) {
|
|
6894
7287
|
const manifest = await templateManifest(root, fs);
|
|
6895
7288
|
for (const required of [
|
|
7289
|
+
...PROJECT_AGENT_GUIDANCE_FILES.map((relative) => relative.split("/").join(path8.sep)),
|
|
6896
7290
|
"package.json",
|
|
6897
7291
|
"tsconfig.json",
|
|
6898
7292
|
".env.example",
|
|
@@ -6901,6 +7295,7 @@ async function validateScaffoldTemplate(root, fs) {
|
|
|
6901
7295
|
"bun.lock",
|
|
6902
7296
|
"pnpm-workspace.yaml",
|
|
6903
7297
|
"vitest.config.ts",
|
|
7298
|
+
path8.join("scripts", "audit-policy.ts"),
|
|
6904
7299
|
path8.join("scripts", "audit.ts"),
|
|
6905
7300
|
path8.join("scripts", "package-manager.ts"),
|
|
6906
7301
|
path8.join("tests", "consumer", "validate-template.ts"),
|
|
@@ -7286,18 +7681,19 @@ var HELP_TEXT = `create-next-pro
|
|
|
7286
7681
|
|
|
7287
7682
|
Usage:
|
|
7288
7683
|
create-next-pro <project-name> [--force] [--json]
|
|
7289
|
-
create-next-pro addpage [options] [--json]
|
|
7290
|
-
create-next-pro addcomponent [
|
|
7684
|
+
create-next-pro addpage [name] --area <public|user> [options] [--json]
|
|
7685
|
+
create-next-pro addcomponent [name] [--page <page> --area <public|user>] [--json]
|
|
7291
7686
|
create-next-pro addlib [name] [--json]
|
|
7292
7687
|
create-next-pro addapi [name] [--json]
|
|
7293
7688
|
create-next-pro addlanguage [locale] [--json]
|
|
7294
7689
|
create-next-pro addtext <path> [text] [--json]
|
|
7295
|
-
create-next-pro rmpage [
|
|
7690
|
+
create-next-pro rmpage [name] [--area <public|user>] [--json]
|
|
7296
7691
|
|
|
7297
7692
|
Options:
|
|
7298
7693
|
--help Show this help message
|
|
7299
7694
|
--version Show the CLI version
|
|
7300
7695
|
--json Emit one deterministic JSON document
|
|
7696
|
+
--area Select the public or user area for page operations
|
|
7301
7697
|
--reconfigure Run the configuration assistant again
|
|
7302
7698
|
`;
|
|
7303
7699
|
function parseOptions(args) {
|
|
@@ -7377,7 +7773,7 @@ async function executeCli(context) {
|
|
|
7377
7773
|
}
|
|
7378
7774
|
async function runCompletion(context, args) {
|
|
7379
7775
|
try {
|
|
7380
|
-
for (const candidate of await completionCandidates(args
|
|
7776
|
+
for (const candidate of await completionCandidates(args.slice(1), context)) {
|
|
7381
7777
|
context.terminal.log(candidate);
|
|
7382
7778
|
}
|
|
7383
7779
|
return 0;
|