@sswl/ai-manager 0.1.0

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/lib/cli.js ADDED
@@ -0,0 +1,919 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const readline = require("readline");
6
+ const core = require("./runtime");
7
+
8
+ const SUPPORTED_TOOLS = core.SUPPORTED_TOOLS;
9
+ const DEFAULT_REPO_URL = core.DEFAULT_REPO_URL;
10
+ const ANSI = {
11
+ reset: "\u001b[0m",
12
+ dim: "\u001b[2m",
13
+ bold: "\u001b[1m",
14
+ green: "\u001b[32m",
15
+ cyan: "\u001b[36m",
16
+ yellow: "\u001b[33m",
17
+ white: "\u001b[37m"
18
+ };
19
+
20
+ function color(text, tone) {
21
+ if (!process.stdout.isTTY) {
22
+ return text;
23
+ }
24
+ return `${ANSI[tone] || ""}${text}${ANSI.reset}`;
25
+ }
26
+
27
+ function stripAnsi(text) {
28
+ return String(text || "").replace(/\u001b\[[0-9;]*m/g, "");
29
+ }
30
+
31
+ function truncateText(text, width) {
32
+ const raw = stripAnsi(text);
33
+ if (raw.length <= width) {
34
+ return text;
35
+ }
36
+ return `${raw.slice(0, Math.max(0, width - 1))}…`;
37
+ }
38
+
39
+ function usage(commandName = "ai-manager") {
40
+ console.log(`${color("SSWL AI Manager CLI", "bold")}
41
+
42
+ Usage:
43
+ ${commandName}
44
+ ${commandName} init [--repo-url URL] [--repo-dir PATH]
45
+ ${commandName} sync [--repo-url URL] [--repo-dir PATH]
46
+ ${commandName} list [--project PATH]
47
+ ${commandName} doctor [--project PATH]
48
+ ${commandName} apply --tool TOOL [--scope project|global] [--project PATH] [--profile ID | --asset ID ...]
49
+ ${commandName} update [--project PATH] [--tool TOOL]
50
+ ${commandName} remove --tool TOOL [--scope project|global] [--project PATH]
51
+ `);
52
+ }
53
+
54
+ function parseArgs(argv) {
55
+ const args = argv.slice(2);
56
+ let command = "";
57
+ const options = {};
58
+ let index = 0;
59
+ while (index < args.length) {
60
+ const current = args[index];
61
+ if (!command && !current.startsWith("--")) {
62
+ command = current;
63
+ index += 1;
64
+ continue;
65
+ }
66
+ if (!current.startsWith("--")) {
67
+ throw new Error(`unknown argument: ${current}`);
68
+ }
69
+ const key = current.slice(2);
70
+ if (["verbose", "yes", "help"].includes(key)) {
71
+ options[key] = true;
72
+ index += 1;
73
+ continue;
74
+ }
75
+ const next = args[index + 1];
76
+ if (!next || next.startsWith("--")) {
77
+ throw new Error(`missing value for --${key}`);
78
+ }
79
+ if (key === "asset") {
80
+ if (!Array.isArray(options.asset)) {
81
+ options.asset = [];
82
+ }
83
+ options.asset.push(next.trim());
84
+ } else {
85
+ options[key] = next;
86
+ }
87
+ index += 2;
88
+ }
89
+ return {
90
+ command: command || "interactive",
91
+ options
92
+ };
93
+ }
94
+
95
+ function createPrompt() {
96
+ const rl = readline.createInterface({
97
+ input: process.stdin,
98
+ output: process.stdout
99
+ });
100
+
101
+ let altScreen = false;
102
+ let rawActive = false;
103
+
104
+ function enterAltScreen() {
105
+ if (!process.stdin.isTTY || !process.stdout.isTTY || altScreen) {
106
+ return;
107
+ }
108
+ rl.output.write("\u001b[?1049h\u001b[?25l");
109
+ altScreen = true;
110
+ }
111
+
112
+ function exitAltScreen() {
113
+ if (!altScreen) {
114
+ return;
115
+ }
116
+ rl.output.write("\u001b[?25h\u001b[?1049l");
117
+ altScreen = false;
118
+ }
119
+
120
+ function setRawMode(enabled) {
121
+ if (!process.stdin.isTTY) {
122
+ return;
123
+ }
124
+ if (enabled && !rawActive) {
125
+ process.stdin.setRawMode(true);
126
+ rawActive = true;
127
+ return;
128
+ }
129
+ if (!enabled && rawActive) {
130
+ process.stdin.setRawMode(false);
131
+ rawActive = false;
132
+ }
133
+ }
134
+
135
+ const ask = (message) => new Promise((resolve) => {
136
+ rl.question(message, (answer) => resolve(String(answer || "").trim()));
137
+ });
138
+
139
+ function screenWidth() {
140
+ return Math.max(76, Math.min(process.stdout.columns || 96, 120));
141
+ }
142
+
143
+ function wrapText(text, width) {
144
+ const raw = String(text || "");
145
+ if (!raw) {
146
+ return [""];
147
+ }
148
+ const words = raw.split(/\s+/);
149
+ const result = [];
150
+ let current = "";
151
+ for (const word of words) {
152
+ const next = current ? `${current} ${word}` : word;
153
+ if (stripAnsi(next).length <= width) {
154
+ current = next;
155
+ } else {
156
+ if (current) {
157
+ result.push(current);
158
+ }
159
+ if (word.length > width) {
160
+ result.push(word.slice(0, width));
161
+ current = word.slice(width);
162
+ } else {
163
+ current = word;
164
+ }
165
+ }
166
+ }
167
+ if (current) {
168
+ result.push(current);
169
+ }
170
+ return result.length ? result : [raw];
171
+ }
172
+
173
+ function renderFrame({ eyebrow = "", title = "", subtitle = "", bodyLines = [], footerLines = [] }) {
174
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
175
+ return;
176
+ }
177
+ enterAltScreen();
178
+ const width = screenWidth();
179
+ const innerWidth = width - 4;
180
+ const top = color(`┌${"─".repeat(width - 2)}┐`, "dim");
181
+ const bottom = color(`└${"─".repeat(width - 2)}┘`, "dim");
182
+ const frame = [top];
183
+ const pushLine = (text = "") => {
184
+ const content = truncateText(text, innerWidth);
185
+ const rawLength = stripAnsi(content).length;
186
+ frame.push(`${color("│", "dim")} ${content}${" ".repeat(Math.max(0, innerWidth - rawLength))} ${color("│", "dim")}`);
187
+ };
188
+ pushLine(`${color("SSWL", "green")} ${color("AI Manager", "bold")} ${color("TUI", "dim")}`);
189
+ if (eyebrow) {
190
+ pushLine(color(eyebrow, "dim"));
191
+ }
192
+ if (title) {
193
+ pushLine(color(title, "bold"));
194
+ }
195
+ if (subtitle) {
196
+ for (const line of wrapText(subtitle, innerWidth)) {
197
+ pushLine(color(line, "white"));
198
+ }
199
+ }
200
+ pushLine("");
201
+ for (const line of bodyLines) {
202
+ pushLine(line);
203
+ }
204
+ if (footerLines.length) {
205
+ pushLine("");
206
+ frame.push(`${color("├", "dim")}${color("─".repeat(width - 2), "dim")}${color("┤", "dim")}`);
207
+ for (const line of footerLines) {
208
+ pushLine(color(line, "dim"));
209
+ }
210
+ }
211
+ frame.push(bottom);
212
+ rl.output.write("\u001b[2J\u001b[H");
213
+ rl.output.write(frame.join("\n"));
214
+ }
215
+
216
+ function closeTransientView() {
217
+ if (process.stdin.isTTY && process.stdout.isTTY) {
218
+ rl.output.write("\u001b[2J\u001b[H");
219
+ }
220
+ }
221
+
222
+ return {
223
+ async input(label, defaultValue = "", subtitle = "") {
224
+ renderFrame({
225
+ eyebrow: "文本输入",
226
+ title: label,
227
+ subtitle: subtitle || (defaultValue ? `默认值: ${defaultValue}` : "请输入内容后回车确认"),
228
+ bodyLines: [],
229
+ footerLines: ["回车提交"]
230
+ });
231
+ setRawMode(false);
232
+ const suffix = defaultValue ? ` [${defaultValue}]` : "";
233
+ const answer = await ask(`${color(">", "green")} ${label}${suffix}: `);
234
+ return answer || defaultValue;
235
+ },
236
+ async confirm(label, defaultValue = true) {
237
+ renderFrame({
238
+ eyebrow: "确认操作",
239
+ title: label,
240
+ subtitle: defaultValue ? "默认确认" : "默认取消",
241
+ bodyLines: [],
242
+ footerLines: ["输入 y / yes 确认,直接回车使用默认值"]
243
+ });
244
+ setRawMode(false);
245
+ const hint = defaultValue ? "Y/n" : "y/N";
246
+ const answer = (await ask(`${color("?", "cyan")} ${label} ${color(`(${hint})`, "dim")}: `)).toLowerCase();
247
+ if (!answer) {
248
+ return defaultValue;
249
+ }
250
+ return ["y", "yes", "1"].includes(answer);
251
+ },
252
+ async select(label, choices, defaultIndex = 0) {
253
+ if (!choices.length) {
254
+ throw new Error(`${label} 没有可选项`);
255
+ }
256
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
257
+ console.log(`\n${label}`);
258
+ choices.forEach((choice, index) => {
259
+ console.log(` ${index + 1}. ${choice.label}${choice.hint ? ` ${choice.hint}` : ""}`);
260
+ });
261
+ while (true) {
262
+ const raw = await ask(`请选择 [${defaultIndex + 1}]: `);
263
+ const nextIndex = raw ? Number(raw) - 1 : defaultIndex;
264
+ if (Number.isInteger(nextIndex) && nextIndex >= 0 && nextIndex < choices.length) {
265
+ return choices[nextIndex].value;
266
+ }
267
+ console.log("输入无效,请重新选择。");
268
+ }
269
+ }
270
+ return new Promise((resolve) => {
271
+ let cursor = Math.min(Math.max(defaultIndex, 0), choices.length - 1);
272
+ let scrollOffset = 0;
273
+ const cleanup = () => {
274
+ setRawMode(false);
275
+ process.stdin.removeListener("keypress", onKeypress);
276
+ closeTransientView();
277
+ };
278
+ const render = () => {
279
+ const visibleCount = Math.max(8, Math.min((process.stdout.rows || 28) - 10, 16));
280
+ if (cursor < scrollOffset) {
281
+ scrollOffset = cursor;
282
+ } else if (cursor >= scrollOffset + visibleCount) {
283
+ scrollOffset = cursor - visibleCount + 1;
284
+ }
285
+ const body = [];
286
+ const end = Math.min(choices.length, scrollOffset + visibleCount);
287
+ for (let index = scrollOffset; index < end; index += 1) {
288
+ const choice = choices[index];
289
+ const active = index === cursor;
290
+ body.push(`${active ? color("›", "green") : " "} ${color(String(index + 1).padStart(2, "0"), active ? "green" : "dim")} ${active ? color(choice.label, "bold") : color(choice.label, "white")}${choice.hint ? color(` ${choice.hint}`, "dim") : ""}`);
291
+ }
292
+ renderFrame({
293
+ eyebrow: `单选 ${cursor + 1}/${choices.length}`,
294
+ title: label,
295
+ subtitle: "选择一个目标项并确认",
296
+ bodyLines: body,
297
+ footerLines: ["↑ ↓ 移动 Enter 确认 Ctrl+C 退出"]
298
+ });
299
+ };
300
+ const onKeypress = (_str, key = {}) => {
301
+ if (key.name === "up") {
302
+ cursor = cursor === 0 ? choices.length - 1 : cursor - 1;
303
+ render();
304
+ return;
305
+ }
306
+ if (key.name === "down") {
307
+ cursor = cursor === choices.length - 1 ? 0 : cursor + 1;
308
+ render();
309
+ return;
310
+ }
311
+ if (key.name === "return") {
312
+ cleanup();
313
+ resolve(choices[cursor].value);
314
+ return;
315
+ }
316
+ if (key.ctrl && key.name === "c") {
317
+ cleanup();
318
+ process.exit(130);
319
+ }
320
+ };
321
+ readline.emitKeypressEvents(process.stdin, rl);
322
+ setRawMode(true);
323
+ process.stdin.on("keypress", onKeypress);
324
+ render();
325
+ });
326
+ },
327
+ async multiSelect(label, choices) {
328
+ if (!choices.length) {
329
+ throw new Error(`${label} 没有可选项`);
330
+ }
331
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
332
+ console.log(`\n${label}`);
333
+ choices.forEach((choice, index) => {
334
+ console.log(` ${index + 1}. ${choice.label}${choice.hint ? ` ${choice.hint}` : ""}`);
335
+ });
336
+ while (true) {
337
+ const raw = await ask("请输入编号,支持逗号分隔: ");
338
+ const indexes = raw.split(",").map((item) => Number(item.trim()) - 1).filter((item) => Number.isInteger(item));
339
+ const uniqueIndexes = [...new Set(indexes)].filter((item) => item >= 0 && item < choices.length);
340
+ if (uniqueIndexes.length > 0) {
341
+ return uniqueIndexes.map((item) => choices[item].value);
342
+ }
343
+ console.log("至少选择一个有效项。");
344
+ }
345
+ }
346
+ return new Promise((resolve) => {
347
+ let cursor = 0;
348
+ let scrollOffset = 0;
349
+ const selected = new Set();
350
+ const cleanup = () => {
351
+ setRawMode(false);
352
+ process.stdin.removeListener("keypress", onKeypress);
353
+ closeTransientView();
354
+ };
355
+ const render = () => {
356
+ const visibleCount = Math.max(8, Math.min((process.stdout.rows || 28) - 12, 18));
357
+ if (cursor < scrollOffset) {
358
+ scrollOffset = cursor;
359
+ } else if (cursor >= scrollOffset + visibleCount) {
360
+ scrollOffset = cursor - visibleCount + 1;
361
+ }
362
+ const body = [];
363
+ const end = Math.min(choices.length, scrollOffset + visibleCount);
364
+ for (let index = scrollOffset; index < end; index += 1) {
365
+ const choice = choices[index];
366
+ const active = index === cursor;
367
+ const checked = selected.has(choice.value);
368
+ body.push(`${active ? color("›", "green") : " "} ${checked ? color("[x]", "green") : color("[ ]", "dim")} ${color(String(index + 1).padStart(2, "0"), active ? "green" : "dim")} ${active ? color(choice.label, "bold") : color(choice.label, "white")}${choice.hint ? color(` ${choice.hint}`, "dim") : ""}`);
369
+ }
370
+ renderFrame({
371
+ eyebrow: `复选 ${selected.size} 项`,
372
+ title: label,
373
+ subtitle: "勾选后统一安装,依赖会自动补齐",
374
+ bodyLines: body,
375
+ footerLines: ["↑ ↓ 移动 Space 勾选 Enter 确认 Ctrl+C 退出"]
376
+ });
377
+ };
378
+ const onKeypress = (str, key = {}) => {
379
+ if (key.name === "up") {
380
+ cursor = cursor === 0 ? choices.length - 1 : cursor - 1;
381
+ render();
382
+ return;
383
+ }
384
+ if (key.name === "down") {
385
+ cursor = cursor === choices.length - 1 ? 0 : cursor + 1;
386
+ render();
387
+ return;
388
+ }
389
+ if (key.name === "space" || str === " ") {
390
+ const value = choices[cursor].value;
391
+ if (selected.has(value)) {
392
+ selected.delete(value);
393
+ } else {
394
+ selected.add(value);
395
+ }
396
+ render();
397
+ return;
398
+ }
399
+ if (key.name === "return") {
400
+ if (!selected.size) {
401
+ render();
402
+ return;
403
+ }
404
+ cleanup();
405
+ resolve(choices.filter((choice) => selected.has(choice.value)).map((choice) => choice.value));
406
+ return;
407
+ }
408
+ if (key.ctrl && key.name === "c") {
409
+ cleanup();
410
+ process.exit(130);
411
+ }
412
+ };
413
+ readline.emitKeypressEvents(process.stdin, rl);
414
+ setRawMode(true);
415
+ process.stdin.on("keypress", onKeypress);
416
+ render();
417
+ });
418
+ },
419
+ close() {
420
+ setRawMode(false);
421
+ exitAltScreen();
422
+ rl.close();
423
+ }
424
+ };
425
+ }
426
+
427
+ function isControlPlaneRepo(targetPath) {
428
+ if (!targetPath || !fs.existsSync(targetPath)) {
429
+ return false;
430
+ }
431
+ return ["catalog", "registry", "scripts", "standards"].every((marker) => fs.existsSync(path.join(path.resolve(targetPath), marker)));
432
+ }
433
+
434
+ function looksLikeProjectRoot(projectRoot) {
435
+ if (!projectRoot || !fs.existsSync(projectRoot)) {
436
+ return false;
437
+ }
438
+ const resolved = path.resolve(projectRoot);
439
+ if (isControlPlaneRepo(resolved)) {
440
+ return false;
441
+ }
442
+ return [".git", "package.json", "composer.json", ".claude", ".codex"].some((marker) => fs.existsSync(path.join(resolved, marker)));
443
+ }
444
+
445
+ function ensureProjectDirectory(projectRoot, options = {}) {
446
+ if (!String(projectRoot || "").trim()) {
447
+ throw new Error("目标项目目录不能为空,请输入业务项目的绝对路径");
448
+ }
449
+ const resolved = path.resolve(projectRoot);
450
+ if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
451
+ throw new Error(`目录不存在:${resolved}`);
452
+ }
453
+ if (isControlPlaneRepo(resolved)) {
454
+ throw new Error(`当前目录是控制面仓库,不是业务项目目录:${resolved}`);
455
+ }
456
+ if (!looksLikeProjectRoot(resolved) && !options.allowUnrecognized) {
457
+ throw new Error(`该目录未识别为常见项目根目录:${resolved}`);
458
+ }
459
+ return resolved;
460
+ }
461
+
462
+ function lastKnownProjectRoot() {
463
+ try {
464
+ const state = core.getAppState({});
465
+ const candidate = state?.settings?.last_project_root || "";
466
+ return candidate && fs.existsSync(candidate) && !isControlPlaneRepo(candidate) ? path.resolve(candidate) : "";
467
+ } catch (_error) {
468
+ return "";
469
+ }
470
+ }
471
+
472
+ function normalizeAssetSet(assetIds) {
473
+ return [...new Set((assetIds || []).filter(Boolean))].sort();
474
+ }
475
+
476
+ function resolveAssetDependencies(assets, assetIds) {
477
+ const assetMap = new Map((assets || []).map((asset) => [asset.id, asset]));
478
+ const queue = normalizeAssetSet(assetIds);
479
+ const resolved = new Set();
480
+ while (queue.length > 0) {
481
+ const current = queue.shift();
482
+ if (!current || resolved.has(current)) {
483
+ continue;
484
+ }
485
+ resolved.add(current);
486
+ const dependencies = Array.isArray(assetMap.get(current)?.dependencies) ? assetMap.get(current).dependencies : [];
487
+ for (const dependencyId of dependencies) {
488
+ if (!resolved.has(dependencyId)) {
489
+ queue.push(dependencyId);
490
+ }
491
+ }
492
+ }
493
+ return [...resolved].sort();
494
+ }
495
+
496
+ function createHooks(options = {}) {
497
+ return {
498
+ onLog(message) {
499
+ if (options.verbose) {
500
+ console.log(message);
501
+ }
502
+ },
503
+ onProgress(progress) {
504
+ const percent = progress?.percent != null ? `${String(progress.percent).padStart(3, " ")}%` : " --%";
505
+ const detail = progress?.detail || progress?.stage || "";
506
+ console.log(`[${percent}] ${detail}`);
507
+ }
508
+ };
509
+ }
510
+
511
+ function ensureGitMode(options = {}) {
512
+ core.updateSettings({
513
+ repo_url: options.repoUrl || DEFAULT_REPO_URL,
514
+ repo_dir: options.repoDir || undefined,
515
+ current_scope: options.scope || undefined,
516
+ last_project_root: Object.prototype.hasOwnProperty.call(options, "projectRoot") ? (options.projectRoot || "") : undefined
517
+ });
518
+ }
519
+
520
+ async function syncRepoForCli(options = {}) {
521
+ ensureGitMode(options);
522
+ return core.syncRepo({
523
+ repoUrl: options.repoUrl || "",
524
+ repoDir: options.repoDir || ""
525
+ }, createHooks(options));
526
+ }
527
+
528
+ function describeTool(toolState) {
529
+ const installed = toolState.enabled ? `已安装 ${toolState.installed_asset_ids?.length || 0} 项` : "未安装";
530
+ const detected = toolState.detected_installed ? "本机已检测到" : "本机未检测到";
531
+ return `${toolState.tool} / ${installed} / ${detected}`;
532
+ }
533
+
534
+ async function resolveProjectForCli(prompt, explicitProjectRoot = "") {
535
+ if (explicitProjectRoot) {
536
+ return ensureProjectDirectory(explicitProjectRoot, { allowUnrecognized: true });
537
+ }
538
+ try {
539
+ const currentDir = ensureProjectDirectory(process.cwd(), { allowUnrecognized: true });
540
+ console.log(`\n当前默认项目目录: ${currentDir}`);
541
+ if (!looksLikeProjectRoot(currentDir)) {
542
+ printWarning("当前目录没有命中常见项目标记,但仍可按当前目录处理。");
543
+ }
544
+ const useCurrent = await prompt.confirm("是否直接使用当前目录作为目标项目目录", true);
545
+ if (useCurrent) {
546
+ return currentDir;
547
+ }
548
+ } catch (_error) {}
549
+ const fallbackProject = lastKnownProjectRoot();
550
+ while (true) {
551
+ const selected = await prompt.input(
552
+ "请输入目标项目目录",
553
+ fallbackProject || "",
554
+ "当前目录不是业务项目目录,请输入或粘贴你要安装资产的项目路径"
555
+ );
556
+ try {
557
+ const resolved = ensureProjectDirectory(String(selected || "").trim(), { allowUnrecognized: true });
558
+ if (!looksLikeProjectRoot(resolved)) {
559
+ printWarning("该目录没有命中常见项目标记,但会继续按指定目录处理。");
560
+ }
561
+ return resolved;
562
+ } catch (error) {
563
+ printWarning(error.message || String(error));
564
+ }
565
+ }
566
+ }
567
+
568
+ async function resolveProjectForCommand(explicitProjectRoot = "") {
569
+ if (explicitProjectRoot) {
570
+ return ensureProjectDirectory(explicitProjectRoot, { allowUnrecognized: true });
571
+ }
572
+ try {
573
+ return ensureProjectDirectory(process.cwd(), { allowUnrecognized: true });
574
+ } catch (_error) {
575
+ const fallbackProject = lastKnownProjectRoot();
576
+ if (fallbackProject) {
577
+ return fallbackProject;
578
+ }
579
+ throw new Error("当前目录不是业务项目目录,请使用 --project 指定目标项目目录");
580
+ }
581
+ }
582
+
583
+ function printSection(title) {
584
+ console.log(`\n${color(`◆ ${title}`, "bold")}`);
585
+ }
586
+
587
+ function printBanner() {
588
+ const line = "─".repeat(56);
589
+ console.log(color(line, "dim"));
590
+ console.log(`${color("SSWL", "green")} ${color("AI Manager CLI", "bold")} ${color("项目级资产安装器", "dim")}`);
591
+ console.log(color(line, "dim"));
592
+ }
593
+
594
+ function printKeyValue(label, value, tone = "white") {
595
+ console.log(`${color(label.padEnd(12, " "), "dim")} ${color(value, tone)}`);
596
+ }
597
+
598
+ function printBulletList(title, lines, tone = "white") {
599
+ if (!lines.length) {
600
+ return;
601
+ }
602
+ console.log(color(title, "dim"));
603
+ lines.forEach((line) => console.log(` ${color("•", "green")} ${color(line, tone)}`));
604
+ }
605
+
606
+ function printSuccess(message) {
607
+ console.log(color(`✔ ${message}`, "green"));
608
+ }
609
+
610
+ function printWarning(message) {
611
+ console.log(color(`! ${message}`, "yellow"));
612
+ }
613
+
614
+ function summarizeAssets(state, assetIds) {
615
+ return assetIds.map((assetId) => {
616
+ const asset = (state.assets || []).find((item) => item.id === assetId);
617
+ return asset ? `${assetId} (${asset.type}/${asset.domain})` : assetId;
618
+ });
619
+ }
620
+
621
+ function filterProfilesByTool(state, tool) {
622
+ return (state.profiles || []).filter((profile) => (profile.supported_tools || []).includes(tool));
623
+ }
624
+
625
+ function filterInstallableAssetsByTool(state, tool) {
626
+ return (state.assets || [])
627
+ .filter((asset) => (asset.supported_tools || []).includes(tool))
628
+ .filter((asset) => asset.installable !== false);
629
+ }
630
+
631
+ async function interactiveInstall(options = {}) {
632
+ const prompt = createPrompt();
633
+ try {
634
+ printBanner();
635
+ printSection("项目确认");
636
+ const projectRoot = await resolveProjectForCli(prompt, options.project);
637
+ printSection("仓库同步");
638
+ const syncedState = await syncRepoForCli({ ...options, scope: "project", projectRoot });
639
+ const state = core.getAppState({
640
+ repoDir: syncedState.settings.repo_dir,
641
+ projectRoot
642
+ });
643
+ const projectTools = state.project?.tools || [];
644
+ const availableTools = projectTools.filter((toolState) => toolState.manageable && SUPPORTED_TOOLS.includes(toolState.tool));
645
+ printSection("工具选择");
646
+ const selectedTool = await prompt.select("选择目标工具", availableTools.map((toolState) => ({
647
+ label: toolState.tool,
648
+ hint: describeTool(toolState),
649
+ value: toolState.tool
650
+ })), 0);
651
+ const toolState = availableTools.find((item) => item.tool === selectedTool) || availableTools[0];
652
+ const scope = await prompt.select("选择安装作用域", [
653
+ { label: "项目级配置", hint: "推荐,直接写入当前项目", value: "project" },
654
+ { label: "全局配置", hint: "安装到当前用户全局目录", value: "global" }
655
+ ], 0);
656
+ let inheritGlobal = true;
657
+ if (scope === "project" && (state.tools.find((item) => item.tool === selectedTool)?.installed_asset_ids || []).length) {
658
+ inheritGlobal = await prompt.confirm("项目级安装是否继承当前全局已安装资产", true);
659
+ }
660
+ const profiles = filterProfilesByTool(state, selectedTool);
661
+ printSection("资产组合");
662
+ const profileChoices = profiles.map((profile) => ({
663
+ label: profile.id,
664
+ hint: `${profile.name || profile.id} / ${profile.asset_ids.length} 项`,
665
+ value: profile.id
666
+ }));
667
+ profileChoices.push({
668
+ label: "自定义选择",
669
+ hint: "手工选择资产,依赖会自动补齐",
670
+ value: "__custom__"
671
+ });
672
+ const profileId = await prompt.select("选择安装档位", profileChoices, 0);
673
+ let selectedAssetIds = [];
674
+ let selectionMode = "profile";
675
+ let planName = profileId;
676
+ if (profileId === "__custom__") {
677
+ const assets = filterInstallableAssetsByTool(state, selectedTool);
678
+ selectedAssetIds = await prompt.multiSelect("选择需要同步到项目的资产", assets.map((asset) => ({
679
+ label: `${asset.id}`,
680
+ hint: `${asset.type} / ${asset.domain} / ${asset.status}`,
681
+ value: asset.id
682
+ })));
683
+ selectionMode = "custom";
684
+ planName = "custom-selection";
685
+ } else {
686
+ const profile = profiles.find((item) => item.id === profileId);
687
+ selectedAssetIds = profile?.asset_ids || [];
688
+ }
689
+ const globalInstalled = scope === "project" && inheritGlobal
690
+ ? (state.tools.find((item) => item.tool === selectedTool)?.installed_asset_ids || [])
691
+ : [];
692
+ const previewAssetIds = resolveAssetDependencies(state.assets || [], normalizeAssetSet([...globalInstalled, ...selectedAssetIds]));
693
+ printSection("执行预览");
694
+ printKeyValue("目标项目", projectRoot);
695
+ printKeyValue("目标工具", selectedTool, "cyan");
696
+ printKeyValue("安装作用域", scope === "project" ? "项目级" : "全局");
697
+ printKeyValue("选择模式", selectionMode === "profile" ? `档位 ${profileId}` : "自定义资产");
698
+ printBulletList("即将同步的资产", summarizeAssets(state, previewAssetIds));
699
+ const confirmed = options.yes || await prompt.confirm("确认继续执行安装", true);
700
+ if (!confirmed) {
701
+ printWarning("已取消。");
702
+ return;
703
+ }
704
+ const nextState = await core.applyActivationPlan({
705
+ tool: selectedTool,
706
+ assetIds: selectedAssetIds,
707
+ selectedAssetIds,
708
+ installRoot: scope === "project" ? "" : (state.tools.find((item) => item.tool === selectedTool)?.install_root || ""),
709
+ linkName: toolState.link_name || "sswl-ai-coding-platform",
710
+ linkTarget: toolState.link_target || "skills",
711
+ repoDir: state.settings.repo_dir || "",
712
+ repoUrl: state.settings.repo_url || "",
713
+ planName,
714
+ profileId: profileId === "__custom__" ? "" : profileId,
715
+ selectionMode,
716
+ scope,
717
+ projectRoot,
718
+ inheritGlobal
719
+ }, createHooks(options));
720
+ printSection("完成");
721
+ printSuccess("安装完成");
722
+ printKeyValue("项目", projectRoot);
723
+ printKeyValue("工具", selectedTool, "cyan");
724
+ printKeyValue("已安装资产", `${(scope === "project" ? nextState.project?.tools : nextState.tools).find((item) => item.tool === selectedTool)?.installed_asset_ids?.length || 0} 项`);
725
+ printKeyValue("后续更新", `ai-manager update --project "${projectRoot}"`, "dim");
726
+ } finally {
727
+ prompt.close();
728
+ }
729
+ }
730
+
731
+ async function runInit(options = {}) {
732
+ printBanner();
733
+ const state = await syncRepoForCli(options);
734
+ printSuccess("仓库已准备完成");
735
+ printKeyValue("仓库目录", state.settings.repo_dir);
736
+ }
737
+
738
+ async function runSync(options = {}) {
739
+ printBanner();
740
+ const state = await syncRepoForCli(options);
741
+ printSuccess("仓库已同步");
742
+ printKeyValue("仓库目录", state.settings.repo_dir);
743
+ }
744
+
745
+ function parseScope(value = "") {
746
+ return value === "global" ? "global" : "project";
747
+ }
748
+
749
+ async function runList(options = {}) {
750
+ printBanner();
751
+ ensureGitMode(options);
752
+ const projectRoot = options.project ? path.resolve(options.project) : "";
753
+ const state = core.getAppState({ projectRoot });
754
+ printKeyValue("资产源模式", state.settings.asset_source_mode);
755
+ printKeyValue("仓库目录", state.settings.repo_dir);
756
+ if (projectRoot) {
757
+ printKeyValue("项目目录", projectRoot);
758
+ }
759
+ printSection("可用档位");
760
+ for (const tool of SUPPORTED_TOOLS) {
761
+ const profiles = filterProfilesByTool(state, tool);
762
+ printKeyValue(tool, profiles.map((profile) => profile.id).join(", ") || "无");
763
+ }
764
+ printSection("工具状态");
765
+ const scopedTools = projectRoot && state.project ? state.project.tools : state.tools;
766
+ scopedTools.filter((toolState) => SUPPORTED_TOOLS.includes(toolState.tool)).forEach((toolState) => {
767
+ console.log(` ${color("•", "green")} ${describeTool(toolState)}`);
768
+ });
769
+ }
770
+
771
+ async function runDoctor(options = {}) {
772
+ printBanner();
773
+ ensureGitMode(options);
774
+ let projectRoot = "";
775
+ try {
776
+ projectRoot = options.project ? ensureProjectDirectory(options.project, { allowUnrecognized: true }) : ensureProjectDirectory(process.cwd(), { allowUnrecognized: true });
777
+ } catch (_error) {
778
+ projectRoot = "";
779
+ }
780
+ const network = await core.getEnterpriseNetworkStatus();
781
+ const state = core.getAppState({ projectRoot });
782
+ printKeyValue("企业网络", `${network.status_label}${network.error ? ` (${network.error})` : ""}`, network.reachable ? "green" : "yellow");
783
+ printKeyValue("仓库地址", state.settings.repo_url);
784
+ printKeyValue("仓库目录", state.settings.repo_dir);
785
+ printKeyValue("资产源模式", state.settings.asset_source_mode);
786
+ printKeyValue("当前项目", projectRoot || "未识别");
787
+ printSection("工具检测");
788
+ state.tools.filter((toolState) => SUPPORTED_TOOLS.includes(toolState.tool)).forEach((toolState) => {
789
+ console.log(` ${color("•", "green")} ${toolState.tool}: ${toolState.detected_installed ? "已检测到" : "未检测到"}${toolState.detected_command_path ? ` / ${toolState.detected_command_path}` : ""}`);
790
+ });
791
+ }
792
+
793
+ async function runApply(options = {}) {
794
+ printBanner();
795
+ if (!options.tool || !SUPPORTED_TOOLS.includes(options.tool)) {
796
+ throw new Error(`--tool 必须是 ${SUPPORTED_TOOLS.join(" / ")}`);
797
+ }
798
+ const scope = parseScope(options.scope);
799
+ const projectRoot = scope === "project" ? await resolveProjectForCommand(options.project) : "";
800
+ const state = await syncRepoForCli({ ...options, scope, projectRoot });
801
+ const profiles = filterProfilesByTool(state, options.tool);
802
+ let selectedAssetIds = [];
803
+ let profileId = "";
804
+ let selectionMode = "";
805
+ let planName = "";
806
+ if (options.profile) {
807
+ const profile = profiles.find((item) => item.id === options.profile);
808
+ if (!profile) {
809
+ throw new Error(`未知档位: ${options.profile}`);
810
+ }
811
+ selectedAssetIds = profile.asset_ids;
812
+ profileId = profile.id;
813
+ selectionMode = "profile";
814
+ planName = profile.id;
815
+ } else if (Array.isArray(options.asset) && options.asset.length) {
816
+ selectedAssetIds = options.asset;
817
+ selectionMode = "custom";
818
+ planName = "custom-selection";
819
+ } else {
820
+ throw new Error("apply 必须提供 --profile 或至少一个 --asset");
821
+ }
822
+ await core.applyActivationPlan({
823
+ tool: options.tool,
824
+ assetIds: selectedAssetIds,
825
+ selectedAssetIds,
826
+ installRoot: scope === "global" ? (state.tools.find((item) => item.tool === options.tool)?.install_root || "") : "",
827
+ repoDir: state.settings.repo_dir || "",
828
+ repoUrl: state.settings.repo_url || "",
829
+ planName,
830
+ profileId,
831
+ selectionMode,
832
+ scope,
833
+ projectRoot,
834
+ inheritGlobal: String(options["inherit-global"] || "true").toLowerCase() !== "false"
835
+ }, createHooks(options));
836
+ printSuccess("安装完成");
837
+ }
838
+
839
+ async function runUpdate(options = {}) {
840
+ printBanner();
841
+ const projectRoot = await resolveProjectForCommand(options.project);
842
+ ensureGitMode({ ...options, scope: "project", projectRoot });
843
+ await core.updateProjectInstallations({
844
+ projectRoot,
845
+ repoDir: options.repoDir || options["repo-dir"] || "",
846
+ repoUrl: options.repoUrl || options["repo-url"] || "",
847
+ tool: options.tool || ""
848
+ }, createHooks(options));
849
+ printSuccess("项目已更新");
850
+ printKeyValue("项目目录", projectRoot);
851
+ }
852
+
853
+ async function runRemove(options = {}) {
854
+ printBanner();
855
+ if (!options.tool || !SUPPORTED_TOOLS.includes(options.tool)) {
856
+ throw new Error(`--tool 必须是 ${SUPPORTED_TOOLS.join(" / ")}`);
857
+ }
858
+ const scope = parseScope(options.scope);
859
+ const projectRoot = scope === "project" ? await resolveProjectForCommand(options.project) : "";
860
+ await core.disableTool({
861
+ tool: options.tool,
862
+ scope,
863
+ projectRoot
864
+ }, createHooks(options));
865
+ printSuccess("卸载完成");
866
+ }
867
+
868
+ async function runCli(argv = process.argv, options = {}) {
869
+ const { command, options: parsedOptions } = parseArgs(argv);
870
+ if (parsedOptions.help || command === "help") {
871
+ usage(options.commandName || "ai-manager");
872
+ return;
873
+ }
874
+ const normalizedOptions = {
875
+ ...parsedOptions,
876
+ repoUrl: parsedOptions["repo-url"] || "",
877
+ repoDir: parsedOptions["repo-dir"] || "",
878
+ project: parsedOptions.project || "",
879
+ verbose: Boolean(parsedOptions.verbose),
880
+ yes: Boolean(parsedOptions.yes)
881
+ };
882
+ if (command === "interactive") {
883
+ await interactiveInstall(normalizedOptions);
884
+ return;
885
+ }
886
+ if (command === "init") {
887
+ await runInit(normalizedOptions);
888
+ return;
889
+ }
890
+ if (command === "sync") {
891
+ await runSync(normalizedOptions);
892
+ return;
893
+ }
894
+ if (command === "list") {
895
+ await runList(normalizedOptions);
896
+ return;
897
+ }
898
+ if (command === "doctor") {
899
+ await runDoctor(normalizedOptions);
900
+ return;
901
+ }
902
+ if (command === "apply") {
903
+ await runApply(normalizedOptions);
904
+ return;
905
+ }
906
+ if (command === "update") {
907
+ await runUpdate(normalizedOptions);
908
+ return;
909
+ }
910
+ if (command === "remove") {
911
+ await runRemove(normalizedOptions);
912
+ return;
913
+ }
914
+ throw new Error(`unknown command: ${command}`);
915
+ }
916
+
917
+ module.exports = {
918
+ runCli
919
+ };