@mod-computer/cli 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/README.md +125 -0
- package/commands/execute.md +156 -0
- package/commands/overview.md +233 -0
- package/commands/review.md +151 -0
- package/commands/spec.md +169 -0
- package/dist/app.js +227 -0
- package/dist/cli.bundle.js +25824 -0
- package/dist/cli.bundle.js.map +7 -0
- package/dist/cli.js +121 -0
- package/dist/commands/agents-run.js +71 -0
- package/dist/commands/auth.js +151 -0
- package/dist/commands/branch.js +1411 -0
- package/dist/commands/claude-sync.js +772 -0
- package/dist/commands/index.js +43 -0
- package/dist/commands/init.js +378 -0
- package/dist/commands/recover.js +207 -0
- package/dist/commands/spec.js +386 -0
- package/dist/commands/status.js +329 -0
- package/dist/commands/sync.js +95 -0
- package/dist/commands/workspace.js +423 -0
- package/dist/components/conflict-resolution-ui.js +120 -0
- package/dist/components/messages.js +5 -0
- package/dist/components/thread.js +8 -0
- package/dist/config/features.js +72 -0
- package/dist/config/release-profiles/development.json +11 -0
- package/dist/config/release-profiles/mvp.json +12 -0
- package/dist/config/release-profiles/v0.1.json +11 -0
- package/dist/config/release-profiles/v0.2.json +11 -0
- package/dist/containers/branches-container.js +140 -0
- package/dist/containers/directory-container.js +92 -0
- package/dist/containers/thread-container.js +214 -0
- package/dist/containers/threads-container.js +27 -0
- package/dist/containers/workspaces-container.js +27 -0
- package/dist/daemon-worker.js +257 -0
- package/dist/lib/auth-server.js +153 -0
- package/dist/lib/browser.js +35 -0
- package/dist/lib/storage.js +203 -0
- package/dist/services/automatic-file-tracker.js +303 -0
- package/dist/services/cli-orchestrator.js +227 -0
- package/dist/services/feature-flags.js +187 -0
- package/dist/services/file-import-service.js +283 -0
- package/dist/services/file-transformation-service.js +218 -0
- package/dist/services/logger.js +44 -0
- package/dist/services/mod-config.js +61 -0
- package/dist/services/modignore-service.js +326 -0
- package/dist/services/sync-daemon.js +244 -0
- package/dist/services/thread-notification-service.js +50 -0
- package/dist/services/thread-service.js +147 -0
- package/dist/stores/use-directory-store.js +96 -0
- package/dist/stores/use-threads-store.js +46 -0
- package/dist/stores/use-workspaces-store.js +32 -0
- package/dist/types/config.js +16 -0
- package/dist/types/index.js +2 -0
- package/dist/types/workspace-connection.js +2 -0
- package/dist/types.js +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,386 @@
|
|
|
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
|
+
}
|