create-next-pro-cli 0.1.26 → 0.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +281 -291
- package/create-next-pro-completion.sh +8 -38
- package/create-next-pro-completion.zsh +13 -0
- package/dist/bin.bun.js +2265 -794
- package/dist/bin.node.js +2425 -805
- package/dist/bin.node.js.map +1 -1
- package/package.json +50 -27
- package/templates/Projects/default/.env.example +17 -0
- package/templates/Projects/default/.github/workflows/quality.yml +24 -0
- package/templates/Projects/default/.gitignore.template +47 -0
- package/templates/Projects/default/.prettierignore +3 -0
- package/templates/Projects/default/README.md +52 -108
- package/templates/Projects/default/bun.lock +1152 -0
- package/templates/Projects/default/eslint.config.mjs +27 -11
- package/templates/Projects/default/messages/en/_global_ui.json +23 -10
- package/templates/Projects/default/messages/en.ts +17 -2
- package/templates/Projects/default/messages/fr/_global_ui.json +23 -10
- package/templates/Projects/default/messages/fr.ts +17 -2
- package/templates/Projects/default/next.config.ts +43 -3
- package/templates/Projects/default/package.json +42 -24
- package/templates/Projects/default/playwright.config.ts +26 -0
- package/templates/Projects/default/pnpm-workspace.yaml +5 -0
- package/templates/Projects/default/public/{cnp-logo.svg → logo.svg} +1 -1
- package/templates/Projects/default/src/app/[locale]/(public)/layout.tsx +8 -1
- package/templates/Projects/default/src/app/[locale]/(public)/login/page.tsx +8 -0
- package/templates/Projects/default/src/app/[locale]/(public)/register/page.tsx +8 -0
- package/templates/Projects/default/src/app/[locale]/(user)/dashboard/error.tsx +25 -0
- package/templates/Projects/default/src/app/[locale]/(user)/dashboard/loading.tsx +5 -0
- package/templates/Projects/default/src/app/[locale]/(user)/{Dashboard → dashboard}/page.tsx +1 -1
- package/templates/Projects/default/src/app/[locale]/(user)/layout.tsx +24 -2
- package/templates/Projects/default/src/app/[locale]/(user)/settings/loading.tsx +5 -0
- package/templates/Projects/default/src/app/[locale]/(user)/{Settings → settings}/page.tsx +1 -2
- package/templates/Projects/default/src/app/[locale]/(user)/userInfo/loading.tsx +5 -0
- package/templates/Projects/default/src/app/[locale]/(user)/{UserInfo → userInfo}/page.tsx +1 -2
- package/templates/Projects/default/src/app/[locale]/layout.tsx +34 -10
- package/templates/Projects/default/src/app/[locale]/loading.tsx +0 -9
- package/templates/Projects/default/src/app/[locale]/not-found.tsx +6 -15
- package/templates/Projects/default/src/app/[locale]/page.tsx +10 -1
- package/templates/Projects/default/src/app/api/auth/[...nextauth]/route.ts +8 -60
- package/templates/Projects/default/src/app/not-found.tsx +1 -1
- package/templates/Projects/default/src/app/sitemap.ts +2 -2
- package/templates/Projects/default/src/app/styles/globals.css +166 -113
- package/templates/Projects/default/src/auth.ts +20 -0
- package/templates/Projects/default/src/config.ts +3 -3
- package/templates/Projects/default/src/env.ts +65 -0
- package/templates/Projects/default/src/lib/i18n/messages.ts +8 -0
- package/templates/Projects/default/src/lib/i18n/request.ts +2 -16
- package/templates/Projects/default/src/lib/security/csp.ts +16 -0
- package/templates/Projects/default/src/lib/utils.ts +2 -1
- package/templates/Projects/default/src/proxy.ts +13 -0
- package/templates/Projects/default/src/ui/_global/BackButton.tsx +4 -2
- package/templates/Projects/default/src/ui/_global/Button.tsx +3 -8
- package/templates/Projects/default/src/ui/_global/GlobalHeader.tsx +10 -28
- package/templates/Projects/default/src/ui/_global/GlobalMain.tsx +1 -1
- package/templates/Projects/default/src/ui/_global/LocaleSwitcher.tsx +9 -10
- package/templates/Projects/default/src/ui/_global/PublicNav.tsx +51 -17
- package/templates/Projects/default/src/ui/_global/ThemeToggle.tsx +45 -39
- package/templates/Projects/default/src/ui/_global/UserNav.tsx +6 -6
- package/templates/Projects/default/src/ui/_home/page-ui.tsx +5 -7
- package/templates/Projects/default/src/ui/{Dashboard → dashboard}/LogoutButton.tsx +9 -7
- package/templates/Projects/default/src/ui/{Dashboard → dashboard}/StatsCard.tsx +4 -4
- package/templates/Projects/default/src/ui/{Dashboard → dashboard}/page-ui.tsx +7 -8
- package/templates/Projects/default/src/ui/login/page-ui.tsx +36 -0
- package/templates/Projects/default/src/ui/register/page-ui.tsx +38 -0
- package/templates/Projects/default/src/ui/settings/page-ui.tsx +15 -0
- package/templates/Projects/default/src/ui/userInfo/page-ui.tsx +15 -0
- package/templates/Projects/default/tailwind.config.ts +81 -1
- package/templates/Projects/default/tests/consumer/validate-template.ts +66 -0
- package/templates/Projects/default/tests/e2e/template-remediation.playwright.ts +106 -0
- package/templates/Projects/default/tests/rendering/verify-rendering.ts +56 -0
- package/templates/Projects/default/tests/unit/csp.test.ts +19 -0
- package/templates/Projects/default/tests/unit/env.test.ts +76 -0
- package/templates/Projects/default/tsconfig.json +6 -6
- package/templates/Projects/default/example.env +0 -8
- package/templates/Projects/default/messages/getMergedMessages.ts +0 -31
- package/templates/Projects/default/middleware.ts +0 -11
- package/templates/Projects/default/src/app/[locale]/(public)/Login/page.tsx +0 -6
- package/templates/Projects/default/src/app/[locale]/(public)/Register/page.tsx +0 -6
- package/templates/Projects/default/src/app/[locale]/(user)/Dashboard/error.tsx +0 -38
- package/templates/Projects/default/src/app/[locale]/(user)/Dashboard/loading.tsx +0 -10
- package/templates/Projects/default/src/app/[locale]/(user)/Settings/loading.tsx +0 -17
- package/templates/Projects/default/src/app/[locale]/(user)/UserInfo/loading.tsx +0 -17
- package/templates/Projects/default/src/app/api/auth/post-login/route.ts +0 -26
- package/templates/Projects/default/src/app/api/hello/route.ts +0 -5
- package/templates/Projects/default/src/app/layout.tsx +0 -11
- package/templates/Projects/default/src/app/page.tsx +0 -6
- package/templates/Projects/default/src/auth.config.ts +0 -0
- package/templates/Projects/default/src/lib/auth/disconnect.ts +0 -11
- package/templates/Projects/default/src/lib/auth/isConnected.ts +0 -18
- package/templates/Projects/default/src/lib/sample/example.ts +0 -3
- package/templates/Projects/default/src/lib/sample/index.ts +0 -3
- package/templates/Projects/default/src/ui/Login/page-ui.tsx +0 -22
- package/templates/Projects/default/src/ui/Register/page-ui.tsx +0 -26
- package/templates/Projects/default/src/ui/Settings/page-ui.tsx +0 -17
- package/templates/Projects/default/src/ui/UserInfo/page-ui.tsx +0 -17
- /package/templates/Projects/default/messages/en/{Dashboard.json → dashboard.json} +0 -0
- /package/templates/Projects/default/messages/en/{Login.json → login.json} +0 -0
- /package/templates/Projects/default/messages/en/{Register.json → register.json} +0 -0
- /package/templates/Projects/default/messages/en/{Settings.json → settings.json} +0 -0
- /package/templates/Projects/default/messages/en/{UserInfo.json → userInfo.json} +0 -0
- /package/templates/Projects/default/messages/fr/{Dashboard.json → dashboard.json} +0 -0
- /package/templates/Projects/default/messages/fr/{Login.json → login.json} +0 -0
- /package/templates/Projects/default/messages/fr/{Register.json → register.json} +0 -0
- /package/templates/Projects/default/messages/fr/{Settings.json → settings.json} +0 -0
- /package/templates/Projects/default/messages/fr/{UserInfo.json → userInfo.json} +0 -0
- /package/templates/Projects/default/public/{cnp-logo.png → logo.png} +0 -0
- /package/templates/Projects/default/src/ui/{Dashboard → dashboard}/WelcomeCard.tsx +0 -0
package/dist/bin.bun.js
CHANGED
|
@@ -5,21 +5,35 @@ var __getProtoOf = Object.getPrototypeOf;
|
|
|
5
5
|
var __defProp = Object.defineProperty;
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
function __accessProp(key) {
|
|
9
|
+
return this[key];
|
|
10
|
+
}
|
|
11
|
+
var __toESMCache_node;
|
|
12
|
+
var __toESMCache_esm;
|
|
8
13
|
var __toESM = (mod, isNodeMode, target) => {
|
|
14
|
+
var canCache = mod != null && typeof mod === "object";
|
|
15
|
+
if (canCache) {
|
|
16
|
+
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
17
|
+
var cached = cache.get(mod);
|
|
18
|
+
if (cached)
|
|
19
|
+
return cached;
|
|
20
|
+
}
|
|
9
21
|
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
10
22
|
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
11
23
|
for (let key of __getOwnPropNames(mod))
|
|
12
24
|
if (!__hasOwnProp.call(to, key))
|
|
13
25
|
__defProp(to, key, {
|
|
14
|
-
get: (
|
|
26
|
+
get: __accessProp.bind(mod, key),
|
|
15
27
|
enumerable: true
|
|
16
28
|
});
|
|
29
|
+
if (canCache)
|
|
30
|
+
cache.set(mod, to);
|
|
17
31
|
return to;
|
|
18
32
|
};
|
|
19
33
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
20
34
|
var __require = import.meta.require;
|
|
21
35
|
|
|
22
|
-
// node_modules/
|
|
36
|
+
// node_modules/kleur/index.js
|
|
23
37
|
var require_kleur = __commonJS((exports, module) => {
|
|
24
38
|
var { FORCE_COLOR, NODE_DISABLE_COLORS, TERM } = process.env;
|
|
25
39
|
var $ = {
|
|
@@ -243,7 +257,7 @@ var require_clear = __commonJS((exports, module) => {
|
|
|
243
257
|
if (it)
|
|
244
258
|
o = it;
|
|
245
259
|
var i = 0;
|
|
246
|
-
var F = function
|
|
260
|
+
var F = function F2() {};
|
|
247
261
|
return { s: F, n: function n() {
|
|
248
262
|
if (i >= o.length)
|
|
249
263
|
return { done: true };
|
|
@@ -523,7 +537,7 @@ var require_prompt = __commonJS((exports, module) => {
|
|
|
523
537
|
|
|
524
538
|
// node_modules/prompts/dist/elements/text.js
|
|
525
539
|
var require_text = __commonJS((exports, module) => {
|
|
526
|
-
function asyncGeneratorStep(gen,
|
|
540
|
+
function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
|
|
527
541
|
try {
|
|
528
542
|
var info = gen[key](arg);
|
|
529
543
|
var value = info.value;
|
|
@@ -532,7 +546,7 @@ var require_text = __commonJS((exports, module) => {
|
|
|
532
546
|
return;
|
|
533
547
|
}
|
|
534
548
|
if (info.done) {
|
|
535
|
-
|
|
549
|
+
resolve2(value);
|
|
536
550
|
} else {
|
|
537
551
|
Promise.resolve(value).then(_next, _throw);
|
|
538
552
|
}
|
|
@@ -540,13 +554,13 @@ var require_text = __commonJS((exports, module) => {
|
|
|
540
554
|
function _asyncToGenerator(fn) {
|
|
541
555
|
return function() {
|
|
542
556
|
var self = this, args = arguments;
|
|
543
|
-
return new Promise(function(
|
|
557
|
+
return new Promise(function(resolve2, reject) {
|
|
544
558
|
var gen = fn.apply(self, args);
|
|
545
559
|
function _next(value) {
|
|
546
|
-
asyncGeneratorStep(gen,
|
|
560
|
+
asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "next", value);
|
|
547
561
|
}
|
|
548
562
|
function _throw(err) {
|
|
549
|
-
asyncGeneratorStep(gen,
|
|
563
|
+
asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "throw", err);
|
|
550
564
|
}
|
|
551
565
|
_next(undefined);
|
|
552
566
|
});
|
|
@@ -1260,7 +1274,7 @@ var require_dateparts = __commonJS((exports, module) => {
|
|
|
1260
1274
|
|
|
1261
1275
|
// node_modules/prompts/dist/elements/date.js
|
|
1262
1276
|
var require_date = __commonJS((exports, module) => {
|
|
1263
|
-
function asyncGeneratorStep(gen,
|
|
1277
|
+
function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
|
|
1264
1278
|
try {
|
|
1265
1279
|
var info = gen[key](arg);
|
|
1266
1280
|
var value = info.value;
|
|
@@ -1269,7 +1283,7 @@ var require_date = __commonJS((exports, module) => {
|
|
|
1269
1283
|
return;
|
|
1270
1284
|
}
|
|
1271
1285
|
if (info.done) {
|
|
1272
|
-
|
|
1286
|
+
resolve2(value);
|
|
1273
1287
|
} else {
|
|
1274
1288
|
Promise.resolve(value).then(_next, _throw);
|
|
1275
1289
|
}
|
|
@@ -1277,13 +1291,13 @@ var require_date = __commonJS((exports, module) => {
|
|
|
1277
1291
|
function _asyncToGenerator(fn) {
|
|
1278
1292
|
return function() {
|
|
1279
1293
|
var self = this, args = arguments;
|
|
1280
|
-
return new Promise(function(
|
|
1294
|
+
return new Promise(function(resolve2, reject) {
|
|
1281
1295
|
var gen = fn.apply(self, args);
|
|
1282
1296
|
function _next(value) {
|
|
1283
|
-
asyncGeneratorStep(gen,
|
|
1297
|
+
asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "next", value);
|
|
1284
1298
|
}
|
|
1285
1299
|
function _throw(err) {
|
|
1286
|
-
asyncGeneratorStep(gen,
|
|
1300
|
+
asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "throw", err);
|
|
1287
1301
|
}
|
|
1288
1302
|
_next(undefined);
|
|
1289
1303
|
});
|
|
@@ -1487,7 +1501,7 @@ ${i ? ` ` : figures.pointerSmall} ${color.red().italic(l)}`, ``);
|
|
|
1487
1501
|
|
|
1488
1502
|
// node_modules/prompts/dist/elements/number.js
|
|
1489
1503
|
var require_number = __commonJS((exports, module) => {
|
|
1490
|
-
function asyncGeneratorStep(gen,
|
|
1504
|
+
function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
|
|
1491
1505
|
try {
|
|
1492
1506
|
var info = gen[key](arg);
|
|
1493
1507
|
var value = info.value;
|
|
@@ -1496,7 +1510,7 @@ var require_number = __commonJS((exports, module) => {
|
|
|
1496
1510
|
return;
|
|
1497
1511
|
}
|
|
1498
1512
|
if (info.done) {
|
|
1499
|
-
|
|
1513
|
+
resolve2(value);
|
|
1500
1514
|
} else {
|
|
1501
1515
|
Promise.resolve(value).then(_next, _throw);
|
|
1502
1516
|
}
|
|
@@ -1504,13 +1518,13 @@ var require_number = __commonJS((exports, module) => {
|
|
|
1504
1518
|
function _asyncToGenerator(fn) {
|
|
1505
1519
|
return function() {
|
|
1506
1520
|
var self = this, args = arguments;
|
|
1507
|
-
return new Promise(function(
|
|
1521
|
+
return new Promise(function(resolve2, reject) {
|
|
1508
1522
|
var gen = fn.apply(self, args);
|
|
1509
1523
|
function _next(value) {
|
|
1510
|
-
asyncGeneratorStep(gen,
|
|
1524
|
+
asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "next", value);
|
|
1511
1525
|
}
|
|
1512
1526
|
function _throw(err) {
|
|
1513
|
-
asyncGeneratorStep(gen,
|
|
1527
|
+
asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "throw", err);
|
|
1514
1528
|
}
|
|
1515
1529
|
_next(undefined);
|
|
1516
1530
|
});
|
|
@@ -1946,7 +1960,7 @@ Instructions:
|
|
|
1946
1960
|
|
|
1947
1961
|
// node_modules/prompts/dist/elements/autocomplete.js
|
|
1948
1962
|
var require_autocomplete = __commonJS((exports, module) => {
|
|
1949
|
-
function asyncGeneratorStep(gen,
|
|
1963
|
+
function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
|
|
1950
1964
|
try {
|
|
1951
1965
|
var info = gen[key](arg);
|
|
1952
1966
|
var value = info.value;
|
|
@@ -1955,7 +1969,7 @@ var require_autocomplete = __commonJS((exports, module) => {
|
|
|
1955
1969
|
return;
|
|
1956
1970
|
}
|
|
1957
1971
|
if (info.done) {
|
|
1958
|
-
|
|
1972
|
+
resolve2(value);
|
|
1959
1973
|
} else {
|
|
1960
1974
|
Promise.resolve(value).then(_next, _throw);
|
|
1961
1975
|
}
|
|
@@ -1963,13 +1977,13 @@ var require_autocomplete = __commonJS((exports, module) => {
|
|
|
1963
1977
|
function _asyncToGenerator(fn) {
|
|
1964
1978
|
return function() {
|
|
1965
1979
|
var self = this, args = arguments;
|
|
1966
|
-
return new Promise(function(
|
|
1980
|
+
return new Promise(function(resolve2, reject) {
|
|
1967
1981
|
var gen = fn.apply(self, args);
|
|
1968
1982
|
function _next(value) {
|
|
1969
|
-
asyncGeneratorStep(gen,
|
|
1983
|
+
asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "next", value);
|
|
1970
1984
|
}
|
|
1971
1985
|
function _throw(err) {
|
|
1972
|
-
asyncGeneratorStep(gen,
|
|
1986
|
+
asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "throw", err);
|
|
1973
1987
|
}
|
|
1974
1988
|
_next(undefined);
|
|
1975
1989
|
});
|
|
@@ -2575,7 +2589,7 @@ var require_dist = __commonJS((exports, module) => {
|
|
|
2575
2589
|
if (it)
|
|
2576
2590
|
o = it;
|
|
2577
2591
|
var i = 0;
|
|
2578
|
-
var F = function
|
|
2592
|
+
var F = function F2() {};
|
|
2579
2593
|
return { s: F, n: function n() {
|
|
2580
2594
|
if (i >= o.length)
|
|
2581
2595
|
return { done: true };
|
|
@@ -2627,7 +2641,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
|
|
|
2627
2641
|
arr2[i] = arr[i];
|
|
2628
2642
|
return arr2;
|
|
2629
2643
|
}
|
|
2630
|
-
function asyncGeneratorStep(gen,
|
|
2644
|
+
function asyncGeneratorStep(gen, resolve2, reject, _next, _throw, key, arg) {
|
|
2631
2645
|
try {
|
|
2632
2646
|
var info = gen[key](arg);
|
|
2633
2647
|
var value = info.value;
|
|
@@ -2636,7 +2650,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
|
|
|
2636
2650
|
return;
|
|
2637
2651
|
}
|
|
2638
2652
|
if (info.done) {
|
|
2639
|
-
|
|
2653
|
+
resolve2(value);
|
|
2640
2654
|
} else {
|
|
2641
2655
|
Promise.resolve(value).then(_next, _throw);
|
|
2642
2656
|
}
|
|
@@ -2644,13 +2658,13 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
|
|
|
2644
2658
|
function _asyncToGenerator(fn) {
|
|
2645
2659
|
return function() {
|
|
2646
2660
|
var self = this, args = arguments;
|
|
2647
|
-
return new Promise(function(
|
|
2661
|
+
return new Promise(function(resolve2, reject) {
|
|
2648
2662
|
var gen = fn.apply(self, args);
|
|
2649
2663
|
function _next(value) {
|
|
2650
|
-
asyncGeneratorStep(gen,
|
|
2664
|
+
asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "next", value);
|
|
2651
2665
|
}
|
|
2652
2666
|
function _throw(err) {
|
|
2653
|
-
asyncGeneratorStep(gen,
|
|
2667
|
+
asyncGeneratorStep(gen, resolve2, reject, _next, _throw, "throw", err);
|
|
2654
2668
|
}
|
|
2655
2669
|
_next(undefined);
|
|
2656
2670
|
});
|
|
@@ -2678,7 +2692,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
|
|
|
2678
2692
|
}
|
|
2679
2693
|
return question2.format ? yield question2.format(answer2, answers) : answer2;
|
|
2680
2694
|
});
|
|
2681
|
-
return function
|
|
2695
|
+
return function getFormattedAnswer2(_x, _x2) {
|
|
2682
2696
|
return _ref.apply(this, arguments);
|
|
2683
2697
|
};
|
|
2684
2698
|
}();
|
|
@@ -4937,31 +4951,515 @@ var require_prompts3 = __commonJS((exports, module) => {
|
|
|
4937
4951
|
});
|
|
4938
4952
|
|
|
4939
4953
|
// src/index.ts
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4954
|
+
import path10 from "path";
|
|
4955
|
+
|
|
4956
|
+
// src/cli/completion.ts
|
|
4957
|
+
import path2 from "path";
|
|
4958
|
+
|
|
4959
|
+
// src/core/page-catalog.ts
|
|
4943
4960
|
import path from "path";
|
|
4944
|
-
|
|
4945
|
-
|
|
4961
|
+
function isRouteGroup(segment) {
|
|
4962
|
+
return segment.startsWith("(") && segment.endsWith(")");
|
|
4963
|
+
}
|
|
4964
|
+
async function discoverPages(projectRoot, fs) {
|
|
4965
|
+
const appRoot = path.join(projectRoot, "src", "app", "[locale]");
|
|
4966
|
+
const candidates = [];
|
|
4967
|
+
async function visit(directory, relative = []) {
|
|
4968
|
+
let entries;
|
|
4969
|
+
try {
|
|
4970
|
+
entries = await fs.list(directory);
|
|
4971
|
+
} catch {
|
|
4972
|
+
return;
|
|
4973
|
+
}
|
|
4974
|
+
if (entries.some((entry) => entry.isFile && entry.name === "page.tsx")) {
|
|
4975
|
+
const routeSegments = relative.filter((segment) => !isRouteGroup(segment));
|
|
4976
|
+
if (routeSegments.length > 0 && !routeSegments.some((segment) => segment.startsWith("_") || segment.startsWith("["))) {
|
|
4977
|
+
const logicalName = routeSegments.join(".");
|
|
4978
|
+
candidates.push({
|
|
4979
|
+
logicalName,
|
|
4980
|
+
routeSegments,
|
|
4981
|
+
routeDirectory: directory,
|
|
4982
|
+
uiDirectory: path.join(projectRoot, "src", "ui", ...routeSegments),
|
|
4983
|
+
messageFile: path.join(projectRoot, "messages", "{locale}", `${routeSegments[0]}.json`),
|
|
4984
|
+
messageKey: routeSegments.length > 1 ? routeSegments.at(-1) : undefined
|
|
4985
|
+
});
|
|
4986
|
+
}
|
|
4987
|
+
}
|
|
4988
|
+
for (const entry of entries) {
|
|
4989
|
+
if (entry.isDirectory && !entry.name.startsWith(".")) {
|
|
4990
|
+
await visit(path.join(directory, entry.name), [
|
|
4991
|
+
...relative,
|
|
4992
|
+
entry.name
|
|
4993
|
+
]);
|
|
4994
|
+
}
|
|
4995
|
+
}
|
|
4996
|
+
}
|
|
4997
|
+
await visit(appRoot);
|
|
4998
|
+
return candidates.sort((left, right) => left.logicalName.localeCompare(right.logicalName));
|
|
4999
|
+
}
|
|
4946
5000
|
|
|
4947
|
-
// src/
|
|
4948
|
-
var
|
|
5001
|
+
// src/cli/completion.ts
|
|
5002
|
+
var PUBLIC_COMMANDS = [
|
|
5003
|
+
"addpage",
|
|
5004
|
+
"addcomponent",
|
|
5005
|
+
"addlib",
|
|
5006
|
+
"addapi",
|
|
5007
|
+
"addlanguage",
|
|
5008
|
+
"addtext",
|
|
5009
|
+
"rmpage",
|
|
5010
|
+
"--help",
|
|
5011
|
+
"--version",
|
|
5012
|
+
"--json",
|
|
5013
|
+
"--reconfigure"
|
|
5014
|
+
];
|
|
5015
|
+
var OPTIONS = {
|
|
5016
|
+
addpage: [
|
|
5017
|
+
"--layout",
|
|
5018
|
+
"--page",
|
|
5019
|
+
"--loading",
|
|
5020
|
+
"--not-found",
|
|
5021
|
+
"--error",
|
|
5022
|
+
"--global-error",
|
|
5023
|
+
"--route",
|
|
5024
|
+
"--template",
|
|
5025
|
+
"--default"
|
|
5026
|
+
],
|
|
5027
|
+
addcomponent: ["--page", "-P"]
|
|
5028
|
+
};
|
|
5029
|
+
async function directories(root, context) {
|
|
5030
|
+
try {
|
|
5031
|
+
return (await context.fs.list(root)).filter((entry) => entry.isDirectory && !entry.name.startsWith("_")).map((entry) => entry.name).sort();
|
|
5032
|
+
} catch {
|
|
5033
|
+
return [];
|
|
5034
|
+
}
|
|
5035
|
+
}
|
|
5036
|
+
async function completionCandidates(command, context) {
|
|
5037
|
+
if (!command)
|
|
5038
|
+
return [...PUBLIC_COMMANDS];
|
|
5039
|
+
if (command === "rmpage") {
|
|
5040
|
+
return (await discoverPages(context.cwd, context.fs)).map((candidate) => candidate.logicalName);
|
|
5041
|
+
}
|
|
5042
|
+
if (command === "addcomponent" || command === "addpage") {
|
|
5043
|
+
return [
|
|
5044
|
+
...OPTIONS[command] ?? [],
|
|
5045
|
+
...await directories(path2.join(context.cwd, "src", "ui"), context)
|
|
5046
|
+
];
|
|
5047
|
+
}
|
|
5048
|
+
if (command === "addlanguage")
|
|
5049
|
+
return ["de", "en", "es", "fr", "it", "ja", "pt"];
|
|
5050
|
+
return OPTIONS[command] ?? [];
|
|
5051
|
+
}
|
|
5052
|
+
|
|
5053
|
+
// src/cli/onboarding.ts
|
|
5054
|
+
import path4 from "path";
|
|
5055
|
+
|
|
5056
|
+
// src/core/operations.ts
|
|
5057
|
+
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
|
+
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
|
+
}
|
|
5439
|
+
}
|
|
5440
|
+
}
|
|
5441
|
+
|
|
5442
|
+
// src/lib/addApi.ts
|
|
4949
5443
|
import { join as join2 } from "path";
|
|
4950
|
-
import { mkdir, readFile as readFile2, writeFile, readdir } from "fs/promises";
|
|
4951
5444
|
|
|
4952
5445
|
// src/lib/utils.ts
|
|
4953
|
-
import { readFile } from "fs/promises";
|
|
4954
|
-
import { existsSync } from "fs";
|
|
4955
5446
|
import { join } from "path";
|
|
4956
5447
|
function capitalize(str) {
|
|
4957
5448
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
4958
5449
|
}
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
5450
|
+
function toIdentifier(value) {
|
|
5451
|
+
return value.replace(/[-_]+([A-Za-z0-9])/g, (_, character) => character.toUpperCase());
|
|
5452
|
+
}
|
|
5453
|
+
function configuredAliasPrefix(config) {
|
|
5454
|
+
const alias = config.importAlias ?? "@/*";
|
|
5455
|
+
return alias.endsWith("/*") ? alias.slice(0, -2) : "@";
|
|
5456
|
+
}
|
|
5457
|
+
async function loadConfig(context) {
|
|
5458
|
+
const configPath = join(context.cwd, "cnp.config.json");
|
|
5459
|
+
if (!context.fs.exists(configPath))
|
|
4962
5460
|
return null;
|
|
4963
5461
|
try {
|
|
4964
|
-
const raw = await
|
|
5462
|
+
const raw = await context.fs.readText(configPath);
|
|
4965
5463
|
return JSON.parse(raw);
|
|
4966
5464
|
} catch {
|
|
4967
5465
|
return null;
|
|
@@ -4992,533 +5490,1573 @@ function toFileName(key) {
|
|
|
4992
5490
|
}
|
|
4993
5491
|
}
|
|
4994
5492
|
|
|
4995
|
-
// src/
|
|
4996
|
-
import
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5493
|
+
// src/core/project-paths.ts
|
|
5494
|
+
import path6 from "path";
|
|
5495
|
+
var SAFE_SEGMENT = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
5496
|
+
function hasControlCharacters(value) {
|
|
5497
|
+
return [...value].some((character) => {
|
|
5498
|
+
const code = character.charCodeAt(0);
|
|
5499
|
+
return code <= 31 || code === 127;
|
|
5500
|
+
});
|
|
5501
|
+
}
|
|
5502
|
+
function parseLogicalName(value, label = "name") {
|
|
5503
|
+
if (!value || hasControlCharacters(value)) {
|
|
5504
|
+
throw new CliError(`Invalid ${label}: a non-empty printable value is required.`, { code: "INVALID_ARGUMENT" });
|
|
5003
5505
|
}
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
nestedPath = join2(...pageScope.split("."));
|
|
5506
|
+
if (path6.isAbsolute(value) || value.includes("/") || value.includes("\\")) {
|
|
5507
|
+
throw new CliError(`Invalid ${label}: paths and separators are not allowed.`, { code: "INVALID_ARGUMENT" });
|
|
5007
5508
|
}
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
name: "componentName",
|
|
5012
|
-
message: "\uD83E\uDDE9 Component name to add:",
|
|
5013
|
-
validate: (name) => name ? true : "Component name is required"
|
|
5014
|
-
});
|
|
5015
|
-
componentName = response.componentName;
|
|
5509
|
+
const segments = value.split(".");
|
|
5510
|
+
if (segments.some((segment) => !SAFE_SEGMENT.test(segment))) {
|
|
5511
|
+
throw new CliError(`Invalid ${label}: use dot-separated alphanumeric segments beginning with a letter.`, { code: "INVALID_ARGUMENT" });
|
|
5016
5512
|
}
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5513
|
+
return segments;
|
|
5514
|
+
}
|
|
5515
|
+
function validateProjectName(value) {
|
|
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" });
|
|
5021
5518
|
}
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
console.error("\u274C Messages directory missing. Ensure i18n was configured.");
|
|
5030
|
-
return;
|
|
5031
|
-
}
|
|
5519
|
+
return value;
|
|
5520
|
+
}
|
|
5521
|
+
function resolveInside(root, ...segments) {
|
|
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" });
|
|
5032
5526
|
}
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
}
|
|
5047
|
-
if (!existsSync2(componentTargetPath)) {
|
|
5048
|
-
await mkdir(componentTargetPath, { recursive: true });
|
|
5049
|
-
}
|
|
5050
|
-
const componentFile = join2(componentTargetPath, `${componentNameUpper}.tsx`);
|
|
5051
|
-
const templateComponentPath = join2(templatePath, "Component.tsx");
|
|
5052
|
-
if (existsSync2(templateComponentPath)) {
|
|
5053
|
-
let content = await readFile2(templateComponentPath, "utf-8");
|
|
5054
|
-
content = content.replace(/Component/g, componentNameUpper).replace(/componentPage/g, translationKey);
|
|
5055
|
-
await writeFile(componentFile, content);
|
|
5056
|
-
console.log(`\uD83D\uDCC4 File created: ${componentFile}`);
|
|
5057
|
-
} else {
|
|
5058
|
-
console.error("\u274C Template Component.tsx introuvable :", templateComponentPath);
|
|
5059
|
-
}
|
|
5060
|
-
if (useI18n && messagesPath) {
|
|
5061
|
-
const entries = await readdir(messagesPath, { withFileTypes: true });
|
|
5062
|
-
const langDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
5063
|
-
const jsonTemplate = join2(templatePath, "component.json");
|
|
5064
|
-
if (!existsSync2(jsonTemplate)) {
|
|
5065
|
-
console.error("\u274C Template component.json not found:", jsonTemplate);
|
|
5066
|
-
return;
|
|
5067
|
-
}
|
|
5068
|
-
const jsonContent = await readFile2(jsonTemplate, "utf-8");
|
|
5069
|
-
const parsed = JSON.parse(jsonContent);
|
|
5070
|
-
for (const locale of langDirs) {
|
|
5071
|
-
const localeDir = join2(messagesPath, locale);
|
|
5072
|
-
if (!existsSync2(localeDir) || !statSync(localeDir).isDirectory())
|
|
5073
|
-
continue;
|
|
5074
|
-
let jsonTarget;
|
|
5075
|
-
if (pageScope) {
|
|
5076
|
-
jsonTarget = join2(messagesPath, locale, `${pageScope}.json`);
|
|
5077
|
-
} else {
|
|
5078
|
-
jsonTarget = join2(messagesPath, locale, `_global_ui.json`);
|
|
5079
|
-
}
|
|
5080
|
-
let current = {};
|
|
5081
|
-
if (existsSync2(jsonTarget)) {
|
|
5082
|
-
const jsonFile = await readFile2(jsonTarget, "utf-8");
|
|
5083
|
-
current = JSON.parse(jsonFile);
|
|
5084
|
-
}
|
|
5085
|
-
current[componentNameUpper] = parsed;
|
|
5086
|
-
await writeFile(jsonTarget, JSON.stringify(current, null, 2));
|
|
5087
|
-
console.log(`\uD83D\uDCC4 File updated: ${jsonTarget}`);
|
|
5527
|
+
return target;
|
|
5528
|
+
}
|
|
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);
|
|
5533
|
+
for (const segment of relativeSegments) {
|
|
5534
|
+
if (!segment)
|
|
5535
|
+
continue;
|
|
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" });
|
|
5088
5540
|
}
|
|
5089
|
-
} else {
|
|
5090
|
-
console.log("\u2139\uFE0F Skipping translation entries; next-intl not enabled.");
|
|
5091
5541
|
}
|
|
5092
|
-
|
|
5542
|
+
return safeTarget;
|
|
5543
|
+
}
|
|
5544
|
+
function normalizeImportAlias(value) {
|
|
5545
|
+
if (!/^[A-Za-z@~][A-Za-z0-9@~_-]*\/\*$/.test(value)) {
|
|
5546
|
+
throw new CliError('Invalid import alias: expected a prefix followed by "/*" (for example "@/*" or "@core/*").', { code: "INVALID_ARGUMENT" });
|
|
5547
|
+
}
|
|
5548
|
+
return value;
|
|
5093
5549
|
}
|
|
5094
5550
|
|
|
5095
|
-
// src/lib/
|
|
5096
|
-
var
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5551
|
+
// src/lib/addApi.ts
|
|
5552
|
+
var addApi = async (args, context) => {
|
|
5553
|
+
let apiName = args[1];
|
|
5554
|
+
if (!apiName || apiName.startsWith("-")) {
|
|
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({
|
|
5104
5562
|
type: "text",
|
|
5105
|
-
name: "
|
|
5106
|
-
message: "
|
|
5107
|
-
validate: (name) => name ? true : "
|
|
5563
|
+
name: "apiName",
|
|
5564
|
+
message: "API route name to add:",
|
|
5565
|
+
validate: (name) => name ? true : "API route name is required"
|
|
5108
5566
|
});
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
switch (char) {
|
|
5125
|
-
case "L":
|
|
5126
|
-
flags.add("layout");
|
|
5127
|
-
break;
|
|
5128
|
-
case "P":
|
|
5129
|
-
flags.add("page");
|
|
5130
|
-
break;
|
|
5131
|
-
case "l":
|
|
5132
|
-
flags.add("loading");
|
|
5133
|
-
break;
|
|
5134
|
-
case "n":
|
|
5135
|
-
flags.add("not-found");
|
|
5136
|
-
break;
|
|
5137
|
-
case "e":
|
|
5138
|
-
flags.add("error");
|
|
5139
|
-
break;
|
|
5140
|
-
case "g":
|
|
5141
|
-
flags.add("global-error");
|
|
5142
|
-
break;
|
|
5143
|
-
case "r":
|
|
5144
|
-
flags.add("route");
|
|
5145
|
-
break;
|
|
5146
|
-
case "t":
|
|
5147
|
-
flags.add("template");
|
|
5148
|
-
break;
|
|
5149
|
-
case "d":
|
|
5150
|
-
flags.add("default");
|
|
5151
|
-
break;
|
|
5152
|
-
}
|
|
5153
|
-
}
|
|
5154
|
-
}
|
|
5155
|
-
for (const flag of [
|
|
5156
|
-
"layout",
|
|
5157
|
-
"page",
|
|
5158
|
-
"loading",
|
|
5159
|
-
"not-found",
|
|
5160
|
-
"error",
|
|
5161
|
-
"global-error",
|
|
5162
|
-
"route",
|
|
5163
|
-
"template",
|
|
5164
|
-
"default"
|
|
5165
|
-
]) {
|
|
5166
|
-
if (longFlags.has("--" + flag))
|
|
5167
|
-
flags.add(flag);
|
|
5168
|
-
}
|
|
5169
|
-
const config = await loadConfig();
|
|
5170
|
-
if (!config) {
|
|
5171
|
-
console.error("\u274C Configuration file cnp.config.json not found. Run this command from the project root.");
|
|
5172
|
-
return;
|
|
5173
|
-
}
|
|
5174
|
-
const useI18n = !!config.useI18n;
|
|
5175
|
-
const srcSegments = ["src", "app"];
|
|
5176
|
-
if (useI18n)
|
|
5177
|
-
srcSegments.push("[locale]");
|
|
5178
|
-
const srcPath = join3(process.cwd(), ...srcSegments);
|
|
5179
|
-
if (!existsSync3(srcPath)) {
|
|
5180
|
-
console.error(`\u274C Expected directory not found: ${srcPath}`);
|
|
5181
|
-
return;
|
|
5182
|
-
}
|
|
5183
|
-
let messagesPath = null;
|
|
5184
|
-
let locales = [];
|
|
5185
|
-
if (useI18n) {
|
|
5186
|
-
messagesPath = join3(process.cwd(), "messages");
|
|
5187
|
-
if (!existsSync3(messagesPath)) {
|
|
5188
|
-
console.error("\u274C Messages directory missing. Ensure i18n was configured.");
|
|
5189
|
-
return;
|
|
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
|
+
});
|
|
5190
5582
|
}
|
|
5191
|
-
const entries = await readdir2(messagesPath, { withFileTypes: true });
|
|
5192
|
-
locales = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
5193
|
-
}
|
|
5194
|
-
const templatePath = join3(new URL("..", import.meta.url).pathname, "templates", "Page");
|
|
5195
|
-
let uiPageDir, localePagePath, jsonFileName;
|
|
5196
|
-
if (parentName && childName) {
|
|
5197
|
-
uiPageDir = join3(process.cwd(), "src", "ui", parentName, childName);
|
|
5198
|
-
localePagePath = join3(srcPath, parentName, childName);
|
|
5199
|
-
jsonFileName = parentName;
|
|
5200
|
-
} else {
|
|
5201
|
-
uiPageDir = join3(process.cwd(), "src", "ui", pageName);
|
|
5202
|
-
localePagePath = join3(srcPath, pageName);
|
|
5203
|
-
jsonFileName = pageName;
|
|
5204
|
-
}
|
|
5205
|
-
if (!existsSync3(uiPageDir)) {
|
|
5206
|
-
await mkdir2(uiPageDir, { recursive: true });
|
|
5207
|
-
}
|
|
5208
|
-
const uiPageFile = join3(uiPageDir, "page-ui.tsx");
|
|
5209
|
-
const uiPageTemplate = join3(templatePath, "page-ui.tsx");
|
|
5210
|
-
if (existsSync3(uiPageTemplate)) {
|
|
5211
|
-
let uiContent = await readFile3(uiPageTemplate, "utf-8");
|
|
5212
|
-
uiContent = uiContent.replace(/template/g, childName || pageName).replace(/Template/g, capitalize(childName || pageName));
|
|
5213
|
-
await writeFile2(uiPageFile, uiContent);
|
|
5214
|
-
console.log(`\uD83D\uDCC4 File created: ${uiPageFile}`);
|
|
5215
|
-
} else {
|
|
5216
|
-
console.warn("\u26A0\uFE0F Missing template file: page-ui.tsx at path:", uiPageTemplate);
|
|
5217
5583
|
}
|
|
5218
|
-
|
|
5219
|
-
|
|
5584
|
+
const apiSegments = parseLogicalName(apiName, "API route name");
|
|
5585
|
+
const config = await loadConfig(context);
|
|
5586
|
+
if (!config) {
|
|
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
|
+
});
|
|
5220
5591
|
}
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
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");
|
|
5596
|
+
const routeTemplate = join2(templateDir, "route.ts");
|
|
5597
|
+
const routePath = join2(apiDir, "route.ts");
|
|
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
|
+
});
|
|
5233
5604
|
}
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
await writeFile2(jsonTarget, JSON.stringify(current, null, 2));
|
|
5261
|
-
console.log(`\uD83D\uDCC4 File created: ${jsonTarget}`);
|
|
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"]
|
|
5262
5631
|
}
|
|
5263
|
-
|
|
5264
|
-
}
|
|
5265
|
-
|
|
5266
|
-
}
|
|
5267
|
-
console.log(`\u2705 Page "${pageName}" with templates added${useI18n ? " for each locale" : ""}.`);
|
|
5268
|
-
}
|
|
5632
|
+
]
|
|
5633
|
+
});
|
|
5634
|
+
};
|
|
5269
5635
|
|
|
5270
|
-
// src/lib/
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
type: "text",
|
|
5280
|
-
name: "pageName",
|
|
5281
|
-
message: "\uD83D\uDDD1\uFE0F Page name to remove:",
|
|
5282
|
-
validate: (name) => name ? true : "Page name is required"
|
|
5636
|
+
// src/lib/addComponent.ts
|
|
5637
|
+
import { join as join3 } from "path";
|
|
5638
|
+
var addComponent = async (args, context) => {
|
|
5639
|
+
let componentName = args[1];
|
|
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"
|
|
5283
5645
|
});
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
await import("child_process").then((cp) => cp.execSync(`rm -f '${jsonTarget}'`));
|
|
5294
|
-
console.log(`\uD83D\uDDD1\uFE0F Deleted: ${jsonTarget}`);
|
|
5295
|
-
}
|
|
5296
|
-
}
|
|
5297
|
-
const uiPageDir = join4(process.cwd(), "src", "ui", pageName);
|
|
5298
|
-
if (existsSync4(uiPageDir)) {
|
|
5299
|
-
await import("child_process").then((cp) => cp.execSync(`rm -rf '${uiPageDir}'`));
|
|
5300
|
-
console.log(`\uD83D\uDDD1\uFE0F Deleted: ${uiPageDir}`);
|
|
5301
|
-
}
|
|
5302
|
-
const appLocaleDir = join4(process.cwd(), "src", "app", "[locale]", pageName);
|
|
5303
|
-
if (existsSync4(appLocaleDir)) {
|
|
5304
|
-
await import("child_process").then((cp) => cp.execSync(`rm -rf '${appLocaleDir}'`));
|
|
5305
|
-
console.log(`\uD83D\uDDD1\uFE0F Deleted: ${appLocaleDir}`);
|
|
5306
|
-
}
|
|
5307
|
-
console.log(`\u2705 Page "${pageName}" deleted.`);
|
|
5308
|
-
}
|
|
5309
|
-
|
|
5310
|
-
// src/lib/addLib.ts
|
|
5311
|
-
var import_prompts4 = __toESM(require_prompts3(), 1);
|
|
5312
|
-
import { join as join5 } from "path";
|
|
5313
|
-
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
5314
|
-
import { existsSync as existsSync5 } from "fs";
|
|
5315
|
-
async function addLib(args) {
|
|
5316
|
-
let libArg = args[1];
|
|
5317
|
-
if (!libArg || libArg.startsWith("-")) {
|
|
5318
|
-
const response = await import_prompts4.default.prompt({
|
|
5646
|
+
}
|
|
5647
|
+
if (!componentName || componentName.startsWith("-")) {
|
|
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({
|
|
5319
5655
|
type: "text",
|
|
5320
|
-
name: "
|
|
5321
|
-
message: "
|
|
5322
|
-
validate: (name) => name ? true : "
|
|
5656
|
+
name: "componentName",
|
|
5657
|
+
message: "Component name to add:",
|
|
5658
|
+
validate: (name) => name ? true : "Component name is required"
|
|
5323
5659
|
});
|
|
5324
|
-
|
|
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
|
+
}
|
|
5325
5676
|
}
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
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
|
+
});
|
|
5330
5682
|
}
|
|
5331
|
-
const
|
|
5683
|
+
const pageSegments = pageScope ? parseLogicalName(pageScope, "page name") : [];
|
|
5684
|
+
const config = await loadConfig(context);
|
|
5332
5685
|
if (!config) {
|
|
5333
|
-
|
|
5334
|
-
|
|
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
|
+
});
|
|
5335
5690
|
}
|
|
5336
|
-
const
|
|
5337
|
-
|
|
5338
|
-
|
|
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
|
+
});
|
|
5339
5701
|
}
|
|
5340
|
-
const
|
|
5341
|
-
|
|
5342
|
-
const
|
|
5343
|
-
const
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
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
|
+
});
|
|
5351
5716
|
}
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
await writeFile4(filePath, content);
|
|
5361
|
-
} else {
|
|
5362
|
-
await writeFile4(filePath, `export function ${fileName}() {
|
|
5363
|
-
// TODO: implement
|
|
5364
|
-
}
|
|
5365
|
-
`);
|
|
5366
|
-
}
|
|
5367
|
-
console.log(`\uD83D\uDCC4 File created: ${filePath}`);
|
|
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
|
+
});
|
|
5368
5725
|
}
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
exportsSet.push(...match[1].split(",").map((s) => s.trim()).filter(Boolean));
|
|
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
|
+
});
|
|
5383
5739
|
}
|
|
5384
5740
|
}
|
|
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
|
+
});
|
|
5385
5766
|
}
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5767
|
+
}
|
|
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
|
+
});
|
|
5391
5794
|
}
|
|
5392
|
-
indexContent = imports.join(`
|
|
5393
|
-
`) + `
|
|
5394
|
-
|
|
5395
|
-
export { ` + exportsSet.join(", ") + ` };
|
|
5396
|
-
`;
|
|
5397
|
-
await writeFile4(indexPath, indexContent);
|
|
5398
|
-
console.log(`\u270F\uFE0F Updated index: ${indexPath}`);
|
|
5399
5795
|
}
|
|
5400
|
-
|
|
5401
|
-
|
|
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
|
+
};
|
|
5402
5824
|
|
|
5403
|
-
// src/lib/
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5825
|
+
// src/lib/addLanguage.ts
|
|
5826
|
+
import { join as join4 } from "path";
|
|
5827
|
+
function generateLocales() {
|
|
5828
|
+
const names = new Intl.DisplayNames(["en"], { type: "language" });
|
|
5829
|
+
const locales = [];
|
|
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);
|
|
5833
|
+
try {
|
|
5834
|
+
const name = names.of(code);
|
|
5835
|
+
if (name && name.toLowerCase() !== code)
|
|
5836
|
+
locales.push(code);
|
|
5837
|
+
} catch {}
|
|
5838
|
+
}
|
|
5839
|
+
}
|
|
5840
|
+
return locales.sort();
|
|
5841
|
+
}
|
|
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);
|
|
5865
|
+
if (!config?.useI18n) {
|
|
5866
|
+
throw new CliError("Internationalization is not enabled in this project.", {
|
|
5867
|
+
code: "I18N_DISABLED"
|
|
5416
5868
|
});
|
|
5417
|
-
apiName = response.apiName;
|
|
5418
5869
|
}
|
|
5419
|
-
const
|
|
5420
|
-
if (!
|
|
5421
|
-
|
|
5422
|
-
|
|
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
|
+
});
|
|
5423
5877
|
}
|
|
5424
|
-
const
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5878
|
+
const available = generateLocales();
|
|
5879
|
+
let locale = args[1];
|
|
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({
|
|
5888
|
+
type: "autocomplete",
|
|
5889
|
+
name: "locale",
|
|
5890
|
+
message: "Locale to add:",
|
|
5891
|
+
choices: available.map((value) => ({ title: value, value }))
|
|
5892
|
+
});
|
|
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
|
+
}
|
|
5909
|
+
}
|
|
5910
|
+
parseLogicalName(locale, "locale");
|
|
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
|
+
});
|
|
5916
|
+
}
|
|
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
|
+
});
|
|
5925
|
+
}
|
|
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
|
+
});
|
|
5934
|
+
}
|
|
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
|
+
});
|
|
5943
|
+
}
|
|
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
|
+
});
|
|
5950
|
+
}
|
|
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*\[([^\]]*)\]/);
|
|
5969
|
+
if (!localesMatch) {
|
|
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;/);
|
|
5979
|
+
if (!registryMatch) {
|
|
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 }
|
|
6041
|
+
});
|
|
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
|
+
};
|
|
6083
|
+
|
|
6084
|
+
// src/lib/addLib.ts
|
|
6085
|
+
import { join as join5 } from "path";
|
|
6086
|
+
var addLib = async (args, context) => {
|
|
6087
|
+
let libArg = args[1];
|
|
6088
|
+
if (!libArg || libArg.startsWith("-")) {
|
|
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({
|
|
6096
|
+
type: "text",
|
|
6097
|
+
name: "libArg",
|
|
6098
|
+
message: "Library name to add:",
|
|
6099
|
+
validate: (name) => name ? true : "Library name is required"
|
|
6100
|
+
});
|
|
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
|
+
}
|
|
6117
|
+
}
|
|
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
|
+
});
|
|
6123
|
+
}
|
|
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
|
+
});
|
|
6130
|
+
}
|
|
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");
|
|
6135
|
+
const indexTemplate = join5(templateDir, "index.ts");
|
|
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
|
+
});
|
|
6148
|
+
const indexPath = join5(libDir, "index.ts");
|
|
6149
|
+
if (!context.fs.exists(indexPath)) {
|
|
6150
|
+
await gateway.write(indexPath, await context.fs.readText(indexTemplate), {
|
|
6151
|
+
role: "library-index"
|
|
6152
|
+
});
|
|
6153
|
+
}
|
|
6154
|
+
let moduleAction;
|
|
6155
|
+
if (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}["'];`);
|
|
6166
|
+
const exportRegex = /export\s*{([^}]*)}/m;
|
|
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)))
|
|
6172
|
+
imports.push(importLine);
|
|
6173
|
+
if (!exportsSet.includes(moduleIdentifier))
|
|
6174
|
+
exportsSet.push(moduleIdentifier);
|
|
6175
|
+
const nextIndex = `${imports.join(`
|
|
6176
|
+
`)}
|
|
6177
|
+
|
|
6178
|
+
export { ${exportsSet.join(", ")} };
|
|
6179
|
+
`;
|
|
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
|
+
});
|
|
6198
|
+
}
|
|
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
|
+
};
|
|
5438
6215
|
|
|
5439
|
-
|
|
5440
|
-
|
|
6216
|
+
// src/lib/addPage.ts
|
|
6217
|
+
import { join as join6 } from "path";
|
|
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
|
+
};
|
|
6240
|
+
function registerMessagesFile(content, locale, fileName) {
|
|
6241
|
+
const importPath = `./${locale}/${fileName}.json`;
|
|
6242
|
+
if (content.includes(importPath))
|
|
6243
|
+
return content;
|
|
6244
|
+
const declarationIndex = content.indexOf("const messages =");
|
|
6245
|
+
const registryMatch = content.match(/const messages = \{([\s\S]*?)\n\};/);
|
|
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
|
+
});
|
|
6252
|
+
}
|
|
6253
|
+
const identifier = toIdentifier(fileName);
|
|
6254
|
+
const importStatement = `import ${identifier} from "${importPath}";
|
|
6255
|
+
`;
|
|
6256
|
+
const nextRegistry = registryMatch[0].replace(/\n\};$/, `
|
|
6257
|
+
${identifier},
|
|
6258
|
+
};`);
|
|
6259
|
+
return content.slice(0, declarationIndex) + importStatement + content.slice(declarationIndex).replace(registryMatch[0], nextRegistry);
|
|
5441
6260
|
}
|
|
5442
|
-
|
|
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({
|
|
6299
|
+
type: "text",
|
|
6300
|
+
name: "pageName",
|
|
6301
|
+
message: "Page name to add:",
|
|
6302
|
+
validate: (name) => name ? true : "Page name is required"
|
|
6303
|
+
});
|
|
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);
|
|
6329
|
+
if (!config) {
|
|
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
|
+
});
|
|
6334
|
+
}
|
|
6335
|
+
const useI18n = Boolean(config.useI18n);
|
|
6336
|
+
const aliasPrefix = configuredAliasPrefix(config);
|
|
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
|
+
];
|
|
6364
|
+
if (useI18n)
|
|
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
|
+
});
|
|
6373
|
+
}
|
|
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 = [];
|
|
6377
|
+
if (useI18n) {
|
|
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
|
+
});
|
|
6385
|
+
}
|
|
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
|
+
});
|
|
6393
|
+
}
|
|
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
|
+
});
|
|
6415
|
+
}
|
|
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
|
+
});
|
|
5443
6477
|
}
|
|
5444
|
-
console.log(`\uD83D\uDCC4 File created: ${routePath}`);
|
|
5445
6478
|
} else {
|
|
5446
|
-
|
|
6479
|
+
gateway.skipped(join6(context.cwd, "messages"), {
|
|
6480
|
+
role: "translation-messages",
|
|
6481
|
+
resource: "directory",
|
|
6482
|
+
detail: { reason: "Internationalization is disabled." }
|
|
6483
|
+
});
|
|
5447
6484
|
}
|
|
5448
|
-
|
|
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
|
+
};
|
|
6518
|
+
|
|
6519
|
+
// src/lib/addText.ts
|
|
6520
|
+
import { join as join7 } from "path";
|
|
6521
|
+
function defaultText(key) {
|
|
6522
|
+
return key.replace(/_/g, " ").replace(/\b\w/g, (character) => character.toUpperCase());
|
|
6523
|
+
}
|
|
6524
|
+
var addText = async (args, context) => {
|
|
6525
|
+
const pathArg = args[1];
|
|
6526
|
+
if (!pathArg) {
|
|
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
|
+
});
|
|
6537
|
+
}
|
|
6538
|
+
const providedText = args.slice(2).join(" ");
|
|
6539
|
+
const finalKey = segments.at(-1);
|
|
6540
|
+
const text = providedText || defaultText(finalKey);
|
|
6541
|
+
const config = await loadConfig(context);
|
|
6542
|
+
if (!config?.useI18n) {
|
|
6543
|
+
throw new CliError("Internationalization is not enabled in this project.", {
|
|
6544
|
+
code: "I18N_DISABLED"
|
|
6545
|
+
});
|
|
6546
|
+
}
|
|
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
|
+
});
|
|
6554
|
+
}
|
|
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
|
+
});
|
|
6562
|
+
}
|
|
6563
|
+
const [fileName, ...keySegments] = segments;
|
|
6564
|
+
const prepared = [];
|
|
6565
|
+
for (const locale of locales) {
|
|
6566
|
+
const file = join7(messagesRoot, locale, `${fileName}.json`);
|
|
6567
|
+
await assertSafeTarget(context.cwd, file, context.fs);
|
|
6568
|
+
const existed = context.fs.exists(file);
|
|
6569
|
+
let data = {};
|
|
6570
|
+
if (existed) {
|
|
6571
|
+
try {
|
|
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
|
+
}
|
|
6585
|
+
}
|
|
6586
|
+
let cursor = data;
|
|
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];
|
|
6599
|
+
}
|
|
6600
|
+
const previous = cursor[finalKey];
|
|
6601
|
+
cursor[finalKey] = text;
|
|
6602
|
+
prepared.push({
|
|
6603
|
+
locale,
|
|
6604
|
+
file,
|
|
6605
|
+
data,
|
|
6606
|
+
replaced: previous !== undefined && previous !== text
|
|
6607
|
+
});
|
|
6608
|
+
}
|
|
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
|
+
};
|
|
6649
|
+
|
|
6650
|
+
// src/lib/rmPage.ts
|
|
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
|
+
});
|
|
6669
|
+
}
|
|
6670
|
+
if (!hasImport)
|
|
6671
|
+
return { content, changed: false };
|
|
6672
|
+
return {
|
|
6673
|
+
content: content.replace(importPattern, "").replace(propertyPattern, ""),
|
|
6674
|
+
changed: true
|
|
6675
|
+
};
|
|
6676
|
+
}
|
|
6677
|
+
var rmPage = async (args, context) => {
|
|
6678
|
+
const candidates = await discoverPages(context.cwd, context.fs);
|
|
6679
|
+
let logicalName = args[1];
|
|
6680
|
+
if (!logicalName || logicalName.startsWith("-")) {
|
|
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([
|
|
6688
|
+
{
|
|
6689
|
+
type: "autocomplete",
|
|
6690
|
+
name: "page",
|
|
6691
|
+
message: "Page to remove:",
|
|
6692
|
+
choices: candidates.map((candidate2) => ({
|
|
6693
|
+
title: candidate2.logicalName.replaceAll(".", " > "),
|
|
6694
|
+
value: candidate2.logicalName
|
|
6695
|
+
}))
|
|
6696
|
+
},
|
|
6697
|
+
{
|
|
6698
|
+
type: (value) => value ? "confirm" : null,
|
|
6699
|
+
name: "confirm",
|
|
6700
|
+
message: "Confirm page deletion?",
|
|
6701
|
+
initial: false
|
|
6702
|
+
}
|
|
6703
|
+
]);
|
|
6704
|
+
if (!selected.confirm) {
|
|
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
|
+
});
|
|
6718
|
+
}
|
|
6719
|
+
logicalName = String(selected.page ?? "");
|
|
6720
|
+
}
|
|
6721
|
+
parseLogicalName(logicalName, "page name");
|
|
6722
|
+
const candidate = candidates.find((entry) => entry.logicalName === logicalName);
|
|
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
|
+
});
|
|
6778
|
+
}
|
|
6779
|
+
}
|
|
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
|
+
};
|
|
6829
|
+
|
|
6830
|
+
// src/cli/registry.ts
|
|
6831
|
+
function createCommandRegistry() {
|
|
6832
|
+
return new Map([
|
|
6833
|
+
["addcomponent", addComponent],
|
|
6834
|
+
["addpage", addPage],
|
|
6835
|
+
["addlib", addLib],
|
|
6836
|
+
["addapi", addApi],
|
|
6837
|
+
["addlanguage", addLanguage],
|
|
6838
|
+
["addtext", addText],
|
|
6839
|
+
["rmpage", rmPage]
|
|
6840
|
+
]);
|
|
5449
6841
|
}
|
|
5450
6842
|
|
|
5451
6843
|
// src/scaffold.ts
|
|
5452
|
-
import {
|
|
5453
|
-
import { join as join7 } from "path";
|
|
5454
|
-
import { existsSync as existsSync7 } from "fs";
|
|
6844
|
+
import { join as join8, resolve } from "path";
|
|
5455
6845
|
import { fileURLToPath } from "url";
|
|
5456
6846
|
|
|
5457
|
-
// src/
|
|
5458
|
-
|
|
5459
|
-
var
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
6847
|
+
// src/core/template-manifest.ts
|
|
6848
|
+
import path8 from "path";
|
|
6849
|
+
var TEMPLATE_DENY_NAMES = new Set([
|
|
6850
|
+
".env",
|
|
6851
|
+
".git",
|
|
6852
|
+
".agent",
|
|
6853
|
+
".cursor",
|
|
6854
|
+
".next",
|
|
6855
|
+
"node_modules",
|
|
6856
|
+
"artifacts",
|
|
6857
|
+
"coverage",
|
|
6858
|
+
"playwright-report",
|
|
6859
|
+
"test-results"
|
|
6860
|
+
]);
|
|
6861
|
+
function isDistributableTemplatePath(relativePath) {
|
|
6862
|
+
const segments = relativePath.split(path8.sep);
|
|
6863
|
+
return !segments.some((segment) => TEMPLATE_DENY_NAMES.has(segment) || segment.endsWith(".tsbuildinfo") || segment === ".DS_Store");
|
|
5464
6864
|
}
|
|
5465
|
-
function
|
|
5466
|
-
|
|
6865
|
+
async function templateManifest(root, fs) {
|
|
6866
|
+
const files = [];
|
|
6867
|
+
async function visit(directory, relativeDirectory = "") {
|
|
6868
|
+
const entries = await fs.list(directory);
|
|
6869
|
+
for (const entry of entries) {
|
|
6870
|
+
const relative = path8.join(relativeDirectory, entry.name);
|
|
6871
|
+
if (!isDistributableTemplatePath(relative))
|
|
6872
|
+
continue;
|
|
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 });
|
|
6876
|
+
}
|
|
6877
|
+
if (entry.isDirectory)
|
|
6878
|
+
await visit(source, relative);
|
|
6879
|
+
else if (entry.isFile)
|
|
6880
|
+
files.push(relative);
|
|
6881
|
+
else
|
|
6882
|
+
throw new CliError(`Unsupported template entry: ${relative}`, {
|
|
6883
|
+
code: "TEMPLATE_MISSING",
|
|
6884
|
+
scope: "package",
|
|
6885
|
+
path: relative
|
|
6886
|
+
});
|
|
6887
|
+
}
|
|
6888
|
+
}
|
|
6889
|
+
await visit(root);
|
|
6890
|
+
return files;
|
|
5467
6891
|
}
|
|
5468
|
-
function
|
|
5469
|
-
|
|
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
|
+
});
|
|
6914
|
+
}
|
|
6915
|
+
return manifest;
|
|
6916
|
+
}
|
|
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));
|
|
6932
|
+
packageJson.name = projectName.toLowerCase();
|
|
6933
|
+
delete packageJson.packageManager;
|
|
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);
|
|
6938
|
+
const tsconfig = JSON.parse(tsconfigContent);
|
|
6939
|
+
if (!tsconfig.compilerOptions?.paths?.["@/*"]) {
|
|
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
|
+
});
|
|
6945
|
+
}
|
|
6946
|
+
await gateway.write(tsconfigPath, tsconfigContent.replace('"@/*"', `"${importAlias}"`), { role: "typescript-configuration" });
|
|
6947
|
+
if (importAlias !== "@/*") {
|
|
6948
|
+
const prefix = importAlias.slice(0, -2);
|
|
6949
|
+
for (const relative of await templateManifest(target, context.fs)) {
|
|
6950
|
+
if (!/\.[cm]?[jt]sx?$/.test(relative))
|
|
6951
|
+
continue;
|
|
6952
|
+
const file = path8.join(target, relative);
|
|
6953
|
+
const content = await context.fs.readText(file);
|
|
6954
|
+
const next = content.replaceAll('from "@/', `from "${prefix}/`).replaceAll("from '@/", `from '${prefix}/`);
|
|
6955
|
+
if (next !== content) {
|
|
6956
|
+
await gateway.write(file, next, { role: "import-alias-reference" });
|
|
6957
|
+
}
|
|
6958
|
+
}
|
|
6959
|
+
}
|
|
5470
6960
|
}
|
|
5471
6961
|
|
|
5472
6962
|
// src/scaffold.ts
|
|
5473
|
-
async function scaffoldProject(options) {
|
|
5474
|
-
const
|
|
6963
|
+
async function scaffoldProject(options, runtime) {
|
|
6964
|
+
const requiredFeatures = [
|
|
6965
|
+
"useTypescript",
|
|
6966
|
+
"useEslint",
|
|
6967
|
+
"useTailwind",
|
|
6968
|
+
"useSrcDir",
|
|
6969
|
+
"useTurbopack",
|
|
6970
|
+
"useI18n"
|
|
6971
|
+
];
|
|
6972
|
+
const unsupported = requiredFeatures.filter((feature) => options[feature] !== true);
|
|
6973
|
+
if (unsupported.length > 0) {
|
|
6974
|
+
throw new CliError(`The default Next.js 16 template requires: ${unsupported.join(", ")}.`, { code: "INVALID_ARGUMENT" });
|
|
6975
|
+
}
|
|
6976
|
+
const { context } = runtime;
|
|
6977
|
+
const cwd = context.cwd;
|
|
6978
|
+
const projectName = validateProjectName(options.projectName);
|
|
6979
|
+
const importAlias = normalizeImportAlias(options.customAlias === false ? "@/*" : options.importAlias || "@/*");
|
|
6980
|
+
const targetPath = join8(cwd, projectName);
|
|
5475
6981
|
const __dirname2 = new URL(".", import.meta.url);
|
|
5476
|
-
const templatePath =
|
|
5477
|
-
|
|
6982
|
+
const templatePath = runtime.templatePath ?? join8(fileURLToPath(__dirname2), "..", "templates", "Projects", "default");
|
|
6983
|
+
const resolvedCwd = resolve(cwd);
|
|
6984
|
+
const resolvedTarget = resolve(targetPath);
|
|
6985
|
+
if (!resolvedTarget.startsWith(`${resolvedCwd}/`)) {
|
|
6986
|
+
throw new CliError("The project destination must be a child of the current directory.", { code: "UNSAFE_PATH" });
|
|
6987
|
+
}
|
|
6988
|
+
const gateway = new MutationGateway(context, targetPath);
|
|
6989
|
+
const manifest = await validateScaffoldTemplate(templatePath, context.fs);
|
|
6990
|
+
if (context.fs.exists(targetPath)) {
|
|
5478
6991
|
if (options.force) {
|
|
5479
|
-
|
|
5480
|
-
|
|
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 });
|
|
5481
6999
|
} else {
|
|
5482
|
-
|
|
5483
|
-
|
|
7000
|
+
throw new CliError("Target directory already exists. Use --force to overwrite it.", {
|
|
7001
|
+
code: "TARGET_EXISTS",
|
|
7002
|
+
scope: "project",
|
|
7003
|
+
path: "."
|
|
7004
|
+
});
|
|
5484
7005
|
}
|
|
5485
7006
|
}
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
}
|
|
5500
|
-
await writeFile6(join7(targetPath, "cnp.config.json"), JSON.stringify(options, null, 2));
|
|
5501
|
-
console.log("Project setup complete!");
|
|
5502
|
-
console.log("");
|
|
5503
|
-
console.log("To get started:");
|
|
5504
|
-
console.log(" " + green(`cd ${options.projectName}`));
|
|
5505
|
-
console.log("");
|
|
5506
|
-
console.log("Then install dependencies and launch the dev server with your preferred tool:");
|
|
5507
|
-
console.log(" " + green(`bun install && bun dev`));
|
|
5508
|
-
console.log(" " + green(`npm install && npm run dev`));
|
|
5509
|
-
console.log(" " + green(`pnpm install && pnpm run dev`));
|
|
5510
|
-
console.log("");
|
|
5511
|
-
console.log("Documentation and examples can be found at:");
|
|
5512
|
-
console.log(" " + cyan("https://github.com/Rising-Corporation/create-next-pro-cli"));
|
|
5513
|
-
console.log("_-`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`-_");
|
|
5514
|
-
} catch (err) {
|
|
5515
|
-
console.error(red("[X] Error during project creation:"), err);
|
|
5516
|
-
process.exit(1);
|
|
5517
|
-
}
|
|
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 };
|
|
5518
7020
|
}
|
|
5519
7021
|
|
|
5520
7022
|
// src/lib/createProject.ts
|
|
5521
|
-
async function
|
|
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
|
+
}
|
|
7059
|
+
async function createProject(nameArg, force, context) {
|
|
5522
7060
|
const response = {
|
|
5523
7061
|
projectName: nameArg,
|
|
5524
7062
|
useTypescript: true,
|
|
@@ -5531,14 +7069,12 @@ async function createProject(nameArg, force) {
|
|
|
5531
7069
|
importAlias: "@/*",
|
|
5532
7070
|
force
|
|
5533
7071
|
};
|
|
5534
|
-
|
|
5535
|
-
await scaffoldProject(response);
|
|
7072
|
+
return createProjectFromOptions(response, context);
|
|
5536
7073
|
}
|
|
5537
7074
|
|
|
5538
7075
|
// src/lib/createProjectWithPrompt.ts
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
const response = await import_prompts6.default.prompt([
|
|
7076
|
+
async function createProjectWithPrompt(context) {
|
|
7077
|
+
const response = await context.prompt([
|
|
5542
7078
|
{
|
|
5543
7079
|
type: "text",
|
|
5544
7080
|
name: "projectName",
|
|
@@ -5608,308 +7144,243 @@ async function createProjectWithPrompt() {
|
|
|
5608
7144
|
initial: "@core/*"
|
|
5609
7145
|
}
|
|
5610
7146
|
]);
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
const locales = [];
|
|
5625
|
-
for (let i = 0;i < 26; i++) {
|
|
5626
|
-
for (let j = 0;j < 26; j++) {
|
|
5627
|
-
const code = String.fromCharCode(97 + i) + String.fromCharCode(97 + j);
|
|
5628
|
-
try {
|
|
5629
|
-
const name = dn.of(code);
|
|
5630
|
-
if (name && name.toLowerCase() !== code) {
|
|
5631
|
-
locales.push(code);
|
|
5632
|
-
}
|
|
5633
|
-
} catch {}
|
|
5634
|
-
}
|
|
5635
|
-
}
|
|
5636
|
-
return locales.sort();
|
|
5637
|
-
}
|
|
5638
|
-
async function addLanguage(args) {
|
|
5639
|
-
const config = await loadConfig();
|
|
5640
|
-
if (!config?.useI18n) {
|
|
5641
|
-
console.error("\u274C i18n is not enabled in this project.");
|
|
5642
|
-
return;
|
|
5643
|
-
}
|
|
5644
|
-
const messagesPath = join8(process.cwd(), "messages");
|
|
5645
|
-
if (!existsSync8(messagesPath)) {
|
|
5646
|
-
console.error("\u274C Messages directory missing. Ensure i18n was configured.");
|
|
5647
|
-
return;
|
|
5648
|
-
}
|
|
5649
|
-
const available = generateLocales();
|
|
5650
|
-
let locale = args[1];
|
|
5651
|
-
if (!locale || !available.includes(locale)) {
|
|
5652
|
-
const response = await import_prompts7.default({
|
|
5653
|
-
type: "autocomplete",
|
|
5654
|
-
name: "locale",
|
|
5655
|
-
message: "\uD83C\uDF10 Locale to add:",
|
|
5656
|
-
choices: available.map((l) => ({ title: l, value: l }))
|
|
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"
|
|
5657
7160
|
});
|
|
5658
|
-
locale = response.locale;
|
|
5659
|
-
}
|
|
5660
|
-
if (!locale)
|
|
5661
|
-
return;
|
|
5662
|
-
if (existsSync8(join8(messagesPath, locale))) {
|
|
5663
|
-
console.error(`\u274C Locale ${locale} already exists.`);
|
|
5664
|
-
return;
|
|
5665
|
-
}
|
|
5666
|
-
const routingFile = join8(process.cwd(), "src", "lib", "i18n", "routing.ts");
|
|
5667
|
-
if (!existsSync8(routingFile)) {
|
|
5668
|
-
console.error("\u274C routing.ts not found. Are you in project root?");
|
|
5669
|
-
return;
|
|
5670
|
-
}
|
|
5671
|
-
const routingContent = await readFile7(routingFile, "utf-8");
|
|
5672
|
-
const defaultMatch = routingContent.match(/defaultLocale:\s*"([^"]+)"/);
|
|
5673
|
-
const defaultLocale = defaultMatch ? defaultMatch[1] : null;
|
|
5674
|
-
if (!defaultLocale || !existsSync8(join8(messagesPath, defaultLocale))) {
|
|
5675
|
-
console.error("\u274C Default locale not found.");
|
|
5676
|
-
return;
|
|
5677
|
-
}
|
|
5678
|
-
await cp2(join8(messagesPath, defaultLocale), join8(messagesPath, locale), {
|
|
5679
|
-
recursive: true
|
|
5680
|
-
});
|
|
5681
|
-
console.log(`\uD83D\uDCC4 Directory created: ${join8(messagesPath, locale)}`);
|
|
5682
|
-
const localesMatch = routingContent.match(/locales:\s*\[([^\]]*)\]/);
|
|
5683
|
-
if (localesMatch) {
|
|
5684
|
-
const localesArr = localesMatch[1].split(",").map((s) => s.trim().replace(/["']/g, "")).filter(Boolean);
|
|
5685
|
-
if (!localesArr.includes(locale)) {
|
|
5686
|
-
localesArr.push(locale);
|
|
5687
|
-
const newLocales = `locales: [${localesArr.map((l) => `"${l}"`).join(", ")}]`;
|
|
5688
|
-
const newContent = routingContent.replace(/locales:\s*\[[^\]]*\]/, newLocales);
|
|
5689
|
-
await writeFile7(routingFile, newContent);
|
|
5690
|
-
console.log(`\uD83D\uDCC4 File updated: ${routingFile}`);
|
|
5691
|
-
}
|
|
5692
|
-
}
|
|
5693
|
-
console.log(`\u2705 Locale "${locale}" added and copied from default locale "${defaultLocale}".`);
|
|
5694
|
-
}
|
|
5695
|
-
|
|
5696
|
-
// src/lib/addText.ts
|
|
5697
|
-
import { join as join9 } from "path";
|
|
5698
|
-
import { existsSync as existsSync9 } from "fs";
|
|
5699
|
-
import { readFile as readFile8, writeFile as writeFile8, readdir as readdir4 } from "fs/promises";
|
|
5700
|
-
function defaultText(key) {
|
|
5701
|
-
return key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
5702
|
-
}
|
|
5703
|
-
async function addText(args) {
|
|
5704
|
-
const pathArg = args[1];
|
|
5705
|
-
if (!pathArg) {
|
|
5706
|
-
console.error("\u274C Dot path parameter is required.");
|
|
5707
|
-
return;
|
|
5708
|
-
}
|
|
5709
|
-
const providedText = args.slice(2).join(" ");
|
|
5710
|
-
const config = await loadConfig();
|
|
5711
|
-
if (!config?.useI18n) {
|
|
5712
|
-
console.error("\u274C i18n is not enabled in this project.");
|
|
5713
|
-
return;
|
|
5714
|
-
}
|
|
5715
|
-
const messagesPath = join9(process.cwd(), "messages");
|
|
5716
|
-
if (!existsSync9(messagesPath)) {
|
|
5717
|
-
console.error("\u274C Messages directory missing. Ensure i18n was configured.");
|
|
5718
|
-
return;
|
|
5719
|
-
}
|
|
5720
|
-
const entries = await readdir4(messagesPath, { withFileTypes: true });
|
|
5721
|
-
const locales = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
5722
|
-
const [fileName, ...segments] = pathArg.split(".");
|
|
5723
|
-
if (!fileName || segments.length === 0) {
|
|
5724
|
-
console.error("\u274C Invalid dot path provided.");
|
|
5725
|
-
return;
|
|
5726
|
-
}
|
|
5727
|
-
const finalKey = segments[segments.length - 1];
|
|
5728
|
-
const text = providedText || defaultText(finalKey);
|
|
5729
|
-
for (const locale of locales) {
|
|
5730
|
-
const filePath = join9(messagesPath, locale, `${fileName}.json`);
|
|
5731
|
-
let data = {};
|
|
5732
|
-
if (existsSync9(filePath)) {
|
|
5733
|
-
const raw = await readFile8(filePath, "utf-8");
|
|
5734
|
-
try {
|
|
5735
|
-
data = JSON.parse(raw);
|
|
5736
|
-
} catch {}
|
|
5737
|
-
}
|
|
5738
|
-
let cursor = data;
|
|
5739
|
-
for (let i = 0;i < segments.length - 1; i++) {
|
|
5740
|
-
const seg = segments[i];
|
|
5741
|
-
if (!cursor[seg])
|
|
5742
|
-
cursor[seg] = {};
|
|
5743
|
-
cursor = cursor[seg];
|
|
5744
|
-
}
|
|
5745
|
-
cursor[finalKey] = text;
|
|
5746
|
-
await writeFile8(filePath, JSON.stringify(data, null, 2));
|
|
5747
|
-
console.log(`\uD83D\uDCC4 File updated: ${filePath}`);
|
|
5748
7161
|
}
|
|
5749
|
-
|
|
7162
|
+
const options = response;
|
|
7163
|
+
return createProjectFromOptions(options, context);
|
|
5750
7164
|
}
|
|
5751
7165
|
|
|
5752
|
-
// src/
|
|
5753
|
-
var
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
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;
|
|
5760
7193
|
}
|
|
5761
7194
|
}
|
|
5762
|
-
function
|
|
5763
|
-
|
|
5764
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
|
|
5765
|
-
}
|
|
5766
|
-
function rcFile(shell) {
|
|
5767
|
-
return path.join(os.homedir(), shell === "zsh" ? ".zshrc" : ".bashrc");
|
|
5768
|
-
}
|
|
5769
|
-
function ensureLineInRc(file, line) {
|
|
5770
|
-
try {
|
|
5771
|
-
const cur = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
5772
|
-
if (!cur.includes(line))
|
|
5773
|
-
fs.appendFileSync(file, `
|
|
5774
|
-
${line}
|
|
5775
|
-
`);
|
|
5776
|
-
} catch {}
|
|
7195
|
+
function resolvePackageRoot(metaUrl = import.meta.url) {
|
|
7196
|
+
return findPackageRoot(path9.dirname(fileURLToPath2(metaUrl)));
|
|
5777
7197
|
}
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
async
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
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
|
+
}
|
|
5804
7252
|
},
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
message: "Install autocompletion?",
|
|
5809
|
-
initial: true,
|
|
5810
|
-
active: "Yes",
|
|
5811
|
-
inactive: "No"
|
|
5812
|
-
}
|
|
5813
|
-
], { onCancel: () => process.exit(1) });
|
|
5814
|
-
const cfg = {
|
|
5815
|
-
version: 1,
|
|
5816
|
-
shell: res.shell,
|
|
5817
|
-
completionInstalled: !!res.completion,
|
|
5818
|
-
createdAt: new Date().toISOString(),
|
|
5819
|
-
updatedAt: new Date().toISOString()
|
|
7253
|
+
operations: new OperationJournal,
|
|
7254
|
+
outputMode: "human",
|
|
7255
|
+
...overrides
|
|
5820
7256
|
};
|
|
5821
|
-
if (cfg.completionInstalled)
|
|
5822
|
-
await installCompletion(cfg.shell);
|
|
5823
|
-
writeCfg(cfg);
|
|
5824
|
-
console.log(`
|
|
5825
|
-
\u2705 Configuration saved.`);
|
|
5826
|
-
console.log("you can now use the CLI ! ex : ");
|
|
5827
|
-
console.log(" Without prompt (will change in future) :");
|
|
5828
|
-
console.log(" create-next-pro my-next-project");
|
|
5829
|
-
console.log(" With prompt :");
|
|
5830
|
-
console.log(" create-next-pro");
|
|
5831
|
-
console.log("For more information, visit: https://github.com/Rising-Corporation/create-next-pro-cli");
|
|
5832
|
-
console.log("Happy coding! \uD83C\uDF89");
|
|
5833
|
-
return cfg;
|
|
5834
7257
|
}
|
|
5835
|
-
|
|
5836
|
-
|
|
7258
|
+
|
|
7259
|
+
// src/index.ts
|
|
7260
|
+
var HELP_TEXT = `create-next-pro
|
|
5837
7261
|
|
|
5838
7262
|
Usage:
|
|
5839
|
-
create-next-pro <project-name> [--force]
|
|
5840
|
-
create-next-pro addpage [options]
|
|
5841
|
-
create-next-pro addcomponent [options]
|
|
5842
|
-
create-next-pro addlib [name]
|
|
5843
|
-
create-next-pro addapi [name]
|
|
5844
|
-
create-next-pro addlanguage [locale]
|
|
5845
|
-
create-next-pro addtext <path> [text]
|
|
5846
|
-
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]
|
|
5847
7271
|
|
|
5848
7272
|
Options:
|
|
5849
7273
|
--help Show this help message
|
|
7274
|
+
--version Show the CLI version
|
|
7275
|
+
--json Emit one deterministic JSON document
|
|
5850
7276
|
--reconfigure Run the configuration assistant again
|
|
5851
|
-
|
|
7277
|
+
`;
|
|
7278
|
+
function parseOptions(args) {
|
|
7279
|
+
return {
|
|
7280
|
+
force: args.includes("--force"),
|
|
7281
|
+
help: args.includes("--help"),
|
|
7282
|
+
version: args.includes("--version") || args.includes("-v"),
|
|
7283
|
+
reconfigure: args.includes("--reconfigure"),
|
|
7284
|
+
json: args.includes("--json")
|
|
7285
|
+
};
|
|
5852
7286
|
}
|
|
5853
|
-
function
|
|
5854
|
-
|
|
5855
|
-
console.log(`v${pkg.version}`);
|
|
7287
|
+
function withoutGlobalOptions(args) {
|
|
7288
|
+
return args.filter((argument) => argument !== "--json");
|
|
5856
7289
|
}
|
|
5857
|
-
async function
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
}
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
return;
|
|
5881
|
-
}
|
|
5882
|
-
if (args[0] === "addpage") {
|
|
5883
|
-
addPage(args);
|
|
5884
|
-
return;
|
|
7290
|
+
async function packageVersion(context) {
|
|
7291
|
+
const packageJson = JSON.parse(await context.fs.readText(path10.join(context.packageRoot, "package.json")));
|
|
7292
|
+
return packageJson.version;
|
|
7293
|
+
}
|
|
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
|
+
});
|
|
5885
7313
|
}
|
|
5886
|
-
if (
|
|
5887
|
-
|
|
5888
|
-
|
|
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
|
+
});
|
|
5889
7322
|
}
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
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);
|
|
5893
7329
|
}
|
|
5894
|
-
if (
|
|
5895
|
-
|
|
5896
|
-
|
|
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);
|
|
5897
7340
|
}
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
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.");
|
|
5901
7350
|
}
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
7351
|
+
return createProjectWithPrompt(context);
|
|
7352
|
+
}
|
|
7353
|
+
async function runCompletion(context, args) {
|
|
7354
|
+
try {
|
|
7355
|
+
for (const candidate of await completionCandidates(args[1], context)) {
|
|
7356
|
+
context.terminal.log(candidate);
|
|
7357
|
+
}
|
|
7358
|
+
return 0;
|
|
7359
|
+
} catch {
|
|
7360
|
+
return 1;
|
|
5905
7361
|
}
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
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);
|
|
7373
|
+
} catch (error) {
|
|
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
|
+
});
|
|
5910
7380
|
}
|
|
5911
|
-
|
|
7381
|
+
renderResult(result, context, context.outputMode);
|
|
7382
|
+
return result.exitCode;
|
|
5912
7383
|
}
|
|
5913
7384
|
|
|
5914
7385
|
// bin.bun.ts
|
|
5915
|
-
main();
|
|
7386
|
+
process.exitCode = await main();
|