@mod-computer/cli 0.2.3 → 0.2.5

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.
Files changed (75) hide show
  1. package/dist/cli.bundle.js +216 -36371
  2. package/package.json +3 -3
  3. package/dist/app.js +0 -227
  4. package/dist/cli.bundle.js.map +0 -7
  5. package/dist/cli.js +0 -132
  6. package/dist/commands/add.js +0 -245
  7. package/dist/commands/agents-run.js +0 -71
  8. package/dist/commands/auth.js +0 -259
  9. package/dist/commands/branch.js +0 -1411
  10. package/dist/commands/claude-sync.js +0 -772
  11. package/dist/commands/comment.js +0 -568
  12. package/dist/commands/diff.js +0 -182
  13. package/dist/commands/index.js +0 -73
  14. package/dist/commands/init.js +0 -597
  15. package/dist/commands/ls.js +0 -135
  16. package/dist/commands/members.js +0 -687
  17. package/dist/commands/mv.js +0 -282
  18. package/dist/commands/recover.js +0 -207
  19. package/dist/commands/rm.js +0 -257
  20. package/dist/commands/spec.js +0 -386
  21. package/dist/commands/status.js +0 -296
  22. package/dist/commands/sync.js +0 -119
  23. package/dist/commands/trace.js +0 -1752
  24. package/dist/commands/workspace.js +0 -447
  25. package/dist/components/conflict-resolution-ui.js +0 -120
  26. package/dist/components/messages.js +0 -5
  27. package/dist/components/thread.js +0 -8
  28. package/dist/config/features.js +0 -83
  29. package/dist/containers/branches-container.js +0 -140
  30. package/dist/containers/directory-container.js +0 -92
  31. package/dist/containers/thread-container.js +0 -214
  32. package/dist/containers/threads-container.js +0 -27
  33. package/dist/containers/workspaces-container.js +0 -27
  34. package/dist/daemon/conflict-resolution.js +0 -172
  35. package/dist/daemon/content-hash.js +0 -31
  36. package/dist/daemon/file-sync.js +0 -985
  37. package/dist/daemon/index.js +0 -203
  38. package/dist/daemon/mime-types.js +0 -166
  39. package/dist/daemon/offline-queue.js +0 -211
  40. package/dist/daemon/path-utils.js +0 -64
  41. package/dist/daemon/share-policy.js +0 -83
  42. package/dist/daemon/wasm-errors.js +0 -189
  43. package/dist/daemon/worker.js +0 -557
  44. package/dist/daemon-worker.js +0 -258
  45. package/dist/errors/workspace-errors.js +0 -48
  46. package/dist/lib/auth-server.js +0 -216
  47. package/dist/lib/browser.js +0 -35
  48. package/dist/lib/diff.js +0 -284
  49. package/dist/lib/formatters.js +0 -204
  50. package/dist/lib/git.js +0 -137
  51. package/dist/lib/local-fs.js +0 -201
  52. package/dist/lib/prompts.js +0 -56
  53. package/dist/lib/storage.js +0 -213
  54. package/dist/lib/trace-formatters.js +0 -314
  55. package/dist/services/add-service.js +0 -554
  56. package/dist/services/add-validation.js +0 -124
  57. package/dist/services/automatic-file-tracker.js +0 -303
  58. package/dist/services/cli-orchestrator.js +0 -227
  59. package/dist/services/feature-flags.js +0 -187
  60. package/dist/services/file-import-service.js +0 -283
  61. package/dist/services/file-transformation-service.js +0 -218
  62. package/dist/services/logger.js +0 -44
  63. package/dist/services/mod-config.js +0 -67
  64. package/dist/services/modignore-service.js +0 -328
  65. package/dist/services/sync-daemon.js +0 -244
  66. package/dist/services/thread-notification-service.js +0 -50
  67. package/dist/services/thread-service.js +0 -147
  68. package/dist/stores/use-directory-store.js +0 -96
  69. package/dist/stores/use-threads-store.js +0 -46
  70. package/dist/stores/use-workspaces-store.js +0 -54
  71. package/dist/types/add-types.js +0 -99
  72. package/dist/types/config.js +0 -16
  73. package/dist/types/index.js +0 -2
  74. package/dist/types/workspace-connection.js +0 -53
  75. package/dist/types.js +0 -1
@@ -1,257 +0,0 @@
1
- // glassware[type="implementation", id="impl-cli-rm-cmd--81378802", requirements="requirement-cli-rm-cmd--40931f9e,requirement-cli-rm-requires-workspace--1a926c90,requirement-cli-rm-confirm--027ace3f,requirement-cli-rm-force--5bd56511,requirement-cli-rm-workspace-only--405d41f5,requirement-cli-rm-local--a58b09c9,requirement-cli-rm-recursive--91e50991,requirement-cli-rm-dry-run--c3af989d"]
2
- // spec: packages/mod-cli/specs/file-directory.md
3
- import { createModWorkspace } from '@mod/mod-core';
4
- import { readWorkspaceConnection } from '../lib/storage.js';
5
- import { select } from '../lib/prompts.js';
6
- import { matchGlob, deleteLocalFile, deleteLocalDirectory } from '../lib/local-fs.js';
7
- import path from 'path';
8
- // glassware[type="implementation", id="impl-cli-rm-requires-workspace--8a7b05f5", requirements="requirement-cli-rm-requires-workspace--1a926c90,requirement-cli-fd-error-no-workspace--1919b4fb"]
9
- function requireWorkspaceConnection() {
10
- const currentDir = process.cwd();
11
- const connection = readWorkspaceConnection(currentDir);
12
- if (!connection) {
13
- console.error('Error: Not connected to a workspace.');
14
- console.error('Run `mod init` to connect this directory first.');
15
- process.exit(1);
16
- }
17
- return {
18
- workspaceId: connection.workspaceId,
19
- workspaceName: connection.workspaceName,
20
- workspacePath: connection.path,
21
- };
22
- }
23
- function parseArgs(args) {
24
- const options = {
25
- recursive: false,
26
- force: false,
27
- local: false,
28
- dryRun: false,
29
- quiet: false,
30
- };
31
- const paths = [];
32
- for (let i = 0; i < args.length; i++) {
33
- const arg = args[i];
34
- if (arg === '--recursive' || arg === '-r') {
35
- options.recursive = true;
36
- }
37
- else if (arg === '--force' || arg === '-f') {
38
- options.force = true;
39
- }
40
- else if (arg === '--local') {
41
- options.local = true;
42
- }
43
- else if (arg === '--dry-run') {
44
- options.dryRun = true;
45
- }
46
- else if (arg === '--quiet' || arg === '-q') {
47
- options.quiet = true;
48
- }
49
- else if (!arg.startsWith('-')) {
50
- paths.push(arg);
51
- }
52
- }
53
- return { paths, options };
54
- }
55
- // glassware[type="implementation", id="impl-cli-fd-error-not-found--00ef7ef6", requirements="requirement-cli-fd-error-not-found--9c364146"]
56
- function showNotFoundError(filePath) {
57
- console.error(`Error: File not found in workspace: ${filePath}`);
58
- console.error("Run `mod ls` to see workspace files.");
59
- }
60
- export async function rmCommand(args, repo) {
61
- const { workspaceId, workspacePath } = requireWorkspaceConnection();
62
- const { paths, options } = parseArgs(args);
63
- if (paths.length === 0) {
64
- console.error('Usage: mod rm <path...>');
65
- console.error('');
66
- console.error('Options:');
67
- console.error(' --recursive, -r Remove directories recursively');
68
- console.error(' --force, -f Skip confirmation prompt');
69
- console.error(' --local Also delete from local filesystem');
70
- console.error(' --dry-run Show what would be removed');
71
- console.error(' --quiet, -q Only show errors');
72
- process.exit(1);
73
- }
74
- try {
75
- const modWorkspace = createModWorkspace(repo);
76
- const workspaceHandle = await modWorkspace.openWorkspace(workspaceId);
77
- // Get all files and folders
78
- const fileRefs = await workspaceHandle.file.list();
79
- const folderRefs = await workspaceHandle.folder.list();
80
- // Build list of files to remove
81
- const filesToRemove = [];
82
- const foldersToRemove = [];
83
- for (const targetPath of paths) {
84
- const isGlob = targetPath.includes('*');
85
- const isFolder = targetPath.endsWith('/');
86
- const normalizedPath = targetPath.replace(/\/$/, '');
87
- if (isGlob) {
88
- // Glob pattern matching
89
- for (const file of fileRefs) {
90
- const filePath = file.metadata?.path || file.name;
91
- if (matchGlob(filePath, targetPath)) {
92
- filesToRemove.push({ id: file.id, path: filePath, isFolder: false });
93
- }
94
- }
95
- }
96
- else {
97
- // Exact path or folder
98
- const matchingFile = fileRefs.find(f => {
99
- const filePath = f.metadata?.path || f.name;
100
- return filePath === normalizedPath;
101
- });
102
- if (matchingFile) {
103
- filesToRemove.push({
104
- id: matchingFile.id,
105
- path: matchingFile.metadata?.path || matchingFile.name,
106
- isFolder: false,
107
- });
108
- }
109
- else {
110
- // Check if it's a folder
111
- const matchingFolder = folderRefs.find(f => f.path === normalizedPath);
112
- if (matchingFolder) {
113
- if (!options.recursive) {
114
- console.error(`Error: ${normalizedPath}/ is a folder. Use --recursive to remove.`);
115
- process.exit(1);
116
- }
117
- foldersToRemove.push({
118
- id: matchingFolder.id,
119
- path: matchingFolder.path,
120
- isFolder: true,
121
- });
122
- // Add all files in folder
123
- for (const file of fileRefs) {
124
- const filePath = file.metadata?.path || file.name;
125
- if (filePath.startsWith(normalizedPath + '/')) {
126
- filesToRemove.push({ id: file.id, path: filePath, isFolder: false });
127
- }
128
- }
129
- }
130
- else {
131
- // Check if path matches folder prefix
132
- const filesInPath = fileRefs.filter(f => {
133
- const filePath = f.metadata?.path || f.name;
134
- return filePath.startsWith(normalizedPath + '/');
135
- });
136
- if (filesInPath.length > 0) {
137
- if (!options.recursive) {
138
- console.error(`Error: ${normalizedPath}/ is a folder. Use --recursive to remove.`);
139
- process.exit(1);
140
- }
141
- for (const file of filesInPath) {
142
- filesToRemove.push({
143
- id: file.id,
144
- path: file.metadata?.path || file.name,
145
- isFolder: false,
146
- });
147
- }
148
- }
149
- else {
150
- showNotFoundError(targetPath);
151
- process.exit(1);
152
- }
153
- }
154
- }
155
- }
156
- }
157
- // Deduplicate
158
- const uniqueFiles = [...new Map(filesToRemove.map(f => [f.id, f])).values()];
159
- if (uniqueFiles.length === 0) {
160
- console.log('No files matched the specified pattern(s).');
161
- return;
162
- }
163
- // Dry run - show what would be removed (impl-cli-rm-dry-run)
164
- if (options.dryRun) {
165
- console.log(`Would remove ${uniqueFiles.length} file${uniqueFiles.length === 1 ? '' : 's'}:`);
166
- for (const file of uniqueFiles.slice(0, 20)) {
167
- console.log(` ${file.path}`);
168
- }
169
- if (uniqueFiles.length > 20) {
170
- console.log(` ... (${uniqueFiles.length - 20} more)`);
171
- }
172
- return;
173
- }
174
- // Confirm/force check (impl-cli-rm-confirm-force)
175
- if (uniqueFiles.length > 1 && !options.force) {
176
- const choice = await select(`This will remove ${uniqueFiles.length} files from the workspace. Continue?`, [
177
- { label: 'Yes', value: 'yes' },
178
- { label: 'No', value: 'no' },
179
- ]);
180
- if (choice === 'no') {
181
- console.log('Cancelled.');
182
- return;
183
- }
184
- }
185
- // Remove files
186
- let removed = 0;
187
- let localDeleted = 0;
188
- const errors = [];
189
- for (const file of uniqueFiles) {
190
- try {
191
- await workspaceHandle.file.delete(file.id);
192
- removed++;
193
- if (!options.quiet) {
194
- console.log(`Removed from workspace: ${file.path}`);
195
- }
196
- // Local deletion (impl-cli-rm-local)
197
- if (options.local) {
198
- const localPath = path.join(workspacePath, file.path);
199
- const result = await deleteLocalFile(localPath);
200
- if (result.success) {
201
- localDeleted++;
202
- if (!options.quiet) {
203
- console.log(`Deleted local file: ${file.path}`);
204
- }
205
- }
206
- else if (result.error && !options.quiet) {
207
- console.warn(`Warning: ${result.error}`);
208
- }
209
- }
210
- }
211
- catch (error) {
212
- errors.push(`Failed to remove ${file.path}: ${error.message}`);
213
- }
214
- }
215
- // Remove empty folders if recursive
216
- if (options.recursive && foldersToRemove.length > 0) {
217
- for (const folder of foldersToRemove) {
218
- try {
219
- await workspaceHandle.folder.delete(folder.id);
220
- if (!options.quiet) {
221
- console.log(`Removed folder: ${folder.path}/`);
222
- }
223
- if (options.local) {
224
- const localPath = path.join(workspacePath, folder.path);
225
- await deleteLocalDirectory(localPath);
226
- }
227
- }
228
- catch {
229
- // Folder might have other contents, ignore
230
- }
231
- }
232
- }
233
- // Summary
234
- if (!options.quiet) {
235
- console.log('');
236
- console.log(`Removed ${removed} file${removed === 1 ? '' : 's'} from workspace.`);
237
- // Workspace-only reminder (impl-cli-rm-workspace-only)
238
- if (!options.local && removed > 0) {
239
- console.log(`Note: Local files unchanged. Use --local to also delete locally.`);
240
- }
241
- else if (options.local && localDeleted > 0) {
242
- console.log(`Deleted ${localDeleted} local file${localDeleted === 1 ? '' : 's'}.`);
243
- }
244
- }
245
- if (errors.length > 0) {
246
- console.error('');
247
- for (const error of errors) {
248
- console.error(error);
249
- }
250
- process.exit(1);
251
- }
252
- }
253
- catch (error) {
254
- console.error('Error removing files:', error.message);
255
- process.exit(1);
256
- }
257
- }
@@ -1,386 +0,0 @@
1
- import { SpecificationParserService, RequirementCategory } from '@mod/mod-core';
2
- import * as fs from 'fs/promises';
3
- import * as path from 'path';
4
- import { log } from '../services/logger.js';
5
- import { readModConfig } from '../services/mod-config.js';
6
- import { createModWorkspace } from '@mod/mod-core';
7
- export async function specCommand(args, repo) {
8
- const subcommand = args[0] || 'status';
9
- const remainingArgs = args.slice(1);
10
- switch (subcommand) {
11
- case 'status':
12
- await specStatusCommand(remainingArgs, repo);
13
- break;
14
- case 'list':
15
- await specListCommand(remainingArgs);
16
- break;
17
- default:
18
- // If no valid subcommand, assume it's a spec file for status
19
- await specStatusCommand([subcommand, ...remainingArgs], repo);
20
- }
21
- }
22
- async function specStatusCommand(args, repo) {
23
- try {
24
- let specName = args[0];
25
- if (!specName && repo) {
26
- const activeBranchSpec = await getActiveBranchSpec(repo);
27
- if (activeBranchSpec) {
28
- specName = activeBranchSpec;
29
- console.log(`Using active branch specification: ${specName}`);
30
- }
31
- }
32
- if (!specName) {
33
- console.error('Error: Specification file required');
34
- console.error('Usage: mod spec status [spec-file]');
35
- console.error(' mod spec status (uses active branch spec if available)');
36
- process.exit(1);
37
- }
38
- if (!specName.endsWith('.spec.md')) {
39
- specName += '.spec.md';
40
- }
41
- const workspaceRoot = process.cwd();
42
- const analysis = await analyzeSpecification(workspaceRoot, specName);
43
- printTscStyleOutput(analysis);
44
- process.exit(0);
45
- }
46
- catch (error) {
47
- handleError(error, args[0]);
48
- }
49
- }
50
- async function specListCommand(args) {
51
- try {
52
- const workspaceRoot = process.cwd();
53
- const specsDir = path.join(workspaceRoot, '.mod/specs');
54
- const specs = await findAllSpecifications(specsDir);
55
- if (specs.length === 0) {
56
- console.log('No specification files found in .mod/specs');
57
- return;
58
- }
59
- console.log('Available specifications:\n');
60
- // Group by directory
61
- const grouped = new Map();
62
- for (const spec of specs) {
63
- const dir = path.dirname(spec);
64
- if (!grouped.has(dir)) {
65
- grouped.set(dir, []);
66
- }
67
- grouped.get(dir).push(path.basename(spec));
68
- }
69
- // Display grouped specs
70
- for (const [dir, files] of grouped) {
71
- const relDir = dir === '.' ? '' : `${dir}/`;
72
- for (const file of files) {
73
- console.log(` ${relDir}${file}`);
74
- }
75
- }
76
- console.log(`\nTotal: ${specs.length} specification files`);
77
- }
78
- catch (error) {
79
- console.error('Failed to list specifications:', error);
80
- process.exit(1);
81
- }
82
- }
83
- async function getActiveBranchSpec(repo) {
84
- try {
85
- const cfg = readModConfig();
86
- if (!cfg?.workspaceId || !cfg?.activeBranchId) {
87
- return undefined;
88
- }
89
- const modWorkspace = createModWorkspace(repo);
90
- const workspaceHandles = await modWorkspace.listWorkspaces();
91
- const workspaceHandle = workspaceHandles.find(wh => wh.id === cfg.workspaceId);
92
- if (!workspaceHandle)
93
- return undefined;
94
- const branches = await workspaceHandle.branch.list();
95
- const activeBranch = branches.find(b => b.id === cfg.activeBranchId);
96
- if (!activeBranch?.metadata?.linkedSpec)
97
- return undefined;
98
- return activeBranch.metadata.linkedSpec;
99
- }
100
- catch (error) {
101
- log('[Spec] Failed to get active branch spec:', error);
102
- return undefined;
103
- }
104
- }
105
- async function analyzeSpecification(workspaceRoot, specName) {
106
- const parser = new SpecificationParserService({
107
- workspaceRoot,
108
- specsDirectory: '.mod/specs',
109
- cacheEnabled: false
110
- });
111
- log('[Spec] Scanning workspace for @spec traces');
112
- const startTime = Date.now();
113
- const traceMap = await parser.scanWorkspaceForTraces();
114
- const scanTime = Date.now() - startTime;
115
- log(`[Spec] Scan completed in ${scanTime}ms, found ${traceMap.size} requirement IDs`);
116
- const specFilePath = await findSpecificationFile(workspaceRoot, specName);
117
- const content = await fs.readFile(specFilePath, 'utf-8');
118
- const parsed = await parser.parseSpecification(content, specFilePath);
119
- const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
120
- const frontmatterOffset = frontmatterMatch ? frontmatterMatch[0].split('\n').length - 1 : 0;
121
- const allRequirements = extractNestedRequirements(content, parsed.requirements, frontmatterOffset);
122
- const filteredTraces = filterTracesBySpec(traceMap, specName);
123
- const requirements = allRequirements.map(req => {
124
- const traces = filteredTraces.get(req.id) || [];
125
- const status = traces.length === 0 ? 'todo' : 'traced';
126
- return {
127
- requirementId: req.id,
128
- category: req.category,
129
- title: req.title,
130
- status: status,
131
- traceCount: traces.length,
132
- traces: traces.map(trace => ({
133
- file: path.relative(workspaceRoot, trace.filePath),
134
- line: trace.lineRange.start,
135
- description: trace.description
136
- })),
137
- hierarchyLevel: calculateHierarchyLevel(req.id),
138
- lineRange: req.lineRange
139
- };
140
- });
141
- applyHierarchicalAggregation(requirements);
142
- const summary = {
143
- traced: requirements.filter(r => r.status === 'traced').length,
144
- todo: requirements.filter(r => r.status === 'todo').length,
145
- totalTraces: requirements.reduce((sum, r) => sum + r.traceCount, 0)
146
- };
147
- return {
148
- specFile: path.relative(workspaceRoot, specFilePath),
149
- title: parsed.metadata.title,
150
- requirements,
151
- summary
152
- };
153
- }
154
- async function findSpecificationFile(workspaceRoot, specName) {
155
- const specsDir = path.join(workspaceRoot, '.mod/specs');
156
- const matches = [];
157
- async function searchDir(dir, relativePath = '') {
158
- try {
159
- const entries = await fs.readdir(dir, { withFileTypes: true });
160
- for (const entry of entries) {
161
- const fullPath = path.join(dir, entry.name);
162
- const relPath = path.join(relativePath, entry.name);
163
- if (entry.isDirectory()) {
164
- await searchDir(fullPath, relPath);
165
- }
166
- else if (entry.name.endsWith('.spec.md')) {
167
- // Check exact match with path or just filename
168
- if (relPath === specName || entry.name === specName) {
169
- matches.push(fullPath);
170
- }
171
- }
172
- }
173
- }
174
- catch {
175
- // Directory doesn't exist or can't be read
176
- }
177
- }
178
- await searchDir(specsDir);
179
- if (matches.length === 0) {
180
- throw new Error(`Specification file "${specName}" not found in ${specsDir}`);
181
- }
182
- if (matches.length > 1) {
183
- const errorMsg = [`Multiple specifications found with name "${specName}":`];
184
- const sortedPaths = matches.map(m => path.relative(specsDir, m)).sort();
185
- for (const specPath of sortedPaths) {
186
- errorMsg.push(` - ${specPath}`);
187
- }
188
- errorMsg.push('');
189
- errorMsg.push('Please specify the full path from .mod/specs/');
190
- errorMsg.push(`Example: mod spec status ${sortedPaths[0]}`);
191
- throw new Error(errorMsg.join('\n'));
192
- }
193
- return matches[0];
194
- }
195
- // Helper function to find all specifications
196
- async function findAllSpecifications(specsDir) {
197
- const specs = [];
198
- async function searchDir(dir, relativePath = '') {
199
- try {
200
- const entries = await fs.readdir(dir, { withFileTypes: true });
201
- for (const entry of entries) {
202
- const fullPath = path.join(dir, entry.name);
203
- const relPath = path.join(relativePath, entry.name);
204
- if (entry.isDirectory()) {
205
- await searchDir(fullPath, relPath);
206
- }
207
- else if (entry.name.endsWith('.spec.md')) {
208
- specs.push(relPath);
209
- }
210
- }
211
- }
212
- catch {
213
- // Directory doesn't exist or can't be read
214
- }
215
- }
216
- await searchDir(specsDir);
217
- return specs.sort();
218
- }
219
- // Extract nested requirements (reused from status.ts)
220
- function extractNestedRequirements(content, topLevelRequirements, frontmatterOffset) {
221
- const adjustedTopLevel = topLevelRequirements.map(req => ({
222
- ...req,
223
- lineRange: req.lineRange ? {
224
- start: req.lineRange.start + frontmatterOffset,
225
- end: req.lineRange.end + frontmatterOffset
226
- } : undefined
227
- }));
228
- const allRequirements = [...adjustedTopLevel];
229
- const nestedReqRegex = /^\s*-\s*\*\*(?<id>REQ-[A-Z]+-\d+(?:\.\d+)*)\*\*:\s*(?<description>.*)/gm;
230
- let match;
231
- while ((match = nestedReqRegex.exec(content)) !== null) {
232
- const groups = match.groups;
233
- if (!groups)
234
- continue;
235
- const { id, description } = groups;
236
- const categoryMatch = id.match(/REQ-([A-Z]+)/);
237
- if (!categoryMatch)
238
- continue;
239
- const categoryStr = categoryMatch[1];
240
- const category = Object.values(RequirementCategory).find(cat => cat === categoryStr);
241
- if (!category)
242
- continue;
243
- if (topLevelRequirements.some(req => req.id === id))
244
- continue;
245
- const lineNumber = content.substring(0, match.index).split('\n').length;
246
- allRequirements.push({
247
- id,
248
- category,
249
- title: description.trim(),
250
- description: description.trim(),
251
- children: [],
252
- lineRange: { start: lineNumber, end: lineNumber },
253
- parent: determineParentRequirement(id)
254
- });
255
- }
256
- return allRequirements;
257
- }
258
- function filterTracesBySpec(traceMap, specName) {
259
- const filtered = new Map();
260
- const specBaseName = specName.replace('.spec.md', '');
261
- for (const [requirementId, traces] of traceMap.entries()) {
262
- const relevantTraces = traces.filter(trace => {
263
- if (!trace.specPath)
264
- return true;
265
- return trace.specPath.includes(specBaseName);
266
- });
267
- if (relevantTraces.length > 0) {
268
- filtered.set(requirementId, relevantTraces);
269
- }
270
- }
271
- return filtered;
272
- }
273
- // Helper functions
274
- function determineParentRequirement(requirementId) {
275
- const parts = requirementId.split('-');
276
- if (parts.length < 3)
277
- return undefined;
278
- const [, category, numberPart] = parts;
279
- const dotIndex = numberPart.indexOf('.');
280
- if (dotIndex === -1)
281
- return undefined;
282
- const parentNumber = numberPart.substring(0, dotIndex);
283
- return `REQ-${category}-${parentNumber}`;
284
- }
285
- function calculateHierarchyLevel(requirementId) {
286
- const parts = requirementId.split('-');
287
- if (parts.length < 3)
288
- return 1;
289
- const numberPart = parts[2];
290
- const dotCount = (numberPart.match(/\./g) || []).length;
291
- return 2 + dotCount;
292
- }
293
- function applyHierarchicalAggregation(requirements) {
294
- const sortedByHierarchy = [...requirements].sort((a, b) => b.hierarchyLevel - a.hierarchyLevel);
295
- for (const requirement of sortedByHierarchy) {
296
- const children = requirements.filter(r => r.requirementId.startsWith(requirement.requirementId + '.') &&
297
- r.hierarchyLevel === requirement.hierarchyLevel + 1);
298
- if (children.length > 0) {
299
- if (requirement.traceCount === 0 && children.every(child => child.status === 'traced')) {
300
- requirement.status = 'traced';
301
- }
302
- }
303
- }
304
- }
305
- function printTscStyleOutput(analysis) {
306
- const colors = {
307
- traced: '\x1b[32m', // Green
308
- todo: '\x1b[31m', // Red
309
- reset: '\x1b[0m',
310
- dim: '\x1b[90m', // Dim gray
311
- count: '\x1b[36m' // Cyan for trace counts
312
- };
313
- const totalReqs = analysis.requirements.length;
314
- console.log(`${analysis.specFile} - ${totalReqs} requirements\n`);
315
- const requirementsByCategory = groupRequirementsByCategory(analysis.requirements);
316
- for (const [category, requirements] of requirementsByCategory.entries()) {
317
- console.log(`${colors.dim}## ${category} Requirements${colors.reset}\n`);
318
- for (const req of requirements) {
319
- const statusLabel = req.status;
320
- const color = colors[statusLabel];
321
- const truncatedTitle = req.title.length > 60 ?
322
- req.title.substring(0, 57) + '...' : req.title;
323
- const indent = ' '.repeat(Math.max(0, req.hierarchyLevel - 2));
324
- if (req.traces.length > 0) {
325
- const trace = req.traces[0];
326
- const traceCountDisplay = req.traceCount > 1 ? ` ${colors.count}(${req.traceCount})${colors.reset}` : '';
327
- console.log(`${indent}${trace.file}:${trace.line}:1 - ${color}${statusLabel}${colors.reset}${traceCountDisplay} ${req.requirementId}: ${truncatedTitle}`);
328
- }
329
- else {
330
- const lineNumber = req.lineRange?.start || 1;
331
- console.log(`${indent}${analysis.specFile}:${lineNumber}:1 - ${color}${statusLabel}${colors.reset} ${req.requirementId}: ${truncatedTitle}`);
332
- }
333
- }
334
- console.log();
335
- }
336
- const tracedText = `${colors.traced}${analysis.summary.traced} traced${colors.reset}`;
337
- const todoText = `${colors.todo}${analysis.summary.todo} todo${colors.reset}`;
338
- const totalTracesText = `${colors.count}${analysis.summary.totalTraces} traces${colors.reset}`;
339
- console.log(`Found ${tracedText}, ${todoText} (${totalTracesText} total).`);
340
- }
341
- function groupRequirementsByCategory(requirements) {
342
- const categoryOrder = ['UX', 'BUS', 'DATA', 'INT', 'INFRA', 'QUAL'];
343
- const grouped = new Map();
344
- for (const category of categoryOrder) {
345
- grouped.set(category, []);
346
- }
347
- for (const req of requirements) {
348
- const category = req.category;
349
- if (!grouped.has(category)) {
350
- grouped.set(category, []);
351
- }
352
- grouped.get(category).push(req);
353
- }
354
- for (const [, reqs] of grouped.entries()) {
355
- reqs.sort((a, b) => a.requirementId.localeCompare(b.requirementId));
356
- }
357
- const result = new Map();
358
- for (const [category, reqs] of grouped.entries()) {
359
- if (reqs.length > 0) {
360
- result.set(category, reqs);
361
- }
362
- }
363
- return result;
364
- }
365
- function handleError(error, specName) {
366
- if (error instanceof Error) {
367
- if (error.message.includes('not found')) {
368
- console.error(`Error: Specification file "${specName}" not found`);
369
- process.exit(1);
370
- }
371
- else if (error.message.includes('parse')) {
372
- console.error(`Error: Failed to parse specification: ${error.message}`);
373
- process.exit(2);
374
- }
375
- else if (error.message.includes('scan')) {
376
- console.error(`Error: Failed to scan workspace: ${error.message}`);
377
- process.exit(3);
378
- }
379
- else if (error.message.includes('Multiple specifications found')) {
380
- console.error(error.message);
381
- process.exit(1);
382
- }
383
- }
384
- console.error(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`);
385
- process.exit(3);
386
- }