pan-stack-cli 0.2.1 → 0.2.2
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 +4955 -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
package/lib/manifest.js
DELETED
|
@@ -1,467 +0,0 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
const path = require("path");
|
|
3
|
-
const { t } = require("./i18n");
|
|
4
|
-
|
|
5
|
-
const PLACEHOLDER_VALUES = new Set(["", "repo_url_to_be_filled", "pan_stack_repo_url"]);
|
|
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: {}, commands: {} };
|
|
32
|
-
let section = "";
|
|
33
|
-
let currentRepo = null;
|
|
34
|
-
let currentCommandName = "";
|
|
35
|
-
let currentCommand = null;
|
|
36
|
-
let currentTargetName = "";
|
|
37
|
-
let blockMode = null;
|
|
38
|
-
let blockIndent = 0;
|
|
39
|
-
let blockLines = [];
|
|
40
|
-
|
|
41
|
-
function flushBlock() {
|
|
42
|
-
if (!blockMode || !currentCommand) {
|
|
43
|
-
blockMode = null;
|
|
44
|
-
blockLines = [];
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
const body = blockLines.join("\n");
|
|
48
|
-
if (blockMode === "command-shell") {
|
|
49
|
-
currentCommand.shell = body;
|
|
50
|
-
} else if (blockMode === "command-sequence") {
|
|
51
|
-
currentCommand.sequence = body;
|
|
52
|
-
} else if (blockMode === "target-shell" && currentCommand.targets[currentTargetName]) {
|
|
53
|
-
currentCommand.targets[currentTargetName].shell = body;
|
|
54
|
-
}
|
|
55
|
-
blockMode = null;
|
|
56
|
-
blockLines = [];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const lines = fs.readFileSync(manifestPath, "utf8").split(/\r?\n/);
|
|
60
|
-
for (const rawLine of lines) {
|
|
61
|
-
if (blockMode) {
|
|
62
|
-
const trimmed = rawLine.trimEnd();
|
|
63
|
-
if (!trimmed.trim()) {
|
|
64
|
-
blockLines.push("");
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
const indent = rawLine.length - rawLine.trimStart().length;
|
|
68
|
-
if (indent < blockIndent) {
|
|
69
|
-
flushBlock();
|
|
70
|
-
} else {
|
|
71
|
-
blockLines.push(rawLine.slice(blockIndent));
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (!rawLine.trim() || rawLine.trimStart().startsWith("#")) {
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const indent = rawLine.length - rawLine.trimStart().length;
|
|
81
|
-
const text = rawLine.trim();
|
|
82
|
-
|
|
83
|
-
if (indent === 0) {
|
|
84
|
-
flushBlock();
|
|
85
|
-
const [key, value] = splitKeyValue(text);
|
|
86
|
-
section = key;
|
|
87
|
-
currentRepo = null;
|
|
88
|
-
currentCommandName = "";
|
|
89
|
-
currentCommand = null;
|
|
90
|
-
currentTargetName = "";
|
|
91
|
-
if (value) {
|
|
92
|
-
data[key] = value;
|
|
93
|
-
} else if (!(key in data)) {
|
|
94
|
-
data[key] = key === "commands" ? {} : {};
|
|
95
|
-
}
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (section === "stack" && indent === 2) {
|
|
100
|
-
const [key, value] = splitKeyValue(text);
|
|
101
|
-
data.stack[key] = value;
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (section === "repositories") {
|
|
106
|
-
if (indent === 2 && text.startsWith("- ")) {
|
|
107
|
-
currentRepo = {};
|
|
108
|
-
data.repositories.push(currentRepo);
|
|
109
|
-
const rest = text.slice(2).trim();
|
|
110
|
-
if (rest) {
|
|
111
|
-
const [key, value] = splitKeyValue(rest);
|
|
112
|
-
currentRepo[key] = value;
|
|
113
|
-
}
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
if (currentRepo && indent === 4) {
|
|
117
|
-
const [key, value] = splitKeyValue(text);
|
|
118
|
-
currentRepo[key] = value;
|
|
119
|
-
}
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (section === "commands") {
|
|
124
|
-
if (indent === 2) {
|
|
125
|
-
flushBlock();
|
|
126
|
-
const [key, value] = splitKeyValue(text);
|
|
127
|
-
currentCommandName = key;
|
|
128
|
-
currentCommand = {};
|
|
129
|
-
currentTargetName = "";
|
|
130
|
-
data.commands[currentCommandName] = currentCommand;
|
|
131
|
-
if (value) {
|
|
132
|
-
currentCommand[key] = value;
|
|
133
|
-
}
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (indent === 4 && currentCommand) {
|
|
138
|
-
flushBlock();
|
|
139
|
-
const [key, value] = splitKeyValue(text);
|
|
140
|
-
if (key === "shell" && (value === "|" || value === ">")) {
|
|
141
|
-
blockMode = "command-shell";
|
|
142
|
-
blockIndent = indent + 2;
|
|
143
|
-
blockLines = [];
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
if (key === "sequence" && (value === "|" || value === ">")) {
|
|
147
|
-
blockMode = "command-sequence";
|
|
148
|
-
blockIndent = indent + 2;
|
|
149
|
-
blockLines = [];
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
if (key === "targets") {
|
|
153
|
-
currentCommand.targets = currentCommand.targets || {};
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
currentCommand[key] = value;
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (indent === 6 && currentCommand) {
|
|
161
|
-
flushBlock();
|
|
162
|
-
const [key, value] = splitKeyValue(text);
|
|
163
|
-
currentTargetName = key;
|
|
164
|
-
currentCommand.targets = currentCommand.targets || {};
|
|
165
|
-
const target = {};
|
|
166
|
-
currentCommand.targets[currentTargetName] = target;
|
|
167
|
-
if (value) {
|
|
168
|
-
target[key] = value;
|
|
169
|
-
}
|
|
170
|
-
if (key === "shell" && (value === "|" || value === ">")) {
|
|
171
|
-
blockMode = "target-shell";
|
|
172
|
-
blockIndent = indent + 2;
|
|
173
|
-
blockLines = [];
|
|
174
|
-
}
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (indent === 8 && currentCommand && currentCommand.targets && currentTargetName) {
|
|
179
|
-
flushBlock();
|
|
180
|
-
const target = currentCommand.targets[currentTargetName];
|
|
181
|
-
const [key, value] = splitKeyValue(text);
|
|
182
|
-
if (key === "shell" && (value === "|" || value === ">")) {
|
|
183
|
-
blockMode = "target-shell";
|
|
184
|
-
blockIndent = indent + 2;
|
|
185
|
-
blockLines = [];
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
target[key] = value;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
flushBlock();
|
|
194
|
-
return data;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function uniqueRepos(repos) {
|
|
198
|
-
const seen = new Set();
|
|
199
|
-
const result = [];
|
|
200
|
-
for (const repo of repos) {
|
|
201
|
-
const key = [
|
|
202
|
-
cleanValue(repo.name),
|
|
203
|
-
cleanValue(repo.url),
|
|
204
|
-
cleanValue(repo.path),
|
|
205
|
-
].join("\u0000");
|
|
206
|
-
if (seen.has(key)) {
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
seen.add(key);
|
|
210
|
-
result.push(repo);
|
|
211
|
-
}
|
|
212
|
-
return result;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function manifestRepos(data) {
|
|
216
|
-
return uniqueRepos(data.repositories || []);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function validateRepo(repo) {
|
|
220
|
-
const name = cleanValue(repo.name);
|
|
221
|
-
const url = cleanValue(repo.url);
|
|
222
|
-
const repoPath = cleanValue(repo.path) || name;
|
|
223
|
-
|
|
224
|
-
if (!name) {
|
|
225
|
-
throw new Error(t("REPO_MISSING_NAME"));
|
|
226
|
-
}
|
|
227
|
-
if (PLACEHOLDER_VALUES.has(url)) {
|
|
228
|
-
throw new Error(t("REPO_MISSING_URL__NAME", { name }));
|
|
229
|
-
}
|
|
230
|
-
if (!repoPath) {
|
|
231
|
-
throw new Error(t("REPO_MISSING_PATH__NAME", { name }));
|
|
232
|
-
}
|
|
233
|
-
return { name, url, repoPath };
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const LOCAL_MANIFEST_FILE = ".project.local.yaml";
|
|
237
|
-
|
|
238
|
-
function localManifestPath(manifestPath) {
|
|
239
|
-
return path.join(path.dirname(manifestPath), LOCAL_MANIFEST_FILE);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Parse `.project.yaml` rồi GỘP THÊM `repositories:` từ `.project.local.yaml` cùng thư mục
|
|
244
|
-
* (nếu có) — file cá nhân, KHÔNG commit (gitignore), chỉ cần có field `repositories:`. Dùng để
|
|
245
|
-
* mỗi người tự thêm repo riêng (thử nghiệm, fork cá nhân…) mà không đụng `.project.yaml` chung.
|
|
246
|
-
* Mọi lệnh đọc repo (clone/pull/push/fetch/grv/gst/sync-main/mr/checkout/stats/pan.yaml sources)
|
|
247
|
-
* đều đi qua đây (loadManifest / loadManifestRepos) nên tự động thấy repo local, không cần sửa
|
|
248
|
-
* riêng từng lệnh.
|
|
249
|
-
*/
|
|
250
|
-
function parseManifestWithLocal(manifestPath) {
|
|
251
|
-
const data = parseManifest(manifestPath);
|
|
252
|
-
const localPath = localManifestPath(manifestPath);
|
|
253
|
-
if (fs.existsSync(localPath)) {
|
|
254
|
-
const localData = parseManifest(localPath);
|
|
255
|
-
const localRepos = Array.isArray(localData.repositories) ? localData.repositories : [];
|
|
256
|
-
if (localRepos.length) {
|
|
257
|
-
data.repositories = [...(data.repositories || []), ...localRepos];
|
|
258
|
-
console.error(`ℹ️ +${localRepos.length} repo từ ${LOCAL_MANIFEST_FILE}`);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
return data;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function loadManifest(manifestPath) {
|
|
265
|
-
const resolved = path.resolve(manifestPath);
|
|
266
|
-
if (!fs.existsSync(resolved)) {
|
|
267
|
-
throw new Error(t("MANIFEST_NOT_FOUND__PATH", { path: manifestPath }));
|
|
268
|
-
}
|
|
269
|
-
const data = parseManifestWithLocal(resolved);
|
|
270
|
-
return {
|
|
271
|
-
path: resolved,
|
|
272
|
-
root: path.dirname(resolved),
|
|
273
|
-
project: cleanValue(data.project) || path.basename(path.dirname(resolved)),
|
|
274
|
-
managedBy: cleanValue(data.managed_by),
|
|
275
|
-
stack: data.stack || {},
|
|
276
|
-
repositories: manifestRepos(data),
|
|
277
|
-
commands: data.commands || {},
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const PAN_CONFIG_FILE = ".pan.yaml";
|
|
282
|
-
|
|
283
|
-
function loadPanConfig(configPath, meta = {}) {
|
|
284
|
-
const resolved = path.resolve(configPath);
|
|
285
|
-
if (!fs.existsSync(resolved)) {
|
|
286
|
-
throw new Error(t("CONFIG_NOT_FOUND__PATH", { path: configPath }));
|
|
287
|
-
}
|
|
288
|
-
const data = parseManifest(resolved);
|
|
289
|
-
const root = path.dirname(resolved);
|
|
290
|
-
return {
|
|
291
|
-
path: resolved,
|
|
292
|
-
root,
|
|
293
|
-
workspaceRoot: meta.workspaceRoot || root,
|
|
294
|
-
project: cleanValue(data.project) || meta.project || path.basename(root),
|
|
295
|
-
repoName: meta.repoName || "",
|
|
296
|
-
scope: meta.scope || "repo",
|
|
297
|
-
commands: data.commands || {},
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function commandAliases(def) {
|
|
302
|
-
if (!def) {
|
|
303
|
-
return [];
|
|
304
|
-
}
|
|
305
|
-
const result = [];
|
|
306
|
-
if (def.alias) {
|
|
307
|
-
result.push(cleanValue(def.alias));
|
|
308
|
-
}
|
|
309
|
-
if (def.aliases) {
|
|
310
|
-
const raw = def.aliases;
|
|
311
|
-
if (Array.isArray(raw)) {
|
|
312
|
-
result.push(...raw.map((item) => cleanValue(item)));
|
|
313
|
-
} else {
|
|
314
|
-
const value = cleanValue(raw);
|
|
315
|
-
if (value.startsWith("[") && value.endsWith("]")) {
|
|
316
|
-
result.push(
|
|
317
|
-
...value
|
|
318
|
-
.slice(1, -1)
|
|
319
|
-
.split(",")
|
|
320
|
-
.map((item) => cleanValue(item))
|
|
321
|
-
.filter(Boolean)
|
|
322
|
-
);
|
|
323
|
-
} else if (value.includes(",")) {
|
|
324
|
-
result.push(
|
|
325
|
-
...value
|
|
326
|
-
.split(",")
|
|
327
|
-
.map((item) => cleanValue(item))
|
|
328
|
-
.filter(Boolean)
|
|
329
|
-
);
|
|
330
|
-
} else if (value) {
|
|
331
|
-
result.push(value);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
return [...new Set(result.filter(Boolean))];
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function resolveCommandName(sources, commandName) {
|
|
339
|
-
if (sources.some((source) => source.commands[commandName])) {
|
|
340
|
-
return commandName;
|
|
341
|
-
}
|
|
342
|
-
for (const source of sources) {
|
|
343
|
-
for (const [primaryName, def] of Object.entries(source.commands || {})) {
|
|
344
|
-
if (commandAliases(def).includes(commandName)) {
|
|
345
|
-
return primaryName;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
return commandName;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function getCommandDefinition(source, commandName, subArg) {
|
|
353
|
-
const def = source.commands[commandName];
|
|
354
|
-
if (!def) {
|
|
355
|
-
return null;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (def.targets && typeof def.targets === "object") {
|
|
359
|
-
const targetName = subArg || def.default || Object.keys(def.targets)[0];
|
|
360
|
-
const target = def.targets[targetName];
|
|
361
|
-
if (!target) {
|
|
362
|
-
const valid = Object.keys(def.targets).join(" | ");
|
|
363
|
-
throw new Error(t("VALID_TARGETS__COMMAND_VALID", { command: commandName, valid }));
|
|
364
|
-
}
|
|
365
|
-
return {
|
|
366
|
-
...target,
|
|
367
|
-
targetName,
|
|
368
|
-
description: def.description,
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
return def;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function listCommands(source) {
|
|
376
|
-
return Object.entries(source.commands || {})
|
|
377
|
-
.map(([name, def]) => ({
|
|
378
|
-
name,
|
|
379
|
-
aliases: commandAliases(def),
|
|
380
|
-
description: def.description || "",
|
|
381
|
-
hasTargets: Boolean(def.targets),
|
|
382
|
-
targets: def.targets ? Object.keys(def.targets) : [],
|
|
383
|
-
hasSequence: Boolean(def.sequence),
|
|
384
|
-
}))
|
|
385
|
-
// Sort A-Z theo tên lệnh — dễ dò trong menu tương tác (`pan` trống) và list help (`pan -h`),
|
|
386
|
-
// thay vì theo thứ tự khai báo trong .pan.yaml (khó đoán khi file có nhiều lệnh).
|
|
387
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Parse field `sequence` của 1 command → danh sách step chạy TUẦN TỰ theo thứ tự.
|
|
392
|
-
* 2 cách viết:
|
|
393
|
-
* sequence: build-fe, build-be # 1 dòng, phân tách bởi ,
|
|
394
|
-
* sequence: | # nhiều dòng (khuyến nghị khi cần set biến)
|
|
395
|
-
* build-fe
|
|
396
|
-
* MODE=prod build-be:dev
|
|
397
|
-
* build-extra? # dấu ? cuối = optional (lỗi vẫn chạy tiếp bước sau)
|
|
398
|
-
* Mỗi dòng: [KEY=VAL ...] tênLệnh[:target][?]
|
|
399
|
-
* • tênLệnh phải là 1 command KHÁC đã khai báo trong CÙNG file .pan.yaml này.
|
|
400
|
-
* • `:target` chọn target nếu lệnh đó có `targets:`.
|
|
401
|
-
* • `KEY=VAL` (đặt trước tên lệnh, cách nhau bởi khoảng trắng) → biến môi trường CHỈ áp dụng
|
|
402
|
-
* cho riêng bước đó (không ảnh hưởng các bước khác).
|
|
403
|
-
* • `?` cuối tên lệnh → bước lỗi (exit code != 0) KHÔNG dừng cả sequence, chỉ cảnh báo rồi
|
|
404
|
-
* chạy tiếp bước sau. Không có `?` → lỗi là DỪNG NGAY (không chạy bước còn lại).
|
|
405
|
-
*/
|
|
406
|
-
function parseSequenceSteps(raw) {
|
|
407
|
-
const text = String(raw || "");
|
|
408
|
-
const lines = text.includes("\n") ? text.split(/\r?\n/) : text.split(",");
|
|
409
|
-
const steps = [];
|
|
410
|
-
for (const rawLine of lines) {
|
|
411
|
-
const line = rawLine.trim();
|
|
412
|
-
if (!line || line.startsWith("#")) {
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
-
const tokens = line.split(/\s+/).filter(Boolean);
|
|
416
|
-
const env = {};
|
|
417
|
-
let ref = null;
|
|
418
|
-
for (const token of tokens) {
|
|
419
|
-
const match = ref === null && token.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
420
|
-
if (match) {
|
|
421
|
-
env[match[1]] = cleanValue(match[2]);
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
if (ref !== null) {
|
|
425
|
-
throw new Error(`Invalid sequence step: "${line}" (expected "[KEY=VAL ...] commandName[:target][?]")`);
|
|
426
|
-
}
|
|
427
|
-
ref = token;
|
|
428
|
-
}
|
|
429
|
-
if (!ref) {
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
let optional = false;
|
|
433
|
-
if (ref.endsWith("?")) {
|
|
434
|
-
optional = true;
|
|
435
|
-
ref = ref.slice(0, -1);
|
|
436
|
-
}
|
|
437
|
-
const [name, target = ""] = ref.split(":");
|
|
438
|
-
if (!name) {
|
|
439
|
-
throw new Error(`Invalid sequence step: "${line}" — missing command name.`);
|
|
440
|
-
}
|
|
441
|
-
steps.push({ raw: line, name, target, env, optional });
|
|
442
|
-
}
|
|
443
|
-
return steps;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function commandBaseDir(source) {
|
|
447
|
-
return source.scope === "workspace" ? source.workspaceRoot : source.root;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
module.exports = {
|
|
451
|
-
PLACEHOLDER_VALUES,
|
|
452
|
-
PAN_CONFIG_FILE,
|
|
453
|
-
LOCAL_MANIFEST_FILE,
|
|
454
|
-
cleanValue,
|
|
455
|
-
parseManifest,
|
|
456
|
-
parseManifestWithLocal,
|
|
457
|
-
loadManifest,
|
|
458
|
-
loadPanConfig,
|
|
459
|
-
manifestRepos,
|
|
460
|
-
validateRepo,
|
|
461
|
-
commandAliases,
|
|
462
|
-
resolveCommandName,
|
|
463
|
-
getCommandDefinition,
|
|
464
|
-
parseSequenceSteps,
|
|
465
|
-
listCommands,
|
|
466
|
-
commandBaseDir,
|
|
467
|
-
};
|
package/lib/menu-prompt.js
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
// Custom @inquirer prompt: danh sách chọn (↑↓/Enter, gõ chữ lọc) + PHÍM TẮT NHẤN-1-PHÍM chạy ngay
|
|
2
|
-
// (vd h/v/b/x). Dùng @inquirer/core (dynamic ESM từ CJS). Thiếu core → menuSelect throw 'no-core'
|
|
3
|
-
// để caller fallback sang @inquirer/prompts select.
|
|
4
|
-
let cachePrompt = null;
|
|
5
|
-
|
|
6
|
-
async function buildMenuPrompt() {
|
|
7
|
-
if (cachePrompt) return cachePrompt;
|
|
8
|
-
const core = await import("@inquirer/core").catch(() => null);
|
|
9
|
-
if (!core) return null;
|
|
10
|
-
const {
|
|
11
|
-
createPrompt, useState, useKeypress, usePrefix, usePagination, useMemo,
|
|
12
|
-
isEnterKey, isUpKey, isDownKey, Separator, makeTheme,
|
|
13
|
-
} = core;
|
|
14
|
-
|
|
15
|
-
cachePrompt = createPrompt((config, done) => {
|
|
16
|
-
const { message, choices, hotkeys = [], pageSize = 15, footer = "" } = config;
|
|
17
|
-
const theme = makeTheme(config.theme);
|
|
18
|
-
const [status, setStatus] = useState("idle");
|
|
19
|
-
const prefix = usePrefix({ status, theme });
|
|
20
|
-
const items = useMemo(() => choices, [choices]);
|
|
21
|
-
const firstSel = items.findIndex((it) => !Separator.isSeparator(it));
|
|
22
|
-
const [active, setActive] = useState(firstSel < 0 ? 0 : firstSel);
|
|
23
|
-
|
|
24
|
-
useKeypress((key, rl) => {
|
|
25
|
-
if (status !== "idle") return;
|
|
26
|
-
if (isEnterKey(key)) {
|
|
27
|
-
const it = items[active];
|
|
28
|
-
if (it && !Separator.isSeparator(it) && !it.disabled) {
|
|
29
|
-
setStatus("done");
|
|
30
|
-
done(it.value);
|
|
31
|
-
}
|
|
32
|
-
} else if (isUpKey(key) || isDownKey(key)) {
|
|
33
|
-
rl.clearLine(0);
|
|
34
|
-
const dir = isUpKey(key) ? -1 : 1;
|
|
35
|
-
let n = active;
|
|
36
|
-
do {
|
|
37
|
-
n = (n + dir + items.length) % items.length;
|
|
38
|
-
} while (Separator.isSeparator(items[n]) && n !== active);
|
|
39
|
-
setActive(n);
|
|
40
|
-
} else if (key.name && hotkeys.some((h) => h.key === key.name)) {
|
|
41
|
-
// PHÍM TẮT: nhấn 1 phím → chạy ngay.
|
|
42
|
-
setStatus("done");
|
|
43
|
-
done(hotkeys.find((h) => h.key === key.name).value);
|
|
44
|
-
} else {
|
|
45
|
-
// type-ahead: nhảy tới item tên bắt đầu bằng ký tự đang gõ.
|
|
46
|
-
const term = String(rl.line || "").toLowerCase();
|
|
47
|
-
if (term) {
|
|
48
|
-
const idx = items.findIndex(
|
|
49
|
-
(it) => !Separator.isSeparator(it) && String(it.name).toLowerCase().startsWith(term)
|
|
50
|
-
);
|
|
51
|
-
if (idx >= 0) setActive(idx);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const activeItem = items[active] || {};
|
|
57
|
-
if (status === "done") {
|
|
58
|
-
return `${prefix} ${message} ${activeItem.name || ""}`.trim();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const page = usePagination({
|
|
62
|
-
items,
|
|
63
|
-
active,
|
|
64
|
-
renderItem({ item, isActive }) {
|
|
65
|
-
if (Separator.isSeparator(item)) return ` ${item.separator || "──────────"}`;
|
|
66
|
-
const cursor = isActive ? "❯" : " ";
|
|
67
|
-
const line = ` ${cursor} ${item.name}`;
|
|
68
|
-
return isActive ? `\x1b[36m${line}\x1b[39m` : line;
|
|
69
|
-
},
|
|
70
|
-
pageSize,
|
|
71
|
-
loop: false,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const desc = activeItem.description ? `\x1b[2m ${activeItem.description}\x1b[22m` : "";
|
|
75
|
-
const hk = hotkeys.length
|
|
76
|
-
? " " + hotkeys.map((h) => `\x1b[1m${h.key}\x1b[22m ${h.label}`).join(" · ")
|
|
77
|
-
: "";
|
|
78
|
-
const help = footer || hk ? `\x1b[2m${footer}\x1b[22m${hk}` : "";
|
|
79
|
-
return [`${prefix} ${message}`, page, desc, help].filter(Boolean).join("\n");
|
|
80
|
-
});
|
|
81
|
-
return cachePrompt;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function menuSelect(config) {
|
|
85
|
-
const prompt = await buildMenuPrompt();
|
|
86
|
-
if (!prompt) throw new Error("no-core");
|
|
87
|
-
return prompt(config);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Multi-select với phím tắt (x thoát, b quay lại...) — trả:
|
|
91
|
-
// { action: 'submit', values: string[] } khi Enter
|
|
92
|
-
// { action: <hotkey.value> } khi bấm hotkey
|
|
93
|
-
let cachePromptCheckbox = null;
|
|
94
|
-
|
|
95
|
-
async function buildMenuCheckbox() {
|
|
96
|
-
if (cachePromptCheckbox) return cachePromptCheckbox;
|
|
97
|
-
const core = await import("@inquirer/core").catch(() => null);
|
|
98
|
-
if (!core) return null;
|
|
99
|
-
const {
|
|
100
|
-
createPrompt, useState, useKeypress, usePrefix, usePagination, useMemo,
|
|
101
|
-
isEnterKey, isUpKey, isDownKey, isSpaceKey, Separator, makeTheme,
|
|
102
|
-
} = core;
|
|
103
|
-
|
|
104
|
-
cachePromptCheckbox = createPrompt((config, done) => {
|
|
105
|
-
const { message, choices, hotkeys = [], pageSize = 15, footer = "" } = config;
|
|
106
|
-
const theme = makeTheme(config.theme);
|
|
107
|
-
const [status, setStatus] = useState("idle");
|
|
108
|
-
const prefix = usePrefix({ status, theme });
|
|
109
|
-
const items = useMemo(() => choices, [choices]);
|
|
110
|
-
const firstSel = items.findIndex((it) => !Separator.isSeparator(it));
|
|
111
|
-
const [active, setActive] = useState(firstSel < 0 ? 0 : firstSel);
|
|
112
|
-
const [checked, setChecked] = useState(new Set());
|
|
113
|
-
|
|
114
|
-
function toggle(i) {
|
|
115
|
-
const it = items[i];
|
|
116
|
-
if (!it || Separator.isSeparator(it) || it.disabled) return;
|
|
117
|
-
const next = new Set(checked);
|
|
118
|
-
if (next.has(i)) next.delete(i);
|
|
119
|
-
else next.add(i);
|
|
120
|
-
setChecked(next);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
useKeypress((key, rl) => {
|
|
124
|
-
if (status !== "idle") return;
|
|
125
|
-
if (isEnterKey(key)) {
|
|
126
|
-
setStatus("done");
|
|
127
|
-
const values = [];
|
|
128
|
-
items.forEach((it, i) => {
|
|
129
|
-
if (!Separator.isSeparator(it) && !it.disabled && checked.has(i)) values.push(it.value);
|
|
130
|
-
});
|
|
131
|
-
done({ action: "submit", values });
|
|
132
|
-
} else if (isSpaceKey && isSpaceKey(key)) {
|
|
133
|
-
rl.clearLine(0);
|
|
134
|
-
toggle(active);
|
|
135
|
-
} else if (key.name === "space") {
|
|
136
|
-
rl.clearLine(0);
|
|
137
|
-
toggle(active);
|
|
138
|
-
} else if (isUpKey(key) || isDownKey(key)) {
|
|
139
|
-
rl.clearLine(0);
|
|
140
|
-
const dir = isUpKey(key) ? -1 : 1;
|
|
141
|
-
let n = active;
|
|
142
|
-
do {
|
|
143
|
-
n = (n + dir + items.length) % items.length;
|
|
144
|
-
} while (Separator.isSeparator(items[n]) && n !== active);
|
|
145
|
-
setActive(n);
|
|
146
|
-
} else if (key.name === "a") {
|
|
147
|
-
// toggle-all: nếu chưa đủ → chọn hết; đã đủ → bỏ hết
|
|
148
|
-
const selectable = items.map((it, i) => ({ it, i })).filter(({ it }) => !Separator.isSeparator(it) && !it.disabled);
|
|
149
|
-
const allChecked = selectable.every(({ i }) => checked.has(i));
|
|
150
|
-
const next = new Set();
|
|
151
|
-
if (!allChecked) selectable.forEach(({ i }) => next.add(i));
|
|
152
|
-
setChecked(next);
|
|
153
|
-
} else if (key.name === "i") {
|
|
154
|
-
// invert
|
|
155
|
-
const next = new Set();
|
|
156
|
-
items.forEach((it, i) => {
|
|
157
|
-
if (!Separator.isSeparator(it) && !it.disabled && !checked.has(i)) next.add(i);
|
|
158
|
-
});
|
|
159
|
-
setChecked(next);
|
|
160
|
-
} else if (key.name && hotkeys.some((h) => h.key === key.name)) {
|
|
161
|
-
setStatus("done");
|
|
162
|
-
done({ action: hotkeys.find((h) => h.key === key.name).value });
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
if (status === "done") {
|
|
167
|
-
return `${prefix} ${message}`.trim();
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const page = usePagination({
|
|
171
|
-
items,
|
|
172
|
-
active,
|
|
173
|
-
renderItem({ item, index, isActive }) {
|
|
174
|
-
if (Separator.isSeparator(item)) return ` ${item.separator || "──────────"}`;
|
|
175
|
-
const box = checked.has(index) ? "◉" : "◯";
|
|
176
|
-
const cursor = isActive ? "❯" : " ";
|
|
177
|
-
const line = ` ${cursor} ${box} ${item.name}`;
|
|
178
|
-
return isActive ? `\x1b[36m${line}\x1b[39m` : line;
|
|
179
|
-
},
|
|
180
|
-
pageSize,
|
|
181
|
-
loop: false,
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
const baseHelp = "space chọn · a chọn tất cả · i đảo";
|
|
185
|
-
const hk = hotkeys.length
|
|
186
|
-
? " " + hotkeys.map((h) => `\x1b[1m${h.key}\x1b[22m ${h.label}`).join(" · ")
|
|
187
|
-
: "";
|
|
188
|
-
const enterTip = ` \x1b[1mEnter\x1b[22m \x1b[2mxác nhận\x1b[22m`;
|
|
189
|
-
const help = `\x1b[2m${footer || baseHelp}\x1b[22m${hk}${enterTip}`;
|
|
190
|
-
return [`${prefix} ${message}`, page, help].filter(Boolean).join("\n");
|
|
191
|
-
});
|
|
192
|
-
return cachePromptCheckbox;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async function menuCheckbox(config) {
|
|
196
|
-
const prompt = await buildMenuCheckbox();
|
|
197
|
-
if (!prompt) throw new Error("no-core");
|
|
198
|
-
return prompt(config);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
module.exports = { menuSelect, buildMenuPrompt, menuCheckbox, buildMenuCheckbox };
|