opensdd 0.1.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 +21 -0
- package/bin/opensdd.js +2 -0
- package/opensdd/cli.md +633 -0
- package/opensdd/sdd-generate.md +209 -0
- package/opensdd/sdd-manager.md +134 -0
- package/opensdd/spec-format.md +494 -0
- package/package.json +31 -0
- package/src/commands/init.js +184 -0
- package/src/commands/install.js +161 -0
- package/src/commands/list.js +40 -0
- package/src/commands/publish.js +235 -0
- package/src/commands/status.js +91 -0
- package/src/commands/update.js +227 -0
- package/src/commands/updateApply.js +149 -0
- package/src/commands/validate.js +95 -0
- package/src/index.js +126 -0
- package/src/lib/manifest.js +38 -0
- package/src/lib/registry.js +176 -0
- package/src/lib/skills.js +204 -0
- package/src/lib/validation.js +122 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createPatch } from 'diff';
|
|
4
|
+
import {
|
|
5
|
+
findManifestPath,
|
|
6
|
+
readManifest,
|
|
7
|
+
getDepsDir,
|
|
8
|
+
} from '../lib/manifest.js';
|
|
9
|
+
import {
|
|
10
|
+
resolveRegistry,
|
|
11
|
+
fetchSpecIndex,
|
|
12
|
+
fetchSpecManifest,
|
|
13
|
+
fetchSpecFiles,
|
|
14
|
+
} from '../lib/registry.js';
|
|
15
|
+
|
|
16
|
+
export async function updateCommand(name, options) {
|
|
17
|
+
const manifestPath = findManifestPath(process.cwd());
|
|
18
|
+
if (!manifestPath) {
|
|
19
|
+
console.error('Error: OpenSDD not initialized. Run `opensdd init` to get started.');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const manifest = readManifest(manifestPath);
|
|
24
|
+
const projectRoot = path.dirname(manifestPath);
|
|
25
|
+
const depsDir = getDepsDir(manifest);
|
|
26
|
+
const depsDirPath = path.join(projectRoot, depsDir);
|
|
27
|
+
const registrySource = resolveRegistry(options, manifest);
|
|
28
|
+
const deps = manifest.dependencies || {};
|
|
29
|
+
|
|
30
|
+
// Determine which specs to update
|
|
31
|
+
let specsToUpdate;
|
|
32
|
+
if (name) {
|
|
33
|
+
if (!deps[name]) {
|
|
34
|
+
console.error(`Error: ${name} is not installed. Run \`opensdd install ${name}\` first.`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
specsToUpdate = [name];
|
|
38
|
+
} else {
|
|
39
|
+
specsToUpdate = Object.keys(deps);
|
|
40
|
+
if (specsToUpdate.length === 0) {
|
|
41
|
+
console.log('No dependencies installed.');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const results = [];
|
|
47
|
+
|
|
48
|
+
for (const specName of specsToUpdate) {
|
|
49
|
+
const entry = deps[specName];
|
|
50
|
+
const specDir = path.join(depsDirPath, specName);
|
|
51
|
+
|
|
52
|
+
// Fetch latest from registry
|
|
53
|
+
let index;
|
|
54
|
+
try {
|
|
55
|
+
index = await fetchSpecIndex(registrySource, specName);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error(`Error: Could not reach registry for ${specName}: ${err.message}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!index) {
|
|
62
|
+
console.warn(
|
|
63
|
+
`Warning: ${specName} is no longer available in the registry. Local files preserved.`
|
|
64
|
+
);
|
|
65
|
+
results.push({ name: specName, status: 'not_found' });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const latestVersion = index.latest;
|
|
70
|
+
|
|
71
|
+
if (latestVersion === entry.version) {
|
|
72
|
+
results.push({ name: specName, status: 'up_to_date', version: entry.version });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Fetch new version files
|
|
77
|
+
const newManifest = await fetchSpecManifest(registrySource, specName, latestVersion);
|
|
78
|
+
const newFiles = await fetchSpecFiles(registrySource, specName, latestVersion);
|
|
79
|
+
|
|
80
|
+
if (!newManifest || !newFiles) {
|
|
81
|
+
console.error(`Error: Could not fetch ${specName}@${latestVersion} from registry.`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Step d: Compute unified diffs of all spec-owned files before overwriting
|
|
86
|
+
const diffs = {};
|
|
87
|
+
const changedFiles = [];
|
|
88
|
+
|
|
89
|
+
for (const [fileName, newContent] of Object.entries(newFiles)) {
|
|
90
|
+
// deviations.md is consumer-owned — skip it in diffs and overwrites
|
|
91
|
+
if (fileName === 'deviations.md') continue;
|
|
92
|
+
|
|
93
|
+
const existingPath = path.join(specDir, fileName);
|
|
94
|
+
const oldContent = fs.existsSync(existingPath)
|
|
95
|
+
? fs.readFileSync(existingPath, 'utf-8')
|
|
96
|
+
: '';
|
|
97
|
+
|
|
98
|
+
if (oldContent !== newContent) {
|
|
99
|
+
diffs[fileName] = createPatch(fileName, oldContent, newContent);
|
|
100
|
+
changedFiles.push(fileName);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Step e: Overwrite all spec-owned files
|
|
105
|
+
fs.mkdirSync(specDir, { recursive: true });
|
|
106
|
+
for (const [fileName, content] of Object.entries(newFiles)) {
|
|
107
|
+
// Step f: MUST NOT overwrite deviations.md
|
|
108
|
+
if (fileName === 'deviations.md') continue;
|
|
109
|
+
fs.writeFileSync(path.join(specDir, fileName), content);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Step g: Create staging directory
|
|
113
|
+
const updatesDir = path.join(depsDirPath, '.updates', specName);
|
|
114
|
+
const wasReplaced = fs.existsSync(updatesDir);
|
|
115
|
+
fs.mkdirSync(updatesDir, { recursive: true });
|
|
116
|
+
|
|
117
|
+
// Write changeset.md
|
|
118
|
+
const specFormatOld = entry.spec_format || '0.1.0';
|
|
119
|
+
const specFormatNew = newManifest.spec_format || '0.1.0';
|
|
120
|
+
const specFormatChange =
|
|
121
|
+
specFormatOld === specFormatNew
|
|
122
|
+
? 'unchanged'
|
|
123
|
+
: `${specFormatOld} \u2192 ${specFormatNew}`;
|
|
124
|
+
|
|
125
|
+
let changeset = `# Changeset: ${specName}\n\n`;
|
|
126
|
+
changeset += `**Previous version:** ${entry.version}\n`;
|
|
127
|
+
changeset += `**New version:** ${latestVersion}\n`;
|
|
128
|
+
changeset += `**Spec-format:** ${specFormatChange}\n`;
|
|
129
|
+
changeset += `**Date:** ${new Date().toISOString().split('T')[0]}\n\n`;
|
|
130
|
+
changeset += '## Changed Files\n';
|
|
131
|
+
|
|
132
|
+
for (const [fileName, diff] of Object.entries(diffs)) {
|
|
133
|
+
changeset += `\n### ${fileName}\n\n\`\`\`diff\n${diff}\`\`\`\n`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (changedFiles.length === 0) {
|
|
137
|
+
changeset += '\nNo file content changes (metadata only).\n';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fs.writeFileSync(path.join(updatesDir, 'changeset.md'), changeset);
|
|
141
|
+
|
|
142
|
+
// Write manifest.json for staging
|
|
143
|
+
const stageManifest = {
|
|
144
|
+
name: specName,
|
|
145
|
+
previous_version: entry.version,
|
|
146
|
+
version: latestVersion,
|
|
147
|
+
source: registrySource,
|
|
148
|
+
spec_format: newManifest.spec_format || '0.1.0',
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
fs.writeFileSync(
|
|
152
|
+
path.join(updatesDir, 'manifest.json'),
|
|
153
|
+
JSON.stringify(stageManifest, null, 2) + '\n'
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
results.push({
|
|
157
|
+
name: specName,
|
|
158
|
+
status: 'updated',
|
|
159
|
+
oldVersion: entry.version,
|
|
160
|
+
newVersion: latestVersion,
|
|
161
|
+
changedFiles,
|
|
162
|
+
replaced: wasReplaced,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Print output
|
|
167
|
+
if (name) {
|
|
168
|
+
// Single spec output
|
|
169
|
+
const result = results[0];
|
|
170
|
+
if (result.status === 'up_to_date') {
|
|
171
|
+
console.log(`${result.name} v${result.version} is already up to date.`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (result.status === 'not_found') {
|
|
175
|
+
return; // Warning already printed
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log(`Updated ${result.name}: v${result.oldVersion} -> v${result.newVersion}`);
|
|
179
|
+
if (result.replaced) {
|
|
180
|
+
console.log(' (replaced existing pending update)');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (result.changedFiles.length > 0) {
|
|
184
|
+
console.log('\nChanged files:');
|
|
185
|
+
for (const f of result.changedFiles) {
|
|
186
|
+
console.log(` ${f.padEnd(16)}updated`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check for deviations.md
|
|
191
|
+
const deviationsPath = path.join(depsDirPath, result.name, 'deviations.md');
|
|
192
|
+
if (fs.existsSync(deviationsPath)) {
|
|
193
|
+
console.log('\nPreserved:');
|
|
194
|
+
console.log(' deviations.md (consumer-owned, not modified)');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.log('\nStaged update:');
|
|
198
|
+
console.log(` ${depsDir}/.updates/${result.name}/changeset.md`);
|
|
199
|
+
console.log(` ${depsDir}/.updates/${result.name}/manifest.json`);
|
|
200
|
+
console.log(`\nRun "process the ${result.name} spec update" in your agent.`);
|
|
201
|
+
console.log(`After confirming, run: opensdd update apply ${result.name}`);
|
|
202
|
+
} else {
|
|
203
|
+
// All specs output
|
|
204
|
+
const updated = results.filter((r) => r.status === 'updated');
|
|
205
|
+
|
|
206
|
+
console.log(`Updated ${updated.length} of ${results.length} installed specs:\n`);
|
|
207
|
+
|
|
208
|
+
for (const r of results) {
|
|
209
|
+
const namePad = r.name.padEnd(16);
|
|
210
|
+
if (r.status === 'updated') {
|
|
211
|
+
console.log(` ${namePad}v${r.oldVersion} -> v${r.newVersion} staged`);
|
|
212
|
+
} else if (r.status === 'up_to_date') {
|
|
213
|
+
console.log(` ${namePad}v${r.version} already up to date`);
|
|
214
|
+
} else if (r.status === 'not_found') {
|
|
215
|
+
console.log(` ${namePad}not found in registry`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (updated.length > 0) {
|
|
220
|
+
console.log('\nRun "process spec updates" in your agent.');
|
|
221
|
+
console.log('After confirming each update, run:');
|
|
222
|
+
for (const r of updated) {
|
|
223
|
+
console.log(` opensdd update apply ${r.name}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import readline from 'node:readline';
|
|
4
|
+
import {
|
|
5
|
+
findManifestPath,
|
|
6
|
+
readManifest,
|
|
7
|
+
writeManifest,
|
|
8
|
+
getDepsDir,
|
|
9
|
+
} from '../lib/manifest.js';
|
|
10
|
+
|
|
11
|
+
function prompt(question) {
|
|
12
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
rl.question(question, (answer) => {
|
|
15
|
+
rl.close();
|
|
16
|
+
resolve(answer);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function updateApplyCommand(name) {
|
|
22
|
+
const manifestPath = findManifestPath(process.cwd());
|
|
23
|
+
if (!manifestPath) {
|
|
24
|
+
console.error('Error: OpenSDD not initialized. Run `opensdd init` to get started.');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const manifest = readManifest(manifestPath);
|
|
29
|
+
const projectRoot = path.dirname(manifestPath);
|
|
30
|
+
const depsDir = getDepsDir(manifest);
|
|
31
|
+
const updatesDir = path.join(projectRoot, depsDir, '.updates');
|
|
32
|
+
|
|
33
|
+
// Determine which updates to apply
|
|
34
|
+
let pendingNames;
|
|
35
|
+
if (name) {
|
|
36
|
+
const updateDir = path.join(updatesDir, name);
|
|
37
|
+
if (!fs.existsSync(updateDir)) {
|
|
38
|
+
console.error(`Error: No pending update for ${name}.`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
pendingNames = [name];
|
|
42
|
+
} else {
|
|
43
|
+
if (!fs.existsSync(updatesDir)) {
|
|
44
|
+
console.log('No pending updates.');
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
const entries = fs.readdirSync(updatesDir, { withFileTypes: true });
|
|
48
|
+
pendingNames = entries
|
|
49
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.'))
|
|
50
|
+
.map((e) => e.name);
|
|
51
|
+
if (pendingNames.length === 0) {
|
|
52
|
+
console.log('No pending updates.');
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Print warning
|
|
58
|
+
console.log('\u26A0 This will finalize the update in opensdd.json.');
|
|
59
|
+
console.log(' Only proceed if you have confirmed that all spec changes');
|
|
60
|
+
console.log(' have been implemented and tests pass.\n');
|
|
61
|
+
|
|
62
|
+
// Prompt for confirmation
|
|
63
|
+
let confirmMsg;
|
|
64
|
+
if (pendingNames.length === 1) {
|
|
65
|
+
const updateManifestPath = path.join(updatesDir, pendingNames[0], 'manifest.json');
|
|
66
|
+
const updateManifest = JSON.parse(fs.readFileSync(updateManifestPath, 'utf-8'));
|
|
67
|
+
confirmMsg = `Apply update for ${pendingNames[0]} v${updateManifest.previous_version} -> v${updateManifest.version}? (y/n) `;
|
|
68
|
+
} else {
|
|
69
|
+
confirmMsg = `Apply ${pendingNames.length} pending updates? (y/n) `;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const answer = await prompt(confirmMsg);
|
|
73
|
+
if (answer.toLowerCase() !== 'y') {
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log('');
|
|
78
|
+
|
|
79
|
+
// Apply each update
|
|
80
|
+
const applied = [];
|
|
81
|
+
for (const specName of pendingNames) {
|
|
82
|
+
const updateDir = path.join(updatesDir, specName);
|
|
83
|
+
const manifestJsonPath = path.join(updateDir, 'manifest.json');
|
|
84
|
+
|
|
85
|
+
if (!fs.existsSync(manifestJsonPath)) {
|
|
86
|
+
console.error(`Error: Missing manifest.json for ${specName} update.`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let updateManifest;
|
|
91
|
+
try {
|
|
92
|
+
updateManifest = JSON.parse(fs.readFileSync(manifestJsonPath, 'utf-8'));
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error(`Error: Malformed manifest.json for ${specName}: ${err.message}`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Update opensdd.json dependency entry, preserving consumer-managed fields
|
|
99
|
+
if (!manifest.dependencies) manifest.dependencies = {};
|
|
100
|
+
const existing = manifest.dependencies[specName] || {};
|
|
101
|
+
|
|
102
|
+
manifest.dependencies[specName] = {
|
|
103
|
+
...existing,
|
|
104
|
+
version: updateManifest.version,
|
|
105
|
+
source: updateManifest.source,
|
|
106
|
+
spec_format: updateManifest.spec_format,
|
|
107
|
+
// Preserve consumer-managed fields
|
|
108
|
+
implementation:
|
|
109
|
+
existing.implementation !== undefined ? existing.implementation : null,
|
|
110
|
+
tests: existing.tests !== undefined ? existing.tests : null,
|
|
111
|
+
has_deviations:
|
|
112
|
+
existing.has_deviations !== undefined ? existing.has_deviations : false,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Delete staging directory
|
|
116
|
+
fs.rmSync(updateDir, { recursive: true, force: true });
|
|
117
|
+
|
|
118
|
+
applied.push({
|
|
119
|
+
name: specName,
|
|
120
|
+
oldVersion: updateManifest.previous_version,
|
|
121
|
+
newVersion: updateManifest.version,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Write updated manifest
|
|
126
|
+
writeManifest(manifestPath, manifest);
|
|
127
|
+
|
|
128
|
+
// Clean up .updates/ directory if empty
|
|
129
|
+
if (fs.existsSync(updatesDir)) {
|
|
130
|
+
const remaining = fs.readdirSync(updatesDir);
|
|
131
|
+
if (remaining.length === 0) {
|
|
132
|
+
fs.rmSync(updatesDir, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Print output
|
|
137
|
+
if (applied.length === 1) {
|
|
138
|
+
const a = applied[0];
|
|
139
|
+
console.log(`Applied update for ${a.name}: v${a.oldVersion} -> v${a.newVersion}\n`);
|
|
140
|
+
console.log(' opensdd.json updated');
|
|
141
|
+
console.log(' staged files cleaned up');
|
|
142
|
+
} else {
|
|
143
|
+
console.log(`Applied ${applied.length} updates:\n`);
|
|
144
|
+
for (const a of applied) {
|
|
145
|
+
console.log(` ${a.name.padEnd(16)}v${a.oldVersion} -> v${a.newVersion} applied`);
|
|
146
|
+
}
|
|
147
|
+
console.log('\nopensdd.json updated.');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { validateSpec } from '../lib/validation.js';
|
|
4
|
+
|
|
5
|
+
export async function validateCommand(specPath) {
|
|
6
|
+
let targetDir;
|
|
7
|
+
|
|
8
|
+
if (specPath) {
|
|
9
|
+
targetDir = path.resolve(specPath);
|
|
10
|
+
} else {
|
|
11
|
+
targetDir = path.join(process.cwd(), 'opensdd');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(targetDir)) {
|
|
15
|
+
if (!specPath) {
|
|
16
|
+
console.error(
|
|
17
|
+
'No spec directory found. Provide a path or run from a directory containing `opensdd/`.'
|
|
18
|
+
);
|
|
19
|
+
} else {
|
|
20
|
+
console.error(`Error: Path does not exist: ${specPath}`);
|
|
21
|
+
}
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!fs.statSync(targetDir).isDirectory()) {
|
|
26
|
+
console.error(`Error: ${specPath} is not a directory.`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const result = validateSpec(targetDir);
|
|
31
|
+
|
|
32
|
+
// Determine name and version for display
|
|
33
|
+
let displayName = path.basename(targetDir);
|
|
34
|
+
let displayVersion = '';
|
|
35
|
+
|
|
36
|
+
if (result.manifest) {
|
|
37
|
+
displayName = result.manifest.name || displayName;
|
|
38
|
+
displayVersion = result.manifest.version ? ` v${result.manifest.version}` : '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (result.errors.length > 0) {
|
|
42
|
+
console.log(`Validation failed for ${displayName}\n`);
|
|
43
|
+
} else {
|
|
44
|
+
console.log(`Validated ${displayName}${displayVersion}\n`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// spec.md structure
|
|
48
|
+
if (result.specErrors.length > 0) {
|
|
49
|
+
console.log(' spec.md structure error');
|
|
50
|
+
for (const err of result.specErrors) {
|
|
51
|
+
console.log(` - ${err}`);
|
|
52
|
+
}
|
|
53
|
+
} else if (result.specWarnings.length > 0) {
|
|
54
|
+
console.log(
|
|
55
|
+
` spec.md structure ${result.specWarnings.length} warning${result.specWarnings.length > 1 ? 's' : ''}`
|
|
56
|
+
);
|
|
57
|
+
for (const w of result.specWarnings) {
|
|
58
|
+
console.log(` - ${w}`);
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
console.log(' spec.md structure ok');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// manifest.json
|
|
65
|
+
if (result.manifestExists) {
|
|
66
|
+
if (result.manifestErrors.length > 0) {
|
|
67
|
+
console.log(' manifest.json error');
|
|
68
|
+
for (const err of result.manifestErrors) {
|
|
69
|
+
console.log(` - ${err}`);
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
console.log(' manifest.json ok');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// deviations.md check
|
|
77
|
+
if (result.hasDeviations) {
|
|
78
|
+
console.log(' deviations.md found (should not be in publishable spec)');
|
|
79
|
+
} else {
|
|
80
|
+
console.log(' no deviations.md ok');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Summary
|
|
84
|
+
console.log('');
|
|
85
|
+
if (result.errors.length > 0) {
|
|
86
|
+
console.log(
|
|
87
|
+
`${result.errors.length} error${result.errors.length > 1 ? 's' : ''}. Fix errors before publishing.`
|
|
88
|
+
);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
} else if (result.warnings.length > 0) {
|
|
91
|
+
console.log('Valid with warnings. Review warnings before publishing.');
|
|
92
|
+
} else {
|
|
93
|
+
console.log('Valid. Ready for publishing to registry.');
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { initCommand } from './commands/init.js';
|
|
2
|
+
import { listCommand } from './commands/list.js';
|
|
3
|
+
import { installCommand } from './commands/install.js';
|
|
4
|
+
import { updateCommand } from './commands/update.js';
|
|
5
|
+
import { updateApplyCommand } from './commands/updateApply.js';
|
|
6
|
+
import { publishCommand } from './commands/publish.js';
|
|
7
|
+
import { statusCommand } from './commands/status.js';
|
|
8
|
+
import { validateCommand } from './commands/validate.js';
|
|
9
|
+
|
|
10
|
+
function parseArgs(argv) {
|
|
11
|
+
const flags = {};
|
|
12
|
+
const positional = [];
|
|
13
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14
|
+
if (argv[i].startsWith('--')) {
|
|
15
|
+
const key = argv[i].slice(2);
|
|
16
|
+
if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
|
|
17
|
+
flags[key] = argv[i + 1];
|
|
18
|
+
i++;
|
|
19
|
+
} else {
|
|
20
|
+
flags[key] = true;
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
positional.push(argv[i]);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { flags, positional };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function printHelp() {
|
|
30
|
+
console.log(`opensdd v0.1.0 - Open Spec-Driven Development CLI
|
|
31
|
+
|
|
32
|
+
Usage: opensdd <command> [options]
|
|
33
|
+
|
|
34
|
+
Commands:
|
|
35
|
+
init Initialize OpenSDD in the current project
|
|
36
|
+
list List specs available in the registry
|
|
37
|
+
install <name> [ver] Install a spec from the registry
|
|
38
|
+
update [name] Fetch latest version of dependency specs
|
|
39
|
+
update apply [name] Apply a staged update to opensdd.json
|
|
40
|
+
publish Publish an authored spec to the registry
|
|
41
|
+
status Show status of authored and installed specs
|
|
42
|
+
validate [path] Validate a spec directory
|
|
43
|
+
|
|
44
|
+
Options:
|
|
45
|
+
--registry <url> Alternative registry source
|
|
46
|
+
--branch <name> Branch name for publish PR
|
|
47
|
+
--version Show version
|
|
48
|
+
--help Show help`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function main() {
|
|
52
|
+
const rawArgs = process.argv.slice(2);
|
|
53
|
+
const command = rawArgs[0];
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
switch (command) {
|
|
57
|
+
case 'init':
|
|
58
|
+
await initCommand();
|
|
59
|
+
break;
|
|
60
|
+
|
|
61
|
+
case 'list': {
|
|
62
|
+
const { flags } = parseArgs(rawArgs.slice(1));
|
|
63
|
+
await listCommand({ registry: flags.registry });
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
case 'install': {
|
|
68
|
+
const { flags, positional } = parseArgs(rawArgs.slice(1));
|
|
69
|
+
if (!positional[0]) {
|
|
70
|
+
console.error('Usage: opensdd install <name> [version]');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
await installCommand(positional[0], positional[1], { registry: flags.registry });
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
case 'update': {
|
|
78
|
+
if (rawArgs[1] === 'apply') {
|
|
79
|
+
const { positional } = parseArgs(rawArgs.slice(2));
|
|
80
|
+
await updateApplyCommand(positional[0]);
|
|
81
|
+
} else {
|
|
82
|
+
const { flags, positional } = parseArgs(rawArgs.slice(1));
|
|
83
|
+
await updateCommand(positional[0], { registry: flags.registry });
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case 'publish': {
|
|
89
|
+
const { flags } = parseArgs(rawArgs.slice(1));
|
|
90
|
+
await publishCommand({ branch: flags.branch, registry: flags.registry });
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
case 'status':
|
|
95
|
+
await statusCommand();
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
case 'validate': {
|
|
99
|
+
const { positional } = parseArgs(rawArgs.slice(1));
|
|
100
|
+
await validateCommand(positional[0]);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case '--version':
|
|
105
|
+
case '-v':
|
|
106
|
+
console.log('0.1.0');
|
|
107
|
+
break;
|
|
108
|
+
|
|
109
|
+
case '--help':
|
|
110
|
+
case '-h':
|
|
111
|
+
case undefined:
|
|
112
|
+
printHelp();
|
|
113
|
+
break;
|
|
114
|
+
|
|
115
|
+
default:
|
|
116
|
+
console.error(`Unknown command: ${command}`);
|
|
117
|
+
printHelp();
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error(`Error: ${err.message}`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
main();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Search upward from startDir for opensdd.json, stopping at filesystem root.
|
|
6
|
+
* Returns the absolute path to opensdd.json, or null if not found.
|
|
7
|
+
*/
|
|
8
|
+
export function findManifestPath(startDir) {
|
|
9
|
+
let dir = path.resolve(startDir);
|
|
10
|
+
while (true) {
|
|
11
|
+
const candidate = path.join(dir, 'opensdd.json');
|
|
12
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
13
|
+
const parent = path.dirname(dir);
|
|
14
|
+
if (parent === dir) return null;
|
|
15
|
+
dir = parent;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readManifest(manifestPath) {
|
|
20
|
+
const content = fs.readFileSync(manifestPath, 'utf-8');
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(content);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
throw new Error(`opensdd.json is malformed JSON: ${err.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function writeManifest(manifestPath, data) {
|
|
29
|
+
fs.writeFileSync(manifestPath, JSON.stringify(data, null, 2) + '\n');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getSpecsDir(manifest) {
|
|
33
|
+
return manifest.specs_dir || 'opensdd';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getDepsDir(manifest) {
|
|
37
|
+
return manifest.deps_dir || '.opensdd.deps';
|
|
38
|
+
}
|