flower-trellis 0.6.3 → 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flower-trellis",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "一键安装/升级 Trellis 并自动融合 skill-garden 强化包(默认 Claude + agents)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -70,9 +70,9 @@
|
|
|
70
70
|
"commit-and-tag-version": "^12.7.3"
|
|
71
71
|
},
|
|
72
72
|
"flowerReleaseNotes": {
|
|
73
|
-
"version": "0.6.
|
|
73
|
+
"version": "0.6.4",
|
|
74
74
|
"source": "CHANGELOG.md",
|
|
75
|
-
"body": "###
|
|
75
|
+
"body": "### 🐛 修复 Bug Fixes\n\n* **plugin:** 支持离线冻结未更新的外部 Plugin ([6210cfa](https://github.com/SilentFlower/flower-trellis/commit/6210cfae8d459a3e7b105043e4ecbc23d1b5bed9))",
|
|
76
76
|
"truncated": false
|
|
77
77
|
},
|
|
78
78
|
"optionalDependencies": {
|
|
@@ -537,12 +537,15 @@ export async function runPluginManagementCommand(parsed, ctx, options, output) {
|
|
|
537
537
|
/**
|
|
538
538
|
* 为生命周期命令登记所需的远程 Source Provider。
|
|
539
539
|
*
|
|
540
|
-
* @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 远程准备上下文
|
|
541
541
|
* @returns {Promise<void>} 登记完成
|
|
542
542
|
*/
|
|
543
|
-
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);
|
|
544
545
|
const remoteIds = new Set(["add", "update", "remove", "verify", "replay"].includes(parsed.command)
|
|
545
|
-
? (lock?.plugins || [])
|
|
546
|
+
? (lock?.plugins || [])
|
|
547
|
+
.filter(({ id, source }) => !preserved.has(id) && ["gitlab", "github"].includes(source.type))
|
|
548
|
+
.map(({ source }) => source.id)
|
|
546
549
|
: []);
|
|
547
550
|
let sourceStore = null;
|
|
548
551
|
let configuredSources = null;
|
|
@@ -584,6 +587,15 @@ export async function registerRemotePluginSources({ parsed, projectRoot, options
|
|
|
584
587
|
remoteIds.add(simpleSourceOption);
|
|
585
588
|
}
|
|
586
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
|
+
}
|
|
587
599
|
remoteIds.delete("local");
|
|
588
600
|
if (remoteIds.size === 0) return;
|
|
589
601
|
|
|
@@ -628,25 +640,57 @@ export async function registerRemotePluginSources({ parsed, projectRoot, options
|
|
|
628
640
|
/**
|
|
629
641
|
* 准备远程 Plugin 候选及固定 lock 缓存。
|
|
630
642
|
*
|
|
631
|
-
* @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 候选准备上下文
|
|
632
644
|
* @returns {Promise<void>} 准备完成
|
|
633
645
|
*/
|
|
634
|
-
export async function prepareRemotePluginCandidates({ parsed, canonicalId, registry, lock }) {
|
|
646
|
+
export async function prepareRemotePluginCandidates({ parsed, canonicalId, registry, lock, preserveIds = [] }) {
|
|
635
647
|
if (parsed.command === "add" && registry.has(parseCanonicalPluginId(canonicalId).sourceId)) {
|
|
636
|
-
await prepareRemoteLock(registry, lock);
|
|
637
|
-
await prepareRemoteClosure(registry, [canonicalId]);
|
|
648
|
+
await prepareRemoteLock(registry, lock, preserveIds);
|
|
649
|
+
await prepareRemoteClosure(registry, [canonicalId], preserveIds);
|
|
638
650
|
} else if (parsed.command === "update") {
|
|
639
651
|
// 更新单一 Plugin 时,解析器仍会 lock-first 重放完整图;先恢复全部远程固定包,
|
|
640
652
|
// 再只为本轮显式更新目标加载新候选,避免顺带升级其它外部 Plugin。
|
|
641
|
-
await prepareRemoteLock(registry, lock);
|
|
653
|
+
await prepareRemoteLock(registry, lock, preserveIds);
|
|
642
654
|
const updateIds = canonicalId
|
|
643
655
|
? [canonicalId]
|
|
644
656
|
: (lock?.plugins || []).filter(({ source }) => ["gitlab", "github"].includes(source.type)).map(({ id }) => id);
|
|
645
|
-
await prepareRemoteClosure(registry, updateIds);
|
|
657
|
+
await prepareRemoteClosure(registry, updateIds, preserveIds);
|
|
646
658
|
assertExternalVersionsNotReused(registry, lock, updateIds);
|
|
647
659
|
} else if (["remove", "verify", "replay"].includes(parsed.command)) {
|
|
648
|
-
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
|
+
}
|
|
649
692
|
}
|
|
693
|
+
return [...sourceIds].sort(compareUtf8);
|
|
650
694
|
}
|
|
651
695
|
|
|
652
696
|
/**
|
|
@@ -654,20 +698,21 @@ export async function prepareRemotePluginCandidates({ parsed, canonicalId, regis
|
|
|
654
698
|
*
|
|
655
699
|
* @param {object} registry Provider 注册表
|
|
656
700
|
* @param {string[]} initialIds 初始 canonical ID
|
|
701
|
+
* @param {string[]} [preserveIds] 不读取来源的冻结 Plugin ID
|
|
657
702
|
* @returns {Promise<void>} 准备完成
|
|
658
703
|
*/
|
|
659
|
-
async function prepareRemoteClosure(registry, initialIds) {
|
|
704
|
+
async function prepareRemoteClosure(registry, initialIds, preserveIds = []) {
|
|
705
|
+
const preserved = new Set(preserveIds);
|
|
660
706
|
const pending = [...new Set(initialIds)];
|
|
661
707
|
const visited = new Set();
|
|
662
708
|
while (pending.length > 0) {
|
|
663
709
|
const id = pending.shift();
|
|
664
|
-
if (visited.has(id)) continue;
|
|
710
|
+
if (visited.has(id) || preserved.has(id)) continue;
|
|
665
711
|
visited.add(id);
|
|
666
712
|
const { sourceId } = parseCanonicalPluginId(id);
|
|
667
713
|
if (!registry.has(sourceId)) continue;
|
|
668
714
|
const provider = registry.get(sourceId);
|
|
669
|
-
if (typeof provider.prepare
|
|
670
|
-
await provider.prepare(id);
|
|
715
|
+
if (typeof provider.prepare === "function") await provider.prepare(id);
|
|
671
716
|
for (const candidate of provider.listCandidates(id)) {
|
|
672
717
|
pending.push(...Object.keys(candidate.manifest.dependencies || {}));
|
|
673
718
|
}
|
|
@@ -679,11 +724,17 @@ async function prepareRemoteClosure(registry, initialIds) {
|
|
|
679
724
|
*
|
|
680
725
|
* @param {object} registry Provider 注册表
|
|
681
726
|
* @param {import("../plugin/contracts.js").PluginLock|null} lock 当前 lock
|
|
727
|
+
* @param {string[]} [preserveIds] 不读取来源的冻结 Plugin ID
|
|
682
728
|
* @returns {Promise<void>} 恢复完成
|
|
683
729
|
*/
|
|
684
|
-
async function prepareRemoteLock(registry, lock) {
|
|
730
|
+
async function prepareRemoteLock(registry, lock, preserveIds = []) {
|
|
731
|
+
const preserved = new Set(preserveIds);
|
|
685
732
|
for (const plugin of lock?.plugins || []) {
|
|
686
|
-
if (
|
|
733
|
+
if (
|
|
734
|
+
preserved.has(plugin.id) ||
|
|
735
|
+
!["gitlab", "github"].includes(plugin.source.type) ||
|
|
736
|
+
!registry.has(plugin.source.id)
|
|
737
|
+
) continue;
|
|
687
738
|
const provider = registry.get(plugin.source.id);
|
|
688
739
|
if (typeof provider.prepareLocked === "function") await provider.prepareLocked(plugin);
|
|
689
740
|
else if (typeof provider.prepare === "function") await provider.prepare(plugin.id);
|
package/src/commands/plugin.js
CHANGED
|
@@ -747,7 +747,7 @@ function pluginExitCode(error) {
|
|
|
747
747
|
* 执行 Plugin 生命周期命令。
|
|
748
748
|
*
|
|
749
749
|
* @param {object} ctx cli-args.js 的执行上下文
|
|
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,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、输出与交互确认注入
|
|
751
751
|
* @returns {Promise<number>} 进程退出码
|
|
752
752
|
*/
|
|
753
753
|
export async function plugin(ctx, options = {}) {
|
|
@@ -834,6 +834,7 @@ export async function plugin(ctx, options = {}) {
|
|
|
834
834
|
options,
|
|
835
835
|
registry,
|
|
836
836
|
lock,
|
|
837
|
+
preserveIds: options.preserveIds || [],
|
|
837
838
|
});
|
|
838
839
|
|
|
839
840
|
if (parsed.command !== "add" && canonicalId && !canonicalId.includes("/")) {
|
|
@@ -889,7 +890,13 @@ export async function plugin(ctx, options = {}) {
|
|
|
889
890
|
}));
|
|
890
891
|
}
|
|
891
892
|
|
|
892
|
-
await remoteRuntime?.prepareRemotePluginCandidates({
|
|
893
|
+
await remoteRuntime?.prepareRemotePluginCandidates({
|
|
894
|
+
parsed,
|
|
895
|
+
canonicalId,
|
|
896
|
+
registry,
|
|
897
|
+
lock,
|
|
898
|
+
preserveIds: options.preserveIds || [],
|
|
899
|
+
});
|
|
893
900
|
|
|
894
901
|
const service = new PluginApplicationService(ctx.target, { registry, store });
|
|
895
902
|
let result;
|
|
@@ -906,6 +913,7 @@ export async function plugin(ctx, options = {}) {
|
|
|
906
913
|
platforms: parsed.platforms,
|
|
907
914
|
...(parsed.contentSelection ? { contentSelection: parsed.contentSelection } : {}),
|
|
908
915
|
dryRun: parsed.dryRun,
|
|
916
|
+
preserveIds: options.preserveIds || [],
|
|
909
917
|
onPreflight: options.onPreflight,
|
|
910
918
|
};
|
|
911
919
|
result = parsed.dryRun ? service.add(addOptions) : await executeWithCapabilityApproval(
|
|
@@ -925,6 +933,7 @@ export async function plugin(ctx, options = {}) {
|
|
|
925
933
|
platforms: parsed.platforms,
|
|
926
934
|
...(parsed.contentSelection ? { contentSelection: parsed.contentSelection } : {}),
|
|
927
935
|
dryRun: parsed.dryRun,
|
|
936
|
+
preserveIds: options.preserveIds || [],
|
|
928
937
|
onPreflight: options.onPreflight,
|
|
929
938
|
};
|
|
930
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,
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
selectContentSkillEntries,
|
|
23
23
|
} from "./content-selection.js";
|
|
24
24
|
|
|
25
|
+
const SKILL_GARDEN_MIGRATION_OWNER_ID = "flower/skill-garden";
|
|
26
|
+
|
|
25
27
|
/**
|
|
26
28
|
* 计算 lock roots 可达的全部 Plugin。
|
|
27
29
|
*
|
|
@@ -122,6 +124,94 @@ function standardContentPlan(graph) {
|
|
|
122
124
|
};
|
|
123
125
|
}
|
|
124
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
|
+
|
|
125
215
|
/**
|
|
126
216
|
* Flower Plugin 生命周期应用服务。
|
|
127
217
|
*/
|
|
@@ -156,7 +246,7 @@ export class PluginApplicationService {
|
|
|
156
246
|
/**
|
|
157
247
|
* 添加或更新一个直接 Plugin 声明并应用完整图。
|
|
158
248
|
*
|
|
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 添加选项
|
|
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 添加选项
|
|
160
250
|
* @returns {object} 生命周期结果
|
|
161
251
|
*/
|
|
162
252
|
add(options) {
|
|
@@ -188,6 +278,7 @@ export class PluginApplicationService {
|
|
|
188
278
|
approvals: options.approvals || [],
|
|
189
279
|
approvedDigests: options.approvedDigests,
|
|
190
280
|
nonInteractive: options.nonInteractive,
|
|
281
|
+
preserveIds: options.preserveIds || [],
|
|
191
282
|
onPreflight: options.onPreflight,
|
|
192
283
|
});
|
|
193
284
|
}
|
|
@@ -199,7 +290,7 @@ export class PluginApplicationService {
|
|
|
199
290
|
* 其锁定包也已不可重放,逐个更新会在其它节点上撞 `已锁定 Plugin 包不可重放`。
|
|
200
291
|
* 传入 `widen` 时按 `update: "all"` 解析,让全部节点都允许升级,一次性解开这种死锁。
|
|
201
292
|
*
|
|
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] 更新选项
|
|
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] 更新选项
|
|
203
294
|
* @returns {object} 生命周期结果
|
|
204
295
|
*/
|
|
205
296
|
update(options = {}) {
|
|
@@ -280,6 +371,7 @@ export class PluginApplicationService {
|
|
|
280
371
|
approvals: options.approvals || [],
|
|
281
372
|
approvedDigests: options.approvedDigests,
|
|
282
373
|
nonInteractive: options.nonInteractive,
|
|
374
|
+
preserveIds: options.preserveIds || [],
|
|
283
375
|
onPreflight: options.onPreflight,
|
|
284
376
|
});
|
|
285
377
|
}
|
|
@@ -557,10 +649,39 @@ export class PluginApplicationService {
|
|
|
557
649
|
const pluginsFile = validatePluginsFile(input.plugins);
|
|
558
650
|
const previousLock = this.store.readLock();
|
|
559
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]));
|
|
560
655
|
const resolution = resolvePluginGraph(pluginsFile.plugins, this.registry, {
|
|
561
656
|
lockedPlugins: previousLock?.plugins || [],
|
|
562
657
|
update: input.update,
|
|
658
|
+
preserveIds: [...preservedIds],
|
|
563
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));
|
|
564
685
|
let projection = {
|
|
565
686
|
mutations: [],
|
|
566
687
|
payloads: new Map(),
|
|
@@ -569,47 +690,45 @@ export class PluginApplicationService {
|
|
|
569
690
|
directoryRemovals: [],
|
|
570
691
|
};
|
|
571
692
|
let platformSelection = { platforms: [], targets: [] };
|
|
572
|
-
if (
|
|
693
|
+
if (activeGraph.plugins.length > 0) {
|
|
573
694
|
const platforms = input.platforms.length > 0
|
|
574
695
|
? input.platforms
|
|
575
696
|
: statePlatforms(previousState);
|
|
576
697
|
platformSelection = this.platformDetector(this.projectRoot, platforms);
|
|
577
698
|
projection = projectPluginContent({
|
|
578
699
|
projectRoot: this.projectRoot,
|
|
579
|
-
graph:
|
|
580
|
-
selected:
|
|
700
|
+
graph: activeGraph,
|
|
701
|
+
selected: activeSelected,
|
|
581
702
|
registry: this.registry,
|
|
582
703
|
platformSelection,
|
|
583
704
|
previousState,
|
|
584
705
|
});
|
|
585
706
|
}
|
|
586
|
-
const preservedIds = new Set(input.preserveIds || []);
|
|
587
707
|
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);
|
|
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);
|
|
613
732
|
}
|
|
614
733
|
|
|
615
734
|
const previousDirectories = new Map();
|
|
@@ -619,7 +738,11 @@ export class PluginApplicationService {
|
|
|
619
738
|
}
|
|
620
739
|
}
|
|
621
740
|
const directoryClaims = [];
|
|
622
|
-
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
|
+
);
|
|
623
746
|
const directoryOwners = new Map();
|
|
624
747
|
for (const claim of projection.directoryClaims) {
|
|
625
748
|
desiredDirectoryPaths.add(claim.path);
|
|
@@ -697,6 +820,12 @@ export class PluginApplicationService {
|
|
|
697
820
|
});
|
|
698
821
|
}
|
|
699
822
|
}
|
|
823
|
+
if (preservedIds.size > 0) {
|
|
824
|
+
assertNoPreservedTargetConflicts(preservedEntries, {
|
|
825
|
+
mutations: removalMutations,
|
|
826
|
+
directoryRemovals,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
700
829
|
|
|
701
830
|
const patchPlanner = getPluginPatchPlanner();
|
|
702
831
|
let patchResult = standardContentPlan(resolution.graph);
|
|
@@ -774,6 +903,11 @@ export class PluginApplicationService {
|
|
|
774
903
|
});
|
|
775
904
|
}
|
|
776
905
|
}
|
|
906
|
+
if (preservedIds.size > 0) {
|
|
907
|
+
assertNoPreservedTargetConflicts(preservedEntries, {
|
|
908
|
+
patchMutations: patchResult.patchMutations,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
777
911
|
const grants = new Map(patchResult.grants.map(({ pluginId, grant }) => [pluginId, grant]));
|
|
778
912
|
for (const plugin of resolution.graph.plugins) {
|
|
779
913
|
const grant = grants.get(plugin.id) || (
|
|
@@ -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
|
});
|