@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/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { stringify as stringifyYaml } from 'yaml';
|
|
4
3
|
import { appendText, exists, writeText, ensureDir, writeJson } from './shared/fs.js';
|
|
5
|
-
import { findProjectRoot, loadConfig, saveConfig, defaultConfig, configPathOf } from './config/load.js';
|
|
6
|
-
import {
|
|
4
|
+
import { findProjectRoot, loadConfig, loadProjectConfig, saveConfig, defaultConfig, configPathOf } from './config/load.js';
|
|
5
|
+
import { usePreset } from './config/preset-use.js';
|
|
7
6
|
import { effectiveTaskCategories } from './config/schema.js';
|
|
7
|
+
import { syncPresetCache } from './config/preset-repo.js';
|
|
8
8
|
import { createChange, listChangeNames, requireChange, resolveChangeName } from './core/change.js';
|
|
9
9
|
import { nextAction } from './core/flow.js';
|
|
10
10
|
import { buildInstructions } from './core/instructions.js';
|
|
@@ -36,39 +36,33 @@ export function initProject(input) {
|
|
|
36
36
|
ensureDir(resolveInside(projectRoot, path.posix.join(OPENSPEC_DIR, 'changes'), 'openspec_path_outside_project'));
|
|
37
37
|
ensureDir(resolveInside(projectRoot, path.posix.join(OPENSPEC_DIR, 'archive'), 'openspec_path_outside_project'));
|
|
38
38
|
const hadConfig = Boolean(configPathOf(projectRoot));
|
|
39
|
-
let config = hadConfig ?
|
|
39
|
+
let config = hadConfig ? loadProjectConfig(projectRoot) : defaultConfig();
|
|
40
40
|
if (input.tools !== undefined)
|
|
41
41
|
config = { ...config, tools: input.tools };
|
|
42
|
+
if (config.preset && !input.preset) {
|
|
43
|
+
progress(`同步 preset ${config.preset.id}(${config.preset.ref})`);
|
|
44
|
+
syncPresetCache(projectRoot, config.preset);
|
|
45
|
+
}
|
|
46
|
+
const rewriteConfig = !hadConfig || input.tools !== undefined;
|
|
47
|
+
if (rewriteConfig)
|
|
48
|
+
saveConfig(projectRoot, config);
|
|
42
49
|
if (input.preset) {
|
|
43
|
-
progress(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
progress(` 保留宿主 marketplace ${kept.name}@${kept.ref.slice(0, 12)}(preset 基线 ${kept.presetRef.slice(0, 12)})`);
|
|
47
|
-
}
|
|
48
|
-
config = mergePreset(config, preset);
|
|
49
|
-
scaffold = scaffoldPreset(projectRoot, preset);
|
|
50
|
+
progress(`引用 preset ${input.preset.id}(${input.preset.ref})`);
|
|
51
|
+
scaffold = usePreset({ projectRoot, ...input.preset }).scaffold;
|
|
52
|
+
config = loadProjectConfig(projectRoot);
|
|
50
53
|
for (const item of scaffold) {
|
|
51
54
|
progress(item.status === 'created' ? ` 生成 ${item.path}(模板,需填写)` : ` 保留已有 ${item.path}`);
|
|
52
55
|
}
|
|
53
|
-
const lockedMarketplaces = (preset.marketplaces ?? []).map((item) => requireImmutableMarketplace(item));
|
|
54
|
-
writeText(resolveInside(projectRoot, path.posix.join(OPENSPEC_DIR, 'preset-lock.yaml'), 'preset_lock_outside_project'), stringifyYaml({
|
|
55
|
-
schemaVersion: preset.schemaVersion,
|
|
56
|
-
id: preset.id,
|
|
57
|
-
version: preset.version,
|
|
58
|
-
marketplaces: lockedMarketplaces,
|
|
59
|
-
plugins: preset.plugins ?? [],
|
|
60
|
-
}, { lineWidth: 0 }));
|
|
61
56
|
}
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
saveConfig(projectRoot, config);
|
|
57
|
+
// 插件与宿主按生效配置安装:preset 基线声明的插件也要装上。
|
|
58
|
+
const effective = loadConfig(projectRoot);
|
|
59
|
+
const presetPlugins = effective.plugins ?? [];
|
|
60
|
+
const presetMarketplaces = (effective.marketplaces ?? []).map((item) => requireImmutableMarketplace(item));
|
|
67
61
|
writeInstallCli(projectRoot, VERSION, PACKAGE_NAME);
|
|
68
62
|
appendGitignore(projectRoot);
|
|
69
|
-
const hosts =
|
|
63
|
+
const hosts = effective.tools ?? [];
|
|
70
64
|
const mode = input.skipPlugins ? 'skip' : input.yes ? 'install' : 'pending';
|
|
71
|
-
progress(rewriteConfig
|
|
65
|
+
progress(rewriteConfig || input.preset
|
|
72
66
|
? `写入 openspec/config.yaml 与 install-cli.mjs`
|
|
73
67
|
: `刷新 install-cli.mjs 与宿主交付(保留已有 config.yaml)`);
|
|
74
68
|
if (hosts.length > 0)
|
|
@@ -120,7 +114,7 @@ export function initProject(input) {
|
|
|
120
114
|
if (pluginInstalls.length > 0)
|
|
121
115
|
writeInstallReceipt(projectRoot, pluginInstalls);
|
|
122
116
|
// 入口在安装时就解析一遍;marketplace 未锁定已在上面记为 issue,不重复记。
|
|
123
|
-
for (const entry of inspectHookEntries(projectRoot,
|
|
117
|
+
for (const entry of inspectHookEntries(projectRoot, effective)) {
|
|
124
118
|
if (!entry.ok && entry.code !== 'marketplace_not_locked') {
|
|
125
119
|
issues.push({ scope: `entry:${entry.hook}`, error: `${entry.code}: ${entry.error}` });
|
|
126
120
|
}
|
|
@@ -154,7 +148,7 @@ export function initProject(input) {
|
|
|
154
148
|
});
|
|
155
149
|
return result;
|
|
156
150
|
}
|
|
157
|
-
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,
|
|
151
|
+
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, };
|
|
158
152
|
function appendGitignore(projectRoot) {
|
|
159
153
|
const file = resolveInside(projectRoot, '.gitignore', 'gitignore_outside_project');
|
|
160
154
|
const entries = [
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { isImmutableRef } from '../shared/git-snapshot.js';
|
|
2
3
|
declare const RecordSchema: z.ZodObject<{
|
|
3
4
|
name: z.ZodString;
|
|
4
5
|
repo: z.ZodOptional<z.ZodString>;
|
|
@@ -66,6 +67,5 @@ export declare function inspectMarketplaceDependencies(projectRoot: string, mark
|
|
|
66
67
|
marketplace: string;
|
|
67
68
|
minVersion?: string;
|
|
68
69
|
}>): DependencyCheck[];
|
|
69
|
-
export
|
|
70
|
-
export {};
|
|
70
|
+
export { isImmutableRef };
|
|
71
71
|
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -2,9 +2,9 @@ import path from 'node:path';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { renameSync } from 'node:fs';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { execFile } from '../shared/exec.js';
|
|
6
5
|
import { copyPath, ensureDir, exists, readJson, removePath, writeJson } from '../shared/fs.js';
|
|
7
6
|
import { digestDirectory } from '../shared/hash.js';
|
|
7
|
+
import { isImmutableRef, snapshotGitRef } from '../shared/git-snapshot.js';
|
|
8
8
|
import { AppError } from '../shared/errors.js';
|
|
9
9
|
import { resolveInside } from '../shared/paths.js';
|
|
10
10
|
import { compareVersions } from '../shared/version.js';
|
|
@@ -223,57 +223,10 @@ function addGitMarketplace(input) {
|
|
|
223
223
|
return { name, repo, ref, resolvedSha: ref, digest: digestDirectory(cached), cachePath: cached };
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const checkout = execFile('git', ['checkout', '--detach', 'FETCH_HEAD'], { cwd: temporary });
|
|
231
|
-
if (!checkout.ok)
|
|
232
|
-
throw new AppError('marketplace_checkout_failed', checkout.stderr || checkout.stdout);
|
|
233
|
-
const resolved = execFile('git', ['rev-parse', 'HEAD'], { cwd: temporary });
|
|
234
|
-
if (!resolved.ok)
|
|
235
|
-
throw new AppError('marketplace_revision_failed', resolved.stderr || resolved.stdout);
|
|
236
|
-
const resolvedSha = resolved.stdout.trim();
|
|
237
|
-
removePath(path.join(temporary, '.git'));
|
|
238
|
-
const cachePath = marketplaceCacheDir(name, resolvedSha);
|
|
239
|
-
if (exists(cachePath))
|
|
240
|
-
removePath(temporary);
|
|
241
|
-
else {
|
|
242
|
-
ensureDir(path.dirname(cachePath));
|
|
243
|
-
renameSync(temporary, cachePath);
|
|
244
|
-
}
|
|
245
|
-
return { name, repo, ref, resolvedSha, digest: digestDirectory(cachePath), cachePath };
|
|
246
|
-
}
|
|
247
|
-
finally {
|
|
248
|
-
if (exists(temporary))
|
|
249
|
-
removePath(temporary);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
/** 网络操作给足时间,且禁止 git 等待终端输入(否则会静默挂到超时)。 */
|
|
253
|
-
const GIT_NETWORK_OPTIONS = { timeoutMs: 300_000, env: { GIT_TERMINAL_PROMPT: '0' } };
|
|
254
|
-
/**
|
|
255
|
-
* 只拉取不可变 ref 指向的那一个提交:init + remote add + fetch --depth 1。
|
|
256
|
-
* 服务端不允许按 SHA 拉取时回退为完整 clone 后再 fetch。
|
|
257
|
-
*/
|
|
258
|
-
function fetchImmutableRef(repo, ref, temporary) {
|
|
259
|
-
ensureDir(temporary);
|
|
260
|
-
const init = execFile('git', ['init', '-q'], { cwd: temporary });
|
|
261
|
-
if (!init.ok)
|
|
262
|
-
throw new AppError('marketplace_clone_failed', `git init 失败: ${init.stderr || init.stdout}`);
|
|
263
|
-
const remote = execFile('git', ['remote', 'add', 'origin', repo], { cwd: temporary });
|
|
264
|
-
if (!remote.ok)
|
|
265
|
-
throw new AppError('marketplace_clone_failed', `git remote add 失败: ${remote.stderr || remote.stdout}`);
|
|
266
|
-
const shallow = execFile('git', ['fetch', '--depth', '1', 'origin', ref], { cwd: temporary, ...GIT_NETWORK_OPTIONS });
|
|
267
|
-
if (shallow.ok)
|
|
268
|
-
return;
|
|
269
|
-
removePath(temporary);
|
|
270
|
-
const clone = execFile('git', ['clone', '--no-checkout', repo, temporary], { cwd: path.dirname(temporary), ...GIT_NETWORK_OPTIONS });
|
|
271
|
-
if (!clone.ok) {
|
|
272
|
-
throw new AppError('marketplace_clone_failed', `git 拉取 ${repo} 失败。浅拉取 ${ref}: ${shallow.stderr || shallow.stdout};完整 clone: ${clone.stderr || clone.stdout}`);
|
|
273
|
-
}
|
|
274
|
-
const fetch = execFile('git', ['fetch', '--depth', '1', 'origin', ref], { cwd: temporary, ...GIT_NETWORK_OPTIONS });
|
|
275
|
-
if (!fetch.ok)
|
|
276
|
-
throw new AppError('marketplace_fetch_failed', `git fetch ${ref} 失败: ${fetch.stderr || fetch.stdout}`);
|
|
226
|
+
const snapshot = snapshotGitRef({
|
|
227
|
+
repo, ref, label: 'marketplace', cacheRoot: marketplaceCacheRoot(), cacheDir: (sha) => marketplaceCacheDir(name, sha),
|
|
228
|
+
});
|
|
229
|
+
return { name, repo, ref, ...snapshot };
|
|
277
230
|
}
|
|
278
231
|
function validateMarketplace(root) {
|
|
279
232
|
const ids = new Set();
|
|
@@ -333,10 +286,8 @@ function inferName(repo) {
|
|
|
333
286
|
const base = repo.split('/').pop() ?? 'marketplace';
|
|
334
287
|
return base.replace(/\.git$/, '');
|
|
335
288
|
}
|
|
336
|
-
export function isImmutableRef(ref) {
|
|
337
|
-
return /^[0-9a-f]{40}$/i.test(ref) || /^refs\/tags\/[^/]+$/.test(ref);
|
|
338
|
-
}
|
|
339
289
|
function errorCode(error) {
|
|
340
290
|
return error instanceof AppError ? error.code : 'dependency_check_failed';
|
|
341
291
|
}
|
|
292
|
+
export { isImmutableRef };
|
|
342
293
|
//# sourceMappingURL=registry.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { isScalar, parseDocument } from 'yaml';
|
|
3
|
-
import { configPathOf, loadConfig } from '../config/load.js';
|
|
3
|
+
import { configPathOf, loadConfig, loadProjectConfig } from '../config/load.js';
|
|
4
4
|
import { listChangeNames } from '../core/change.js';
|
|
5
5
|
import { inspectHookEntries } from '../hooks/engine.js';
|
|
6
6
|
import { AppError } from '../shared/errors.js';
|
|
@@ -17,10 +17,12 @@ import { addMarketplace, assertPluginsPresent, isImmutableRef } from './registry
|
|
|
17
17
|
*/
|
|
18
18
|
export function upgradeMarketplace(input) {
|
|
19
19
|
const { projectRoot, name, ref } = input;
|
|
20
|
+
// ref 写回项目层,所以只在项目层里找声明;插件依赖取生效配置。
|
|
21
|
+
const layer = loadProjectConfig(projectRoot);
|
|
20
22
|
const config = loadConfig(projectRoot);
|
|
21
23
|
const configFile = configPathOf(projectRoot);
|
|
22
|
-
const index = (
|
|
23
|
-
const declared =
|
|
24
|
+
const index = (layer.marketplaces ?? []).findIndex((item) => item.name === name);
|
|
25
|
+
const declared = layer.marketplaces?.[index];
|
|
24
26
|
if (!configFile || !declared) {
|
|
25
27
|
throw new AppError('marketplace_not_configured', `config 未声明 marketplace: ${name};升级只针对宿主配置中的远端 marketplace`);
|
|
26
28
|
}
|
package/dist/shared/fs.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export declare function ensureDir(dir: string): void;
|
|
|
3
3
|
export declare function readText(file: string): string;
|
|
4
4
|
export declare function writeText(file: string, content: string): void;
|
|
5
5
|
export declare function readJson<T>(file: string): T;
|
|
6
|
+
/** 同目录临时文件加 rename:中断时旧内容保持完整。 */
|
|
7
|
+
export declare function writeTextAtomic(file: string, content: string): void;
|
|
6
8
|
export declare function writeJson(file: string, value: unknown): void;
|
|
7
9
|
export declare function appendText(file: string, line: string): void;
|
|
8
10
|
export declare function copyDir(from: string, to: string): void;
|
package/dist/shared/fs.js
CHANGED
|
@@ -23,11 +23,12 @@ export function readJson(file) {
|
|
|
23
23
|
throw new AppError('invalid_json', `JSON 文件无法解析: ${file}`, error instanceof Error ? error.message : String(error));
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
/** 同目录临时文件加 rename:中断时旧内容保持完整。 */
|
|
27
|
+
export function writeTextAtomic(file, content) {
|
|
27
28
|
ensureDir(path.dirname(file));
|
|
28
29
|
const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${randomUUID()}.tmp`);
|
|
29
30
|
try {
|
|
30
|
-
writeFileSync(temporary,
|
|
31
|
+
writeFileSync(temporary, content, 'utf8');
|
|
31
32
|
renameSync(temporary, file);
|
|
32
33
|
}
|
|
33
34
|
catch (error) {
|
|
@@ -36,6 +37,9 @@ export function writeJson(file, value) {
|
|
|
36
37
|
throw error;
|
|
37
38
|
}
|
|
38
39
|
}
|
|
40
|
+
export function writeJson(file, value) {
|
|
41
|
+
writeTextAtomic(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
42
|
+
}
|
|
39
43
|
export function appendText(file, line) {
|
|
40
44
|
ensureDir(path.dirname(file));
|
|
41
45
|
writeFileSync(file, line, { encoding: 'utf8', flag: 'a' });
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare function isImmutableRef(ref: string): boolean;
|
|
2
|
+
/**
|
|
3
|
+
* 把 Git 仓库在不可变 ref 上的内容取成去掉 .git 的只读快照,按解析出的 SHA 缓存。
|
|
4
|
+
* 总是联网拉取;同一 SHA 已有缓存时要求内容与刚拉到的一致,否则报错而不采用缓存,避免把被改过的缓存写进锁。
|
|
5
|
+
* `label` 作为错误码前缀(如 marketplace_clone_failed),让调用方保留各自的错误码。
|
|
6
|
+
*/
|
|
7
|
+
export declare function snapshotGitRef(input: {
|
|
8
|
+
repo: string;
|
|
9
|
+
ref: string;
|
|
10
|
+
label: string;
|
|
11
|
+
cacheRoot: string;
|
|
12
|
+
cacheDir: (resolvedSha: string) => string;
|
|
13
|
+
}): {
|
|
14
|
+
resolvedSha: string;
|
|
15
|
+
cachePath: string;
|
|
16
|
+
digest: string;
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=git-snapshot.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { renameSync } from 'node:fs';
|
|
4
|
+
import { execFile } from './exec.js';
|
|
5
|
+
import { ensureDir, exists, removePath } from './fs.js';
|
|
6
|
+
import { digestDirectory } from './hash.js';
|
|
7
|
+
import { AppError } from './errors.js';
|
|
8
|
+
import { resolveInside } from './paths.js';
|
|
9
|
+
export function isImmutableRef(ref) {
|
|
10
|
+
return /^[0-9a-f]{40}$/i.test(ref) || /^refs\/tags\/[^/]+$/.test(ref);
|
|
11
|
+
}
|
|
12
|
+
/** 网络操作给足时间,且禁止 git 等待终端输入(否则会静默挂到超时)。 */
|
|
13
|
+
const GIT_NETWORK_OPTIONS = { timeoutMs: 300_000, env: { GIT_TERMINAL_PROMPT: '0' } };
|
|
14
|
+
/**
|
|
15
|
+
* 把 Git 仓库在不可变 ref 上的内容取成去掉 .git 的只读快照,按解析出的 SHA 缓存。
|
|
16
|
+
* 总是联网拉取;同一 SHA 已有缓存时要求内容与刚拉到的一致,否则报错而不采用缓存,避免把被改过的缓存写进锁。
|
|
17
|
+
* `label` 作为错误码前缀(如 marketplace_clone_failed),让调用方保留各自的错误码。
|
|
18
|
+
*/
|
|
19
|
+
export function snapshotGitRef(input) {
|
|
20
|
+
const { repo, ref, label } = input;
|
|
21
|
+
ensureDir(input.cacheRoot);
|
|
22
|
+
const temporary = resolveInside(input.cacheRoot, `.clone-${randomUUID()}`, `${label}_cache_outside_home`);
|
|
23
|
+
try {
|
|
24
|
+
fetchImmutableRef(repo, ref, temporary, label);
|
|
25
|
+
const checkout = execFile('git', ['checkout', '--detach', 'FETCH_HEAD'], { cwd: temporary });
|
|
26
|
+
if (!checkout.ok)
|
|
27
|
+
throw new AppError(`${label}_checkout_failed`, checkout.stderr || checkout.stdout);
|
|
28
|
+
const resolved = execFile('git', ['rev-parse', 'HEAD'], { cwd: temporary });
|
|
29
|
+
if (!resolved.ok)
|
|
30
|
+
throw new AppError(`${label}_revision_failed`, resolved.stderr || resolved.stdout);
|
|
31
|
+
const resolvedSha = resolved.stdout.trim();
|
|
32
|
+
removePath(path.join(temporary, '.git'));
|
|
33
|
+
const digest = digestDirectory(temporary);
|
|
34
|
+
const cachePath = input.cacheDir(resolvedSha);
|
|
35
|
+
if (exists(cachePath)) {
|
|
36
|
+
if (digestDirectory(cachePath) !== digest) {
|
|
37
|
+
throw new AppError(`${label}_digest_mismatch`, `本机缓存与仓库 ${ref} 的内容不一致,可能被改动过: ${cachePath};删除该目录后重试`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
ensureDir(path.dirname(cachePath));
|
|
42
|
+
renameSync(temporary, cachePath);
|
|
43
|
+
}
|
|
44
|
+
return { resolvedSha, cachePath, digest };
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
if (exists(temporary))
|
|
48
|
+
removePath(temporary);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 只拉取不可变 ref 指向的那一个提交:init + remote add + fetch --depth 1。
|
|
53
|
+
* 服务端不允许按 SHA 拉取时回退为完整 clone 后再 fetch。
|
|
54
|
+
*/
|
|
55
|
+
function fetchImmutableRef(repo, ref, temporary, label) {
|
|
56
|
+
ensureDir(temporary);
|
|
57
|
+
const init = execFile('git', ['init', '-q'], { cwd: temporary });
|
|
58
|
+
if (!init.ok)
|
|
59
|
+
throw new AppError(`${label}_clone_failed`, `git init 失败: ${init.stderr || init.stdout}`);
|
|
60
|
+
const remote = execFile('git', ['remote', 'add', 'origin', repo], { cwd: temporary });
|
|
61
|
+
if (!remote.ok)
|
|
62
|
+
throw new AppError(`${label}_clone_failed`, `git remote add 失败: ${remote.stderr || remote.stdout}`);
|
|
63
|
+
const shallow = execFile('git', ['fetch', '--depth', '1', 'origin', ref], { cwd: temporary, ...GIT_NETWORK_OPTIONS });
|
|
64
|
+
if (shallow.ok)
|
|
65
|
+
return;
|
|
66
|
+
removePath(temporary);
|
|
67
|
+
const clone = execFile('git', ['clone', '--no-checkout', repo, temporary], { cwd: path.dirname(temporary), ...GIT_NETWORK_OPTIONS });
|
|
68
|
+
if (!clone.ok) {
|
|
69
|
+
throw new AppError(`${label}_clone_failed`, `git 拉取 ${repo} 失败。浅拉取 ${ref}: ${shallow.stderr || shallow.stdout};完整 clone: ${clone.stderr || clone.stdout}`);
|
|
70
|
+
}
|
|
71
|
+
const fetch = execFile('git', ['fetch', '--depth', '1', 'origin', ref], { cwd: temporary, ...GIT_NETWORK_OPTIONS });
|
|
72
|
+
if (!fetch.ok)
|
|
73
|
+
throw new AppError(`${label}_fetch_failed`, `git fetch ${ref} 失败: ${fetch.stderr || fetch.stdout}`);
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=git-snapshot.js.map
|
package/dist/shared/pkg.js
CHANGED
|
@@ -8,7 +8,7 @@ export function packageRoot() {
|
|
|
8
8
|
for (let i = 0; i < 8; i += 1) {
|
|
9
9
|
if (existsSync(path.join(dir, 'package.json'))) {
|
|
10
10
|
fallback = dir;
|
|
11
|
-
if (existsSync(path.join(dir, 'skills'))
|
|
11
|
+
if (existsSync(path.join(dir, 'skills')))
|
|
12
12
|
return dir;
|
|
13
13
|
}
|
|
14
14
|
const parent = path.dirname(dir);
|
package/docs/getting-started.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
4. 审查 Gate-A 后调用 `/opsx-apply`。
|
|
7
7
|
5. `/opsx-verify` 通过 Gate-C 后 `/opsx-archive`。
|
|
8
8
|
|
|
9
|
-
`init --preset flutter-mobile --yes`
|
|
9
|
+
`init --preset flutter-mobile --preset-repo <preset 仓库> --preset-ref refs/tags/<tag> --yes` 引用团队 preset 仓库中的 preset(见[团队 preset](guides/configuration.md#团队-preset)),再按生效配置中的完整 Git SHA(或 `refs/tags/<tag>`)解析 marketplace,校验插件后调用 Claude/Codex 原生 marketplace 与 selector。项目 `openspec/marketplace-lock.json` 记录 resolved SHA、marketplace digest 和 plugin digest;adapter 合同来自插件 `plugin.json` 的 `extensions["org.openspec"]`,不写进 CLI。
|
|
10
10
|
|
|
11
11
|
Claude 的 marketplace 与插件一律以 `--scope local` 安装,写入 `.claude/settings.local.json`(项目级、仅本人、不入库):其中的 marketplace 路径是本机缓存的绝对路径,用 project scope 会随 `.claude/settings.json` 提交给团队成员。每位成员在自己机器上执行一次 `openspec init --yes` 即可;团队共享的是 `openspec/config.yaml` 与 `marketplace-lock.json`。
|
|
12
12
|
|
|
@@ -20,7 +20,7 @@ verify `command` 由引擎直接启动子进程执行,不经 shell;需要管
|
|
|
20
20
|
|
|
21
21
|
verify 每次创建独立 run。命令、skill/file/check evidence 和 QA report 都必须属于当前 run;required evidence 缺失、file digest 变化或旧 report 不会通过。`reviewScope` 先裁剪引擎 changed-set,`failurePolicy` 只控制 optional 失败,不能放行 required Hook。
|
|
22
22
|
|
|
23
|
-
Config 为严格 schema:重复 Hook ID、重复 collect capability、未知 category 引用、旧 `context/rules`、可变 marketplace branch ref 都会报错。
|
|
23
|
+
Config 为严格 schema:重复 Hook ID、重复 collect capability、未知 category 引用、旧 `context/rules`、可变 marketplace branch ref 都会报错。
|
|
24
24
|
|
|
25
25
|
`designRefs` 是 task-plan 的可选节点 id 数组,省略/空数组合法,非空项必须是非空字符串。`packet.refs.design` 提供设计来源 locator 与采集 artifact;详细格式与 status 提示见 [工作流](workflow.md#设计节点引用)。
|
|
26
26
|
|
|
@@ -28,6 +28,13 @@ Config 为严格 schema:重复 Hook ID、重复 collect capability、未知 ca
|
|
|
28
28
|
|
|
29
29
|
插件 Hook 通过 `use: entry@marketplace` 引用根 manifest 的 `extensions.org.openspec.hooks`,首轮支持 skill/command;与 name/command/path 等内联实现互斥。`operation: preview` 切换为 source adapter 的预演入口。宿主 input 为 JSON 参数,inputFiles 为额外依赖文件路径;动态 --input 的顶层值优先。类型、阶段、重复 ID、损坏和缺失入口明确失败。详情及示例见[插件运行契约](../maintainers/plugin-runtime-contract.md)。
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
项目层同 ID 的 hook 整条替换 preset 条目,覆盖时要写出完整的 Hook(类型、command、required、参数、范围)。
|
|
32
32
|
|
|
33
33
|
parse/tech/plan 的 Hook 可用 capability 限定仅在当前来源账本包含该能力时适用;省略则应用于整个阶段。required tech command 只在适用时禁止 skipTech。
|
|
34
|
+
|
|
35
|
+
## 团队 preset
|
|
36
|
+
|
|
37
|
+
- `preset: { repo, ref, id }` 引用独立 Git 仓库中的一个 preset,`ref` 必须是完整 SHA 或 `refs/tags/<tag>`。config 其余字段是项目层,生效配置 = preset 基线 + 项目层:分类按名覆盖(`null` 删除);hook 同 ID 整条替换并保留基线位置,`disableHooks: [id]` 禁用基线 hook,项目新增 hook 排在基线之后,需要插在中间时用 `hookOrder: { <stage>: [id, ...] }` 指定该阶段的顺序(列出的依次在前,其余保持原相对顺序);验收设置按字段覆盖;插件取并集,同一插件取较高 `minVersion`;marketplace 以项目声明为准,基线只补缺;`tools` 只来自项目层。
|
|
38
|
+
- `openspec/preset-lock.json` 记录解析出的 SHA 与内容摘要,与 config 一起提交。运行时只读本机缓存、不联网;成员拉取后执行 `openspec init` 按锁补齐缓存,`openspec doctor` 报告锁缺失、锁与 config 不一致、缓存缺失或被改。
|
|
39
|
+
- 命令:`preset use <id> --repo <repo> --ref <ref>` 引用;`preset upgrade --ref <ref>` 升级,列出基线变化与被项目层遮住的更新,不兼容时还原;`preset status` 查看每项来源(生效配置本体用 `config get`);`preset list`、`preset show <id>` 查看仓库内容;`preset migrate <id> --repo --ref [--dry-run]` 把旧的拷贝式配置迁移为引用,迁移前后生效配置必须一致(同阶段 hook 顺序不同时自动生成 `hookOrder`;preset 提高的插件最低版本无法取消,会在结果中列出)。
|
|
40
|
+
- preset 仓库:每个 preset 一个目录,`<id>/preset.yaml`(`schemaVersion: openspec.team-preset.v2`,可用 `requires.openspec` 声明最低 OpenSpec 版本)加模板文件。`scaffold: [{ path, template, description }]` 的 template 相对该目录,引用或升级时只创建不覆盖;模板首行的 `<!-- openspec:fill-me -->` 标记未删除前,指向该文件的 `file` hook 不能回执 `passed`。仓库根部的 `openspec-presets.json` 由 `openspec preset index` 生成,CI 用 `openspec preset validate` 校验规则、模板、插件引用与索引一致性。
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
OpenSpec 只负责来源、阶段、调用和证据身份。插件提供 Skill、source adapter / Hook 声明和公开工具入口绑定;已有独立工具项目负责格式解析、执行实现、兼容范围和功能测试。预设选择入口;宿主拥有参数、真实路径与业务断言。`org.openspec.hooks` 与 Claude/Codex 原生事件 Hook 是独立契约。
|
|
4
4
|
|
|
5
|
-
`flutter-mobile/plugin-hooks.json` 是可供既有宿主选择性迁移的插件配置片段,遵循现有 ProjectConfigPatch schema,没有新增预设合并机制。它声明来源指导、只读预演、生成、国际化实施和真实 locale 命令。预演参数通过 Worker `--input` 提供;locale 命令通过宿主 `input.manifest` 或动态输入指定清单。verify 如需相同校验,复用 `intl-locales-check`,指定独立 Hook ID、stage: verify 和固定宿主参数。
|
|
5
|
+
preset 仓库中的 `flutter-mobile/plugin-hooks.json` 是可供既有宿主选择性迁移的插件配置片段,遵循现有 ProjectConfigPatch schema,没有新增预设合并机制。它声明来源指导、只读预演、生成、国际化实施和真实 locale 命令。预演参数通过 Worker `--input` 提供;locale 命令通过宿主 `input.manifest` 或动态输入指定清单。verify 如需相同校验,复用 `intl-locales-check`,指定独立 Hook ID、stage: verify 和固定宿主参数。
|
|
6
6
|
|
|
7
7
|
## 版本归属与独立升级
|
|
8
8
|
|
|
@@ -11,11 +11,11 @@ OpenSpec 只负责来源、阶段、调用和证据身份。插件提供 Skill
|
|
|
11
11
|
| 层 | 负责 | 何时变化 |
|
|
12
12
|
|---|---|---|
|
|
13
13
|
| OpenSpec | Hook 入口解析、[插件运行契约](../maintainers/plugin-runtime-contract.md)、回执身份 | 契约变化时 |
|
|
14
|
-
| preset | 平台约定、引用的入口 ID、所需插件最低版本、已验证的基线 ref |
|
|
14
|
+
| preset | 平台约定、引用的入口 ID、所需插件最低版本、已验证的基线 ref | 在 preset 仓库打 tag 发布 |
|
|
15
15
|
| 插件与工具 | 工具行为和用法(写在插件 Skill)、所需工具版本与能力核对 | 独立发版 |
|
|
16
16
|
| 宿主 | `openspec/config.yaml` 的 marketplace ref 与 `openspec/marketplace-lock.json`,即实际使用的精确版本 | 按需升级 |
|
|
17
17
|
|
|
18
|
-
preset 的 ref 只是发布时验证过的基线,在
|
|
18
|
+
preset 的 ref 只是发布时验证过的基线,在 preset 仓库下次发布时更新。工具生成什么形状、有哪些参数、输入文件格式是什么,写在插件 Skill 或插件 Hook 的 `inputDescription` 里;preset 与核心节点只写平台约定和验收原则,不复述工具行为,否则工具每次变化 preset 都得跟版。例如计划覆盖检查的输入格式见 yapi-source-guidance,国际化快照清单见 intl-workflow。
|
|
19
19
|
|
|
20
20
|
`flutter-mobile` 的基线 ref 为 `06081ec27a557d8f5e4d443aa8549ab026173f94`(2.2.1 起)。自 2.2.3 起引用的 `yapi-plan-coverage` 需要 yapi-dart-coding 插件 0.2.1,任务级翻译需要 intl-utils 插件 0.2.3,基线 ref 早于这两个版本。发布 preset 前先发布工具和插件,再把基线 ref 更新为包含它们的已发布提交。插件需要的工具版本由插件 Skill 声明,不写在 preset 里。
|
|
21
21
|
|
|
@@ -37,7 +37,7 @@ openspec marketplace upgrade <marketplace> --ref <完整 Git SHA 或 refs/tags/<
|
|
|
37
37
|
|
|
38
38
|
之后按插件 Skill 的要求升级宿主工具依赖(例如 pubspec 中的 Git ref),核对依赖锁解析到的提交,`openspec doctor` 通过后再开始新的 change。
|
|
39
39
|
|
|
40
|
-
同一 marketplace 的插件共用一个 ref,升级其中一个插件就是取该市场的新快照,市场自身的 CI
|
|
40
|
+
同一 marketplace 的插件共用一个 ref,升级其中一个插件就是取该市场的新快照,市场自身的 CI 负责整体一致。项目层声明的 marketplace ref 优先于 preset 基线,preset 只补充项目缺少的 marketplace;`openspec preset status` 中标为 `override` 的 marketplace 就是项目覆盖了基线的项。
|
|
41
41
|
|
|
42
42
|
### 兼容检查
|
|
43
43
|
|
|
@@ -52,7 +52,7 @@ openspec marketplace upgrade <marketplace> --ref <完整 Git SHA 或 refs/tags/<
|
|
|
52
52
|
|
|
53
53
|
## 迁移与安装注意事项
|
|
54
54
|
|
|
55
|
-
迁移已有宿主时先冻结配置与插件锁,再按 Hook ID 精确修改对应实现。保留原来的 required、command 参数、taskCategories、target/include/exclude/allowEmpty 与顺序;已有真实 command 不能退化成 check 或文字 passed。更新为插件入口时移除互斥的内联 command/name
|
|
55
|
+
迁移已有宿主时先冻结配置与插件锁,再按 Hook ID 精确修改对应实现。保留原来的 required、command 参数、taskCategories、target/include/exclude/allowEmpty 与顺序;已有真实 command 不能退化成 check 或文字 passed。更新为插件入口时移除互斥的内联 command/name,保留原意并显式填入参数。引用 preset 的项目在项目层写同 ID 的 Hook 即可覆盖 preset 条目,覆盖是整项替换,要写出完整内容。新增 file 模板只创建不覆盖,真实宿主约定不能回退为模板。
|
|
56
56
|
|
|
57
57
|
工具依赖可使用已发布的 tag,并在依赖锁中核验解析提交;marketplace 使用已验收的固定提交。插件或分发版本更新后,须通过原生 CLI 刷新安装,核对实际版本、作用域、路径与完整摘要,再生成宿主锁和回执。仅修改锁文件不能证明安装已更新。模板记录运行参数的来源,个人模型、凭据和服务地址由宿主及本机配置提供;显式临时覆盖仍由工具支持。
|
|
58
58
|
|
|
@@ -6,7 +6,7 @@ flutter-mobile preset 的 parse / tech / plan Worker 以 required `file` hook
|
|
|
6
6
|
|
|
7
7
|
## 由 init 自动生成
|
|
8
8
|
|
|
9
|
-
`openspec
|
|
9
|
+
`openspec preset use flutter-mobile`(或 `openspec init --preset flutter-mobile`)会按 preset 的 `scaffold` 声明,把 preset 仓库中的模板放到项目的 `docs/agent/ui-conventions.md`。只创建、不覆盖:已存在的文件不会被重复引用或升级改动。
|
|
10
10
|
|
|
11
11
|
路径由 `openspec/config.yaml` 中 `type: file` hook 的 `path` 决定;preset 默认在 parse / tech / plan / ui code 四处声明为 `docs/agent/ui-conventions.md`。项目可以改,但四个 hook 与 `scaffold.path` 要一起改。
|
|
12
12
|
|
|
@@ -36,4 +36,4 @@ flutter-mobile preset 的 parse / tech / plan Worker 以 required `file` hook
|
|
|
36
36
|
|
|
37
37
|
国际化流程填写项目实际选用的资源格式、代码读取入口、翻译、合入、生成和校验命令,以及参数配置来源。工具指导由所选插件 Hook 提供,模板不预填生成器、资源格式或第三方版本。保留宿主已确认的增量策略、必需语言、占位符及历史文案保护要求;无国际化需求时注明不适用。
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
升级 preset 用 `openspec preset upgrade`:项目层覆盖的条目保持不变,输出会列出被覆盖遮住的 preset 更新;旧的拷贝式配置先用 `openspec preset migrate` 迁移。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@birdie_moblie/open_spec",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "AI-native Spec Coding 工具:确定性产品规格工作流",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openspec",
|
|
@@ -34,7 +34,6 @@
|
|
|
34
34
|
"dist",
|
|
35
35
|
"bin/openspec.js",
|
|
36
36
|
"skills",
|
|
37
|
-
"presets",
|
|
38
37
|
"plugin.json",
|
|
39
38
|
".claude-plugin",
|
|
40
39
|
".codex-plugin",
|
package/plugin.json
CHANGED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
schemaVersion: openspec.team-preset.v1
|
|
2
|
-
version: 2.0.0
|
|
3
|
-
id: backend-service
|
|
4
|
-
title: Backend Service
|
|
5
|
-
description: 后端服务团队的 spec-product 默认配置
|
|
6
|
-
configPatch:
|
|
7
|
-
schema: spec-product
|
|
8
|
-
hooks:
|
|
9
|
-
- id: contract-check
|
|
10
|
-
stage: verify
|
|
11
|
-
type: check
|
|
12
|
-
category: contract
|
|
13
|
-
description: 核对接口契约与错误码是否与 spec 一致
|
|
14
|
-
required: true
|
|
15
|
-
- id: unit-test
|
|
16
|
-
stage: verify
|
|
17
|
-
type: command
|
|
18
|
-
command: pnpm test
|
|
19
|
-
required: true
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schema": "spec-product",
|
|
3
|
-
"hooks": [
|
|
4
|
-
{
|
|
5
|
-
"id": "yapi-guidance-parse",
|
|
6
|
-
"stage": "parse",
|
|
7
|
-
"type": "skill",
|
|
8
|
-
"use": "yapi-source-guidance@mobile-marketplace",
|
|
9
|
-
"capability": "interfaces",
|
|
10
|
-
"required": true
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
"id": "yapi-guidance-tech",
|
|
14
|
-
"stage": "tech",
|
|
15
|
-
"type": "skill",
|
|
16
|
-
"use": "yapi-source-guidance@mobile-marketplace",
|
|
17
|
-
"capability": "interfaces",
|
|
18
|
-
"required": true
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"id": "yapi-preview",
|
|
22
|
-
"stage": "tech",
|
|
23
|
-
"type": "command",
|
|
24
|
-
"use": "yapi-discovery@mobile-marketplace",
|
|
25
|
-
"operation": "preview",
|
|
26
|
-
"capability": "interfaces",
|
|
27
|
-
"required": true
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"id": "yapi-plan-coverage",
|
|
31
|
-
"stage": "plan",
|
|
32
|
-
"type": "command",
|
|
33
|
-
"use": "yapi-plan-coverage@mobile-marketplace",
|
|
34
|
-
"capability": "interfaces",
|
|
35
|
-
"required": true
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"id": "yapi-generation",
|
|
39
|
-
"stage": "execute",
|
|
40
|
-
"type": "skill",
|
|
41
|
-
"use": "yapi-codegen@mobile-marketplace",
|
|
42
|
-
"taskCategories": [
|
|
43
|
-
"api-codegen"
|
|
44
|
-
],
|
|
45
|
-
"required": true
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
"id": "intl-workflow",
|
|
49
|
-
"stage": "execute",
|
|
50
|
-
"type": "skill",
|
|
51
|
-
"use": "intl-workflow@mobile-marketplace",
|
|
52
|
-
"taskCategories": [
|
|
53
|
-
"ui"
|
|
54
|
-
],
|
|
55
|
-
"required": true
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
"id": "i18n-inline",
|
|
59
|
-
"stage": "execute",
|
|
60
|
-
"type": "command",
|
|
61
|
-
"use": "intl-locales-check@mobile-marketplace",
|
|
62
|
-
"taskCategories": [
|
|
63
|
-
"ui"
|
|
64
|
-
],
|
|
65
|
-
"required": true
|
|
66
|
-
}
|
|
67
|
-
]
|
|
68
|
-
}
|