openspec-cn 0.23.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/LICENSE +22 -0
- package/README.md +153 -0
- package/bin/openspec.js +3 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +480 -0
- package/dist/commands/change.d.ts +35 -0
- package/dist/commands/change.js +277 -0
- package/dist/commands/completion.d.ts +72 -0
- package/dist/commands/completion.js +257 -0
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +198 -0
- package/dist/commands/feedback.d.ts +9 -0
- package/dist/commands/feedback.js +183 -0
- package/dist/commands/schema.d.ts +6 -0
- package/dist/commands/schema.js +869 -0
- package/dist/commands/show.d.ts +14 -0
- package/dist/commands/show.js +132 -0
- package/dist/commands/spec.d.ts +15 -0
- package/dist/commands/spec.js +225 -0
- package/dist/commands/validate.d.ts +24 -0
- package/dist/commands/validate.js +294 -0
- package/dist/commands/workflow/index.d.ts +17 -0
- package/dist/commands/workflow/index.js +12 -0
- package/dist/commands/workflow/instructions.d.ts +29 -0
- package/dist/commands/workflow/instructions.js +381 -0
- package/dist/commands/workflow/new-change.d.ts +11 -0
- package/dist/commands/workflow/new-change.js +44 -0
- package/dist/commands/workflow/schemas.d.ts +10 -0
- package/dist/commands/workflow/schemas.js +34 -0
- package/dist/commands/workflow/shared.d.ts +52 -0
- package/dist/commands/workflow/shared.js +111 -0
- package/dist/commands/workflow/status.d.ts +14 -0
- package/dist/commands/workflow/status.js +58 -0
- package/dist/commands/workflow/templates.d.ts +16 -0
- package/dist/commands/workflow/templates.js +68 -0
- package/dist/core/archive.d.ts +11 -0
- package/dist/core/archive.js +280 -0
- package/dist/core/artifact-graph/graph.d.ts +56 -0
- package/dist/core/artifact-graph/graph.js +141 -0
- package/dist/core/artifact-graph/index.d.ts +7 -0
- package/dist/core/artifact-graph/index.js +13 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +143 -0
- package/dist/core/artifact-graph/instruction-loader.js +214 -0
- package/dist/core/artifact-graph/resolver.d.ts +81 -0
- package/dist/core/artifact-graph/resolver.js +257 -0
- package/dist/core/artifact-graph/schema.d.ts +13 -0
- package/dist/core/artifact-graph/schema.js +108 -0
- package/dist/core/artifact-graph/state.d.ts +12 -0
- package/dist/core/artifact-graph/state.js +54 -0
- package/dist/core/artifact-graph/types.d.ts +45 -0
- package/dist/core/artifact-graph/types.js +43 -0
- package/dist/core/command-generation/adapters/amazon-q.d.ts +13 -0
- package/dist/core/command-generation/adapters/amazon-q.js +26 -0
- package/dist/core/command-generation/adapters/antigravity.d.ts +13 -0
- package/dist/core/command-generation/adapters/antigravity.js +26 -0
- package/dist/core/command-generation/adapters/auggie.d.ts +13 -0
- package/dist/core/command-generation/adapters/auggie.js +27 -0
- package/dist/core/command-generation/adapters/claude.d.ts +13 -0
- package/dist/core/command-generation/adapters/claude.js +50 -0
- package/dist/core/command-generation/adapters/cline.d.ts +14 -0
- package/dist/core/command-generation/adapters/cline.js +27 -0
- package/dist/core/command-generation/adapters/codebuddy.d.ts +13 -0
- package/dist/core/command-generation/adapters/codebuddy.js +28 -0
- package/dist/core/command-generation/adapters/codex.d.ts +13 -0
- package/dist/core/command-generation/adapters/codex.js +27 -0
- package/dist/core/command-generation/adapters/continue.d.ts +13 -0
- package/dist/core/command-generation/adapters/continue.js +28 -0
- package/dist/core/command-generation/adapters/costrict.d.ts +13 -0
- package/dist/core/command-generation/adapters/costrict.js +27 -0
- package/dist/core/command-generation/adapters/crush.d.ts +13 -0
- package/dist/core/command-generation/adapters/crush.js +30 -0
- package/dist/core/command-generation/adapters/cursor.d.ts +14 -0
- package/dist/core/command-generation/adapters/cursor.js +44 -0
- package/dist/core/command-generation/adapters/factory.d.ts +13 -0
- package/dist/core/command-generation/adapters/factory.js +27 -0
- package/dist/core/command-generation/adapters/gemini.d.ts +13 -0
- package/dist/core/command-generation/adapters/gemini.js +26 -0
- package/dist/core/command-generation/adapters/github-copilot.d.ts +13 -0
- package/dist/core/command-generation/adapters/github-copilot.js +26 -0
- package/dist/core/command-generation/adapters/iflow.d.ts +13 -0
- package/dist/core/command-generation/adapters/iflow.js +29 -0
- package/dist/core/command-generation/adapters/index.d.ts +27 -0
- package/dist/core/command-generation/adapters/index.js +27 -0
- package/dist/core/command-generation/adapters/kilocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/kilocode.js +23 -0
- package/dist/core/command-generation/adapters/opencode.d.ts +13 -0
- package/dist/core/command-generation/adapters/opencode.js +26 -0
- package/dist/core/command-generation/adapters/qoder.d.ts +13 -0
- package/dist/core/command-generation/adapters/qoder.js +30 -0
- package/dist/core/command-generation/adapters/qwen.d.ts +13 -0
- package/dist/core/command-generation/adapters/qwen.js +26 -0
- package/dist/core/command-generation/adapters/roocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/roocode.js +27 -0
- package/dist/core/command-generation/adapters/windsurf.d.ts +14 -0
- package/dist/core/command-generation/adapters/windsurf.js +51 -0
- package/dist/core/command-generation/generator.d.ts +21 -0
- package/dist/core/command-generation/generator.js +27 -0
- package/dist/core/command-generation/index.d.ts +22 -0
- package/dist/core/command-generation/index.js +24 -0
- package/dist/core/command-generation/registry.d.ts +36 -0
- package/dist/core/command-generation/registry.js +88 -0
- package/dist/core/command-generation/types.d.ts +55 -0
- package/dist/core/command-generation/types.js +8 -0
- package/dist/core/completions/command-registry.d.ts +7 -0
- package/dist/core/completions/command-registry.js +456 -0
- package/dist/core/completions/completion-provider.d.ts +60 -0
- package/dist/core/completions/completion-provider.js +102 -0
- package/dist/core/completions/factory.d.ts +64 -0
- package/dist/core/completions/factory.js +75 -0
- package/dist/core/completions/generators/bash-generator.d.ts +32 -0
- package/dist/core/completions/generators/bash-generator.js +174 -0
- package/dist/core/completions/generators/fish-generator.d.ts +32 -0
- package/dist/core/completions/generators/fish-generator.js +157 -0
- package/dist/core/completions/generators/powershell-generator.d.ts +33 -0
- package/dist/core/completions/generators/powershell-generator.js +207 -0
- package/dist/core/completions/generators/zsh-generator.d.ts +44 -0
- package/dist/core/completions/generators/zsh-generator.js +250 -0
- package/dist/core/completions/installers/bash-installer.d.ts +87 -0
- package/dist/core/completions/installers/bash-installer.js +318 -0
- package/dist/core/completions/installers/fish-installer.d.ts +43 -0
- package/dist/core/completions/installers/fish-installer.js +143 -0
- package/dist/core/completions/installers/powershell-installer.d.ts +88 -0
- package/dist/core/completions/installers/powershell-installer.js +327 -0
- package/dist/core/completions/installers/zsh-installer.d.ts +125 -0
- package/dist/core/completions/installers/zsh-installer.js +449 -0
- package/dist/core/completions/templates/bash-templates.d.ts +6 -0
- package/dist/core/completions/templates/bash-templates.js +24 -0
- package/dist/core/completions/templates/fish-templates.d.ts +7 -0
- package/dist/core/completions/templates/fish-templates.js +39 -0
- package/dist/core/completions/templates/powershell-templates.d.ts +6 -0
- package/dist/core/completions/templates/powershell-templates.js +25 -0
- package/dist/core/completions/templates/zsh-templates.d.ts +6 -0
- package/dist/core/completions/templates/zsh-templates.js +36 -0
- package/dist/core/completions/types.d.ts +79 -0
- package/dist/core/completions/types.js +2 -0
- package/dist/core/config-prompts.d.ts +9 -0
- package/dist/core/config-prompts.js +34 -0
- package/dist/core/config-schema.d.ts +76 -0
- package/dist/core/config-schema.js +200 -0
- package/dist/core/config.d.ts +17 -0
- package/dist/core/config.js +30 -0
- package/dist/core/converters/json-converter.d.ts +6 -0
- package/dist/core/converters/json-converter.js +51 -0
- package/dist/core/global-config.d.ts +39 -0
- package/dist/core/global-config.js +115 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +3 -0
- package/dist/core/init.d.ts +32 -0
- package/dist/core/init.js +433 -0
- package/dist/core/legacy-cleanup.d.ts +162 -0
- package/dist/core/legacy-cleanup.js +501 -0
- package/dist/core/list.d.ts +9 -0
- package/dist/core/list.js +171 -0
- package/dist/core/parsers/change-parser.d.ts +13 -0
- package/dist/core/parsers/change-parser.js +193 -0
- package/dist/core/parsers/markdown-parser.d.ts +22 -0
- package/dist/core/parsers/markdown-parser.js +187 -0
- package/dist/core/parsers/requirement-blocks.d.ts +37 -0
- package/dist/core/parsers/requirement-blocks.js +201 -0
- package/dist/core/project-config.d.ts +64 -0
- package/dist/core/project-config.js +223 -0
- package/dist/core/schemas/base.schema.d.ts +13 -0
- package/dist/core/schemas/base.schema.js +13 -0
- package/dist/core/schemas/change.schema.d.ts +73 -0
- package/dist/core/schemas/change.schema.js +31 -0
- package/dist/core/schemas/index.d.ts +4 -0
- package/dist/core/schemas/index.js +4 -0
- package/dist/core/schemas/spec.schema.d.ts +18 -0
- package/dist/core/schemas/spec.schema.js +15 -0
- package/dist/core/shared/index.d.ts +8 -0
- package/dist/core/shared/index.js +8 -0
- package/dist/core/shared/skill-generation.d.ts +41 -0
- package/dist/core/shared/skill-generation.js +74 -0
- package/dist/core/shared/tool-detection.d.ts +66 -0
- package/dist/core/shared/tool-detection.js +140 -0
- package/dist/core/specs-apply.d.ts +73 -0
- package/dist/core/specs-apply.js +384 -0
- package/dist/core/styles/palette.d.ts +7 -0
- package/dist/core/styles/palette.js +8 -0
- package/dist/core/templates/index.d.ts +8 -0
- package/dist/core/templates/index.js +9 -0
- package/dist/core/templates/skill-templates.d.ts +112 -0
- package/dist/core/templates/skill-templates.js +2893 -0
- package/dist/core/update.d.ts +42 -0
- package/dist/core/update.js +306 -0
- package/dist/core/validation/constants.d.ts +34 -0
- package/dist/core/validation/constants.js +40 -0
- package/dist/core/validation/types.d.ts +18 -0
- package/dist/core/validation/types.js +2 -0
- package/dist/core/validation/validator.d.ts +33 -0
- package/dist/core/validation/validator.js +409 -0
- package/dist/core/view.d.ts +8 -0
- package/dist/core/view.js +168 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/prompts/searchable-multi-select.d.ts +27 -0
- package/dist/prompts/searchable-multi-select.js +149 -0
- package/dist/telemetry/config.d.ts +32 -0
- package/dist/telemetry/config.js +68 -0
- package/dist/telemetry/index.d.ts +31 -0
- package/dist/telemetry/index.js +145 -0
- package/dist/ui/ascii-patterns.d.ts +16 -0
- package/dist/ui/ascii-patterns.js +133 -0
- package/dist/ui/welcome-screen.d.ts +10 -0
- package/dist/ui/welcome-screen.js +146 -0
- package/dist/utils/change-metadata.d.ts +51 -0
- package/dist/utils/change-metadata.js +147 -0
- package/dist/utils/change-utils.d.ts +62 -0
- package/dist/utils/change-utils.js +121 -0
- package/dist/utils/file-system.d.ts +36 -0
- package/dist/utils/file-system.js +281 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.js +7 -0
- package/dist/utils/interactive.d.ts +18 -0
- package/dist/utils/interactive.js +21 -0
- package/dist/utils/item-discovery.d.ts +4 -0
- package/dist/utils/item-discovery.js +72 -0
- package/dist/utils/match.d.ts +3 -0
- package/dist/utils/match.js +22 -0
- package/dist/utils/shell-detection.d.ts +20 -0
- package/dist/utils/shell-detection.js +41 -0
- package/dist/utils/task-progress.d.ts +8 -0
- package/dist/utils/task-progress.js +36 -0
- package/package.json +84 -0
- package/schemas/spec-driven/schema.yaml +148 -0
- package/schemas/spec-driven/templates/design.md +19 -0
- package/schemas/spec-driven/templates/proposal.md +23 -0
- package/schemas/spec-driven/templates/spec.md +8 -0
- package/schemas/spec-driven/templates/tasks.md +9 -0
- package/schemas/tdd/schema.yaml +213 -0
- package/schemas/tdd/templates/docs.md +15 -0
- package/schemas/tdd/templates/implementation.md +11 -0
- package/schemas/tdd/templates/spec.md +11 -0
- package/schemas/tdd/templates/test.md +11 -0
- package/scripts/postinstall.js +147 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { promises as fs, constants as fsConstants } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
function isMarkerOnOwnLine(content, markerIndex, markerLength) {
|
|
4
|
+
let leftIndex = markerIndex - 1;
|
|
5
|
+
while (leftIndex >= 0 && content[leftIndex] !== '\n') {
|
|
6
|
+
const char = content[leftIndex];
|
|
7
|
+
if (char !== ' ' && char !== '\t' && char !== '\r') {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
leftIndex--;
|
|
11
|
+
}
|
|
12
|
+
let rightIndex = markerIndex + markerLength;
|
|
13
|
+
while (rightIndex < content.length && content[rightIndex] !== '\n') {
|
|
14
|
+
const char = content[rightIndex];
|
|
15
|
+
if (char !== ' ' && char !== '\t' && char !== '\r') {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
rightIndex++;
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
function findMarkerIndex(content, marker, fromIndex = 0) {
|
|
23
|
+
let currentIndex = content.indexOf(marker, fromIndex);
|
|
24
|
+
while (currentIndex !== -1) {
|
|
25
|
+
if (isMarkerOnOwnLine(content, currentIndex, marker.length)) {
|
|
26
|
+
return currentIndex;
|
|
27
|
+
}
|
|
28
|
+
currentIndex = content.indexOf(marker, currentIndex + marker.length);
|
|
29
|
+
}
|
|
30
|
+
return -1;
|
|
31
|
+
}
|
|
32
|
+
export class FileSystemUtils {
|
|
33
|
+
/**
|
|
34
|
+
* Converts a path to use forward slashes (POSIX style).
|
|
35
|
+
* Essential for cross-platform compatibility with glob libraries like fast-glob.
|
|
36
|
+
*/
|
|
37
|
+
static toPosixPath(p) {
|
|
38
|
+
return p.replace(/\\/g, '/');
|
|
39
|
+
}
|
|
40
|
+
static isWindowsBasePath(basePath) {
|
|
41
|
+
return /^[A-Za-z]:[\\/]/.test(basePath) || basePath.startsWith('\\');
|
|
42
|
+
}
|
|
43
|
+
static normalizeSegments(segments) {
|
|
44
|
+
return segments
|
|
45
|
+
.flatMap((segment) => segment.split(/[\\/]+/u))
|
|
46
|
+
.filter((part) => part.length > 0);
|
|
47
|
+
}
|
|
48
|
+
static joinPath(basePath, ...segments) {
|
|
49
|
+
const normalizedSegments = this.normalizeSegments(segments);
|
|
50
|
+
if (this.isWindowsBasePath(basePath)) {
|
|
51
|
+
const normalizedBasePath = path.win32.normalize(basePath);
|
|
52
|
+
return normalizedSegments.length
|
|
53
|
+
? path.win32.join(normalizedBasePath, ...normalizedSegments)
|
|
54
|
+
: normalizedBasePath;
|
|
55
|
+
}
|
|
56
|
+
const posixBasePath = basePath.replace(/\\/g, '/');
|
|
57
|
+
return normalizedSegments.length
|
|
58
|
+
? path.posix.join(posixBasePath, ...normalizedSegments)
|
|
59
|
+
: path.posix.normalize(posixBasePath);
|
|
60
|
+
}
|
|
61
|
+
static async createDirectory(dirPath) {
|
|
62
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
static async fileExists(filePath) {
|
|
65
|
+
try {
|
|
66
|
+
await fs.access(filePath);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error.code !== 'ENOENT') {
|
|
71
|
+
console.debug(`Unable to check if file exists at ${filePath}: ${error.message}`);
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Finds the first existing parent directory by walking up the directory tree.
|
|
78
|
+
* @param dirPath Starting directory path
|
|
79
|
+
* @returns The first existing directory path, or null if root is reached without finding one
|
|
80
|
+
*/
|
|
81
|
+
static async findFirstExistingDirectory(dirPath) {
|
|
82
|
+
let currentDir = dirPath;
|
|
83
|
+
while (true) {
|
|
84
|
+
try {
|
|
85
|
+
const stats = await fs.stat(currentDir);
|
|
86
|
+
if (stats.isDirectory()) {
|
|
87
|
+
return currentDir;
|
|
88
|
+
}
|
|
89
|
+
// Path component exists but is not a directory (edge case)
|
|
90
|
+
console.debug(`Path component ${currentDir} exists but is not a directory`);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error.code === 'ENOENT') {
|
|
95
|
+
// Directory doesn't exist, move up one level
|
|
96
|
+
const parentDir = path.dirname(currentDir);
|
|
97
|
+
if (parentDir === currentDir) {
|
|
98
|
+
// Reached filesystem root without finding existing directory
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
currentDir = parentDir;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Unexpected error (permissions, I/O error, etc.)
|
|
105
|
+
console.debug(`Error checking directory ${currentDir}: ${error.message}`);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
static async canWriteFile(filePath) {
|
|
112
|
+
try {
|
|
113
|
+
const stats = await fs.stat(filePath);
|
|
114
|
+
if (!stats.isFile()) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
// On Windows, stats.mode doesn't reliably indicate write permissions.
|
|
118
|
+
// Use fs.access with W_OK to check actual write permissions cross-platform.
|
|
119
|
+
try {
|
|
120
|
+
await fs.access(filePath, fsConstants.W_OK);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (error.code === 'ENOENT') {
|
|
129
|
+
// File doesn't exist - find first existing parent directory and check its permissions
|
|
130
|
+
const parentDir = path.dirname(filePath);
|
|
131
|
+
const existingDir = await this.findFirstExistingDirectory(parentDir);
|
|
132
|
+
if (existingDir === null) {
|
|
133
|
+
// No existing parent directory found (edge case)
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
// Check if the existing parent directory is writable
|
|
137
|
+
try {
|
|
138
|
+
await fs.access(existingDir, fsConstants.W_OK);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
console.debug(`Unable to determine write permissions for ${filePath}: ${error.message}`);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
static async directoryExists(dirPath) {
|
|
150
|
+
try {
|
|
151
|
+
const stats = await fs.stat(dirPath);
|
|
152
|
+
return stats.isDirectory();
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (error.code !== 'ENOENT') {
|
|
156
|
+
console.debug(`Unable to check if directory exists at ${dirPath}: ${error.message}`);
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
static async writeFile(filePath, content) {
|
|
162
|
+
const dir = path.dirname(filePath);
|
|
163
|
+
await this.createDirectory(dir);
|
|
164
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
165
|
+
}
|
|
166
|
+
static async readFile(filePath) {
|
|
167
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
168
|
+
}
|
|
169
|
+
static async updateFileWithMarkers(filePath, content, startMarker, endMarker) {
|
|
170
|
+
let existingContent = '';
|
|
171
|
+
if (await this.fileExists(filePath)) {
|
|
172
|
+
existingContent = await this.readFile(filePath);
|
|
173
|
+
const startIndex = findMarkerIndex(existingContent, startMarker);
|
|
174
|
+
const endIndex = startIndex !== -1
|
|
175
|
+
? findMarkerIndex(existingContent, endMarker, startIndex + startMarker.length)
|
|
176
|
+
: findMarkerIndex(existingContent, endMarker);
|
|
177
|
+
if (startIndex !== -1 && endIndex !== -1) {
|
|
178
|
+
if (endIndex < startIndex) {
|
|
179
|
+
throw new Error(`Invalid marker state in ${filePath}. End marker appears before start marker.`);
|
|
180
|
+
}
|
|
181
|
+
const before = existingContent.substring(0, startIndex);
|
|
182
|
+
const after = existingContent.substring(endIndex + endMarker.length);
|
|
183
|
+
existingContent = before + startMarker + '\n' + content + '\n' + endMarker + after;
|
|
184
|
+
}
|
|
185
|
+
else if (startIndex === -1 && endIndex === -1) {
|
|
186
|
+
existingContent = startMarker + '\n' + content + '\n' + endMarker + '\n\n' + existingContent;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
throw new Error(`Invalid marker state in ${filePath}. Found start: ${startIndex !== -1}, Found end: ${endIndex !== -1}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
existingContent = startMarker + '\n' + content + '\n' + endMarker;
|
|
194
|
+
}
|
|
195
|
+
await this.writeFile(filePath, existingContent);
|
|
196
|
+
}
|
|
197
|
+
static async ensureWritePermissions(dirPath) {
|
|
198
|
+
try {
|
|
199
|
+
// If directory doesn't exist, check parent directory permissions
|
|
200
|
+
if (!await this.directoryExists(dirPath)) {
|
|
201
|
+
const parentDir = path.dirname(dirPath);
|
|
202
|
+
if (!await this.directoryExists(parentDir)) {
|
|
203
|
+
await this.createDirectory(parentDir);
|
|
204
|
+
}
|
|
205
|
+
return await this.ensureWritePermissions(parentDir);
|
|
206
|
+
}
|
|
207
|
+
const testFile = path.join(dirPath, '.openspec-test-' + Date.now() + '-' + Math.random().toString(36).slice(2));
|
|
208
|
+
await fs.writeFile(testFile, '');
|
|
209
|
+
// On Windows, file may be temporarily locked by antivirus or indexing services.
|
|
210
|
+
// Retry unlink with a small delay if it fails.
|
|
211
|
+
const maxRetries = 3;
|
|
212
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
213
|
+
try {
|
|
214
|
+
await fs.unlink(testFile);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
catch (unlinkError) {
|
|
218
|
+
if (attempt === maxRetries - 1) {
|
|
219
|
+
// Last attempt failed, but we successfully wrote the file, so permissions are OK
|
|
220
|
+
// Just log and continue - the temp file will be cleaned up eventually
|
|
221
|
+
console.debug(`Could not clean up test file ${testFile}: ${unlinkError.message}`);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
// Wait briefly before retrying (Windows file lock release)
|
|
225
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
console.debug(`Insufficient permissions to write to ${dirPath}: ${error.message}`);
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Removes a marker block from file content.
|
|
239
|
+
* Only removes markers that are on their own lines (ignores inline mentions).
|
|
240
|
+
* Cleans up double blank lines that may result from removal.
|
|
241
|
+
*
|
|
242
|
+
* @param content - File content with markers
|
|
243
|
+
* @param startMarker - The start marker string
|
|
244
|
+
* @param endMarker - The end marker string
|
|
245
|
+
* @returns Content with marker block removed, or original content if markers not found/invalid
|
|
246
|
+
*/
|
|
247
|
+
export function removeMarkerBlock(content, startMarker, endMarker) {
|
|
248
|
+
const startIndex = findMarkerIndex(content, startMarker);
|
|
249
|
+
const endIndex = startIndex !== -1
|
|
250
|
+
? findMarkerIndex(content, endMarker, startIndex + startMarker.length)
|
|
251
|
+
: findMarkerIndex(content, endMarker);
|
|
252
|
+
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
|
|
253
|
+
return content;
|
|
254
|
+
}
|
|
255
|
+
// Find the start of the line containing the start marker
|
|
256
|
+
let lineStart = startIndex;
|
|
257
|
+
while (lineStart > 0 && content[lineStart - 1] !== '\n') {
|
|
258
|
+
lineStart--;
|
|
259
|
+
}
|
|
260
|
+
// Find the end of the line containing the end marker
|
|
261
|
+
let lineEnd = endIndex + endMarker.length;
|
|
262
|
+
while (lineEnd < content.length && content[lineEnd] !== '\n') {
|
|
263
|
+
lineEnd++;
|
|
264
|
+
}
|
|
265
|
+
// Include the trailing newline if present
|
|
266
|
+
if (lineEnd < content.length && content[lineEnd] === '\n') {
|
|
267
|
+
lineEnd++;
|
|
268
|
+
}
|
|
269
|
+
const before = content.substring(0, lineStart);
|
|
270
|
+
const after = content.substring(lineEnd);
|
|
271
|
+
// Clean up double blank lines (handle both Unix \n and Windows \r\n)
|
|
272
|
+
let result = before + after;
|
|
273
|
+
result = result.replace(/(\r?\n){3,}/g, '\n\n');
|
|
274
|
+
// Trim trailing whitespace but preserve leading whitespace and original newline style
|
|
275
|
+
if (result.trimEnd() === '') {
|
|
276
|
+
return '';
|
|
277
|
+
}
|
|
278
|
+
const newline = content.includes('\r\n') ? '\r\n' : '\n';
|
|
279
|
+
return result.trimEnd() + newline;
|
|
280
|
+
}
|
|
281
|
+
//# sourceMappingURL=file-system.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { validateChangeName, createChange } from './change-utils.js';
|
|
2
|
+
export type { ValidationResult, CreateChangeOptions } from './change-utils.js';
|
|
3
|
+
export { readChangeMetadata, writeChangeMetadata, resolveSchemaForChange, validateSchemaName, ChangeMetadataError, } from './change-metadata.js';
|
|
4
|
+
export { FileSystemUtils, removeMarkerBlock } from './file-system.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Shared utilities
|
|
2
|
+
export { validateChangeName, createChange } from './change-utils.js';
|
|
3
|
+
// Change metadata utilities
|
|
4
|
+
export { readChangeMetadata, writeChangeMetadata, resolveSchemaForChange, validateSchemaName, ChangeMetadataError, } from './change-metadata.js';
|
|
5
|
+
// File system utilities
|
|
6
|
+
export { FileSystemUtils, removeMarkerBlock } from './file-system.js';
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type InteractiveOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* Explicit "disable prompts" flag passed by internal callers.
|
|
4
|
+
*/
|
|
5
|
+
noInteractive?: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Commander-style negated option: `--no-interactive` sets this to false.
|
|
8
|
+
*/
|
|
9
|
+
interactive?: boolean;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Resolves whether non-interactive mode is requested.
|
|
13
|
+
* Handles both explicit `noInteractive: true` and Commander.js style `interactive: false`.
|
|
14
|
+
* Use this helper instead of manually checking options.noInteractive to avoid bugs.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveNoInteractive(value?: boolean | InteractiveOptions): boolean;
|
|
17
|
+
export declare function isInteractive(value?: boolean | InteractiveOptions): boolean;
|
|
18
|
+
//# sourceMappingURL=interactive.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves whether non-interactive mode is requested.
|
|
3
|
+
* Handles both explicit `noInteractive: true` and Commander.js style `interactive: false`.
|
|
4
|
+
* Use this helper instead of manually checking options.noInteractive to avoid bugs.
|
|
5
|
+
*/
|
|
6
|
+
export function resolveNoInteractive(value) {
|
|
7
|
+
if (typeof value === 'boolean')
|
|
8
|
+
return value;
|
|
9
|
+
return value?.noInteractive === true || value?.interactive === false;
|
|
10
|
+
}
|
|
11
|
+
export function isInteractive(value) {
|
|
12
|
+
if (resolveNoInteractive(value))
|
|
13
|
+
return false;
|
|
14
|
+
if (process.env.OPEN_SPEC_INTERACTIVE === '0')
|
|
15
|
+
return false;
|
|
16
|
+
// Respect the standard CI environment variable (set by GitHub Actions, GitLab CI, Travis, etc.)
|
|
17
|
+
if ('CI' in process.env)
|
|
18
|
+
return false;
|
|
19
|
+
return !!process.stdin.isTTY;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=interactive.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function getActiveChangeIds(root?: string): Promise<string[]>;
|
|
2
|
+
export declare function getSpecIds(root?: string): Promise<string[]>;
|
|
3
|
+
export declare function getArchivedChangeIds(root?: string): Promise<string[]>;
|
|
4
|
+
//# sourceMappingURL=item-discovery.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export async function getActiveChangeIds(root = process.cwd()) {
|
|
4
|
+
const changesPath = path.join(root, 'openspec', 'changes');
|
|
5
|
+
try {
|
|
6
|
+
const entries = await fs.readdir(changesPath, { withFileTypes: true });
|
|
7
|
+
const result = [];
|
|
8
|
+
for (const entry of entries) {
|
|
9
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'archive')
|
|
10
|
+
continue;
|
|
11
|
+
const proposalPath = path.join(changesPath, entry.name, 'proposal.md');
|
|
12
|
+
try {
|
|
13
|
+
await fs.access(proposalPath);
|
|
14
|
+
result.push(entry.name);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// skip directories without proposal.md
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return result.sort();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function getSpecIds(root = process.cwd()) {
|
|
27
|
+
const specsPath = path.join(root, 'openspec', 'specs');
|
|
28
|
+
const result = [];
|
|
29
|
+
try {
|
|
30
|
+
const entries = await fs.readdir(specsPath, { withFileTypes: true });
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
33
|
+
continue;
|
|
34
|
+
const specFile = path.join(specsPath, entry.name, 'spec.md');
|
|
35
|
+
try {
|
|
36
|
+
await fs.access(specFile);
|
|
37
|
+
result.push(entry.name);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// ignore
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// ignore
|
|
46
|
+
}
|
|
47
|
+
return result.sort();
|
|
48
|
+
}
|
|
49
|
+
export async function getArchivedChangeIds(root = process.cwd()) {
|
|
50
|
+
const archivePath = path.join(root, 'openspec', 'changes', 'archive');
|
|
51
|
+
try {
|
|
52
|
+
const entries = await fs.readdir(archivePath, { withFileTypes: true });
|
|
53
|
+
const result = [];
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
56
|
+
continue;
|
|
57
|
+
const proposalPath = path.join(archivePath, entry.name, 'proposal.md');
|
|
58
|
+
try {
|
|
59
|
+
await fs.access(proposalPath);
|
|
60
|
+
result.push(entry.name);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// skip directories without proposal.md
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return result.sort();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=item-discovery.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function nearestMatches(input, candidates, max = 5) {
|
|
2
|
+
const scored = candidates.map(candidate => ({ candidate, distance: levenshtein(input, candidate) }));
|
|
3
|
+
scored.sort((a, b) => a.distance - b.distance);
|
|
4
|
+
return scored.slice(0, max).map(s => s.candidate);
|
|
5
|
+
}
|
|
6
|
+
export function levenshtein(a, b) {
|
|
7
|
+
const m = a.length;
|
|
8
|
+
const n = b.length;
|
|
9
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
10
|
+
for (let i = 0; i <= m; i++)
|
|
11
|
+
dp[i][0] = i;
|
|
12
|
+
for (let j = 0; j <= n; j++)
|
|
13
|
+
dp[0][j] = j;
|
|
14
|
+
for (let i = 1; i <= m; i++) {
|
|
15
|
+
for (let j = 1; j <= n; j++) {
|
|
16
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
17
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return dp[m][n];
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=match.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supported shell types for completion generation
|
|
3
|
+
*/
|
|
4
|
+
export type SupportedShell = 'zsh' | 'bash' | 'fish' | 'powershell';
|
|
5
|
+
/**
|
|
6
|
+
* Result of shell detection
|
|
7
|
+
*/
|
|
8
|
+
export interface ShellDetectionResult {
|
|
9
|
+
/** The detected shell if supported, otherwise undefined */
|
|
10
|
+
shell: SupportedShell | undefined;
|
|
11
|
+
/** The raw shell name detected (even if unsupported), or undefined if nothing detected */
|
|
12
|
+
detected: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Detects the current user's shell based on environment variables
|
|
16
|
+
*
|
|
17
|
+
* @returns Detection result with supported shell and raw detected name
|
|
18
|
+
*/
|
|
19
|
+
export declare function detectShell(): ShellDetectionResult;
|
|
20
|
+
//# sourceMappingURL=shell-detection.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects the current user's shell based on environment variables
|
|
3
|
+
*
|
|
4
|
+
* @returns Detection result with supported shell and raw detected name
|
|
5
|
+
*/
|
|
6
|
+
export function detectShell() {
|
|
7
|
+
// Try SHELL environment variable first (Unix-like systems)
|
|
8
|
+
const shellPath = process.env.SHELL;
|
|
9
|
+
if (shellPath) {
|
|
10
|
+
const shellName = shellPath.toLowerCase();
|
|
11
|
+
if (shellName.includes('zsh')) {
|
|
12
|
+
return { shell: 'zsh', detected: 'zsh' };
|
|
13
|
+
}
|
|
14
|
+
if (shellName.includes('bash')) {
|
|
15
|
+
return { shell: 'bash', detected: 'bash' };
|
|
16
|
+
}
|
|
17
|
+
if (shellName.includes('fish')) {
|
|
18
|
+
return { shell: 'fish', detected: 'fish' };
|
|
19
|
+
}
|
|
20
|
+
// Shell detected but not supported
|
|
21
|
+
// Extract shell name from path (e.g., /bin/tcsh -> tcsh)
|
|
22
|
+
const match = shellPath.match(/\/([^/]+)$/);
|
|
23
|
+
const detectedName = match ? match[1] : shellPath;
|
|
24
|
+
return { shell: undefined, detected: detectedName };
|
|
25
|
+
}
|
|
26
|
+
// Check for PowerShell on Windows
|
|
27
|
+
// PSModulePath is a reliable PowerShell-specific environment variable
|
|
28
|
+
if (process.env.PSModulePath || process.platform === 'win32') {
|
|
29
|
+
const comspec = process.env.COMSPEC?.toLowerCase();
|
|
30
|
+
// If PSModulePath exists, we're definitely in PowerShell
|
|
31
|
+
if (process.env.PSModulePath) {
|
|
32
|
+
return { shell: 'powershell', detected: 'powershell' };
|
|
33
|
+
}
|
|
34
|
+
// On Windows without PSModulePath, we might be in cmd.exe
|
|
35
|
+
if (comspec?.includes('cmd.exe')) {
|
|
36
|
+
return { shell: undefined, detected: 'cmd.exe' };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { shell: undefined, detected: undefined };
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=shell-detection.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface TaskProgress {
|
|
2
|
+
total: number;
|
|
3
|
+
completed: number;
|
|
4
|
+
}
|
|
5
|
+
export declare function countTasksFromContent(content: string): TaskProgress;
|
|
6
|
+
export declare function getTaskProgressForChange(changesDir: string, changeName: string): Promise<TaskProgress>;
|
|
7
|
+
export declare function formatTaskStatus(progress: TaskProgress): string;
|
|
8
|
+
//# sourceMappingURL=task-progress.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const TASK_PATTERN = /^[-*]\s+\[[\sx]\]/i;
|
|
4
|
+
const COMPLETED_TASK_PATTERN = /^[-*]\s+\[x\]/i;
|
|
5
|
+
export function countTasksFromContent(content) {
|
|
6
|
+
const lines = content.split('\n');
|
|
7
|
+
let total = 0;
|
|
8
|
+
let completed = 0;
|
|
9
|
+
for (const line of lines) {
|
|
10
|
+
if (line.match(TASK_PATTERN)) {
|
|
11
|
+
total++;
|
|
12
|
+
if (line.match(COMPLETED_TASK_PATTERN)) {
|
|
13
|
+
completed++;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return { total, completed };
|
|
18
|
+
}
|
|
19
|
+
export async function getTaskProgressForChange(changesDir, changeName) {
|
|
20
|
+
const tasksPath = path.join(changesDir, changeName, 'tasks.md');
|
|
21
|
+
try {
|
|
22
|
+
const content = await fs.readFile(tasksPath, 'utf-8');
|
|
23
|
+
return countTasksFromContent(content);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { total: 0, completed: 0 };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function formatTaskStatus(progress) {
|
|
30
|
+
if (progress.total === 0)
|
|
31
|
+
return 'No tasks';
|
|
32
|
+
if (progress.completed === progress.total)
|
|
33
|
+
return '✓ Complete';
|
|
34
|
+
return `${progress.completed}/${progress.total} tasks`;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=task-progress.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openspec-cn",
|
|
3
|
+
"version": "0.23.0",
|
|
4
|
+
"description": "AI-native system for spec-driven development (Chinese Edition)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"openspec",
|
|
7
|
+
"openspec-cn",
|
|
8
|
+
"specs",
|
|
9
|
+
"cli",
|
|
10
|
+
"ai",
|
|
11
|
+
"development"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/xiangagou163/OpenSpec-cn",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/xiangagou163/OpenSpec-cn"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "OpenSpec-cn Contributors",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"bin": {
|
|
31
|
+
"openspec": "./bin/openspec.js"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"bin",
|
|
36
|
+
"schemas",
|
|
37
|
+
"scripts/postinstall.js",
|
|
38
|
+
"!dist/**/*.test.js",
|
|
39
|
+
"!dist/**/__tests__",
|
|
40
|
+
"!dist/**/*.map"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"lint": "eslint src/",
|
|
44
|
+
"build": "node build.js",
|
|
45
|
+
"dev": "tsc --watch",
|
|
46
|
+
"dev:cli": "pnpm build && node bin/openspec.js",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"test:watch": "vitest",
|
|
49
|
+
"test:ui": "vitest --ui",
|
|
50
|
+
"test:coverage": "vitest --coverage",
|
|
51
|
+
"test:postinstall": "node scripts/postinstall.js",
|
|
52
|
+
"prepare": "pnpm run build",
|
|
53
|
+
"prepublishOnly": "pnpm run build",
|
|
54
|
+
"postinstall": "node scripts/postinstall.js",
|
|
55
|
+
"check:pack-version": "node scripts/pack-version-check.mjs",
|
|
56
|
+
"release": "pnpm run release:ci",
|
|
57
|
+
"release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
|
|
58
|
+
"changeset": "changeset"
|
|
59
|
+
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=20.19.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@changesets/changelog-github": "^0.5.2",
|
|
65
|
+
"@changesets/cli": "^2.27.7",
|
|
66
|
+
"@types/node": "^24.2.0",
|
|
67
|
+
"@vitest/ui": "^3.2.4",
|
|
68
|
+
"eslint": "^9.39.2",
|
|
69
|
+
"typescript": "^5.9.3",
|
|
70
|
+
"typescript-eslint": "^8.50.1",
|
|
71
|
+
"vitest": "^3.2.4"
|
|
72
|
+
},
|
|
73
|
+
"dependencies": {
|
|
74
|
+
"@inquirer/core": "^10.2.2",
|
|
75
|
+
"@inquirer/prompts": "^7.8.0",
|
|
76
|
+
"chalk": "^5.5.0",
|
|
77
|
+
"commander": "^14.0.0",
|
|
78
|
+
"fast-glob": "^3.3.3",
|
|
79
|
+
"ora": "^8.2.0",
|
|
80
|
+
"posthog-node": "^5.20.0",
|
|
81
|
+
"yaml": "^2.8.2",
|
|
82
|
+
"zod": "^4.0.17"
|
|
83
|
+
}
|
|
84
|
+
}
|