flower-trellis 0.6.3 → 0.6.5
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/README.md +20 -1
- package/enhancements/0.6/overrides/bundles/control-plane-integrity.json +2 -0
- package/enhancements/0.6/overrides/bundles/intent-routing.json +2 -1
- package/enhancements/0.6/overrides/bundles/task-reference-contract.json +24 -0
- package/enhancements/0.6/overrides/conflicts.json +33 -0
- package/enhancements/0.6/overrides/patches/scripts/task-current-query-contract/patch.json +19 -0
- package/enhancements/0.6/overrides/patches/scripts/task-current-query-contract/task-current-content.py +39 -0
- package/enhancements/0.6/overrides/patches/scripts/task-current-query-contract/task-current-selector.py +38 -0
- package/enhancements/0.6/overrides/patches/scripts/task-reference-resolution/patch.json +20 -0
- package/enhancements/0.6/overrides/patches/scripts/task-reference-resolution/task-reference-content.py +64 -0
- package/enhancements/0.6/overrides/patches/scripts/task-reference-resolution/task-reference-selector.py +2 -0
- package/enhancements/0.6/scripts/decision_log.py +29 -24
- package/enhancements/0.6/scripts/task_progress.py +54 -13
- package/enhancements/0.6/scripts/untracked_flow.py +13 -5
- package/enhancements/MANIFEST.json +9 -2
- package/package.json +4 -4
- package/src/assets/flower_session_start.py +195 -0
- package/src/assets/flower_telemetry_hook.py +98 -0
- package/src/builtin-plugins/skill-garden/content-adapter.js +15 -1
- package/src/cli.js +3 -1
- package/src/commands/init.js +36 -1
- package/src/commands/plugin-remote.js +118 -20
- package/src/commands/plugin.js +25 -3
- package/src/commands/self-check.js +20 -0
- package/src/commands/self-update.js +38 -3
- package/src/commands/telemetry.js +30 -1
- package/src/commands/uninstall.js +20 -0
- package/src/commands/update-check.js +19 -0
- package/src/commands/update.js +59 -8
- package/src/commands/worktree.js +114 -0
- package/src/lib/cli-args.js +10 -0
- package/src/lib/developer.js +3 -2
- package/src/lib/flower-assets.js +21 -6
- package/src/lib/platform-patch-adapters.js +46 -8
- package/src/lib/telemetry-context.js +275 -0
- package/src/lib/telemetry-files.js +144 -0
- package/src/lib/telemetry-operation.js +84 -0
- package/src/lib/telemetry-queue.js +310 -0
- package/src/lib/telemetry-sender.js +7 -0
- package/src/lib/telemetry.js +12 -361
- package/src/patches/bundles/flower-platform-integration.json +4 -1
- package/src/patches/platforms/claude/session-start-hooks/patch.json +28 -0
- package/src/patches/platforms/claude/telemetry-hooks/patch.json +59 -0
- package/src/patches/platforms/codex/session-start-hooks/patch.json +3 -2
- package/src/patches/platforms/codex/telemetry-hooks/patch.json +59 -0
- package/src/plugin/application-service.js +170 -53
- package/src/plugin/contracts.js +1 -0
- package/src/plugin/lock-reachability.js +37 -0
- package/src/plugin/resolver/dependency-resolver.js +58 -7
- package/src/plugin/schemas/project-files.js +1 -0
- package/src/plugin/sources/gitlab-provider.js +50 -9
|
@@ -21,26 +21,9 @@ import {
|
|
|
21
21
|
normalizeContentSelection,
|
|
22
22
|
selectContentSkillEntries,
|
|
23
23
|
} from "./content-selection.js";
|
|
24
|
+
import { classifyLockReachability } from "./lock-reachability.js";
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
* 计算 lock roots 可达的全部 Plugin。
|
|
27
|
-
*
|
|
28
|
-
* @param {import("./contracts.js").PluginLock} lock Plugin lock
|
|
29
|
-
* @returns {Set<string>} 可达 Plugin ID
|
|
30
|
-
*/
|
|
31
|
-
function reachableLockIds(lock) {
|
|
32
|
-
const byId = new Map(lock.plugins.map((plugin) => [plugin.id, plugin]));
|
|
33
|
-
const reachable = new Set();
|
|
34
|
-
const visit = (id) => {
|
|
35
|
-
if (reachable.has(id)) return;
|
|
36
|
-
reachable.add(id);
|
|
37
|
-
const plugin = byId.get(id);
|
|
38
|
-
if (!plugin) return;
|
|
39
|
-
Object.keys(plugin.dependencies).sort(compareUtf8).forEach(visit);
|
|
40
|
-
};
|
|
41
|
-
[...lock.roots].sort(compareUtf8).forEach(visit);
|
|
42
|
-
return reachable;
|
|
43
|
-
}
|
|
26
|
+
const SKILL_GARDEN_MIGRATION_OWNER_ID = "flower/skill-garden";
|
|
44
27
|
|
|
45
28
|
/**
|
|
46
29
|
* 汇总既有 Plugin state 的实际投影平台,供无显式选择的生命周期继续复用。
|
|
@@ -122,6 +105,94 @@ function standardContentPlan(graph) {
|
|
|
122
105
|
};
|
|
123
106
|
}
|
|
124
107
|
|
|
108
|
+
/**
|
|
109
|
+
* 校验冻结 Plugin 的 lock/state 对应关系与当前目标摘要。
|
|
110
|
+
*
|
|
111
|
+
* @param {string} projectRoot 项目根
|
|
112
|
+
* @param {import("./contracts.js").ResolvedPlugin} locked 冻结 lock entry
|
|
113
|
+
* @param {import("./contracts.js").PluginStateEntry} applied 冻结 state entry
|
|
114
|
+
* @returns {void}
|
|
115
|
+
*/
|
|
116
|
+
function assertPreservedState(projectRoot, locked, applied) {
|
|
117
|
+
if (
|
|
118
|
+
applied.version !== locked.version ||
|
|
119
|
+
!contentSelectionsEqual(applied.contentSelection, locked.contentSelection)
|
|
120
|
+
) {
|
|
121
|
+
throw new PluginRuntimeError(`冻结 Plugin 的 lock/state 不一致:${locked.id}`, {
|
|
122
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.TARGET_DRIFT,
|
|
123
|
+
path: locked.id,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
for (const entry of applied.paths) {
|
|
127
|
+
const target = path.join(projectRoot, ...entry.path.split("/"));
|
|
128
|
+
const actual = entry.kind === "directory"
|
|
129
|
+
? hashDirectoryIfExists(target)
|
|
130
|
+
: hashFileIfExists(target);
|
|
131
|
+
if (actual !== entry.hash) {
|
|
132
|
+
throw new PluginRuntimeError(`冻结 Plugin 目标摘要漂移:${entry.path}`, {
|
|
133
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.TARGET_DRIFT,
|
|
134
|
+
path: entry.path,
|
|
135
|
+
details: { expected: entry.hash, actual },
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const patchEntry of applied.patches) {
|
|
140
|
+
const target = path.join(projectRoot, ...patchEntry.target.split("/"));
|
|
141
|
+
const actual = hashFileIfExists(target);
|
|
142
|
+
if (actual !== patchEntry.resultHash) {
|
|
143
|
+
throw new PluginRuntimeError(`冻结 Plugin Patch 目标摘要漂移:${patchEntry.target}`, {
|
|
144
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.TARGET_DRIFT,
|
|
145
|
+
path: patchEntry.target,
|
|
146
|
+
details: { expected: patchEntry.resultHash, actual },
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 判断两个项目内路径是否相同或存在父子关系。
|
|
154
|
+
*
|
|
155
|
+
* @param {string} left 左路径
|
|
156
|
+
* @param {string} right 右路径
|
|
157
|
+
* @returns {boolean} 是否相交
|
|
158
|
+
*/
|
|
159
|
+
function pathsOverlap(left, right) {
|
|
160
|
+
return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 阻止活跃 Plugin 的计划改写冻结 Plugin 的受管目标。
|
|
165
|
+
*
|
|
166
|
+
* @param {import("./contracts.js").PluginStateEntry[]} preservedEntries 冻结 state entries
|
|
167
|
+
* @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 活跃计划片段
|
|
168
|
+
* @returns {void}
|
|
169
|
+
*/
|
|
170
|
+
function assertNoPreservedTargetConflicts(preservedEntries, plan) {
|
|
171
|
+
const protectedTargets = preservedEntries.flatMap((plugin) => [
|
|
172
|
+
...plugin.paths.map(({ path: target }) => ({ owner: plugin.id, target })),
|
|
173
|
+
...plugin.patches.map(({ target }) => ({ owner: plugin.id, target })),
|
|
174
|
+
]);
|
|
175
|
+
const activeTargets = [
|
|
176
|
+
...(plan.mutations || []).map(({ owner, target }) => ({ owner, target })),
|
|
177
|
+
...(plan.directoryClaims || []).map(({ owner, path: target }) => ({ owner, target })),
|
|
178
|
+
...(plan.directoryRemovals || []).map(({ owner, path: target }) => ({ owner, target })),
|
|
179
|
+
...(plan.patchMutations || []).map(({ owner, target }) => ({ owner, target })),
|
|
180
|
+
];
|
|
181
|
+
for (const active of activeTargets) {
|
|
182
|
+
const protectedTarget = protectedTargets.find((entry) => pathsOverlap(active.target, entry.target));
|
|
183
|
+
if (!protectedTarget) continue;
|
|
184
|
+
throw new PluginRuntimeError(`活跃 Plugin 计划与冻结目标冲突:${active.target}`, {
|
|
185
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.CONTENT_CONFLICT,
|
|
186
|
+
path: active.target,
|
|
187
|
+
details: {
|
|
188
|
+
activeOwner: active.owner,
|
|
189
|
+
preservedOwner: protectedTarget.owner,
|
|
190
|
+
preservedTarget: protectedTarget.target,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
125
196
|
/**
|
|
126
197
|
* Flower Plugin 生命周期应用服务。
|
|
127
198
|
*/
|
|
@@ -156,7 +227,7 @@ export class PluginApplicationService {
|
|
|
156
227
|
/**
|
|
157
228
|
* 添加或更新一个直接 Plugin 声明并应用完整图。
|
|
158
229
|
*
|
|
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 添加选项
|
|
230
|
+
* @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 添加选项
|
|
160
231
|
* @returns {object} 生命周期结果
|
|
161
232
|
*/
|
|
162
233
|
add(options) {
|
|
@@ -188,6 +259,7 @@ export class PluginApplicationService {
|
|
|
188
259
|
approvals: options.approvals || [],
|
|
189
260
|
approvedDigests: options.approvedDigests,
|
|
190
261
|
nonInteractive: options.nonInteractive,
|
|
262
|
+
preserveIds: options.preserveIds || [],
|
|
191
263
|
onPreflight: options.onPreflight,
|
|
192
264
|
});
|
|
193
265
|
}
|
|
@@ -199,7 +271,7 @@ export class PluginApplicationService {
|
|
|
199
271
|
* 其锁定包也已不可重放,逐个更新会在其它节点上撞 `已锁定 Plugin 包不可重放`。
|
|
200
272
|
* 传入 `widen` 时按 `update: "all"` 解析,让全部节点都允许升级,一次性解开这种死锁。
|
|
201
273
|
*
|
|
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] 更新选项
|
|
274
|
+
* @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] 更新选项
|
|
203
275
|
* @returns {object} 生命周期结果
|
|
204
276
|
*/
|
|
205
277
|
update(options = {}) {
|
|
@@ -280,6 +352,7 @@ export class PluginApplicationService {
|
|
|
280
352
|
approvals: options.approvals || [],
|
|
281
353
|
approvedDigests: options.approvedDigests,
|
|
282
354
|
nonInteractive: options.nonInteractive,
|
|
355
|
+
preserveIds: options.preserveIds || [],
|
|
283
356
|
onPreflight: options.onPreflight,
|
|
284
357
|
});
|
|
285
358
|
}
|
|
@@ -397,10 +470,10 @@ export class PluginApplicationService {
|
|
|
397
470
|
}
|
|
398
471
|
}
|
|
399
472
|
if (lock) {
|
|
400
|
-
const
|
|
473
|
+
const { reachableIds } = classifyLockReachability(pluginsFile.plugins, lock);
|
|
401
474
|
for (const id of [...lockById.keys()].sort(compareUtf8)) {
|
|
402
475
|
if (options.id && id !== options.id) continue;
|
|
403
|
-
if (!
|
|
476
|
+
if (!reachableIds.has(id)) {
|
|
404
477
|
diagnostics.push({
|
|
405
478
|
code: "verify.lock-orphan",
|
|
406
479
|
path: id,
|
|
@@ -557,10 +630,39 @@ export class PluginApplicationService {
|
|
|
557
630
|
const pluginsFile = validatePluginsFile(input.plugins);
|
|
558
631
|
const previousLock = this.store.readLock();
|
|
559
632
|
const previousState = this.store.readState();
|
|
633
|
+
const preservedIds = new Set(input.preserveIds || []);
|
|
634
|
+
const previousLockById = new Map((previousLock?.plugins || []).map((plugin) => [plugin.id, plugin]));
|
|
635
|
+
const previousStateById = new Map((previousState?.plugins || []).map((plugin) => [plugin.id, plugin]));
|
|
560
636
|
const resolution = resolvePluginGraph(pluginsFile.plugins, this.registry, {
|
|
561
637
|
lockedPlugins: previousLock?.plugins || [],
|
|
562
638
|
update: input.update,
|
|
639
|
+
preserveIds: [...preservedIds],
|
|
563
640
|
});
|
|
641
|
+
const resolvedIds = new Set(resolution.graph.plugins.map(({ id }) => id));
|
|
642
|
+
const preservedEntries = [];
|
|
643
|
+
for (const id of [...preservedIds].sort(compareUtf8)) {
|
|
644
|
+
const previousLocked = previousLockById.get(id);
|
|
645
|
+
const previousEntry = previousStateById.get(id);
|
|
646
|
+
if (!previousLocked || !resolvedIds.has(id)) {
|
|
647
|
+
throw new PluginRuntimeError(`冻结 Plugin 缺少既有 lock:${id}`, {
|
|
648
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.TARGET_DRIFT,
|
|
649
|
+
path: id,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
if (!previousEntry) {
|
|
653
|
+
throw new PluginRuntimeError(`冻结 Plugin 缺少既有 state:${id}`, {
|
|
654
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.TARGET_DRIFT,
|
|
655
|
+
path: id,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
assertPreservedState(this.projectRoot, previousLocked, previousEntry);
|
|
659
|
+
preservedEntries.push(previousEntry);
|
|
660
|
+
}
|
|
661
|
+
const activeGraph = {
|
|
662
|
+
roots: resolution.graph.roots.filter((id) => !preservedIds.has(id)),
|
|
663
|
+
plugins: resolution.graph.plugins.filter(({ id }) => !preservedIds.has(id)),
|
|
664
|
+
};
|
|
665
|
+
const activeSelected = resolution.selected.filter(({ id }) => !preservedIds.has(id));
|
|
564
666
|
let projection = {
|
|
565
667
|
mutations: [],
|
|
566
668
|
payloads: new Map(),
|
|
@@ -569,47 +671,45 @@ export class PluginApplicationService {
|
|
|
569
671
|
directoryRemovals: [],
|
|
570
672
|
};
|
|
571
673
|
let platformSelection = { platforms: [], targets: [] };
|
|
572
|
-
if (
|
|
674
|
+
if (activeGraph.plugins.length > 0) {
|
|
573
675
|
const platforms = input.platforms.length > 0
|
|
574
676
|
? input.platforms
|
|
575
677
|
: statePlatforms(previousState);
|
|
576
678
|
platformSelection = this.platformDetector(this.projectRoot, platforms);
|
|
577
679
|
projection = projectPluginContent({
|
|
578
680
|
projectRoot: this.projectRoot,
|
|
579
|
-
graph:
|
|
580
|
-
selected:
|
|
681
|
+
graph: activeGraph,
|
|
682
|
+
selected: activeSelected,
|
|
581
683
|
registry: this.registry,
|
|
582
684
|
platformSelection,
|
|
583
685
|
previousState,
|
|
584
686
|
});
|
|
585
687
|
}
|
|
586
|
-
const preservedIds = new Set(input.preserveIds || []);
|
|
587
688
|
if (preservedIds.size > 0) {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
projection.
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
if (previousState?.migration) projection.state.migration = structuredClone(previousState.migration);
|
|
689
|
+
const projectedStateById = new Map(projection.state.plugins.map((plugin) => [plugin.id, plugin]));
|
|
690
|
+
projection.state.plugins = resolution.graph.plugins.map(({ id }) => {
|
|
691
|
+
if (preservedIds.has(id)) return structuredClone(previousStateById.get(id));
|
|
692
|
+
const projected = projectedStateById.get(id);
|
|
693
|
+
if (!projected) throw new Error(`Resolved Plugin 缺少投影 state:${id}`);
|
|
694
|
+
return projected;
|
|
695
|
+
});
|
|
696
|
+
resolution.graph.plugins = resolution.graph.plugins.map((plugin) => (
|
|
697
|
+
preservedIds.has(plugin.id)
|
|
698
|
+
? structuredClone(previousLockById.get(plugin.id))
|
|
699
|
+
: plugin
|
|
700
|
+
));
|
|
701
|
+
assertNoPreservedTargetConflicts(preservedEntries, {
|
|
702
|
+
mutations: projection.mutations,
|
|
703
|
+
directoryClaims: projection.directoryClaims,
|
|
704
|
+
directoryRemovals: projection.directoryRemovals,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
if (
|
|
708
|
+
preservedIds.has(SKILL_GARDEN_MIGRATION_OWNER_ID) &&
|
|
709
|
+
!projection.state.migration &&
|
|
710
|
+
previousState?.migration
|
|
711
|
+
) {
|
|
712
|
+
projection.state.migration = structuredClone(previousState.migration);
|
|
613
713
|
}
|
|
614
714
|
|
|
615
715
|
const previousDirectories = new Map();
|
|
@@ -619,7 +719,11 @@ export class PluginApplicationService {
|
|
|
619
719
|
}
|
|
620
720
|
}
|
|
621
721
|
const directoryClaims = [];
|
|
622
|
-
const desiredDirectoryPaths = new Set(
|
|
722
|
+
const desiredDirectoryPaths = new Set(
|
|
723
|
+
projection.state.plugins.flatMap((plugin) => plugin.paths
|
|
724
|
+
.filter(({ kind }) => kind === "directory")
|
|
725
|
+
.map(({ path: target }) => target)),
|
|
726
|
+
);
|
|
623
727
|
const directoryOwners = new Map();
|
|
624
728
|
for (const claim of projection.directoryClaims) {
|
|
625
729
|
desiredDirectoryPaths.add(claim.path);
|
|
@@ -665,6 +769,7 @@ export class PluginApplicationService {
|
|
|
665
769
|
if (desiredDirectoryPaths.has(entry.path)) continue;
|
|
666
770
|
const target = path.join(this.projectRoot, ...entry.path.split("/"));
|
|
667
771
|
const currentHash = hashDirectoryIfExists(target);
|
|
772
|
+
if (currentHash === null) continue;
|
|
668
773
|
if (currentHash !== entry.hash) {
|
|
669
774
|
throw new PluginRuntimeError(`受管 Plugin 目录已被用户修改,拒绝删除:${entry.path}`, {
|
|
670
775
|
code: PLUGIN_RUNTIME_ERROR_CODES.CONTENT_CONFLICT,
|
|
@@ -680,6 +785,7 @@ export class PluginApplicationService {
|
|
|
680
785
|
if (desiredPaths.has(entry.path)) continue;
|
|
681
786
|
const target = path.join(this.projectRoot, ...entry.path.split("/"));
|
|
682
787
|
const currentHash = hashFileIfExists(target);
|
|
788
|
+
if (currentHash === null) continue;
|
|
683
789
|
if (currentHash !== entry.hash) {
|
|
684
790
|
throw new PluginRuntimeError(`受管 Plugin 文件已被用户修改,拒绝删除:${entry.path}`, {
|
|
685
791
|
code: PLUGIN_RUNTIME_ERROR_CODES.CONTENT_CONFLICT,
|
|
@@ -697,6 +803,12 @@ export class PluginApplicationService {
|
|
|
697
803
|
});
|
|
698
804
|
}
|
|
699
805
|
}
|
|
806
|
+
if (preservedIds.size > 0) {
|
|
807
|
+
assertNoPreservedTargetConflicts(preservedEntries, {
|
|
808
|
+
mutations: removalMutations,
|
|
809
|
+
directoryRemovals,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
700
812
|
|
|
701
813
|
const patchPlanner = getPluginPatchPlanner();
|
|
702
814
|
let patchResult = standardContentPlan(resolution.graph);
|
|
@@ -774,6 +886,11 @@ export class PluginApplicationService {
|
|
|
774
886
|
});
|
|
775
887
|
}
|
|
776
888
|
}
|
|
889
|
+
if (preservedIds.size > 0) {
|
|
890
|
+
assertNoPreservedTargetConflicts(preservedEntries, {
|
|
891
|
+
patchMutations: patchResult.patchMutations,
|
|
892
|
+
});
|
|
893
|
+
}
|
|
777
894
|
const grants = new Map(patchResult.grants.map(({ pluginId, grant }) => [pluginId, grant]));
|
|
778
895
|
for (const plugin of resolution.graph.plugins) {
|
|
779
896
|
const grant = grants.get(plugin.id) || (
|
package/src/plugin/contracts.js
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { compareUtf8 } from "./stable-order.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 从当前直接声明出发,分类既有 lock 中的可达、孤立和缺失节点。
|
|
5
|
+
*
|
|
6
|
+
* lock roots 可能来自半清理的旧项目,不能作为活跃 Plugin 的事实入口。调用方应使用当前
|
|
7
|
+
* `plugins.json` 声明开始遍历,才能在不访问外部来源的前提下识别可冻结节点和孤立状态。
|
|
8
|
+
*
|
|
9
|
+
* @param {import("./contracts.js").ProjectPluginDeclaration[]} declarations 当前直接声明
|
|
10
|
+
* @param {import("./contracts.js").PluginLock|null} lock 既有 Plugin lock
|
|
11
|
+
* @returns {{reachableIds:Set<string>,orphanIds:Set<string>,missingIds:Set<string>}} lock 节点分类
|
|
12
|
+
*/
|
|
13
|
+
export function classifyLockReachability(declarations, lock) {
|
|
14
|
+
const plugins = lock?.plugins || [];
|
|
15
|
+
const byId = new Map(plugins.map((plugin) => [plugin.id, plugin]));
|
|
16
|
+
const visited = new Set();
|
|
17
|
+
const reachableIds = new Set();
|
|
18
|
+
const missingIds = new Set();
|
|
19
|
+
|
|
20
|
+
const visit = (id) => {
|
|
21
|
+
if (visited.has(id)) return;
|
|
22
|
+
visited.add(id);
|
|
23
|
+
const plugin = byId.get(id);
|
|
24
|
+
if (!plugin) {
|
|
25
|
+
missingIds.add(id);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
reachableIds.add(id);
|
|
29
|
+
Object.keys(plugin.dependencies).sort(compareUtf8).forEach(visit);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
declarations.map(({ id }) => id).sort(compareUtf8).forEach(visit);
|
|
33
|
+
const orphanIds = new Set(
|
|
34
|
+
plugins.map(({ id }) => id).filter((id) => !reachableIds.has(id)).sort(compareUtf8),
|
|
35
|
+
);
|
|
36
|
+
return { reachableIds, orphanIds, missingIds };
|
|
37
|
+
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
PLUGIN_RUNTIME_ERROR_CODES,
|
|
4
4
|
PluginRuntimeError,
|
|
5
5
|
} from "../runtime-errors.js";
|
|
6
|
+
import { parseCanonicalPluginId } from "../schemas/shared.js";
|
|
6
7
|
import { compareUtf8 } from "../stable-order.js";
|
|
7
8
|
import { normalizeContentSelection } from "../content-selection.js";
|
|
8
9
|
|
|
@@ -41,6 +42,40 @@ function defaultCapabilityGrant(request) {
|
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
/**
|
|
46
|
+
* 从旧 lock 合成只参与依赖求解的冻结候选。
|
|
47
|
+
*
|
|
48
|
+
* 冻结候选不代表本地仍有可读取的 Plugin 包,因此 content 只放合法占位值;调用方必须在
|
|
49
|
+
* 内容投影前剥离该候选,并原样复用旧 lock/state。
|
|
50
|
+
*
|
|
51
|
+
* @param {import("../contracts.js").ResolvedPlugin} locked 已锁定 Plugin
|
|
52
|
+
* @returns {import("../contracts.js").PluginCandidate} 冻结候选
|
|
53
|
+
*/
|
|
54
|
+
function createPreservedCandidate(locked) {
|
|
55
|
+
const { pluginId } = parseCanonicalPluginId(locked.id);
|
|
56
|
+
return {
|
|
57
|
+
id: locked.id,
|
|
58
|
+
version: locked.version,
|
|
59
|
+
source: structuredClone(locked.source),
|
|
60
|
+
commit: locked.commit,
|
|
61
|
+
integrity: locked.integrity,
|
|
62
|
+
manifest: {
|
|
63
|
+
schemaVersion: 1,
|
|
64
|
+
id: pluginId,
|
|
65
|
+
name: locked.id,
|
|
66
|
+
version: locked.version,
|
|
67
|
+
compatibility: structuredClone(locked.compatibility),
|
|
68
|
+
dependencies: structuredClone(locked.dependencies),
|
|
69
|
+
capabilities: {
|
|
70
|
+
profile: locked.capabilities.profile,
|
|
71
|
+
required: [...locked.capabilities.granted],
|
|
72
|
+
optional: [...locked.capabilities.denied],
|
|
73
|
+
},
|
|
74
|
+
content: { skills: [] },
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
44
79
|
/**
|
|
45
80
|
* 深复制约束映射。
|
|
46
81
|
*
|
|
@@ -162,7 +197,7 @@ function stableTopologicalOrder(roots, selected) {
|
|
|
162
197
|
*
|
|
163
198
|
* @param {import("../contracts.js").ProjectPluginDeclaration[]} declarations 直接声明
|
|
164
199
|
* @param {{listCandidates:(id:string)=>import("../contracts.js").PluginCandidate[]}} registry Source Registry
|
|
165
|
-
* @param {{lockedPlugins?:import("../contracts.js").ResolvedPlugin[],update?:string[]|"all",grantCapabilities?:(request:import("../contracts.js").CapabilityRequest,candidate:import("../contracts.js").PluginCandidate)=>import("../contracts.js").CapabilityGrant}} [options] 解析选项
|
|
200
|
+
* @param {{lockedPlugins?:import("../contracts.js").ResolvedPlugin[],update?:string[]|"all",preserveIds?:string[],grantCapabilities?:(request:import("../contracts.js").CapabilityRequest,candidate:import("../contracts.js").PluginCandidate)=>import("../contracts.js").CapabilityGrant}} [options] 解析选项
|
|
166
201
|
* @returns {{graph:import("../contracts.js").ResolvedGraph,selected:import("../contracts.js").PluginCandidate[],orphans:string[],constraints:Record<string,Array<{range:string,from:string}>>}} 解析结果
|
|
167
202
|
*/
|
|
168
203
|
export function resolvePluginGraph(declarations, registry, options = {}) {
|
|
@@ -172,10 +207,20 @@ export function resolvePluginGraph(declarations, registry, options = {}) {
|
|
|
172
207
|
const updateIds = options.update === "all"
|
|
173
208
|
? new Set(lockedPlugins.map(({ id }) => id))
|
|
174
209
|
: new Set(options.update || []);
|
|
210
|
+
const preservedIds = new Set(options.preserveIds || []);
|
|
175
211
|
const grantCapabilities = options.grantCapabilities || defaultCapabilityGrant;
|
|
176
212
|
const initialConstraints = new Map();
|
|
177
213
|
const rootDeclarations = new Map(declarations.map((declaration) => [declaration.id, declaration]));
|
|
178
214
|
|
|
215
|
+
for (const id of [...preservedIds].sort(compareUtf8)) {
|
|
216
|
+
if (!lockedById.has(id)) {
|
|
217
|
+
throw new PluginRuntimeError(`冻结 Plugin 缺少既有 lock:${id}`, {
|
|
218
|
+
code: PLUGIN_RUNTIME_ERROR_CODES.TARGET_DRIFT,
|
|
219
|
+
path: id,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
179
224
|
for (const declaration of [...declarations].sort((left, right) => compareUtf8(left.id, right.id))) {
|
|
180
225
|
const entries = initialConstraints.get(declaration.id) || [];
|
|
181
226
|
entries.push({ range: declaration.version, from: "project" });
|
|
@@ -201,11 +246,15 @@ export function resolvePluginGraph(declarations, registry, options = {}) {
|
|
|
201
246
|
if (!nextId) return { selected, constraints };
|
|
202
247
|
|
|
203
248
|
let candidates;
|
|
204
|
-
|
|
205
|
-
candidates =
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
249
|
+
if (preservedIds.has(nextId)) {
|
|
250
|
+
candidates = [createPreservedCandidate(lockedById.get(nextId))];
|
|
251
|
+
} else {
|
|
252
|
+
try {
|
|
253
|
+
candidates = registry.listCandidates(nextId);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (error?.code !== PLUGIN_RUNTIME_ERROR_CODES.SOURCE_NOT_FOUND) throw error;
|
|
256
|
+
candidates = [];
|
|
257
|
+
}
|
|
209
258
|
}
|
|
210
259
|
if (candidates.length === 0) {
|
|
211
260
|
throw new PluginRuntimeError(`找不到 Plugin 依赖:${nextId}`, {
|
|
@@ -298,7 +347,9 @@ export function resolvePluginGraph(declarations, registry, options = {}) {
|
|
|
298
347
|
integrity: candidate.integrity,
|
|
299
348
|
dependencies,
|
|
300
349
|
compatibility: candidate.manifest.compatibility,
|
|
301
|
-
capabilities:
|
|
350
|
+
capabilities: preservedIds.has(id)
|
|
351
|
+
? structuredClone(lockedById.get(id).capabilities)
|
|
352
|
+
: grantCapabilities(candidate.manifest.capabilities, candidate),
|
|
302
353
|
...(contentSelection ? { contentSelection } : {}),
|
|
303
354
|
};
|
|
304
355
|
});
|
|
@@ -20,6 +20,9 @@ import {
|
|
|
20
20
|
} from "./remote-archive.js";
|
|
21
21
|
|
|
22
22
|
const PROFILE_RANK = Object.freeze({ standard: 0, integration: 1, system: 2 });
|
|
23
|
+
const RD_GUIDE_SOURCE_ID = "rd-guide";
|
|
24
|
+
const RD_GUIDE_PROJECT = "digital-rd-governance/rd-guide";
|
|
25
|
+
const LEGACY_RD_GUIDE_MARKETPLACE_PATH = ".flower-marketplace/marketplace.json";
|
|
23
26
|
|
|
24
27
|
/**
|
|
25
28
|
* GitLab Marketplace Provider;网络阶段由 prepare() 显式触发。
|
|
@@ -178,6 +181,7 @@ export class GitLabSourceProvider {
|
|
|
178
181
|
inspected.version,
|
|
179
182
|
manifest,
|
|
180
183
|
inspected.indexCommit,
|
|
184
|
+
inspected.indexPath,
|
|
181
185
|
);
|
|
182
186
|
if (lockedPlugin) this.#assertLockedInspectionMatches(lockedPlugin, candidate);
|
|
183
187
|
return candidate;
|
|
@@ -191,6 +195,7 @@ export class GitLabSourceProvider {
|
|
|
191
195
|
*/
|
|
192
196
|
async prepareLocked(plugin) {
|
|
193
197
|
if (this.packageRoots.has(this.#key(plugin))) return;
|
|
198
|
+
const indexPath = this.#lockedMarketplacePath(plugin);
|
|
194
199
|
const cached = this.#findCachedPackage(plugin);
|
|
195
200
|
if (cached) {
|
|
196
201
|
this.#registerLockedCandidate(plugin, cached.root, cached.manifest, "standard");
|
|
@@ -198,7 +203,7 @@ export class GitLabSourceProvider {
|
|
|
198
203
|
}
|
|
199
204
|
const raw = await this.client.readRawFile(
|
|
200
205
|
this.source.project,
|
|
201
|
-
|
|
206
|
+
indexPath,
|
|
202
207
|
plugin.source.indexCommit,
|
|
203
208
|
);
|
|
204
209
|
let marketplace;
|
|
@@ -627,7 +632,7 @@ export class GitLabSourceProvider {
|
|
|
627
632
|
*
|
|
628
633
|
* @param {string} pluginId Plugin 本地 ID
|
|
629
634
|
* @param {string} requestedVersion 请求版本
|
|
630
|
-
* @returns {Promise<{entry:object,version:object,indexCommit:string}>} Marketplace 版本上下文
|
|
635
|
+
* @returns {Promise<{entry:object,version:object,indexCommit:string,indexPath:string}>} Marketplace 版本上下文
|
|
631
636
|
*/
|
|
632
637
|
async #marketplaceVersionForInspection(pluginId, requestedVersion) {
|
|
633
638
|
const marketplace = await this.prepareIndex();
|
|
@@ -639,7 +644,7 @@ export class GitLabSourceProvider {
|
|
|
639
644
|
path: `${this.id}/${pluginId}`,
|
|
640
645
|
});
|
|
641
646
|
}
|
|
642
|
-
return { entry, version, indexCommit: this.indexCommit };
|
|
647
|
+
return { entry, version, indexCommit: this.indexCommit, indexPath: this.source.marketplacePath };
|
|
643
648
|
}
|
|
644
649
|
|
|
645
650
|
/**
|
|
@@ -647,12 +652,13 @@ export class GitLabSourceProvider {
|
|
|
647
652
|
*
|
|
648
653
|
* @param {string} pluginId Plugin 本地 ID
|
|
649
654
|
* @param {import("../contracts.js").ResolvedPlugin} plugin 已锁定 Plugin
|
|
650
|
-
* @returns {Promise<{entry:object,version:object,indexCommit:string}>} Marketplace 版本上下文
|
|
655
|
+
* @returns {Promise<{entry:object,version:object,indexCommit:string,indexPath:string}>} Marketplace 版本上下文
|
|
651
656
|
*/
|
|
652
657
|
async #lockedMarketplaceVersionForInspection(pluginId, plugin) {
|
|
658
|
+
const indexPath = this.#lockedMarketplacePath(plugin);
|
|
653
659
|
const raw = await this.client.readRawFile(
|
|
654
660
|
this.source.project,
|
|
655
|
-
|
|
661
|
+
indexPath,
|
|
656
662
|
plugin.source.indexCommit,
|
|
657
663
|
);
|
|
658
664
|
let marketplace;
|
|
@@ -683,7 +689,7 @@ export class GitLabSourceProvider {
|
|
|
683
689
|
path: plugin.id,
|
|
684
690
|
});
|
|
685
691
|
}
|
|
686
|
-
return { entry, version, indexCommit: plugin.source.indexCommit };
|
|
692
|
+
return { entry, version, indexCommit: plugin.source.indexCommit, indexPath };
|
|
687
693
|
}
|
|
688
694
|
|
|
689
695
|
/**
|
|
@@ -700,7 +706,8 @@ export class GitLabSourceProvider {
|
|
|
700
706
|
lockedPlugin.commit.toLowerCase() !== candidate.commit ||
|
|
701
707
|
lockedPlugin.integrity !== candidate.integrity ||
|
|
702
708
|
lockedPlugin.source.reference !== candidate.source.reference ||
|
|
703
|
-
lockedPlugin.source.indexCommit !== candidate.source.indexCommit
|
|
709
|
+
lockedPlugin.source.indexCommit !== candidate.source.indexCommit ||
|
|
710
|
+
this.#lockedMarketplacePath(lockedPlugin) !== candidate.source.indexPath
|
|
704
711
|
) {
|
|
705
712
|
throw new PluginRuntimeError(`GitLab lock 与 Marketplace 不一致:${lockedPlugin.id}`, {
|
|
706
713
|
code: PLUGIN_RUNTIME_ERROR_CODES.TARGET_DRIFT,
|
|
@@ -717,9 +724,17 @@ export class GitLabSourceProvider {
|
|
|
717
724
|
* @param {object} version Marketplace 版本条目
|
|
718
725
|
* @param {object} manifest 已校验 Plugin manifest
|
|
719
726
|
* @param {string} [indexCommit] Marketplace index commit
|
|
727
|
+
* @param {string} [indexPath] Marketplace index path
|
|
720
728
|
* @returns {import("../contracts.js").PluginCandidate} Provider 候选
|
|
721
729
|
*/
|
|
722
|
-
#candidateFromMarketplaceVersion(
|
|
730
|
+
#candidateFromMarketplaceVersion(
|
|
731
|
+
canonicalId,
|
|
732
|
+
entry,
|
|
733
|
+
version,
|
|
734
|
+
manifest,
|
|
735
|
+
indexCommit = this.indexCommit,
|
|
736
|
+
indexPath = this.source.marketplacePath,
|
|
737
|
+
) {
|
|
723
738
|
const id = composeCanonicalPluginId(this.id, manifest.id);
|
|
724
739
|
if (id !== canonicalId || manifest.version !== version.version) {
|
|
725
740
|
throw new PluginRuntimeError(`远程 Plugin 身份与索引不一致:${canonicalId}@${version.version}`, {
|
|
@@ -741,6 +756,7 @@ export class GitLabSourceProvider {
|
|
|
741
756
|
type: "gitlab",
|
|
742
757
|
reference: this.#sourceReference(entry.source),
|
|
743
758
|
indexCommit,
|
|
759
|
+
indexPath: assertSafePosixRelativePath(indexPath, "Marketplace index path"),
|
|
744
760
|
},
|
|
745
761
|
commit: version.commit.toLowerCase(),
|
|
746
762
|
integrity: version.integrity,
|
|
@@ -825,7 +841,10 @@ export class GitLabSourceProvider {
|
|
|
825
841
|
const candidate = {
|
|
826
842
|
id: plugin.id,
|
|
827
843
|
version: plugin.version,
|
|
828
|
-
source:
|
|
844
|
+
source: {
|
|
845
|
+
...plugin.source,
|
|
846
|
+
indexPath: this.#lockedMarketplacePath(plugin),
|
|
847
|
+
},
|
|
829
848
|
commit: plugin.commit,
|
|
830
849
|
integrity: plugin.integrity,
|
|
831
850
|
manifest,
|
|
@@ -834,6 +853,28 @@ export class GitLabSourceProvider {
|
|
|
834
853
|
this.#registerCandidate(candidate, root);
|
|
835
854
|
}
|
|
836
855
|
|
|
856
|
+
/**
|
|
857
|
+
* 解析旧 lock 固定使用的 Marketplace 路径。
|
|
858
|
+
*
|
|
859
|
+
* 旧 RD Guide 独立 Plugin 只存在于历史 `.flower-marketplace` 索引;该映射同时限定来源
|
|
860
|
+
* ID、项目和 Plugin ID 族,避免对其它 GitLab Marketplace 做多路径探测或误判。
|
|
861
|
+
*
|
|
862
|
+
* @param {import("../contracts.js").ResolvedPlugin} plugin 已锁定 Plugin
|
|
863
|
+
* @returns {string} 安全的固定 Marketplace 索引路径
|
|
864
|
+
*/
|
|
865
|
+
#lockedMarketplacePath(plugin) {
|
|
866
|
+
const indexPath = plugin.source.indexPath || (
|
|
867
|
+
this.id === RD_GUIDE_SOURCE_ID &&
|
|
868
|
+
this.source.project === RD_GUIDE_PROJECT &&
|
|
869
|
+
plugin.source.id === RD_GUIDE_SOURCE_ID &&
|
|
870
|
+
plugin.source.reference === RD_GUIDE_PROJECT &&
|
|
871
|
+
plugin.id.startsWith(`${RD_GUIDE_SOURCE_ID}/xhgj-`)
|
|
872
|
+
? LEGACY_RD_GUIDE_MARKETPLACE_PATH
|
|
873
|
+
: this.source.marketplacePath
|
|
874
|
+
);
|
|
875
|
+
return assertSafePosixRelativePath(indexPath, "Marketplace index path");
|
|
876
|
+
}
|
|
877
|
+
|
|
837
878
|
/**
|
|
838
879
|
* 登记已准备固定包候选。
|
|
839
880
|
*
|