flower-trellis 0.6.2 → 0.6.4
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/enhancements/MANIFEST.json +1 -1
- package/package.json +3 -3
- package/src/builtin-marketplaces/rd-guide.json +6 -3
- package/src/builtin-plugins/flower-plugin-author/plugin.json +6 -1
- package/src/builtin-plugins/flower-plugin-author/skills/flower-plugin-author/references/ci-and-review.md +2 -2
- package/src/commands/plugin-interactive.js +546 -38
- package/src/commands/plugin-remote.js +223 -26
- package/src/commands/plugin.js +46 -6
- package/src/commands/update.js +10 -2
- package/src/lib/skill-catalog.js +20 -1
- package/src/plugin/application-service.js +245 -37
- package/src/plugin/auth/credential-store.js +25 -3
- package/src/plugin/auth/gitlab-credential-resolver.js +234 -0
- package/src/plugin/auth/gitlab-oauth.js +26 -5
- package/src/plugin/authoring/scaffold.js +6 -1
- package/src/plugin/authoring/templates/rd-guide/CODEOWNERS +1 -1
- package/src/plugin/authoring/templates/rd-guide/gitlab-ci.yml +2 -2
- package/src/plugin/authoring/templates/rd-guide/verify-integration-review.mjs +1 -1
- package/src/plugin/content-selection.js +189 -0
- package/src/plugin/contracts.js +21 -2
- package/src/plugin/formats/adapters.js +27 -3
- package/src/plugin/formats/normalized-package.js +126 -14
- package/src/plugin/gitlab/rest-client.js +21 -0
- package/src/plugin/install/content-projector.js +19 -7
- package/src/plugin/resolver/dependency-resolver.js +64 -7
- package/src/plugin/runtime-errors.js +1 -0
- package/src/plugin/schemas/marketplace-manifest.js +4 -1
- package/src/plugin/schemas/plugin-manifest.js +55 -3
- package/src/plugin/schemas/project-files.js +18 -0
- package/src/plugin/schemas/shared.js +10 -0
- package/src/plugin/sources/github-provider.js +1 -1
- package/src/plugin/sources/gitlab-provider.js +377 -51
- package/src/plugin/sources/user-source-store.js +17 -1
|
@@ -7,13 +7,17 @@ import {
|
|
|
7
7
|
PluginRuntimeError,
|
|
8
8
|
} from "../plugin/runtime-errors.js";
|
|
9
9
|
import { parseCanonicalPluginId } from "../plugin/schemas/shared.js";
|
|
10
|
+
import { GITLAB_OAUTH_REQUEST_SCOPES } from "../plugin/auth/credential-store.js";
|
|
11
|
+
import { GitLabCredentialResolver } from "../plugin/auth/gitlab-credential-resolver.js";
|
|
10
12
|
import { createCredentialStore } from "../plugin/auth/keyring-credential-store.js";
|
|
11
13
|
import { GitLabCredentialManager, GitLabOAuthClient } from "../plugin/auth/gitlab-oauth.js";
|
|
12
14
|
import { GitLabRestClient } from "../plugin/gitlab/rest-client.js";
|
|
13
15
|
import { GitHubRestClient } from "../plugin/github/rest-client.js";
|
|
14
16
|
import { GitLabSourceProvider } from "../plugin/sources/gitlab-provider.js";
|
|
15
17
|
import { GitHubSourceProvider } from "../plugin/sources/github-provider.js";
|
|
18
|
+
import { SourceRegistry } from "../plugin/sources/source-registry.js";
|
|
16
19
|
import { UserSourceStore } from "../plugin/sources/user-source-store.js";
|
|
20
|
+
import { listContentSkillChoices } from "../plugin/content-selection.js";
|
|
17
21
|
import { compareUtf8 } from "../plugin/stable-order.js";
|
|
18
22
|
|
|
19
23
|
/**
|
|
@@ -53,8 +57,9 @@ function printManagementResult(result, json, output) {
|
|
|
53
57
|
return;
|
|
54
58
|
}
|
|
55
59
|
if (result.subcommand === "status") {
|
|
60
|
+
const scopeSuffix = result.scopes?.length ? `,scope=${result.scopes.join(",")}` : "";
|
|
56
61
|
output.log(result.authorized
|
|
57
|
-
? `GitLab 已登录:${result.sourceId}
|
|
62
|
+
? `GitLab 已登录:${result.sourceId}${scopeSuffix}`
|
|
58
63
|
: `GitLab 未登录:${result.sourceId}`);
|
|
59
64
|
} else output.log(`GitLab auth ${result.subcommand} 完成:${result.sourceId}`);
|
|
60
65
|
if (result.persistent === false) output.log("系统 Keyring 不可用,凭据仅在当前进程内有效");
|
|
@@ -134,16 +139,23 @@ export async function getPluginAuthStatus(sourceId, options = {}) {
|
|
|
134
139
|
};
|
|
135
140
|
}
|
|
136
141
|
const credentialBundle = options.credentialBundle || await createCredentialStore(options.credentialStoreOptions);
|
|
137
|
-
const
|
|
142
|
+
const resolver = options.credentialResolver || new GitLabCredentialResolver({
|
|
143
|
+
store: credentialBundle.store,
|
|
144
|
+
persistent: credentialBundle.persistent,
|
|
145
|
+
env: options.env,
|
|
146
|
+
runGlab: options.runGlab,
|
|
147
|
+
glabCommand: options.glabCommand,
|
|
148
|
+
});
|
|
149
|
+
const status = await resolver.status(source);
|
|
138
150
|
return {
|
|
139
151
|
ok: true,
|
|
140
152
|
command: "auth",
|
|
141
153
|
subcommand: "status",
|
|
142
154
|
sourceId: source.id,
|
|
143
|
-
authorized:
|
|
144
|
-
scopes:
|
|
145
|
-
expiresAt:
|
|
146
|
-
persistent:
|
|
155
|
+
authorized: status.authorized,
|
|
156
|
+
scopes: status.scopes,
|
|
157
|
+
expiresAt: status.expiresAt,
|
|
158
|
+
persistent: status.persistent,
|
|
147
159
|
};
|
|
148
160
|
}
|
|
149
161
|
|
|
@@ -167,7 +179,16 @@ export async function searchPluginMarketplaces(parsed, ctx, options = {}) {
|
|
|
167
179
|
sleep: options.sleep,
|
|
168
180
|
now: options.now,
|
|
169
181
|
});
|
|
170
|
-
manager = new GitLabCredentialManager({
|
|
182
|
+
manager = new GitLabCredentialManager({
|
|
183
|
+
store: credentialBundle.store,
|
|
184
|
+
oauth,
|
|
185
|
+
now: options.now,
|
|
186
|
+
persistent: credentialBundle.persistent,
|
|
187
|
+
credentialResolver: options.credentialResolver,
|
|
188
|
+
env: options.env,
|
|
189
|
+
runGlab: options.runGlab,
|
|
190
|
+
glabCommand: options.glabCommand,
|
|
191
|
+
});
|
|
171
192
|
}
|
|
172
193
|
const results = [];
|
|
173
194
|
const diagnostics = [];
|
|
@@ -193,6 +214,122 @@ export async function searchPluginMarketplaces(parsed, ctx, options = {}) {
|
|
|
193
214
|
};
|
|
194
215
|
}
|
|
195
216
|
|
|
217
|
+
/**
|
|
218
|
+
* 为 TUI 按需读取 Marketplace Plugin 的 manifest Skill 清单。
|
|
219
|
+
*
|
|
220
|
+
* @param {{pluginId:string,version?:string,source?:string|null,lockedPlugin?:import("../plugin/contracts.js").ResolvedPlugin|null}} request 读取请求
|
|
221
|
+
* @param {object} ctx CLI 上下文
|
|
222
|
+
* @param {object} [options] Provider、凭据和测试注入
|
|
223
|
+
* @returns {Promise<{ok:true,pluginId:string,version:string,name:string,skills:Array<{name:string,path:string,description?:string,version?:string}>}>} Skill 清单
|
|
224
|
+
*/
|
|
225
|
+
export async function inspectPluginContentSkills(request, ctx, options = {}) {
|
|
226
|
+
const canonicalId = request.pluginId || request.lockedPlugin?.id;
|
|
227
|
+
if (!canonicalId) {
|
|
228
|
+
throw new PluginRuntimeError("读取 Plugin Skill 清单需要 Plugin ID", {
|
|
229
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.USAGE_ERROR,
|
|
230
|
+
path: "pluginId",
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
const registry = new SourceRegistry(options.providers || []);
|
|
234
|
+
const lock = request.lockedPlugin
|
|
235
|
+
? { schemaVersion: 1, roots: [request.lockedPlugin.id], plugins: [request.lockedPlugin] }
|
|
236
|
+
: null;
|
|
237
|
+
const parsed = request.lockedPlugin
|
|
238
|
+
? { command: "verify", pluginId: canonicalId, source: null }
|
|
239
|
+
: { command: "add", pluginId: canonicalId, source: request.source || null };
|
|
240
|
+
|
|
241
|
+
await registerRemotePluginSources({
|
|
242
|
+
parsed,
|
|
243
|
+
projectRoot: ctx.target,
|
|
244
|
+
options,
|
|
245
|
+
registry,
|
|
246
|
+
lock,
|
|
247
|
+
});
|
|
248
|
+
const manifestInspection = await inspectRemotePluginContentManifest({
|
|
249
|
+
canonicalId,
|
|
250
|
+
registry,
|
|
251
|
+
version: request.version,
|
|
252
|
+
lockedPlugin: request.lockedPlugin || null,
|
|
253
|
+
});
|
|
254
|
+
if (manifestInspection) {
|
|
255
|
+
return {
|
|
256
|
+
ok: true,
|
|
257
|
+
pluginId: canonicalId,
|
|
258
|
+
version: manifestInspection.version,
|
|
259
|
+
name: manifestInspection.manifest.name,
|
|
260
|
+
skills: listContentSkillChoices(manifestInspection.manifest.content.skills || [], canonicalId),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
await prepareRemotePluginForSkillInspection({
|
|
264
|
+
parsed,
|
|
265
|
+
canonicalId,
|
|
266
|
+
registry,
|
|
267
|
+
lock,
|
|
268
|
+
version: request.version,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
let plugin = request.lockedPlugin || null;
|
|
272
|
+
if (!plugin) {
|
|
273
|
+
const candidates = registry.listCandidates(canonicalId);
|
|
274
|
+
plugin = candidates.find((candidate) => candidate.version === request.version) || null;
|
|
275
|
+
if (!plugin) {
|
|
276
|
+
throw new PluginRuntimeError(`Marketplace Plugin 版本不存在:${canonicalId}@${request.version}`, {
|
|
277
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.SOURCE_NOT_FOUND,
|
|
278
|
+
path: canonicalId,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const pluginPackage = registry.readPackage(plugin);
|
|
284
|
+
return {
|
|
285
|
+
ok: true,
|
|
286
|
+
pluginId: canonicalId,
|
|
287
|
+
version: plugin.version,
|
|
288
|
+
name: pluginPackage.manifest.name,
|
|
289
|
+
skills: listContentSkillChoices(pluginPackage.manifest.content.skills || [], canonicalId),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 优先用 Provider 的 manifest-only inspection 读取 Skill 清单元数据。
|
|
295
|
+
*
|
|
296
|
+
* 这里不需要运行时包内容,也不需要 canonical tree hash;完整安装/更新仍会在生命周期
|
|
297
|
+
* 中准备固定包并校验摘要。TUI 首屏只展示 manifest 声明,避免为 checkbox 下载 archive。
|
|
298
|
+
*
|
|
299
|
+
* @param {{canonicalId:string,registry:object,version?:string|null,lockedPlugin?:object|null}} context 读取上下文
|
|
300
|
+
* @returns {Promise<{version:string,manifest:import("../plugin/contracts.js").PluginManifest}|null>} manifest inspection 结果
|
|
301
|
+
*/
|
|
302
|
+
async function inspectRemotePluginContentManifest({ canonicalId, registry, version, lockedPlugin }) {
|
|
303
|
+
const { sourceId } = parseCanonicalPluginId(canonicalId);
|
|
304
|
+
if (!registry.has(sourceId)) return null;
|
|
305
|
+
const provider = registry.get(sourceId);
|
|
306
|
+
if (typeof provider.inspectContentManifest !== "function") return null;
|
|
307
|
+
return provider.inspectContentManifest(canonicalId, { version, lockedPlugin });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 为 TUI Skill inspection 准备远程包。
|
|
312
|
+
*
|
|
313
|
+
* inspection 只需要读取用户当前选择的一个包版本;Provider 支持单版本准备时不走完整依赖闭包,
|
|
314
|
+
* 避免为了展示 checkbox 下载同一 Plugin 的全部历史版本。
|
|
315
|
+
*
|
|
316
|
+
* @param {{parsed:object,canonicalId:string,registry:object,lock:object|null,version?:string|null}} context 准备上下文
|
|
317
|
+
* @returns {Promise<void>} 准备完成
|
|
318
|
+
*/
|
|
319
|
+
async function prepareRemotePluginForSkillInspection({ parsed, canonicalId, registry, lock, version }) {
|
|
320
|
+
if (lock) {
|
|
321
|
+
await prepareRemotePluginCandidates({ parsed, canonicalId, registry, lock });
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const { sourceId } = parseCanonicalPluginId(canonicalId);
|
|
325
|
+
const provider = registry.has(sourceId) ? registry.get(sourceId) : null;
|
|
326
|
+
if (version && typeof provider?.prepareVersion === "function") {
|
|
327
|
+
await provider.prepareVersion(canonicalId, version);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
await prepareRemotePluginCandidates({ parsed, canonicalId, registry, lock });
|
|
331
|
+
}
|
|
332
|
+
|
|
196
333
|
/**
|
|
197
334
|
* 探测 GitHub 来源并返回可持久化 descriptor 与兼容性结果。
|
|
198
335
|
*
|
|
@@ -309,10 +446,10 @@ export async function runPluginManagementCommand(parsed, ctx, options, output) {
|
|
|
309
446
|
baseUrl: parsed.baseUrl || base.baseUrl,
|
|
310
447
|
project: parsed.project || base.project,
|
|
311
448
|
ref: parsed.ref || base.ref || "main",
|
|
312
|
-
marketplacePath: parsed.marketplacePath || base.marketplacePath || ".flower-
|
|
449
|
+
marketplacePath: parsed.marketplacePath || base.marketplacePath || ".flower-plugin/marketplace.json",
|
|
313
450
|
oauth: {
|
|
314
451
|
applicationId: parsed.applicationId || base.oauth?.applicationId,
|
|
315
|
-
scopes:
|
|
452
|
+
scopes: GITLAB_OAUTH_REQUEST_SCOPES,
|
|
316
453
|
},
|
|
317
454
|
};
|
|
318
455
|
if (sourceType === "github") {
|
|
@@ -400,12 +537,15 @@ export async function runPluginManagementCommand(parsed, ctx, options, output) {
|
|
|
400
537
|
/**
|
|
401
538
|
* 为生命周期命令登记所需的远程 Source Provider。
|
|
402
539
|
*
|
|
403
|
-
* @param {{parsed:object,projectRoot:string,options:object,registry:object,lock:object|null}} context 远程准备上下文
|
|
540
|
+
* @param {{parsed:object,projectRoot:string,options:object,registry:object,lock:object|null,preserveIds?:string[]}} context 远程准备上下文
|
|
404
541
|
* @returns {Promise<void>} 登记完成
|
|
405
542
|
*/
|
|
406
|
-
export async function registerRemotePluginSources({ parsed, projectRoot, options, registry, lock }) {
|
|
543
|
+
export async function registerRemotePluginSources({ parsed, projectRoot, options, registry, lock, preserveIds = [] }) {
|
|
544
|
+
const preserved = new Set(preserveIds);
|
|
407
545
|
const remoteIds = new Set(["add", "update", "remove", "verify", "replay"].includes(parsed.command)
|
|
408
|
-
? (lock?.plugins || [])
|
|
546
|
+
? (lock?.plugins || [])
|
|
547
|
+
.filter(({ id, source }) => !preserved.has(id) && ["gitlab", "github"].includes(source.type))
|
|
548
|
+
.map(({ source }) => source.id)
|
|
409
549
|
: []);
|
|
410
550
|
let sourceStore = null;
|
|
411
551
|
let configuredSources = null;
|
|
@@ -447,6 +587,15 @@ export async function registerRemotePluginSources({ parsed, projectRoot, options
|
|
|
447
587
|
remoteIds.add(simpleSourceOption);
|
|
448
588
|
}
|
|
449
589
|
}
|
|
590
|
+
if (parsed.command === "update" && parsed.pluginId?.includes("/")) {
|
|
591
|
+
for (const sourceId of collectUnregisteredDependencySources(
|
|
592
|
+
registry,
|
|
593
|
+
[parsed.pluginId],
|
|
594
|
+
preserveIds,
|
|
595
|
+
)) {
|
|
596
|
+
remoteIds.add(sourceId);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
450
599
|
remoteIds.delete("local");
|
|
451
600
|
if (remoteIds.size === 0) return;
|
|
452
601
|
|
|
@@ -470,7 +619,16 @@ export async function registerRemotePluginSources({ parsed, projectRoot, options
|
|
|
470
619
|
sleep: options.sleep,
|
|
471
620
|
now: options.now,
|
|
472
621
|
});
|
|
473
|
-
manager = new GitLabCredentialManager({
|
|
622
|
+
manager = new GitLabCredentialManager({
|
|
623
|
+
store: credentialBundle.store,
|
|
624
|
+
oauth,
|
|
625
|
+
now: options.now,
|
|
626
|
+
persistent: credentialBundle.persistent,
|
|
627
|
+
credentialResolver: options.credentialResolver,
|
|
628
|
+
env: options.env,
|
|
629
|
+
runGlab: options.runGlab,
|
|
630
|
+
glabCommand: options.glabCommand,
|
|
631
|
+
});
|
|
474
632
|
}
|
|
475
633
|
// 依赖闭包可能跨 source,因此登记全部可用来源,而不是只登记入口来源。
|
|
476
634
|
for (const source of sources) {
|
|
@@ -482,25 +640,57 @@ export async function registerRemotePluginSources({ parsed, projectRoot, options
|
|
|
482
640
|
/**
|
|
483
641
|
* 准备远程 Plugin 候选及固定 lock 缓存。
|
|
484
642
|
*
|
|
485
|
-
* @param {{parsed:object,canonicalId:string|null,registry:object,lock:object|null}} context 候选准备上下文
|
|
643
|
+
* @param {{parsed:object,canonicalId:string|null,registry:object,lock:object|null,preserveIds?:string[]}} context 候选准备上下文
|
|
486
644
|
* @returns {Promise<void>} 准备完成
|
|
487
645
|
*/
|
|
488
|
-
export async function prepareRemotePluginCandidates({ parsed, canonicalId, registry, lock }) {
|
|
646
|
+
export async function prepareRemotePluginCandidates({ parsed, canonicalId, registry, lock, preserveIds = [] }) {
|
|
489
647
|
if (parsed.command === "add" && registry.has(parseCanonicalPluginId(canonicalId).sourceId)) {
|
|
490
|
-
await prepareRemoteLock(registry, lock);
|
|
491
|
-
await prepareRemoteClosure(registry, [canonicalId]);
|
|
648
|
+
await prepareRemoteLock(registry, lock, preserveIds);
|
|
649
|
+
await prepareRemoteClosure(registry, [canonicalId], preserveIds);
|
|
492
650
|
} else if (parsed.command === "update") {
|
|
493
651
|
// 更新单一 Plugin 时,解析器仍会 lock-first 重放完整图;先恢复全部远程固定包,
|
|
494
652
|
// 再只为本轮显式更新目标加载新候选,避免顺带升级其它外部 Plugin。
|
|
495
|
-
await prepareRemoteLock(registry, lock);
|
|
653
|
+
await prepareRemoteLock(registry, lock, preserveIds);
|
|
496
654
|
const updateIds = canonicalId
|
|
497
655
|
? [canonicalId]
|
|
498
656
|
: (lock?.plugins || []).filter(({ source }) => ["gitlab", "github"].includes(source.type)).map(({ id }) => id);
|
|
499
|
-
await prepareRemoteClosure(registry, updateIds);
|
|
657
|
+
await prepareRemoteClosure(registry, updateIds, preserveIds);
|
|
500
658
|
assertExternalVersionsNotReused(registry, lock, updateIds);
|
|
501
659
|
} else if (["remove", "verify", "replay"].includes(parsed.command)) {
|
|
502
|
-
await prepareRemoteLock(registry, lock);
|
|
660
|
+
await prepareRemoteLock(registry, lock, preserveIds);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* 从当前可读取的活跃候选中收集尚未登记的依赖来源。
|
|
666
|
+
*
|
|
667
|
+
* 注册阶段不能准备远程候选,但 builtin/local 候选已经可读。先沿这些候选展开依赖,才能在
|
|
668
|
+
* 旧 lock 的同来源节点全部被冻结时,仍为本轮新增的非冻结依赖登记 Provider。
|
|
669
|
+
*
|
|
670
|
+
* @param {object} registry Provider 注册表
|
|
671
|
+
* @param {string[]} initialIds 活跃入口 canonical ID
|
|
672
|
+
* @param {string[]} [preserveIds] 不读取来源的冻结 Plugin ID
|
|
673
|
+
* @returns {string[]} 尚未登记的依赖来源 ID
|
|
674
|
+
*/
|
|
675
|
+
function collectUnregisteredDependencySources(registry, initialIds, preserveIds = []) {
|
|
676
|
+
const preserved = new Set(preserveIds);
|
|
677
|
+
const pending = [...new Set(initialIds)];
|
|
678
|
+
const visited = new Set();
|
|
679
|
+
const sourceIds = new Set();
|
|
680
|
+
while (pending.length > 0) {
|
|
681
|
+
const id = pending.shift();
|
|
682
|
+
if (visited.has(id) || preserved.has(id)) continue;
|
|
683
|
+
visited.add(id);
|
|
684
|
+
const { sourceId } = parseCanonicalPluginId(id);
|
|
685
|
+
if (!registry.has(sourceId)) {
|
|
686
|
+
sourceIds.add(sourceId);
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
for (const candidate of registry.listCandidates(id)) {
|
|
690
|
+
pending.push(...Object.keys(candidate.manifest.dependencies || {}));
|
|
691
|
+
}
|
|
503
692
|
}
|
|
693
|
+
return [...sourceIds].sort(compareUtf8);
|
|
504
694
|
}
|
|
505
695
|
|
|
506
696
|
/**
|
|
@@ -508,20 +698,21 @@ export async function prepareRemotePluginCandidates({ parsed, canonicalId, regis
|
|
|
508
698
|
*
|
|
509
699
|
* @param {object} registry Provider 注册表
|
|
510
700
|
* @param {string[]} initialIds 初始 canonical ID
|
|
701
|
+
* @param {string[]} [preserveIds] 不读取来源的冻结 Plugin ID
|
|
511
702
|
* @returns {Promise<void>} 准备完成
|
|
512
703
|
*/
|
|
513
|
-
async function prepareRemoteClosure(registry, initialIds) {
|
|
704
|
+
async function prepareRemoteClosure(registry, initialIds, preserveIds = []) {
|
|
705
|
+
const preserved = new Set(preserveIds);
|
|
514
706
|
const pending = [...new Set(initialIds)];
|
|
515
707
|
const visited = new Set();
|
|
516
708
|
while (pending.length > 0) {
|
|
517
709
|
const id = pending.shift();
|
|
518
|
-
if (visited.has(id)) continue;
|
|
710
|
+
if (visited.has(id) || preserved.has(id)) continue;
|
|
519
711
|
visited.add(id);
|
|
520
712
|
const { sourceId } = parseCanonicalPluginId(id);
|
|
521
713
|
if (!registry.has(sourceId)) continue;
|
|
522
714
|
const provider = registry.get(sourceId);
|
|
523
|
-
if (typeof provider.prepare
|
|
524
|
-
await provider.prepare(id);
|
|
715
|
+
if (typeof provider.prepare === "function") await provider.prepare(id);
|
|
525
716
|
for (const candidate of provider.listCandidates(id)) {
|
|
526
717
|
pending.push(...Object.keys(candidate.manifest.dependencies || {}));
|
|
527
718
|
}
|
|
@@ -533,11 +724,17 @@ async function prepareRemoteClosure(registry, initialIds) {
|
|
|
533
724
|
*
|
|
534
725
|
* @param {object} registry Provider 注册表
|
|
535
726
|
* @param {import("../plugin/contracts.js").PluginLock|null} lock 当前 lock
|
|
727
|
+
* @param {string[]} [preserveIds] 不读取来源的冻结 Plugin ID
|
|
536
728
|
* @returns {Promise<void>} 恢复完成
|
|
537
729
|
*/
|
|
538
|
-
async function prepareRemoteLock(registry, lock) {
|
|
730
|
+
async function prepareRemoteLock(registry, lock, preserveIds = []) {
|
|
731
|
+
const preserved = new Set(preserveIds);
|
|
539
732
|
for (const plugin of lock?.plugins || []) {
|
|
540
|
-
if (
|
|
733
|
+
if (
|
|
734
|
+
preserved.has(plugin.id) ||
|
|
735
|
+
!["gitlab", "github"].includes(plugin.source.type) ||
|
|
736
|
+
!registry.has(plugin.source.id)
|
|
737
|
+
) continue;
|
|
541
738
|
const provider = registry.get(plugin.source.id);
|
|
542
739
|
if (typeof provider.prepareLocked === "function") await provider.prepareLocked(plugin);
|
|
543
740
|
else if (typeof provider.prepare === "function") await provider.prepare(plugin.id);
|
package/src/commands/plugin.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
SKILL_GARDEN_PLUGIN_ID,
|
|
18
18
|
SkillGardenBuiltinProvider,
|
|
19
19
|
} from "../builtin-plugins/skill-garden/provider.js";
|
|
20
|
+
import { contentSelectionFromSkillNames } from "../plugin/content-selection.js";
|
|
20
21
|
|
|
21
22
|
const REMOTE_PLUGIN_ENTRY = new URL("./plugin-remote.js", import.meta.url);
|
|
22
23
|
const PATCH_RUNTIME_ENTRY = new URL("../plugin/install/patch-planner.js", import.meta.url);
|
|
@@ -35,6 +36,7 @@ const PLUGIN_CONFLICT_CODES = new Set([
|
|
|
35
36
|
PLUGIN_RUNTIME_ERROR_CODES.DEPENDENCY_MISSING,
|
|
36
37
|
PLUGIN_RUNTIME_ERROR_CODES.DEPENDENCY_CONFLICT,
|
|
37
38
|
PLUGIN_RUNTIME_ERROR_CODES.DEPENDENCY_CYCLE,
|
|
39
|
+
PLUGIN_RUNTIME_ERROR_CODES.CONTENT_SELECTION_INVALID,
|
|
38
40
|
PLUGIN_RUNTIME_ERROR_CODES.CONTENT_CONFLICT,
|
|
39
41
|
PLUGIN_RUNTIME_ERROR_CODES.TARGET_DRIFT,
|
|
40
42
|
PLUGIN_RUNTIME_ERROR_CODES.VERIFY_FAILED,
|
|
@@ -227,7 +229,7 @@ function parseManagementArgs(argv) {
|
|
|
227
229
|
* 解析 Plugin 多级命令参数。
|
|
228
230
|
*
|
|
229
231
|
* @param {string[]} argv `plugin` 后的参数
|
|
230
|
-
* @returns {{command:string,pluginId:string|null,source:string|null,version:string|null,platforms:string[],dryRun:boolean,json:boolean,help:boolean}} 解析结果
|
|
232
|
+
* @returns {{command:string,pluginId:string|null,source:string|null,version:string|null,platforms:string[],contentSelection:object|null,dryRun:boolean,json:boolean,help:boolean}} 解析结果
|
|
231
233
|
*/
|
|
232
234
|
export function parsePluginArgs(argv) {
|
|
233
235
|
const authoring = parsePluginAuthoringArgs(argv);
|
|
@@ -238,6 +240,7 @@ export function parsePluginArgs(argv) {
|
|
|
238
240
|
const command = rootHelp ? "list" : (argv[0] || "list");
|
|
239
241
|
const positional = [];
|
|
240
242
|
const platforms = [];
|
|
243
|
+
const contentSkills = [];
|
|
241
244
|
const widen = [];
|
|
242
245
|
let source = null;
|
|
243
246
|
let version = null;
|
|
@@ -247,7 +250,13 @@ export function parsePluginArgs(argv) {
|
|
|
247
250
|
|
|
248
251
|
for (let index = 1; index < argv.length; index += 1) {
|
|
249
252
|
const token = argv[index];
|
|
250
|
-
if (
|
|
253
|
+
if (
|
|
254
|
+
token === "--source" ||
|
|
255
|
+
token === "--version" ||
|
|
256
|
+
token === "--platform" ||
|
|
257
|
+
token === "--widen" ||
|
|
258
|
+
token === "--content-skill"
|
|
259
|
+
) {
|
|
251
260
|
const value = argv[index + 1];
|
|
252
261
|
if (!value || value.startsWith("--")) {
|
|
253
262
|
throw new PluginRuntimeError(`${token} 缺少取值`, {
|
|
@@ -259,6 +268,7 @@ export function parsePluginArgs(argv) {
|
|
|
259
268
|
if (token === "--source") source = value;
|
|
260
269
|
else if (token === "--version") version = value;
|
|
261
270
|
else if (token === "--widen") widen.push(value);
|
|
271
|
+
else if (token === "--content-skill") contentSkills.push(value);
|
|
262
272
|
else platforms.push(value);
|
|
263
273
|
continue;
|
|
264
274
|
}
|
|
@@ -341,6 +351,24 @@ export function parsePluginArgs(argv) {
|
|
|
341
351
|
path: command,
|
|
342
352
|
});
|
|
343
353
|
}
|
|
354
|
+
const contentSelection = contentSkills.length > 0
|
|
355
|
+
? contentSelectionFromSkillNames(contentSkills, {
|
|
356
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.USAGE_ERROR,
|
|
357
|
+
path: "--content-skill",
|
|
358
|
+
})
|
|
359
|
+
: null;
|
|
360
|
+
if (contentSelection && !["add", "update"].includes(command)) {
|
|
361
|
+
throw new PluginRuntimeError("--content-skill 仅支持 plugin add 与单个 plugin update", {
|
|
362
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.USAGE_ERROR,
|
|
363
|
+
path: command,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
if (contentSelection && command === "update" && (!positional[0] || widen.length > 0)) {
|
|
367
|
+
throw new PluginRuntimeError("--content-skill 仅支持单个 plugin update,且不能与 --widen 同时使用", {
|
|
368
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.USAGE_ERROR,
|
|
369
|
+
path: "--content-skill",
|
|
370
|
+
});
|
|
371
|
+
}
|
|
344
372
|
return {
|
|
345
373
|
command,
|
|
346
374
|
pluginId: positional[0] || null,
|
|
@@ -348,6 +376,7 @@ export function parsePluginArgs(argv) {
|
|
|
348
376
|
version,
|
|
349
377
|
widen: parseWidenPairs(widen),
|
|
350
378
|
platforms: parsePlatforms(platforms),
|
|
379
|
+
contentSelection,
|
|
351
380
|
dryRun,
|
|
352
381
|
json,
|
|
353
382
|
help,
|
|
@@ -365,8 +394,8 @@ function printPluginHelp(output) {
|
|
|
365
394
|
: "";
|
|
366
395
|
output.log(`用法:
|
|
367
396
|
flower-trellis plugin list [--json]
|
|
368
|
-
flower-trellis plugin add <plugin> [--source <来源 ID|项目内路径>] [--version <range>] [--platform <id>] [--dry-run] [--json]
|
|
369
|
-
flower-trellis plugin update [plugin] [--version <range>] [--widen <plugin>=<range>]... [--platform <id>] [--dry-run] [--json]
|
|
397
|
+
flower-trellis plugin add <plugin> [--source <来源 ID|项目内路径>] [--version <range>] [--platform <id>] [--content-skill <name>]... [--dry-run] [--json]
|
|
398
|
+
flower-trellis plugin update [plugin] [--version <range>] [--widen <plugin>=<range>]... [--platform <id>] [--content-skill <name>]... [--dry-run] [--json]
|
|
370
399
|
flower-trellis plugin remove <plugin> [--dry-run] [--json]
|
|
371
400
|
flower-trellis plugin verify [plugin] [--json]
|
|
372
401
|
flower-trellis plugin init --id <source/plugin> --name <name> [--version <semver>] [--profile <standard|integration>] [--patches] [--marketplace] [--non-interactive] [--json]
|
|
@@ -718,7 +747,7 @@ function pluginExitCode(error) {
|
|
|
718
747
|
* 执行 Plugin 生命周期命令。
|
|
719
748
|
*
|
|
720
749
|
* @param {object} ctx cli-args.js 的执行上下文
|
|
721
|
-
* @param {{cwd?:string,providers?:object[],output?:{log:(message:string)=>void,error:(message:string)=>void},interactive?:boolean,prompts?:object,confirmApproval?:(requests:object[])=>Promise<boolean>|boolean,compact?:boolean,onPreflight?:(result:object)=>void,trellisControlMode?:"materialized"|"restoring"}} [options] 测试、Provider、输出与交互确认注入
|
|
750
|
+
* @param {{cwd?:string,providers?:object[],output?:{log:(message:string)=>void,error:(message:string)=>void},interactive?:boolean,prompts?:object,confirmApproval?:(requests:object[])=>Promise<boolean>|boolean,compact?:boolean,preserveIds?:string[],onPreflight?:(result:object)=>void,trellisControlMode?:"materialized"|"restoring"}} [options] 测试、Provider、输出与交互确认注入
|
|
722
751
|
* @returns {Promise<number>} 进程退出码
|
|
723
752
|
*/
|
|
724
753
|
export async function plugin(ctx, options = {}) {
|
|
@@ -805,6 +834,7 @@ export async function plugin(ctx, options = {}) {
|
|
|
805
834
|
options,
|
|
806
835
|
registry,
|
|
807
836
|
lock,
|
|
837
|
+
preserveIds: options.preserveIds || [],
|
|
808
838
|
});
|
|
809
839
|
|
|
810
840
|
if (parsed.command !== "add" && canonicalId && !canonicalId.includes("/")) {
|
|
@@ -860,7 +890,13 @@ export async function plugin(ctx, options = {}) {
|
|
|
860
890
|
}));
|
|
861
891
|
}
|
|
862
892
|
|
|
863
|
-
await remoteRuntime?.prepareRemotePluginCandidates({
|
|
893
|
+
await remoteRuntime?.prepareRemotePluginCandidates({
|
|
894
|
+
parsed,
|
|
895
|
+
canonicalId,
|
|
896
|
+
registry,
|
|
897
|
+
lock,
|
|
898
|
+
preserveIds: options.preserveIds || [],
|
|
899
|
+
});
|
|
864
900
|
|
|
865
901
|
const service = new PluginApplicationService(ctx.target, { registry, store });
|
|
866
902
|
let result;
|
|
@@ -875,7 +911,9 @@ export async function plugin(ctx, options = {}) {
|
|
|
875
911
|
: "*"
|
|
876
912
|
),
|
|
877
913
|
platforms: parsed.platforms,
|
|
914
|
+
...(parsed.contentSelection ? { contentSelection: parsed.contentSelection } : {}),
|
|
878
915
|
dryRun: parsed.dryRun,
|
|
916
|
+
preserveIds: options.preserveIds || [],
|
|
879
917
|
onPreflight: options.onPreflight,
|
|
880
918
|
};
|
|
881
919
|
result = parsed.dryRun ? service.add(addOptions) : await executeWithCapabilityApproval(
|
|
@@ -893,7 +931,9 @@ export async function plugin(ctx, options = {}) {
|
|
|
893
931
|
: parsed.version ? { version: parsed.version } : {}),
|
|
894
932
|
...(Object.keys(parsed.widen || {}).length > 0 ? { widen: parsed.widen } : {}),
|
|
895
933
|
platforms: parsed.platforms,
|
|
934
|
+
...(parsed.contentSelection ? { contentSelection: parsed.contentSelection } : {}),
|
|
896
935
|
dryRun: parsed.dryRun,
|
|
936
|
+
preserveIds: options.preserveIds || [],
|
|
897
937
|
onPreflight: options.onPreflight,
|
|
898
938
|
};
|
|
899
939
|
result = parsed.dryRun ? service.update(updateOptions) : await executeWithCapabilityApproval(
|
package/src/commands/update.js
CHANGED
|
@@ -89,6 +89,13 @@ function pluginPlatformArgs(platforms) {
|
|
|
89
89
|
*/
|
|
90
90
|
export async function replayPlugins(ctx, target, dryRun, compensationSnapshot = null) {
|
|
91
91
|
const output = ctx.trellisControlQuiet ? SILENT_OUTPUT : console;
|
|
92
|
+
const store = new ProjectStore(target);
|
|
93
|
+
const lock = store.readLock();
|
|
94
|
+
const preserveIds = (lock?.plugins || [])
|
|
95
|
+
.filter(({ id, source }) => (
|
|
96
|
+
id !== SKILL_GARDEN_PLUGIN_ID && ["gitlab", "github"].includes(source.type)
|
|
97
|
+
))
|
|
98
|
+
.map(({ id }) => id);
|
|
92
99
|
const onPreflight = compensationSnapshot || ctx.trellisControlExtendSnapshot
|
|
93
100
|
? ({ plan }) => {
|
|
94
101
|
const targets = [
|
|
@@ -100,7 +107,7 @@ export async function replayPlugins(ctx, target, dryRun, compensationSnapshot =
|
|
|
100
107
|
}
|
|
101
108
|
: undefined;
|
|
102
109
|
if (ctx.enhance) {
|
|
103
|
-
const declared =
|
|
110
|
+
const declared = store.readPlugins().plugins
|
|
104
111
|
.some(({ id }) => id === SKILL_GARDEN_PLUGIN_ID);
|
|
105
112
|
const code = await plugin({
|
|
106
113
|
...ctx,
|
|
@@ -113,6 +120,7 @@ export async function replayPlugins(ctx, target, dryRun, compensationSnapshot =
|
|
|
113
120
|
],
|
|
114
121
|
}, {
|
|
115
122
|
skillGarden: { variant: ctx.variant, skills: ctx.skills },
|
|
123
|
+
preserveIds,
|
|
116
124
|
compact: true,
|
|
117
125
|
onPreflight,
|
|
118
126
|
output,
|
|
@@ -122,7 +130,7 @@ export async function replayPlugins(ctx, target, dryRun, compensationSnapshot =
|
|
|
122
130
|
}
|
|
123
131
|
|
|
124
132
|
output.log("· --no-enhance:跳过 Skill-Garden,仅重放其它已声明 Plugin");
|
|
125
|
-
const preserveSkillGarden =
|
|
133
|
+
const preserveSkillGarden = lock?.plugins
|
|
126
134
|
.some(({ id }) => id === SKILL_GARDEN_PLUGIN_ID) === true;
|
|
127
135
|
const code = await plugin({
|
|
128
136
|
...ctx,
|
package/src/lib/skill-catalog.js
CHANGED
|
@@ -444,6 +444,16 @@ function isCommonSkillInstalled(target, name) {
|
|
|
444
444
|
);
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
+
/**
|
|
448
|
+
* 判断目标是否已经初始化为 Trellis 项目。
|
|
449
|
+
*
|
|
450
|
+
* @param {string} target 目标项目根目录
|
|
451
|
+
* @returns {boolean} 是否存在 `.trellis` 入口
|
|
452
|
+
*/
|
|
453
|
+
function hasTrellisProject(target) {
|
|
454
|
+
return fs.existsSync(path.join(target, ".trellis"));
|
|
455
|
+
}
|
|
456
|
+
|
|
447
457
|
/**
|
|
448
458
|
* 读取当前可管理的通用技能元数据。
|
|
449
459
|
*
|
|
@@ -502,6 +512,15 @@ function listWorkflowEnhancementSkills(target, variantDir) {
|
|
|
502
512
|
* @returns {{variant: string, version: string, commonSkills: object[], enhancementSkills: object[]}} 菜单清单
|
|
503
513
|
*/
|
|
504
514
|
export function listSkillCatalog(target, variantOverride) {
|
|
515
|
+
if (!hasTrellisProject(target)) {
|
|
516
|
+
return {
|
|
517
|
+
variant: variantOverride || "common",
|
|
518
|
+
version: "",
|
|
519
|
+
commonSkills: listCommonSkills(target),
|
|
520
|
+
enhancementSkills: [],
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
505
524
|
const { variant, version, variantDir } = resolveEnhancementSnapshot(
|
|
506
525
|
target,
|
|
507
526
|
variantOverride,
|
|
@@ -625,7 +644,7 @@ export function syncInstalledCommonSkills(target) {
|
|
|
625
644
|
* @returns {{removed: string[], skipped: string[]}} 已删除与未发现的路径
|
|
626
645
|
*/
|
|
627
646
|
export function removeCommonSkills(target, variantOverride, names) {
|
|
628
|
-
resolveEnhancementSnapshot(target, variantOverride);
|
|
647
|
+
if (hasTrellisProject(target)) resolveEnhancementSnapshot(target, variantOverride);
|
|
629
648
|
const available = new Set(listCommonSnapshotNames());
|
|
630
649
|
const removed = [];
|
|
631
650
|
const skipped = [];
|