flower-trellis 0.6.2 → 0.6.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.
Files changed (31) hide show
  1. package/package.json +3 -3
  2. package/src/builtin-marketplaces/rd-guide.json +6 -3
  3. package/src/builtin-plugins/flower-plugin-author/plugin.json +6 -1
  4. package/src/builtin-plugins/flower-plugin-author/skills/flower-plugin-author/references/ci-and-review.md +2 -2
  5. package/src/commands/plugin-interactive.js +546 -38
  6. package/src/commands/plugin-remote.js +156 -10
  7. package/src/commands/plugin.js +35 -4
  8. package/src/lib/skill-catalog.js +20 -1
  9. package/src/plugin/application-service.js +81 -7
  10. package/src/plugin/auth/credential-store.js +25 -3
  11. package/src/plugin/auth/gitlab-credential-resolver.js +234 -0
  12. package/src/plugin/auth/gitlab-oauth.js +26 -5
  13. package/src/plugin/authoring/scaffold.js +6 -1
  14. package/src/plugin/authoring/templates/rd-guide/CODEOWNERS +1 -1
  15. package/src/plugin/authoring/templates/rd-guide/gitlab-ci.yml +2 -2
  16. package/src/plugin/authoring/templates/rd-guide/verify-integration-review.mjs +1 -1
  17. package/src/plugin/content-selection.js +189 -0
  18. package/src/plugin/contracts.js +21 -2
  19. package/src/plugin/formats/adapters.js +27 -3
  20. package/src/plugin/formats/normalized-package.js +126 -14
  21. package/src/plugin/gitlab/rest-client.js +21 -0
  22. package/src/plugin/install/content-projector.js +19 -7
  23. package/src/plugin/resolver/dependency-resolver.js +6 -0
  24. package/src/plugin/runtime-errors.js +1 -0
  25. package/src/plugin/schemas/marketplace-manifest.js +4 -1
  26. package/src/plugin/schemas/plugin-manifest.js +55 -3
  27. package/src/plugin/schemas/project-files.js +18 -0
  28. package/src/plugin/schemas/shared.js +10 -0
  29. package/src/plugin/sources/github-provider.js +1 -1
  30. package/src/plugin/sources/gitlab-provider.js +377 -51
  31. package/src/plugin/sources/user-source-store.js +17 -1
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import readline from "node:readline";
3
4
  import chalk from "chalk";
4
5
  import semver from "semver";
5
6
  import {
@@ -7,6 +8,8 @@ import {
7
8
  SkillGardenBuiltinProvider,
8
9
  } from "../builtin-plugins/skill-garden/provider.js";
9
10
  import { readLegacyManifestStatus } from "../lib/manifest.js";
11
+ import { summarizeSkillDescription } from "../lib/skill-catalog.js";
12
+ import { flowerVersion } from "../lib/versions.js";
10
13
  import { createCredentialStore } from "../plugin/auth/keyring-credential-store.js";
11
14
  import {
12
15
  detectPluginPlatforms,
@@ -18,6 +21,13 @@ import {
18
21
  UserSourceStore,
19
22
  } from "../plugin/sources/user-source-store.js";
20
23
  import { ProjectStore } from "../plugin/state/project-store.js";
24
+ import {
25
+ contentSelectionsEqual,
26
+ contentSelectionArgs,
27
+ contentSelectionFromSkillNames,
28
+ normalizeContentSelection,
29
+ } from "../plugin/content-selection.js";
30
+ import { parseCanonicalPluginId } from "../plugin/schemas/shared.js";
21
31
  import {
22
32
  pluginActionPrompt,
23
33
  pluginManagerPrompt,
@@ -140,6 +150,304 @@ function withPlatforms(args, platforms) {
140
150
  ];
141
151
  }
142
152
 
153
+ /**
154
+ * 计算纯文本宽度,用于对齐 Skill 名称。
155
+ *
156
+ * @param {string[]} values 文本列表
157
+ * @returns {number} 最大宽度
158
+ */
159
+ function maxTextWidth(values) {
160
+ return Math.max(0, ...values.map((value) => String(value).length));
161
+ }
162
+
163
+ /**
164
+ * 判断 canonical Plugin 是否来自 rd-guide Marketplace。
165
+ *
166
+ * @param {string} pluginId canonical Plugin ID
167
+ * @param {string|null|undefined} source 显式来源 ID
168
+ * @returns {boolean} 是否 rd-guide 来源
169
+ */
170
+ function isRdGuideMarketplacePlugin(pluginId, source) {
171
+ const sourceId = source || parseCanonicalPluginId(pluginId).sourceId;
172
+ return sourceId === "rd-guide";
173
+ }
174
+
175
+ /**
176
+ * 从多份 Project 记录中读取第一份显式 Skill 选择。
177
+ *
178
+ * @param {...object|null|undefined} entries Project plugin 记录
179
+ * @returns {import("../plugin/contracts.js").PluginContentSelection|undefined} 归一化后的选择
180
+ */
181
+ function firstContentSelection(...entries) {
182
+ for (const entry of entries) {
183
+ const selection = normalizeContentSelection(entry?.contentSelection);
184
+ if (selection) return selection;
185
+ }
186
+ return undefined;
187
+ }
188
+
189
+ /**
190
+ * 生成 rd-guide 来源级 Skill 入口的元信息。
191
+ *
192
+ * @param {string} sourceName 来源展示名
193
+ * @param {object|null|undefined} selection 当前 Skill 选择
194
+ * @returns {string} 不暴露底层 Plugin 包版本的元信息
195
+ */
196
+ function rdGuideSkillMeta(sourceName, selection) {
197
+ const normalized = normalizeContentSelection(selection);
198
+ const count = normalized?.skills?.length || 0;
199
+ return count > 0 ? `${sourceName} · 已启用 ${count} 个技能` : sourceName;
200
+ }
201
+
202
+ /**
203
+ * 为一次 Skill 清单 inspection 构造会话缓存键。
204
+ *
205
+ * @param {{pluginId:string,version?:string,source?:string|null,lockedPlugin?:object|null}} input 选择输入
206
+ * @returns {string|null} 缓存键
207
+ */
208
+ function skillInspectionCacheKey(input) {
209
+ const pluginId = input.pluginId || input.lockedPlugin?.id;
210
+ if (!pluginId) return null;
211
+ const sourceId = input.source || input.lockedPlugin?.source?.id || parseCanonicalPluginId(pluginId).sourceId;
212
+ const version = input.version || input.lockedPlugin?.version || "";
213
+ const integrity = input.lockedPlugin?.integrity || "";
214
+ return [sourceId, pluginId, version, integrity].join("\u0000");
215
+ }
216
+
217
+ /**
218
+ * 生成 Marketplace Skill 管理标题。
219
+ *
220
+ * @param {string} pluginId canonical Plugin ID
221
+ * @returns {string} 标题
222
+ */
223
+ function skillSelectionTitle(pluginId) {
224
+ const { sourceId } = parseCanonicalPluginId(pluginId);
225
+ return sourceId === "rd-guide" ? "RD Guide 技能管理" : `${pluginId} Skill 管理`;
226
+ }
227
+
228
+ /**
229
+ * 生成普通 Marketplace Skill 选择页文案。
230
+ *
231
+ * @param {{pluginId:string,source?:string|null}} input 选择输入
232
+ * @param {{version?:string,name?:string}} inspection inspection 结果
233
+ * @returns {{title:string,meta:string,section:string,hint:string,message:string,empty:string}} 文案集合
234
+ */
235
+ function skillSelectionLabels(input, inspection) {
236
+ if (isRdGuideMarketplacePlugin(input.pluginId, input.source)) {
237
+ return {
238
+ title: "RD Guide 技能管理",
239
+ meta: "来源 rd-guide",
240
+ section: "可选研发技能",
241
+ hint: "勾选表示启用,取消勾选表示停用;未勾选项仍保留在清单中,可重新启用。",
242
+ message: "选择要启用的 RD Guide 技能",
243
+ empty: "未选择 RD Guide 技能,已取消安装",
244
+ };
245
+ }
246
+ return {
247
+ title: skillSelectionTitle(input.pluginId),
248
+ meta: [inspection.name || input.pluginId, inspection.version ? `版本 ${inspection.version}` : ""]
249
+ .filter(Boolean)
250
+ .join(" · "),
251
+ section: "可选 Plugin 技能",
252
+ hint: "勾选表示启用,取消勾选表示停用。",
253
+ message: "选择要启用的 Plugin 技能",
254
+ empty: "未选择 Plugin 技能,已取消",
255
+ };
256
+ }
257
+
258
+ /**
259
+ * 打印普通 Marketplace Skill 选择页头部。
260
+ *
261
+ * @param {object} context 交互上下文
262
+ * @param {{title:string,meta:string,section:string,hint:string}} labels 文案集合
263
+ * @returns {void}
264
+ */
265
+ function printMarketplaceSkillHeader(context, labels) {
266
+ context.output.log("");
267
+ context.output.log(chalk.hex(TITLE_COLOR).bold(labels.title));
268
+ if (labels.meta) context.output.log(chalk.gray(` ${labels.meta}`));
269
+ context.output.log("");
270
+ context.output.log(chalk.bold(labels.section));
271
+ context.output.log(chalk.gray(` ${labels.hint}`));
272
+ }
273
+
274
+ /**
275
+ * 构造 Marketplace Skill checkbox 选项。
276
+ *
277
+ * @param {Array<{name:string,path:string,description?:string,version?:string}>} skills Skill 清单
278
+ * @param {Set<string>} defaults 默认勾选项
279
+ * @returns {Array<object>} checkbox choices
280
+ */
281
+ function buildMarketplaceSkillChoices(skills, defaults) {
282
+ const width = maxTextWidth(skills.map(({ name }) => name));
283
+ const versionWidth = maxTextWidth(skills.map(({ version }) => version ? `v${version}` : ""));
284
+ return skills.map((skill) => {
285
+ const description = skill.description
286
+ ? summarizeSkillDescription(skill.description, 34)
287
+ : summarizeSkillDescription(skill.path, 34);
288
+ const version = skill.version ? `v${skill.version}` : "";
289
+ const detail = versionWidth > 0
290
+ ? `${version.padEnd(versionWidth)} ${description}`
291
+ : description;
292
+ const label = `${skill.name.padEnd(width)} ${chalk.gray(detail)}`;
293
+ return {
294
+ name: label,
295
+ short: skill.name,
296
+ checkedName: label,
297
+ value: skill.name,
298
+ checked: defaults.has(skill.name),
299
+ };
300
+ });
301
+ }
302
+
303
+ /**
304
+ * 创建与内置 skill 管理一致的中文 checkbox 主题。
305
+ *
306
+ * @returns {object} Inquirer theme
307
+ */
308
+ function marketplaceSkillCheckboxTheme() {
309
+ return {
310
+ style: {
311
+ description: (text) => chalk.gray(text),
312
+ keysHelpTip: (keys) =>
313
+ keys
314
+ .map(([key, action]) => {
315
+ const keyLabels = {
316
+ escape: "Esc",
317
+ space: "空格",
318
+ "⏎": "回车",
319
+ };
320
+ const labels = {
321
+ navigate: "移动",
322
+ select: "选择",
323
+ submit: "确认",
324
+ };
325
+ return `${chalk.bold(keyLabels[key] || key)} ${chalk.gray(labels[action] || action)}`;
326
+ })
327
+ .concat(`${chalk.bold("ESC")} ${chalk.gray("退出")}`)
328
+ .join(chalk.gray(" · ")),
329
+ renderSelectedChoices: (selectedChoices) =>
330
+ selectedChoices.map((choice) => choice.short).join(", "),
331
+ },
332
+ };
333
+ }
334
+
335
+ /**
336
+ * 创建 Esc 取消控制器。
337
+ *
338
+ * @returns {{signal?:AbortSignal,dispose:Function,isEscAbort:Function}} prompt signal 与清理函数
339
+ */
340
+ function createEscAbortController() {
341
+ if (!process.stdin?.isTTY) {
342
+ return { dispose: () => {}, isEscAbort: () => false };
343
+ }
344
+ const controller = new AbortController();
345
+ let abortedByEsc = false;
346
+ readline.emitKeypressEvents(process.stdin);
347
+
348
+ const onKeypress = (_value, key) => {
349
+ if (key && key.name === "escape") {
350
+ abortedByEsc = true;
351
+ controller.abort();
352
+ }
353
+ };
354
+ process.stdin.on("keypress", onKeypress);
355
+
356
+ return {
357
+ signal: controller.signal,
358
+ dispose: () => process.stdin.off("keypress", onKeypress),
359
+ isEscAbort: () => abortedByEsc,
360
+ };
361
+ }
362
+
363
+ /**
364
+ * 判断已安装 Plugin 是否适合展示普通 Marketplace Skill 选择入口。
365
+ *
366
+ * @param {string} pluginId canonical Plugin ID
367
+ * @param {object|null|undefined} declaration 直接声明
368
+ * @returns {boolean} 是否展示入口
369
+ */
370
+ function supportsMarketplaceSkillSelection(pluginId, declaration) {
371
+ if (pluginId === SKILL_GARDEN_PLUGIN_ID) return false;
372
+ const sourceId = declaration?.source || parseCanonicalPluginId(pluginId).sourceId;
373
+ return sourceId !== "local" && sourceId !== "flower";
374
+ }
375
+
376
+ /**
377
+ * 读取并提示用户选择普通 Marketplace Plugin 的 Skill 子集。
378
+ *
379
+ * @param {object} context 交互上下文
380
+ * @param {{pluginId:string,version?:string,source?:string|null,currentSelection?:object|null,lockedPlugin?:object|null,emptySelectionAction?:"cancel"|"return",defaultSelected?:boolean}} input 选择输入
381
+ * @returns {Promise<{ok:boolean,selection:object|null,emptySelection?:boolean}>} 选择结果;无 Skill 时 selection 为 null
382
+ */
383
+ async function promptMarketplaceSkillSelection(context, input) {
384
+ let inspection;
385
+ const cacheKey = skillInspectionCacheKey(input);
386
+ try {
387
+ inspection = cacheKey ? context.state.skillInspections.get(cacheKey) : null;
388
+ if (!inspection) {
389
+ if (isRdGuideMarketplacePlugin(input.pluginId, input.source)) {
390
+ context.output.log("\n正在读取 RD Guide 技能清单...");
391
+ }
392
+ inspection = await context.inspectPluginContentSkills(input);
393
+ if (cacheKey) context.state.skillInspections.set(cacheKey, inspection);
394
+ }
395
+ } catch (error) {
396
+ recordIssue(context.state, `${input.pluginId} Skill 清单读取失败`, error);
397
+ context.state.lastFailure = `${input.pluginId} Skill 清单读取失败`;
398
+ return { ok: false, selection: null };
399
+ }
400
+ const skills = inspection.skills || [];
401
+ if (skills.length === 0) return { ok: true, selection: null };
402
+ const currentSelection = normalizeContentSelection(input.currentSelection);
403
+ const initialDefaultSkills = input.defaultSelected === false
404
+ ? []
405
+ : skills.map(({ name }) => name);
406
+ const defaults = new Set(
407
+ currentSelection?.skills || initialDefaultSkills,
408
+ );
409
+ const labels = skillSelectionLabels(input, inspection);
410
+ printMarketplaceSkillHeader(context, labels);
411
+ let selected;
412
+ const escAbort = createEscAbortController();
413
+ try {
414
+ selected = await context.prompts.checkbox({
415
+ message: labels.message,
416
+ choices: buildMarketplaceSkillChoices(skills, defaults),
417
+ required: false,
418
+ loop: false,
419
+ pageSize: Math.min(skills.length, 12),
420
+ shortcuts: { all: null, invert: null },
421
+ theme: marketplaceSkillCheckboxTheme(),
422
+ }, escAbort.signal ? { signal: escAbort.signal } : undefined);
423
+ } catch (err) {
424
+ if (err && err.name === "AbortPromptError" && escAbort.isEscAbort()) {
425
+ context.output.log(" · 已取消 Skill 选择");
426
+ return { ok: false, selection: null };
427
+ }
428
+ if (err && err.name === "ExitPromptError") {
429
+ throw new Error("已取消 Skill 管理");
430
+ }
431
+ throw err;
432
+ } finally {
433
+ escAbort.dispose();
434
+ }
435
+ if (selected.length === 0) {
436
+ if (input.emptySelectionAction === "return") {
437
+ return { ok: true, selection: null, emptySelection: true };
438
+ }
439
+ context.output.log(` · ${labels.empty}`);
440
+ return { ok: false, selection: null, emptySelection: true };
441
+ }
442
+ return {
443
+ ok: true,
444
+ selection: contentSelectionFromSkillNames(selected, {
445
+ code: PLUGIN_RUNTIME_ERROR_CODES.USAGE_ERROR,
446
+ path: input.pluginId,
447
+ }),
448
+ };
449
+ }
450
+
143
451
  /**
144
452
  * 把用户可识别的仓库或项目名转换成内部来源标识。
145
453
  *
@@ -326,7 +634,7 @@ async function promptGitLabSource(prompts, current, existingIds, sourceStore) {
326
634
  values.id = uniqueSourceId(sourceIdBaseName(values.project), existingIds);
327
635
  values.name = defaultSourceName(values.id);
328
636
  values.ref = "main";
329
- values.marketplacePath = ".flower-marketplace/marketplace.json";
637
+ values.marketplacePath = ".flower-plugin/marketplace.json";
330
638
  values.applicationId = defaults?.oauth?.applicationId || await prompts.input({
331
639
  message: "OAuth 应用 ID(Application ID)",
332
640
  required: true,
@@ -351,7 +659,7 @@ async function promptGitLabSource(prompts, current, existingIds, sourceStore) {
351
659
  values.ref = await prompts.input({ message: "索引 ref(branch/tag/commit)", default: current?.ref || "main", required: true });
352
660
  values.marketplacePath = await prompts.input({
353
661
  message: "Marketplace 文件路径",
354
- default: current?.marketplacePath || ".flower-marketplace/marketplace.json",
662
+ default: current?.marketplacePath || ".flower-plugin/marketplace.json",
355
663
  required: true,
356
664
  });
357
665
  values.applicationId = await prompts.input({
@@ -538,6 +846,7 @@ function recordIssue(state, title, error) {
538
846
  function invalidateDiscovery(state) {
539
847
  state.discovery = null;
540
848
  state.authStatuses.clear();
849
+ state.skillInspections?.clear();
541
850
  }
542
851
 
543
852
  /**
@@ -547,7 +856,9 @@ function invalidateDiscovery(state) {
547
856
  * @returns {object[]} 内置 entry
548
857
  */
549
858
  function buildBuiltinDiscoverEntries(context) {
550
- if (!fs.existsSync(path.join(context.ctx.target, ".trellis"))) return [];
859
+ if (!fs.existsSync(path.join(context.ctx.target, ".trellis"))) {
860
+ return [{ kind: "builtin", id: SKILL_GARDEN_PLUGIN_ID, version: flowerVersion() }];
861
+ }
551
862
  try {
552
863
  const candidate = context.skillGardenProvider.listCandidates(SKILL_GARDEN_PLUGIN_ID)[0];
553
864
  if (!candidate) return [];
@@ -632,7 +943,7 @@ async function loadDiscoverEntries(context, statuses) {
632
943
  });
633
944
  }
634
945
  } catch (error) {
635
- if (error?.code === PLUGIN_RUNTIME_ERROR_CODES.AUTH_REQUIRED) {
946
+ if ([PLUGIN_RUNTIME_ERROR_CODES.AUTH_REQUIRED, PLUGIN_RUNTIME_ERROR_CODES.AUTH_SCOPE_INVALID].includes(error?.code)) {
636
947
  entries.push({ kind: "auth", source, invalid: true });
637
948
  } else {
638
949
  recordIssue(context.state, `${source.name} Marketplace 加载失败`, error);
@@ -648,7 +959,7 @@ async function loadDiscoverEntries(context, statuses) {
648
959
  * 计算一个 Marketplace Plugin 相对当前项目的安装状态。
649
960
  *
650
961
  * @param {object} plugin Marketplace Plugin 记录
651
- * @param {{declared:Map<string,object>,locked:Map<string,object>}} project 项目视图索引
962
+ * @param {{declared:Map<string,object>,locked:Map<string,object>,applied?:Map<string,object>}} project 项目视图索引
652
963
  * @returns {{installed:boolean,current:string|null,latest:string,outdated:boolean}} 安装状态
653
964
  */
654
965
  function installedStatus(plugin, project) {
@@ -666,7 +977,7 @@ function installedStatus(plugin, project) {
666
977
  * 把发现页 entry 渲染成当前项目视角下的条目。
667
978
  *
668
979
  * @param {object[]} entries 发现页 entry
669
- * @param {{declared:Map<string,object>,locked:Map<string,object>}} project 项目视图索引
980
+ * @param {{declared:Map<string,object>,locked:Map<string,object>,applied?:Map<string,object>}} project 项目视图索引
670
981
  * @param {Map<string,object>} actions 动作索引
671
982
  * @returns {object[]} 发现页条目
672
983
  */
@@ -705,19 +1016,37 @@ function renderDiscoverItems(entries, project, actions) {
705
1016
  const { plugin, source } = entry;
706
1017
  const status = installedStatus(plugin, project);
707
1018
  const key = `plugin:${source.id}:${plugin.id}`;
1019
+ const declaration = project.declared.get(plugin.id);
1020
+ const lock = project.locked.get(plugin.id);
1021
+ const state = project.applied?.get(plugin.id);
1022
+ const isRdGuide = isRdGuideMarketplacePlugin(plugin.id, source.id);
1023
+ const selection = firstContentSelection(declaration, state, lock);
1024
+ const meta = isRdGuide
1025
+ ? rdGuideSkillMeta(source.name, selection)
1026
+ : status.outdated
1027
+ ? `${source.name} · ${status.current} → ${status.latest}`
1028
+ : `${source.name} · ${status.current || status.latest}`;
1029
+ const badge = isRdGuide
1030
+ ? status.installed ? "已安装" : source.id
1031
+ : status.outdated ? "可更新" : status.installed ? "已安装" : source.id;
1032
+ const tone = isRdGuide
1033
+ ? status.installed ? "success" : "info"
1034
+ : status.outdated ? "warning" : status.installed ? "success" : "info";
708
1035
  actions.set(key, status.installed
709
- ? { type: "installed", pluginId: plugin.id }
1036
+ ? { type: isRdGuide && selection ? "marketplace-skills" : "installed", pluginId: plugin.id }
710
1037
  : { type: "plugin", plugin });
711
1038
  items.push({
712
- title: plugin.id,
713
- meta: status.outdated
714
- ? `${source.name} · ${status.current} → ${status.latest}`
715
- : `${source.name} · ${status.current || status.latest}`,
716
- description: status.installed
717
- ? `${plugin.description || "暂无描述"}(已安装,按 Enter 校验、更新或卸载)`
718
- : plugin.description || "暂无描述",
719
- badge: status.outdated ? "可更新" : status.installed ? "已安装" : source.id,
720
- tone: status.outdated ? "warning" : status.installed ? "success" : "info",
1039
+ title: isRdGuide ? "RD Guide 技能" : plugin.id,
1040
+ meta,
1041
+ description: isRdGuide
1042
+ ? status.installed
1043
+ ? "已安装,按 Enter 管理当前启用的 RD Guide 技能。"
1044
+ : "按 Enter 选择要启用的 RD Guide 技能。"
1045
+ : status.installed
1046
+ ? `${plugin.description || "暂无描述"}(已安装,按 Enter 校验、更新或卸载)`
1047
+ : plugin.description || "暂无描述",
1048
+ badge,
1049
+ tone,
721
1050
  value: key,
722
1051
  });
723
1052
  continue;
@@ -782,18 +1111,26 @@ async function buildManagerModel(context) {
782
1111
  const applied = new Map((view.state?.plugins || []).map((plugin) => [plugin.id, plugin]));
783
1112
  const declared = new Map(view.plugins.plugins.map((plugin) => [plugin.id, plugin]));
784
1113
  const discoverEntries = await loadDiscoverEntries(context, statuses);
785
- const discover = renderDiscoverItems(discoverEntries, { declared, locked }, actions);
1114
+ const discover = renderDiscoverItems(discoverEntries, { declared, locked, applied }, actions);
1115
+ const sourceNames = new Map(sources.map(({ id, name }) => [id, name]));
786
1116
  const installed = view.plugins.plugins.map((declaration) => {
787
1117
  const lock = locked.get(declaration.id);
788
1118
  const state = applied.get(declaration.id);
789
1119
  const version = lock?.version || declaration.version;
790
1120
  const platforms = state?.platforms?.join(", ") || "未应用";
791
1121
  const key = `installed:${declaration.id}`;
792
- actions.set(key, { type: "installed", pluginId: declaration.id });
1122
+ const isRdGuide = isRdGuideMarketplacePlugin(declaration.id, declaration.source);
1123
+ const selection = firstContentSelection(declaration, state, lock);
1124
+ const directSkillManager = isRdGuide && Boolean(selection);
1125
+ actions.set(key, { type: directSkillManager ? "marketplace-skills" : "installed", pluginId: declaration.id });
793
1126
  return {
794
- title: declaration.id,
795
- meta: `${version} · ${platforms}`,
796
- description: `来源 ${declaration.source},按 Enter 校验、更新或卸载。`,
1127
+ title: directSkillManager ? "RD Guide 技能" : declaration.id,
1128
+ meta: directSkillManager
1129
+ ? rdGuideSkillMeta(sourceNames.get(declaration.source) || declaration.source, selection)
1130
+ : `${version} · ${platforms}`,
1131
+ description: directSkillManager
1132
+ ? "按 Enter 管理当前启用的 RD Guide 技能。"
1133
+ : `来源 ${declaration.source},按 Enter 校验、更新或卸载。`,
797
1134
  badge: state ? "已应用" : "未应用",
798
1135
  tone: state ? "success" : "warning",
799
1136
  value: key,
@@ -904,27 +1241,13 @@ async function buildManagerModel(context) {
904
1241
  }
905
1242
 
906
1243
  /**
907
- * 展示远程 Plugin 详情并执行安装。
1244
+ * 执行 Marketplace Plugin 安装计划。
908
1245
  *
909
1246
  * @param {object} context 交互上下文
910
1247
  * @param {object} plugin Plugin 搜索结果
911
1248
  * @returns {Promise<void>} 完成信号
912
1249
  */
913
- async function installPlugin(context, plugin) {
914
- context.output.log("");
915
- context.output.log(chalk.hex(TITLE_COLOR).bold(plugin.id));
916
- context.output.log(` ${plugin.description || "暂无描述"}`);
917
- context.output.log(chalk.gray(` 来源 ${plugin.source} · 版本 ${plugin.versions.join(", ")}`));
918
- const action = await context.prompts.select({
919
- message: "Plugin 详情",
920
- choices: [
921
- { name: "安装到当前项目", value: "install" },
922
- { name: "返回发现", value: "back" },
923
- ],
924
- loop: false,
925
- });
926
- if (action === "back") return;
927
-
1250
+ async function installSelectedPlugin(context, plugin) {
928
1251
  const versions = [...plugin.versions].sort(semver.rcompare);
929
1252
  // Marketplace 只发一个版本时没有可选项,多问一步只是噪音。
930
1253
  const version = versions.length === 1
@@ -937,6 +1260,13 @@ async function installPlugin(context, plugin) {
937
1260
  // 声明写兼容范围而不是精确版本,否则 Marketplace 一发新版,解析器就再也筛不到候选。
938
1261
  const range = `^${version}`;
939
1262
  const args = ["add", plugin.id, "--version", range];
1263
+ const skillSelection = await promptMarketplaceSkillSelection(context, {
1264
+ pluginId: plugin.id,
1265
+ version,
1266
+ source: plugin.source,
1267
+ });
1268
+ if (!skillSelection.ok) return;
1269
+ args.push(...contentSelectionArgs(skillSelection.selection));
940
1270
  // 平台交给服务层的「既有 state → 项目探测」链;只有项目完全没有平台证据时才问用户。
941
1271
  const current = readProjectView(context.store, context.ctx.target);
942
1272
  if (inferPlatforms(context.ctx.target, current.state).length === 0) {
@@ -969,6 +1299,110 @@ async function installPlugin(context, plugin) {
969
1299
  }
970
1300
  }
971
1301
 
1302
+ /**
1303
+ * 读取 Marketplace 返回的最新版本。
1304
+ *
1305
+ * @param {object} plugin Plugin 搜索结果
1306
+ * @returns {string|null} 最新 SemVer 版本
1307
+ */
1308
+ function latestMarketplaceVersion(plugin) {
1309
+ const versions = (plugin.versions || []).filter((value) => semver.valid(value));
1310
+ if (versions.length === 0) return null;
1311
+ return [...versions].sort(semver.rcompare)[0];
1312
+ }
1313
+
1314
+ /**
1315
+ * 直接应用 rd-guide Skill 选择。
1316
+ *
1317
+ * rd-guide 在 TUI 中按来源级 Skill 管理呈现;平台缺失时沿用普通安装页的默认平台推断,
1318
+ * 但不把底层平台选择、dry-run 预览和确认流暴露给用户。
1319
+ *
1320
+ * @param {object} context 交互上下文
1321
+ * @param {object} plugin Plugin 搜索结果
1322
+ * @returns {Promise<void>} 完成信号
1323
+ */
1324
+ async function installRdGuideSkills(context, plugin) {
1325
+ const version = latestMarketplaceVersion(plugin);
1326
+ if (!version) {
1327
+ recordIssue(context.state, `${plugin.id} 安装失败`, new Error("Marketplace 没有可安装版本"));
1328
+ context.state.lastFailure = `${plugin.id} 安装失败`;
1329
+ return;
1330
+ }
1331
+ const skillSelection = await promptMarketplaceSkillSelection(context, {
1332
+ pluginId: plugin.id,
1333
+ version,
1334
+ source: plugin.source,
1335
+ defaultSelected: false,
1336
+ });
1337
+ if (!skillSelection.ok || !skillSelection.selection) return;
1338
+ let args = [
1339
+ "add",
1340
+ plugin.id,
1341
+ "--version",
1342
+ `^${version}`,
1343
+ ...contentSelectionArgs(skillSelection.selection),
1344
+ ];
1345
+ const current = readProjectView(context.store, context.ctx.target);
1346
+ if (inferPlatforms(context.ctx.target, current.state).length === 0) {
1347
+ args = withPlatforms(args, defaultPlatforms(context.ctx.target, current.state));
1348
+ }
1349
+ context.output.log("\n正在应用 RD Guide 技能选择...");
1350
+ if (await runChecked(context, args, `${plugin.id} 安装失败`) === 0) {
1351
+ context.output.log(" · RD Guide 技能已应用");
1352
+ context.state.activeTab = "installed";
1353
+ }
1354
+ }
1355
+
1356
+ /**
1357
+ * 停用全部 RD Guide 技能。
1358
+ *
1359
+ * 当前 contentSelection.skills 不允许为空;当用户在已安装的 rd-guide 技能管理中取消全部勾选时,
1360
+ * 最接近内置 Skill 管理语义的项目状态是移除这个 rd-guide Plugin 声明与投影文件。
1361
+ *
1362
+ * @param {object} context 交互上下文
1363
+ * @param {string} pluginId canonical Plugin ID
1364
+ * @returns {Promise<void>} 完成信号
1365
+ */
1366
+ async function removeRdGuideSkills(context, pluginId) {
1367
+ context.output.log("\n正在停用全部 RD Guide 技能...");
1368
+ if (await runChecked(context, ["remove", pluginId], `${pluginId} 停用失败`) === 0) {
1369
+ context.output.log(" · RD Guide 技能已全部停用");
1370
+ context.state.activeTab = "discover";
1371
+ }
1372
+ }
1373
+
1374
+ /**
1375
+ * 展示远程 Plugin 详情并执行安装。
1376
+ *
1377
+ * rd-guide 是来源级 Skill 管理体验,用户选中后直接进入技能列表;普通 Marketplace
1378
+ * Plugin 仍保留详情页,避免把已有插件安装流程全部改成 Skill 管理模型。
1379
+ *
1380
+ * @param {object} context 交互上下文
1381
+ * @param {object} plugin Plugin 搜索结果
1382
+ * @returns {Promise<void>} 完成信号
1383
+ */
1384
+ async function installPlugin(context, plugin) {
1385
+ if (isRdGuideMarketplacePlugin(plugin.id, plugin.source)) {
1386
+ await installRdGuideSkills(context, plugin);
1387
+ return;
1388
+ }
1389
+
1390
+ context.output.log("");
1391
+ context.output.log(chalk.hex(TITLE_COLOR).bold(plugin.id));
1392
+ context.output.log(` ${plugin.description || "暂无描述"}`);
1393
+ context.output.log(chalk.gray(` 来源 ${plugin.source} · 版本 ${plugin.versions.join(", ")}`));
1394
+ const action = await context.prompts.select({
1395
+ message: "Plugin 详情",
1396
+ choices: [
1397
+ { name: "安装到当前项目", value: "install" },
1398
+ { name: "返回发现", value: "back" },
1399
+ ],
1400
+ loop: false,
1401
+ });
1402
+ if (action === "back") return;
1403
+ await installSelectedPlugin(context, plugin);
1404
+ }
1405
+
972
1406
  /**
973
1407
  * 计算一个已安装 Plugin 的更新版本约束。
974
1408
  *
@@ -1032,6 +1466,65 @@ function printWidenPlan(context, plan) {
1032
1466
  }
1033
1467
  }
1034
1468
 
1469
+ /**
1470
+ * 管理已安装 Marketplace Plugin 的 Skill 选择。
1471
+ *
1472
+ * @param {object} context 交互上下文
1473
+ * @param {string} pluginId canonical Plugin ID
1474
+ * @returns {Promise<void>} 完成信号
1475
+ */
1476
+ async function managePluginSkillSelection(context, pluginId) {
1477
+ const view = readProjectView(context.store, context.ctx.target);
1478
+ const declaration = view.plugins.plugins.find(({ id }) => id === pluginId) || null;
1479
+ const lockedPlugin = (view.lock?.plugins || []).find(({ id }) => id === pluginId) || null;
1480
+ const applied = (view.state?.plugins || []).find(({ id }) => id === pluginId) || null;
1481
+ const currentSelection = declaration?.contentSelection || applied?.contentSelection || lockedPlugin?.contentSelection || null;
1482
+ if (!declaration) {
1483
+ recordIssue(context.state, `${pluginId} Skill 选择失败`, new Error(`项目未声明 Plugin:${pluginId}`));
1484
+ context.state.lastFailure = `${pluginId} Skill 选择失败`;
1485
+ return;
1486
+ }
1487
+ const rdGuide = isRdGuideMarketplacePlugin(pluginId, declaration.source);
1488
+ const version = lockedPlugin?.version || marketplaceVersions(context.state, pluginId)[0] || null;
1489
+ const skillSelection = await promptMarketplaceSkillSelection(context, {
1490
+ pluginId,
1491
+ version,
1492
+ source: declaration.source,
1493
+ currentSelection,
1494
+ emptySelectionAction: rdGuide ? "return" : "cancel",
1495
+ ...(lockedPlugin ? { lockedPlugin } : {}),
1496
+ });
1497
+ if (!skillSelection.ok) return;
1498
+ if (skillSelection.emptySelection) {
1499
+ if (rdGuide) await removeRdGuideSkills(context, pluginId);
1500
+ return;
1501
+ }
1502
+ if (!skillSelection.selection) return;
1503
+ if (contentSelectionsEqual(currentSelection, skillSelection.selection)) {
1504
+ context.output.log(" · Skill 选择没有修改");
1505
+ return;
1506
+ }
1507
+ const args = ["update", pluginId, ...contentSelectionArgs(skillSelection.selection)];
1508
+ if (rdGuide) {
1509
+ context.output.log("\n正在应用 RD Guide 技能选择...");
1510
+ if (await runChecked(context, args, `${pluginId} Skill 选择更新失败`) === 0) {
1511
+ context.output.log(" · RD Guide 技能选择已应用");
1512
+ }
1513
+ return;
1514
+ }
1515
+ context.output.log("\n更新 Skill 选择预览:");
1516
+ if (await runChecked(context, [...args, "--dry-run"], `${pluginId} Skill 选择预览失败`) !== 0) return;
1517
+ const confirmed = await context.prompts.confirm({
1518
+ message: `应用 ${pluginId} 的 Skill 选择?`,
1519
+ default: true,
1520
+ });
1521
+ if (!confirmed) {
1522
+ context.output.log(" · 已取消 Skill 选择更新");
1523
+ return;
1524
+ }
1525
+ await runChecked(context, args, `${pluginId} Skill 选择更新失败`);
1526
+ }
1527
+
1035
1528
  /**
1036
1529
  * 管理一个已安装 Plugin。
1037
1530
  *
@@ -1040,10 +1533,14 @@ function printWidenPlan(context, plan) {
1040
1533
  * @returns {Promise<void>} 完成信号
1041
1534
  */
1042
1535
  async function manageInstalledPlugin(context, pluginId) {
1536
+ const view = readProjectView(context.store, context.ctx.target);
1537
+ const declaration = view.plugins.plugins.find(({ id }) => id === pluginId) || null;
1538
+ const canManageSkills = supportsMarketplaceSkillSelection(pluginId, declaration);
1043
1539
  const action = await context.prompts.select({
1044
1540
  message: `管理 ${pluginId}`,
1045
1541
  choices: [
1046
1542
  { name: "校验安装状态", value: "verify" },
1543
+ ...(canManageSkills ? [{ name: "管理 Skill 选择", value: "skills" }] : []),
1047
1544
  { name: "检查并更新", value: "update" },
1048
1545
  { name: "卸载", value: "remove" },
1049
1546
  { name: "返回已安装", value: "back" },
@@ -1059,6 +1556,10 @@ async function manageInstalledPlugin(context, pluginId) {
1059
1556
  await runUpdate(context, collectWidenPlan(context), pluginId);
1060
1557
  return;
1061
1558
  }
1559
+ if (action === "skills") {
1560
+ await managePluginSkillSelection(context, pluginId);
1561
+ return;
1562
+ }
1062
1563
  context.output.log("\n卸载预览:");
1063
1564
  if (await runChecked(context, ["remove", pluginId, "--dry-run"], `${pluginId} 卸载预览失败`) !== 0) return;
1064
1565
  const confirmed = await context.prompts.confirm({
@@ -1416,6 +1917,8 @@ async function handleAction(context, actionKey, actions) {
1416
1917
  await installPlugin(context, action.plugin);
1417
1918
  } else if (action.type === "skill-manager") {
1418
1919
  await context.openSkillManager();
1920
+ } else if (action.type === "marketplace-skills") {
1921
+ await managePluginSkillSelection(context, action.pluginId);
1419
1922
  } else if (action.type === "installed") {
1420
1923
  await manageInstalledPlugin(context, action.pluginId);
1421
1924
  } else if (action.type === "source") {
@@ -1436,7 +1939,7 @@ async function handleAction(context, actionKey, actions) {
1436
1939
  * 运行交互式 Flower Plugin 管理器。
1437
1940
  *
1438
1941
  * @param {object} ctx cli.js 的解析上下文
1439
- * @param {{prompts?:object,output?:{log:(message:string)=>void,error?:(message:string)=>void},store?:ProjectStore,sourceStore?:UserSourceStore,credentialBundle?:object,credentialStoreOptions?:object,remoteRuntime?:object,skillGardenProvider?:SkillGardenBuiltinProvider,openSkillManager?:()=>Promise<void>,runCommand:(args:string[],commandOptions?:object)=>Promise<number>|number,searchPlugins?:(query:string,sourceId:string)=>Promise<object[]>,authStatus?:(sourceId:string)=>Promise<object>,inspectGitHubSource?:(source:object)=>Promise<object>,confirmApproval?:Function,promptAuthoring?:Function}} options 交互依赖
1942
+ * @param {{prompts?:object,output?:{log:(message:string)=>void,error?:(message:string)=>void},store?:ProjectStore,sourceStore?:UserSourceStore,credentialBundle?:object,credentialStoreOptions?:object,remoteRuntime?:object,skillGardenProvider?:SkillGardenBuiltinProvider,openSkillManager?:()=>Promise<void>,runCommand:(args:string[],commandOptions?:object)=>Promise<number>|number,searchPlugins?:(query:string,sourceId:string)=>Promise<object[]>,authStatus?:(sourceId:string)=>Promise<object>,inspectGitHubSource?:(source:object)=>Promise<object>,inspectPluginContentSkills?:(request:object)=>Promise<object>,confirmApproval?:Function,promptAuthoring?:Function}} options 交互依赖
1440
1943
  * @returns {Promise<number>} 退出码
1441
1944
  */
1442
1945
  export async function runPluginInteractive(ctx, options) {
@@ -1494,6 +1997,9 @@ export async function runPluginInteractive(ctx, options) {
1494
1997
  ctx,
1495
1998
  commandOptions,
1496
1999
  ));
2000
+ const inspectPluginContentSkills = options.inspectPluginContentSkills || ((request) => (
2001
+ remoteRuntime.inspectPluginContentSkills(request, ctx, commandOptions)
2002
+ ));
1497
2003
  const openSkillManager = options.openSkillManager || (async () => {
1498
2004
  const { skill } = await import("./skill.js");
1499
2005
  await skill({ ...ctx, passthrough: [] });
@@ -1504,6 +2010,7 @@ export async function runPluginInteractive(ctx, options) {
1504
2010
  selectedByTab: Object.fromEntries(TAB_IDS.map((id) => [id, null])),
1505
2011
  discovery: null,
1506
2012
  authStatuses: new Map(),
2013
+ skillInspections: new Map(),
1507
2014
  issues: [],
1508
2015
  lastFailure: null,
1509
2016
  exitRequested: false,
@@ -1518,6 +2025,7 @@ export async function runPluginInteractive(ctx, options) {
1518
2025
  searchPlugins,
1519
2026
  authStatus,
1520
2027
  inspectGitHubSource,
2028
+ inspectPluginContentSkills,
1521
2029
  openSkillManager,
1522
2030
  runCommand: options.runCommand,
1523
2031
  commandOptions,