@birdie_moblie/open_spec 2.1.7 → 2.2.0
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/ext.js +140 -21
- package/dist/cli/commands/init.js +22 -12
- package/dist/config/load.d.ts +4 -0
- package/dist/config/load.js +23 -5
- package/dist/config/preset-repo-tools.d.ts +36 -0
- package/dist/config/preset-repo-tools.js +114 -0
- package/dist/config/preset-repo.d.ts +164 -0
- package/dist/config/preset-repo.js +180 -0
- package/dist/config/preset-use.d.ts +79 -0
- package/dist/config/preset-use.js +282 -0
- package/dist/config/presets.d.ts +38 -113
- package/dist/config/presets.js +159 -84
- package/dist/config/schema.d.ts +136 -9
- package/dist/config/schema.js +30 -2
- package/dist/core/source-operations.js +6 -1
- package/dist/index.d.ts +15 -3
- package/dist/index.js +22 -28
- package/dist/marketplace/registry.d.ts +2 -2
- package/dist/marketplace/registry.js +6 -55
- package/dist/marketplace/upgrade.js +5 -3
- package/dist/shared/fs.d.ts +2 -0
- package/dist/shared/fs.js +6 -2
- package/dist/shared/git-snapshot.d.ts +18 -0
- package/dist/shared/git-snapshot.js +75 -0
- package/dist/shared/pkg.js +1 -1
- package/docs/getting-started.md +1 -1
- package/docs/guides/configuration.md +9 -2
- package/docs/guides/plugin-migration.md +5 -5
- package/docs/guides/ui-conventions-template.md +2 -2
- package/package.json +1 -2
- package/plugin.json +1 -1
- package/presets/backend-service.yaml +0 -19
- package/presets/flutter-mobile/plugin-hooks.json +0 -68
- package/presets/flutter-mobile/ui-conventions.md +0 -122
- package/presets/flutter-mobile.yaml +0 -205
- package/presets/web-product.yaml +0 -23
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
import { isMap, isScalar, parseDocument, stringify as stringifyYaml } from 'yaml';
|
|
4
|
+
import { exists, readText, removePath, writeText, writeTextAtomic } from '../shared/fs.js';
|
|
5
|
+
import { AppError } from '../shared/errors.js';
|
|
6
|
+
import { compareVersions } from '../shared/version.js';
|
|
7
|
+
import { configPathOf, loadConfig, loadProjectConfig } from './load.js';
|
|
8
|
+
import { listChangeNames } from '../core/change.js';
|
|
9
|
+
import { diffBaselines, resolveEffectiveConfig, scaffoldFrom } from './presets.js';
|
|
10
|
+
import { fetchPresetRepo, presetBaseline, presetLockPath, presetRootForProject, readRepoPreset, writePresetLock } from './preset-repo.js';
|
|
11
|
+
import { effectiveTaskCategories, HOOK_STAGES } from './schema.js';
|
|
12
|
+
import { resolveInside } from '../shared/paths.js';
|
|
13
|
+
/** 在已有 config.yaml 中写入 preset 引用;保留其余内容与注释,新键放在 schema 之后。 */
|
|
14
|
+
function writePresetReference(configFile, original, reference) {
|
|
15
|
+
const document = parseDocument(original);
|
|
16
|
+
if (document.has('preset'))
|
|
17
|
+
document.set('preset', reference);
|
|
18
|
+
else if (isMap(document.contents)) {
|
|
19
|
+
const map = document.contents;
|
|
20
|
+
const schemaIndex = map.items.findIndex((pair) => isScalar(pair.key) && pair.key.value === 'schema');
|
|
21
|
+
map.items.splice(schemaIndex + 1, 0, document.createPair('preset', reference));
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
document.set('preset', reference);
|
|
25
|
+
}
|
|
26
|
+
writeText(configFile, document.toString());
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 执行会改 config 与锁的步骤(参数为 config 原文);任何一步失败都按原字节还原两者再抛出。
|
|
30
|
+
* 锁是 JSON,按规范用同目录临时文件加 rename 写回。
|
|
31
|
+
*/
|
|
32
|
+
function withRollback(configFile, lockFile, action) {
|
|
33
|
+
const original = { config: readText(configFile), lock: exists(lockFile) ? readText(lockFile) : undefined };
|
|
34
|
+
try {
|
|
35
|
+
return action(original.config);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
writeText(configFile, original.config);
|
|
39
|
+
if (original.lock === undefined)
|
|
40
|
+
removePath(lockFile);
|
|
41
|
+
else
|
|
42
|
+
writeTextAtomic(lockFile, original.lock);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 让项目引用 preset 仓库中的一个 preset:拉取并锁定快照,写入引用,确认合成后的生效配置合法,再生成模板文件。
|
|
48
|
+
* 任何一步失败都按原字节还原 config 与锁。
|
|
49
|
+
*/
|
|
50
|
+
export function usePreset(input) {
|
|
51
|
+
const { projectRoot, id, repo, ref } = input;
|
|
52
|
+
const configFile = configPathOf(projectRoot);
|
|
53
|
+
if (!configFile)
|
|
54
|
+
throw new AppError('project_uninitialized', '缺少 openspec/config.yaml,请先运行 openspec init');
|
|
55
|
+
const snapshot = fetchPresetRepo(repo, ref);
|
|
56
|
+
const preset = readRepoPreset(snapshot.cachePath, id);
|
|
57
|
+
const scaffold = withRollback(configFile, presetLockPath(projectRoot), (original) => {
|
|
58
|
+
writePresetLock(projectRoot, { version: 1, repo, ref, id, resolvedSha: snapshot.resolvedSha, digest: snapshot.digest });
|
|
59
|
+
writePresetReference(configFile, original, { repo, ref, id });
|
|
60
|
+
loadConfig(projectRoot);
|
|
61
|
+
return scaffoldFrom(projectRoot, path.join(snapshot.cachePath, id), id, preset.scaffold ?? []);
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
id, repo, ref, resolvedSha: snapshot.resolvedSha, digest: snapshot.digest, scaffold,
|
|
65
|
+
notes: preset.notes ?? [], warnings: preset.warnings ?? [],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** 生效配置中 diffBaselines 所指的一项;分类取含默认分类与删除语义后的结果。 */
|
|
69
|
+
function effectiveItem(config, section, id) {
|
|
70
|
+
switch (section) {
|
|
71
|
+
case 'taskCategories': return effectiveTaskCategories(config)[id];
|
|
72
|
+
case 'hooks': return config.hooks?.find((hook) => hook.id === id);
|
|
73
|
+
case 'plugins': return config.plugins?.find((item) => `${item.name}@${item.marketplace}` === id);
|
|
74
|
+
case 'marketplaces': return config.marketplaces?.find((item) => item.name === id);
|
|
75
|
+
case 'verify': {
|
|
76
|
+
const [group = '', field = ''] = id.split('.');
|
|
77
|
+
return config.verify?.[group]?.[field];
|
|
78
|
+
}
|
|
79
|
+
default: return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 把项目引用的 preset 升级到新 ref:列出基线的新增、修改与删除;按升级前后的生效配置,
|
|
84
|
+
* 生效值不变的条目即被项目层遮住(覆盖、禁用或更高的插件最低版本),单独列出。
|
|
85
|
+
* 只改 config 中的 ref 值并更新锁,确认生效配置合法后补齐新模板。任何一步失败都按原字节还原 config 与锁。
|
|
86
|
+
*/
|
|
87
|
+
export function upgradePreset(input) {
|
|
88
|
+
const { projectRoot, ref } = input;
|
|
89
|
+
const configFile = configPathOf(projectRoot);
|
|
90
|
+
const layer = loadProjectConfig(projectRoot);
|
|
91
|
+
if (!configFile || !layer.preset)
|
|
92
|
+
throw new AppError('preset_not_configured', '项目没有引用 preset;先运行 openspec preset use');
|
|
93
|
+
const active = listChangeNames(projectRoot);
|
|
94
|
+
if (active.length > 0 && !input.allowActiveChanges) {
|
|
95
|
+
throw new AppError('active_changes_present', `存在进行中的 change:${active.join(', ')}。它们按旧规则生成计划与回执,升级后需要重新确认;确认后加 --allow-active-changes`);
|
|
96
|
+
}
|
|
97
|
+
const reference = layer.preset;
|
|
98
|
+
const current = presetRootForProject(projectRoot, reference);
|
|
99
|
+
const previous = presetBaseline(readRepoPreset(current.root, reference.id));
|
|
100
|
+
const snapshot = fetchPresetRepo(reference.repo, ref);
|
|
101
|
+
const preset = readRepoPreset(snapshot.cachePath, reference.id);
|
|
102
|
+
const next = presetBaseline(preset);
|
|
103
|
+
const changes = diffBaselines(previous.patch, next.patch);
|
|
104
|
+
const before = resolveEffectiveConfig(layer, previous);
|
|
105
|
+
const after = resolveEffectiveConfig(layer, next);
|
|
106
|
+
const shadowed = Object.entries(changes).flatMap(([section, diff]) => [...diff.added, ...diff.changed, ...diff.removed]
|
|
107
|
+
.filter((id) => same(effectiveItem(before, section, id), effectiveItem(after, section, id)))
|
|
108
|
+
.map((id) => ({ section, id })));
|
|
109
|
+
const from = { ref: reference.ref, resolvedSha: current.lock.resolvedSha };
|
|
110
|
+
const to = { ref, resolvedSha: snapshot.resolvedSha };
|
|
111
|
+
if (ref === reference.ref && snapshot.resolvedSha === current.lock.resolvedSha) {
|
|
112
|
+
return { id: reference.id, status: 'unchanged', from, to, changes, shadowed, scaffold: [] };
|
|
113
|
+
}
|
|
114
|
+
const scaffold = withRollback(configFile, presetLockPath(projectRoot), (original) => {
|
|
115
|
+
writePresetLock(projectRoot, { version: 1, ...reference, ref, resolvedSha: snapshot.resolvedSha, digest: snapshot.digest });
|
|
116
|
+
const document = parseDocument(original);
|
|
117
|
+
// 改原 Scalar 的值而不是替换节点,保留写在 ref 行上的注释。
|
|
118
|
+
const node = document.getIn(['preset', 'ref'], true);
|
|
119
|
+
if (isScalar(node))
|
|
120
|
+
node.value = ref;
|
|
121
|
+
else
|
|
122
|
+
document.setIn(['preset', 'ref'], ref);
|
|
123
|
+
writeText(configFile, document.toString());
|
|
124
|
+
loadConfig(projectRoot);
|
|
125
|
+
return scaffoldFrom(projectRoot, path.join(snapshot.cachePath, reference.id), reference.id, preset.scaffold ?? []);
|
|
126
|
+
});
|
|
127
|
+
return { id: reference.id, status: 'upgraded', from, to, changes, shadowed, scaffold };
|
|
128
|
+
}
|
|
129
|
+
const same = (a, b) => isDeepStrictEqual(a, b);
|
|
130
|
+
/**
|
|
131
|
+
* 生效配置的等价比较:hook 按阶段分组比较组内顺序(同阶段的注入顺序有语义,跨阶段无关),
|
|
132
|
+
* 插件(不含最低版本,另行比较)与 marketplace 按集合比较,分类按键比较,验收设置忽略空对象。
|
|
133
|
+
*/
|
|
134
|
+
function comparable(config) {
|
|
135
|
+
const bySelector = (items = []) => [...items].sort((a, b) => `${a.name}@${a.marketplace ?? ''}`.localeCompare(`${b.name}@${b.marketplace ?? ''}`));
|
|
136
|
+
return {
|
|
137
|
+
taskCategories: effectiveTaskCategories(config),
|
|
138
|
+
hooks: Object.fromEntries(HOOK_STAGES.map((stage) => [stage, (config.hooks ?? []).filter((hook) => hook.stage === stage)])),
|
|
139
|
+
verify: nonEmptyVerify(config.verify) ?? {},
|
|
140
|
+
plugins: bySelector((config.plugins ?? []).map(({ name, marketplace }) => ({ name, marketplace }))),
|
|
141
|
+
marketplaces: bySelector(config.marketplaces),
|
|
142
|
+
tools: config.tools ?? [],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** 去掉验收设置里的空对象;全空时视为未设置。 */
|
|
146
|
+
function nonEmptyVerify(verify) {
|
|
147
|
+
const entries = Object.entries(verify ?? {}).filter(([, value]) => value && Object.keys(value).length > 0);
|
|
148
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* preset 的 minVersion 只能提高、不能被项目层调低,迁移后插件最低版本可能变严。
|
|
152
|
+
* 返回提高了的插件;出现变宽松(理论上不会发生)时返回 null,按不等价处理。
|
|
153
|
+
*/
|
|
154
|
+
function raisedMinVersions(before, after) {
|
|
155
|
+
const raised = [];
|
|
156
|
+
for (const plugin of after.plugins ?? []) {
|
|
157
|
+
const previous = (before.plugins ?? []).find((item) => item.name === plugin.name && item.marketplace === plugin.marketplace);
|
|
158
|
+
if (previous?.minVersion === plugin.minVersion)
|
|
159
|
+
continue;
|
|
160
|
+
if (!plugin.minVersion)
|
|
161
|
+
return null;
|
|
162
|
+
if (previous?.minVersion && (compareVersions(plugin.minVersion, previous.minVersion) ?? -1) < 0)
|
|
163
|
+
return null;
|
|
164
|
+
raised.push({ plugin: `${plugin.name}@${plugin.marketplace}`, ...(previous?.minVersion ? { from: previous.minVersion } : {}), to: plugin.minVersion });
|
|
165
|
+
}
|
|
166
|
+
return raised;
|
|
167
|
+
}
|
|
168
|
+
/** 验收设置按字段合成,迁移同样按字段只保留与基线不同的值。 */
|
|
169
|
+
function verifyOverrides(current, base) {
|
|
170
|
+
const baseFields = (base ?? {});
|
|
171
|
+
const groups = Object.entries((current ?? {}))
|
|
172
|
+
.map(([group, fields]) => [group, Object.fromEntries(Object.entries(fields ?? {})
|
|
173
|
+
.filter(([field, value]) => !same(value, baseFields[group]?.[field])))])
|
|
174
|
+
.filter(([, fields]) => Object.keys(fields).length > 0);
|
|
175
|
+
return groups.length > 0 ? Object.fromEntries(groups) : undefined;
|
|
176
|
+
}
|
|
177
|
+
/** 分层后各阶段 hook 顺序与原配置不同时,用 hookOrder 固定为原顺序。 */
|
|
178
|
+
function preserveHookOrder(layer, baseline, current) {
|
|
179
|
+
const merged = resolveEffectiveConfig(layer, baseline);
|
|
180
|
+
const stageIds = (config, stage) => (config.hooks ?? []).filter((hook) => hook.stage === stage).map((hook) => hook.id);
|
|
181
|
+
const hookOrder = Object.fromEntries(HOOK_STAGES
|
|
182
|
+
.filter((stage) => !same(stageIds(merged, stage), stageIds(current, stage)))
|
|
183
|
+
.map((stage) => [stage, stageIds(current, stage)]));
|
|
184
|
+
return Object.keys(hookOrder).length > 0 ? { ...layer, hookOrder } : layer;
|
|
185
|
+
}
|
|
186
|
+
/** 从拷贝式配置中提取项目层:与基线相同的条目改为继承,不同的保留为覆盖,基线多出的 hook 写入 disableHooks。 */
|
|
187
|
+
function extractProjectLayer(current, base, reference) {
|
|
188
|
+
const baseCategories = base.taskCategories ?? {};
|
|
189
|
+
const ownCategories = current.taskCategories ?? {};
|
|
190
|
+
const effectiveCurrent = effectiveTaskCategories(current);
|
|
191
|
+
const taskCategories = {};
|
|
192
|
+
for (const [id, value] of Object.entries(ownCategories)) {
|
|
193
|
+
if (!same(value, baseCategories[id]))
|
|
194
|
+
taskCategories[id] = value;
|
|
195
|
+
}
|
|
196
|
+
for (const id of Object.keys(baseCategories)) {
|
|
197
|
+
if (Object.hasOwn(ownCategories, id))
|
|
198
|
+
continue;
|
|
199
|
+
// 基线定义了项目原本没有写的分类:保留项目原来的生效值(默认分类)或删除它。
|
|
200
|
+
taskCategories[id] = effectiveCurrent[id] ? { notes: effectiveCurrent[id].notes } : null;
|
|
201
|
+
}
|
|
202
|
+
const baseHooks = new Map((base.hooks ?? []).map((hook) => [hook.id, hook]));
|
|
203
|
+
const ownHookIds = new Set((current.hooks ?? []).map((hook) => hook.id));
|
|
204
|
+
const hooks = (current.hooks ?? []).filter((hook) => !same(hook, baseHooks.get(hook.id)));
|
|
205
|
+
const disableHooks = [...baseHooks.keys()].filter((id) => !ownHookIds.has(id));
|
|
206
|
+
const basePlugins = base.plugins ?? [];
|
|
207
|
+
// 同一插件的最低版本合成时取较高值:项目的最低版本不高于基线时,继承基线即可。
|
|
208
|
+
const plugins = (current.plugins ?? []).filter((item) => {
|
|
209
|
+
const base = basePlugins.find((candidate) => candidate.name === item.name && candidate.marketplace === item.marketplace);
|
|
210
|
+
if (!base)
|
|
211
|
+
return true;
|
|
212
|
+
if (!item.minVersion)
|
|
213
|
+
return false;
|
|
214
|
+
return !base.minVersion || (compareVersions(item.minVersion, base.minVersion) ?? 1) > 0;
|
|
215
|
+
});
|
|
216
|
+
const baseMarkets = base.marketplaces ?? [];
|
|
217
|
+
const marketplaces = (current.marketplaces ?? []).filter((item) => !baseMarkets.some((candidate) => same(candidate, item)));
|
|
218
|
+
const verify = verifyOverrides(current.verify, base.verify);
|
|
219
|
+
return {
|
|
220
|
+
schema: 'spec-product',
|
|
221
|
+
preset: reference,
|
|
222
|
+
...(Object.keys(taskCategories).length > 0 ? { taskCategories } : {}),
|
|
223
|
+
...(hooks.length > 0 ? { hooks } : {}),
|
|
224
|
+
...(disableHooks.length > 0 ? { disableHooks } : {}),
|
|
225
|
+
...(verify ? { verify } : {}),
|
|
226
|
+
...(plugins.length > 0 ? { plugins } : {}),
|
|
227
|
+
...(marketplaces.length > 0 ? { marketplaces } : {}),
|
|
228
|
+
...(current.tools ? { tools: current.tools } : {}),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* 把拷贝式配置迁移为“引用 preset + 项目层”。迁移前后生效配置必须逐项一致(唯一例外:preset 提高的插件最低版本,
|
|
233
|
+
* 在结果中列出),否则中止且不写任何文件;
|
|
234
|
+
* 写入时替换 config(原注释不保留)、写新锁并删除旧的 preset-lock.yaml。dryRun 只返回结果。
|
|
235
|
+
*/
|
|
236
|
+
export function migratePreset(input) {
|
|
237
|
+
const { projectRoot, id, repo, ref } = input;
|
|
238
|
+
const configFile = configPathOf(projectRoot);
|
|
239
|
+
const current = loadProjectConfig(projectRoot);
|
|
240
|
+
if (!configFile)
|
|
241
|
+
throw new AppError('project_uninitialized', '缺少 openspec/config.yaml,请先运行 openspec init');
|
|
242
|
+
if (current.preset)
|
|
243
|
+
throw new AppError('preset_already_referenced', `项目已引用 preset ${current.preset.id};升级请用 openspec preset upgrade`);
|
|
244
|
+
const snapshot = fetchPresetRepo(repo, ref);
|
|
245
|
+
const baseline = presetBaseline(readRepoPreset(snapshot.cachePath, id));
|
|
246
|
+
const reference = { repo, ref, id };
|
|
247
|
+
const before = resolveEffectiveConfig(current);
|
|
248
|
+
const layer = preserveHookOrder(extractProjectLayer(current, baseline.patch, reference), baseline, before);
|
|
249
|
+
const after = resolveEffectiveConfig(layer, baseline);
|
|
250
|
+
const raised = raisedMinVersions(before, after);
|
|
251
|
+
const sections = Object.keys(comparable(before)).filter((key) => !same(comparable(before)[key], comparable(after)[key]));
|
|
252
|
+
if (!raised)
|
|
253
|
+
sections.push('plugins.minVersion');
|
|
254
|
+
if (sections.length > 0) {
|
|
255
|
+
throw new AppError('preset_migration_not_equivalent', `迁移后生效配置与迁移前不一致,未写入:${sections.join(', ')}`, { sections });
|
|
256
|
+
}
|
|
257
|
+
const inherited = {
|
|
258
|
+
taskCategories: Object.keys(baseline.patch.taskCategories ?? {}).filter((key) => !Object.hasOwn(layer.taskCategories ?? {}, key)),
|
|
259
|
+
hooks: (baseline.patch.hooks ?? []).map((hook) => hook.id)
|
|
260
|
+
.filter((hookId) => !(layer.hooks ?? []).some((hook) => hook.id === hookId) && !(layer.disableHooks ?? []).includes(hookId)),
|
|
261
|
+
plugins: (baseline.patch.plugins ?? []).map((item) => `${item.name}@${item.marketplace}`)
|
|
262
|
+
.filter((selector) => !(layer.plugins ?? []).some((item) => `${item.name}@${item.marketplace}` === selector)),
|
|
263
|
+
marketplaces: (baseline.patch.marketplaces ?? []).map((item) => item.name)
|
|
264
|
+
.filter((name) => !(layer.marketplaces ?? []).some((item) => item.name === name)),
|
|
265
|
+
};
|
|
266
|
+
const legacyLock = resolveInside(projectRoot, 'openspec/preset-lock.yaml', 'preset_lock_outside_project');
|
|
267
|
+
const result = { id, ref, resolvedSha: snapshot.resolvedSha, removedLegacyLock: exists(legacyLock), layer, inherited, raisedMinVersions: raised ?? [] };
|
|
268
|
+
if (input.dryRun)
|
|
269
|
+
return { ...result, written: false };
|
|
270
|
+
withRollback(configFile, presetLockPath(projectRoot), () => {
|
|
271
|
+
writePresetLock(projectRoot, { version: 1, ...reference, resolvedSha: snapshot.resolvedSha, digest: snapshot.digest });
|
|
272
|
+
writeText(configFile, stringifyYaml(layer, { lineWidth: 0 }));
|
|
273
|
+
const written = loadConfig(projectRoot);
|
|
274
|
+
if (!same(comparable(written), comparable(before)) || !raisedMinVersions(before, written)) {
|
|
275
|
+
throw new AppError('preset_migration_not_equivalent', '写入后生效配置与迁移前不一致');
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
if (result.removedLegacyLock)
|
|
279
|
+
removePath(legacyLock);
|
|
280
|
+
return { ...result, written: true };
|
|
281
|
+
}
|
|
282
|
+
//# sourceMappingURL=preset-use.js.map
|
package/dist/config/presets.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { type ProjectConfig } from './schema.js';
|
|
3
|
-
declare const ScaffoldEntrySchema: z.ZodObject<{
|
|
2
|
+
import { ProjectConfigPatchSchema, type ProjectConfig } from './schema.js';
|
|
3
|
+
export declare const ScaffoldEntrySchema: z.ZodObject<{
|
|
4
4
|
path: z.ZodString;
|
|
5
5
|
template: z.ZodString;
|
|
6
6
|
description: z.ZodOptional<z.ZodString>;
|
|
@@ -11,119 +11,44 @@ export type ScaffoldResult = {
|
|
|
11
11
|
status: 'created' | 'exists';
|
|
12
12
|
description?: string;
|
|
13
13
|
};
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
skill: "skill";
|
|
48
|
-
notes: "notes";
|
|
49
|
-
file: "file";
|
|
50
|
-
check: "check";
|
|
51
|
-
}>>;
|
|
52
|
-
capability: z.ZodOptional<z.ZodEnum<{
|
|
53
|
-
requirements: "requirements";
|
|
54
|
-
interfaces: "interfaces";
|
|
55
|
-
design: "design";
|
|
56
|
-
}>>;
|
|
57
|
-
use: z.ZodOptional<z.ZodString>;
|
|
58
|
-
operation: z.ZodOptional<z.ZodLiteral<"preview">>;
|
|
59
|
-
input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
60
|
-
inputFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
61
|
-
name: z.ZodOptional<z.ZodString>;
|
|
62
|
-
notes: z.ZodOptional<z.ZodString>;
|
|
63
|
-
taskCategories: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
64
|
-
command: z.ZodOptional<z.ZodString>;
|
|
65
|
-
target: z.ZodOptional<z.ZodEnum<{
|
|
66
|
-
project: "project";
|
|
67
|
-
inScopeChangedFiles: "inScopeChangedFiles";
|
|
68
|
-
}>>;
|
|
69
|
-
include: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
70
|
-
exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
71
|
-
allowEmpty: z.ZodOptional<z.ZodBoolean>;
|
|
72
|
-
required: z.ZodOptional<z.ZodBoolean>;
|
|
73
|
-
purpose: z.ZodOptional<z.ZodString>;
|
|
74
|
-
path: z.ZodOptional<z.ZodString>;
|
|
75
|
-
category: z.ZodOptional<z.ZodString>;
|
|
76
|
-
description: z.ZodOptional<z.ZodString>;
|
|
77
|
-
}, z.core.$strict>>>>;
|
|
78
|
-
verify: z.ZodOptional<z.ZodOptional<z.ZodObject<{
|
|
79
|
-
reviewScope: z.ZodOptional<z.ZodObject<{
|
|
80
|
-
includeDirs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
81
|
-
excludeDirs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
82
|
-
}, z.core.$strict>>;
|
|
83
|
-
failurePolicy: z.ZodOptional<z.ZodObject<{
|
|
84
|
-
failOnHookFailure: z.ZodOptional<z.ZodBoolean>;
|
|
85
|
-
failOnRequiredCommandFailure: z.ZodOptional<z.ZodBoolean>;
|
|
86
|
-
}, z.core.$strict>>;
|
|
87
|
-
}, z.core.$strict>>>;
|
|
88
|
-
plugins: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
89
|
-
name: z.ZodString;
|
|
90
|
-
marketplace: z.ZodString;
|
|
91
|
-
minVersion: z.ZodOptional<z.ZodString>;
|
|
92
|
-
}, z.core.$strict>>>>;
|
|
93
|
-
marketplaces: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
94
|
-
name: z.ZodString;
|
|
95
|
-
repo: z.ZodString;
|
|
96
|
-
ref: z.ZodString;
|
|
97
|
-
}, z.core.$strict>>>>;
|
|
98
|
-
tools: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
99
|
-
claude: "claude";
|
|
100
|
-
codex: "codex";
|
|
101
|
-
cursor: "cursor";
|
|
102
|
-
}>>>>;
|
|
103
|
-
}, z.core.$strict>>;
|
|
104
|
-
scaffold: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
105
|
-
path: z.ZodString;
|
|
106
|
-
template: z.ZodString;
|
|
107
|
-
description: z.ZodOptional<z.ZodString>;
|
|
108
|
-
}, z.core.$strict>>>;
|
|
109
|
-
notes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
110
|
-
warnings: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
111
|
-
}, z.core.$strict>;
|
|
112
|
-
export type TeamPreset = z.infer<typeof TeamPresetSchema>;
|
|
113
|
-
export declare function presetsDir(): string;
|
|
114
|
-
export declare function listPresets(): TeamPreset[];
|
|
115
|
-
export declare function loadPreset(id: string): TeamPreset;
|
|
116
|
-
export declare function mergePreset(base: ProjectConfig, preset: TeamPreset): ProjectConfig;
|
|
14
|
+
export type PresetBaseline = {
|
|
15
|
+
id: string;
|
|
16
|
+
patch: z.infer<typeof ProjectConfigPatchSchema>;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* 生效配置 = preset 基线 + 项目层,项目层优先:分类按名覆盖(null 删除);hook 同 ID 整条替换并保留基线位置,
|
|
20
|
+
* disableHooks 按 ID 去掉基线 hook,项目新增 hook 追加在后;验收设置按字段覆盖;插件取较高 minVersion;
|
|
21
|
+
* marketplace 以项目声明为准,基线只补缺;tools 只来自项目层。没有基线时项目层原样即生效配置。
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveEffectiveConfig(layer: ProjectConfig, baseline?: PresetBaseline): ProjectConfig;
|
|
24
|
+
/** 生效配置中一项的来源:preset 原样、项目覆盖了 preset 的同名项、项目独有、项目删除或禁用了 preset 的项。 */
|
|
25
|
+
export type ConfigOrigin = 'preset' | 'override' | 'project' | 'removed';
|
|
26
|
+
export interface ConfigOrigins {
|
|
27
|
+
taskCategories: Record<string, ConfigOrigin>;
|
|
28
|
+
hooks: Record<string, ConfigOrigin>;
|
|
29
|
+
plugins: Record<string, ConfigOrigin>;
|
|
30
|
+
marketplaces: Record<string, ConfigOrigin>;
|
|
31
|
+
/** 按字段,键为 `<分组>.<字段>`(如 reviewScope.excludeDirs),与 diffBaselines 一致 */
|
|
32
|
+
verify: Record<string, ConfigOrigin>;
|
|
33
|
+
}
|
|
34
|
+
/** 逐项标注生效配置的来源,供查看生效配置、升级提示遮蔽与迁移使用。 */
|
|
35
|
+
export declare function describeOrigins(layer: ProjectConfig, baseline?: PresetBaseline): ConfigOrigins;
|
|
36
|
+
export interface BaselineChanges {
|
|
37
|
+
added: string[];
|
|
38
|
+
changed: string[];
|
|
39
|
+
removed: string[];
|
|
40
|
+
}
|
|
41
|
+
type BaselineSection = 'taskCategories' | 'hooks' | 'plugins' | 'marketplaces' | 'verify';
|
|
42
|
+
/**
|
|
43
|
+
* 按条目比较两个基线:分类按名、hook 按 ID、插件按 selector、marketplace 按名;
|
|
44
|
+
* 验收设置与合成一样按字段,条目为 `<分组>.<字段>`(如 reviewScope.excludeDirs)。
|
|
45
|
+
*/
|
|
46
|
+
export declare function diffBaselines(previous: PresetBaseline['patch'], next: PresetBaseline['patch']): Record<BaselineSection, BaselineChanges>;
|
|
117
47
|
/**
|
|
118
48
|
* 把 preset 声明的模板文件放到项目里。已存在的文件一律不动——
|
|
119
|
-
*
|
|
49
|
+
* 团队填写后的内容不能被重复引用或升级覆盖;模板本身带 openspec:fill-me 标记,未填写时 file hook 不会放行。
|
|
50
|
+
* 先核对全部模板与目标路径再写入,缺模板时不留下部分生成的文件。
|
|
120
51
|
*/
|
|
121
|
-
export declare function
|
|
122
|
-
/** 合并时保留了宿主 ref、且与 preset 基线不同的 marketplace,供 init / preset apply 提示。 */
|
|
123
|
-
export declare function keptMarketplaceRefs(base: ProjectConfig, preset: TeamPreset): Array<{
|
|
124
|
-
name: string;
|
|
125
|
-
ref: string;
|
|
126
|
-
presetRef: string;
|
|
127
|
-
}>;
|
|
52
|
+
export declare function scaffoldFrom(projectRoot: string, templateRoot: string, presetId: string, entries: ScaffoldEntry[]): ScaffoldResult[];
|
|
128
53
|
export {};
|
|
129
54
|
//# sourceMappingURL=presets.d.ts.map
|