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.
Files changed (59) hide show
  1. package/README.md +37 -731
  2. package/bin/pan.js +4955 -269
  3. package/package.json +17 -5
  4. package/lib/ai-skills.js +0 -829
  5. package/lib/create.js +0 -105
  6. package/lib/discover.js +0 -294
  7. package/lib/git/git-checkout.js +0 -395
  8. package/lib/git/git-clone.js +0 -206
  9. package/lib/git/git-fetch-all.js +0 -129
  10. package/lib/git/git-grv.js +0 -125
  11. package/lib/git/git-gst.js +0 -165
  12. package/lib/git/git-mr.js +0 -546
  13. package/lib/git/git-pull.js +0 -137
  14. package/lib/git/git-push.js +0 -185
  15. package/lib/git/git-sync-main.js +0 -334
  16. package/lib/git/helpers/bulk-report.js +0 -202
  17. package/lib/git/helpers/cli.js +0 -188
  18. package/lib/git/helpers/colors.js +0 -19
  19. package/lib/git/helpers/git.js +0 -136
  20. package/lib/git/helpers/manifest.js +0 -223
  21. package/lib/git/helpers/repo-select.js +0 -416
  22. package/lib/git/helpers/repo-summary.js +0 -161
  23. package/lib/i18n.js +0 -61
  24. package/lib/inquirer-theme.js +0 -20
  25. package/lib/language.js +0 -121
  26. package/lib/languages.js +0 -14
  27. package/lib/manifest.js +0 -467
  28. package/lib/menu-prompt.js +0 -201
  29. package/lib/menu.js +0 -134
  30. package/lib/messages.js +0 -647
  31. package/lib/platform.js +0 -140
  32. package/lib/run-command.js +0 -334
  33. package/lib/select.js +0 -0
  34. package/lib/serve.js +0 -75
  35. package/lib/skill-registry-client.js +0 -173
  36. package/lib/stats/aggregate.js +0 -51
  37. package/lib/stats/identity.js +0 -20
  38. package/lib/stats/index.js +0 -189
  39. package/lib/stats/parse.js +0 -46
  40. package/lib/stats/range.js +0 -38
  41. package/lib/stats/report.js +0 -264
  42. package/lib/stats/review.js +0 -157
  43. package/lib/stats/scan.js +0 -92
  44. package/lib/stats/serve.js +0 -25
  45. package/lib/ui/api.js +0 -202
  46. package/lib/ui/clone.js +0 -115
  47. package/lib/ui/config.js +0 -182
  48. package/lib/ui/diff.js +0 -128
  49. package/lib/ui/export-gate.js +0 -79
  50. package/lib/ui/gen-react-mobile-code.js +0 -6
  51. package/lib/ui/gen-react-web-code.js +0 -75
  52. package/lib/ui/index.js +0 -89
  53. package/lib/ui/init.js +0 -252
  54. package/lib/ui/login.js +0 -128
  55. package/lib/ui/logout.js +0 -22
  56. package/lib/ui/palettes.js +0 -103
  57. package/lib/ui/published-html.js +0 -24
  58. package/lib/ui/sync.js +0 -137
  59. package/lib/ui/workspace.js +0 -213
package/lib/ai-skills.js DELETED
@@ -1,829 +0,0 @@
1
- // pan ai — enable/disable agent skills at workspace root by renaming
2
- // SKILL.md / AGENTS.md ↔ *.disabled across .claude / .cursor / .codex.
3
- const fs = require("fs");
4
- const path = require("path");
5
- const { execFileSync } = require("child_process");
6
- const { findNearestWorkspace } = require("./discover");
7
- const { t } = require("./i18n");
8
- const { helpTheme } = require("./inquirer-theme");
9
- const { menuSelect, menuCheckbox } = require("./menu-prompt");
10
- const {
11
- getRegistryBaseUrl,
12
- fetchRegistry,
13
- loadLock,
14
- saveLock,
15
- diffRegistry,
16
- downloadAndExtract,
17
- } = require("./skill-registry-client");
18
-
19
- const AGENT_KEYS = ["claude", "cursor", "codex"];
20
- const AGENT_DIRS = {
21
- claude: ".claude",
22
- cursor: ".cursor",
23
- codex: ".codex",
24
- };
25
- const DOC_BASES = ["SKILL.md", "AGENTS.md"];
26
-
27
- // Trả các key agent (claude/cursor/codex) mà workspace ĐÃ có thư mục .claude/.cursor/.codex
28
- // (không tạo mới, chỉ detect sẵn có) — dùng cho luồng Install để tự chọn agent đích.
29
- function detectAgentDirs(workspaceRoot) {
30
- return AGENT_KEYS.filter((key) => {
31
- const dir = path.join(workspaceRoot, AGENT_DIRS[key]);
32
- return fs.existsSync(dir) && fs.statSync(dir).isDirectory();
33
- });
34
- }
35
-
36
- const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
37
-
38
- // Chạy `task()` (Promise) trong lúc hiện spinner text xoay tại chỗ (ghi đè bằng \r).
39
- async function withSpinner(message, task) {
40
- if (!process.stdout.isTTY) {
41
- return task();
42
- }
43
- let i = 0;
44
- const timer = setInterval(() => {
45
- process.stdout.write(`\r${SPINNER_FRAMES[i % SPINNER_FRAMES.length]} ${message}`);
46
- i += 1;
47
- }, 80);
48
- try {
49
- return await task();
50
- } finally {
51
- clearInterval(timer);
52
- process.stdout.write(`\r${" ".repeat(message.length + 4)}\r`);
53
- }
54
- }
55
-
56
- function truncateDescription(desc, max = 80) {
57
- const s = String(desc || "");
58
- return s.length > max ? `${s.slice(0, max - 3)}...` : s;
59
- }
60
-
61
- function itemLabel(item, versionsSuffix) {
62
- const prefix = item.type === "agent" ? "[Agent] " : "";
63
- return `${prefix}${item.name}${versionsSuffix}`;
64
- }
65
-
66
- let inquirerModulePromise = null;
67
- async function loadInquirer() {
68
- if (inquirerModulePromise === null) {
69
- inquirerModulePromise = import("@inquirer/prompts").catch(() => null);
70
- }
71
- return inquirerModulePromise;
72
- }
73
-
74
- function isCancelError(err) {
75
- if (!err) return false;
76
- const name = String(err.name || "");
77
- return (
78
- name === "ExitPromptError" ||
79
- name === "CancelPromptError" ||
80
- /User force closed/i.test(String(err.message || ""))
81
- );
82
- }
83
-
84
- function printAiHelp() {
85
- console.log(`usage: pan ai [options] (alias: pan skills)
86
-
87
- Enable/disable PAN Stack agent skills at the workspace root
88
- (.claude/skills, .cursor/skills, .codex/skills).
89
-
90
- Disable renames SKILL.md → SKILL.md.disabled (and AGENTS.md likewise)
91
- on every agent folder that has that skill. Enable reverses the rename.
92
-
93
- options:
94
- --list List skills and status (no changes)
95
- --disable <name...> Disable named skill(s)
96
- --enable <name...> Enable named skill(s)
97
- --dry-run Print renames only
98
- -h, --help Show this help
99
-
100
- interactive (TTY, no --list/--disable/--enable):
101
- 1) Choose Disable | Enable | List | Install | Update (press 'x' to exit)
102
- 2) Multi-select skills (space chọn · a chọn tất cả · i đảo)
103
- 'b' quay lại menu · 'x' thoát · Enter áp dụng
104
-
105
- Install/Update fetch from the registry server (PAN_SKILL_REGISTRY_URL, default
106
- http://localhost:4600) and track installed versions in .pan/skills-lock.json.
107
- `);
108
- }
109
-
110
- function parseAiArgs(argv) {
111
- const result = {
112
- help: false,
113
- list: false,
114
- dryRun: false,
115
- disable: [],
116
- enable: [],
117
- };
118
- let mode = null;
119
- for (let i = 0; i < argv.length; i += 1) {
120
- const a = argv[i];
121
- if (a === "-h" || a === "--help") {
122
- result.help = true;
123
- } else if (a === "--list") {
124
- result.list = true;
125
- } else if (a === "--dry-run") {
126
- result.dryRun = true;
127
- } else if (a === "--disable") {
128
- mode = "disable";
129
- } else if (a === "--enable") {
130
- mode = "enable";
131
- } else if (a.startsWith("-")) {
132
- throw new Error(t("AI_UNKNOWN_FLAG__FLAG", { flag: a }));
133
- } else if (mode === "disable") {
134
- result.disable.push(a);
135
- } else if (mode === "enable") {
136
- result.enable.push(a);
137
- } else {
138
- throw new Error(t("AI_UNEXPECTED_ARG__ARG", { arg: a }));
139
- }
140
- }
141
- return result;
142
- }
143
-
144
- function listSkillDirs(skillsRoot) {
145
- if (!fs.existsSync(skillsRoot) || !fs.statSync(skillsRoot).isDirectory()) {
146
- return [];
147
- }
148
- return fs
149
- .readdirSync(skillsRoot, { withFileTypes: true })
150
- .filter((d) => d.isDirectory())
151
- .map((d) => d.name)
152
- .filter((name) => !name.startsWith("."));
153
- }
154
-
155
- function collectFilesForDir(skillDir) {
156
- const files = [];
157
- for (const base of DOC_BASES) {
158
- const activePath = path.join(skillDir, base);
159
- const disabledPath = path.join(skillDir, `${base}.disabled`);
160
- const hasActive = fs.existsSync(activePath);
161
- const hasDisabled = fs.existsSync(disabledPath);
162
- if (!hasActive && !hasDisabled) continue;
163
- files.push({
164
- base,
165
- activePath: hasActive ? activePath : null,
166
- disabledPath: hasDisabled ? disabledPath : null,
167
- });
168
- }
169
- return files;
170
- }
171
-
172
- function computeStatus(files) {
173
- let activeCount = 0;
174
- let disabledCount = 0;
175
- for (const f of files) {
176
- if (f.activePath) activeCount += 1;
177
- if (f.disabledPath) disabledCount += 1;
178
- }
179
- if (activeCount > 0 && disabledCount > 0) return "mixed";
180
- if (disabledCount > 0 && activeCount === 0) return "disabled";
181
- return "active";
182
- }
183
-
184
- function agentHint(skill) {
185
- const parts = [];
186
- for (const key of AGENT_KEYS) {
187
- const entry = skill.agents[key];
188
- if (!entry) continue;
189
- const st = computeStatus(entry.files);
190
- const short = st === "active" ? "on" : st === "disabled" ? "off" : "mix";
191
- parts.push(`${key}:${short}`);
192
- }
193
- return parts.join(" ");
194
- }
195
-
196
- /**
197
- * @returns {Array<{
198
- * name: string,
199
- * status: 'active'|'disabled'|'mixed',
200
- * agents: Record<string, { dir: string, files: Array }>,
201
- * files: Array<{ agent: string, base: string, activePath: string|null, disabledPath: string|null }>
202
- * }>}
203
- */
204
- function scanSkills(workspaceRoot) {
205
- const byName = new Map();
206
-
207
- for (const key of AGENT_KEYS) {
208
- const skillsRoot = path.join(workspaceRoot, AGENT_DIRS[key], "skills");
209
- for (const name of listSkillDirs(skillsRoot)) {
210
- const skillDir = path.join(skillsRoot, name);
211
- const files = collectFilesForDir(skillDir);
212
- if (files.length === 0) continue;
213
-
214
- if (!byName.has(name)) {
215
- byName.set(name, {
216
- name,
217
- agents: {},
218
- files: [],
219
- });
220
- }
221
- const skill = byName.get(name);
222
- skill.agents[key] = { dir: skillDir, files };
223
- for (const f of files) {
224
- skill.files.push({
225
- agent: key,
226
- base: f.base,
227
- activePath: f.activePath,
228
- disabledPath: f.disabledPath,
229
- });
230
- }
231
- }
232
- }
233
-
234
- const skills = [...byName.values()].map((skill) => {
235
- skill.status = computeStatus(skill.files);
236
- return skill;
237
- });
238
- skills.sort((a, b) => a.name.localeCompare(b.name));
239
- return skills;
240
- }
241
-
242
- function printSkillList(skills) {
243
- if (skills.length === 0) {
244
- console.log(t("AI_NO_SKILLS"));
245
- return;
246
- }
247
- const nameWidth = Math.max(8, ...skills.map((s) => s.name.length));
248
- const statusWidth = 8;
249
- console.log(
250
- `${"NAME".padEnd(nameWidth)} ${"STATUS".padEnd(statusWidth)} AGENTS`
251
- );
252
- for (const s of skills) {
253
- const hint = agentHint(s);
254
- console.log(
255
- `${s.name.padEnd(nameWidth)} ${s.status.padEnd(statusWidth)} ${hint}`
256
- );
257
- }
258
- console.log("");
259
- console.log(t("AI_LIST_FOOTER__COUNT", { count: String(skills.length) }));
260
- }
261
-
262
- /**
263
- * @returns {{ name: string, code: 'ok'|'noop'|'skip'|'error', lines: string[] }}
264
- */
265
- function applySkillAction(skill, action, { dryRun = false } = {}) {
266
- const lines = [];
267
- let renamed = 0;
268
- let skipped = 0;
269
-
270
- for (const f of skill.files) {
271
- if (action === "disable") {
272
- if (!f.activePath) {
273
- continue;
274
- }
275
- const dest = `${f.activePath}.disabled`;
276
- if (fs.existsSync(dest)) {
277
- skipped += 1;
278
- lines.push(`! ${skill.name} [${f.agent}] ${f.base}: already has .disabled`);
279
- continue;
280
- }
281
- if (dryRun) {
282
- lines.push(`would rename ${f.activePath} → ${dest}`);
283
- renamed += 1;
284
- } else {
285
- try {
286
- fs.renameSync(f.activePath, dest);
287
- lines.push(
288
- `✓ ${path.relative(process.cwd(), f.activePath)} → ${path.basename(dest)}`
289
- );
290
- renamed += 1;
291
- } catch (err) {
292
- lines.push(`✗ ${skill.name} [${f.agent}] ${f.base}: ${err.message}`);
293
- return { name: skill.name, code: "error", lines };
294
- }
295
- }
296
- } else if (action === "enable") {
297
- if (!f.disabledPath) {
298
- continue;
299
- }
300
- const dest = f.disabledPath.replace(/\.disabled$/, "");
301
- if (fs.existsSync(dest)) {
302
- skipped += 1;
303
- lines.push(`! ${skill.name} [${f.agent}] ${f.base}: active file exists`);
304
- continue;
305
- }
306
- if (dryRun) {
307
- lines.push(`would rename ${f.disabledPath} → ${dest}`);
308
- renamed += 1;
309
- } else {
310
- try {
311
- fs.renameSync(f.disabledPath, dest);
312
- lines.push(
313
- `✓ ${path.relative(process.cwd(), f.disabledPath)} → ${path.basename(dest)}`
314
- );
315
- renamed += 1;
316
- } catch (err) {
317
- lines.push(`✗ ${skill.name} [${f.agent}] ${f.base}: ${err.message}`);
318
- return { name: skill.name, code: "error", lines };
319
- }
320
- }
321
- }
322
- }
323
-
324
- let code = "ok";
325
- if (renamed === 0 && skipped === 0) code = "noop";
326
- else if (renamed === 0 && skipped > 0) code = "skip";
327
- return { name: skill.name, code, lines };
328
- }
329
-
330
- function skillsForAction(skills, action) {
331
- if (action === "disable") {
332
- return skills.filter((s) => s.status === "active" || s.status === "mixed");
333
- }
334
- return skills.filter((s) => s.status === "disabled" || s.status === "mixed");
335
- }
336
-
337
- function findSkillsByNames(skills, names) {
338
- const map = new Map(skills.map((s) => [s.name, s]));
339
- const found = [];
340
- const missing = [];
341
- for (const raw of names) {
342
- const name = String(raw || "").trim();
343
- if (!name) continue;
344
- if (!map.has(name)) {
345
- missing.push(name);
346
- continue;
347
- }
348
- found.push(map.get(name));
349
- }
350
- return { found, missing };
351
- }
352
-
353
- function printApplyResults(results) {
354
- for (const r of results) {
355
- if (r.lines.length === 0) {
356
- const icon = r.code === "noop" ? "=" : r.code === "skip" ? "!" : "✓";
357
- console.log(`${icon} ${r.name}`);
358
- continue;
359
- }
360
- for (const line of r.lines) {
361
- console.log(line);
362
- }
363
- }
364
- }
365
-
366
- function countSkillStatuses(skills) {
367
- let active = 0;
368
- let disabled = 0;
369
- let mixed = 0;
370
- for (const s of skills) {
371
- if (s.status === "active") active += 1;
372
- else if (s.status === "disabled") disabled += 1;
373
- else if (s.status === "mixed") mixed += 1;
374
- }
375
- return { active, disabled, mixed, total: skills.length };
376
- }
377
-
378
- async function promptAction(skills) {
379
- const { active, disabled, mixed, total } = countSkillStatuses(skills);
380
- const disableCount = active + mixed;
381
- const enableCount = disabled + mixed;
382
- const message = t("AI_SELECT_ACTION__ACTIVE_DISABLED_TOTAL", {
383
- active: String(active),
384
- disabled: String(disabled),
385
- total: String(total),
386
- });
387
- // Không thêm item "Thoát" trong danh sách — đã có phím tắt `x`.
388
- const choices = [
389
- {
390
- name: t("AI_CHOICE_DISABLE__COUNT", { count: String(disableCount) }),
391
- value: "disable",
392
- short: "disable",
393
- disabled: disableCount === 0 ? t("AI_NONE") : false,
394
- },
395
- {
396
- name: t("AI_CHOICE_ENABLE__COUNT", { count: String(enableCount) }),
397
- value: "enable",
398
- short: "enable",
399
- disabled: enableCount === 0 ? t("AI_NONE") : false,
400
- },
401
- {
402
- name: t("AI_CHOICE_LIST__COUNT", { count: String(total) }),
403
- value: "list",
404
- short: "list",
405
- },
406
- {
407
- name: t("AI_CHOICE_INSTALL"),
408
- value: "install",
409
- short: "install",
410
- },
411
- {
412
- name: t("AI_CHOICE_UPDATE"),
413
- value: "update",
414
- short: "update",
415
- },
416
- ];
417
- const hotkeys = [{ key: "x", label: t("EXIT_LABEL"), value: "exit" }];
418
-
419
- try {
420
- // Ưu tiên menu-prompt (phím x thoát ngay); thiếu @inquirer/core → select + item hotkey fallback.
421
- try {
422
- return await menuSelect({ message, choices, hotkeys, pageSize: 10 });
423
- } catch (err) {
424
- if (String(err && err.message) !== "no-core") throw err;
425
- const inq = await loadInquirer();
426
- if (!inq || typeof inq.select !== "function") {
427
- throw new Error(t("AI_NEED_TTY_OR_FLAGS"));
428
- }
429
- // Fallback không có hotkey — vẫn cần một cách thoát → thêm item ẩn ở cuối.
430
- return await inq.select({
431
- message,
432
- choices: [
433
- ...choices,
434
- new inq.Separator(),
435
- { name: `x ${t("EXIT_LABEL")}`, value: "exit", short: "x" },
436
- ],
437
- pageSize: 10,
438
- loop: false,
439
- theme: helpTheme(false),
440
- });
441
- }
442
- } catch (err) {
443
- if (isCancelError(err)) {
444
- throw new Error(t("AI_CANCELLED"));
445
- }
446
- throw err;
447
- }
448
- }
449
-
450
- /**
451
- * @returns {{ action: 'submit'|'exit'|'back', values: string[] }}
452
- */
453
- async function promptSkillNames(candidates, action) {
454
- if (candidates.length === 0) {
455
- console.log(t("AI_EMPTY_FOR_ACTION__ACTION", { action }));
456
- return { action: "submit", values: [] };
457
- }
458
- const choices = candidates.map((s) => ({
459
- name:
460
- s.status === "mixed"
461
- ? `${s.name} (mixed · ${agentHint(s)})`
462
- : `${s.name} (${s.status})`,
463
- value: s.name,
464
- short: s.name,
465
- }));
466
- const hotkeys = [
467
- { key: "x", label: t("EXIT_LABEL"), value: "exit" },
468
- { key: "b", label: t("BACK_LABEL"), value: "back" },
469
- ];
470
-
471
- try {
472
- // Ưu tiên menu-prompt: hỗ trợ hotkey x/b để thoát / quay lại.
473
- try {
474
- const res = await menuCheckbox({
475
- message: t("AI_SELECT_SKILLS"),
476
- choices,
477
- hotkeys,
478
- pageSize: Math.min(Math.max(candidates.length, 8), 20),
479
- });
480
- // res = { action, values? }
481
- if (res.action === "submit") return { action: "submit", values: res.values || [] };
482
- return { action: res.action, values: [] };
483
- } catch (err) {
484
- if (String(err && err.message) !== "no-core") throw err;
485
- const inq = await loadInquirer();
486
- if (!inq || typeof inq.checkbox !== "function") {
487
- throw new Error(t("AI_NEED_TTY_OR_FLAGS"));
488
- }
489
- // Fallback không có hotkey → chỉ Enter/Space; Ctrl+C thoát toàn cục.
490
- const values = await inq.checkbox({
491
- message: t("AI_SELECT_SKILLS"),
492
- choices,
493
- pageSize: Math.min(Math.max(candidates.length, 8), 20),
494
- theme: helpTheme(true),
495
- });
496
- return { action: "submit", values };
497
- }
498
- } catch (err) {
499
- if (isCancelError(err)) {
500
- throw new Error(t("AI_CANCELLED"));
501
- }
502
- throw err;
503
- }
504
- }
505
-
506
- // Menu chọn agent đích (claude/cursor/codex) khi workspace chưa có sẵn thư mục nào.
507
- async function promptAgentDirs() {
508
- const choices = AGENT_KEYS.map((key) => ({
509
- name: AGENT_DIRS[key],
510
- value: key,
511
- short: key,
512
- }));
513
- const hotkeys = [
514
- { key: "x", label: t("EXIT_LABEL"), value: "__exit__" },
515
- { key: "b", label: t("BACK_LABEL"), value: "__back__" },
516
- ];
517
- const res = await menuCheckbox({
518
- message: t("AI_SELECT_AGENT_DIRS"),
519
- choices,
520
- hotkeys,
521
- pageSize: 10,
522
- });
523
- return res; // { action: 'submit', values } | { action: '__exit__' } | { action: '__back__' }
524
- }
525
-
526
- async function runUpdateFlow(workspace) {
527
- const baseUrl = getRegistryBaseUrl();
528
- const { items, lock, diff } = await withSpinner(t("AI_CHECKING_UPDATES"), async () => {
529
- const items = await fetchRegistry(baseUrl);
530
- const lock = loadLock(workspace.root);
531
- const diff = diffRegistry(items, lock);
532
- return { items, lock, diff };
533
- });
534
-
535
- if (diff.updatable.length === 0) {
536
- console.log(t("AI_NO_UPDATES"));
537
- return;
538
- }
539
-
540
- const choices = [
541
- { name: t("AI_UPDATE_ALL__COUNT", { count: String(diff.updatable.length) }), value: "__all__" },
542
- ...diff.updatable.map((item) => ({
543
- name: itemLabel(item, ` (${item.oldVersion} → ${item.version})`),
544
- value: item.name,
545
- short: item.name,
546
- })),
547
- ];
548
- const hotkeys = [
549
- { key: "x", label: t("EXIT_LABEL"), value: "exit" },
550
- { key: "b", label: t("BACK_LABEL"), value: "back" },
551
- ];
552
-
553
- const res = await menuCheckbox({ message: t("AI_UPDATE_SELECT"), choices, hotkeys, pageSize: 15 });
554
- if (res.action === "exit") {
555
- console.log(t("EXITED"));
556
- process.exit(0);
557
- }
558
- if (res.action === "back") return;
559
- const values = res.values || [];
560
- if (values.length === 0) {
561
- console.log(t("AI_EMPTY_SELECTION"));
562
- return;
563
- }
564
-
565
- const toUpdate = values.includes("__all__")
566
- ? diff.updatable
567
- : diff.updatable.filter((item) => values.includes(item.name));
568
-
569
- let count = 0;
570
- for (const item of toUpdate) {
571
- const entry = lock.installed[item.name] || {};
572
- const agents = entry.agents && entry.agents.length ? entry.agents : detectAgentDirs(workspace.root);
573
- process.stdout.write(` ↻ ${item.name} (${entry.version || "?"} → ${item.version}) ... `);
574
- try {
575
- for (const agentKey of agents) {
576
- const destDir = path.join(workspace.root, AGENT_DIRS[agentKey], "skills");
577
- await downloadAndExtract(baseUrl, item, destDir);
578
- }
579
- lock.installed[item.name] = { version: item.version, type: item.type, agents };
580
- saveLock(workspace.root, lock);
581
- console.log("ok");
582
- count += 1;
583
- } catch (err) {
584
- console.log(`✗ ${err.message}`);
585
- }
586
- }
587
- console.log(t("AI_UPDATE_DONE__COUNT", { count: String(count) }));
588
- }
589
-
590
- async function runInstallFlow(workspace) {
591
- const baseUrl = getRegistryBaseUrl();
592
- const { items, lock, diff } = await withSpinner(t("AI_CHECKING_UPDATES"), async () => {
593
- const items = await fetchRegistry(baseUrl);
594
- const lock = loadLock(workspace.root);
595
- const diff = diffRegistry(items, lock);
596
- return { items, lock, diff };
597
- });
598
-
599
- if (diff.notInstalled.length === 0 && diff.updatable.length === 0) {
600
- console.log(t("AI_NOTHING_TO_INSTALL"));
601
- return;
602
- }
603
-
604
- const choices = [];
605
- if (diff.updatable.length > 0) {
606
- choices.push({
607
- name: t("AI_UPDATE_AVAILABLE__COUNT", { count: String(diff.updatable.length) }),
608
- value: "__update__",
609
- });
610
- }
611
- for (const item of diff.notInstalled) {
612
- choices.push({
613
- name: itemLabel(item, ` (v${item.version})`),
614
- value: item.name,
615
- short: item.name,
616
- description: truncateDescription(item.description),
617
- });
618
- }
619
- const hotkeys = [
620
- { key: "x", label: t("EXIT_LABEL"), value: "exit" },
621
- { key: "b", label: t("BACK_LABEL"), value: "back" },
622
- ];
623
-
624
- const res = await menuCheckbox({ message: t("AI_INSTALL_SELECT"), choices, hotkeys, pageSize: 15 });
625
- if (res.action === "exit") {
626
- console.log(t("EXITED"));
627
- process.exit(0);
628
- }
629
- if (res.action === "back") return;
630
-
631
- const values = res.values || [];
632
- const wantsUpdate = values.includes("__update__");
633
- const toInstallNames = values.filter((v) => v !== "__update__");
634
- const toInstall = diff.notInstalled.filter((item) => toInstallNames.includes(item.name));
635
-
636
- if (toInstall.length === 0 && !wantsUpdate) {
637
- console.log(t("AI_EMPTY_SELECTION"));
638
- return;
639
- }
640
-
641
- if (toInstall.length > 0) {
642
- let agentDirs = detectAgentDirs(workspace.root);
643
- if (agentDirs.length > 0) {
644
- console.log(t("AI_INSTALL_USING_AGENTS__AGENTS", {
645
- agents: agentDirs.map((k) => AGENT_DIRS[k]).join(", "),
646
- }));
647
- } else {
648
- const agentRes = await promptAgentDirs();
649
- if (agentRes.action === "__exit__") {
650
- console.log(t("EXITED"));
651
- process.exit(0);
652
- }
653
- if (agentRes.action === "__back__" || !agentRes.values || agentRes.values.length === 0) {
654
- console.log(t("AI_EMPTY_SELECTION"));
655
- agentDirs = [];
656
- } else {
657
- agentDirs = agentRes.values;
658
- }
659
- }
660
-
661
- if (agentDirs.length > 0) {
662
- let count = 0;
663
- for (const item of toInstall) {
664
- process.stdout.write(` + ${item.name} (v${item.version}) ... `);
665
- try {
666
- for (const agentKey of agentDirs) {
667
- const destDir = path.join(workspace.root, AGENT_DIRS[agentKey], "skills");
668
- await downloadAndExtract(baseUrl, item, destDir);
669
- }
670
- lock.installed[item.name] = { version: item.version, type: item.type, agents: agentDirs };
671
- saveLock(workspace.root, lock);
672
- console.log("ok");
673
- count += 1;
674
- } catch (err) {
675
- console.log(`✗ ${err.message}`);
676
- }
677
- }
678
- console.log(t("AI_INSTALL_DONE__COUNT", { count: String(count) }));
679
- }
680
- }
681
-
682
- if (wantsUpdate) {
683
- await runUpdateFlow(workspace);
684
- }
685
- }
686
-
687
- // Không có .project.yaml: hỏi tạo dự án mới ngay tại cwd ("Không" mặc định đứng trước "Có").
688
- // @returns workspace mới (đã tạo .project.yaml) hoặc null nếu user chọn "no" / không TTY.
689
- async function promptCreateWorkspaceHere() {
690
- if (!process.stdin.isTTY) return null;
691
- const choices = [
692
- { name: t("NO_LABEL"), value: "no" },
693
- { name: t("YES_LABEL"), value: "yes" },
694
- ];
695
- let answer;
696
- try {
697
- answer = await menuSelect({ message: t("AI_NO_PROJECT_ASK_CREATE"), choices, pageSize: 5 });
698
- } catch (err) {
699
- if (String(err && err.message) !== "no-core") throw err;
700
- const inq = await loadInquirer();
701
- if (!inq || typeof inq.select !== "function") return null;
702
- answer = await inq.select({ message: t("AI_NO_PROJECT_ASK_CREATE"), choices, theme: helpTheme(false) });
703
- }
704
- if (answer !== "yes") return null;
705
-
706
- function gitConfigValue(key) {
707
- try {
708
- const v = execFileSync("git", ["config", key], { encoding: "utf8" }).trim();
709
- return v || "YourName";
710
- } catch {
711
- return "YourName";
712
- }
713
- }
714
- const name = gitConfigValue("user.name");
715
- const email = gitConfigValue("user.email");
716
- const managedBy = `${name} - ${email}`;
717
- const projectName = path.basename(process.cwd());
718
- const content = `project: ${projectName}\nlanguage: vi\nmanaged_by: ${managedBy}\n`;
719
- fs.writeFileSync(path.join(process.cwd(), ".project.yaml"), content, "utf8");
720
- console.log(t("AI_PROJECT_CREATED__PROJECT_MANAGEDBY", { project: projectName, managedBy }));
721
- return findNearestWorkspace();
722
- }
723
-
724
- async function runAiSkills(argv) {
725
- const args = parseAiArgs(argv);
726
- if (args.help) {
727
- printAiHelp();
728
- return;
729
- }
730
-
731
- if (args.disable.length && args.enable.length) {
732
- throw new Error(t("AI_DISABLE_ENABLE_MUTUAL"));
733
- }
734
-
735
- let workspace = findNearestWorkspace();
736
- if (!workspace) {
737
- workspace = await promptCreateWorkspaceHere();
738
- if (!workspace) {
739
- throw new Error(t("AI_NO_WORKSPACE"));
740
- }
741
- }
742
-
743
- let skills = scanSkills(workspace.root);
744
-
745
- const runNamed = (action, names) => {
746
- const { found, missing } = findSkillsByNames(skills, names);
747
- if (missing.length) {
748
- throw new Error(
749
- t("AI_UNKNOWN_SKILL__NAME", { name: missing.join(", ") })
750
- );
751
- }
752
- const results = found.map((s) =>
753
- applySkillAction(s, action, { dryRun: args.dryRun })
754
- );
755
- printApplyResults(results);
756
- };
757
-
758
- if (args.list && !args.disable.length && !args.enable.length) {
759
- printSkillList(skills);
760
- return;
761
- }
762
-
763
- if (args.disable.length) {
764
- runNamed("disable", args.disable);
765
- return;
766
- }
767
- if (args.enable.length) {
768
- runNamed("enable", args.enable);
769
- return;
770
- }
771
-
772
- if (!process.stdin.isTTY) {
773
- printAiHelp();
774
- throw new Error(t("AI_NEED_TTY_OR_FLAGS"));
775
- }
776
-
777
- // Loop: cho phép `b` từ bước chọn skill quay lại menu action.
778
- while (true) {
779
- const action = await promptAction(skills);
780
- if (action === "exit") {
781
- console.log(t("EXITED"));
782
- return;
783
- }
784
- if (action === "list") {
785
- printSkillList(skills);
786
- continue; // quay lại menu action (KHÔNG return — 'list' xong vẫn ở trong pan ai)
787
- }
788
- if (action === "install") {
789
- await runInstallFlow(workspace);
790
- continue;
791
- }
792
- if (action === "update") {
793
- await runUpdateFlow(workspace);
794
- continue;
795
- }
796
-
797
- skills = scanSkills(workspace.root);
798
- const candidates = skillsForAction(skills, action);
799
- const res = await promptSkillNames(candidates, action);
800
- if (res.action === "exit") {
801
- console.log(t("EXITED"));
802
- return;
803
- }
804
- if (res.action === "back") {
805
- continue; // quay lại menu action
806
- }
807
- if (!res.values.length) {
808
- console.log(t("AI_EMPTY_SELECTION"));
809
- continue; // quay lại menu action
810
- }
811
- runNamed(action, res.values);
812
- skills = scanSkills(workspace.root);
813
- continue; // quay lại menu action — làm nhiều việc trong 1 lần chạy 'pan ai', 'x' để thoát
814
- }
815
- }
816
-
817
- module.exports = {
818
- scanSkills,
819
- applySkillAction,
820
- printSkillList,
821
- parseAiArgs,
822
- runAiSkills,
823
- printAiHelp,
824
- AGENT_KEYS,
825
- AGENT_DIRS,
826
- detectAgentDirs,
827
- runInstallFlow,
828
- runUpdateFlow,
829
- };