@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,18 @@
|
|
|
1
|
+
export declare function exists(target: string): boolean;
|
|
2
|
+
export declare function ensureDir(dir: string): void;
|
|
3
|
+
export declare function readText(file: string): string;
|
|
4
|
+
export declare function writeText(file: string, content: string): void;
|
|
5
|
+
export declare function readJson<T>(file: string): T;
|
|
6
|
+
export declare function writeJson(file: string, value: unknown): void;
|
|
7
|
+
export declare function appendText(file: string, line: string): void;
|
|
8
|
+
export declare function copyDir(from: string, to: string): void;
|
|
9
|
+
export declare function copyPath(from: string, to: string, options?: {
|
|
10
|
+
excludeNames?: string[];
|
|
11
|
+
}): void;
|
|
12
|
+
export declare function removePath(target: string): void;
|
|
13
|
+
export declare function listDirs(dir: string): string[];
|
|
14
|
+
export declare function listFilesRecursive(dir: string): string[];
|
|
15
|
+
export declare function readFileBytes(file: string): Buffer;
|
|
16
|
+
export declare function readSymlink(file: string): string;
|
|
17
|
+
export declare function isSymlink(file: string): boolean;
|
|
18
|
+
//# sourceMappingURL=fs.d.ts.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { AppError } from './errors.js';
|
|
5
|
+
export function exists(target) {
|
|
6
|
+
return existsSync(target);
|
|
7
|
+
}
|
|
8
|
+
export function ensureDir(dir) {
|
|
9
|
+
mkdirSync(dir, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
export function readText(file) {
|
|
12
|
+
return readFileSync(file, 'utf8');
|
|
13
|
+
}
|
|
14
|
+
export function writeText(file, content) {
|
|
15
|
+
ensureDir(path.dirname(file));
|
|
16
|
+
writeFileSync(file, content, 'utf8');
|
|
17
|
+
}
|
|
18
|
+
export function readJson(file) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(readText(file));
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
throw new AppError('invalid_json', `JSON 文件无法解析: ${file}`, error instanceof Error ? error.message : String(error));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function writeJson(file, value) {
|
|
27
|
+
ensureDir(path.dirname(file));
|
|
28
|
+
const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${randomUUID()}.tmp`);
|
|
29
|
+
try {
|
|
30
|
+
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
31
|
+
renameSync(temporary, file);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (exists(temporary))
|
|
35
|
+
unlinkSync(temporary);
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function appendText(file, line) {
|
|
40
|
+
ensureDir(path.dirname(file));
|
|
41
|
+
writeFileSync(file, line, { encoding: 'utf8', flag: 'a' });
|
|
42
|
+
}
|
|
43
|
+
export function copyDir(from, to) {
|
|
44
|
+
ensureDir(to);
|
|
45
|
+
cpSync(from, to, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
export function copyPath(from, to, options = {}) {
|
|
48
|
+
ensureDir(path.dirname(to));
|
|
49
|
+
const excluded = new Set(options.excludeNames ?? []);
|
|
50
|
+
cpSync(from, to, {
|
|
51
|
+
recursive: true,
|
|
52
|
+
filter: excluded.size === 0 ? undefined : (source) => !excluded.has(path.basename(source)),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export function removePath(target) {
|
|
56
|
+
rmSync(target, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
export function listDirs(dir) {
|
|
59
|
+
if (!exists(dir))
|
|
60
|
+
return [];
|
|
61
|
+
return readdirSync(dir).filter((name) => {
|
|
62
|
+
try {
|
|
63
|
+
return statSync(path.join(dir, name)).isDirectory();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
export function listFilesRecursive(dir) {
|
|
71
|
+
if (!exists(dir))
|
|
72
|
+
return [];
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const name of readdirSync(dir)) {
|
|
75
|
+
const full = path.join(dir, name);
|
|
76
|
+
const stat = lstatSync(full);
|
|
77
|
+
if (stat.isDirectory())
|
|
78
|
+
out.push(...listFilesRecursive(full));
|
|
79
|
+
else
|
|
80
|
+
out.push(full);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
export function readFileBytes(file) {
|
|
85
|
+
return readFileSync(file);
|
|
86
|
+
}
|
|
87
|
+
export function readSymlink(file) {
|
|
88
|
+
return readlinkSync(file);
|
|
89
|
+
}
|
|
90
|
+
export function isSymlink(file) {
|
|
91
|
+
return lstatSync(file).isSymbolicLink();
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=fs.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface GitSnapshot {
|
|
2
|
+
available: boolean;
|
|
3
|
+
branch: string | null;
|
|
4
|
+
headSha: string | null;
|
|
5
|
+
}
|
|
6
|
+
export interface DiffBaseline {
|
|
7
|
+
snapshot: GitSnapshot;
|
|
8
|
+
entries: Record<string, DirtyEntry>;
|
|
9
|
+
capturedAt: string;
|
|
10
|
+
}
|
|
11
|
+
export interface DirtyEntry {
|
|
12
|
+
status: string;
|
|
13
|
+
fingerprint: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function parseDiffBaseline(value: unknown): DiffBaseline;
|
|
16
|
+
export declare function readGitSnapshot(projectRoot: string): GitSnapshot;
|
|
17
|
+
export declare function captureDiffBaseline(projectRoot: string): DiffBaseline;
|
|
18
|
+
/** Stable checkout identity, excluding workflow artifacts but retaining dirty host files. */
|
|
19
|
+
export declare function checkoutFingerprint(projectRoot: string): string;
|
|
20
|
+
/** 相对创建 worker 时的基线,得到本次实际改动的相对路径。 */
|
|
21
|
+
export declare function changedFilesSince(projectRoot: string, baseline: DiffBaseline): string[];
|
|
22
|
+
//# sourceMappingURL=git.d.ts.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, lstatSync, readlinkSync } from 'node:fs';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { AppError } from './errors.js';
|
|
5
|
+
import { sha256, sha256File } from './hash.js';
|
|
6
|
+
import { isOpsxInternal, normalizeRel, resolveInside } from './paths.js';
|
|
7
|
+
const DiffBaselineSchema = z.object({
|
|
8
|
+
snapshot: z.object({
|
|
9
|
+
available: z.boolean(),
|
|
10
|
+
branch: z.string().nullable(),
|
|
11
|
+
headSha: z.string().nullable(),
|
|
12
|
+
}).strict(),
|
|
13
|
+
entries: z.record(z.string(), z.object({ status: z.string(), fingerprint: z.string() }).strict()),
|
|
14
|
+
capturedAt: z.string(),
|
|
15
|
+
}).strict();
|
|
16
|
+
export function parseDiffBaseline(value) {
|
|
17
|
+
const parsed = DiffBaselineSchema.safeParse(value);
|
|
18
|
+
if (!parsed.success)
|
|
19
|
+
throw new AppError('invalid_worker_baseline', 'Worker Git baseline 不合法', parsed.error.issues);
|
|
20
|
+
return parsed.data;
|
|
21
|
+
}
|
|
22
|
+
function git(projectRoot, args) {
|
|
23
|
+
try {
|
|
24
|
+
return execFileSync('git', args, {
|
|
25
|
+
cwd: projectRoot,
|
|
26
|
+
encoding: 'utf8',
|
|
27
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
28
|
+
}).trim();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function gitRaw(projectRoot, args) {
|
|
35
|
+
try {
|
|
36
|
+
return execFileSync('git', args, {
|
|
37
|
+
cwd: projectRoot,
|
|
38
|
+
encoding: 'utf8',
|
|
39
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function readGitSnapshot(projectRoot) {
|
|
47
|
+
const headSha = git(projectRoot, ['rev-parse', 'HEAD']);
|
|
48
|
+
if (!headSha)
|
|
49
|
+
return { available: false, branch: null, headSha: null };
|
|
50
|
+
const branch = git(projectRoot, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
51
|
+
return { available: true, branch: branch || null, headSha };
|
|
52
|
+
}
|
|
53
|
+
export function captureDiffBaseline(projectRoot) {
|
|
54
|
+
const snapshot = readGitSnapshot(projectRoot);
|
|
55
|
+
if (!snapshot.available) {
|
|
56
|
+
throw new AppError('git_required', 'code Worker 需要在 Git 仓库中运行');
|
|
57
|
+
}
|
|
58
|
+
const entries = readDirtyEntries(projectRoot);
|
|
59
|
+
return { snapshot, entries, capturedAt: new Date().toISOString() };
|
|
60
|
+
}
|
|
61
|
+
/** Stable checkout identity, excluding workflow artifacts but retaining dirty host files. */
|
|
62
|
+
export function checkoutFingerprint(projectRoot) {
|
|
63
|
+
const baseline = captureDiffBaseline(projectRoot);
|
|
64
|
+
const entries = Object.entries(baseline.entries).filter(([file]) => !isOpsxInternal(file)).sort(([a], [b]) => a.localeCompare(b));
|
|
65
|
+
return sha256(JSON.stringify({ head: baseline.snapshot.headSha, entries }));
|
|
66
|
+
}
|
|
67
|
+
/** 相对创建 worker 时的基线,得到本次实际改动的相对路径。 */
|
|
68
|
+
export function changedFilesSince(projectRoot, baseline) {
|
|
69
|
+
const currentSnapshot = readGitSnapshot(projectRoot);
|
|
70
|
+
if (!currentSnapshot.available) {
|
|
71
|
+
throw new AppError('git_required', 'code Worker 需要在 Git 仓库中运行');
|
|
72
|
+
}
|
|
73
|
+
if (currentSnapshot.headSha !== baseline.snapshot.headSha) {
|
|
74
|
+
throw new AppError('worker_git_head_changed', 'Worker 执行期间 Git HEAD 已变化,无法可信计算 changed-set');
|
|
75
|
+
}
|
|
76
|
+
const now = readDirtyEntries(projectRoot);
|
|
77
|
+
const files = new Set();
|
|
78
|
+
for (const [file, entry] of Object.entries(now)) {
|
|
79
|
+
if (isOpsxInternal(file))
|
|
80
|
+
continue;
|
|
81
|
+
const before = baseline.entries[file];
|
|
82
|
+
if (!before || before.fingerprint !== entry.fingerprint || before.status !== entry.status) {
|
|
83
|
+
files.add(file);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const [file, before] of Object.entries(baseline.entries)) {
|
|
87
|
+
if (isOpsxInternal(file))
|
|
88
|
+
continue;
|
|
89
|
+
if (now[file])
|
|
90
|
+
continue;
|
|
91
|
+
if (fingerprintPath(projectRoot, file) !== before.fingerprint)
|
|
92
|
+
files.add(file);
|
|
93
|
+
}
|
|
94
|
+
return [...files].sort();
|
|
95
|
+
}
|
|
96
|
+
function readDirtyEntries(projectRoot) {
|
|
97
|
+
const raw = gitRaw(projectRoot, ['status', '--porcelain=v1', '-z', '-uall']);
|
|
98
|
+
if (raw === null)
|
|
99
|
+
throw new AppError('git_status_failed', '无法读取 Git 工作树状态');
|
|
100
|
+
const records = raw.split('\0');
|
|
101
|
+
const entries = {};
|
|
102
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
103
|
+
const record = records[index];
|
|
104
|
+
if (!record || record.length < 4)
|
|
105
|
+
continue;
|
|
106
|
+
const status = record.slice(0, 2);
|
|
107
|
+
const relative = normalizeRel(record.slice(3));
|
|
108
|
+
if (!relative)
|
|
109
|
+
continue;
|
|
110
|
+
entries[relative] = { status, fingerprint: fingerprintPath(projectRoot, relative) };
|
|
111
|
+
if (status.includes('R') || status.includes('C')) {
|
|
112
|
+
const previous = normalizeRel(records[index + 1] ?? '');
|
|
113
|
+
index += 1;
|
|
114
|
+
if (previous) {
|
|
115
|
+
entries[previous] = { status: `${status}:source`, fingerprint: fingerprintPath(projectRoot, previous) };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return entries;
|
|
120
|
+
}
|
|
121
|
+
function fingerprintPath(projectRoot, relative) {
|
|
122
|
+
const file = resolveInside(projectRoot, relative, 'git_path_outside_project');
|
|
123
|
+
if (!existsSync(file))
|
|
124
|
+
return 'missing';
|
|
125
|
+
const stat = lstatSync(file);
|
|
126
|
+
if (stat.isSymbolicLink())
|
|
127
|
+
return sha256(`symlink:${readlinkSync(file)}`);
|
|
128
|
+
if (stat.isFile())
|
|
129
|
+
return sha256File(file);
|
|
130
|
+
return sha256(`type:${stat.mode}:${stat.size}`);
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=git.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstatSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { listFilesRecursive, readFileBytes, readSymlink } from './fs.js';
|
|
5
|
+
export function sha256(content) {
|
|
6
|
+
return createHash('sha256').update(content).digest('hex');
|
|
7
|
+
}
|
|
8
|
+
/** 对目录内全部文件做稳定 digest,用于技能/插件安装校验。 */
|
|
9
|
+
export function digestDirectory(dir) {
|
|
10
|
+
const files = listFilesRecursive(dir).sort();
|
|
11
|
+
const hash = createHash('sha256');
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
const stat = lstatSync(file);
|
|
14
|
+
// socket / fifo 等不可读取内容,也不属于可分发的插件产物
|
|
15
|
+
if (!stat.isFile() && !stat.isSymbolicLink())
|
|
16
|
+
continue;
|
|
17
|
+
const relative = path.relative(dir, file).replace(/\\/g, '/');
|
|
18
|
+
hash.update(relative);
|
|
19
|
+
hash.update('\0');
|
|
20
|
+
hash.update(stat.isSymbolicLink() ? `symlink:${readSymlink(file)}` : readFileBytes(file));
|
|
21
|
+
hash.update('\0');
|
|
22
|
+
}
|
|
23
|
+
return hash.digest('hex');
|
|
24
|
+
}
|
|
25
|
+
export function sha256File(file) {
|
|
26
|
+
const stat = lstatSync(file);
|
|
27
|
+
return sha256(stat.isSymbolicLink() ? `symlink:${readSymlink(file)}` : readFileBytes(file));
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=hash.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** 把路径规范成仓库内相对 POSIX 路径。 */
|
|
2
|
+
export declare function normalizeRel(value: string): string;
|
|
3
|
+
export declare function ensureTrailingSlash(value: string): string;
|
|
4
|
+
export declare function isInsideDir(file: string, dir: string): boolean;
|
|
5
|
+
export declare function isOpsxInternal(file: string): boolean;
|
|
6
|
+
export declare function matchPattern(file: string, pattern: string): boolean;
|
|
7
|
+
/** 解析仓库内路径,并拒绝绝对路径、词法越界和已有 symlink 越界。 */
|
|
8
|
+
export declare function resolveInside(root: string, relative: string, code?: string): string;
|
|
9
|
+
/** 校验会被用作文件名的标识;不限制业务命名,只排除路径语义。 */
|
|
10
|
+
export declare function assertPathToken(value: string, label: string): void;
|
|
11
|
+
export declare function changeDir(changeName: string): string;
|
|
12
|
+
export declare function archiveDir(stamp: string, changeName: string): string;
|
|
13
|
+
export declare const OPENSPEC_DIR = "openspec";
|
|
14
|
+
export declare const OPSX_DIR = ".opsx";
|
|
15
|
+
export declare const CONFIG_FILES: readonly ["config.yaml", "config.yml"];
|
|
16
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { AppError } from './errors.js';
|
|
4
|
+
/** 把路径规范成仓库内相对 POSIX 路径。 */
|
|
5
|
+
export function normalizeRel(value) {
|
|
6
|
+
return value.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/{2,}/g, '/');
|
|
7
|
+
}
|
|
8
|
+
export function ensureTrailingSlash(value) {
|
|
9
|
+
const normalized = normalizeRel(value);
|
|
10
|
+
return normalized.endsWith('/') ? normalized : `${normalized}/`;
|
|
11
|
+
}
|
|
12
|
+
export function isInsideDir(file, dir) {
|
|
13
|
+
const relative = normalizeRel(file);
|
|
14
|
+
const prefix = ensureTrailingSlash(dir);
|
|
15
|
+
return relative === normalizeRel(dir) || relative.startsWith(prefix);
|
|
16
|
+
}
|
|
17
|
+
export function isOpsxInternal(file) {
|
|
18
|
+
const relative = normalizeRel(file);
|
|
19
|
+
return relative === '.opsx' || relative.startsWith('.opsx/')
|
|
20
|
+
|| relative.includes('/.opsx/') || relative.endsWith('/.opsx')
|
|
21
|
+
|| /^openspec\/changes\/[^/]+(?:\/|$)/.test(relative);
|
|
22
|
+
}
|
|
23
|
+
export function matchPattern(file, pattern) {
|
|
24
|
+
const normalizedFile = normalizeRel(file);
|
|
25
|
+
const normalizedPattern = normalizeRel(pattern);
|
|
26
|
+
if (!/[*?]/.test(normalizedPattern)) {
|
|
27
|
+
return normalizedFile === normalizedPattern || isInsideDir(normalizedFile, normalizedPattern);
|
|
28
|
+
}
|
|
29
|
+
if (normalizedPattern.startsWith('**/') && matchPattern(normalizedFile, normalizedPattern.slice(3))) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return globToRegExp(normalizedPattern).test(normalizedFile);
|
|
33
|
+
}
|
|
34
|
+
/** 解析仓库内路径,并拒绝绝对路径、词法越界和已有 symlink 越界。 */
|
|
35
|
+
export function resolveInside(root, relative, code = 'path_outside_root') {
|
|
36
|
+
const portable = normalizeRel(relative);
|
|
37
|
+
if (!relative || path.isAbsolute(relative) || /^[A-Za-z]:\//.test(portable) || portable.startsWith('/') || relative.includes('\0')) {
|
|
38
|
+
throw new AppError(code, `路径必须是非空相对路径: ${relative}`);
|
|
39
|
+
}
|
|
40
|
+
const rootAbs = path.resolve(root);
|
|
41
|
+
const target = path.resolve(rootAbs, portable);
|
|
42
|
+
assertContained(rootAbs, target, relative, code);
|
|
43
|
+
const rootReal = existsSync(rootAbs) ? realpathSync(rootAbs) : rootAbs;
|
|
44
|
+
let cursor = rootAbs;
|
|
45
|
+
for (const segment of path.relative(rootAbs, target).split(path.sep).filter(Boolean)) {
|
|
46
|
+
cursor = path.join(cursor, segment);
|
|
47
|
+
if (!existsSync(cursor))
|
|
48
|
+
continue;
|
|
49
|
+
const real = realpathSync(cursor);
|
|
50
|
+
assertContained(rootReal, real, relative, code);
|
|
51
|
+
}
|
|
52
|
+
return target;
|
|
53
|
+
}
|
|
54
|
+
/** 校验会被用作文件名的标识;不限制业务命名,只排除路径语义。 */
|
|
55
|
+
export function assertPathToken(value, label) {
|
|
56
|
+
if (!value || value === '.' || value === '..' || /[\\/\0]/.test(value)) {
|
|
57
|
+
throw new AppError('invalid_path_token', `${label} 不能包含路径语义: ${value}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function assertContained(root, target, display, code) {
|
|
61
|
+
const relative = path.relative(root, target);
|
|
62
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
63
|
+
throw new AppError(code, `路径越出允许目录: ${display}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function globToRegExp(pattern) {
|
|
67
|
+
let source = '';
|
|
68
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
69
|
+
const char = pattern[i];
|
|
70
|
+
const next = pattern[i + 1];
|
|
71
|
+
if (char === '*' && next === '*') {
|
|
72
|
+
source += '.*';
|
|
73
|
+
i += 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (char === '*') {
|
|
77
|
+
source += '[^/]*';
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (char === '?') {
|
|
81
|
+
source += '[^/]';
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
source += char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
85
|
+
}
|
|
86
|
+
return new RegExp(`^${source}$`);
|
|
87
|
+
}
|
|
88
|
+
export function changeDir(changeName) {
|
|
89
|
+
return path.posix.join('openspec', 'changes', changeName);
|
|
90
|
+
}
|
|
91
|
+
export function archiveDir(stamp, changeName) {
|
|
92
|
+
return path.posix.join('openspec', 'archive', `${stamp}-${changeName}`);
|
|
93
|
+
}
|
|
94
|
+
export const OPENSPEC_DIR = 'openspec';
|
|
95
|
+
export const OPSX_DIR = '.opsx';
|
|
96
|
+
export const CONFIG_FILES = ['config.yaml', 'config.yml'];
|
|
97
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
/** 定位本包根目录(含 package.json;优先带 skills/ 的那一层)。 */
|
|
5
|
+
export function packageRoot() {
|
|
6
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
let fallback = null;
|
|
8
|
+
for (let i = 0; i < 8; i += 1) {
|
|
9
|
+
if (existsSync(path.join(dir, 'package.json'))) {
|
|
10
|
+
fallback = dir;
|
|
11
|
+
if (existsSync(path.join(dir, 'skills')) || existsSync(path.join(dir, 'presets')))
|
|
12
|
+
return dir;
|
|
13
|
+
}
|
|
14
|
+
const parent = path.dirname(dir);
|
|
15
|
+
if (parent === dir)
|
|
16
|
+
break;
|
|
17
|
+
dir = parent;
|
|
18
|
+
}
|
|
19
|
+
if (fallback)
|
|
20
|
+
return fallback;
|
|
21
|
+
throw new Error('无法定位 OpenSpec 包根目录');
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=pkg.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const VERSION: string;
|
|
2
|
+
export declare const PACKAGE_NAME: string;
|
|
3
|
+
/**
|
|
4
|
+
* 比较 major.minor.patch 版本;预发布后缀(-rc 等)低于同号正式版。
|
|
5
|
+
* 任一侧无法解析时返回 null,调用方按「版本未知」处理,不当作满足要求。
|
|
6
|
+
*/
|
|
7
|
+
export declare function compareVersions(a: string, b: string): number | null;
|
|
8
|
+
export declare function shortHash(value: string): string;
|
|
9
|
+
//# sourceMappingURL=version.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { packageRoot } from './pkg.js';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
const pkg = JSON.parse(readFileSync(path.join(packageRoot(), 'package.json'), 'utf8'));
|
|
6
|
+
export const VERSION = pkg.version;
|
|
7
|
+
export const PACKAGE_NAME = pkg.name;
|
|
8
|
+
/**
|
|
9
|
+
* 比较 major.minor.patch 版本;预发布后缀(-rc 等)低于同号正式版。
|
|
10
|
+
* 任一侧无法解析时返回 null,调用方按「版本未知」处理,不当作满足要求。
|
|
11
|
+
*/
|
|
12
|
+
export function compareVersions(a, b) {
|
|
13
|
+
const parse = (value) => /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value.trim());
|
|
14
|
+
const left = parse(a);
|
|
15
|
+
const right = parse(b);
|
|
16
|
+
if (!left || !right)
|
|
17
|
+
return null;
|
|
18
|
+
for (let index = 1; index <= 3; index += 1) {
|
|
19
|
+
const diff = Number(left[index]) - Number(right[index]);
|
|
20
|
+
if (diff !== 0)
|
|
21
|
+
return Math.sign(diff);
|
|
22
|
+
}
|
|
23
|
+
if (left[4] === right[4])
|
|
24
|
+
return 0;
|
|
25
|
+
if (left[4] === undefined)
|
|
26
|
+
return 1;
|
|
27
|
+
if (right[4] === undefined)
|
|
28
|
+
return -1;
|
|
29
|
+
return left[4] < right[4] ? -1 : 1;
|
|
30
|
+
}
|
|
31
|
+
export function shortHash(value) {
|
|
32
|
+
return createHash('sha256').update(value).digest('hex').slice(0, 12);
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { UsageOptions, UsageReport } from './types.js';
|
|
2
|
+
export { sumTokens } from './parse.js';
|
|
3
|
+
export declare function analyzeUsage(options: UsageOptions): UsageReport;
|
|
4
|
+
export declare function renderUsageMarkdown(report: UsageReport): string;
|
|
5
|
+
//# sourceMappingURL=analyze.d.ts.map
|