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,223 +0,0 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
const path = require("path");
|
|
3
|
-
|
|
4
|
-
const PLACEHOLDER_VALUES = new Set(["", "repo_url_to_be_filled", "pan_stack_repo_url"]);
|
|
5
|
-
const LOCAL_MANIFEST_FILE = ".project.local.yaml";
|
|
6
|
-
|
|
7
|
-
function cleanValue(value) {
|
|
8
|
-
let result = String(value || "").trim();
|
|
9
|
-
const commentIndex = result.indexOf(" #");
|
|
10
|
-
if (commentIndex >= 0) {
|
|
11
|
-
result = result.slice(0, commentIndex).trimEnd();
|
|
12
|
-
}
|
|
13
|
-
if (
|
|
14
|
-
(result.startsWith('"') && result.endsWith('"')) ||
|
|
15
|
-
(result.startsWith("'") && result.endsWith("'"))
|
|
16
|
-
) {
|
|
17
|
-
result = result.slice(1, -1);
|
|
18
|
-
}
|
|
19
|
-
return result;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function splitKeyValue(text) {
|
|
23
|
-
const index = text.indexOf(":");
|
|
24
|
-
if (index < 0) {
|
|
25
|
-
return [text.trim(), ""];
|
|
26
|
-
}
|
|
27
|
-
return [text.slice(0, index).trim(), cleanValue(text.slice(index + 1))];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function parseManifest(manifestPath) {
|
|
31
|
-
const data = { repositories: [], stack: {} };
|
|
32
|
-
let section = "";
|
|
33
|
-
let currentRepo = null;
|
|
34
|
-
|
|
35
|
-
const lines = fs.readFileSync(manifestPath, "utf8").split(/\r?\n/);
|
|
36
|
-
for (const rawLine of lines) {
|
|
37
|
-
if (!rawLine.trim() || rawLine.trimStart().startsWith("#")) {
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const indent = rawLine.length - rawLine.trimStart().length;
|
|
42
|
-
const text = rawLine.trim();
|
|
43
|
-
|
|
44
|
-
if (indent === 0) {
|
|
45
|
-
const [key, value] = splitKeyValue(text);
|
|
46
|
-
section = key;
|
|
47
|
-
currentRepo = null;
|
|
48
|
-
if (value) {
|
|
49
|
-
data[key] = value;
|
|
50
|
-
} else if (!(key in data)) {
|
|
51
|
-
data[key] = {};
|
|
52
|
-
}
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (section === "stack" && indent === 2) {
|
|
57
|
-
const [key, value] = splitKeyValue(text);
|
|
58
|
-
data.stack[key] = value;
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (section === "repositories") {
|
|
63
|
-
if (indent === 2 && text.startsWith("- ")) {
|
|
64
|
-
currentRepo = {};
|
|
65
|
-
data.repositories.push(currentRepo);
|
|
66
|
-
const rest = text.slice(2).trim();
|
|
67
|
-
if (rest) {
|
|
68
|
-
const [key, value] = splitKeyValue(rest);
|
|
69
|
-
currentRepo[key] = value;
|
|
70
|
-
}
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if (currentRepo && indent === 4) {
|
|
74
|
-
const [key, value] = splitKeyValue(text);
|
|
75
|
-
currentRepo[key] = value;
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return data;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Parse `.project.yaml` rồi GỘP THÊM `repositories:` từ `.project.local.yaml` cùng thư mục
|
|
86
|
-
* (nếu có) — file cá nhân (gitignore), chỉ cần field `repositories:`. Mọi lệnh git built-in
|
|
87
|
-
* (clone/pull/push/fetch/grv/gst/sync-main/mr/checkout) đi qua `loadManifestRepos` (./cli.js) nên
|
|
88
|
-
* tự động thấy repo local — xem lib/manifest.js#parseManifestWithLocal (bản dùng cho .pan.yaml
|
|
89
|
-
* project commands) để biết thêm; 2 nơi khác parser (helpers riêng cho git built-in) nên duplicate
|
|
90
|
-
* logic merge ở đây, KHÔNG duplicate parser.
|
|
91
|
-
*/
|
|
92
|
-
function parseManifestWithLocal(manifestPath) {
|
|
93
|
-
const data = parseManifest(manifestPath);
|
|
94
|
-
const localPath = path.join(path.dirname(manifestPath), LOCAL_MANIFEST_FILE);
|
|
95
|
-
if (fs.existsSync(localPath)) {
|
|
96
|
-
const localData = parseManifest(localPath);
|
|
97
|
-
const localRepos = Array.isArray(localData.repositories) ? localData.repositories : [];
|
|
98
|
-
if (localRepos.length) {
|
|
99
|
-
data.repositories = [...(data.repositories || []), ...localRepos];
|
|
100
|
-
console.error(`ℹ️ +${localRepos.length} repo từ ${LOCAL_MANIFEST_FILE}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return data;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Nhận diện 1 tham số CÓ PHẢI git URL hay không (để `pan clone <url>` phân biệt với
|
|
107
|
-
// `pan clone` (không tham số = clone all) / `pan clone custom.project.yaml` (chỉ định manifest)).
|
|
108
|
-
function looksLikeGitUrl(arg) {
|
|
109
|
-
if (!arg) return false;
|
|
110
|
-
if (/^(https?|ssh|git):\/\//i.test(arg)) return true;
|
|
111
|
-
if (/^[\w.-]+@[\w.-]+:.+/.test(arg)) return true; // scp-like: git@host:group/repo.git
|
|
112
|
-
if (/\.git$/i.test(arg)) return true;
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Tách tên dự án từ git URL — bỏ đuôi ".git", lấy phần cuối sau "/" (hoặc ":" kiểu scp-like).
|
|
117
|
-
// https://host/group/my-repo.git -> my-repo
|
|
118
|
-
// git@github.com:user/my-repo.git -> my-repo
|
|
119
|
-
// https://github.com/user/my-repo -> my-repo
|
|
120
|
-
function repoNameFromUrl(url) {
|
|
121
|
-
let s = String(url || "").trim().replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
122
|
-
const idx = Math.max(s.lastIndexOf("/"), s.lastIndexOf(":"));
|
|
123
|
-
return idx >= 0 ? s.slice(idx + 1) : s;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Thêm 1 entry mới vào cuối danh sách `repositories:` trong `.project.yaml` (ghi trực tiếp vào
|
|
128
|
-
* FILE, không phải `.project.local.yaml`) — dùng bởi `pan clone <url>`. Chèn ngay TRƯỚC section
|
|
129
|
-
* top-level kế tiếp (nếu `repositories:` không phải section cuối file), hoặc cuối file nếu là
|
|
130
|
-
* section cuối; nếu file chưa có `repositories:` thì thêm mới section này ở cuối.
|
|
131
|
-
* @param {string} manifestPath
|
|
132
|
-
* @param {{name:string, url:string, path:string, description?:string}} repo
|
|
133
|
-
*/
|
|
134
|
-
function appendRepository(manifestPath, repo) {
|
|
135
|
-
const content = fs.readFileSync(manifestPath, "utf8");
|
|
136
|
-
const lines = content.split(/\r?\n/);
|
|
137
|
-
// Bỏ các dòng trống Ở CUỐI (do file gốc kết bằng newline) — tránh chèn nhầm sau dòng trống đó
|
|
138
|
-
// (sinh ra 1 dòng trống thừa) và đảm bảo luôn ghi lại với ĐÚNG 1 newline cuối file.
|
|
139
|
-
while (lines.length && lines[lines.length - 1].trim() === "") {
|
|
140
|
-
lines.pop();
|
|
141
|
-
}
|
|
142
|
-
const entryLines = [
|
|
143
|
-
` - name: ${repo.name}`,
|
|
144
|
-
...(repo.description ? [` description: ${repo.description}`] : []),
|
|
145
|
-
` url: ${repo.url}`,
|
|
146
|
-
` path: ${repo.path}`,
|
|
147
|
-
];
|
|
148
|
-
|
|
149
|
-
const repoLineIdx = lines.findIndex((l) => /^repositories:\s*$/.test(l));
|
|
150
|
-
if (repoLineIdx === -1) {
|
|
151
|
-
const trimmed = content.replace(/\s+$/, "");
|
|
152
|
-
fs.writeFileSync(manifestPath, `${trimmed}\n\nrepositories:\n${entryLines.join("\n")}\n`, "utf8");
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
let endIdx = lines.length;
|
|
157
|
-
for (let i = repoLineIdx + 1; i < lines.length; i += 1) {
|
|
158
|
-
const line = lines[i];
|
|
159
|
-
if (line.trim() === "") continue;
|
|
160
|
-
const indent = line.length - line.trimStart().length;
|
|
161
|
-
if (indent === 0) {
|
|
162
|
-
endIdx = i;
|
|
163
|
-
break;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
const newLines = [...lines.slice(0, endIdx), ...entryLines, ...lines.slice(endIdx)];
|
|
167
|
-
fs.writeFileSync(manifestPath, `${newLines.join("\n")}\n`, "utf8");
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function uniqueRepos(repos) {
|
|
171
|
-
const seen = new Set();
|
|
172
|
-
const result = [];
|
|
173
|
-
for (const repo of repos) {
|
|
174
|
-
const key = [
|
|
175
|
-
cleanValue(repo.name),
|
|
176
|
-
cleanValue(repo.url),
|
|
177
|
-
cleanValue(repo.path),
|
|
178
|
-
].join("\u0000");
|
|
179
|
-
if (seen.has(key)) {
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
seen.add(key);
|
|
183
|
-
result.push(repo);
|
|
184
|
-
}
|
|
185
|
-
return result;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function manifestRepos(data) {
|
|
189
|
-
const repositories = data.repositories || [];
|
|
190
|
-
return uniqueRepos(repositories);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function validateRepo(repo) {
|
|
194
|
-
const name = cleanValue(repo.name);
|
|
195
|
-
const url = cleanValue(repo.url);
|
|
196
|
-
const repoPath = cleanValue(repo.path) || name;
|
|
197
|
-
|
|
198
|
-
if (!name) {
|
|
199
|
-
throw new Error("Repository entry is missing 'name'.");
|
|
200
|
-
}
|
|
201
|
-
if (PLACEHOLDER_VALUES.has(url)) {
|
|
202
|
-
throw new Error(`Repository '${name}' is missing a real url.`);
|
|
203
|
-
}
|
|
204
|
-
if (!repoPath) {
|
|
205
|
-
throw new Error(`Repository '${name}' is missing 'path'.`);
|
|
206
|
-
}
|
|
207
|
-
return { name, url, repoPath };
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
module.exports = {
|
|
211
|
-
PLACEHOLDER_VALUES,
|
|
212
|
-
LOCAL_MANIFEST_FILE,
|
|
213
|
-
cleanValue,
|
|
214
|
-
splitKeyValue,
|
|
215
|
-
parseManifest,
|
|
216
|
-
parseManifestWithLocal,
|
|
217
|
-
looksLikeGitUrl,
|
|
218
|
-
repoNameFromUrl,
|
|
219
|
-
appendRepository,
|
|
220
|
-
uniqueRepos,
|
|
221
|
-
manifestRepos,
|
|
222
|
-
validateRepo,
|
|
223
|
-
};
|
|
@@ -1,416 +0,0 @@
|
|
|
1
|
-
const readline = require("readline");
|
|
2
|
-
const { isGitRepo } = require("./git");
|
|
3
|
-
const { helpTheme } = require("../../inquirer-theme");
|
|
4
|
-
const { t } = require("../../i18n");
|
|
5
|
-
const { menuSelect, menuCheckbox } = require("../../menu-prompt");
|
|
6
|
-
|
|
7
|
-
// Lazy-load @inquirer/prompts (ESM) from CJS via dynamic import. Cached so
|
|
8
|
-
// repeated pickers in one run reuse the same module. Returns null when the
|
|
9
|
-
// dependency is missing (e.g. deps not installed) so callers can fall back to
|
|
10
|
-
// the readline-based prompts.
|
|
11
|
-
let inquirerModulePromise = null;
|
|
12
|
-
async function loadInquirer() {
|
|
13
|
-
if (inquirerModulePromise === null) {
|
|
14
|
-
inquirerModulePromise = import("@inquirer/prompts").catch(() => null);
|
|
15
|
-
}
|
|
16
|
-
return inquirerModulePromise;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function isCancelError(err) {
|
|
20
|
-
if (!err) {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
const name = String(err.name || "");
|
|
24
|
-
return (
|
|
25
|
-
name === "ExitPromptError" ||
|
|
26
|
-
name === "CancelPromptError" ||
|
|
27
|
-
/User force closed/i.test(String(err.message || ""))
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function buildTargets({ repos, workspaceRoot, includeWorkspace }) {
|
|
32
|
-
const targets = [];
|
|
33
|
-
if (includeWorkspace && isGitRepo(workspaceRoot)) {
|
|
34
|
-
targets.push({
|
|
35
|
-
name: "workspace",
|
|
36
|
-
destination: workspaceRoot,
|
|
37
|
-
label: "workspace (stack root)",
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
for (const repo of repos) {
|
|
41
|
-
targets.push({
|
|
42
|
-
name: repo.name,
|
|
43
|
-
destination: repo.destination,
|
|
44
|
-
label: repo.name,
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
return targets;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function findTargetByName(targets, name) {
|
|
51
|
-
const needle = String(name || "").trim();
|
|
52
|
-
if (!needle) {
|
|
53
|
-
return [];
|
|
54
|
-
}
|
|
55
|
-
return targets.filter((target) => target.name === needle);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function splitSelectionTokens(text) {
|
|
59
|
-
return String(text || "")
|
|
60
|
-
.split(/[\s,]+/)
|
|
61
|
-
.map((tok) => tok.trim())
|
|
62
|
-
.filter(Boolean);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function findTargetsByNames(targets, names) {
|
|
66
|
-
const seen = new Set();
|
|
67
|
-
const picked = [];
|
|
68
|
-
const missing = [];
|
|
69
|
-
for (const raw of names) {
|
|
70
|
-
const name = String(raw || "").trim();
|
|
71
|
-
if (!name) {
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
const match = targets.find((target) => target.name === name);
|
|
75
|
-
if (!match) {
|
|
76
|
-
missing.push(name);
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (seen.has(match.name)) {
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
seen.add(match.name);
|
|
83
|
-
picked.push(match);
|
|
84
|
-
}
|
|
85
|
-
return { picked, missing };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function promptRepoChoiceReadline(targets, message) {
|
|
89
|
-
return new Promise((resolve, reject) => {
|
|
90
|
-
const rl = readline.createInterface({
|
|
91
|
-
input: process.stdin,
|
|
92
|
-
output: process.stdout,
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
console.log("");
|
|
96
|
-
console.log(message);
|
|
97
|
-
console.log(` 1. ${t("REPO_SELECT_ALL__N", { n: targets.length })}`);
|
|
98
|
-
targets.forEach((target, index) => {
|
|
99
|
-
console.log(` ${index + 2}. ${target.label}`);
|
|
100
|
-
console.log(` ${target.destination}`);
|
|
101
|
-
});
|
|
102
|
-
console.log("");
|
|
103
|
-
|
|
104
|
-
rl.question(t("REPO_SELECT_SINGLE_PROMPT"), (answer) => {
|
|
105
|
-
rl.close();
|
|
106
|
-
const choice = Number.parseInt(String(answer).trim(), 10);
|
|
107
|
-
if (
|
|
108
|
-
!Number.isFinite(choice) ||
|
|
109
|
-
choice < 1 ||
|
|
110
|
-
choice > targets.length + 1
|
|
111
|
-
) {
|
|
112
|
-
reject(new Error(t("REPO_SELECT_INVALID")));
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
if (choice === 1) {
|
|
116
|
-
resolve(targets);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
resolve([targets[choice - 2]]);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const ALL_SENTINEL = "__all__";
|
|
125
|
-
|
|
126
|
-
async function promptRepoChoice(targets, { message } = {}) {
|
|
127
|
-
const promptMsg = message || t("REPO_SELECT_PROMPT_MSG");
|
|
128
|
-
const choices = [
|
|
129
|
-
{
|
|
130
|
-
name: t("REPO_SELECT_ALL__N", { n: targets.length }),
|
|
131
|
-
value: ALL_SENTINEL,
|
|
132
|
-
short: "all",
|
|
133
|
-
},
|
|
134
|
-
...targets.map((target) => ({
|
|
135
|
-
name: `${target.label} ${dimHint(target.destination)}`,
|
|
136
|
-
value: target,
|
|
137
|
-
short: target.name,
|
|
138
|
-
})),
|
|
139
|
-
];
|
|
140
|
-
const hotkeys = [{ key: "x", label: t("EXIT_LABEL"), value: "__exit__" }];
|
|
141
|
-
|
|
142
|
-
// Ưu tiên menu-prompt (phím x thoát ngay); thiếu @inquirer/core → @inquirer/prompts select
|
|
143
|
-
// với item thoát ẩn ở cuối; thiếu cả hai → readline thô (không hotkey, Ctrl+C để thoát).
|
|
144
|
-
try {
|
|
145
|
-
const picked = await menuSelect({ message: promptMsg, choices, pageSize: Math.min(Math.max(targets.length + 2, 8), 20), hotkeys });
|
|
146
|
-
if (picked === "__exit__") {
|
|
147
|
-
console.log(t("EXITED"));
|
|
148
|
-
process.exit(0);
|
|
149
|
-
}
|
|
150
|
-
return picked === ALL_SENTINEL ? targets.slice() : [picked];
|
|
151
|
-
} catch (err) {
|
|
152
|
-
if (String(err && err.message) !== "no-core") {
|
|
153
|
-
if (isCancelError(err)) throw new Error(t("REPO_SELECT_CANCELLED"));
|
|
154
|
-
throw err;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const inq = await loadInquirer();
|
|
159
|
-
if (!inq || typeof inq.select !== "function") {
|
|
160
|
-
return promptRepoChoiceReadline(targets, promptMsg);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
const picked = await inq.select({
|
|
165
|
-
message: promptMsg,
|
|
166
|
-
choices: [...choices, new inq.Separator(), { name: `x ${t("EXIT_LABEL")}`, value: "__exit__", short: "x" }],
|
|
167
|
-
pageSize: Math.min(Math.max(targets.length + 2, 8), 20),
|
|
168
|
-
loop: false,
|
|
169
|
-
theme: helpTheme(false),
|
|
170
|
-
});
|
|
171
|
-
if (picked === "__exit__") {
|
|
172
|
-
console.log(t("EXITED"));
|
|
173
|
-
process.exit(0);
|
|
174
|
-
}
|
|
175
|
-
return picked === ALL_SENTINEL ? targets.slice() : [picked];
|
|
176
|
-
} catch (err) {
|
|
177
|
-
if (isCancelError(err)) {
|
|
178
|
-
throw new Error(t("REPO_SELECT_CANCELLED"));
|
|
179
|
-
}
|
|
180
|
-
throw err;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async function resolveTargets({
|
|
185
|
-
repos,
|
|
186
|
-
workspaceRoot,
|
|
187
|
-
includeWorkspace,
|
|
188
|
-
selection,
|
|
189
|
-
message,
|
|
190
|
-
}) {
|
|
191
|
-
const targets = buildTargets({ repos, workspaceRoot, includeWorkspace });
|
|
192
|
-
if (targets.length === 0) {
|
|
193
|
-
throw new Error(t("REPO_SELECT_NO_REPO_SYNC"));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const picked = String(selection || "").trim();
|
|
197
|
-
if (picked === "all") {
|
|
198
|
-
return targets;
|
|
199
|
-
}
|
|
200
|
-
if (picked) {
|
|
201
|
-
const matches = findTargetByName(targets, picked);
|
|
202
|
-
if (matches.length === 0) {
|
|
203
|
-
const names = targets.map((target) => target.name).join(", ");
|
|
204
|
-
throw new Error(
|
|
205
|
-
t("REPO_SELECT_ONE_MISSING__PICKED_ALL", { picked, all: names })
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
return matches;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (process.stdin.isTTY) {
|
|
212
|
-
return promptRepoChoice(targets, { message: message || t("REPO_SELECT_SYNCMAIN_MSG") });
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return targets;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function pickTargetsByIndices(targets, indices) {
|
|
219
|
-
const seen = new Set();
|
|
220
|
-
const picked = [];
|
|
221
|
-
for (const raw of indices) {
|
|
222
|
-
const num = Number.parseInt(String(raw).trim(), 10);
|
|
223
|
-
if (!Number.isFinite(num) || num < 1 || num > targets.length + 1) {
|
|
224
|
-
const err = new Error(t("REPO_SELECT_INVALID_INDEX__RAW", { raw }));
|
|
225
|
-
err.code = "INVALID_CHOICE";
|
|
226
|
-
throw err;
|
|
227
|
-
}
|
|
228
|
-
if (num === 1) {
|
|
229
|
-
return targets.slice();
|
|
230
|
-
}
|
|
231
|
-
const idx = num - 2;
|
|
232
|
-
if (seen.has(idx)) {
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
seen.add(idx);
|
|
236
|
-
picked.push(targets[idx]);
|
|
237
|
-
}
|
|
238
|
-
return picked;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function promptRepoChoiceMultiReadline(targets, { action } = {}) {
|
|
242
|
-
const actionLabel = action || t("REPO_SELECT_ACTION_CHOOSE");
|
|
243
|
-
return new Promise((resolve, reject) => {
|
|
244
|
-
const rl = readline.createInterface({
|
|
245
|
-
input: process.stdin,
|
|
246
|
-
output: process.stdout,
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
console.log("");
|
|
250
|
-
console.log(
|
|
251
|
-
t("REPO_SELECT_MULTI_HELP__ACTION", { action: actionLabel })
|
|
252
|
-
);
|
|
253
|
-
console.log(` 1. ${t("REPO_SELECT_ALL__N", { n: targets.length })}`);
|
|
254
|
-
targets.forEach((target, index) => {
|
|
255
|
-
console.log(` ${index + 2}. ${target.label}`);
|
|
256
|
-
console.log(` ${target.destination}`);
|
|
257
|
-
});
|
|
258
|
-
console.log("");
|
|
259
|
-
|
|
260
|
-
rl.question(t("REPO_SELECT_MULTI_PROMPT"), (answer) => {
|
|
261
|
-
rl.close();
|
|
262
|
-
const raw = String(answer || "").trim();
|
|
263
|
-
if (!raw) {
|
|
264
|
-
reject(new Error(t("REPO_SELECT_EMPTY_ANSWER")));
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
if (raw.toLowerCase() === "all") {
|
|
268
|
-
resolve(targets.slice());
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
const tokens = splitSelectionTokens(raw);
|
|
272
|
-
if (tokens.length === 0) {
|
|
273
|
-
reject(new Error(t("REPO_SELECT_INVALID")));
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
try {
|
|
277
|
-
const picked = pickTargetsByIndices(targets, tokens);
|
|
278
|
-
if (picked.length === 0) {
|
|
279
|
-
reject(new Error(t("REPO_SELECT_NONE_PICKED")));
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
resolve(picked);
|
|
283
|
-
} catch (err) {
|
|
284
|
-
reject(err);
|
|
285
|
-
}
|
|
286
|
-
});
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
async function promptRepoChoiceMulti(targets, { action } = {}) {
|
|
291
|
-
const actionLabel = action || t("REPO_SELECT_ACTION_CHOOSE");
|
|
292
|
-
const choices = targets.map((target, i) => ({
|
|
293
|
-
name: `${target.label} ${dimHint(target.destination)}`,
|
|
294
|
-
value: i,
|
|
295
|
-
short: target.name,
|
|
296
|
-
}));
|
|
297
|
-
const hotkeys = [{ key: "x", label: t("EXIT_LABEL"), value: "exit" }];
|
|
298
|
-
|
|
299
|
-
// Ưu tiên menu-prompt (menuCheckbox: có sẵn a=chọn tất cả, i=đảo chọn, x=thoát) — nhất quán
|
|
300
|
-
// với mọi nơi chọn-nhiều khác trong CLI; thiếu @inquirer/core → @inquirer/prompts checkbox
|
|
301
|
-
// (không có a/i, chỉ Enter/Space); thiếu cả hai → readline thô.
|
|
302
|
-
try {
|
|
303
|
-
const res = await menuCheckbox({
|
|
304
|
-
message: actionLabel,
|
|
305
|
-
choices,
|
|
306
|
-
hotkeys,
|
|
307
|
-
pageSize: Math.min(Math.max(targets.length + 2, 8), 20),
|
|
308
|
-
});
|
|
309
|
-
if (res.action === "exit") {
|
|
310
|
-
console.log(t("EXITED"));
|
|
311
|
-
process.exit(0);
|
|
312
|
-
}
|
|
313
|
-
const values = res.values || [];
|
|
314
|
-
if (values.length === 0) {
|
|
315
|
-
throw new Error(t("REPO_SELECT_NONE_PICKED"));
|
|
316
|
-
}
|
|
317
|
-
return values.map((i) => targets[i]);
|
|
318
|
-
} catch (err) {
|
|
319
|
-
if (String(err && err.message) !== "no-core") {
|
|
320
|
-
if (isCancelError(err)) throw new Error(t("REPO_SELECT_CANCELLED"));
|
|
321
|
-
throw err;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const inq = await loadInquirer();
|
|
326
|
-
if (!inq || typeof inq.checkbox !== "function") {
|
|
327
|
-
return promptRepoChoiceMultiReadline(targets, { action: actionLabel });
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
try {
|
|
331
|
-
const picked = await inq.checkbox({
|
|
332
|
-
message: actionLabel,
|
|
333
|
-
choices: targets.map((target) => ({
|
|
334
|
-
name: `${target.label} ${dimHint(target.destination)}`,
|
|
335
|
-
value: target,
|
|
336
|
-
short: target.name,
|
|
337
|
-
})),
|
|
338
|
-
required: true,
|
|
339
|
-
pageSize: Math.min(Math.max(targets.length + 2, 8), 20),
|
|
340
|
-
theme: helpTheme(true),
|
|
341
|
-
});
|
|
342
|
-
if (!Array.isArray(picked) || picked.length === 0) {
|
|
343
|
-
throw new Error(t("REPO_SELECT_NONE_PICKED"));
|
|
344
|
-
}
|
|
345
|
-
return picked;
|
|
346
|
-
} catch (err) {
|
|
347
|
-
if (isCancelError(err)) {
|
|
348
|
-
throw new Error(t("REPO_SELECT_CANCELLED"));
|
|
349
|
-
}
|
|
350
|
-
throw err;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function dimHint(text) {
|
|
355
|
-
if (!process.stdout.isTTY || process.env.NO_COLOR) {
|
|
356
|
-
return `— ${text}`;
|
|
357
|
-
}
|
|
358
|
-
return `\u001b[2m— ${text}\u001b[0m`;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
async function resolveMultiTargets({
|
|
362
|
-
repos,
|
|
363
|
-
workspaceRoot,
|
|
364
|
-
includeWorkspace,
|
|
365
|
-
selection,
|
|
366
|
-
action,
|
|
367
|
-
}) {
|
|
368
|
-
const actionLabel = action || t("REPO_SELECT_ACTION_DEFAULT");
|
|
369
|
-
const targets = buildTargets({ repos, workspaceRoot, includeWorkspace });
|
|
370
|
-
if (targets.length === 0) {
|
|
371
|
-
throw new Error(t("REPO_SELECT_NO_REPO"));
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const raw = String(selection || "").trim();
|
|
375
|
-
if (raw.toLowerCase() === "all") {
|
|
376
|
-
return targets.slice();
|
|
377
|
-
}
|
|
378
|
-
if (raw) {
|
|
379
|
-
const tokens = splitSelectionTokens(raw);
|
|
380
|
-
const allNumeric =
|
|
381
|
-
tokens.length > 0 && tokens.every((tok) => /^[0-9]+$/.test(tok));
|
|
382
|
-
if (allNumeric) {
|
|
383
|
-
const picked = pickTargetsByIndices(targets, tokens);
|
|
384
|
-
if (picked.length === 0) {
|
|
385
|
-
throw new Error(t("REPO_SELECT_NONE_PICKED"));
|
|
386
|
-
}
|
|
387
|
-
return picked;
|
|
388
|
-
}
|
|
389
|
-
const { picked, missing } = findTargetsByNames(targets, tokens);
|
|
390
|
-
if (missing.length > 0) {
|
|
391
|
-
const names = targets.map((target) => target.name).join(", ");
|
|
392
|
-
throw new Error(
|
|
393
|
-
t("REPO_SELECT_MISSING__MISSING_ALL", { missing: missing.join(", "), all: names })
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
|
-
if (picked.length === 0) {
|
|
397
|
-
throw new Error(t("REPO_SELECT_NONE_PICKED"));
|
|
398
|
-
}
|
|
399
|
-
return picked;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
if (process.stdin.isTTY) {
|
|
403
|
-
return promptRepoChoiceMulti(targets, { action: actionLabel });
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
throw new Error(t("REPO_SELECT_NEED_NON_INTERACTIVE"));
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
module.exports = {
|
|
410
|
-
buildTargets,
|
|
411
|
-
findTargetsByNames,
|
|
412
|
-
pickTargetsByIndices,
|
|
413
|
-
resolveTargets,
|
|
414
|
-
resolveMultiTargets,
|
|
415
|
-
splitSelectionTokens,
|
|
416
|
-
};
|