@birdie_moblie/open_spec 2.1.7
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/.agents/plugins/marketplace.json +13 -0
- package/.claude-plugin/marketplace.json +12 -0
- package/.claude-plugin/plugin.json +9 -0
- package/.codex-plugin/plugin.json +9 -0
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/bin/openspec.js +3 -0
- package/dist/cli/commands/change.d.ts +3 -0
- package/dist/cli/commands/change.js +37 -0
- package/dist/cli/commands/ext.d.ts +3 -0
- package/dist/cli/commands/ext.js +196 -0
- package/dist/cli/commands/figma.d.ts +3 -0
- package/dist/cli/commands/figma.js +73 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.js +71 -0
- package/dist/cli/commands/opsx.d.ts +3 -0
- package/dist/cli/commands/opsx.js +324 -0
- package/dist/cli/commands/usage.d.ts +3 -0
- package/dist/cli/commands/usage.js +21 -0
- package/dist/cli/output.d.ts +8 -0
- package/dist/cli/output.js +17 -0
- package/dist/cli/program.d.ts +4 -0
- package/dist/cli/program.js +42 -0
- package/dist/cli/register.d.ts +3 -0
- package/dist/cli/register.js +13 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3 -0
- package/dist/config/load.d.ts +7 -0
- package/dist/config/load.js +55 -0
- package/dist/config/presets.d.ts +129 -0
- package/dist/config/presets.js +125 -0
- package/dist/config/schema.d.ts +281 -0
- package/dist/config/schema.js +178 -0
- package/dist/core/archive.d.ts +5 -0
- package/dist/core/archive.js +33 -0
- package/dist/core/change.d.ts +12 -0
- package/dist/core/change.js +69 -0
- package/dist/core/code-changes.d.ts +9 -0
- package/dist/core/code-changes.js +31 -0
- package/dist/core/flow.d.ts +6 -0
- package/dist/core/flow.js +99 -0
- package/dist/core/gates.d.ts +5 -0
- package/dist/core/gates.js +71 -0
- package/dist/core/instructions.d.ts +21 -0
- package/dist/core/instructions.js +102 -0
- package/dist/core/source-operations.d.ts +25 -0
- package/dist/core/source-operations.js +94 -0
- package/dist/core/sources.d.ts +33 -0
- package/dist/core/sources.js +234 -0
- package/dist/core/state.d.ts +15 -0
- package/dist/core/state.js +75 -0
- package/dist/core/status.d.ts +31 -0
- package/dist/core/status.js +22 -0
- package/dist/core/tasks.d.ts +15 -0
- package/dist/core/tasks.js +143 -0
- package/dist/core/todo.d.ts +31 -0
- package/dist/core/todo.js +114 -0
- package/dist/core/types.d.ts +297 -0
- package/dist/core/types.js +227 -0
- package/dist/core/verify.d.ts +229 -0
- package/dist/core/verify.js +391 -0
- package/dist/core/worker-evidence.d.ts +71 -0
- package/dist/core/worker-evidence.js +253 -0
- package/dist/core/worker.d.ts +22 -0
- package/dist/core/worker.js +478 -0
- package/dist/delivery/claude-install.d.ts +4 -0
- package/dist/delivery/claude-install.js +44 -0
- package/dist/delivery/distribution.d.ts +3 -0
- package/dist/delivery/distribution.js +53 -0
- package/dist/delivery/skills.d.ts +36 -0
- package/dist/delivery/skills.js +178 -0
- package/dist/figma/cache.d.ts +19 -0
- package/dist/figma/cache.js +115 -0
- package/dist/figma/claude.d.ts +14 -0
- package/dist/figma/claude.js +121 -0
- package/dist/figma/collect.d.ts +3 -0
- package/dist/figma/collect.js +57 -0
- package/dist/figma/index.d.ts +35 -0
- package/dist/figma/index.js +178 -0
- package/dist/figma/media.d.ts +5 -0
- package/dist/figma/media.js +77 -0
- package/dist/figma/response.d.ts +9 -0
- package/dist/figma/response.js +27 -0
- package/dist/hooks/command-context.d.ts +19 -0
- package/dist/hooks/command-context.js +33 -0
- package/dist/hooks/engine.d.ts +61 -0
- package/dist/hooks/engine.js +314 -0
- package/dist/hooks/plugin-runtime.d.ts +188 -0
- package/dist/hooks/plugin-runtime.js +198 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +185 -0
- package/dist/marketplace/adapters.d.ts +202 -0
- package/dist/marketplace/adapters.js +203 -0
- package/dist/marketplace/builtin.d.ts +9 -0
- package/dist/marketplace/builtin.js +9 -0
- package/dist/marketplace/install.d.ts +38 -0
- package/dist/marketplace/install.js +108 -0
- package/dist/marketplace/registry.d.ts +71 -0
- package/dist/marketplace/registry.js +342 -0
- package/dist/marketplace/upgrade.d.ts +39 -0
- package/dist/marketplace/upgrade.js +104 -0
- package/dist/shared/command-log.d.ts +9 -0
- package/dist/shared/command-log.js +55 -0
- package/dist/shared/errors.d.ts +7 -0
- package/dist/shared/errors.js +14 -0
- package/dist/shared/exec.d.ts +16 -0
- package/dist/shared/exec.js +86 -0
- package/dist/shared/fs.d.ts +18 -0
- package/dist/shared/fs.js +93 -0
- package/dist/shared/git.d.ts +22 -0
- package/dist/shared/git.js +132 -0
- package/dist/shared/hash.d.ts +5 -0
- package/dist/shared/hash.js +29 -0
- package/dist/shared/paths.d.ts +16 -0
- package/dist/shared/paths.js +97 -0
- package/dist/shared/pkg.d.ts +3 -0
- package/dist/shared/pkg.js +23 -0
- package/dist/shared/version.d.ts +9 -0
- package/dist/shared/version.js +34 -0
- package/dist/usage/analyze.d.ts +5 -0
- package/dist/usage/analyze.js +212 -0
- package/dist/usage/claude.d.ts +9 -0
- package/dist/usage/claude.js +59 -0
- package/dist/usage/collect.d.ts +11 -0
- package/dist/usage/collect.js +71 -0
- package/dist/usage/doctor.d.ts +72 -0
- package/dist/usage/doctor.js +121 -0
- package/dist/usage/parse.d.ts +28 -0
- package/dist/usage/parse.js +345 -0
- package/dist/usage/types.d.ts +103 -0
- package/dist/usage/types.js +2 -0
- package/docs/getting-started.md +20 -0
- package/docs/guides/configuration.md +33 -0
- package/docs/guides/cost-benchmark.md +88 -0
- package/docs/guides/plugin-migration.md +65 -0
- package/docs/guides/smart-figma.md +51 -0
- package/docs/guides/source-reading.md +35 -0
- package/docs/guides/ui-conventions-template.md +39 -0
- package/docs/guides/worker-evidence.md +73 -0
- package/docs/guides/workflow.md +74 -0
- package/docs/maintainers/architecture.md +27 -0
- package/docs/maintainers/plugin-runtime-contract.md +55 -0
- package/package.json +78 -0
- package/plugin.json +9 -0
- package/presets/backend-service.yaml +19 -0
- package/presets/flutter-mobile/plugin-hooks.json +68 -0
- package/presets/flutter-mobile/ui-conventions.md +122 -0
- package/presets/flutter-mobile.yaml +205 -0
- package/presets/web-product.yaml +23 -0
- package/skills/opsx-apply/SKILL.md +35 -0
- package/skills/opsx-apply/nodes/blocked.md +7 -0
- package/skills/opsx-apply/nodes/code-task.md +29 -0
- package/skills/opsx-apply/nodes/gate-a.md +9 -0
- package/skills/opsx-apply/nodes/stop.md +5 -0
- package/skills/opsx-apply/nodes/task-build.md +50 -0
- package/skills/opsx-apply/nodes/task-repair.md +20 -0
- package/skills/opsx-apply/nodes/task-scout.md +28 -0
- package/skills/opsx-apply/nodes/task-verify.md +38 -0
- package/skills/opsx-archive/SKILL.md +24 -0
- package/skills/opsx-archive/nodes/archive.md +15 -0
- package/skills/opsx-explore/SKILL.md +20 -0
- package/skills/opsx-explore/nodes/explore.md +3 -0
- package/skills/opsx-fix-verify/SKILL.md +30 -0
- package/skills/opsx-fix-verify/nodes/repair.md +11 -0
- package/skills/opsx-fix-verify/nodes/reverify.md +3 -0
- package/skills/opsx-fix-verify/nodes/stop.md +3 -0
- package/skills/opsx-gate-decision/SKILL.md +20 -0
- package/skills/opsx-gate-decision/nodes/decide.md +15 -0
- package/skills/opsx-propose/SKILL.md +36 -0
- package/skills/opsx-propose/nodes/blocked.md +13 -0
- package/skills/opsx-propose/nodes/collect.md +35 -0
- package/skills/opsx-propose/nodes/parse.md +72 -0
- package/skills/opsx-propose/nodes/plan.md +65 -0
- package/skills/opsx-propose/nodes/stop.md +17 -0
- package/skills/opsx-propose/nodes/tech.md +77 -0
- package/skills/opsx-propose/scripts/read-json-source.mjs +22 -0
- package/skills/opsx-resume/SKILL.md +21 -0
- package/skills/opsx-resume/nodes/route.md +12 -0
- package/skills/opsx-source-update/SKILL.md +23 -0
- package/skills/opsx-source-update/nodes/update.md +14 -0
- package/skills/opsx-verify/SKILL.md +32 -0
- package/skills/opsx-verify/nodes/blocked.md +3 -0
- package/skills/opsx-verify/nodes/failed.md +3 -0
- package/skills/opsx-verify/nodes/gate-b.md +7 -0
- package/skills/opsx-verify/nodes/stop.md +5 -0
- package/skills/opsx-verify/nodes/verify.md +10 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { parse as parseYaml } from 'yaml';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { exists, listFilesRecursive, readText, writeText } from '../shared/fs.js';
|
|
5
|
+
import { AppError } from '../shared/errors.js';
|
|
6
|
+
import { packageRoot } from '../shared/pkg.js';
|
|
7
|
+
import { resolveInside } from '../shared/paths.js';
|
|
8
|
+
import { PluginMinVersionSchema, ProjectConfigPatchSchema, ProjectConfigSchema } from './schema.js';
|
|
9
|
+
import { compareVersions } from '../shared/version.js';
|
|
10
|
+
const ScaffoldEntrySchema = z.object({
|
|
11
|
+
/** 项目内目标路径,通常与某个 required file hook 的 path 一致 */
|
|
12
|
+
path: z.string().min(1),
|
|
13
|
+
/** 相对 presets/ 目录的模板文件 */
|
|
14
|
+
template: z.string().min(1),
|
|
15
|
+
description: z.string().optional(),
|
|
16
|
+
}).strict();
|
|
17
|
+
export const TeamPresetSchema = z.object({
|
|
18
|
+
schemaVersion: z.literal('openspec.team-preset.v1'),
|
|
19
|
+
id: z.string(),
|
|
20
|
+
version: z.string(),
|
|
21
|
+
title: z.string(),
|
|
22
|
+
description: z.string().optional(),
|
|
23
|
+
marketplaces: z.array(z.object({
|
|
24
|
+
name: z.string(),
|
|
25
|
+
repo: z.string(),
|
|
26
|
+
ref: z.string(),
|
|
27
|
+
}).strict()).optional(),
|
|
28
|
+
plugins: z.array(z.object({
|
|
29
|
+
name: z.string(),
|
|
30
|
+
marketplace: z.string(),
|
|
31
|
+
minVersion: PluginMinVersionSchema.optional(),
|
|
32
|
+
}).strict()).optional(),
|
|
33
|
+
configPatch: ProjectConfigPatchSchema.optional(),
|
|
34
|
+
/** 应用 preset 时按需生成的项目文件:只创建、不覆盖 */
|
|
35
|
+
scaffold: z.array(ScaffoldEntrySchema).optional(),
|
|
36
|
+
notes: z.array(z.string()).optional(),
|
|
37
|
+
warnings: z.array(z.string()).optional(),
|
|
38
|
+
}).strict();
|
|
39
|
+
export function presetsDir() {
|
|
40
|
+
return path.join(packageRoot(), 'presets');
|
|
41
|
+
}
|
|
42
|
+
export function listPresets() {
|
|
43
|
+
return listFilesRecursive(presetsDir())
|
|
44
|
+
.filter((file) => file.endsWith('.yaml') || file.endsWith('.yml'))
|
|
45
|
+
.map((file) => loadPresetFile(file));
|
|
46
|
+
}
|
|
47
|
+
export function loadPreset(id) {
|
|
48
|
+
const file = path.join(presetsDir(), `${id}.yaml`);
|
|
49
|
+
if (!exists(file))
|
|
50
|
+
throw new AppError('preset_not_found', `找不到 preset: ${id}`);
|
|
51
|
+
return loadPresetFile(file);
|
|
52
|
+
}
|
|
53
|
+
export function mergePreset(base, preset) {
|
|
54
|
+
const patch = preset.configPatch ?? { schema: 'spec-product' };
|
|
55
|
+
const merged = {
|
|
56
|
+
schema: 'spec-product',
|
|
57
|
+
taskCategories: { ...base.taskCategories, ...patch.taskCategories },
|
|
58
|
+
hooks: upsertBy(base.hooks ?? [], patch.hooks ?? [], (item) => item.id),
|
|
59
|
+
verify: {
|
|
60
|
+
reviewScope: {
|
|
61
|
+
...base.verify?.reviewScope,
|
|
62
|
+
...patch.verify?.reviewScope,
|
|
63
|
+
},
|
|
64
|
+
failurePolicy: {
|
|
65
|
+
...base.verify?.failurePolicy,
|
|
66
|
+
...patch.verify?.failurePolicy,
|
|
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,
|
|
76
|
+
};
|
|
77
|
+
return ProjectConfigSchema.parse(merged);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 把 preset 声明的模板文件放到项目里。已存在的文件一律不动——
|
|
81
|
+
* 团队填写后的内容不能被重复 init 覆盖;模板本身带 openspec:fill-me 标记,未填写时 file hook 不会放行。
|
|
82
|
+
*/
|
|
83
|
+
export function scaffoldPreset(projectRoot, preset) {
|
|
84
|
+
return (preset.scaffold ?? []).map((entry) => {
|
|
85
|
+
const source = resolveInside(presetsDir(), entry.template, 'preset_template_outside_presets');
|
|
86
|
+
if (!exists(source))
|
|
87
|
+
throw new AppError('preset_template_missing', `preset ${preset.id} 缺少模板文件: ${entry.template}`);
|
|
88
|
+
const target = resolveInside(projectRoot, entry.path, 'scaffold_path_outside_project');
|
|
89
|
+
if (exists(target))
|
|
90
|
+
return { path: entry.path, status: 'exists', description: entry.description };
|
|
91
|
+
writeText(target, readText(source));
|
|
92
|
+
return { path: entry.path, status: 'created', description: entry.description };
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function upsertBy(base, patch, key) {
|
|
96
|
+
const values = new Map(base.map((item) => [key(item), item]));
|
|
97
|
+
for (const item of patch)
|
|
98
|
+
values.set(key(item), item);
|
|
99
|
+
return [...values.values()];
|
|
100
|
+
}
|
|
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
|
+
/** 同一插件取较高的 minVersion:宿主提高过的最低版本不被 preset 默认值调低。 */
|
|
109
|
+
function mergePluginDependencies(base, patch) {
|
|
110
|
+
const previous = new Map(base.map((item) => [`${item.name}@${item.marketplace}`, item]));
|
|
111
|
+
return upsertBy(base, patch.map((item) => {
|
|
112
|
+
const existing = previous.get(`${item.name}@${item.marketplace}`)?.minVersion;
|
|
113
|
+
if (!existing || !item.minVersion)
|
|
114
|
+
return existing && !item.minVersion ? { ...item, minVersion: existing } : item;
|
|
115
|
+
return (compareVersions(existing, item.minVersion) ?? 0) > 0 ? { ...item, minVersion: existing } : item;
|
|
116
|
+
}), (item) => `${item.name}@${item.marketplace}`);
|
|
117
|
+
}
|
|
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
|
+
//# sourceMappingURL=presets.js.map
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const HOOK_STAGES: readonly ["collect", "parse", "tech", "plan", "execute", "verify"];
|
|
3
|
+
export declare const HOOK_TYPES: readonly ["command", "skill", "notes", "file", "check"];
|
|
4
|
+
export declare const SOURCE_CAPABILITIES: readonly ["requirements", "interfaces", "design"];
|
|
5
|
+
export declare const DEFAULT_TASK_CATEGORIES: Record<string, {
|
|
6
|
+
notes: string;
|
|
7
|
+
}>;
|
|
8
|
+
export declare const HookSchema: z.ZodObject<{
|
|
9
|
+
id: z.ZodString;
|
|
10
|
+
stage: z.ZodEnum<{
|
|
11
|
+
collect: "collect";
|
|
12
|
+
parse: "parse";
|
|
13
|
+
tech: "tech";
|
|
14
|
+
plan: "plan";
|
|
15
|
+
execute: "execute";
|
|
16
|
+
verify: "verify";
|
|
17
|
+
}>;
|
|
18
|
+
type: z.ZodOptional<z.ZodEnum<{
|
|
19
|
+
command: "command";
|
|
20
|
+
skill: "skill";
|
|
21
|
+
notes: "notes";
|
|
22
|
+
file: "file";
|
|
23
|
+
check: "check";
|
|
24
|
+
}>>;
|
|
25
|
+
capability: z.ZodOptional<z.ZodEnum<{
|
|
26
|
+
requirements: "requirements";
|
|
27
|
+
interfaces: "interfaces";
|
|
28
|
+
design: "design";
|
|
29
|
+
}>>;
|
|
30
|
+
use: z.ZodOptional<z.ZodString>;
|
|
31
|
+
operation: z.ZodOptional<z.ZodLiteral<"preview">>;
|
|
32
|
+
input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
33
|
+
inputFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
34
|
+
name: z.ZodOptional<z.ZodString>;
|
|
35
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
36
|
+
taskCategories: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
37
|
+
command: z.ZodOptional<z.ZodString>;
|
|
38
|
+
target: z.ZodOptional<z.ZodEnum<{
|
|
39
|
+
project: "project";
|
|
40
|
+
inScopeChangedFiles: "inScopeChangedFiles";
|
|
41
|
+
}>>;
|
|
42
|
+
include: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
43
|
+
exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
44
|
+
allowEmpty: z.ZodOptional<z.ZodBoolean>;
|
|
45
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
46
|
+
purpose: z.ZodOptional<z.ZodString>;
|
|
47
|
+
path: z.ZodOptional<z.ZodString>;
|
|
48
|
+
category: z.ZodOptional<z.ZodString>;
|
|
49
|
+
description: z.ZodOptional<z.ZodString>;
|
|
50
|
+
}, z.core.$strict>;
|
|
51
|
+
/** 插件最低版本:与插件 manifest 的 version 比较,只接受 major.minor.patch。 */
|
|
52
|
+
export declare const PluginMinVersionSchema: z.ZodString;
|
|
53
|
+
declare const ProjectConfigObjectSchema: z.ZodObject<{
|
|
54
|
+
schema: z.ZodLiteral<"spec-product">;
|
|
55
|
+
taskCategories: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodObject<{
|
|
56
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
57
|
+
}, z.core.$strict>>>>;
|
|
58
|
+
hooks: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
59
|
+
id: z.ZodString;
|
|
60
|
+
stage: z.ZodEnum<{
|
|
61
|
+
collect: "collect";
|
|
62
|
+
parse: "parse";
|
|
63
|
+
tech: "tech";
|
|
64
|
+
plan: "plan";
|
|
65
|
+
execute: "execute";
|
|
66
|
+
verify: "verify";
|
|
67
|
+
}>;
|
|
68
|
+
type: z.ZodOptional<z.ZodEnum<{
|
|
69
|
+
command: "command";
|
|
70
|
+
skill: "skill";
|
|
71
|
+
notes: "notes";
|
|
72
|
+
file: "file";
|
|
73
|
+
check: "check";
|
|
74
|
+
}>>;
|
|
75
|
+
capability: z.ZodOptional<z.ZodEnum<{
|
|
76
|
+
requirements: "requirements";
|
|
77
|
+
interfaces: "interfaces";
|
|
78
|
+
design: "design";
|
|
79
|
+
}>>;
|
|
80
|
+
use: z.ZodOptional<z.ZodString>;
|
|
81
|
+
operation: z.ZodOptional<z.ZodLiteral<"preview">>;
|
|
82
|
+
input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
83
|
+
inputFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
84
|
+
name: z.ZodOptional<z.ZodString>;
|
|
85
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
86
|
+
taskCategories: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
87
|
+
command: z.ZodOptional<z.ZodString>;
|
|
88
|
+
target: z.ZodOptional<z.ZodEnum<{
|
|
89
|
+
project: "project";
|
|
90
|
+
inScopeChangedFiles: "inScopeChangedFiles";
|
|
91
|
+
}>>;
|
|
92
|
+
include: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
93
|
+
exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
94
|
+
allowEmpty: z.ZodOptional<z.ZodBoolean>;
|
|
95
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
96
|
+
purpose: z.ZodOptional<z.ZodString>;
|
|
97
|
+
path: z.ZodOptional<z.ZodString>;
|
|
98
|
+
category: z.ZodOptional<z.ZodString>;
|
|
99
|
+
description: z.ZodOptional<z.ZodString>;
|
|
100
|
+
}, z.core.$strict>>>;
|
|
101
|
+
verify: z.ZodOptional<z.ZodObject<{
|
|
102
|
+
reviewScope: z.ZodOptional<z.ZodObject<{
|
|
103
|
+
includeDirs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
104
|
+
excludeDirs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
105
|
+
}, z.core.$strict>>;
|
|
106
|
+
failurePolicy: z.ZodOptional<z.ZodObject<{
|
|
107
|
+
failOnHookFailure: z.ZodOptional<z.ZodBoolean>;
|
|
108
|
+
failOnRequiredCommandFailure: z.ZodOptional<z.ZodBoolean>;
|
|
109
|
+
}, z.core.$strict>>;
|
|
110
|
+
}, z.core.$strict>>;
|
|
111
|
+
plugins: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
112
|
+
name: z.ZodString;
|
|
113
|
+
marketplace: z.ZodString;
|
|
114
|
+
minVersion: z.ZodOptional<z.ZodString>;
|
|
115
|
+
}, z.core.$strict>>>;
|
|
116
|
+
marketplaces: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
117
|
+
name: z.ZodString;
|
|
118
|
+
repo: z.ZodString;
|
|
119
|
+
ref: z.ZodString;
|
|
120
|
+
}, z.core.$strict>>>;
|
|
121
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
122
|
+
claude: "claude";
|
|
123
|
+
codex: "codex";
|
|
124
|
+
cursor: "cursor";
|
|
125
|
+
}>>>;
|
|
126
|
+
}, z.core.$strict>;
|
|
127
|
+
export declare const ProjectConfigPatchSchema: z.ZodObject<{
|
|
128
|
+
schema: z.ZodOptional<z.ZodLiteral<"spec-product">>;
|
|
129
|
+
taskCategories: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodObject<{
|
|
130
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
131
|
+
}, z.core.$strict>>>>>;
|
|
132
|
+
hooks: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
133
|
+
id: z.ZodString;
|
|
134
|
+
stage: z.ZodEnum<{
|
|
135
|
+
collect: "collect";
|
|
136
|
+
parse: "parse";
|
|
137
|
+
tech: "tech";
|
|
138
|
+
plan: "plan";
|
|
139
|
+
execute: "execute";
|
|
140
|
+
verify: "verify";
|
|
141
|
+
}>;
|
|
142
|
+
type: z.ZodOptional<z.ZodEnum<{
|
|
143
|
+
command: "command";
|
|
144
|
+
skill: "skill";
|
|
145
|
+
notes: "notes";
|
|
146
|
+
file: "file";
|
|
147
|
+
check: "check";
|
|
148
|
+
}>>;
|
|
149
|
+
capability: z.ZodOptional<z.ZodEnum<{
|
|
150
|
+
requirements: "requirements";
|
|
151
|
+
interfaces: "interfaces";
|
|
152
|
+
design: "design";
|
|
153
|
+
}>>;
|
|
154
|
+
use: z.ZodOptional<z.ZodString>;
|
|
155
|
+
operation: z.ZodOptional<z.ZodLiteral<"preview">>;
|
|
156
|
+
input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
157
|
+
inputFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
158
|
+
name: z.ZodOptional<z.ZodString>;
|
|
159
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
160
|
+
taskCategories: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
161
|
+
command: z.ZodOptional<z.ZodString>;
|
|
162
|
+
target: z.ZodOptional<z.ZodEnum<{
|
|
163
|
+
project: "project";
|
|
164
|
+
inScopeChangedFiles: "inScopeChangedFiles";
|
|
165
|
+
}>>;
|
|
166
|
+
include: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
167
|
+
exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
168
|
+
allowEmpty: z.ZodOptional<z.ZodBoolean>;
|
|
169
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
170
|
+
purpose: z.ZodOptional<z.ZodString>;
|
|
171
|
+
path: z.ZodOptional<z.ZodString>;
|
|
172
|
+
category: z.ZodOptional<z.ZodString>;
|
|
173
|
+
description: z.ZodOptional<z.ZodString>;
|
|
174
|
+
}, z.core.$strict>>>>;
|
|
175
|
+
verify: z.ZodOptional<z.ZodOptional<z.ZodObject<{
|
|
176
|
+
reviewScope: z.ZodOptional<z.ZodObject<{
|
|
177
|
+
includeDirs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
178
|
+
excludeDirs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
179
|
+
}, z.core.$strict>>;
|
|
180
|
+
failurePolicy: z.ZodOptional<z.ZodObject<{
|
|
181
|
+
failOnHookFailure: z.ZodOptional<z.ZodBoolean>;
|
|
182
|
+
failOnRequiredCommandFailure: z.ZodOptional<z.ZodBoolean>;
|
|
183
|
+
}, z.core.$strict>>;
|
|
184
|
+
}, z.core.$strict>>>;
|
|
185
|
+
plugins: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
186
|
+
name: z.ZodString;
|
|
187
|
+
marketplace: z.ZodString;
|
|
188
|
+
minVersion: z.ZodOptional<z.ZodString>;
|
|
189
|
+
}, z.core.$strict>>>>;
|
|
190
|
+
marketplaces: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
191
|
+
name: z.ZodString;
|
|
192
|
+
repo: z.ZodString;
|
|
193
|
+
ref: z.ZodString;
|
|
194
|
+
}, z.core.$strict>>>>;
|
|
195
|
+
tools: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
196
|
+
claude: "claude";
|
|
197
|
+
codex: "codex";
|
|
198
|
+
cursor: "cursor";
|
|
199
|
+
}>>>>;
|
|
200
|
+
}, z.core.$strict>;
|
|
201
|
+
export declare const ProjectConfigSchema: z.ZodObject<{
|
|
202
|
+
schema: z.ZodLiteral<"spec-product">;
|
|
203
|
+
taskCategories: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodObject<{
|
|
204
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
205
|
+
}, z.core.$strict>>>>;
|
|
206
|
+
hooks: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
207
|
+
id: z.ZodString;
|
|
208
|
+
stage: z.ZodEnum<{
|
|
209
|
+
collect: "collect";
|
|
210
|
+
parse: "parse";
|
|
211
|
+
tech: "tech";
|
|
212
|
+
plan: "plan";
|
|
213
|
+
execute: "execute";
|
|
214
|
+
verify: "verify";
|
|
215
|
+
}>;
|
|
216
|
+
type: z.ZodOptional<z.ZodEnum<{
|
|
217
|
+
command: "command";
|
|
218
|
+
skill: "skill";
|
|
219
|
+
notes: "notes";
|
|
220
|
+
file: "file";
|
|
221
|
+
check: "check";
|
|
222
|
+
}>>;
|
|
223
|
+
capability: z.ZodOptional<z.ZodEnum<{
|
|
224
|
+
requirements: "requirements";
|
|
225
|
+
interfaces: "interfaces";
|
|
226
|
+
design: "design";
|
|
227
|
+
}>>;
|
|
228
|
+
use: z.ZodOptional<z.ZodString>;
|
|
229
|
+
operation: z.ZodOptional<z.ZodLiteral<"preview">>;
|
|
230
|
+
input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
231
|
+
inputFiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
232
|
+
name: z.ZodOptional<z.ZodString>;
|
|
233
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
234
|
+
taskCategories: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
235
|
+
command: z.ZodOptional<z.ZodString>;
|
|
236
|
+
target: z.ZodOptional<z.ZodEnum<{
|
|
237
|
+
project: "project";
|
|
238
|
+
inScopeChangedFiles: "inScopeChangedFiles";
|
|
239
|
+
}>>;
|
|
240
|
+
include: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
241
|
+
exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
242
|
+
allowEmpty: z.ZodOptional<z.ZodBoolean>;
|
|
243
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
244
|
+
purpose: z.ZodOptional<z.ZodString>;
|
|
245
|
+
path: z.ZodOptional<z.ZodString>;
|
|
246
|
+
category: z.ZodOptional<z.ZodString>;
|
|
247
|
+
description: z.ZodOptional<z.ZodString>;
|
|
248
|
+
}, z.core.$strict>>>;
|
|
249
|
+
verify: z.ZodOptional<z.ZodObject<{
|
|
250
|
+
reviewScope: z.ZodOptional<z.ZodObject<{
|
|
251
|
+
includeDirs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
252
|
+
excludeDirs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
253
|
+
}, z.core.$strict>>;
|
|
254
|
+
failurePolicy: z.ZodOptional<z.ZodObject<{
|
|
255
|
+
failOnHookFailure: z.ZodOptional<z.ZodBoolean>;
|
|
256
|
+
failOnRequiredCommandFailure: z.ZodOptional<z.ZodBoolean>;
|
|
257
|
+
}, z.core.$strict>>;
|
|
258
|
+
}, z.core.$strict>>;
|
|
259
|
+
plugins: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
260
|
+
name: z.ZodString;
|
|
261
|
+
marketplace: z.ZodString;
|
|
262
|
+
minVersion: z.ZodOptional<z.ZodString>;
|
|
263
|
+
}, z.core.$strict>>>;
|
|
264
|
+
marketplaces: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
265
|
+
name: z.ZodString;
|
|
266
|
+
repo: z.ZodString;
|
|
267
|
+
ref: z.ZodString;
|
|
268
|
+
}, z.core.$strict>>>;
|
|
269
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
270
|
+
claude: "claude";
|
|
271
|
+
codex: "codex";
|
|
272
|
+
cursor: "cursor";
|
|
273
|
+
}>>>;
|
|
274
|
+
}, z.core.$strict>;
|
|
275
|
+
export type ProjectConfig = z.infer<typeof ProjectConfigObjectSchema>;
|
|
276
|
+
export type ProjectHook = z.infer<typeof HookSchema>;
|
|
277
|
+
export declare function effectiveTaskCategories(config: Pick<ProjectConfig, 'taskCategories'>): Record<string, {
|
|
278
|
+
notes: string;
|
|
279
|
+
}>;
|
|
280
|
+
export {};
|
|
281
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const HOOK_STAGES = ['collect', 'parse', 'tech', 'plan', 'execute', 'verify'];
|
|
3
|
+
export const HOOK_TYPES = ['command', 'skill', 'notes', 'file', 'check'];
|
|
4
|
+
export const SOURCE_CAPABILITIES = ['requirements', 'interfaces', 'design'];
|
|
5
|
+
export const DEFAULT_TASK_CATEGORIES = {
|
|
6
|
+
ui: { notes: '遵循项目 UI 约定,优先复用现有组件与设计 token。' },
|
|
7
|
+
logic: { notes: '保持业务逻辑内聚,避免跨边界改动。' },
|
|
8
|
+
'api-codegen': { notes: '仅通过已声明的接口源生成客户端或模型代码。' },
|
|
9
|
+
test: { notes: '覆盖本次变更的可观察行为与回归路径。' },
|
|
10
|
+
infra: { notes: '基础设施改动需可回滚,并写明影响范围。' },
|
|
11
|
+
};
|
|
12
|
+
const PathTokenSchema = z.string().min(1).refine((value) => value !== '.' && value !== '..' && !/[\\/\0]/.test(value), '标识不能包含路径语义');
|
|
13
|
+
const CategoryIdSchema = PathTokenSchema.refine((value) => !['__proto__', 'prototype', 'constructor'].includes(value), '分类 ID 不能使用保留对象键');
|
|
14
|
+
const RelativePathSchema = z.string().min(1).refine((value) => {
|
|
15
|
+
const normalized = value.replace(/\\/g, '/');
|
|
16
|
+
return !normalized.startsWith('/')
|
|
17
|
+
&& !/^[A-Za-z]:\//.test(normalized)
|
|
18
|
+
&& !normalized.includes('\0')
|
|
19
|
+
&& !normalized.split('/').includes('..');
|
|
20
|
+
}, '必须是未越界的相对路径');
|
|
21
|
+
export const HookSchema = z.object({
|
|
22
|
+
id: PathTokenSchema,
|
|
23
|
+
stage: z.enum(HOOK_STAGES),
|
|
24
|
+
type: z.enum(HOOK_TYPES).optional(),
|
|
25
|
+
capability: z.enum(SOURCE_CAPABILITIES).optional(),
|
|
26
|
+
use: z.string().min(1).optional(),
|
|
27
|
+
operation: z.literal('preview').optional(),
|
|
28
|
+
input: z.record(z.string(), z.unknown()).optional(),
|
|
29
|
+
inputFiles: z.array(RelativePathSchema).optional(),
|
|
30
|
+
name: z.string().min(1).optional(),
|
|
31
|
+
notes: z.string().min(1).optional(),
|
|
32
|
+
taskCategories: z.array(CategoryIdSchema).optional(),
|
|
33
|
+
command: z.string().min(1).optional(),
|
|
34
|
+
target: z.enum(['project', 'inScopeChangedFiles']).optional(),
|
|
35
|
+
include: z.array(z.string()).optional(),
|
|
36
|
+
exclude: z.array(z.string()).optional(),
|
|
37
|
+
allowEmpty: z.boolean().optional(),
|
|
38
|
+
required: z.boolean().optional(),
|
|
39
|
+
purpose: z.string().optional(),
|
|
40
|
+
path: RelativePathSchema.optional(),
|
|
41
|
+
category: z.string().min(1).optional(),
|
|
42
|
+
description: z.string().min(1).optional(),
|
|
43
|
+
}).strict().superRefine((hook, ctx) => {
|
|
44
|
+
const type = hook.type;
|
|
45
|
+
const allowedByStage = {
|
|
46
|
+
collect: [undefined, 'skill'],
|
|
47
|
+
parse: [...HOOK_TYPES],
|
|
48
|
+
tech: [...HOOK_TYPES],
|
|
49
|
+
plan: [...HOOK_TYPES],
|
|
50
|
+
execute: [...HOOK_TYPES],
|
|
51
|
+
verify: ['command', 'skill', 'file', 'check'],
|
|
52
|
+
};
|
|
53
|
+
if (!allowedByStage[hook.stage].includes(type)) {
|
|
54
|
+
ctx.addIssue({ code: 'custom', message: `${hook.stage} stage 不支持 type: ${String(type)}` });
|
|
55
|
+
}
|
|
56
|
+
if (hook.stage === 'collect' && !hook.use && type !== 'skill') {
|
|
57
|
+
ctx.addIssue({ code: 'custom', message: `collect hook ${hook.id} 需要 use 或 type: skill` });
|
|
58
|
+
}
|
|
59
|
+
if (hook.stage !== 'collect' && !type) {
|
|
60
|
+
ctx.addIssue({ code: 'custom', message: `${hook.stage} hook ${hook.id} 需要 type` });
|
|
61
|
+
}
|
|
62
|
+
if (type === 'notes' && !hook.notes)
|
|
63
|
+
ctx.addIssue({ code: 'custom', message: `notes hook ${hook.id} 需要 notes` });
|
|
64
|
+
if (type === 'command' && !hook.command && !hook.use)
|
|
65
|
+
ctx.addIssue({ code: 'custom', message: `command hook ${hook.id} 需要 command` });
|
|
66
|
+
if (type === 'skill' && !hook.name && !hook.use)
|
|
67
|
+
ctx.addIssue({ code: 'custom', message: `skill hook ${hook.id} 需要 name` });
|
|
68
|
+
if (type === 'file' && !hook.path)
|
|
69
|
+
ctx.addIssue({ code: 'custom', message: `file hook ${hook.id} 需要 path` });
|
|
70
|
+
if (type === 'check' && (!hook.category || !hook.description)) {
|
|
71
|
+
ctx.addIssue({ code: 'custom', message: `check hook ${hook.id} 需要 category 与 description` });
|
|
72
|
+
}
|
|
73
|
+
if (hook.use && [hook.name, hook.command, hook.path, hook.notes, hook.category, hook.description].some((value) => value !== undefined)) {
|
|
74
|
+
ctx.addIssue({ code: 'custom', message: `hook ${hook.id} 的 use 与内联实现互斥` });
|
|
75
|
+
}
|
|
76
|
+
if (hook.use && hook.stage !== 'collect' && type !== 'command' && type !== 'skill') {
|
|
77
|
+
ctx.addIssue({ code: 'custom', message: `插件 Hook 仅支持 command/skill: ${hook.id}` });
|
|
78
|
+
}
|
|
79
|
+
if (hook.operation && (!hook.use || hook.type !== 'command' || hook.stage === 'collect')) {
|
|
80
|
+
ctx.addIssue({ code: 'custom', message: `preview 需要非 collect 的 command use: ${hook.id}` });
|
|
81
|
+
}
|
|
82
|
+
if (!hook.use && (hook.input || hook.inputFiles)) {
|
|
83
|
+
ctx.addIssue({ code: 'custom', message: `结构化参数需要插件 use: ${hook.id}` });
|
|
84
|
+
}
|
|
85
|
+
if (hook.stage === 'collect' && (hook.input || hook.inputFiles)) {
|
|
86
|
+
ctx.addIssue({ code: 'custom', message: `collect 沿用旧来源协议,不接受结构化操作参数: ${hook.id}` });
|
|
87
|
+
}
|
|
88
|
+
if (hook.taskCategories && hook.stage !== 'execute') {
|
|
89
|
+
ctx.addIssue({ code: 'custom', message: `taskCategories 只允许用于 execute hook: ${hook.id}` });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
const TaskCategorySchema = z.object({ notes: z.string().optional() }).strict();
|
|
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
|
+
const MarketplaceSchema = z.object({
|
|
95
|
+
name: PathTokenSchema,
|
|
96
|
+
repo: z.string().min(1),
|
|
97
|
+
ref: ImmutableMarketplaceRefSchema,
|
|
98
|
+
}).strict();
|
|
99
|
+
/** 插件最低版本:与插件 manifest 的 version 比较,只接受 major.minor.patch。 */
|
|
100
|
+
export const PluginMinVersionSchema = z.string().regex(/^\d+\.\d+\.\d+$/, 'minVersion 必须是 major.minor.patch');
|
|
101
|
+
const PluginDependencySchema = z.object({
|
|
102
|
+
name: PathTokenSchema,
|
|
103
|
+
marketplace: PathTokenSchema,
|
|
104
|
+
minVersion: PluginMinVersionSchema.optional(),
|
|
105
|
+
}).strict();
|
|
106
|
+
const VerifySchema = z.object({
|
|
107
|
+
reviewScope: z.object({
|
|
108
|
+
includeDirs: z.array(z.string()).optional(),
|
|
109
|
+
excludeDirs: z.array(z.string()).optional(),
|
|
110
|
+
}).strict().optional(),
|
|
111
|
+
failurePolicy: z.object({
|
|
112
|
+
failOnHookFailure: z.boolean().optional(),
|
|
113
|
+
failOnRequiredCommandFailure: z.boolean().optional(),
|
|
114
|
+
}).strict().optional(),
|
|
115
|
+
}).strict();
|
|
116
|
+
const ProjectConfigObjectSchema = z.object({
|
|
117
|
+
schema: z.literal('spec-product'),
|
|
118
|
+
taskCategories: z.record(CategoryIdSchema, TaskCategorySchema.nullable()).optional(),
|
|
119
|
+
hooks: z.array(HookSchema).optional(),
|
|
120
|
+
verify: VerifySchema.optional(),
|
|
121
|
+
plugins: z.array(PluginDependencySchema).optional(),
|
|
122
|
+
marketplaces: z.array(MarketplaceSchema).optional(),
|
|
123
|
+
tools: z.array(z.enum(['claude', 'codex', 'cursor'])).optional(),
|
|
124
|
+
}).strict();
|
|
125
|
+
export const ProjectConfigPatchSchema = ProjectConfigObjectSchema.partial();
|
|
126
|
+
export const ProjectConfigSchema = ProjectConfigObjectSchema.superRefine((config, ctx) => {
|
|
127
|
+
const ids = new Set();
|
|
128
|
+
const collectCapabilities = new Set();
|
|
129
|
+
for (const [index, hook] of (config.hooks ?? []).entries()) {
|
|
130
|
+
if (ids.has(hook.id))
|
|
131
|
+
ctx.addIssue({ code: 'custom', path: ['hooks', index, 'id'], message: `重复 hook id: ${hook.id}` });
|
|
132
|
+
ids.add(hook.id);
|
|
133
|
+
if (hook.stage === 'collect' && hook.capability) {
|
|
134
|
+
if (collectCapabilities.has(hook.capability)) {
|
|
135
|
+
ctx.addIssue({ code: 'custom', path: ['hooks', index, 'capability'], message: `重复 collect capability: ${hook.capability}` });
|
|
136
|
+
}
|
|
137
|
+
collectCapabilities.add(hook.capability);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const marketplaceNames = new Set();
|
|
141
|
+
for (const [index, marketplace] of (config.marketplaces ?? []).entries()) {
|
|
142
|
+
if (marketplaceNames.has(marketplace.name)) {
|
|
143
|
+
ctx.addIssue({ code: 'custom', path: ['marketplaces', index, 'name'], message: `重复 marketplace: ${marketplace.name}` });
|
|
144
|
+
}
|
|
145
|
+
marketplaceNames.add(marketplace.name);
|
|
146
|
+
}
|
|
147
|
+
const pluginSelectors = new Set();
|
|
148
|
+
for (const [index, plugin] of (config.plugins ?? []).entries()) {
|
|
149
|
+
const selector = `${plugin.name}@${plugin.marketplace}`;
|
|
150
|
+
if (pluginSelectors.has(selector)) {
|
|
151
|
+
ctx.addIssue({ code: 'custom', path: ['plugins', index], message: `重复 plugin: ${selector}` });
|
|
152
|
+
}
|
|
153
|
+
pluginSelectors.add(selector);
|
|
154
|
+
}
|
|
155
|
+
if (new Set(config.tools ?? []).size !== (config.tools ?? []).length) {
|
|
156
|
+
ctx.addIssue({ code: 'custom', path: ['tools'], message: 'tools 不能重复' });
|
|
157
|
+
}
|
|
158
|
+
const categories = new Set(Object.keys(effectiveTaskCategories(config)));
|
|
159
|
+
for (const [index, hook] of (config.hooks ?? []).entries()) {
|
|
160
|
+
for (const category of hook.taskCategories ?? []) {
|
|
161
|
+
if (!categories.has(category)) {
|
|
162
|
+
ctx.addIssue({ code: 'custom', path: ['hooks', index, 'taskCategories'], message: `未知 task category: ${category}` });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
export function effectiveTaskCategories(config) {
|
|
168
|
+
const merged = Object.assign(Object.create(null), DEFAULT_TASK_CATEGORIES);
|
|
169
|
+
for (const [id, value] of Object.entries(config.taskCategories ?? {})) {
|
|
170
|
+
if (value === null) {
|
|
171
|
+
delete merged[id];
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
merged[id] = { notes: value.notes ?? merged[id]?.notes ?? '' };
|
|
175
|
+
}
|
|
176
|
+
return merged;
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { exists, ensureDir, removePath, writeText } from '../shared/fs.js';
|
|
3
|
+
import { AppError } from '../shared/errors.js';
|
|
4
|
+
import { changeRoot, stampArchivePath } from './change.js';
|
|
5
|
+
import { appendEvent } from './state.js';
|
|
6
|
+
import { NODES } from './types.js';
|
|
7
|
+
import { requireChange } from './change.js';
|
|
8
|
+
import { renameSync } from 'node:fs';
|
|
9
|
+
import { loadVerifiedReport } from './verify.js';
|
|
10
|
+
import { collectPendingWork, renderPendingWork } from './todo.js';
|
|
11
|
+
import { loadConfig } from '../config/load.js';
|
|
12
|
+
import { resolveInside } from '../shared/paths.js';
|
|
13
|
+
export function archiveChange(projectRoot, changeName) {
|
|
14
|
+
const state = requireChange(projectRoot, changeName);
|
|
15
|
+
if (state.node !== NODES.COMPLETED) {
|
|
16
|
+
throw new AppError('not_completed', '只有 completed 的 change 才能归档');
|
|
17
|
+
}
|
|
18
|
+
const from = changeRoot(projectRoot, changeName);
|
|
19
|
+
const toRel = stampArchivePath(projectRoot, changeName);
|
|
20
|
+
const to = resolveInside(projectRoot, toRel, 'archive_path_outside_project');
|
|
21
|
+
if (!exists(from))
|
|
22
|
+
throw new AppError('change_not_found', `找不到 change: ${changeName}`);
|
|
23
|
+
const report = state.verifyRunId ? loadVerifiedReport(projectRoot, changeName, state.verifyRunId) : undefined;
|
|
24
|
+
const pending = report?.pendingWork ?? collectPendingWork(projectRoot, changeName, loadConfig(projectRoot));
|
|
25
|
+
writeText(resolveInside(from, 'reports/pending-work.md'), renderPendingWork(pending));
|
|
26
|
+
ensureDir(path.dirname(to));
|
|
27
|
+
appendEvent(projectRoot, changeName, 'change.archived', { to: toRel });
|
|
28
|
+
removePath(path.join(from, '.opsx', 'workers'));
|
|
29
|
+
removePath(path.join(from, '.opsx', 'source-update-backup'));
|
|
30
|
+
renameSync(from, to);
|
|
31
|
+
return { from: path.relative(projectRoot, from), to: toRel };
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=archive.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ChangeState } from './types.js';
|
|
2
|
+
export declare function changeRoot(projectRoot: string, changeName: string): string;
|
|
3
|
+
export declare function createChange(projectRoot: string, name: string, description?: string): ChangeState;
|
|
4
|
+
export declare function listChangeNames(projectRoot: string): string[];
|
|
5
|
+
export declare function requireChange(projectRoot: string, changeName: string): ChangeState;
|
|
6
|
+
export declare function resolveChangeName(projectRoot: string, requested?: string): string;
|
|
7
|
+
export declare function artifactPath(projectRoot: string, changeName: string, relative: string): string;
|
|
8
|
+
export declare function artifactRel(changeName: string, relative: string): string;
|
|
9
|
+
export declare function stampArchivePath(projectRoot: string, changeName: string): string;
|
|
10
|
+
export declare function assertChangeName(name: string): void;
|
|
11
|
+
export declare function readOptional(projectRoot: string, changeName: string, relative: string): string | null;
|
|
12
|
+
//# sourceMappingURL=change.d.ts.map
|