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.
- 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 +156 -10
- package/src/commands/plugin.js +35 -4
- package/src/lib/skill-catalog.js +20 -1
- package/src/plugin/application-service.js +81 -7
- 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 +6 -0
- 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") {
|
|
@@ -470,7 +607,16 @@ export async function registerRemotePluginSources({ parsed, projectRoot, options
|
|
|
470
607
|
sleep: options.sleep,
|
|
471
608
|
now: options.now,
|
|
472
609
|
});
|
|
473
|
-
manager = new GitLabCredentialManager({
|
|
610
|
+
manager = new GitLabCredentialManager({
|
|
611
|
+
store: credentialBundle.store,
|
|
612
|
+
oauth,
|
|
613
|
+
now: options.now,
|
|
614
|
+
persistent: credentialBundle.persistent,
|
|
615
|
+
credentialResolver: options.credentialResolver,
|
|
616
|
+
env: options.env,
|
|
617
|
+
runGlab: options.runGlab,
|
|
618
|
+
glabCommand: options.glabCommand,
|
|
619
|
+
});
|
|
474
620
|
}
|
|
475
621
|
// 依赖闭包可能跨 source,因此登记全部可用来源,而不是只登记入口来源。
|
|
476
622
|
for (const source of sources) {
|
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]
|
|
@@ -875,6 +904,7 @@ export async function plugin(ctx, options = {}) {
|
|
|
875
904
|
: "*"
|
|
876
905
|
),
|
|
877
906
|
platforms: parsed.platforms,
|
|
907
|
+
...(parsed.contentSelection ? { contentSelection: parsed.contentSelection } : {}),
|
|
878
908
|
dryRun: parsed.dryRun,
|
|
879
909
|
onPreflight: options.onPreflight,
|
|
880
910
|
};
|
|
@@ -893,6 +923,7 @@ export async function plugin(ctx, options = {}) {
|
|
|
893
923
|
: parsed.version ? { version: parsed.version } : {}),
|
|
894
924
|
...(Object.keys(parsed.widen || {}).length > 0 ? { widen: parsed.widen } : {}),
|
|
895
925
|
platforms: parsed.platforms,
|
|
926
|
+
...(parsed.contentSelection ? { contentSelection: parsed.contentSelection } : {}),
|
|
896
927
|
dryRun: parsed.dryRun,
|
|
897
928
|
onPreflight: options.onPreflight,
|
|
898
929
|
};
|
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 = [];
|
|
@@ -16,6 +16,11 @@ import { hashDirectoryIfExists, hashFileIfExists } from "./install/content-hash.
|
|
|
16
16
|
import { getPluginPatchPlanner } from "./runtime-extensions.js";
|
|
17
17
|
import { isRuntimeBuiltinProviderTrusted } from "./runtime-extensions.js";
|
|
18
18
|
import { compareUtf8 } from "./stable-order.js";
|
|
19
|
+
import {
|
|
20
|
+
contentSelectionsEqual,
|
|
21
|
+
normalizeContentSelection,
|
|
22
|
+
selectContentSkillEntries,
|
|
23
|
+
} from "./content-selection.js";
|
|
19
24
|
|
|
20
25
|
/**
|
|
21
26
|
* 计算 lock roots 可达的全部 Plugin。
|
|
@@ -151,7 +156,7 @@ export class PluginApplicationService {
|
|
|
151
156
|
/**
|
|
152
157
|
* 添加或更新一个直接 Plugin 声明并应用完整图。
|
|
153
158
|
*
|
|
154
|
-
* @param {{id:string,version?:string,platforms?:string[],dryRun?:boolean,approvals?:string[],approvedDigests?:Map<string,string>|Record<string,string>,nonInteractive?:boolean,onPreflight?:(result:object)=>void}} options 添加选项
|
|
159
|
+
* @param {{id:string,version?:string,platforms?:string[],contentSelection?:import("./contracts.js").PluginContentSelection,dryRun?:boolean,approvals?:string[],approvedDigests?:Map<string,string>|Record<string,string>,nonInteractive?:boolean,onPreflight?:(result:object)=>void}} options 添加选项
|
|
155
160
|
* @returns {object} 生命周期结果
|
|
156
161
|
*/
|
|
157
162
|
add(options) {
|
|
@@ -168,6 +173,11 @@ export class PluginApplicationService {
|
|
|
168
173
|
source: sourceId,
|
|
169
174
|
version: options.version || "*",
|
|
170
175
|
};
|
|
176
|
+
const contentSelection = normalizeContentSelection(options.contentSelection, {
|
|
177
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.USAGE_ERROR,
|
|
178
|
+
path: options.id,
|
|
179
|
+
});
|
|
180
|
+
if (contentSelection) declaration.contentSelection = contentSelection;
|
|
171
181
|
const declarations = pluginsFile.plugins.filter(({ id }) => id !== options.id);
|
|
172
182
|
declarations.push(declaration);
|
|
173
183
|
return this.#applyLifecycle("add", {
|
|
@@ -189,12 +199,13 @@ export class PluginApplicationService {
|
|
|
189
199
|
* 其锁定包也已不可重放,逐个更新会在其它节点上撞 `已锁定 Plugin 包不可重放`。
|
|
190
200
|
* 传入 `widen` 时按 `update: "all"` 解析,让全部节点都允许升级,一次性解开这种死锁。
|
|
191
201
|
*
|
|
192
|
-
* @param {{id?:string|null,version?:string,widen?:Map<string,string>|Record<string,string>,platforms?:string[],dryRun?:boolean,approvals?:string[],approvedDigests?:Map<string,string>|Record<string,string>,nonInteractive?:boolean,onPreflight?:(result:object)=>void}} [options] 更新选项
|
|
202
|
+
* @param {{id?:string|null,version?:string,widen?:Map<string,string>|Record<string,string>,contentSelection?:import("./contracts.js").PluginContentSelection|null,platforms?:string[],dryRun?:boolean,approvals?:string[],approvedDigests?:Map<string,string>|Record<string,string>,nonInteractive?:boolean,onPreflight?:(result:object)=>void}} [options] 更新选项
|
|
193
203
|
* @returns {object} 生命周期结果
|
|
194
204
|
*/
|
|
195
205
|
update(options = {}) {
|
|
196
206
|
const pluginsFile = this.store.readPlugins();
|
|
197
207
|
const widen = normalizeVersionOverrides(options.widen);
|
|
208
|
+
const hasContentSelection = Object.prototype.hasOwnProperty.call(options, "contentSelection");
|
|
198
209
|
if (options.id && !pluginsFile.plugins.some(({ id }) => id === options.id)) {
|
|
199
210
|
throw new PluginRuntimeError(`项目未声明 Plugin:${options.id}`, {
|
|
200
211
|
code: PLUGIN_RUNTIME_ERROR_CODES.USAGE_ERROR,
|
|
@@ -214,6 +225,18 @@ export class PluginApplicationService {
|
|
|
214
225
|
path: "widen",
|
|
215
226
|
});
|
|
216
227
|
}
|
|
228
|
+
if (hasContentSelection && !options.id) {
|
|
229
|
+
throw new PluginRuntimeError("更新 Content Skill 选择时必须指定 Plugin ID", {
|
|
230
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.USAGE_ERROR,
|
|
231
|
+
path: "contentSelection",
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (hasContentSelection && widen.size > 0) {
|
|
235
|
+
throw new PluginRuntimeError("更新 Content Skill 选择时不能同时放宽声明范围", {
|
|
236
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.USAGE_ERROR,
|
|
237
|
+
path: "contentSelection",
|
|
238
|
+
});
|
|
239
|
+
}
|
|
217
240
|
const declaredIds = new Set(pluginsFile.plugins.map(({ id }) => id));
|
|
218
241
|
for (const id of [...widen.keys()].sort(compareUtf8)) {
|
|
219
242
|
if (!declaredIds.has(id)) {
|
|
@@ -229,12 +252,24 @@ export class PluginApplicationService {
|
|
|
229
252
|
const overrides = options.version && options.id
|
|
230
253
|
? new Map([[options.id, options.version]])
|
|
231
254
|
: widen;
|
|
232
|
-
const
|
|
255
|
+
const contentSelection = hasContentSelection
|
|
256
|
+
? normalizeContentSelection(options.contentSelection, {
|
|
257
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.USAGE_ERROR,
|
|
258
|
+
path: options.id,
|
|
259
|
+
})
|
|
260
|
+
: undefined;
|
|
261
|
+
const nextPlugins = (overrides.size > 0 || hasContentSelection)
|
|
233
262
|
? {
|
|
234
263
|
...pluginsFile,
|
|
235
|
-
plugins: pluginsFile.plugins.map((plugin) =>
|
|
236
|
-
overrides.has(plugin.id) ? { ...plugin, version: overrides.get(plugin.id) } : plugin
|
|
237
|
-
|
|
264
|
+
plugins: pluginsFile.plugins.map((plugin) => {
|
|
265
|
+
let next = overrides.has(plugin.id) ? { ...plugin, version: overrides.get(plugin.id) } : plugin;
|
|
266
|
+
if (hasContentSelection && plugin.id === options.id) {
|
|
267
|
+
next = { ...next };
|
|
268
|
+
if (contentSelection) next.contentSelection = contentSelection;
|
|
269
|
+
else delete next.contentSelection;
|
|
270
|
+
}
|
|
271
|
+
return next;
|
|
272
|
+
}),
|
|
238
273
|
}
|
|
239
274
|
: pluginsFile;
|
|
240
275
|
return this.#applyLifecycle("update", {
|
|
@@ -410,13 +445,32 @@ export class PluginApplicationService {
|
|
|
410
445
|
severity: "error",
|
|
411
446
|
});
|
|
412
447
|
}
|
|
448
|
+
const locked = lockById.get(declaration.id);
|
|
449
|
+
const applied = stateById.get(declaration.id);
|
|
450
|
+
if (locked && !contentSelectionsEqual(declaration.contentSelection, locked.contentSelection)) {
|
|
451
|
+
diagnostics.push({
|
|
452
|
+
code: "verify.content-selection-lock-mismatch",
|
|
453
|
+
path: declaration.id,
|
|
454
|
+
message: `Plugin 声明与 lock 的 Content Skill 选择不一致:${declaration.id}`,
|
|
455
|
+
severity: "error",
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
if (applied && !contentSelectionsEqual(declaration.contentSelection, applied.contentSelection)) {
|
|
459
|
+
diagnostics.push({
|
|
460
|
+
code: "verify.content-selection-state-mismatch",
|
|
461
|
+
path: declaration.id,
|
|
462
|
+
message: `Plugin 声明与 state 的 Content Skill 选择不一致:${declaration.id}`,
|
|
463
|
+
severity: "error",
|
|
464
|
+
});
|
|
465
|
+
}
|
|
413
466
|
}
|
|
414
467
|
for (const id of requestedIds) {
|
|
415
468
|
const locked = lockById.get(id);
|
|
416
469
|
const applied = stateById.get(id);
|
|
417
470
|
if (!locked) continue;
|
|
471
|
+
let pluginPackage = null;
|
|
418
472
|
try {
|
|
419
|
-
this.registry.readPackage(locked);
|
|
473
|
+
pluginPackage = this.registry.readPackage(locked);
|
|
420
474
|
} catch (error) {
|
|
421
475
|
diagnostics.push({
|
|
422
476
|
code: "verify.package-invalid",
|
|
@@ -425,6 +479,18 @@ export class PluginApplicationService {
|
|
|
425
479
|
severity: "error",
|
|
426
480
|
});
|
|
427
481
|
}
|
|
482
|
+
if (pluginPackage) {
|
|
483
|
+
try {
|
|
484
|
+
selectContentSkillEntries(pluginPackage.manifest.content.skills || [], locked.contentSelection, id);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
diagnostics.push({
|
|
487
|
+
code: "verify.content-selection-invalid",
|
|
488
|
+
path: id,
|
|
489
|
+
message: error.message,
|
|
490
|
+
severity: "error",
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
428
494
|
if (!applied) {
|
|
429
495
|
diagnostics.push({
|
|
430
496
|
code: "verify.state-missing",
|
|
@@ -442,6 +508,14 @@ export class PluginApplicationService {
|
|
|
442
508
|
severity: "error",
|
|
443
509
|
});
|
|
444
510
|
}
|
|
511
|
+
if (!contentSelectionsEqual(locked.contentSelection, applied.contentSelection)) {
|
|
512
|
+
diagnostics.push({
|
|
513
|
+
code: "verify.content-selection-apply-mismatch",
|
|
514
|
+
path: id,
|
|
515
|
+
message: `Plugin lock 与 state 的 Content Skill 选择不一致:${id}`,
|
|
516
|
+
severity: "error",
|
|
517
|
+
});
|
|
518
|
+
}
|
|
445
519
|
for (const entry of applied.paths) {
|
|
446
520
|
const target = path.join(this.projectRoot, ...entry.path.split("/"));
|
|
447
521
|
let actual = null;
|
|
@@ -3,8 +3,20 @@ import { PLUGIN_RUNTIME_ERROR_CODES, PluginRuntimeError } from "../runtime-error
|
|
|
3
3
|
/** 凭据载荷 schema 版本。 */
|
|
4
4
|
export const CREDENTIAL_SCHEMA_VERSION = 1;
|
|
5
5
|
|
|
6
|
-
/** Flower GitLab OAuth
|
|
7
|
-
export const
|
|
6
|
+
/** Flower GitLab OAuth 主动请求 scope。 */
|
|
7
|
+
export const GITLAB_OAUTH_REQUEST_SCOPES = Object.freeze([
|
|
8
|
+
"openid",
|
|
9
|
+
"profile",
|
|
10
|
+
"read_user",
|
|
11
|
+
"write_repository",
|
|
12
|
+
"api",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
/** 旧版 GitLab OAuth 读取能力 scope。 */
|
|
16
|
+
export const GITLAB_OAUTH_LEGACY_SCOPES = Object.freeze(["read_api", "read_repository"]);
|
|
17
|
+
|
|
18
|
+
/** 兼容旧内部导入名;新代码应使用 GITLAB_OAUTH_REQUEST_SCOPES。 */
|
|
19
|
+
export const GITLAB_OAUTH_SCOPES = GITLAB_OAUTH_REQUEST_SCOPES;
|
|
8
20
|
|
|
9
21
|
/**
|
|
10
22
|
* 规范化 GitLab 来源凭据账户名。
|
|
@@ -18,6 +30,16 @@ export function credentialAccount(source) {
|
|
|
18
30
|
return `${url.hostname.toLowerCase()}${port}/${source.id}`;
|
|
19
31
|
}
|
|
20
32
|
|
|
33
|
+
/**
|
|
34
|
+
* 判断 GitLab OAuth scope 是否具备当前 REST 读取能力。
|
|
35
|
+
*
|
|
36
|
+
* @param {string[]} scopes 实际授权 scope
|
|
37
|
+
* @returns {boolean} 是否满足读取 Marketplace 所需能力
|
|
38
|
+
*/
|
|
39
|
+
export function isGitLabCredentialScopeSufficient(scopes) {
|
|
40
|
+
return scopes.includes("api") || GITLAB_OAUTH_LEGACY_SCOPES.every((scope) => scopes.includes(scope));
|
|
41
|
+
}
|
|
42
|
+
|
|
21
43
|
/**
|
|
22
44
|
* 校验并规范化版本化 OAuth 凭据。
|
|
23
45
|
*
|
|
@@ -38,7 +60,7 @@ export function validateCredential(value, source) {
|
|
|
38
60
|
typeof credential.accessToken !== "string" ||
|
|
39
61
|
credential.accessToken.length === 0 ||
|
|
40
62
|
typeof credential.tokenType !== "string" ||
|
|
41
|
-
!
|
|
63
|
+
!isGitLabCredentialScopeSufficient(scopes) ||
|
|
42
64
|
!(credential.refreshToken === null || typeof credential.refreshToken === "string") ||
|
|
43
65
|
!(credential.createdAt === undefined || credential.createdAt === null || Number.isFinite(credential.createdAt)) ||
|
|
44
66
|
!(credential.expiresAt === null || Number.isFinite(credential.expiresAt)) ||
|