@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
package/dist/config/presets.js
CHANGED
|
@@ -1,91 +1,180 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { parse as parseYaml } from 'yaml';
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
2
|
import { z } from 'zod';
|
|
4
|
-
import { exists,
|
|
3
|
+
import { exists, readText, writeText } from '../shared/fs.js';
|
|
5
4
|
import { AppError } from '../shared/errors.js';
|
|
6
|
-
import { packageRoot } from '../shared/pkg.js';
|
|
7
5
|
import { resolveInside } from '../shared/paths.js';
|
|
8
|
-
import {
|
|
6
|
+
import { ProjectConfigSchema } from './schema.js';
|
|
9
7
|
import { compareVersions } from '../shared/version.js';
|
|
10
|
-
const ScaffoldEntrySchema = z.object({
|
|
8
|
+
export const ScaffoldEntrySchema = z.object({
|
|
11
9
|
/** 项目内目标路径,通常与某个 required file hook 的 path 一致 */
|
|
12
10
|
path: z.string().min(1),
|
|
13
|
-
/**
|
|
11
|
+
/** 模板文件,相对该 preset 所在目录 */
|
|
14
12
|
template: z.string().min(1),
|
|
15
13
|
description: z.string().optional(),
|
|
16
14
|
}).strict();
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
15
|
+
/**
|
|
16
|
+
* 生效配置 = preset 基线 + 项目层,项目层优先:分类按名覆盖(null 删除);hook 同 ID 整条替换并保留基线位置,
|
|
17
|
+
* disableHooks 按 ID 去掉基线 hook,项目新增 hook 追加在后;验收设置按字段覆盖;插件取较高 minVersion;
|
|
18
|
+
* marketplace 以项目声明为准,基线只补缺;tools 只来自项目层。没有基线时项目层原样即生效配置。
|
|
19
|
+
*/
|
|
20
|
+
export function resolveEffectiveConfig(layer, baseline) {
|
|
21
|
+
const { disableHooks = [], hookOrder, ...project } = layer;
|
|
22
|
+
if (!baseline) {
|
|
23
|
+
if (disableHooks.length > 0) {
|
|
24
|
+
throw new AppError('disable_hooks_without_preset', `disableHooks 只能禁用 preset 中的 hook,当前未引用 preset: ${disableHooks.join(', ')}`);
|
|
25
|
+
}
|
|
26
|
+
if (!hookOrder)
|
|
27
|
+
return parseEffective(project, '项目配置不合法');
|
|
28
|
+
return parseEffective({ ...project, hooks: orderHooks(project.hooks ?? [], hookOrder) }, '项目配置不合法');
|
|
29
|
+
}
|
|
30
|
+
const base = baseline.patch;
|
|
31
|
+
const baseHooks = base.hooks ?? [];
|
|
32
|
+
const baseHookIds = new Set(baseHooks.map((hook) => hook.id));
|
|
33
|
+
const ownHooks = project.hooks ?? [];
|
|
34
|
+
const unknown = disableHooks.filter((id) => !baseHookIds.has(id));
|
|
35
|
+
if (unknown.length > 0) {
|
|
36
|
+
throw new AppError('unknown_disabled_hook', `disableHooks 引用了 preset ${baseline.id} 中不存在的 hook: ${unknown.join(', ')}`);
|
|
37
|
+
}
|
|
38
|
+
const conflicting = ownHooks.filter((hook) => disableHooks.includes(hook.id)).map((hook) => hook.id);
|
|
39
|
+
if (conflicting.length > 0) {
|
|
40
|
+
throw new AppError('hook_disabled_and_overridden', `hook 不能同时被禁用和覆盖: ${conflicting.join(', ')}`);
|
|
41
|
+
}
|
|
42
|
+
const overrides = new Map(ownHooks.map((hook) => [hook.id, hook]));
|
|
43
|
+
const hooks = orderHooks([
|
|
44
|
+
...baseHooks.filter((hook) => !disableHooks.includes(hook.id)).map((hook) => overrides.get(hook.id) ?? hook),
|
|
45
|
+
...ownHooks.filter((hook) => !baseHookIds.has(hook.id)),
|
|
46
|
+
], hookOrder);
|
|
47
|
+
const reviewScope = { ...base.verify?.reviewScope, ...project.verify?.reviewScope };
|
|
48
|
+
const failurePolicy = { ...base.verify?.failurePolicy, ...project.verify?.failurePolicy };
|
|
49
|
+
const plugins = mergePluginDependencies(base.plugins ?? [], project.plugins ?? []);
|
|
50
|
+
const marketplaces = [
|
|
51
|
+
...(project.marketplaces ?? []),
|
|
52
|
+
...(base.marketplaces ?? []).filter((item) => !(project.marketplaces ?? []).some((existing) => existing.name === item.name)),
|
|
53
|
+
];
|
|
54
|
+
const taskCategories = { ...base.taskCategories, ...project.taskCategories };
|
|
55
55
|
const merged = {
|
|
56
56
|
schema: 'spec-product',
|
|
57
|
-
taskCategories
|
|
58
|
-
hooks
|
|
59
|
-
verify
|
|
60
|
-
|
|
61
|
-
...
|
|
62
|
-
...
|
|
57
|
+
...(Object.keys(taskCategories).length > 0 ? { taskCategories } : {}),
|
|
58
|
+
...(hooks.length > 0 ? { hooks } : {}),
|
|
59
|
+
...(base.verify || project.verify ? {
|
|
60
|
+
verify: {
|
|
61
|
+
...(Object.keys(reviewScope).length > 0 ? { reviewScope } : {}),
|
|
62
|
+
...(Object.keys(failurePolicy).length > 0 ? { failurePolicy } : {}),
|
|
63
63
|
},
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
},
|
|
69
|
-
plugins: mergePluginDependencies(base.plugins ?? [], preset.plugins ?? []),
|
|
70
|
-
// 宿主已声明的 marketplace 保留自己的 repo/ref:preset 的 ref 只是基线,不能把独立升级过的宿主降回去。
|
|
71
|
-
marketplaces: [
|
|
72
|
-
...(base.marketplaces ?? []),
|
|
73
|
-
...(preset.marketplaces ?? []).filter((item) => !(base.marketplaces ?? []).some((existing) => existing.name === item.name)),
|
|
74
|
-
],
|
|
75
|
-
tools: base.tools,
|
|
64
|
+
} : {}),
|
|
65
|
+
...(plugins.length > 0 ? { plugins } : {}),
|
|
66
|
+
...(marketplaces.length > 0 ? { marketplaces } : {}),
|
|
67
|
+
...(project.tools ? { tools: project.tools } : {}),
|
|
76
68
|
};
|
|
77
|
-
return
|
|
69
|
+
return parseEffective(merged, `preset ${baseline.id} 与项目配置合成后不合法`);
|
|
70
|
+
}
|
|
71
|
+
/** 逐项标注生效配置的来源,供查看生效配置、升级提示遮蔽与迁移使用。 */
|
|
72
|
+
export function describeOrigins(layer, baseline) {
|
|
73
|
+
const base = baseline?.patch ?? {};
|
|
74
|
+
const classify = (inBase, inProject) => (inBase ? (inProject ? 'override' : 'preset') : 'project');
|
|
75
|
+
const taskCategories = {};
|
|
76
|
+
for (const id of new Set([...Object.keys(base.taskCategories ?? {}), ...Object.keys(layer.taskCategories ?? {})])) {
|
|
77
|
+
const own = layer.taskCategories?.[id];
|
|
78
|
+
taskCategories[id] = own === null ? 'removed' : classify(Boolean(base.taskCategories?.[id]), own !== undefined);
|
|
79
|
+
}
|
|
80
|
+
const hooks = {};
|
|
81
|
+
const ownHookIds = new Set((layer.hooks ?? []).map((hook) => hook.id));
|
|
82
|
+
for (const hook of base.hooks ?? []) {
|
|
83
|
+
hooks[hook.id] = (layer.disableHooks ?? []).includes(hook.id) ? 'removed' : classify(true, ownHookIds.has(hook.id));
|
|
84
|
+
}
|
|
85
|
+
for (const id of ownHookIds)
|
|
86
|
+
hooks[id] ??= 'project';
|
|
87
|
+
const selector = (item) => `${item.name}@${item.marketplace}`;
|
|
88
|
+
const plugins = {};
|
|
89
|
+
const ownPlugins = new Set((layer.plugins ?? []).map(selector));
|
|
90
|
+
// 同一插件取较高的最低版本:生效条目仍等于 preset 的条目时,项目的声明没有改变生效值。
|
|
91
|
+
const effectivePlugins = new Map(mergePluginDependencies(base.plugins ?? [], layer.plugins ?? []).map((item) => [selector(item), item]));
|
|
92
|
+
for (const item of base.plugins ?? []) {
|
|
93
|
+
const id = selector(item);
|
|
94
|
+
plugins[id] = ownPlugins.has(id) && !isDeepStrictEqual(effectivePlugins.get(id), item) ? 'override' : 'preset';
|
|
95
|
+
}
|
|
96
|
+
for (const id of ownPlugins)
|
|
97
|
+
plugins[id] ??= 'project';
|
|
98
|
+
const marketplaces = {};
|
|
99
|
+
const ownMarkets = new Set((layer.marketplaces ?? []).map((item) => item.name));
|
|
100
|
+
for (const item of base.marketplaces ?? [])
|
|
101
|
+
marketplaces[item.name] = classify(true, ownMarkets.has(item.name));
|
|
102
|
+
for (const name of ownMarkets)
|
|
103
|
+
marketplaces[name] ??= 'project';
|
|
104
|
+
const verifyFields = (value) => Object.entries(value ?? {})
|
|
105
|
+
.flatMap(([group, fields]) => Object.keys(fields ?? {}).map((field) => `${group}.${field}`));
|
|
106
|
+
const verify = {};
|
|
107
|
+
const ownVerify = new Set(verifyFields(layer.verify));
|
|
108
|
+
for (const id of verifyFields(base.verify))
|
|
109
|
+
verify[id] = classify(true, ownVerify.has(id));
|
|
110
|
+
for (const id of ownVerify)
|
|
111
|
+
verify[id] ??= 'project';
|
|
112
|
+
return { taskCategories, hooks, plugins, marketplaces, verify };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 按条目比较两个基线:分类按名、hook 按 ID、插件按 selector、marketplace 按名;
|
|
116
|
+
* 验收设置与合成一样按字段,条目为 `<分组>.<字段>`(如 reviewScope.excludeDirs)。
|
|
117
|
+
*/
|
|
118
|
+
export function diffBaselines(previous, next) {
|
|
119
|
+
const keyed = (patch) => ({
|
|
120
|
+
taskCategories: new Map(Object.entries(patch.taskCategories ?? {}).map(([id, value]) => [id, JSON.stringify(value)])),
|
|
121
|
+
hooks: new Map((patch.hooks ?? []).map((hook) => [hook.id, JSON.stringify(hook)])),
|
|
122
|
+
plugins: new Map((patch.plugins ?? []).map((item) => [`${item.name}@${item.marketplace}`, JSON.stringify(item)])),
|
|
123
|
+
marketplaces: new Map((patch.marketplaces ?? []).map((item) => [item.name, JSON.stringify(item)])),
|
|
124
|
+
verify: new Map(Object.entries(patch.verify ?? {}).flatMap(([group, fields]) => Object.entries(fields ?? {}).map(([field, value]) => [`${group}.${field}`, JSON.stringify(value)]))),
|
|
125
|
+
});
|
|
126
|
+
const before = keyed(previous);
|
|
127
|
+
const after = keyed(next);
|
|
128
|
+
const sections = Object.keys(before);
|
|
129
|
+
return Object.fromEntries(sections.map((section) => {
|
|
130
|
+
const old = before[section];
|
|
131
|
+
const now = after[section];
|
|
132
|
+
return [section, {
|
|
133
|
+
added: [...now.keys()].filter((id) => !old.has(id)),
|
|
134
|
+
changed: [...now.keys()].filter((id) => old.has(id) && old.get(id) !== now.get(id)),
|
|
135
|
+
removed: [...old.keys()].filter((id) => !now.has(id)),
|
|
136
|
+
}];
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
/** 按 hookOrder 重排各阶段:列出的 ID 依次在前,其余保持原相对顺序;各阶段仍占原来的位置。 */
|
|
140
|
+
function orderHooks(hooks, hookOrder) {
|
|
141
|
+
if (!hookOrder)
|
|
142
|
+
return hooks;
|
|
143
|
+
const result = [...hooks];
|
|
144
|
+
for (const [stage, ids] of Object.entries(hookOrder)) {
|
|
145
|
+
const slots = hooks.flatMap((hook, index) => (hook.stage === stage ? [index] : []));
|
|
146
|
+
const inStage = slots.map((index) => hooks[index]);
|
|
147
|
+
const unknown = ids.filter((id) => !inStage.some((hook) => hook.id === id));
|
|
148
|
+
if (unknown.length > 0) {
|
|
149
|
+
throw new AppError('unknown_ordered_hook', `hookOrder.${stage} 引用了该阶段不存在的 hook: ${unknown.join(', ')}`);
|
|
150
|
+
}
|
|
151
|
+
const ordered = [
|
|
152
|
+
...ids.map((id) => inStage.find((hook) => hook.id === id)),
|
|
153
|
+
...inStage.filter((hook) => !ids.includes(hook.id)),
|
|
154
|
+
];
|
|
155
|
+
slots.forEach((slot, position) => { result[slot] = ordered[position]; });
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
function parseEffective(config, message) {
|
|
160
|
+
const result = ProjectConfigSchema.safeParse(config);
|
|
161
|
+
if (!result.success)
|
|
162
|
+
throw new AppError('invalid_config', message, result.error.issues);
|
|
163
|
+
return result.data;
|
|
78
164
|
}
|
|
79
165
|
/**
|
|
80
166
|
* 把 preset 声明的模板文件放到项目里。已存在的文件一律不动——
|
|
81
|
-
*
|
|
167
|
+
* 团队填写后的内容不能被重复引用或升级覆盖;模板本身带 openspec:fill-me 标记,未填写时 file hook 不会放行。
|
|
168
|
+
* 先核对全部模板与目标路径再写入,缺模板时不留下部分生成的文件。
|
|
82
169
|
*/
|
|
83
|
-
export function
|
|
84
|
-
|
|
85
|
-
const source = resolveInside(
|
|
170
|
+
export function scaffoldFrom(projectRoot, templateRoot, presetId, entries) {
|
|
171
|
+
const planned = entries.map((entry) => {
|
|
172
|
+
const source = resolveInside(templateRoot, entry.template, 'preset_template_outside_presets');
|
|
86
173
|
if (!exists(source))
|
|
87
|
-
throw new AppError('preset_template_missing', `preset ${
|
|
88
|
-
|
|
174
|
+
throw new AppError('preset_template_missing', `preset ${presetId} 缺少模板文件: ${entry.template}`);
|
|
175
|
+
return { entry, source, target: resolveInside(projectRoot, entry.path, 'scaffold_path_outside_project') };
|
|
176
|
+
});
|
|
177
|
+
return planned.map(({ entry, source, target }) => {
|
|
89
178
|
if (exists(target))
|
|
90
179
|
return { path: entry.path, status: 'exists', description: entry.description };
|
|
91
180
|
writeText(target, readText(source));
|
|
@@ -98,13 +187,6 @@ function upsertBy(base, patch, key) {
|
|
|
98
187
|
values.set(key(item), item);
|
|
99
188
|
return [...values.values()];
|
|
100
189
|
}
|
|
101
|
-
/** 合并时保留了宿主 ref、且与 preset 基线不同的 marketplace,供 init / preset apply 提示。 */
|
|
102
|
-
export function keptMarketplaceRefs(base, preset) {
|
|
103
|
-
return (preset.marketplaces ?? []).flatMap((item) => {
|
|
104
|
-
const existing = (base.marketplaces ?? []).find((candidate) => candidate.name === item.name);
|
|
105
|
-
return existing && existing.ref !== item.ref ? [{ name: item.name, ref: existing.ref, presetRef: item.ref }] : [];
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
190
|
/** 同一插件取较高的 minVersion:宿主提高过的最低版本不被 preset 默认值调低。 */
|
|
109
191
|
function mergePluginDependencies(base, patch) {
|
|
110
192
|
const previous = new Map(base.map((item) => [`${item.name}@${item.marketplace}`, item]));
|
|
@@ -115,11 +197,4 @@ function mergePluginDependencies(base, patch) {
|
|
|
115
197
|
return (compareVersions(existing, item.minVersion) ?? 0) > 0 ? { ...item, minVersion: existing } : item;
|
|
116
198
|
}), (item) => `${item.name}@${item.marketplace}`);
|
|
117
199
|
}
|
|
118
|
-
function loadPresetFile(file) {
|
|
119
|
-
const result = TeamPresetSchema.safeParse(parseYaml(readText(file)));
|
|
120
|
-
if (!result.success) {
|
|
121
|
-
throw new AppError('invalid_preset', `preset 不合法: ${file}`, result.error.issues);
|
|
122
|
-
}
|
|
123
|
-
return result.data;
|
|
124
|
-
}
|
|
125
200
|
//# sourceMappingURL=presets.js.map
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -48,10 +48,21 @@ export declare const HookSchema: z.ZodObject<{
|
|
|
48
48
|
category: z.ZodOptional<z.ZodString>;
|
|
49
49
|
description: z.ZodOptional<z.ZodString>;
|
|
50
50
|
}, z.core.$strict>;
|
|
51
|
+
/** 团队 preset 引用:独立 Git 仓库中的一个 preset,ref 同样必须不可变。 */
|
|
52
|
+
declare const PresetReferenceSchema: z.ZodObject<{
|
|
53
|
+
repo: z.ZodString;
|
|
54
|
+
ref: z.ZodString;
|
|
55
|
+
id: z.ZodString;
|
|
56
|
+
}, z.core.$strict>;
|
|
51
57
|
/** 插件最低版本:与插件 manifest 的 version 比较,只接受 major.minor.patch。 */
|
|
52
58
|
export declare const PluginMinVersionSchema: z.ZodString;
|
|
53
59
|
declare const ProjectConfigObjectSchema: z.ZodObject<{
|
|
54
60
|
schema: z.ZodLiteral<"spec-product">;
|
|
61
|
+
preset: z.ZodOptional<z.ZodObject<{
|
|
62
|
+
repo: z.ZodString;
|
|
63
|
+
ref: z.ZodString;
|
|
64
|
+
id: z.ZodString;
|
|
65
|
+
}, z.core.$strict>>;
|
|
55
66
|
taskCategories: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodObject<{
|
|
56
67
|
notes: z.ZodOptional<z.ZodString>;
|
|
57
68
|
}, z.core.$strict>>>>;
|
|
@@ -123,12 +134,31 @@ declare const ProjectConfigObjectSchema: z.ZodObject<{
|
|
|
123
134
|
codex: "codex";
|
|
124
135
|
cursor: "cursor";
|
|
125
136
|
}>>>;
|
|
137
|
+
disableHooks: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
138
|
+
hookOrder: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
|
|
139
|
+
collect: "collect";
|
|
140
|
+
parse: "parse";
|
|
141
|
+
tech: "tech";
|
|
142
|
+
plan: "plan";
|
|
143
|
+
execute: "execute";
|
|
144
|
+
verify: "verify";
|
|
145
|
+
}> & z.core.$partial, z.ZodArray<z.ZodString>>>;
|
|
126
146
|
}, z.core.$strict>;
|
|
127
147
|
export declare const ProjectConfigPatchSchema: z.ZodObject<{
|
|
128
|
-
|
|
148
|
+
verify: z.ZodOptional<z.ZodOptional<z.ZodObject<{
|
|
149
|
+
reviewScope: z.ZodOptional<z.ZodObject<{
|
|
150
|
+
includeDirs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
151
|
+
excludeDirs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
152
|
+
}, z.core.$strict>>;
|
|
153
|
+
failurePolicy: z.ZodOptional<z.ZodObject<{
|
|
154
|
+
failOnHookFailure: z.ZodOptional<z.ZodBoolean>;
|
|
155
|
+
failOnRequiredCommandFailure: z.ZodOptional<z.ZodBoolean>;
|
|
156
|
+
}, z.core.$strict>>;
|
|
157
|
+
}, z.core.$strict>>>;
|
|
129
158
|
taskCategories: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodObject<{
|
|
130
159
|
notes: z.ZodOptional<z.ZodString>;
|
|
131
160
|
}, z.core.$strict>>>>>;
|
|
161
|
+
schema: z.ZodOptional<z.ZodLiteral<"spec-product">>;
|
|
132
162
|
hooks: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
133
163
|
id: z.ZodString;
|
|
134
164
|
stage: z.ZodEnum<{
|
|
@@ -172,7 +202,80 @@ export declare const ProjectConfigPatchSchema: z.ZodObject<{
|
|
|
172
202
|
category: z.ZodOptional<z.ZodString>;
|
|
173
203
|
description: z.ZodOptional<z.ZodString>;
|
|
174
204
|
}, z.core.$strict>>>>;
|
|
175
|
-
|
|
205
|
+
plugins: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
206
|
+
name: z.ZodString;
|
|
207
|
+
marketplace: z.ZodString;
|
|
208
|
+
minVersion: z.ZodOptional<z.ZodString>;
|
|
209
|
+
}, z.core.$strict>>>>;
|
|
210
|
+
marketplaces: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
211
|
+
name: z.ZodString;
|
|
212
|
+
repo: z.ZodString;
|
|
213
|
+
ref: z.ZodString;
|
|
214
|
+
}, z.core.$strict>>>>;
|
|
215
|
+
tools: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
216
|
+
claude: "claude";
|
|
217
|
+
codex: "codex";
|
|
218
|
+
cursor: "cursor";
|
|
219
|
+
}>>>>;
|
|
220
|
+
}, z.core.$strict>;
|
|
221
|
+
/**
|
|
222
|
+
* 项目层:只做结构与重复校验。分类引用等跨层约束要在合成生效配置后才能判定,
|
|
223
|
+
* 因为 hook 引用的分类可能只由 preset 定义。
|
|
224
|
+
*/
|
|
225
|
+
export declare const ProjectConfigLayerSchema: z.ZodObject<{
|
|
226
|
+
schema: z.ZodLiteral<"spec-product">;
|
|
227
|
+
preset: z.ZodOptional<z.ZodObject<{
|
|
228
|
+
repo: z.ZodString;
|
|
229
|
+
ref: z.ZodString;
|
|
230
|
+
id: z.ZodString;
|
|
231
|
+
}, z.core.$strict>>;
|
|
232
|
+
taskCategories: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodObject<{
|
|
233
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
234
|
+
}, z.core.$strict>>>>;
|
|
235
|
+
hooks: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
236
|
+
id: z.ZodString;
|
|
237
|
+
stage: z.ZodEnum<{
|
|
238
|
+
collect: "collect";
|
|
239
|
+
parse: "parse";
|
|
240
|
+
tech: "tech";
|
|
241
|
+
plan: "plan";
|
|
242
|
+
execute: "execute";
|
|
243
|
+
verify: "verify";
|
|
244
|
+
}>;
|
|
245
|
+
type: z.ZodOptional<z.ZodEnum<{
|
|
246
|
+
command: "command";
|
|
247
|
+
skill: "skill";
|
|
248
|
+
notes: "notes";
|
|
249
|
+
file: "file";
|
|
250
|
+
check: "check";
|
|
251
|
+
}>>;
|
|
252
|
+
capability: z.ZodOptional<z.ZodEnum<{
|
|
253
|
+
requirements: "requirements";
|
|
254
|
+
interfaces: "interfaces";
|
|
255
|
+
design: "design";
|
|
256
|
+
}>>;
|
|
257
|
+
use: z.ZodOptional<z.ZodString>;
|
|
258
|
+
operation: z.ZodOptional<z.ZodLiteral<"preview">>;
|
|
259
|
+
input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
260
|
+
inputFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
261
|
+
name: z.ZodOptional<z.ZodString>;
|
|
262
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
263
|
+
taskCategories: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
264
|
+
command: z.ZodOptional<z.ZodString>;
|
|
265
|
+
target: z.ZodOptional<z.ZodEnum<{
|
|
266
|
+
project: "project";
|
|
267
|
+
inScopeChangedFiles: "inScopeChangedFiles";
|
|
268
|
+
}>>;
|
|
269
|
+
include: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
270
|
+
exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
271
|
+
allowEmpty: z.ZodOptional<z.ZodBoolean>;
|
|
272
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
273
|
+
purpose: z.ZodOptional<z.ZodString>;
|
|
274
|
+
path: z.ZodOptional<z.ZodString>;
|
|
275
|
+
category: z.ZodOptional<z.ZodString>;
|
|
276
|
+
description: z.ZodOptional<z.ZodString>;
|
|
277
|
+
}, z.core.$strict>>>;
|
|
278
|
+
verify: z.ZodOptional<z.ZodObject<{
|
|
176
279
|
reviewScope: z.ZodOptional<z.ZodObject<{
|
|
177
280
|
includeDirs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
178
281
|
excludeDirs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -181,25 +284,39 @@ export declare const ProjectConfigPatchSchema: z.ZodObject<{
|
|
|
181
284
|
failOnHookFailure: z.ZodOptional<z.ZodBoolean>;
|
|
182
285
|
failOnRequiredCommandFailure: z.ZodOptional<z.ZodBoolean>;
|
|
183
286
|
}, z.core.$strict>>;
|
|
184
|
-
}, z.core.$strict
|
|
185
|
-
plugins: z.ZodOptional<z.
|
|
287
|
+
}, z.core.$strict>>;
|
|
288
|
+
plugins: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
186
289
|
name: z.ZodString;
|
|
187
290
|
marketplace: z.ZodString;
|
|
188
291
|
minVersion: z.ZodOptional<z.ZodString>;
|
|
189
|
-
}, z.core.$strict
|
|
190
|
-
marketplaces: z.ZodOptional<z.
|
|
292
|
+
}, z.core.$strict>>>;
|
|
293
|
+
marketplaces: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
191
294
|
name: z.ZodString;
|
|
192
295
|
repo: z.ZodString;
|
|
193
296
|
ref: z.ZodString;
|
|
194
|
-
}, z.core.$strict
|
|
195
|
-
tools: z.ZodOptional<z.
|
|
297
|
+
}, z.core.$strict>>>;
|
|
298
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
196
299
|
claude: "claude";
|
|
197
300
|
codex: "codex";
|
|
198
301
|
cursor: "cursor";
|
|
199
|
-
}
|
|
302
|
+
}>>>;
|
|
303
|
+
disableHooks: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
304
|
+
hookOrder: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
|
|
305
|
+
collect: "collect";
|
|
306
|
+
parse: "parse";
|
|
307
|
+
tech: "tech";
|
|
308
|
+
plan: "plan";
|
|
309
|
+
execute: "execute";
|
|
310
|
+
verify: "verify";
|
|
311
|
+
}> & z.core.$partial, z.ZodArray<z.ZodString>>>;
|
|
200
312
|
}, z.core.$strict>;
|
|
201
313
|
export declare const ProjectConfigSchema: z.ZodObject<{
|
|
202
314
|
schema: z.ZodLiteral<"spec-product">;
|
|
315
|
+
preset: z.ZodOptional<z.ZodObject<{
|
|
316
|
+
repo: z.ZodString;
|
|
317
|
+
ref: z.ZodString;
|
|
318
|
+
id: z.ZodString;
|
|
319
|
+
}, z.core.$strict>>;
|
|
203
320
|
taskCategories: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodObject<{
|
|
204
321
|
notes: z.ZodOptional<z.ZodString>;
|
|
205
322
|
}, z.core.$strict>>>>;
|
|
@@ -271,9 +388,19 @@ export declare const ProjectConfigSchema: z.ZodObject<{
|
|
|
271
388
|
codex: "codex";
|
|
272
389
|
cursor: "cursor";
|
|
273
390
|
}>>>;
|
|
391
|
+
disableHooks: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
392
|
+
hookOrder: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
|
|
393
|
+
collect: "collect";
|
|
394
|
+
parse: "parse";
|
|
395
|
+
tech: "tech";
|
|
396
|
+
plan: "plan";
|
|
397
|
+
execute: "execute";
|
|
398
|
+
verify: "verify";
|
|
399
|
+
}> & z.core.$partial, z.ZodArray<z.ZodString>>>;
|
|
274
400
|
}, z.core.$strict>;
|
|
275
401
|
export type ProjectConfig = z.infer<typeof ProjectConfigObjectSchema>;
|
|
276
402
|
export type ProjectHook = z.infer<typeof HookSchema>;
|
|
403
|
+
export type PresetReference = z.infer<typeof PresetReferenceSchema>;
|
|
277
404
|
export declare function effectiveTaskCategories(config: Pick<ProjectConfig, 'taskCategories'>): Record<string, {
|
|
278
405
|
notes: string;
|
|
279
406
|
}>;
|
package/dist/config/schema.js
CHANGED
|
@@ -91,6 +91,12 @@ export const HookSchema = z.object({
|
|
|
91
91
|
});
|
|
92
92
|
const TaskCategorySchema = z.object({ notes: z.string().optional() }).strict();
|
|
93
93
|
const ImmutableMarketplaceRefSchema = z.string().min(1).refine((value) => /^[0-9a-f]{40}$/i.test(value) || /^refs\/tags\/[^/]+$/.test(value), 'marketplace ref 必须是完整 Git SHA 或 refs/tags/<tag>');
|
|
94
|
+
/** 团队 preset 引用:独立 Git 仓库中的一个 preset,ref 同样必须不可变。 */
|
|
95
|
+
const PresetReferenceSchema = z.object({
|
|
96
|
+
repo: z.string().min(1),
|
|
97
|
+
ref: z.string().min(1).refine((value) => /^[0-9a-f]{40}$/i.test(value) || /^refs\/tags\/[^/]+$/.test(value), 'preset ref 必须是完整 Git SHA 或 refs/tags/<tag>'),
|
|
98
|
+
id: PathTokenSchema,
|
|
99
|
+
}).strict();
|
|
94
100
|
const MarketplaceSchema = z.object({
|
|
95
101
|
name: PathTokenSchema,
|
|
96
102
|
repo: z.string().min(1),
|
|
@@ -115,15 +121,22 @@ const VerifySchema = z.object({
|
|
|
115
121
|
}).strict();
|
|
116
122
|
const ProjectConfigObjectSchema = z.object({
|
|
117
123
|
schema: z.literal('spec-product'),
|
|
124
|
+
/** 引用的团队 preset;其余字段是项目层,与 preset 基线合成生效配置。 */
|
|
125
|
+
preset: PresetReferenceSchema.optional(),
|
|
118
126
|
taskCategories: z.record(CategoryIdSchema, TaskCategorySchema.nullable()).optional(),
|
|
119
127
|
hooks: z.array(HookSchema).optional(),
|
|
120
128
|
verify: VerifySchema.optional(),
|
|
121
129
|
plugins: z.array(PluginDependencySchema).optional(),
|
|
122
130
|
marketplaces: z.array(MarketplaceSchema).optional(),
|
|
123
131
|
tools: z.array(z.enum(['claude', 'codex', 'cursor'])).optional(),
|
|
132
|
+
/** 按 ID 禁用 preset 基线中的 hook;只属于项目层,不进入生效配置。 */
|
|
133
|
+
disableHooks: z.array(PathTokenSchema).optional(),
|
|
134
|
+
/** 按阶段指定 hook 顺序:列出的 ID 依次排在该阶段最前,其余保持原相对顺序;只属于项目层。 */
|
|
135
|
+
hookOrder: z.partialRecord(z.enum(HOOK_STAGES), z.array(PathTokenSchema)).optional(),
|
|
124
136
|
}).strict();
|
|
125
|
-
export const ProjectConfigPatchSchema = ProjectConfigObjectSchema.partial();
|
|
126
|
-
|
|
137
|
+
export const ProjectConfigPatchSchema = ProjectConfigObjectSchema.omit({ preset: true, disableHooks: true, hookOrder: true }).partial();
|
|
138
|
+
/** 单层内即可判定的约束:重复项。项目层与生效配置都要满足。 */
|
|
139
|
+
function checkDuplicates(config, ctx) {
|
|
127
140
|
const ids = new Set();
|
|
128
141
|
const collectCapabilities = new Set();
|
|
129
142
|
for (const [index, hook] of (config.hooks ?? []).entries()) {
|
|
@@ -155,6 +168,21 @@ export const ProjectConfigSchema = ProjectConfigObjectSchema.superRefine((config
|
|
|
155
168
|
if (new Set(config.tools ?? []).size !== (config.tools ?? []).length) {
|
|
156
169
|
ctx.addIssue({ code: 'custom', path: ['tools'], message: 'tools 不能重复' });
|
|
157
170
|
}
|
|
171
|
+
if (new Set(config.disableHooks ?? []).size !== (config.disableHooks ?? []).length) {
|
|
172
|
+
ctx.addIssue({ code: 'custom', path: ['disableHooks'], message: 'disableHooks 不能重复' });
|
|
173
|
+
}
|
|
174
|
+
for (const [stage, ids] of Object.entries(config.hookOrder ?? {})) {
|
|
175
|
+
if (new Set(ids).size !== ids.length)
|
|
176
|
+
ctx.addIssue({ code: 'custom', path: ['hookOrder', stage], message: `hookOrder.${stage} 不能重复` });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* 项目层:只做结构与重复校验。分类引用等跨层约束要在合成生效配置后才能判定,
|
|
181
|
+
* 因为 hook 引用的分类可能只由 preset 定义。
|
|
182
|
+
*/
|
|
183
|
+
export const ProjectConfigLayerSchema = ProjectConfigObjectSchema.superRefine(checkDuplicates);
|
|
184
|
+
export const ProjectConfigSchema = ProjectConfigObjectSchema.superRefine((config, ctx) => {
|
|
185
|
+
checkDuplicates(config, ctx);
|
|
158
186
|
const categories = new Set(Object.keys(effectiveTaskCategories(config)));
|
|
159
187
|
for (const [index, hook] of (config.hooks ?? []).entries()) {
|
|
160
188
|
for (const category of hook.taskCategories ?? []) {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { configPathOf } from '../config/load.js';
|
|
4
|
+
import { presetLockPath } from '../config/preset-repo.js';
|
|
4
5
|
import { resolveAdapterEntry } from '../marketplace/adapters.js';
|
|
5
6
|
import { readPluginInput, runPluginCommand, sameIdentity } from '../hooks/plugin-runtime.js';
|
|
6
7
|
import { sha256File } from '../shared/hash.js';
|
|
7
|
-
import { readText, writeJson } from '../shared/fs.js';
|
|
8
|
+
import { exists, readText, writeJson } from '../shared/fs.js';
|
|
8
9
|
import { AppError } from '../shared/errors.js';
|
|
9
10
|
import { artifactPath, artifactRel, requireChange } from './change.js';
|
|
10
11
|
import { loadLedger } from './sources.js';
|
|
@@ -19,6 +20,10 @@ export function operationContext(root, change, binding, outputDir, inputFile, wo
|
|
|
19
20
|
const configPath = configPathOf(root);
|
|
20
21
|
if (configPath)
|
|
21
22
|
params.files.push(path.relative(root, configPath));
|
|
23
|
+
// 引用 preset 时生效规则还取决于锁定的 preset 快照,锁变化同样要让指纹失效。
|
|
24
|
+
const presetLock = presetLockPath(root);
|
|
25
|
+
if (exists(presetLock))
|
|
26
|
+
params.files.push(path.relative(root, presetLock));
|
|
22
27
|
if (binding.kind === 'adapter') {
|
|
23
28
|
if (state.sourceRevision !== state.collectedRevision)
|
|
24
29
|
throw new AppError('source_not_collected', '来源尚未完成采集');
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ScaffoldResult } from './config/presets.js';
|
|
2
2
|
import { effectiveTaskCategories, type ProjectConfig } from './config/schema.js';
|
|
3
3
|
import { createChange, listChangeNames, requireChange, resolveChangeName } from './core/change.js';
|
|
4
4
|
import { nextAction } from './core/flow.js';
|
|
@@ -22,6 +22,11 @@ export declare function project(cwd?: string): {
|
|
|
22
22
|
projectRoot: string;
|
|
23
23
|
config: {
|
|
24
24
|
schema: "spec-product";
|
|
25
|
+
preset?: {
|
|
26
|
+
repo: string;
|
|
27
|
+
ref: string;
|
|
28
|
+
id: string;
|
|
29
|
+
} | undefined;
|
|
25
30
|
taskCategories?: Record<string, {
|
|
26
31
|
notes?: string | undefined;
|
|
27
32
|
} | null> | undefined;
|
|
@@ -69,12 +74,19 @@ export declare function project(cwd?: string): {
|
|
|
69
74
|
ref: string;
|
|
70
75
|
}[] | undefined;
|
|
71
76
|
tools?: ("claude" | "codex" | "cursor")[] | undefined;
|
|
77
|
+
disableHooks?: string[] | undefined;
|
|
78
|
+
hookOrder?: Partial<Record<"collect" | "parse" | "tech" | "plan" | "execute" | "verify", string[]>> | undefined;
|
|
72
79
|
};
|
|
73
80
|
};
|
|
74
81
|
export declare function initProject(input: {
|
|
75
82
|
cwd?: string;
|
|
76
83
|
tools?: HostId[];
|
|
77
|
-
preset
|
|
84
|
+
/** 引用 preset 仓库中的 preset;等同于初始化后执行 preset use */
|
|
85
|
+
preset?: {
|
|
86
|
+
id: string;
|
|
87
|
+
repo: string;
|
|
88
|
+
ref: string;
|
|
89
|
+
};
|
|
78
90
|
yes?: boolean;
|
|
79
91
|
skipPlugins?: boolean;
|
|
80
92
|
/** 每个耗时步骤开始前调用一次,供 CLI 在终端反馈进度;缺省静默。 */
|
|
@@ -92,7 +104,7 @@ export declare function initProject(input: {
|
|
|
92
104
|
}>;
|
|
93
105
|
scaffold: ScaffoldResult[];
|
|
94
106
|
};
|
|
95
|
-
export { createChange, listChangeNames, requireChange, resolveChangeName, nextAction, buildInstructions, addSources, collect, abortUpdate, parseSourceArg, parseSourceFileArg, createWorker, acceptWorker, answerWorker, cancelWorker, approveGate, rejectGate, isKnownGate, runVerifyCommands, recordEvidence, writeReport, renderReportMarkdown, requestFix, changedFilesFromState, archiveChange, collectPendingWork, renderPendingWork,
|
|
107
|
+
export { createChange, listChangeNames, requireChange, resolveChangeName, nextAction, buildInstructions, addSources, collect, abortUpdate, parseSourceArg, parseSourceFileArg, createWorker, acceptWorker, answerWorker, cancelWorker, approveGate, rejectGate, isKnownGate, runVerifyCommands, recordEvidence, writeReport, renderReportMarkdown, requestFix, changedFilesFromState, archiveChange, collectPendingWork, renderPendingWork, addMarketplace, listMarketplaces, installPlugin, verifyDelivery, verifyInstallCli, effectiveTaskCategories, BUILTIN_MARKETPLACE, assertPluginsPresent, inspectMarketplaceDependencies, inspectHookEntries, };
|
|
96
108
|
export type { HostId, GateId, WorkflowName, ProjectConfig };
|
|
97
109
|
export { analyzeUsage, renderUsageMarkdown } from './usage/analyze.js';
|
|
98
110
|
export type { UsageReport, UsageOptions, Tokens, RateCard, Billing } from './usage/types.js';
|