@mod-computer/cli 0.1.1 → 0.2.1

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 (55) hide show
  1. package/README.md +72 -0
  2. package/dist/cli.bundle.js +23743 -12931
  3. package/dist/cli.bundle.js.map +4 -4
  4. package/dist/cli.js +23 -12
  5. package/dist/commands/add.js +245 -0
  6. package/dist/commands/auth.js +129 -21
  7. package/dist/commands/comment.js +568 -0
  8. package/dist/commands/diff.js +182 -0
  9. package/dist/commands/index.js +33 -3
  10. package/dist/commands/init.js +475 -221
  11. package/dist/commands/ls.js +135 -0
  12. package/dist/commands/members.js +687 -0
  13. package/dist/commands/mv.js +282 -0
  14. package/dist/commands/rm.js +257 -0
  15. package/dist/commands/status.js +273 -306
  16. package/dist/commands/sync.js +99 -75
  17. package/dist/commands/trace.js +1752 -0
  18. package/dist/commands/workspace.js +354 -330
  19. package/dist/config/features.js +8 -3
  20. package/dist/config/release-profiles/development.json +4 -1
  21. package/dist/config/release-profiles/mvp.json +4 -2
  22. package/dist/daemon/conflict-resolution.js +172 -0
  23. package/dist/daemon/content-hash.js +31 -0
  24. package/dist/daemon/file-sync.js +985 -0
  25. package/dist/daemon/index.js +203 -0
  26. package/dist/daemon/mime-types.js +166 -0
  27. package/dist/daemon/offline-queue.js +211 -0
  28. package/dist/daemon/path-utils.js +64 -0
  29. package/dist/daemon/share-policy.js +83 -0
  30. package/dist/daemon/wasm-errors.js +189 -0
  31. package/dist/daemon/worker.js +557 -0
  32. package/dist/daemon-worker.js +3 -2
  33. package/dist/errors/workspace-errors.js +48 -0
  34. package/dist/lib/auth-server.js +89 -26
  35. package/dist/lib/browser.js +1 -1
  36. package/dist/lib/diff.js +284 -0
  37. package/dist/lib/formatters.js +204 -0
  38. package/dist/lib/git.js +137 -0
  39. package/dist/lib/local-fs.js +201 -0
  40. package/dist/lib/prompts.js +23 -83
  41. package/dist/lib/storage.js +11 -1
  42. package/dist/lib/trace-formatters.js +314 -0
  43. package/dist/services/add-service.js +554 -0
  44. package/dist/services/add-validation.js +124 -0
  45. package/dist/services/mod-config.js +8 -2
  46. package/dist/services/modignore-service.js +2 -0
  47. package/dist/stores/use-workspaces-store.js +36 -14
  48. package/dist/types/add-types.js +99 -0
  49. package/dist/types/config.js +1 -1
  50. package/dist/types/workspace-connection.js +53 -2
  51. package/package.json +7 -5
  52. package/commands/execute.md +0 -156
  53. package/commands/overview.md +0 -233
  54. package/commands/review.md +0 -151
  55. package/commands/spec.md +0 -169
@@ -1,329 +1,296 @@
1
- // @spec status-command.spec.md/REQ-INT-1.1 Import SpecificationParserService from mod-core
2
- // @spec status-command.spec.md/REQ-INT-1.3 Reuse RequirementCategory enum and interfaces from mod-core types
3
- import { SpecificationParserService, RequirementCategory } from '@mod/mod-core';
4
- import * as fs from 'fs/promises';
5
- import * as path from 'path';
6
- import { log } from '../services/logger.js';
7
- // @spec status-command.spec.md/REQ-INT-2.2 Standard command signature
8
- // @spec status-command.spec.md/REQ-INFRA-2.1 Follow existing CLI command patterns from mod-cli package
9
- export async function statusCommand(args) {
10
- try {
11
- // @spec status-command.spec.md/REQ-BUS-2.1 Parse spec file argument
12
- if (args.length === 0) {
13
- console.error('Error: Specification file required');
14
- console.error('Usage: mod status <spec-file>');
15
- process.exit(1);
16
- }
17
- let specName = args[0];
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
- specFile: path.relative(workspaceRoot, specFilePath),
109
- title: parsed.metadata.title,
110
- requirements,
111
- summary
20
+ workspaceId: connection.workspaceId,
21
+ workspaceName: connection.workspaceName,
22
+ workspacePath: connection.path,
112
23
  };
113
24
  }
114
- // @spec status-command.spec.md/REQ-DATA-1.2 Extract nested requirements from bullet points
115
- function extractNestedRequirements(content, topLevelRequirements, frontmatterOffset) {
116
- // Adjust line numbers for top-level requirements to account for frontmatter
117
- const adjustedTopLevel = topLevelRequirements.map(req => ({
118
- ...req,
119
- lineRange: req.lineRange ? {
120
- start: req.lineRange.start + frontmatterOffset,
121
- end: req.lineRange.end + frontmatterOffset
122
- } : undefined
123
- }));
124
- const allRequirements = [...adjustedTopLevel];
125
- // @spec status-command.spec.md/REQ-BUS-1.3 Support all @spec comment patterns from spec-parser-utils.ts
126
- // Regular expression to find nested requirements in bullet points
127
- // Matches: - **REQ-CAT-N.N**: Description, - **REQ-CAT-N.N.N**: Description, etc.
128
- const nestedReqRegex = /^\s*-\s*\*\*(?<id>REQ-[A-Z]+-\d+(?:\.\d+)*)\*\*:\s*(?<description>.*)/gm;
129
- let match;
130
- while ((match = nestedReqRegex.exec(content)) !== null) {
131
- const groups = match.groups;
132
- if (!groups)
133
- continue;
134
- const { id, description } = groups;
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 allRequirements;
47
+ return { pathFilter, options };
161
48
  }
162
- // @spec status-command.spec.md/REQ-DATA-1.2 Determine parent requirement for hierarchy
163
- function determineParentRequirement(requirementId) {
164
- const parts = requirementId.split('-');
165
- if (parts.length < 3)
166
- return undefined;
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
- // @spec status-command.spec.md/REQ-DATA-1.1 Find specification file in workspace
175
- async function findSpecificationFile(workspaceRoot, specName) {
176
- const specsDir = path.join(workspaceRoot, '.mod/specs');
177
- // @spec status-command.spec.md/REQ-DATA-1.1 Search recursively in .mod/specs
178
- async function searchDir(dir) {
179
- try {
180
- const entries = await fs.readdir(dir, { withFileTypes: true });
181
- for (const entry of entries) {
182
- const fullPath = path.join(dir, entry.name);
183
- if (entry.isDirectory()) {
184
- const result = await searchDir(fullPath);
185
- if (result)
186
- return result;
187
- }
188
- else if (entry.name === specName) {
189
- return fullPath;
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
- catch {
194
- // Directory doesn't exist or can't be read
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
- return null;
197
- }
198
- const found = await searchDir(specsDir);
199
- if (!found) {
200
- throw new Error(`Specification file "${specName}" not found in ${specsDir}`);
201
- }
202
- return found;
203
- }
204
- // @spec status-command.spec.md/REQ-DATA-2.3 Filter traces by specification name
205
- function filterTracesBySpec(traceMap, specName) {
206
- const filtered = new Map();
207
- const specBaseName = specName.replace('.spec.md', '');
208
- for (const [requirementId, traces] of traceMap.entries()) {
209
- // @spec status-command.spec.md/REQ-DATA-2.2 Support spec-prefixed traces
210
- const relevantTraces = traces.filter(trace => {
211
- if (!trace.specPath)
212
- return true; // Legacy format without spec path
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
- return filtered;
220
- }
221
- // @spec status-command.spec.md/REQ-BUS-1.1 Status classification logic
222
- function getRequirementStatus(traceCount) {
223
- return traceCount === 0 ? 'todo' : 'traced';
224
- }
225
- // @spec status-command.spec.md/REQ-DATA-1.2 Calculate hierarchy level from requirement ID
226
- function calculateHierarchyLevel(requirementId) {
227
- const parts = requirementId.split('-');
228
- if (parts.length < 3)
229
- return 1; // REQ-CAT
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
- // If any child is todo, parent remains todo (current behavior is correct)
212
+ if (statusList.length === 0) {
213
+ console.log('No changes.');
214
+ }
215
+ return;
249
216
  }
250
- }
251
- }
252
- // @spec status-command.spec.md/REQ-UX-1.1 TypeScript compiler-style output formatting
253
- function printTscStyleOutput(analysis) {
254
- // @spec status-command.spec.md/REQ-UX-1.2 Status indicator mapping with colors
255
- const colors = {
256
- traced: '\x1b[32m', // Green
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
- // @spec status-command.spec.md/REQ-UX-1.1 Show spec location with actual line number
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
- console.log(); // Add spacing between sections
293
- }
294
- // @spec status-command.spec.md/REQ-UX-1.1 Color-coded summary line
295
- // @spec status-command.spec.md/REQ-UX-1.3.1 Color-coded summary line with trace counts and requirement status
296
- const tracedText = `${colors.traced}${analysis.summary.traced} traced${colors.reset}`;
297
- const todoText = `${colors.todo}${analysis.summary.todo} todo${colors.reset}`;
298
- const totalTracesText = `${colors.count}${analysis.summary.totalTraces} traces${colors.reset}`;
299
- console.log(`Found ${tracedText}, ${todoText} (${totalTracesText} total).`);
300
- }
301
- // @spec status-command.spec.md/REQ-DATA-1.2 Group requirements by category to maintain spec order
302
- function groupRequirementsByCategory(requirements) {
303
- const categoryOrder = ['UX', 'BUS', 'DATA', 'INT', 'INFRA', 'QUAL'];
304
- const grouped = new Map();
305
- // Initialize with empty arrays in correct order
306
- for (const category of categoryOrder) {
307
- grouped.set(category, []);
308
- }
309
- // Group requirements by category
310
- for (const req of requirements) {
311
- const category = req.category;
312
- if (!grouped.has(category)) {
313
- grouped.set(category, []);
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
- grouped.get(category).push(req);
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
- // Sort requirements within each category by ID
318
- for (const [, reqs] of grouped.entries()) {
319
- reqs.sort((a, b) => a.requirementId.localeCompare(b.requirementId));
274
+ catch (error) {
275
+ console.error('Error checking status:', error.message);
276
+ process.exit(1);
320
277
  }
321
- // Remove empty categories
322
- const result = new Map();
323
- for (const [category, reqs] of grouped.entries()) {
324
- if (reqs.length > 0) {
325
- result.set(category, reqs);
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
  }