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.
@@ -0,0 +1,184 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import readline from 'node:readline';
4
+ import { installSkills } from '../lib/skills.js';
5
+
6
+ const PROJECT_MARKERS = [
7
+ 'package.json',
8
+ 'pyproject.toml',
9
+ 'Cargo.toml',
10
+ 'go.mod',
11
+ '.git',
12
+ 'opensdd.json',
13
+ 'pom.xml',
14
+ 'build.gradle',
15
+ 'build.gradle.kts',
16
+ 'Makefile',
17
+ 'CMakeLists.txt',
18
+ 'composer.json',
19
+ 'Gemfile',
20
+ 'setup.py',
21
+ 'setup.cfg',
22
+ 'mix.exs',
23
+ 'deno.json',
24
+ 'bun.lockb',
25
+ ];
26
+
27
+ function hasProjectMarker(dir) {
28
+ return PROJECT_MARKERS.some((marker) => fs.existsSync(path.join(dir, marker)));
29
+ }
30
+
31
+ function getProjectName(dir) {
32
+ // Try package.json
33
+ const pkgPath = path.join(dir, 'package.json');
34
+ if (fs.existsSync(pkgPath)) {
35
+ try {
36
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
37
+ if (pkg.name) return pkg.name;
38
+ } catch {
39
+ // ignore
40
+ }
41
+ }
42
+ // Try pyproject.toml
43
+ const pyprojectPath = path.join(dir, 'pyproject.toml');
44
+ if (fs.existsSync(pyprojectPath)) {
45
+ try {
46
+ const content = fs.readFileSync(pyprojectPath, 'utf-8');
47
+ const match = content.match(/name\s*=\s*"([^"]+)"/);
48
+ if (match) return match[1];
49
+ } catch {
50
+ // ignore
51
+ }
52
+ }
53
+ // Try Cargo.toml
54
+ const cargoPath = path.join(dir, 'Cargo.toml');
55
+ if (fs.existsSync(cargoPath)) {
56
+ try {
57
+ const content = fs.readFileSync(cargoPath, 'utf-8');
58
+ const match = content.match(/name\s*=\s*"([^"]+)"/);
59
+ if (match) return match[1];
60
+ } catch {
61
+ // ignore
62
+ }
63
+ }
64
+ // Default to directory name
65
+ return path.basename(dir);
66
+ }
67
+
68
+ function promptYN(question) {
69
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
70
+ return new Promise((resolve) => {
71
+ rl.question(question, (answer) => {
72
+ rl.close();
73
+ resolve(answer.toLowerCase() === 'y');
74
+ });
75
+ });
76
+ }
77
+
78
+ export async function initCommand() {
79
+ const cwd = process.cwd();
80
+
81
+ // Step 1: Check for project markers
82
+ if (!hasProjectMarker(cwd)) {
83
+ const proceed = await promptYN(
84
+ 'Warning: No project markers found in the current directory. Continue? (y/n) '
85
+ );
86
+ if (!proceed) {
87
+ process.exit(0);
88
+ }
89
+ }
90
+
91
+ // Step 2: Install skills to all agent formats
92
+ let warnings;
93
+ try {
94
+ warnings = installSkills(cwd);
95
+ } catch (err) {
96
+ console.error(`Error: Could not install skills: ${err.message}`);
97
+ process.exit(1);
98
+ }
99
+
100
+ for (const w of warnings) {
101
+ console.warn(`Warning: ${w}`);
102
+ }
103
+
104
+ // Step 3: Read or create opensdd.json
105
+ const manifestPath = path.join(cwd, 'opensdd.json');
106
+ let manifest = null;
107
+ let manifestCreated = false;
108
+
109
+ if (fs.existsSync(manifestPath)) {
110
+ try {
111
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
112
+ } catch (err) {
113
+ console.error(`Error: opensdd.json is malformed JSON: ${err.message}`);
114
+ process.exit(1);
115
+ }
116
+ }
117
+
118
+ if (!manifest) {
119
+ manifest = {
120
+ opensdd: '0.1.0',
121
+ specs_dir: 'opensdd',
122
+ deps_dir: '.opensdd.deps',
123
+ };
124
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
125
+ manifestCreated = true;
126
+ }
127
+
128
+ const specsDir = manifest.specs_dir || 'opensdd';
129
+ const depsDir = manifest.deps_dir || '.opensdd.deps';
130
+ const specsDirPath = path.join(cwd, specsDir);
131
+ const depsDirPath = path.join(cwd, depsDir);
132
+
133
+ // Step 4: Create specs directory
134
+ const specsDirCreated = !fs.existsSync(specsDirPath);
135
+ fs.mkdirSync(specsDirPath, { recursive: true });
136
+
137
+ // Step 5: Create skeleton spec.md
138
+ const specMdPath = path.join(specsDirPath, 'spec.md');
139
+ let specMdCreated = false;
140
+ if (!fs.existsSync(specMdPath)) {
141
+ const projectName = getProjectName(cwd);
142
+ const skeleton = `# ${projectName}
143
+
144
+ > TODO: One-line description of what this software does.
145
+
146
+ ## Behavioral Contract
147
+
148
+ <!-- Define behaviors here. -->
149
+
150
+ ## NOT Specified (Implementation Freedom)
151
+
152
+ <!-- List aspects left to the implementer's discretion. -->
153
+
154
+ ## Invariants
155
+
156
+ <!-- List properties that must hold true across all inputs and states. -->
157
+ `;
158
+ fs.writeFileSync(specMdPath, skeleton);
159
+ specMdCreated = true;
160
+ }
161
+
162
+ // Step 6: Create deps directory
163
+ const depsDirCreated = !fs.existsSync(depsDirPath);
164
+ fs.mkdirSync(depsDirPath, { recursive: true });
165
+
166
+ // Step 7: Print output
167
+ const isReInit = !manifestCreated;
168
+ const skillVerb = isReInit ? 'updated' : 'installed';
169
+
170
+ console.log('Initialized OpenSDD:');
171
+ console.log(
172
+ ' Skills installed for: Claude Code, Codex CLI, Cursor, GitHub Copilot, Gemini CLI, Amp'
173
+ );
174
+ console.log(` sdd-manager ${skillVerb} (6 agent formats)`);
175
+ console.log(` sdd-generate ${skillVerb} (6 agent formats)`);
176
+ console.log(
177
+ ` opensdd.json ${manifestCreated ? 'created' : 'already exists (preserved)'}`
178
+ );
179
+ console.log(` ${specsDir}/ ${specsDirCreated ? 'created' : 'already exists'}`);
180
+ console.log(
181
+ ` ${specsDir}/spec.md ${specMdCreated ? 'created (skeleton)' : 'already exists (preserved)'}`
182
+ );
183
+ console.log(` ${depsDir}/ ${depsDirCreated ? 'created' : 'already exists'}`);
184
+ }
@@ -0,0 +1,161 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {
4
+ findManifestPath,
5
+ readManifest,
6
+ writeManifest,
7
+ getDepsDir,
8
+ } from '../lib/manifest.js';
9
+ import {
10
+ resolveRegistry,
11
+ fetchSpecIndex,
12
+ fetchSpecManifest,
13
+ fetchSpecFiles,
14
+ listRegistrySpecs,
15
+ } from '../lib/registry.js';
16
+
17
+ function isValidSpecName(name) {
18
+ return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(name);
19
+ }
20
+
21
+ export async function installCommand(name, version, options) {
22
+ // Step 1: Verify opensdd.json exists
23
+ const manifestPath = findManifestPath(process.cwd());
24
+ if (!manifestPath) {
25
+ console.error('Error: OpenSDD not initialized. Run `opensdd init` to get started.');
26
+ process.exit(1);
27
+ }
28
+
29
+ const manifest = readManifest(manifestPath);
30
+ const projectRoot = path.dirname(manifestPath);
31
+ const depsDir = getDepsDir(manifest);
32
+ const depsDirPath = path.join(projectRoot, depsDir);
33
+ const registrySource = resolveRegistry(options, manifest);
34
+
35
+ // Step 2: Check if already installed
36
+ const deps = manifest.dependencies || {};
37
+ const specDirPath = path.join(depsDirPath, name);
38
+
39
+ let useVersion = version;
40
+
41
+ if (deps[name] && fs.existsSync(specDirPath)) {
42
+ console.error(`Error: ${name} is already installed (v${deps[name].version}).`);
43
+ console.error(`Use \`opensdd update ${name}\` to update to the latest version.`);
44
+ process.exit(1);
45
+ }
46
+
47
+ // Handle stale entry (entry exists but directory missing)
48
+ if (deps[name] && !fs.existsSync(specDirPath)) {
49
+ console.log(
50
+ `Note: Found stale entry for ${name} in opensdd.json (directory missing). Re-installing.`
51
+ );
52
+ if (!useVersion) {
53
+ useVersion = deps[name].version;
54
+ }
55
+ }
56
+
57
+ // Step 3: Validate spec name
58
+ if (!isValidSpecName(name)) {
59
+ console.error(
60
+ `Error: Invalid spec name "${name}". Allowed characters: lowercase alphanumeric and hyphens only.`
61
+ );
62
+ process.exit(1);
63
+ }
64
+
65
+ // Step 4: Fetch index.json
66
+ let index;
67
+ try {
68
+ index = await fetchSpecIndex(registrySource, name);
69
+ } catch (err) {
70
+ console.error(`Error: Could not reach registry at ${registrySource}`);
71
+ console.error(err.message);
72
+ process.exit(1);
73
+ }
74
+
75
+ if (!index) {
76
+ console.error(`Error: Spec "${name}" not found in registry.`);
77
+ try {
78
+ const available = await listRegistrySpecs(registrySource);
79
+ if (available.length > 0) {
80
+ console.error('\nAvailable specs:');
81
+ for (const s of available) {
82
+ console.error(` ${s.name}`);
83
+ }
84
+ }
85
+ } catch {
86
+ // ignore listing errors
87
+ }
88
+ process.exit(1);
89
+ }
90
+
91
+ // Resolve version
92
+ const resolvedVersion = useVersion || index.latest;
93
+
94
+ // Check version exists
95
+ if (!index.versions || !index.versions[resolvedVersion]) {
96
+ console.error(`Error: Version ${resolvedVersion} not found for ${name}.`);
97
+ const availableVersions = Object.keys(index.versions || {});
98
+ if (availableVersions.length > 0) {
99
+ console.error(`Available versions: ${availableVersions.join(', ')}`);
100
+ }
101
+ process.exit(1);
102
+ }
103
+
104
+ // Step 5: Fetch manifest for the version
105
+ const specManifest = await fetchSpecManifest(registrySource, name, resolvedVersion);
106
+ if (!specManifest) {
107
+ console.error(`Error: Could not fetch manifest for ${name}@${resolvedVersion}`);
108
+ process.exit(1);
109
+ }
110
+
111
+ // Step 6: Fetch all spec files and write to deps dir
112
+ const files = await fetchSpecFiles(registrySource, name, resolvedVersion);
113
+ if (!files) {
114
+ console.error(`Error: Could not fetch spec files for ${name}@${resolvedVersion}`);
115
+ process.exit(1);
116
+ }
117
+
118
+ fs.mkdirSync(specDirPath, { recursive: true });
119
+ for (const [fileName, content] of Object.entries(files)) {
120
+ // Invariant: opensdd install MUST NOT create a deviations.md file
121
+ if (fileName === 'deviations.md') continue;
122
+ fs.writeFileSync(path.join(specDirPath, fileName), content);
123
+ }
124
+
125
+ // Step 7: Add entry to opensdd.json
126
+ if (!manifest.dependencies) {
127
+ manifest.dependencies = {};
128
+ }
129
+
130
+ // Preserve consumer-managed fields from stale entry if re-installing
131
+ const existingEntry = deps[name];
132
+ manifest.dependencies[name] = {
133
+ version: resolvedVersion,
134
+ source: registrySource,
135
+ spec_format: specManifest.spec_format || '0.1.0',
136
+ implementation: existingEntry?.implementation ?? null,
137
+ tests: existingEntry?.tests ?? null,
138
+ has_deviations: existingEntry?.has_deviations ?? false,
139
+ };
140
+
141
+ writeManifest(manifestPath, manifest);
142
+
143
+ // Step 8: Check for missing dependencies
144
+ if (specManifest.dependencies && specManifest.dependencies.length > 0) {
145
+ const missing = specManifest.dependencies.filter(
146
+ (dep) => !manifest.dependencies[dep]
147
+ );
148
+ if (missing.length > 0) {
149
+ console.log('\nWarning: This spec has uninstalled dependencies:');
150
+ for (const dep of missing) {
151
+ console.log(` Run \`opensdd install ${dep}\` to install ${dep}.`);
152
+ }
153
+ }
154
+ }
155
+
156
+ // Step 9: Print success
157
+ console.log(`Installed ${name} v${resolvedVersion} to ${depsDir}/${name}/`);
158
+ console.log(
159
+ `\nRun "implement the ${name} spec" in your agent to generate an implementation.`
160
+ );
161
+ }
@@ -0,0 +1,40 @@
1
+ import { findManifestPath, readManifest } from '../lib/manifest.js';
2
+ import { resolveRegistry, listRegistrySpecs } from '../lib/registry.js';
3
+
4
+ export async function listCommand(options) {
5
+ // list doesn't require opensdd.json, but uses it for registry resolution if available
6
+ let manifest = null;
7
+ const manifestPath = findManifestPath(process.cwd());
8
+ if (manifestPath) {
9
+ try {
10
+ manifest = readManifest(manifestPath);
11
+ } catch {
12
+ // ignore — list works without opensdd.json
13
+ }
14
+ }
15
+
16
+ const registrySource = resolveRegistry(options, manifest);
17
+
18
+ let specs;
19
+ try {
20
+ specs = await listRegistrySpecs(registrySource);
21
+ } catch (err) {
22
+ console.error(`Error: Could not reach registry at ${registrySource}`);
23
+ console.error(err.message);
24
+ process.exit(1);
25
+ }
26
+
27
+ if (specs.length === 0) {
28
+ console.log('No specs available in the registry.');
29
+ return;
30
+ }
31
+
32
+ console.log('Available specs:\n');
33
+
34
+ for (const spec of specs) {
35
+ const name = (spec.name || 'unknown').padEnd(17);
36
+ const version = `v${spec.latest || '0.0.0'}`.padEnd(8);
37
+ const desc = spec.description || '';
38
+ console.log(` ${name}${version}${desc}`);
39
+ }
40
+ }
@@ -0,0 +1,235 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { execSync } from 'node:child_process';
5
+ import readline from 'node:readline';
6
+ import {
7
+ findManifestPath,
8
+ readManifest,
9
+ getSpecsDir,
10
+ } from '../lib/manifest.js';
11
+ import {
12
+ resolveRegistry,
13
+ isGitHubUrl,
14
+ parseGitHubUrl,
15
+ fetchSpecIndex,
16
+ } from '../lib/registry.js';
17
+ import { validateSpec } from '../lib/validation.js';
18
+
19
+ function prompt(question) {
20
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
21
+ return new Promise((resolve) => {
22
+ rl.question(question, (answer) => {
23
+ rl.close();
24
+ resolve(answer);
25
+ });
26
+ });
27
+ }
28
+
29
+ function commandExists(cmd) {
30
+ try {
31
+ execSync(`which ${cmd}`, { stdio: 'ignore' });
32
+ return true;
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ function copyDirRecursive(src, dest) {
39
+ fs.mkdirSync(dest, { recursive: true });
40
+ const entries = fs.readdirSync(src, { withFileTypes: true });
41
+ for (const entry of entries) {
42
+ const srcPath = path.join(src, entry.name);
43
+ const destPath = path.join(dest, entry.name);
44
+ if (entry.isDirectory()) {
45
+ copyDirRecursive(srcPath, destPath);
46
+ } else {
47
+ fs.copyFileSync(srcPath, destPath);
48
+ }
49
+ }
50
+ }
51
+
52
+ export async function publishCommand(options) {
53
+ // Step 1: Verify opensdd.json
54
+ const manifestPath = findManifestPath(process.cwd());
55
+ if (!manifestPath) {
56
+ console.error('Error: OpenSDD not initialized. Run `opensdd init` to get started.');
57
+ process.exit(1);
58
+ }
59
+
60
+ const manifest = readManifest(manifestPath);
61
+ const projectRoot = path.dirname(manifestPath);
62
+ const registrySource = resolveRegistry(options, manifest);
63
+
64
+ // Step 2: Verify publish section
65
+ if (!manifest.publish) {
66
+ console.error('Error: No `publish` section in opensdd.json.');
67
+ console.error(
68
+ 'Add a `publish` object with name, version, description, and spec_format to publish your spec.'
69
+ );
70
+ process.exit(1);
71
+ }
72
+
73
+ const { name, version, description, spec_format, dependencies } = manifest.publish;
74
+
75
+ if (!name || !version || !description || !spec_format) {
76
+ console.error(
77
+ 'Error: `publish` section must include name, version, description, and spec_format.'
78
+ );
79
+ process.exit(1);
80
+ }
81
+
82
+ // Step 4: Verify spec.md exists
83
+ const specsDir = getSpecsDir(manifest);
84
+ const specsDirPath = path.join(projectRoot, specsDir);
85
+
86
+ if (!fs.existsSync(path.join(specsDirPath, 'spec.md'))) {
87
+ console.error(`Error: ${specsDir}/spec.md not found. Create your spec before publishing.`);
88
+ process.exit(1);
89
+ }
90
+
91
+ // Step 5: Validate spec
92
+ console.log(`Publishing ${name} v${version} to registry...\n`);
93
+
94
+ const validation = validateSpec(specsDirPath);
95
+ if (validation.errors.length > 0) {
96
+ console.error(' Validated spec FAILED\n');
97
+ for (const err of validation.errors) {
98
+ console.error(` - ${err}`);
99
+ }
100
+ process.exit(1);
101
+ }
102
+ console.log(' Validated spec ok');
103
+
104
+ // Step 6: Verify registry is GitHub
105
+ if (!isGitHubUrl(registrySource)) {
106
+ console.error('Error: Publishing requires a GitHub registry URL.');
107
+ console.error(`Current registry: ${registrySource}`);
108
+ process.exit(1);
109
+ }
110
+
111
+ // Check git and gh CLI
112
+ if (!commandExists('git')) {
113
+ console.error('Error: git is not installed. Install git to publish specs.');
114
+ process.exit(1);
115
+ }
116
+ if (!commandExists('gh')) {
117
+ console.error('Error: GitHub CLI (gh) is not installed.');
118
+ console.error(
119
+ 'Install it from https://cli.github.com/ and authenticate with `gh auth login`.'
120
+ );
121
+ process.exit(1);
122
+ }
123
+
124
+ // Step 7: Check if version already exists
125
+ try {
126
+ const index = await fetchSpecIndex(registrySource, name);
127
+ if (index && index.versions && index.versions[version]) {
128
+ console.error(
129
+ `\nError: Version ${version} already exists in the registry for ${name}.`
130
+ );
131
+ console.error('Bump the version in opensdd.json before publishing.');
132
+ process.exit(1);
133
+ }
134
+ } catch {
135
+ // Spec doesn't exist in registry yet — that's fine
136
+ }
137
+
138
+ // Step 9: Determine branch name
139
+ let branchName = options.branch;
140
+ if (!branchName) {
141
+ const answer = await prompt(
142
+ `Enter branch name for the registry PR (default: opensdd/${name}-v${version}): `
143
+ );
144
+ branchName = answer.trim() || `opensdd/${name}-v${version}`;
145
+ }
146
+
147
+ const { owner, repo } = parseGitHubUrl(registrySource);
148
+ const repoUrl = `https://github.com/${owner}/${repo}.git`;
149
+
150
+ // Step 10: Clone, create branch, add files, push, create PR
151
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opensdd-publish-'));
152
+
153
+ try {
154
+ // Clone
155
+ execSync(`git clone --depth 1 "${repoUrl}" "${tmpDir}"`, { stdio: 'pipe' });
156
+
157
+ // Create branch
158
+ execSync(`git checkout -b "${branchName}"`, { cwd: tmpDir, stdio: 'pipe' });
159
+ console.log(` Created branch ${branchName}`);
160
+
161
+ // Create registry entry directory
162
+ const registryDir = path.join(tmpDir, 'registry', name, version);
163
+ fs.mkdirSync(registryDir, { recursive: true });
164
+
165
+ // Copy all spec files first
166
+ copyDirRecursive(specsDirPath, registryDir);
167
+
168
+ // Build manifest.json from publish fields (written after copy so it takes precedence
169
+ // over any manifest.json that might exist in the specs directory)
170
+ const publishManifest = {
171
+ name,
172
+ version,
173
+ spec_format,
174
+ description,
175
+ dependencies: dependencies || [],
176
+ };
177
+ fs.writeFileSync(
178
+ path.join(registryDir, 'manifest.json'),
179
+ JSON.stringify(publishManifest, null, 2) + '\n'
180
+ );
181
+
182
+ console.log(` Created registry entry registry/${name}/${version}/`);
183
+
184
+ // Update (or create) index.json
185
+ const indexDir = path.join(tmpDir, 'registry', name);
186
+ fs.mkdirSync(indexDir, { recursive: true });
187
+ const indexPath = path.join(indexDir, 'index.json');
188
+
189
+ let indexData;
190
+ if (fs.existsSync(indexPath)) {
191
+ indexData = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
192
+ } else {
193
+ indexData = { name, description, latest: version, versions: {} };
194
+ }
195
+ indexData.latest = version;
196
+ indexData.versions[version] = { spec_format };
197
+ fs.writeFileSync(indexPath, JSON.stringify(indexData, null, 2) + '\n');
198
+ console.log(` Updated index.json latest: ${version}`);
199
+
200
+ // Commit and push
201
+ execSync('git add -A', { cwd: tmpDir, stdio: 'pipe' });
202
+ execSync(`git commit -m "Add ${name} v${version}"`, { cwd: tmpDir, stdio: 'pipe' });
203
+ execSync(`git push -u origin "${branchName}"`, { cwd: tmpDir, stdio: 'pipe' });
204
+
205
+ // Create PR
206
+ const prTitle = `Add ${name} v${version}`;
207
+ const prBody = `Adds ${name} v${version} to the OpenSDD registry.\n\n${description}`;
208
+ const prOutput = execSync(
209
+ `gh pr create --repo "${owner}/${repo}" --title "${prTitle}" --body "${prBody}" --head "${branchName}"`,
210
+ { cwd: tmpDir, encoding: 'utf-8' }
211
+ ).trim();
212
+
213
+ console.log(` Opened pull request ${prOutput}`);
214
+ } catch (err) {
215
+ if (err.message.includes('Authentication') || err.message.includes('auth')) {
216
+ console.error('\nError: Git authentication failed.');
217
+ console.error('Run `gh auth login` to authenticate with GitHub.');
218
+ } else {
219
+ console.error(`\nError during publish: ${err.message}`);
220
+ if (err.stderr) {
221
+ console.error(err.stderr.toString());
222
+ }
223
+ }
224
+ process.exit(1);
225
+ } finally {
226
+ // Clean up temp directory
227
+ try {
228
+ fs.rmSync(tmpDir, { recursive: true, force: true });
229
+ } catch {
230
+ // ignore cleanup errors
231
+ }
232
+ }
233
+
234
+ console.log('\nPublished. Spec will be available after PR is merged.');
235
+ }
@@ -0,0 +1,91 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {
4
+ findManifestPath,
5
+ readManifest,
6
+ getSpecsDir,
7
+ getDepsDir,
8
+ } from '../lib/manifest.js';
9
+
10
+ export async function statusCommand() {
11
+ const manifestPath = findManifestPath(process.cwd());
12
+ if (!manifestPath) {
13
+ console.log('OpenSDD not initialized. Run `opensdd init` to get started.');
14
+ return;
15
+ }
16
+
17
+ const manifest = readManifest(manifestPath);
18
+ const projectRoot = path.dirname(manifestPath);
19
+ const specsDir = getSpecsDir(manifest);
20
+ const depsDir = getDepsDir(manifest);
21
+
22
+ let hasContent = false;
23
+
24
+ // Authored spec
25
+ if (manifest.publish) {
26
+ hasContent = true;
27
+ const pub = manifest.publish;
28
+ console.log('Authored spec:\n');
29
+ console.log(` ${pub.name} v${pub.version} ${specsDir}/`);
30
+ console.log('');
31
+ }
32
+
33
+ // Dependencies
34
+ const deps = manifest.dependencies || {};
35
+ const depKeys = Object.keys(deps);
36
+
37
+ if (depKeys.length > 0) {
38
+ hasContent = true;
39
+ console.log('Installed dependencies:\n');
40
+
41
+ for (const depName of depKeys) {
42
+ const entry = deps[depName];
43
+ const namePad = depName.padEnd(16);
44
+ const version = `v${entry.version}`.padEnd(8);
45
+
46
+ let status;
47
+ if (entry.implementation) {
48
+ status = `implemented ${entry.implementation}`;
49
+ } else {
50
+ status = 'not implemented';
51
+ }
52
+
53
+ // Check for deviations
54
+ let deviationInfo = '';
55
+ const deviationsPath = path.join(projectRoot, depsDir, depName, 'deviations.md');
56
+ if (entry.has_deviations || fs.existsSync(deviationsPath)) {
57
+ if (fs.existsSync(deviationsPath)) {
58
+ const content = fs.readFileSync(deviationsPath, 'utf-8');
59
+ const deviationCount = (content.match(/^## /gm) || []).length;
60
+ if (deviationCount > 0) {
61
+ deviationInfo = ` ${deviationCount} deviation${deviationCount > 1 ? 's' : ''}`;
62
+ }
63
+ }
64
+ }
65
+
66
+ console.log(` ${namePad}${version}${status}${deviationInfo}`);
67
+ }
68
+ }
69
+
70
+ // Check for untracked spec directories
71
+ const depsDirPath = path.join(projectRoot, depsDir);
72
+ if (fs.existsSync(depsDirPath)) {
73
+ const dirEntries = fs.readdirSync(depsDirPath, { withFileTypes: true });
74
+ const untracked = dirEntries
75
+ .filter((e) => e.isDirectory() && !e.name.startsWith('.') && !deps[e.name])
76
+ .map((e) => e.name);
77
+
78
+ if (untracked.length > 0) {
79
+ console.log('\nWarning: Untracked spec directories:');
80
+ for (const u of untracked) {
81
+ console.log(` ${depsDir}/${u}/`);
82
+ }
83
+ }
84
+ }
85
+
86
+ if (!hasContent) {
87
+ console.log(
88
+ 'No specs found. Run `opensdd install <name>` to install a dependency or add a `publish` entry to opensdd.json.'
89
+ );
90
+ }
91
+ }