@mod-computer/cli 0.1.1 → 0.2.2
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 +98 -76
- package/dist/cli.bundle.js +23750 -12931
- package/dist/cli.bundle.js.map +4 -4
- package/dist/cli.js +23 -12
- package/dist/commands/add.js +245 -0
- package/dist/commands/auth.js +129 -21
- package/dist/commands/comment.js +568 -0
- package/dist/commands/diff.js +182 -0
- package/dist/commands/index.js +33 -3
- package/dist/commands/init.js +475 -221
- package/dist/commands/ls.js +135 -0
- package/dist/commands/members.js +687 -0
- package/dist/commands/mv.js +282 -0
- package/dist/commands/rm.js +257 -0
- package/dist/commands/status.js +273 -306
- package/dist/commands/sync.js +99 -75
- package/dist/commands/trace.js +1752 -0
- package/dist/commands/workspace.js +354 -330
- package/dist/config/features.js +18 -7
- package/dist/config/release-profiles/development.json +4 -1
- package/dist/config/release-profiles/mvp.json +4 -2
- package/dist/daemon/conflict-resolution.js +172 -0
- package/dist/daemon/content-hash.js +31 -0
- package/dist/daemon/file-sync.js +985 -0
- package/dist/daemon/index.js +203 -0
- package/dist/daemon/mime-types.js +166 -0
- package/dist/daemon/offline-queue.js +211 -0
- package/dist/daemon/path-utils.js +64 -0
- package/dist/daemon/share-policy.js +83 -0
- package/dist/daemon/wasm-errors.js +189 -0
- package/dist/daemon/worker.js +557 -0
- package/dist/daemon-worker.js +3 -2
- package/dist/errors/workspace-errors.js +48 -0
- package/dist/lib/auth-server.js +89 -26
- package/dist/lib/browser.js +1 -1
- package/dist/lib/diff.js +284 -0
- package/dist/lib/formatters.js +204 -0
- package/dist/lib/git.js +137 -0
- package/dist/lib/local-fs.js +201 -0
- package/dist/lib/prompts.js +23 -83
- package/dist/lib/storage.js +11 -1
- package/dist/lib/trace-formatters.js +314 -0
- package/dist/services/add-service.js +554 -0
- package/dist/services/add-validation.js +124 -0
- package/dist/services/mod-config.js +8 -2
- package/dist/services/modignore-service.js +2 -0
- package/dist/stores/use-workspaces-store.js +36 -14
- package/dist/types/add-types.js +99 -0
- package/dist/types/config.js +1 -1
- package/dist/types/workspace-connection.js +53 -2
- package/package.json +7 -5
- package/commands/execute.md +0 -156
- package/commands/overview.md +0 -233
- package/commands/review.md +0 -151
- package/commands/spec.md +0 -169
package/dist/commands/status.js
CHANGED
|
@@ -1,329 +1,296 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// @spec status-command.spec.md/REQ-DATA-1.1 Auto-append .spec.md extension
|
|
19
|
-
if (!specName.endsWith('.spec.md')) {
|
|
20
|
-
specName += '.spec.md';
|
|
21
|
-
}
|
|
22
|
-
const workspaceRoot = process.cwd();
|
|
23
|
-
const analysis = await analyzeSpecification(workspaceRoot, specName);
|
|
24
|
-
// @spec status-command.spec.md/REQ-UX-1.1 TypeScript compiler-style output
|
|
25
|
-
printTscStyleOutput(analysis);
|
|
26
|
-
// Exit successfully
|
|
27
|
-
process.exit(0);
|
|
1
|
+
// glassware[type="implementation", id="impl-cli-status-cmd--a59c0eef", requirements="requirement-cli-status-cmd--73a24f60,requirement-cli-status-requires-workspace--99699152,requirement-cli-status-local-modified--1e5f495a,requirement-cli-status-workspace-modified--10b85f32,requirement-cli-status-local-new--31131959,requirement-cli-status-workspace-new--47a5c7b1,requirement-cli-status-conflicts--8b6fb820,requirement-cli-status-short--81ed94d8,requirement-cli-status-json--9f708de6"]
|
|
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 { readLocalFile, getLocalFileStats, listLocalFiles } from '../lib/local-fs.js';
|
|
6
|
+
import { getWorkspaceContent } from '../lib/diff.js';
|
|
7
|
+
import { detectGitRepo } from '../lib/git.js';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import crypto from 'crypto';
|
|
10
|
+
// glassware[type="implementation", id="impl-cli-status-requires-workspace--7061acd4", requirements="requirement-cli-status-requires-workspace--99699152,requirement-cli-fd-error-no-workspace--1919b4fb"]
|
|
11
|
+
function requireWorkspaceConnection() {
|
|
12
|
+
const currentDir = process.cwd();
|
|
13
|
+
const connection = readWorkspaceConnection(currentDir);
|
|
14
|
+
if (!connection) {
|
|
15
|
+
console.error('Error: Not connected to a workspace.');
|
|
16
|
+
console.error('Run `mod init` to connect this directory first.');
|
|
17
|
+
process.exit(1);
|
|
28
18
|
}
|
|
29
|
-
catch (error) {
|
|
30
|
-
// @spec status-command.spec.md/REQ-BUS-2.1 Handle different error types with appropriate exit codes
|
|
31
|
-
if (error instanceof Error) {
|
|
32
|
-
// @spec status-command.spec.md/REQ-UX-2.1 Clear error messages for file not found
|
|
33
|
-
if (error.message.includes('not found')) {
|
|
34
|
-
console.error(`Error: Specification file "${args[0]}" not found`);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
// @spec status-command.spec.md/REQ-UX-2.1 Clear error messages for parse errors
|
|
37
|
-
}
|
|
38
|
-
else if (error.message.includes('parse')) {
|
|
39
|
-
console.error(`Error: Failed to parse specification: ${error.message}`);
|
|
40
|
-
process.exit(2);
|
|
41
|
-
// @spec status-command.spec.md/REQ-UX-2.1 Clear error messages for scan failures
|
|
42
|
-
}
|
|
43
|
-
else if (error.message.includes('scan')) {
|
|
44
|
-
console.error(`Error: Failed to scan workspace: ${error.message}`);
|
|
45
|
-
process.exit(3);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
console.error(`Unexpected error: ${error instanceof Error ? error.message : String(error)}`);
|
|
49
|
-
process.exit(3);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
// @spec status-command.spec.md/REQ-DATA-1.1 Use SpecificationParserService for parsing and scanning
|
|
53
|
-
async function analyzeSpecification(workspaceRoot, specName) {
|
|
54
|
-
// @spec status-command.spec.md/REQ-INT-1.1 Initialize SpecificationParserService
|
|
55
|
-
// @spec status-command.spec.md/REQ-BUS-2.2 Pure offline operation using local files only, no network or repo connections
|
|
56
|
-
const parser = new SpecificationParserService({
|
|
57
|
-
workspaceRoot,
|
|
58
|
-
specsDirectory: '.mod/specs',
|
|
59
|
-
cacheEnabled: false
|
|
60
|
-
});
|
|
61
|
-
// @spec status-command.spec.md/REQ-DATA-2.1 Scan workspace for @spec traces
|
|
62
|
-
// @spec status-command.spec.md/REQ-INFRA-2.2 Use existing logger service for debug output when MOD_CLI_DEBUG is set
|
|
63
|
-
log('[Status] Scanning workspace for @spec traces');
|
|
64
|
-
const startTime = Date.now();
|
|
65
|
-
const traceMap = await parser.scanWorkspaceForTraces();
|
|
66
|
-
const scanTime = Date.now() - startTime;
|
|
67
|
-
// @spec status-command.spec.md/REQ-INFRA-1.1 Process up to 1000 requirements in under 5 seconds
|
|
68
|
-
log(`[Status] Scan completed in ${scanTime}ms, found ${traceMap.size} requirement IDs`);
|
|
69
|
-
// @spec status-command.spec.md/REQ-DATA-1.1 Find and parse specification file
|
|
70
|
-
const specFilePath = await findSpecificationFile(workspaceRoot, specName);
|
|
71
|
-
const content = await fs.readFile(specFilePath, 'utf-8');
|
|
72
|
-
const parsed = await parser.parseSpecification(content, specFilePath);
|
|
73
|
-
// Calculate frontmatter offset for line number adjustment
|
|
74
|
-
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
|
|
75
|
-
const frontmatterOffset = frontmatterMatch ? frontmatterMatch[0].split('\n').length - 1 : 0;
|
|
76
|
-
// @spec status-command.spec.md/REQ-DATA-1.2 Extract nested requirements from bullet points
|
|
77
|
-
const allRequirements = extractNestedRequirements(content, parsed.requirements, frontmatterOffset);
|
|
78
|
-
// @spec status-command.spec.md/REQ-DATA-2.3 Filter traces by spec name
|
|
79
|
-
const filteredTraces = filterTracesBySpec(traceMap, specName);
|
|
80
|
-
// @spec status-command.spec.md/REQ-BUS-1.1 Calculate status for each requirement
|
|
81
|
-
const requirements = allRequirements.map(req => {
|
|
82
|
-
const traces = filteredTraces.get(req.id) || [];
|
|
83
|
-
const status = getRequirementStatus(traces.length);
|
|
84
|
-
return {
|
|
85
|
-
requirementId: req.id,
|
|
86
|
-
category: req.category,
|
|
87
|
-
title: req.title,
|
|
88
|
-
status,
|
|
89
|
-
traceCount: traces.length,
|
|
90
|
-
traces: traces.map(trace => ({
|
|
91
|
-
file: path.relative(workspaceRoot, trace.filePath),
|
|
92
|
-
line: trace.lineRange.start,
|
|
93
|
-
description: trace.description
|
|
94
|
-
})),
|
|
95
|
-
hierarchyLevel: calculateHierarchyLevel(req.id),
|
|
96
|
-
lineRange: req.lineRange
|
|
97
|
-
};
|
|
98
|
-
});
|
|
99
|
-
// @spec status-command.spec.md/REQ-BUS-1.2 Hierarchical aggregation: parent status reflects worst child status
|
|
100
|
-
applyHierarchicalAggregation(requirements);
|
|
101
|
-
// @spec status-command.spec.md/REQ-BUS-1.2 Calculate summary counts
|
|
102
|
-
const summary = {
|
|
103
|
-
traced: requirements.filter(r => r.status === 'traced').length,
|
|
104
|
-
todo: requirements.filter(r => r.status === 'todo').length,
|
|
105
|
-
totalTraces: requirements.reduce((sum, r) => sum + r.traceCount, 0)
|
|
106
|
-
};
|
|
107
19
|
return {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
summary
|
|
20
|
+
workspaceId: connection.workspaceId,
|
|
21
|
+
workspaceName: connection.workspaceName,
|
|
22
|
+
workspacePath: connection.path,
|
|
112
23
|
};
|
|
113
24
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (!
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
// Determine category from requirement ID
|
|
136
|
-
const categoryMatch = id.match(/REQ-([A-Z]+)/);
|
|
137
|
-
if (!categoryMatch)
|
|
138
|
-
continue;
|
|
139
|
-
const categoryStr = categoryMatch[1];
|
|
140
|
-
// @spec status-command.spec.md/REQ-DATA-1.3 Extract requirement categories UX, BUS, DATA, INT, INFRA, QUAL
|
|
141
|
-
const category = Object.values(RequirementCategory).find(cat => cat === categoryStr);
|
|
142
|
-
if (!category)
|
|
143
|
-
continue;
|
|
144
|
-
// Skip if it's already a top-level requirement (parsed by SpecificationParserService)
|
|
145
|
-
if (topLevelRequirements.some(req => req.id === id))
|
|
146
|
-
continue;
|
|
147
|
-
// @spec status-command.spec.md/REQ-DATA-1.2.1 Extract nested requirements from bullet points using regex pattern matching
|
|
148
|
-
// Add nested requirement with line number from match (1-based indexing)
|
|
149
|
-
const lineNumber = content.substring(0, match.index).split('\n').length;
|
|
150
|
-
allRequirements.push({
|
|
151
|
-
id,
|
|
152
|
-
category,
|
|
153
|
-
title: description.trim(),
|
|
154
|
-
description: description.trim(),
|
|
155
|
-
children: [],
|
|
156
|
-
lineRange: { start: lineNumber, end: lineNumber },
|
|
157
|
-
parent: determineParentRequirement(id)
|
|
158
|
-
});
|
|
25
|
+
function parseArgs(args) {
|
|
26
|
+
const options = {
|
|
27
|
+
short: false,
|
|
28
|
+
json: false,
|
|
29
|
+
quiet: false,
|
|
30
|
+
};
|
|
31
|
+
let pathFilter;
|
|
32
|
+
for (let i = 0; i < args.length; i++) {
|
|
33
|
+
const arg = args[i];
|
|
34
|
+
if (arg === '--short' || arg === '-s') {
|
|
35
|
+
options.short = true;
|
|
36
|
+
}
|
|
37
|
+
else if (arg === '--json') {
|
|
38
|
+
options.json = true;
|
|
39
|
+
}
|
|
40
|
+
else if (arg === '--quiet' || arg === '-q') {
|
|
41
|
+
options.quiet = true;
|
|
42
|
+
}
|
|
43
|
+
else if (!arg.startsWith('-') && !pathFilter) {
|
|
44
|
+
pathFilter = arg;
|
|
45
|
+
}
|
|
159
46
|
}
|
|
160
|
-
return
|
|
47
|
+
return { pathFilter, options };
|
|
161
48
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const [, category, numberPart] = parts;
|
|
168
|
-
const dotIndex = numberPart.indexOf('.');
|
|
169
|
-
if (dotIndex === -1)
|
|
170
|
-
return undefined; // Top-level requirement
|
|
171
|
-
const parentNumber = numberPart.substring(0, dotIndex);
|
|
172
|
-
return `REQ-${category}-${parentNumber}`;
|
|
49
|
+
/**
|
|
50
|
+
* Quick hash for content comparison
|
|
51
|
+
*/
|
|
52
|
+
function hashContent(content) {
|
|
53
|
+
return crypto.createHash('md5').update(content).digest('hex');
|
|
173
54
|
}
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
55
|
+
// glassware[type="implementation", id="impl-cli-fd-perf-status--15f2ce0f", requirements="requirement-cli-fd-perf-status--248b3313"]
|
|
56
|
+
// glassware[type="implementation", id="impl-cli-status-branch--542da302", specifications="specification-spec-status-branch--c6241388,specification-spec-status-no-git--615a5917"]
|
|
57
|
+
export async function statusCommand(args, repo) {
|
|
58
|
+
const { workspaceId, workspacePath } = requireWorkspaceConnection();
|
|
59
|
+
const { pathFilter, options } = parseArgs(args);
|
|
60
|
+
// Detect git repo and branch
|
|
61
|
+
const gitInfo = detectGitRepo(workspacePath);
|
|
62
|
+
try {
|
|
63
|
+
const modWorkspace = createModWorkspace(repo);
|
|
64
|
+
const workspaceHandle = await modWorkspace.openWorkspace(workspaceId);
|
|
65
|
+
// Get workspace files
|
|
66
|
+
const workspaceFiles = await workspaceHandle.file.list();
|
|
67
|
+
// Build workspace file map
|
|
68
|
+
const wsFileMap = new Map();
|
|
69
|
+
for (const file of workspaceFiles) {
|
|
70
|
+
const filePath = file.metadata?.path || file.name;
|
|
71
|
+
if (pathFilter && !filePath.startsWith(pathFilter))
|
|
72
|
+
continue;
|
|
73
|
+
wsFileMap.set(filePath, {
|
|
74
|
+
id: file.id,
|
|
75
|
+
size: file.size,
|
|
76
|
+
updatedAt: file.updatedAt,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// Get local files
|
|
80
|
+
const localFiles = await listLocalFiles(workspacePath);
|
|
81
|
+
const localFileMap = new Map();
|
|
82
|
+
for (const relativePath of localFiles) {
|
|
83
|
+
if (pathFilter && !relativePath.startsWith(pathFilter))
|
|
84
|
+
continue;
|
|
85
|
+
// Skip common ignore patterns
|
|
86
|
+
if (relativePath.includes('node_modules/') ||
|
|
87
|
+
relativePath.includes('.git/') ||
|
|
88
|
+
relativePath.includes('.mod/') ||
|
|
89
|
+
relativePath.startsWith('.')) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const stats = await getLocalFileStats(path.join(workspacePath, relativePath));
|
|
93
|
+
if (stats) {
|
|
94
|
+
localFileMap.set(relativePath, stats);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Compare files
|
|
98
|
+
const statusList = [];
|
|
99
|
+
const processedPaths = new Set();
|
|
100
|
+
// Check workspace files against local
|
|
101
|
+
for (const [filePath, wsFile] of wsFileMap) {
|
|
102
|
+
processedPaths.add(filePath);
|
|
103
|
+
const localFile = localFileMap.get(filePath);
|
|
104
|
+
if (!localFile) {
|
|
105
|
+
// File in workspace but not local
|
|
106
|
+
statusList.push({
|
|
107
|
+
path: filePath,
|
|
108
|
+
status: 'added-workspace',
|
|
109
|
+
workspaceSize: wsFile.size,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// File exists in both - compare content
|
|
114
|
+
const localContent = await readLocalFile(path.join(workspacePath, filePath));
|
|
115
|
+
if (localContent !== null) {
|
|
116
|
+
// Get workspace content
|
|
117
|
+
try {
|
|
118
|
+
const handle = await workspaceHandle.file.get(wsFile.id);
|
|
119
|
+
if (handle) {
|
|
120
|
+
const doc = handle.doc();
|
|
121
|
+
const wsContent = getWorkspaceContent(doc?.content);
|
|
122
|
+
const localHash = hashContent(localContent);
|
|
123
|
+
const wsHash = hashContent(wsContent);
|
|
124
|
+
if (localHash !== wsHash) {
|
|
125
|
+
// Content differs - check timestamps to determine direction
|
|
126
|
+
const localTime = localFile.mtime.getTime();
|
|
127
|
+
const wsTime = new Date(wsFile.updatedAt).getTime();
|
|
128
|
+
// If both modified recently (within 1 second of each other), it's a conflict
|
|
129
|
+
const timeDiff = Math.abs(localTime - wsTime);
|
|
130
|
+
if (timeDiff < 1000 && localTime !== wsTime) {
|
|
131
|
+
// Conflict - modified in both places
|
|
132
|
+
statusList.push({
|
|
133
|
+
path: filePath,
|
|
134
|
+
status: 'conflict',
|
|
135
|
+
localSize: localFile.size,
|
|
136
|
+
workspaceSize: wsFile.size,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
else if (localTime > wsTime) {
|
|
140
|
+
// Local is newer
|
|
141
|
+
statusList.push({
|
|
142
|
+
path: filePath,
|
|
143
|
+
status: 'modified-local',
|
|
144
|
+
localSize: localFile.size,
|
|
145
|
+
workspaceSize: wsFile.size,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// Workspace is newer
|
|
150
|
+
statusList.push({
|
|
151
|
+
path: filePath,
|
|
152
|
+
status: 'modified-workspace',
|
|
153
|
+
localSize: localFile.size,
|
|
154
|
+
workspaceSize: wsFile.size,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// If we can't load the file, assume local is newer
|
|
162
|
+
statusList.push({
|
|
163
|
+
path: filePath,
|
|
164
|
+
status: 'modified-local',
|
|
165
|
+
localSize: localFile.size,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
190
168
|
}
|
|
191
169
|
}
|
|
192
170
|
}
|
|
193
|
-
|
|
194
|
-
|
|
171
|
+
// Check local files not in workspace
|
|
172
|
+
for (const [filePath, localFile] of localFileMap) {
|
|
173
|
+
if (processedPaths.has(filePath))
|
|
174
|
+
continue;
|
|
175
|
+
statusList.push({
|
|
176
|
+
path: filePath,
|
|
177
|
+
status: 'added-local',
|
|
178
|
+
localSize: localFile.size,
|
|
179
|
+
});
|
|
195
180
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return trace.specPath.includes(specBaseName);
|
|
214
|
-
});
|
|
215
|
-
if (relevantTraces.length > 0) {
|
|
216
|
-
filtered.set(requirementId, relevantTraces);
|
|
181
|
+
// Sort by path
|
|
182
|
+
statusList.sort((a, b) => a.path.localeCompare(b.path));
|
|
183
|
+
// Format output
|
|
184
|
+
if (options.json) {
|
|
185
|
+
// JSON output format with git info
|
|
186
|
+
const output = {
|
|
187
|
+
git: {
|
|
188
|
+
isGitRepo: gitInfo.isGitRepo,
|
|
189
|
+
branch: gitInfo.branch,
|
|
190
|
+
isDetached: gitInfo.isDetached,
|
|
191
|
+
},
|
|
192
|
+
localChanges: statusList.filter(f => f.status === 'modified-local' || f.status === 'added-local' || f.status === 'deleted-workspace'),
|
|
193
|
+
workspaceChanges: statusList.filter(f => f.status === 'modified-workspace' || f.status === 'added-workspace' || f.status === 'deleted-local'),
|
|
194
|
+
conflicts: statusList.filter(f => f.status === 'conflict'),
|
|
195
|
+
};
|
|
196
|
+
console.log(JSON.stringify(output, null, 2));
|
|
197
|
+
return;
|
|
217
198
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const numberPart = parts[2]; // The part after REQ-CAT-
|
|
231
|
-
const dotCount = (numberPart.match(/\./g) || []).length;
|
|
232
|
-
// @spec status-command.spec.md/REQ-DATA-1.2.2 Calculate hierarchy levels for proper indentation in output display
|
|
233
|
-
return 2 + dotCount; // REQ-CAT-N = 2, REQ-CAT-N.N = 3, etc.
|
|
234
|
-
}
|
|
235
|
-
// @spec status-command.spec.md/REQ-BUS-1.2 Hierarchical aggregation: parent status reflects worst child status
|
|
236
|
-
function applyHierarchicalAggregation(requirements) {
|
|
237
|
-
// Sort by hierarchy level (deepest first) to process children before parents
|
|
238
|
-
const sortedByHierarchy = [...requirements].sort((a, b) => b.hierarchyLevel - a.hierarchyLevel);
|
|
239
|
-
for (const requirement of sortedByHierarchy) {
|
|
240
|
-
// Find all child requirements
|
|
241
|
-
const children = requirements.filter(r => r.requirementId.startsWith(requirement.requirementId + '.') &&
|
|
242
|
-
r.hierarchyLevel === requirement.hierarchyLevel + 1);
|
|
243
|
-
if (children.length > 0) {
|
|
244
|
-
// If parent has no direct traces but all children are traced, mark parent as traced
|
|
245
|
-
if (requirement.traceCount === 0 && children.every(child => child.status === 'traced')) {
|
|
246
|
-
requirement.status = 'traced';
|
|
199
|
+
if (options.quiet) {
|
|
200
|
+
const localCount = statusList.filter(f => f.status === 'modified-local' || f.status === 'added-local').length;
|
|
201
|
+
const wsCount = statusList.filter(f => f.status === 'modified-workspace' || f.status === 'added-workspace').length;
|
|
202
|
+
const conflictCount = statusList.filter(f => f.status === 'conflict').length;
|
|
203
|
+
console.log(`${localCount} local, ${wsCount} workspace, ${conflictCount} conflicts`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (options.short) {
|
|
207
|
+
// Short format output
|
|
208
|
+
for (const file of statusList) {
|
|
209
|
+
const code = getStatusCode(file.status);
|
|
210
|
+
console.log(`${code} ${file.path}`);
|
|
247
211
|
}
|
|
248
|
-
|
|
212
|
+
if (statusList.length === 0) {
|
|
213
|
+
console.log('No changes.');
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
249
216
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
todo: '\x1b[31m', // Red
|
|
258
|
-
reset: '\x1b[0m', // Reset
|
|
259
|
-
dim: '\x1b[90m', // Dim gray
|
|
260
|
-
count: '\x1b[36m' // Cyan for trace counts
|
|
261
|
-
};
|
|
262
|
-
// @spec status-command.spec.md/REQ-UX-1.3 Header with spec name and count
|
|
263
|
-
const totalReqs = analysis.requirements.length;
|
|
264
|
-
console.log(`${analysis.specFile} - ${totalReqs} requirements\n`);
|
|
265
|
-
// @spec status-command.spec.md/REQ-UX-1.1 Print requirements organized by top-level sections
|
|
266
|
-
const requirementsByCategory = groupRequirementsByCategory(analysis.requirements);
|
|
267
|
-
// @spec status-command.spec.md/REQ-BUS-1.2 Hierarchical aggregation: parent status reflects worst child status
|
|
268
|
-
for (const [category, requirements] of requirementsByCategory.entries()) {
|
|
269
|
-
// @spec status-command.spec.md/REQ-UX-1.2.1 Section headers with color-coded category grouping for visual organization
|
|
270
|
-
// Print section header
|
|
271
|
-
console.log(`${colors.dim}## ${category} Requirements${colors.reset}\n`);
|
|
272
|
-
for (const req of requirements) {
|
|
273
|
-
const statusLabel = req.status === 'todo' ? 'todo' : 'traced';
|
|
274
|
-
const color = colors[statusLabel];
|
|
275
|
-
// @spec status-command.spec.md/REQ-UX-1.3 Truncate title to 60 chars
|
|
276
|
-
const truncatedTitle = req.title.length > 60 ?
|
|
277
|
-
req.title.substring(0, 57) + '...' : req.title;
|
|
278
|
-
// @spec status-command.spec.md/REQ-DATA-1.2 Add indentation based on hierarchy level
|
|
279
|
-
const indent = ' '.repeat(Math.max(0, req.hierarchyLevel - 2));
|
|
280
|
-
if (req.traces.length > 0) {
|
|
281
|
-
// @spec status-command.spec.md/REQ-UX-1.1 Show implementation location with trace count
|
|
282
|
-
const trace = req.traces[0]; // Show first trace
|
|
283
|
-
const traceCountDisplay = req.traceCount > 1 ? ` ${colors.count}(${req.traceCount})${colors.reset}` : '';
|
|
284
|
-
console.log(`${indent}${trace.file}:${trace.line}:1 - ${color}${statusLabel}${colors.reset}${traceCountDisplay} ${req.requirementId}: ${truncatedTitle}`);
|
|
217
|
+
// Default output - show git branch info first
|
|
218
|
+
if (!options.json && !options.short && !options.quiet) {
|
|
219
|
+
if (gitInfo.isGitRepo) {
|
|
220
|
+
const branchDisplay = gitInfo.isDetached
|
|
221
|
+
? `${gitInfo.branch} (detached HEAD)`
|
|
222
|
+
: gitInfo.branch;
|
|
223
|
+
console.log(`On git branch: ${branchDisplay}`);
|
|
285
224
|
}
|
|
286
225
|
else {
|
|
287
|
-
|
|
288
|
-
const lineNumber = req.lineRange?.start || 1;
|
|
289
|
-
console.log(`${indent}${analysis.specFile}:${lineNumber}:1 - ${color}${statusLabel}${colors.reset} ${req.requirementId}: ${truncatedTitle}`);
|
|
226
|
+
console.log('On git branch: (no git)');
|
|
290
227
|
}
|
|
228
|
+
console.log('');
|
|
291
229
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (
|
|
313
|
-
|
|
230
|
+
if (statusList.length === 0) {
|
|
231
|
+
console.log('Everything up to date.');
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
// Group by category
|
|
235
|
+
const localModified = statusList.filter(f => f.status === 'modified-local');
|
|
236
|
+
const localAdded = statusList.filter(f => f.status === 'added-local');
|
|
237
|
+
const wsModified = statusList.filter(f => f.status === 'modified-workspace');
|
|
238
|
+
const wsAdded = statusList.filter(f => f.status === 'added-workspace');
|
|
239
|
+
const conflicts = statusList.filter(f => f.status === 'conflict');
|
|
240
|
+
if (localModified.length > 0 || localAdded.length > 0) {
|
|
241
|
+
console.log('Changes not synced to workspace:');
|
|
242
|
+
for (const file of localModified) {
|
|
243
|
+
console.log(` modified: ${file.path}`);
|
|
244
|
+
}
|
|
245
|
+
for (const file of localAdded) {
|
|
246
|
+
console.log(` new file: ${file.path}`);
|
|
247
|
+
}
|
|
248
|
+
console.log('');
|
|
249
|
+
}
|
|
250
|
+
if (wsModified.length > 0 || wsAdded.length > 0) {
|
|
251
|
+
console.log('Changes not synced to local:');
|
|
252
|
+
for (const file of wsModified) {
|
|
253
|
+
console.log(` modified: ${file.path}`);
|
|
254
|
+
}
|
|
255
|
+
for (const file of wsAdded) {
|
|
256
|
+
console.log(` new file: ${file.path}`);
|
|
257
|
+
}
|
|
258
|
+
console.log('');
|
|
259
|
+
}
|
|
260
|
+
if (conflicts.length > 0) {
|
|
261
|
+
console.log('Conflicts (modified in both):');
|
|
262
|
+
for (const file of conflicts) {
|
|
263
|
+
console.log(` !! ${file.path}`);
|
|
264
|
+
}
|
|
265
|
+
console.log('');
|
|
266
|
+
console.log('Use `mod diff <file>` to view differences.');
|
|
267
|
+
console.log('');
|
|
314
268
|
}
|
|
315
|
-
|
|
269
|
+
// Summary
|
|
270
|
+
const localCount = localModified.length + localAdded.length;
|
|
271
|
+
const wsCount = wsModified.length + wsAdded.length;
|
|
272
|
+
console.log(`${localCount} file${localCount === 1 ? '' : 's'} changed locally, ${wsCount} file${wsCount === 1 ? '' : 's'} changed in workspace`);
|
|
316
273
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
274
|
+
catch (error) {
|
|
275
|
+
console.error('Error checking status:', error.message);
|
|
276
|
+
process.exit(1);
|
|
320
277
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
278
|
+
}
|
|
279
|
+
function getStatusCode(status) {
|
|
280
|
+
switch (status) {
|
|
281
|
+
case 'modified-local':
|
|
282
|
+
return 'M ';
|
|
283
|
+
case 'modified-workspace':
|
|
284
|
+
return ' M';
|
|
285
|
+
case 'added-local':
|
|
286
|
+
return 'A ';
|
|
287
|
+
case 'added-workspace':
|
|
288
|
+
return ' A';
|
|
289
|
+
case 'deleted-local':
|
|
290
|
+
return 'D ';
|
|
291
|
+
case 'deleted-workspace':
|
|
292
|
+
return ' D';
|
|
293
|
+
case 'conflict':
|
|
294
|
+
return '!!';
|
|
327
295
|
}
|
|
328
|
-
return result;
|
|
329
296
|
}
|