pan-stack-cli 0.2.1 → 0.2.3
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 +37 -731
- package/bin/pan.js +4978 -269
- package/package.json +17 -5
- package/lib/ai-skills.js +0 -829
- package/lib/create.js +0 -105
- package/lib/discover.js +0 -294
- package/lib/git/git-checkout.js +0 -395
- package/lib/git/git-clone.js +0 -206
- package/lib/git/git-fetch-all.js +0 -129
- package/lib/git/git-grv.js +0 -125
- package/lib/git/git-gst.js +0 -165
- package/lib/git/git-mr.js +0 -546
- package/lib/git/git-pull.js +0 -137
- package/lib/git/git-push.js +0 -185
- package/lib/git/git-sync-main.js +0 -334
- package/lib/git/helpers/bulk-report.js +0 -202
- package/lib/git/helpers/cli.js +0 -188
- package/lib/git/helpers/colors.js +0 -19
- package/lib/git/helpers/git.js +0 -136
- package/lib/git/helpers/manifest.js +0 -223
- package/lib/git/helpers/repo-select.js +0 -416
- package/lib/git/helpers/repo-summary.js +0 -161
- package/lib/i18n.js +0 -61
- package/lib/inquirer-theme.js +0 -20
- package/lib/language.js +0 -121
- package/lib/languages.js +0 -14
- package/lib/manifest.js +0 -467
- package/lib/menu-prompt.js +0 -201
- package/lib/menu.js +0 -134
- package/lib/messages.js +0 -647
- package/lib/platform.js +0 -140
- package/lib/run-command.js +0 -334
- package/lib/select.js +0 -0
- package/lib/serve.js +0 -75
- package/lib/skill-registry-client.js +0 -173
- package/lib/stats/aggregate.js +0 -51
- package/lib/stats/identity.js +0 -20
- package/lib/stats/index.js +0 -189
- package/lib/stats/parse.js +0 -46
- package/lib/stats/range.js +0 -38
- package/lib/stats/report.js +0 -264
- package/lib/stats/review.js +0 -157
- package/lib/stats/scan.js +0 -92
- package/lib/stats/serve.js +0 -25
- package/lib/ui/api.js +0 -202
- package/lib/ui/clone.js +0 -115
- package/lib/ui/config.js +0 -182
- package/lib/ui/diff.js +0 -128
- package/lib/ui/export-gate.js +0 -79
- package/lib/ui/gen-react-mobile-code.js +0 -6
- package/lib/ui/gen-react-web-code.js +0 -75
- package/lib/ui/index.js +0 -89
- package/lib/ui/init.js +0 -252
- package/lib/ui/login.js +0 -128
- package/lib/ui/logout.js +0 -22
- package/lib/ui/palettes.js +0 -103
- package/lib/ui/published-html.js +0 -24
- package/lib/ui/sync.js +0 -137
- package/lib/ui/workspace.js +0 -213
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
const colors = require("./colors");
|
|
2
|
-
const { getCurrentBranch, runGitCapture, isMergeOrRebaseInProgress } = require("./git");
|
|
3
|
-
const { t } = require("../../i18n");
|
|
4
|
-
|
|
5
|
-
const ICON = {
|
|
6
|
-
ok: "✅",
|
|
7
|
-
warn: "⚠️",
|
|
8
|
-
info: "ℹ️",
|
|
9
|
-
skip: "⏭️",
|
|
10
|
-
up: "↑",
|
|
11
|
-
down: "↓",
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
function computeAheadBehind(directory) {
|
|
15
|
-
const up = runGitCapture(["rev-parse", "--abbrev-ref", "@{u}"], { cwd: directory });
|
|
16
|
-
if (up.status !== 0 || !up.stdout) return { hasUpstream: false, ahead: 0, behind: 0 };
|
|
17
|
-
const rl = runGitCapture(["rev-list", "--left-right", "--count", "HEAD...@{u}"], { cwd: directory });
|
|
18
|
-
if (rl.status !== 0) return { hasUpstream: true, ahead: 0, behind: 0 };
|
|
19
|
-
const [ahead, behind] = rl.stdout.split(/\s+/).map((n) => parseInt(n, 10) || 0);
|
|
20
|
-
return { hasUpstream: true, ahead, behind };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function getRepoStatusSummary(directory) {
|
|
24
|
-
const branch = getCurrentBranch(directory);
|
|
25
|
-
const result = runGitCapture(["status", "--porcelain"], { cwd: directory });
|
|
26
|
-
if (result.status !== 0) {
|
|
27
|
-
return {
|
|
28
|
-
branch,
|
|
29
|
-
error: result.stderr || "git status failed",
|
|
30
|
-
total: 0,
|
|
31
|
-
staged: 0,
|
|
32
|
-
unstaged: 0,
|
|
33
|
-
untracked: 0,
|
|
34
|
-
ahead: 0,
|
|
35
|
-
behind: 0,
|
|
36
|
-
hasUpstream: false,
|
|
37
|
-
inProgress: "",
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const lines = result.stdout
|
|
42
|
-
.split(/\r?\n/)
|
|
43
|
-
.map((line) => line.trimEnd())
|
|
44
|
-
.filter(Boolean);
|
|
45
|
-
|
|
46
|
-
let staged = 0;
|
|
47
|
-
let unstaged = 0;
|
|
48
|
-
let untracked = 0;
|
|
49
|
-
|
|
50
|
-
for (const line of lines) {
|
|
51
|
-
const index = line[0];
|
|
52
|
-
const worktree = line[1];
|
|
53
|
-
|
|
54
|
-
if (index === "?" && worktree === "?") {
|
|
55
|
-
untracked += 1;
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (index !== " ") {
|
|
60
|
-
staged += 1;
|
|
61
|
-
}
|
|
62
|
-
if (worktree !== " ") {
|
|
63
|
-
unstaged += 1;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const { hasUpstream, ahead, behind } = computeAheadBehind(directory);
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
branch,
|
|
71
|
-
total: lines.length,
|
|
72
|
-
staged,
|
|
73
|
-
unstaged,
|
|
74
|
-
untracked,
|
|
75
|
-
ahead,
|
|
76
|
-
behind,
|
|
77
|
-
hasUpstream,
|
|
78
|
-
inProgress: isMergeOrRebaseInProgress(directory), // "merge"|"rebase"|""
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function formatFileBreakdown({ staged, unstaged, untracked }) {
|
|
83
|
-
const parts = [];
|
|
84
|
-
if (staged > 0) parts.push(t("BULK_BREAKDOWN_STAGED__N", { n: staged }));
|
|
85
|
-
if (unstaged > 0) parts.push(t("BULK_BREAKDOWN_UNSTAGED__N", { n: unstaged }));
|
|
86
|
-
if (untracked > 0) parts.push(t("BULK_BREAKDOWN_UNTRACKED__N", { n: untracked }));
|
|
87
|
-
return parts.join(" · ");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function formatSync(item) {
|
|
91
|
-
if (item.error) return colors.red(t("BULK_STATUS_ERROR"));
|
|
92
|
-
if (!item.hasUpstream) return colors.dim(t("BULK_STATUS_NO_UPSTREAM"));
|
|
93
|
-
if (item.behind === 0 && item.ahead === 0) return colors.green(t("BULK_STATUS_IN_SYNC"));
|
|
94
|
-
const parts = [];
|
|
95
|
-
if (item.behind) parts.push(colors.yellow(`${ICON.down} ${item.behind}`));
|
|
96
|
-
if (item.ahead) parts.push(colors.cyan(`${ICON.up} ${item.ahead}`));
|
|
97
|
-
return parts.join(" ");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function formatState(item) {
|
|
101
|
-
if (item.inProgress === "merge") return colors.red(t("BULK_STATUS_MERGING"));
|
|
102
|
-
if (item.inProgress === "rebase") return colors.red(t("BULK_STATUS_REBASING"));
|
|
103
|
-
if (item.error) return colors.red(t("BULK_STATUS_ERROR"));
|
|
104
|
-
if (item.total === 0) return colors.green(t("BULK_STATUS_CLEAN"));
|
|
105
|
-
const label = item.total === 1 ? t("BULK_FILES_ONE") : t("BULK_FILES_MANY");
|
|
106
|
-
return colors.yellow(`${item.total} ${label}`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function printChangeSummary({ title, results, totalRepos }) {
|
|
110
|
-
const dirty = results.filter((item) => item.total > 0 || item.inProgress);
|
|
111
|
-
const errors = results.filter((item) => item.error);
|
|
112
|
-
const shown = results.filter((item) => item.total > 0 || item.inProgress || item.error);
|
|
113
|
-
const rows = shown.map((item) => ({
|
|
114
|
-
repo: item.error ? colors.red(item.name) : colors.cyan(item.name),
|
|
115
|
-
branch: item.branch || colors.dim(t("GST_DETACHED")),
|
|
116
|
-
state: formatState(item),
|
|
117
|
-
breakdown: item.total > 0 ? colors.dim(formatFileBreakdown(item)) : colors.dim("—"),
|
|
118
|
-
sync: formatSync(item),
|
|
119
|
-
}));
|
|
120
|
-
|
|
121
|
-
const totalFiles = dirty.reduce((sum, item) => sum + item.total, 0);
|
|
122
|
-
const cleanCount = totalRepos - dirty.length - errors.length;
|
|
123
|
-
|
|
124
|
-
console.log("");
|
|
125
|
-
console.log(colors.bold(`─── ${t("GST_TITLE")} ───`));
|
|
126
|
-
console.log(colors.dim(`${ICON.info} ${t("GST_FOOTER_COUNTS__TOTAL_DIRTY_FILES_CLEAN_ERRORS", {
|
|
127
|
-
total: totalRepos, dirty: dirty.length, files: totalFiles, clean: cleanCount, errors: errors.length,
|
|
128
|
-
})}`));
|
|
129
|
-
|
|
130
|
-
if (rows.length > 0) {
|
|
131
|
-
const { renderTable, bulkColumns } = require("./bulk-report");
|
|
132
|
-
console.log(renderTable(
|
|
133
|
-
bulkColumns([
|
|
134
|
-
{ key: "repo", labelKey: "BULK_COL_REPO" },
|
|
135
|
-
{ key: "branch", labelKey: "BULK_COL_BRANCH" },
|
|
136
|
-
{ key: "state", labelKey: "BULK_COL_STATE" },
|
|
137
|
-
{ key: "breakdown", labelKey: "BULK_COL_BREAKDOWN" },
|
|
138
|
-
{ key: "sync", labelKey: "BULK_COL_SYNC" },
|
|
139
|
-
]),
|
|
140
|
-
rows
|
|
141
|
-
));
|
|
142
|
-
} else {
|
|
143
|
-
console.log("");
|
|
144
|
-
console.log(colors.green(t("GST_ALL_CLEAN__N", { n: totalRepos })));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (errors.length > 0) {
|
|
148
|
-
console.log("");
|
|
149
|
-
console.log(colors.bold(colors.yellow(`${ICON.warn} ${t("GST_ERRORS_HEADER")}`)));
|
|
150
|
-
for (const item of errors) {
|
|
151
|
-
console.log(` • ${item.name}: ${item.error}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
module.exports = {
|
|
157
|
-
ICON,
|
|
158
|
-
getRepoStatusSummary,
|
|
159
|
-
formatFileBreakdown,
|
|
160
|
-
printChangeSummary,
|
|
161
|
-
};
|
package/lib/i18n.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
// Ngôn ngữ pan CLI: MẶC ĐỊNH 'en'. Ưu tiên `language:` trong .project.yaml gần nhất (walk-up từ cwd),
|
|
2
|
-
// hoặc env PAN_LANG. Cache 1 lần/tiến trình.
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
|
-
const MESSAGES = require("./messages");
|
|
6
|
-
|
|
7
|
-
let cached = null;
|
|
8
|
-
|
|
9
|
-
function detectLang() {
|
|
10
|
-
if (process.env.PAN_LANG) return String(process.env.PAN_LANG).toLowerCase();
|
|
11
|
-
let dir = process.cwd();
|
|
12
|
-
for (let i = 0; i < 50; i += 1) {
|
|
13
|
-
const p = path.join(dir, ".project.yaml");
|
|
14
|
-
try {
|
|
15
|
-
if (fs.existsSync(p)) {
|
|
16
|
-
const m = fs.readFileSync(p, "utf8").match(/^\s*language\s*:\s*['"]?([A-Za-z-]+)/m);
|
|
17
|
-
return m ? m[1].toLowerCase() : "en";
|
|
18
|
-
}
|
|
19
|
-
} catch {
|
|
20
|
-
/* bỏ qua */
|
|
21
|
-
}
|
|
22
|
-
const parent = path.dirname(dir);
|
|
23
|
-
if (parent === dir) break;
|
|
24
|
-
dir = parent;
|
|
25
|
-
}
|
|
26
|
-
return "en";
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function lang() {
|
|
30
|
-
if (cached === null) cached = detectLang();
|
|
31
|
-
return cached;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Ghi đè cache trong tiến trình hiện tại (vd sau khi `pan language <code>` vừa ghi file) — để các
|
|
35
|
-
// thông báo in ra ngay sau đó (cùng lệnh) dùng ngôn ngữ mới thay vì ngôn ngữ đã cache trước đó.
|
|
36
|
-
function setLangOverride(code) {
|
|
37
|
-
cached = String(code).toLowerCase();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function availableLangs() {
|
|
41
|
-
return Object.keys(MESSAGES);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function interpolate(template, vars) {
|
|
45
|
-
if (!vars) return template;
|
|
46
|
-
return template.replace(/\{(\w+)\}/g, (match, name) => (name in vars ? String(vars[name]) : match));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* t('SOME_KEY', { var1, var2 }) → chuỗi theo ngôn ngữ hiện tại, nội suy {var1}/{var2} trong template.
|
|
51
|
-
* Thêm ngôn ngữ mới: thêm object con trong messages.js — KHÔNG cần sửa call site nào.
|
|
52
|
-
* Thiếu key ở ngôn ngữ hiện tại → fallback 'en' → fallback chính key (không throw, luôn có gì đó để in ra).
|
|
53
|
-
*/
|
|
54
|
-
function t(key, vars) {
|
|
55
|
-
const currentLang = lang();
|
|
56
|
-
const dict = MESSAGES[currentLang] || MESSAGES.en;
|
|
57
|
-
const template = dict[key] ?? MESSAGES.en[key] ?? key;
|
|
58
|
-
return interpolate(template, vars);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
module.exports = { lang, t, setLangOverride, availableLangs };
|
package/lib/inquirer-theme.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
// Help-tip dùng chung cho @inquirer select/checkbox (repo-select, select, menu). MẶC ĐỊNH English;
|
|
2
|
-
// 'vi' khi .project.yaml có `language: vi` (xem i18n.js).
|
|
3
|
-
const { lang } = require("./i18n");
|
|
4
|
-
|
|
5
|
-
const HELP = {
|
|
6
|
-
en: { multi: "↑↓ move · space select · a all · i invert · enter confirm", single: "↑↓ move · enter select" },
|
|
7
|
-
vi: { multi: "↑↓ di chuyển · space chọn · a tất cả · i đảo · Enter xác nhận", single: "↑↓ di chuyển · Enter chọn" },
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
function helpText(multi) {
|
|
11
|
-
const h = HELP[lang()] || HELP.en;
|
|
12
|
-
return multi ? h.multi : h.single;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// theme cho @inquirer: override keysHelpTip (nhận mảng [key,action]) → chuỗi help theo ngôn ngữ.
|
|
16
|
-
function helpTheme(multi) {
|
|
17
|
-
return { style: { keysHelpTip: () => ` (${helpText(multi)})` } };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
module.exports = { helpText, helpTheme };
|
package/lib/language.js
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
// `pan language [<code>]` — đổi ngôn ngữ CLI bằng cách ghi `language: <code>` vào .project.yaml
|
|
2
|
-
// gần nhất (walk-up từ cwd, giống findNearestProjectManifest). Chưa có key → thêm; đã có → update.
|
|
3
|
-
// Không truyền code → menu tương tác (danh sách ngôn ngữ available), theo đúng quy ước chung của
|
|
4
|
-
// mọi menu trong pan: ↑↓ di chuyển, Enter chọn, x thoát hẳn, b quay lại (không đổi gì).
|
|
5
|
-
const fs = require("fs");
|
|
6
|
-
const { findNearestProjectManifest } = require("./discover");
|
|
7
|
-
const { loadInquirer } = require("./select");
|
|
8
|
-
const { menuSelect } = require("./menu-prompt");
|
|
9
|
-
const { helpTheme } = require("./inquirer-theme");
|
|
10
|
-
const { t, lang, setLangOverride, availableLangs } = require("./i18n");
|
|
11
|
-
const LANGUAGE_NAMES = require("./languages");
|
|
12
|
-
|
|
13
|
-
function isCancelError(err) {
|
|
14
|
-
const s = String((err && (err.name + " " + err.message)) || "");
|
|
15
|
-
return /ExitPromptError|force closed|SIGINT|cancel/i.test(s);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function langLabel(code) {
|
|
19
|
-
return (LANGUAGE_NAMES[code] && LANGUAGE_NAMES[code].name) || code;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const TOP_LEVEL_LANGUAGE_RE = /^language\s*:.*$/m;
|
|
23
|
-
const TOP_LEVEL_MANAGED_BY_RE = /^managed_by\s*:.*$/m;
|
|
24
|
-
const TOP_LEVEL_PROJECT_RE = /^project\s*:.*$/m;
|
|
25
|
-
|
|
26
|
-
function upsertLanguageLine(content, code) {
|
|
27
|
-
const newLine = `language: ${code}`;
|
|
28
|
-
if (TOP_LEVEL_LANGUAGE_RE.test(content)) {
|
|
29
|
-
return content.replace(TOP_LEVEL_LANGUAGE_RE, newLine);
|
|
30
|
-
}
|
|
31
|
-
const anchorRe = TOP_LEVEL_MANAGED_BY_RE.test(content) ? TOP_LEVEL_MANAGED_BY_RE : TOP_LEVEL_PROJECT_RE;
|
|
32
|
-
if (anchorRe.test(content)) {
|
|
33
|
-
return content.replace(anchorRe, (line) => `${line}\n${newLine}`);
|
|
34
|
-
}
|
|
35
|
-
// Không có project:/managed_by: (manifest lạ) → chèn lên đầu file.
|
|
36
|
-
return `${newLine}\n${content}`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// @returns mã ngôn ngữ được chọn, hoặc "__exit__"/"__back__"/null (không chọn được gì — thiếu
|
|
40
|
-
// TTY/dependency picker, caller tự in usage).
|
|
41
|
-
async function promptLangChoice() {
|
|
42
|
-
const langs = availableLangs();
|
|
43
|
-
const current = lang();
|
|
44
|
-
const choices = langs.map((code) => ({
|
|
45
|
-
name: code === current ? `${langLabel(code)} (${code}) — hiện tại` : `${langLabel(code)} (${code})`,
|
|
46
|
-
value: code,
|
|
47
|
-
short: code,
|
|
48
|
-
}));
|
|
49
|
-
const hotkeys = [
|
|
50
|
-
{ key: "x", label: t("EXIT_LABEL"), value: "__exit__" },
|
|
51
|
-
{ key: "b", label: t("BACK_LABEL"), value: "__back__" },
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
// Ưu tiên menu-prompt (phím tắt x/b nhấn-1-phím); thiếu @inquirer/core → fallback
|
|
56
|
-
// @inquirer/prompts select với item ẩn cuối danh sách cho x/b (vẫn chọn được bằng Enter).
|
|
57
|
-
try {
|
|
58
|
-
return await menuSelect({ message: t("LANGUAGE_SELECT_PROMPT"), choices, hotkeys, pageSize: 10 });
|
|
59
|
-
} catch (err) {
|
|
60
|
-
if (String(err && err.message) !== "no-core") throw err;
|
|
61
|
-
const inq = await loadInquirer();
|
|
62
|
-
if (!inq || typeof inq.select !== "function" || !process.stdin.isTTY) {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
return await inq.select({
|
|
66
|
-
message: t("LANGUAGE_SELECT_PROMPT"),
|
|
67
|
-
choices: [
|
|
68
|
-
...choices,
|
|
69
|
-
new inq.Separator(),
|
|
70
|
-
{ name: `b ${t("BACK_LABEL")}`, value: "__back__", short: "b" },
|
|
71
|
-
{ name: `x ${t("EXIT_LABEL")}`, value: "__exit__", short: "x" },
|
|
72
|
-
],
|
|
73
|
-
pageSize: 10,
|
|
74
|
-
loop: false,
|
|
75
|
-
theme: helpTheme(false),
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
} catch (err) {
|
|
79
|
-
if (isCancelError(err)) return "__exit__";
|
|
80
|
-
throw err;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function runLanguage(args) {
|
|
85
|
-
const manifestPath = findNearestProjectManifest(process.cwd());
|
|
86
|
-
let code = (args[0] || "").trim().toLowerCase();
|
|
87
|
-
|
|
88
|
-
if (!code) {
|
|
89
|
-
if (manifestPath) {
|
|
90
|
-
console.log(t("LANGUAGE_CURRENT__LANG_PATH", { lang: lang(), path: manifestPath }));
|
|
91
|
-
} else {
|
|
92
|
-
console.log(t("LANGUAGE_CURRENT_NO_PROJECT__LANG", { lang: lang() }));
|
|
93
|
-
}
|
|
94
|
-
console.log(t("LANGUAGE_AVAILABLE__LANGS", { langs: availableLangs().map((c) => `${c} (${langLabel(c)})`).join(", ") }));
|
|
95
|
-
code = await promptLangChoice();
|
|
96
|
-
if (code === "__exit__") {
|
|
97
|
-
process.exit(0);
|
|
98
|
-
}
|
|
99
|
-
if (!code || code === "__back__") {
|
|
100
|
-
console.log(t("LANGUAGE_USAGE"));
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const langs = availableLangs();
|
|
106
|
-
if (!langs.includes(code)) {
|
|
107
|
-
throw new Error(t("LANGUAGE_UNKNOWN__LANG_AVAILABLE", { lang: code, available: langs.join(", ") }));
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (!manifestPath) {
|
|
111
|
-
throw new Error(t("LANGUAGE_NO_PROJECT_YAML"));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const content = fs.readFileSync(manifestPath, "utf8");
|
|
115
|
-
const updated = upsertLanguageLine(content, code);
|
|
116
|
-
fs.writeFileSync(manifestPath, updated, "utf8");
|
|
117
|
-
setLangOverride(code);
|
|
118
|
-
console.log(t("LANGUAGE_SET__LANG_PATH", { lang: code, path: manifestPath }));
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
module.exports = { runLanguage };
|
package/lib/languages.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
// Định nghĩa NGÔN NGỮ available của pan CLI — nguồn duy nhất cho tên hiển thị (dùng bởi
|
|
2
|
-
// `pan language`/`pan lang` menu chọn ngôn ngữ). Danh sách mã hợp lệ thực tế lấy từ
|
|
3
|
-
// `Object.keys(messages.js)` (xem lib/i18n.js#availableLangs) — file này chỉ cung cấp
|
|
4
|
-
// tên người-đọc-được cho từng mã.
|
|
5
|
-
//
|
|
6
|
-
// Thêm ngôn ngữ mới:
|
|
7
|
-
// 1. Thêm 1 object con trong lib/messages.js (vd `ja: { ... }`) — copy đủ key từ `en`.
|
|
8
|
-
// 2. Thêm 1 dòng ở đây (mã → tên hiển thị) để menu `pan language` in đúng tên.
|
|
9
|
-
// Thiếu dòng ở đây (mã có trong messages.js nhưng chưa khai ở đây) → menu vẫn hiện, chỉ
|
|
10
|
-
// dùng luôn mã (vd "ja") làm tên hiển thị thay vì tên đầy đủ.
|
|
11
|
-
module.exports = {
|
|
12
|
-
en: { code: "en", name: "English" },
|
|
13
|
-
vi: { code: "vi", name: "Tiếng Việt" },
|
|
14
|
-
};
|