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
|
@@ -16,6 +16,13 @@ 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";
|
|
24
|
+
|
|
25
|
+
const SKILL_GARDEN_MIGRATION_OWNER_ID = "flower/skill-garden";
|
|
19
26
|
|
|
20
27
|
/**
|
|
21
28
|
* 计算 lock roots 可达的全部 Plugin。
|
|
@@ -117,6 +124,94 @@ function standardContentPlan(graph) {
|
|
|
117
124
|
};
|
|
118
125
|
}
|
|
119
126
|
|
|
127
|
+
/**
|
|
128
|
+
* 校验冻结 Plugin 的 lock/state 对应关系与当前目标摘要。
|
|
129
|
+
*
|
|
130
|
+
* @param {string} projectRoot 项目根
|
|
131
|
+
* @param {import("./contracts.js").ResolvedPlugin} locked 冻结 lock entry
|
|
132
|
+
* @param {import("./contracts.js").PluginStateEntry} applied 冻结 state entry
|
|
133
|
+
* @returns {void}
|
|
134
|
+
*/
|
|
135
|
+
function assertPreservedState(projectRoot, locked, applied) {
|
|
136
|
+
if (
|
|
137
|
+
applied.version !== locked.version ||
|
|
138
|
+
!contentSelectionsEqual(applied.contentSelection, locked.contentSelection)
|
|
139
|
+
) {
|
|
140
|
+
throw new PluginRuntimeError(`冻结 Plugin 的 lock/state 不一致:${locked.id}`, {
|
|
141
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.TARGET_DRIFT,
|
|
142
|
+
path: locked.id,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
for (const entry of applied.paths) {
|
|
146
|
+
const target = path.join(projectRoot, ...entry.path.split("/"));
|
|
147
|
+
const actual = entry.kind === "directory"
|
|
148
|
+
? hashDirectoryIfExists(target)
|
|
149
|
+
: hashFileIfExists(target);
|
|
150
|
+
if (actual !== entry.hash) {
|
|
151
|
+
throw new PluginRuntimeError(`冻结 Plugin 目标摘要漂移:${entry.path}`, {
|
|
152
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.TARGET_DRIFT,
|
|
153
|
+
path: entry.path,
|
|
154
|
+
details: { expected: entry.hash, actual },
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const patchEntry of applied.patches) {
|
|
159
|
+
const target = path.join(projectRoot, ...patchEntry.target.split("/"));
|
|
160
|
+
const actual = hashFileIfExists(target);
|
|
161
|
+
if (actual !== patchEntry.resultHash) {
|
|
162
|
+
throw new PluginRuntimeError(`冻结 Plugin Patch 目标摘要漂移:${patchEntry.target}`, {
|
|
163
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.TARGET_DRIFT,
|
|
164
|
+
path: patchEntry.target,
|
|
165
|
+
details: { expected: patchEntry.resultHash, actual },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 判断两个项目内路径是否相同或存在父子关系。
|
|
173
|
+
*
|
|
174
|
+
* @param {string} left 左路径
|
|
175
|
+
* @param {string} right 右路径
|
|
176
|
+
* @returns {boolean} 是否相交
|
|
177
|
+
*/
|
|
178
|
+
function pathsOverlap(left, right) {
|
|
179
|
+
return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 阻止活跃 Plugin 的计划改写冻结 Plugin 的受管目标。
|
|
184
|
+
*
|
|
185
|
+
* @param {import("./contracts.js").PluginStateEntry[]} preservedEntries 冻结 state entries
|
|
186
|
+
* @param {{mutations?:Array<{owner:string,target:string}>,directoryClaims?:Array<{owner:string,path:string}>,directoryRemovals?:Array<{owner:string,path:string}>,patchMutations?:Array<{owner:string,target:string}>}} plan 活跃计划片段
|
|
187
|
+
* @returns {void}
|
|
188
|
+
*/
|
|
189
|
+
function assertNoPreservedTargetConflicts(preservedEntries, plan) {
|
|
190
|
+
const protectedTargets = preservedEntries.flatMap((plugin) => [
|
|
191
|
+
...plugin.paths.map(({ path: target }) => ({ owner: plugin.id, target })),
|
|
192
|
+
...plugin.patches.map(({ target }) => ({ owner: plugin.id, target })),
|
|
193
|
+
]);
|
|
194
|
+
const activeTargets = [
|
|
195
|
+
...(plan.mutations || []).map(({ owner, target }) => ({ owner, target })),
|
|
196
|
+
...(plan.directoryClaims || []).map(({ owner, path: target }) => ({ owner, target })),
|
|
197
|
+
...(plan.directoryRemovals || []).map(({ owner, path: target }) => ({ owner, target })),
|
|
198
|
+
...(plan.patchMutations || []).map(({ owner, target }) => ({ owner, target })),
|
|
199
|
+
];
|
|
200
|
+
for (const active of activeTargets) {
|
|
201
|
+
const protectedTarget = protectedTargets.find((entry) => pathsOverlap(active.target, entry.target));
|
|
202
|
+
if (!protectedTarget) continue;
|
|
203
|
+
throw new PluginRuntimeError(`活跃 Plugin 计划与冻结目标冲突:${active.target}`, {
|
|
204
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.CONTENT_CONFLICT,
|
|
205
|
+
path: active.target,
|
|
206
|
+
details: {
|
|
207
|
+
activeOwner: active.owner,
|
|
208
|
+
preservedOwner: protectedTarget.owner,
|
|
209
|
+
preservedTarget: protectedTarget.target,
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
120
215
|
/**
|
|
121
216
|
* Flower Plugin 生命周期应用服务。
|
|
122
217
|
*/
|
|
@@ -151,7 +246,7 @@ export class PluginApplicationService {
|
|
|
151
246
|
/**
|
|
152
247
|
* 添加或更新一个直接 Plugin 声明并应用完整图。
|
|
153
248
|
*
|
|
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 添加选项
|
|
249
|
+
* @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,preserveIds?:string[],onPreflight?:(result:object)=>void}} options 添加选项
|
|
155
250
|
* @returns {object} 生命周期结果
|
|
156
251
|
*/
|
|
157
252
|
add(options) {
|
|
@@ -168,6 +263,11 @@ export class PluginApplicationService {
|
|
|
168
263
|
source: sourceId,
|
|
169
264
|
version: options.version || "*",
|
|
170
265
|
};
|
|
266
|
+
const contentSelection = normalizeContentSelection(options.contentSelection, {
|
|
267
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.USAGE_ERROR,
|
|
268
|
+
path: options.id,
|
|
269
|
+
});
|
|
270
|
+
if (contentSelection) declaration.contentSelection = contentSelection;
|
|
171
271
|
const declarations = pluginsFile.plugins.filter(({ id }) => id !== options.id);
|
|
172
272
|
declarations.push(declaration);
|
|
173
273
|
return this.#applyLifecycle("add", {
|
|
@@ -178,6 +278,7 @@ export class PluginApplicationService {
|
|
|
178
278
|
approvals: options.approvals || [],
|
|
179
279
|
approvedDigests: options.approvedDigests,
|
|
180
280
|
nonInteractive: options.nonInteractive,
|
|
281
|
+
preserveIds: options.preserveIds || [],
|
|
181
282
|
onPreflight: options.onPreflight,
|
|
182
283
|
});
|
|
183
284
|
}
|
|
@@ -189,12 +290,13 @@ export class PluginApplicationService {
|
|
|
189
290
|
* 其锁定包也已不可重放,逐个更新会在其它节点上撞 `已锁定 Plugin 包不可重放`。
|
|
190
291
|
* 传入 `widen` 时按 `update: "all"` 解析,让全部节点都允许升级,一次性解开这种死锁。
|
|
191
292
|
*
|
|
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] 更新选项
|
|
293
|
+
* @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,preserveIds?:string[],onPreflight?:(result:object)=>void}} [options] 更新选项
|
|
193
294
|
* @returns {object} 生命周期结果
|
|
194
295
|
*/
|
|
195
296
|
update(options = {}) {
|
|
196
297
|
const pluginsFile = this.store.readPlugins();
|
|
197
298
|
const widen = normalizeVersionOverrides(options.widen);
|
|
299
|
+
const hasContentSelection = Object.prototype.hasOwnProperty.call(options, "contentSelection");
|
|
198
300
|
if (options.id && !pluginsFile.plugins.some(({ id }) => id === options.id)) {
|
|
199
301
|
throw new PluginRuntimeError(`项目未声明 Plugin:${options.id}`, {
|
|
200
302
|
code: PLUGIN_RUNTIME_ERROR_CODES.USAGE_ERROR,
|
|
@@ -214,6 +316,18 @@ export class PluginApplicationService {
|
|
|
214
316
|
path: "widen",
|
|
215
317
|
});
|
|
216
318
|
}
|
|
319
|
+
if (hasContentSelection && !options.id) {
|
|
320
|
+
throw new PluginRuntimeError("更新 Content Skill 选择时必须指定 Plugin ID", {
|
|
321
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.USAGE_ERROR,
|
|
322
|
+
path: "contentSelection",
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
if (hasContentSelection && widen.size > 0) {
|
|
326
|
+
throw new PluginRuntimeError("更新 Content Skill 选择时不能同时放宽声明范围", {
|
|
327
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.USAGE_ERROR,
|
|
328
|
+
path: "contentSelection",
|
|
329
|
+
});
|
|
330
|
+
}
|
|
217
331
|
const declaredIds = new Set(pluginsFile.plugins.map(({ id }) => id));
|
|
218
332
|
for (const id of [...widen.keys()].sort(compareUtf8)) {
|
|
219
333
|
if (!declaredIds.has(id)) {
|
|
@@ -229,12 +343,24 @@ export class PluginApplicationService {
|
|
|
229
343
|
const overrides = options.version && options.id
|
|
230
344
|
? new Map([[options.id, options.version]])
|
|
231
345
|
: widen;
|
|
232
|
-
const
|
|
346
|
+
const contentSelection = hasContentSelection
|
|
347
|
+
? normalizeContentSelection(options.contentSelection, {
|
|
348
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.USAGE_ERROR,
|
|
349
|
+
path: options.id,
|
|
350
|
+
})
|
|
351
|
+
: undefined;
|
|
352
|
+
const nextPlugins = (overrides.size > 0 || hasContentSelection)
|
|
233
353
|
? {
|
|
234
354
|
...pluginsFile,
|
|
235
|
-
plugins: pluginsFile.plugins.map((plugin) =>
|
|
236
|
-
overrides.has(plugin.id) ? { ...plugin, version: overrides.get(plugin.id) } : plugin
|
|
237
|
-
|
|
355
|
+
plugins: pluginsFile.plugins.map((plugin) => {
|
|
356
|
+
let next = overrides.has(plugin.id) ? { ...plugin, version: overrides.get(plugin.id) } : plugin;
|
|
357
|
+
if (hasContentSelection && plugin.id === options.id) {
|
|
358
|
+
next = { ...next };
|
|
359
|
+
if (contentSelection) next.contentSelection = contentSelection;
|
|
360
|
+
else delete next.contentSelection;
|
|
361
|
+
}
|
|
362
|
+
return next;
|
|
363
|
+
}),
|
|
238
364
|
}
|
|
239
365
|
: pluginsFile;
|
|
240
366
|
return this.#applyLifecycle("update", {
|
|
@@ -245,6 +371,7 @@ export class PluginApplicationService {
|
|
|
245
371
|
approvals: options.approvals || [],
|
|
246
372
|
approvedDigests: options.approvedDigests,
|
|
247
373
|
nonInteractive: options.nonInteractive,
|
|
374
|
+
preserveIds: options.preserveIds || [],
|
|
248
375
|
onPreflight: options.onPreflight,
|
|
249
376
|
});
|
|
250
377
|
}
|
|
@@ -410,13 +537,32 @@ export class PluginApplicationService {
|
|
|
410
537
|
severity: "error",
|
|
411
538
|
});
|
|
412
539
|
}
|
|
540
|
+
const locked = lockById.get(declaration.id);
|
|
541
|
+
const applied = stateById.get(declaration.id);
|
|
542
|
+
if (locked && !contentSelectionsEqual(declaration.contentSelection, locked.contentSelection)) {
|
|
543
|
+
diagnostics.push({
|
|
544
|
+
code: "verify.content-selection-lock-mismatch",
|
|
545
|
+
path: declaration.id,
|
|
546
|
+
message: `Plugin 声明与 lock 的 Content Skill 选择不一致:${declaration.id}`,
|
|
547
|
+
severity: "error",
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
if (applied && !contentSelectionsEqual(declaration.contentSelection, applied.contentSelection)) {
|
|
551
|
+
diagnostics.push({
|
|
552
|
+
code: "verify.content-selection-state-mismatch",
|
|
553
|
+
path: declaration.id,
|
|
554
|
+
message: `Plugin 声明与 state 的 Content Skill 选择不一致:${declaration.id}`,
|
|
555
|
+
severity: "error",
|
|
556
|
+
});
|
|
557
|
+
}
|
|
413
558
|
}
|
|
414
559
|
for (const id of requestedIds) {
|
|
415
560
|
const locked = lockById.get(id);
|
|
416
561
|
const applied = stateById.get(id);
|
|
417
562
|
if (!locked) continue;
|
|
563
|
+
let pluginPackage = null;
|
|
418
564
|
try {
|
|
419
|
-
this.registry.readPackage(locked);
|
|
565
|
+
pluginPackage = this.registry.readPackage(locked);
|
|
420
566
|
} catch (error) {
|
|
421
567
|
diagnostics.push({
|
|
422
568
|
code: "verify.package-invalid",
|
|
@@ -425,6 +571,18 @@ export class PluginApplicationService {
|
|
|
425
571
|
severity: "error",
|
|
426
572
|
});
|
|
427
573
|
}
|
|
574
|
+
if (pluginPackage) {
|
|
575
|
+
try {
|
|
576
|
+
selectContentSkillEntries(pluginPackage.manifest.content.skills || [], locked.contentSelection, id);
|
|
577
|
+
} catch (error) {
|
|
578
|
+
diagnostics.push({
|
|
579
|
+
code: "verify.content-selection-invalid",
|
|
580
|
+
path: id,
|
|
581
|
+
message: error.message,
|
|
582
|
+
severity: "error",
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
428
586
|
if (!applied) {
|
|
429
587
|
diagnostics.push({
|
|
430
588
|
code: "verify.state-missing",
|
|
@@ -442,6 +600,14 @@ export class PluginApplicationService {
|
|
|
442
600
|
severity: "error",
|
|
443
601
|
});
|
|
444
602
|
}
|
|
603
|
+
if (!contentSelectionsEqual(locked.contentSelection, applied.contentSelection)) {
|
|
604
|
+
diagnostics.push({
|
|
605
|
+
code: "verify.content-selection-apply-mismatch",
|
|
606
|
+
path: id,
|
|
607
|
+
message: `Plugin lock 与 state 的 Content Skill 选择不一致:${id}`,
|
|
608
|
+
severity: "error",
|
|
609
|
+
});
|
|
610
|
+
}
|
|
445
611
|
for (const entry of applied.paths) {
|
|
446
612
|
const target = path.join(this.projectRoot, ...entry.path.split("/"));
|
|
447
613
|
let actual = null;
|
|
@@ -483,10 +649,39 @@ export class PluginApplicationService {
|
|
|
483
649
|
const pluginsFile = validatePluginsFile(input.plugins);
|
|
484
650
|
const previousLock = this.store.readLock();
|
|
485
651
|
const previousState = this.store.readState();
|
|
652
|
+
const preservedIds = new Set(input.preserveIds || []);
|
|
653
|
+
const previousLockById = new Map((previousLock?.plugins || []).map((plugin) => [plugin.id, plugin]));
|
|
654
|
+
const previousStateById = new Map((previousState?.plugins || []).map((plugin) => [plugin.id, plugin]));
|
|
486
655
|
const resolution = resolvePluginGraph(pluginsFile.plugins, this.registry, {
|
|
487
656
|
lockedPlugins: previousLock?.plugins || [],
|
|
488
657
|
update: input.update,
|
|
658
|
+
preserveIds: [...preservedIds],
|
|
489
659
|
});
|
|
660
|
+
const resolvedIds = new Set(resolution.graph.plugins.map(({ id }) => id));
|
|
661
|
+
const preservedEntries = [];
|
|
662
|
+
for (const id of [...preservedIds].sort(compareUtf8)) {
|
|
663
|
+
const previousLocked = previousLockById.get(id);
|
|
664
|
+
const previousEntry = previousStateById.get(id);
|
|
665
|
+
if (!previousLocked || !resolvedIds.has(id)) {
|
|
666
|
+
throw new PluginRuntimeError(`冻结 Plugin 缺少既有 lock:${id}`, {
|
|
667
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.TARGET_DRIFT,
|
|
668
|
+
path: id,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
if (!previousEntry) {
|
|
672
|
+
throw new PluginRuntimeError(`冻结 Plugin 缺少既有 state:${id}`, {
|
|
673
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.TARGET_DRIFT,
|
|
674
|
+
path: id,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
assertPreservedState(this.projectRoot, previousLocked, previousEntry);
|
|
678
|
+
preservedEntries.push(previousEntry);
|
|
679
|
+
}
|
|
680
|
+
const activeGraph = {
|
|
681
|
+
roots: resolution.graph.roots.filter((id) => !preservedIds.has(id)),
|
|
682
|
+
plugins: resolution.graph.plugins.filter(({ id }) => !preservedIds.has(id)),
|
|
683
|
+
};
|
|
684
|
+
const activeSelected = resolution.selected.filter(({ id }) => !preservedIds.has(id));
|
|
490
685
|
let projection = {
|
|
491
686
|
mutations: [],
|
|
492
687
|
payloads: new Map(),
|
|
@@ -495,47 +690,45 @@ export class PluginApplicationService {
|
|
|
495
690
|
directoryRemovals: [],
|
|
496
691
|
};
|
|
497
692
|
let platformSelection = { platforms: [], targets: [] };
|
|
498
|
-
if (
|
|
693
|
+
if (activeGraph.plugins.length > 0) {
|
|
499
694
|
const platforms = input.platforms.length > 0
|
|
500
695
|
? input.platforms
|
|
501
696
|
: statePlatforms(previousState);
|
|
502
697
|
platformSelection = this.platformDetector(this.projectRoot, platforms);
|
|
503
698
|
projection = projectPluginContent({
|
|
504
699
|
projectRoot: this.projectRoot,
|
|
505
|
-
graph:
|
|
506
|
-
selected:
|
|
700
|
+
graph: activeGraph,
|
|
701
|
+
selected: activeSelected,
|
|
507
702
|
registry: this.registry,
|
|
508
703
|
platformSelection,
|
|
509
704
|
previousState,
|
|
510
705
|
});
|
|
511
706
|
}
|
|
512
|
-
const preservedIds = new Set(input.preserveIds || []);
|
|
513
707
|
if (preservedIds.size > 0) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
projection.
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
if (previousState?.migration) projection.state.migration = structuredClone(previousState.migration);
|
|
708
|
+
const projectedStateById = new Map(projection.state.plugins.map((plugin) => [plugin.id, plugin]));
|
|
709
|
+
projection.state.plugins = resolution.graph.plugins.map(({ id }) => {
|
|
710
|
+
if (preservedIds.has(id)) return structuredClone(previousStateById.get(id));
|
|
711
|
+
const projected = projectedStateById.get(id);
|
|
712
|
+
if (!projected) throw new Error(`Resolved Plugin 缺少投影 state:${id}`);
|
|
713
|
+
return projected;
|
|
714
|
+
});
|
|
715
|
+
resolution.graph.plugins = resolution.graph.plugins.map((plugin) => (
|
|
716
|
+
preservedIds.has(plugin.id)
|
|
717
|
+
? structuredClone(previousLockById.get(plugin.id))
|
|
718
|
+
: plugin
|
|
719
|
+
));
|
|
720
|
+
assertNoPreservedTargetConflicts(preservedEntries, {
|
|
721
|
+
mutations: projection.mutations,
|
|
722
|
+
directoryClaims: projection.directoryClaims,
|
|
723
|
+
directoryRemovals: projection.directoryRemovals,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
if (
|
|
727
|
+
preservedIds.has(SKILL_GARDEN_MIGRATION_OWNER_ID) &&
|
|
728
|
+
!projection.state.migration &&
|
|
729
|
+
previousState?.migration
|
|
730
|
+
) {
|
|
731
|
+
projection.state.migration = structuredClone(previousState.migration);
|
|
539
732
|
}
|
|
540
733
|
|
|
541
734
|
const previousDirectories = new Map();
|
|
@@ -545,7 +738,11 @@ export class PluginApplicationService {
|
|
|
545
738
|
}
|
|
546
739
|
}
|
|
547
740
|
const directoryClaims = [];
|
|
548
|
-
const desiredDirectoryPaths = new Set(
|
|
741
|
+
const desiredDirectoryPaths = new Set(
|
|
742
|
+
projection.state.plugins.flatMap((plugin) => plugin.paths
|
|
743
|
+
.filter(({ kind }) => kind === "directory")
|
|
744
|
+
.map(({ path: target }) => target)),
|
|
745
|
+
);
|
|
549
746
|
const directoryOwners = new Map();
|
|
550
747
|
for (const claim of projection.directoryClaims) {
|
|
551
748
|
desiredDirectoryPaths.add(claim.path);
|
|
@@ -623,6 +820,12 @@ export class PluginApplicationService {
|
|
|
623
820
|
});
|
|
624
821
|
}
|
|
625
822
|
}
|
|
823
|
+
if (preservedIds.size > 0) {
|
|
824
|
+
assertNoPreservedTargetConflicts(preservedEntries, {
|
|
825
|
+
mutations: removalMutations,
|
|
826
|
+
directoryRemovals,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
626
829
|
|
|
627
830
|
const patchPlanner = getPluginPatchPlanner();
|
|
628
831
|
let patchResult = standardContentPlan(resolution.graph);
|
|
@@ -700,6 +903,11 @@ export class PluginApplicationService {
|
|
|
700
903
|
});
|
|
701
904
|
}
|
|
702
905
|
}
|
|
906
|
+
if (preservedIds.size > 0) {
|
|
907
|
+
assertNoPreservedTargetConflicts(preservedEntries, {
|
|
908
|
+
patchMutations: patchResult.patchMutations,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
703
911
|
const grants = new Map(patchResult.grants.map(({ pluginId, grant }) => [pluginId, grant]));
|
|
704
912
|
for (const plugin of resolution.graph.plugins) {
|
|
705
913
|
const grant = grants.get(plugin.id) || (
|
|
@@ -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)) ||
|