@mod-computer/cli 0.1.0 → 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 +24633 -13744
  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 +545 -326
  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 +56 -0
  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,4 +1,4 @@
1
- // glassware[type=implementation, id=cli-auth-server, requirements=req-cli-auth-app-2,req-cli-auth-app-4,req-cli-auth-qual-1]
1
+ // glassware[type="implementation", id="impl-auth-server--59aa04a6", specifications="specification-spec-localhost-server--6f8cb512,specification-spec-receive-callback--09de208c,specification-spec-server-timeout--163a7a48"]
2
2
  import http from 'http';
3
3
  import { URL } from 'url';
4
4
  const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
@@ -75,70 +75,133 @@ export async function startAuthServer(options = {}) {
75
75
  };
76
76
  }
77
77
  function getSuccessPage(name) {
78
+ const modLogo = `<svg width="48" height="54" viewBox="0 0 33 36" fill="none" xmlns="http://www.w3.org/2000/svg">
79
+ <path opacity="0.8" d="M16.4287 5.04502C16.4287 2.76982 18.8623 1.32269 20.8613 2.40918L31.2899 8.07724C32.2559 8.60226 32.8573 9.61363 32.8573 10.7131V21.9875C32.8573 24.2715 30.4066 25.7178 28.4072 24.6138L17.9786 18.8558C17.0224 18.3278 16.4287 17.3218 16.4287 16.2295V5.04502Z" fill="#FF2B00"/>
80
+ <path opacity="0.7" d="M8.14282 9.43857C8.14282 7.16338 10.5764 5.71625 12.5754 6.80274L23.004 12.4708C23.97 12.9958 24.5714 14.0072 24.5714 15.1066V26.3811C24.5714 28.6651 22.1208 30.1113 20.1213 29.0074L9.69275 23.2493C8.73652 22.7214 8.14282 21.7154 8.14282 20.6231V9.43857Z" fill="white"/>
81
+ <path opacity="0.8" d="M0 13.9742C0 11.699 2.4336 10.2519 4.43261 11.3384L14.8612 17.0064C15.8271 17.5315 16.4286 18.5428 16.4286 19.6423V30.9167C16.4286 33.2007 13.9779 34.647 11.9785 33.543L1.54993 27.785C0.593696 27.257 0 26.251 0 25.1587V13.9742Z" fill="#3671F1"/>
82
+ </svg>`;
78
83
  return `<!DOCTYPE html>
79
84
  <html>
80
85
  <head>
81
86
  <title>Signed in to Mod</title>
82
87
  <style>
88
+ * { box-sizing: border-box; }
83
89
  body {
84
90
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
85
91
  display: flex;
86
92
  justify-content: center;
87
93
  align-items: center;
88
- height: 100vh;
94
+ min-height: 100vh;
89
95
  margin: 0;
90
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
91
- color: white;
96
+ padding: 1rem;
97
+ background: #09090b;
98
+ color: #fafafa;
92
99
  }
93
- .container {
100
+ .card {
101
+ width: 100%;
102
+ max-width: 24rem;
103
+ background: #18181b;
104
+ border: 1px solid #27272a;
105
+ border-radius: 0.5rem;
106
+ padding: 1.5rem;
107
+ box-shadow: 0 1px 3px rgba(0,0,0,0.3);
108
+ }
109
+ .logo-container {
110
+ display: flex;
111
+ flex-direction: column;
112
+ align-items: center;
113
+ }
114
+ .logo {
115
+ margin-bottom: 1rem;
116
+ }
117
+ h1 {
118
+ font-size: 1.125rem;
119
+ font-weight: 600;
120
+ margin: 0 0 0.25rem 0;
121
+ text-align: center;
122
+ }
123
+ .subtitle {
124
+ color: #a1a1aa;
125
+ font-size: 0.875rem;
94
126
  text-align: center;
95
- padding: 2rem;
96
- background: rgba(255,255,255,0.1);
97
- border-radius: 12px;
98
- backdrop-filter: blur(10px);
127
+ margin: 0;
99
128
  }
100
- h1 { margin-bottom: 0.5rem; }
101
- p { opacity: 0.9; }
102
- .check { font-size: 3rem; margin-bottom: 1rem; }
103
129
  </style>
104
130
  </head>
105
131
  <body>
106
- <div class="container">
107
- <div class="check">✓</div>
108
- <h1>Welcome, ${escapeHtml(name)}!</h1>
109
- <p>You can close this window and return to your terminal.</p>
132
+ <div class="card">
133
+ <div class="logo-container">
134
+ <div class="logo">${modLogo}</div>
135
+ <h1>Welcome, ${escapeHtml(name)}!</h1>
136
+ <p class="subtitle">You can close this window and return to your terminal.</p>
137
+ </div>
110
138
  </div>
111
139
  </body>
112
140
  </html>`;
113
141
  }
114
142
  function getErrorPage(message) {
143
+ const modLogo = `<svg width="48" height="54" viewBox="0 0 33 36" fill="none" xmlns="http://www.w3.org/2000/svg">
144
+ <path opacity="0.8" d="M16.4287 5.04502C16.4287 2.76982 18.8623 1.32269 20.8613 2.40918L31.2899 8.07724C32.2559 8.60226 32.8573 9.61363 32.8573 10.7131V21.9875C32.8573 24.2715 30.4066 25.7178 28.4072 24.6138L17.9786 18.8558C17.0224 18.3278 16.4287 17.3218 16.4287 16.2295V5.04502Z" fill="#FF2B00"/>
145
+ <path opacity="0.7" d="M8.14282 9.43857C8.14282 7.16338 10.5764 5.71625 12.5754 6.80274L23.004 12.4708C23.97 12.9958 24.5714 14.0072 24.5714 15.1066V26.3811C24.5714 28.6651 22.1208 30.1113 20.1213 29.0074L9.69275 23.2493C8.73652 22.7214 8.14282 21.7154 8.14282 20.6231V9.43857Z" fill="white"/>
146
+ <path opacity="0.8" d="M0 13.9742C0 11.699 2.4336 10.2519 4.43261 11.3384L14.8612 17.0064C15.8271 17.5315 16.4286 18.5428 16.4286 19.6423V30.9167C16.4286 33.2007 13.9779 34.647 11.9785 33.543L1.54993 27.785C0.593696 27.257 0 26.251 0 25.1587V13.9742Z" fill="#3671F1"/>
147
+ </svg>`;
115
148
  return `<!DOCTYPE html>
116
149
  <html>
117
150
  <head>
118
151
  <title>Authentication Error</title>
119
152
  <style>
153
+ * { box-sizing: border-box; }
120
154
  body {
121
155
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
122
156
  display: flex;
123
157
  justify-content: center;
124
158
  align-items: center;
125
- height: 100vh;
159
+ min-height: 100vh;
126
160
  margin: 0;
127
- background: #1a1a2e;
128
- color: white;
161
+ padding: 1rem;
162
+ background: #09090b;
163
+ color: #fafafa;
164
+ }
165
+ .card {
166
+ width: 100%;
167
+ max-width: 24rem;
168
+ background: #18181b;
169
+ border: 1px solid #27272a;
170
+ border-radius: 0.5rem;
171
+ padding: 1.5rem;
172
+ box-shadow: 0 1px 3px rgba(0,0,0,0.3);
173
+ }
174
+ .logo-container {
175
+ display: flex;
176
+ flex-direction: column;
177
+ align-items: center;
178
+ }
179
+ .logo {
180
+ margin-bottom: 1rem;
181
+ }
182
+ h1 {
183
+ font-size: 1.125rem;
184
+ font-weight: 600;
185
+ margin: 0 0 0.5rem 0;
186
+ color: #ef4444;
187
+ text-align: center;
129
188
  }
130
- .container {
189
+ .message {
190
+ color: #a1a1aa;
191
+ font-size: 0.875rem;
131
192
  text-align: center;
132
- padding: 2rem;
193
+ margin: 0 0 0.25rem 0;
133
194
  }
134
- h1 { color: #ff6b6b; }
135
195
  </style>
136
196
  </head>
137
197
  <body>
138
- <div class="container">
139
- <h1>Authentication Error</h1>
140
- <p>${escapeHtml(message)}</p>
141
- <p>Please try again from your terminal.</p>
198
+ <div class="card">
199
+ <div class="logo-container">
200
+ <div class="logo">${modLogo}</div>
201
+ <h1>Authentication Error</h1>
202
+ <p class="message">${escapeHtml(message)}</p>
203
+ <p class="message">Please try again from your terminal.</p>
204
+ </div>
142
205
  </div>
143
206
  </body>
144
207
  </html>`;
@@ -1,4 +1,4 @@
1
- // glassware[type=implementation, id=cli-browser-opener, requirements=req-cli-auth-ux-2,req-cli-auth-ux-4]
1
+ // glassware[type="implementation", id="impl-browser-opener--15759c87", specifications="specification-spec-open-browser--397b6a28,specification-spec-manual-url--b37b2760"]
2
2
  import { exec } from 'child_process';
3
3
  import { platform } from 'os';
4
4
  /**
@@ -0,0 +1,284 @@
1
+ // glassware[type="implementation", id="impl-cli-fd-diff--22cb6c01", requirements="requirement-cli-fd-diff-text--54d22be4,requirement-cli-fd-diff-binary--4aeb6fda,requirement-cli-fd-diff-automerge--8310553b"]
2
+ // spec: packages/mod-cli/specs/file-directory.md
3
+ /**
4
+ * Extract text content from ModFile content structure
5
+ */
6
+ // glassware[type="implementation", id="impl-cli-fd-diff-automerge--19e08758", requirements="requirement-cli-fd-diff-automerge--8310553b"]
7
+ export function getWorkspaceContent(content) {
8
+ if (typeof content === 'string') {
9
+ return content;
10
+ }
11
+ if (content?.text) {
12
+ // TextFileContent or CodeFileContent
13
+ if (typeof content.text === 'string') {
14
+ return content.text;
15
+ }
16
+ // Automerge Text type - convert to string
17
+ if (content.text.toString) {
18
+ return content.text.toString();
19
+ }
20
+ }
21
+ return '';
22
+ }
23
+ /**
24
+ * Check if content appears to be binary
25
+ */
26
+ // glassware[type="implementation", id="impl-cli-fd-diff-binary--80add1e0", requirements="requirement-cli-fd-diff-binary--4aeb6fda"]
27
+ export function isBinaryContent(content) {
28
+ // Check for null bytes or high proportion of non-printable characters
29
+ let nonPrintable = 0;
30
+ const checkLength = Math.min(content.length, 8000);
31
+ for (let i = 0; i < checkLength; i++) {
32
+ const code = content.charCodeAt(i);
33
+ if (code === 0) {
34
+ return true; // Null byte = definitely binary
35
+ }
36
+ // Non-printable excluding common whitespace
37
+ if (code < 32 && code !== 9 && code !== 10 && code !== 13) {
38
+ nonPrintable++;
39
+ }
40
+ }
41
+ return nonPrintable / checkLength > 0.3;
42
+ }
43
+ /**
44
+ * Compute unified diff between two strings
45
+ */
46
+ // glassware[type="implementation", id="impl-cli-fd-diff-text--9c318f0e", requirements="requirement-cli-fd-diff-text--54d22be4"]
47
+ export function computeDiff(oldContent, newContent, contextLines = 3) {
48
+ const oldLines = oldContent.split('\n');
49
+ const newLines = newContent.split('\n');
50
+ // Simple LCS-based diff algorithm
51
+ const lcs = computeLCS(oldLines, newLines);
52
+ const hunks = [];
53
+ let oldIndex = 0;
54
+ let newIndex = 0;
55
+ let lcsIndex = 0;
56
+ let currentHunk = null;
57
+ while (oldIndex < oldLines.length || newIndex < newLines.length) {
58
+ // Check if current lines match LCS
59
+ const matchesLCS = lcsIndex < lcs.length &&
60
+ oldIndex < oldLines.length &&
61
+ newIndex < newLines.length &&
62
+ oldLines[oldIndex] === lcs[lcsIndex] &&
63
+ newLines[newIndex] === lcs[lcsIndex];
64
+ if (matchesLCS) {
65
+ // Context line
66
+ if (currentHunk) {
67
+ currentHunk.lines.push({
68
+ type: 'context',
69
+ content: oldLines[oldIndex],
70
+ oldLineNumber: oldIndex + 1,
71
+ newLineNumber: newIndex + 1,
72
+ });
73
+ }
74
+ oldIndex++;
75
+ newIndex++;
76
+ lcsIndex++;
77
+ }
78
+ else {
79
+ // Start a new hunk if needed
80
+ if (!currentHunk) {
81
+ const startOld = Math.max(0, oldIndex - contextLines);
82
+ const startNew = Math.max(0, newIndex - contextLines);
83
+ currentHunk = {
84
+ oldStart: startOld + 1,
85
+ oldLines: 0,
86
+ newStart: startNew + 1,
87
+ newLines: 0,
88
+ lines: [],
89
+ };
90
+ // Add leading context
91
+ for (let i = startOld; i < oldIndex; i++) {
92
+ currentHunk.lines.push({
93
+ type: 'context',
94
+ content: oldLines[i],
95
+ oldLineNumber: i + 1,
96
+ newLineNumber: startNew + (i - startOld) + 1,
97
+ });
98
+ }
99
+ }
100
+ // Handle deletions
101
+ while (oldIndex < oldLines.length &&
102
+ (lcsIndex >= lcs.length || oldLines[oldIndex] !== lcs[lcsIndex])) {
103
+ currentHunk.lines.push({
104
+ type: 'deletion',
105
+ content: oldLines[oldIndex],
106
+ oldLineNumber: oldIndex + 1,
107
+ });
108
+ oldIndex++;
109
+ }
110
+ // Handle additions
111
+ while (newIndex < newLines.length &&
112
+ (lcsIndex >= lcs.length || newLines[newIndex] !== lcs[lcsIndex])) {
113
+ currentHunk.lines.push({
114
+ type: 'addition',
115
+ content: newLines[newIndex],
116
+ newLineNumber: newIndex + 1,
117
+ });
118
+ newIndex++;
119
+ }
120
+ // Check if we should close the hunk (no more changes for a while)
121
+ const nextChange = findNextChange(oldLines, newLines, lcs, oldIndex, newIndex, lcsIndex);
122
+ if (nextChange > contextLines * 2 || (oldIndex >= oldLines.length && newIndex >= newLines.length)) {
123
+ // Add trailing context
124
+ const endContext = Math.min(contextLines, oldLines.length - oldIndex);
125
+ for (let i = 0; i < endContext; i++) {
126
+ if (oldIndex + i < oldLines.length && lcsIndex + i < lcs.length) {
127
+ currentHunk.lines.push({
128
+ type: 'context',
129
+ content: oldLines[oldIndex + i],
130
+ oldLineNumber: oldIndex + i + 1,
131
+ newLineNumber: newIndex + i + 1,
132
+ });
133
+ }
134
+ }
135
+ // Calculate hunk line counts
136
+ currentHunk.oldLines = currentHunk.lines.filter(l => l.type === 'context' || l.type === 'deletion').length;
137
+ currentHunk.newLines = currentHunk.lines.filter(l => l.type === 'context' || l.type === 'addition').length;
138
+ hunks.push(currentHunk);
139
+ currentHunk = null;
140
+ // Skip context we just added
141
+ oldIndex += endContext;
142
+ newIndex += endContext;
143
+ lcsIndex += endContext;
144
+ }
145
+ }
146
+ }
147
+ // Close any remaining hunk
148
+ if (currentHunk && currentHunk.lines.length > 0) {
149
+ currentHunk.oldLines = currentHunk.lines.filter(l => l.type === 'context' || l.type === 'deletion').length;
150
+ currentHunk.newLines = currentHunk.lines.filter(l => l.type === 'context' || l.type === 'addition').length;
151
+ hunks.push(currentHunk);
152
+ }
153
+ return hunks;
154
+ }
155
+ /**
156
+ * Compute LCS (Longest Common Subsequence) of two arrays
157
+ */
158
+ function computeLCS(a, b) {
159
+ const m = a.length;
160
+ const n = b.length;
161
+ // DP table
162
+ const dp = Array(m + 1)
163
+ .fill(null)
164
+ .map(() => Array(n + 1).fill(0));
165
+ for (let i = 1; i <= m; i++) {
166
+ for (let j = 1; j <= n; j++) {
167
+ if (a[i - 1] === b[j - 1]) {
168
+ dp[i][j] = dp[i - 1][j - 1] + 1;
169
+ }
170
+ else {
171
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
172
+ }
173
+ }
174
+ }
175
+ // Backtrack to find LCS
176
+ const lcs = [];
177
+ let i = m;
178
+ let j = n;
179
+ while (i > 0 && j > 0) {
180
+ if (a[i - 1] === b[j - 1]) {
181
+ lcs.unshift(a[i - 1]);
182
+ i--;
183
+ j--;
184
+ }
185
+ else if (dp[i - 1][j] > dp[i][j - 1]) {
186
+ i--;
187
+ }
188
+ else {
189
+ j--;
190
+ }
191
+ }
192
+ return lcs;
193
+ }
194
+ /**
195
+ * Find distance to next change
196
+ */
197
+ function findNextChange(oldLines, newLines, lcs, oldIndex, newIndex, lcsIndex) {
198
+ let count = 0;
199
+ while (oldIndex + count < oldLines.length &&
200
+ newIndex + count < newLines.length &&
201
+ lcsIndex + count < lcs.length &&
202
+ oldLines[oldIndex + count] === lcs[lcsIndex + count] &&
203
+ newLines[newIndex + count] === lcs[lcsIndex + count]) {
204
+ count++;
205
+ }
206
+ return count;
207
+ }
208
+ /**
209
+ * Format diff hunks as unified diff string
210
+ */
211
+ export function formatUnifiedDiff(oldPath, newPath, hunks, color = true) {
212
+ if (hunks.length === 0) {
213
+ return '';
214
+ }
215
+ const lines = [];
216
+ // Header
217
+ const oldHeader = `--- ${oldPath}`;
218
+ const newHeader = `+++ ${newPath}`;
219
+ lines.push(color ? `\x1b[1m${oldHeader}\x1b[0m` : oldHeader);
220
+ lines.push(color ? `\x1b[1m${newHeader}\x1b[0m` : newHeader);
221
+ for (const hunk of hunks) {
222
+ // Hunk header
223
+ const hunkHeader = `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`;
224
+ lines.push(color ? `\x1b[36m${hunkHeader}\x1b[0m` : hunkHeader);
225
+ for (const line of hunk.lines) {
226
+ switch (line.type) {
227
+ case 'context':
228
+ lines.push(` ${line.content}`);
229
+ break;
230
+ case 'deletion':
231
+ const delLine = `-${line.content}`;
232
+ lines.push(color ? `\x1b[31m${delLine}\x1b[0m` : delLine);
233
+ break;
234
+ case 'addition':
235
+ const addLine = `+${line.content}`;
236
+ lines.push(color ? `\x1b[32m${addLine}\x1b[0m` : addLine);
237
+ break;
238
+ }
239
+ }
240
+ }
241
+ return lines.join('\n');
242
+ }
243
+ /**
244
+ * Calculate diff statistics
245
+ */
246
+ export function calculateDiffStats(hunks) {
247
+ let additions = 0;
248
+ let deletions = 0;
249
+ for (const hunk of hunks) {
250
+ for (const line of hunk.lines) {
251
+ if (line.type === 'addition')
252
+ additions++;
253
+ if (line.type === 'deletion')
254
+ deletions++;
255
+ }
256
+ }
257
+ return { additions, deletions };
258
+ }
259
+ /**
260
+ * Format diffstat summary
261
+ */
262
+ export function formatDiffStat(files, color = true) {
263
+ const lines = [];
264
+ let totalAdditions = 0;
265
+ let totalDeletions = 0;
266
+ const maxPathLen = Math.max(...files.map(f => f.path.length), 20);
267
+ for (const file of files) {
268
+ const total = file.additions + file.deletions;
269
+ const plusStr = '+'.repeat(Math.min(file.additions, 20));
270
+ const minusStr = '-'.repeat(Math.min(file.deletions, 20));
271
+ let line = ` ${file.path.padEnd(maxPathLen)} | ${String(total).padStart(4)} `;
272
+ if (color) {
273
+ line += `\x1b[32m${plusStr}\x1b[0m\x1b[31m${minusStr}\x1b[0m`;
274
+ }
275
+ else {
276
+ line += `${plusStr}${minusStr}`;
277
+ }
278
+ lines.push(line);
279
+ totalAdditions += file.additions;
280
+ totalDeletions += file.deletions;
281
+ }
282
+ lines.push(` ${files.length} file${files.length === 1 ? '' : 's'} changed, ${totalAdditions} insertion${totalAdditions === 1 ? '' : 's'}(+), ${totalDeletions} deletion${totalDeletions === 1 ? '' : 's'}(-)`);
283
+ return lines.join('\n');
284
+ }
@@ -0,0 +1,204 @@
1
+ // glassware[type="implementation", id="impl-cli-fd-formatters--faafb955", requirements="requirement-cli-ls-output-default--6e584280,requirement-cli-ls-output-tree--43babfa8,requirement-cli-ls-output-json--d62469fe"]
2
+ // spec: packages/mod-cli/specs/file-directory.md
3
+ /**
4
+ * Format file size in human readable format
5
+ */
6
+ export function formatSize(bytes) {
7
+ if (bytes === 0)
8
+ return '0 B';
9
+ const units = ['B', 'KB', 'MB', 'GB'];
10
+ const k = 1024;
11
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
12
+ if (i === 0)
13
+ return `${bytes} B`;
14
+ const size = bytes / Math.pow(k, i);
15
+ return `${size.toFixed(1)} ${units[i]}`;
16
+ }
17
+ /**
18
+ * Format relative time
19
+ */
20
+ export function formatRelativeTime(isoString) {
21
+ const date = new Date(isoString);
22
+ const now = new Date();
23
+ const diffMs = now.getTime() - date.getTime();
24
+ const minutes = Math.floor(diffMs / (1000 * 60));
25
+ const hours = Math.floor(diffMs / (1000 * 60 * 60));
26
+ const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
27
+ if (minutes < 1)
28
+ return 'just now';
29
+ if (minutes < 60)
30
+ return `${minutes} min ago`;
31
+ if (hours < 24)
32
+ return `${hours} hour${hours === 1 ? '' : 's'} ago`;
33
+ if (days < 7)
34
+ return `${days} day${days === 1 ? '' : 's'} ago`;
35
+ return date.toLocaleDateString();
36
+ }
37
+ /**
38
+ * Format default table output for file list
39
+ */
40
+ // glassware[type="implementation", id="impl-cli-ls-output-default--8774cf55", requirements="requirement-cli-ls-output-default--6e584280"]
41
+ export function formatFileTable(files) {
42
+ if (files.length === 0) {
43
+ return 'No files in workspace.\n\nRun `mod init` to import files from this directory.';
44
+ }
45
+ const lines = [];
46
+ // Calculate column widths
47
+ const pathWidth = Math.max(4, Math.min(50, Math.max(...files.map(f => f.path.length))));
48
+ // Header
49
+ lines.push(`${'PATH'.padEnd(pathWidth)} ${'SIZE'.padStart(10)} MODIFIED`);
50
+ // Rows
51
+ for (const file of files) {
52
+ const path = file.path.length > pathWidth
53
+ ? '...' + file.path.slice(-(pathWidth - 3))
54
+ : file.path.padEnd(pathWidth);
55
+ const size = formatSize(file.size).padStart(10);
56
+ const modified = formatRelativeTime(file.updatedAt);
57
+ lines.push(`${path} ${size} ${modified}`);
58
+ }
59
+ // Summary
60
+ const totalSize = files.reduce((sum, f) => sum + f.size, 0);
61
+ lines.push('');
62
+ lines.push(`${files.length} file${files.length === 1 ? '' : 's'} (${formatSize(totalSize)} total)`);
63
+ return lines.join('\n');
64
+ }
65
+ function buildTree(files) {
66
+ const root = { name: '.', isFolder: true, children: new Map() };
67
+ for (const file of files) {
68
+ const parts = file.path.split('/');
69
+ let current = root;
70
+ for (let i = 0; i < parts.length; i++) {
71
+ const part = parts[i];
72
+ const isLast = i === parts.length - 1;
73
+ if (!current.children.has(part)) {
74
+ current.children.set(part, {
75
+ name: part,
76
+ isFolder: !isLast,
77
+ size: isLast ? file.size : undefined,
78
+ children: new Map(),
79
+ });
80
+ }
81
+ current = current.children.get(part);
82
+ }
83
+ }
84
+ return root;
85
+ }
86
+ /**
87
+ * Format tree output
88
+ */
89
+ // glassware[type="implementation", id="impl-cli-ls-output-tree--c098fff8", requirements="requirement-cli-ls-output-tree--43babfa8"]
90
+ export function formatFileTree(files) {
91
+ if (files.length === 0) {
92
+ return 'No files in workspace.\n\nRun `mod init` to import files from this directory.';
93
+ }
94
+ const tree = buildTree(files);
95
+ const lines = ['.'];
96
+ function renderNode(node, prefix, isLast) {
97
+ const children = Array.from(node.children.values());
98
+ // Sort: folders first, then alphabetically
99
+ children.sort((a, b) => {
100
+ if (a.isFolder !== b.isFolder)
101
+ return a.isFolder ? -1 : 1;
102
+ return a.name.localeCompare(b.name);
103
+ });
104
+ for (let i = 0; i < children.length; i++) {
105
+ const child = children[i];
106
+ const isLastChild = i === children.length - 1;
107
+ const connector = isLastChild ? '\\u2514\\u2500\\u2500 ' : '\\u251c\\u2500\\u2500 ';
108
+ const nextPrefix = prefix + (isLastChild ? ' ' : '\\u2502 ');
109
+ if (child.isFolder) {
110
+ lines.push(`${prefix}${connector}${child.name}/`);
111
+ renderNode(child, nextPrefix, isLastChild);
112
+ }
113
+ else {
114
+ const sizeStr = child.size !== undefined ? ` (${formatSize(child.size)})` : '';
115
+ lines.push(`${prefix}${connector}${child.name}${sizeStr}`);
116
+ }
117
+ }
118
+ }
119
+ renderNode(tree, '', true);
120
+ return lines.join('\n')
121
+ .replace(/\\u2514/g, '\u2514')
122
+ .replace(/\\u2500/g, '\u2500')
123
+ .replace(/\\u251c/g, '\u251c')
124
+ .replace(/\\u2502/g, '\u2502');
125
+ }
126
+ /**
127
+ * Format JSON output
128
+ */
129
+ // glassware[type="implementation", id="impl-cli-ls-output-json--75e73ac5", requirements="requirement-cli-ls-output-json--d62469fe"]
130
+ export function formatFileJson(files) {
131
+ const totalSize = files.reduce((sum, f) => sum + f.size, 0);
132
+ const output = {
133
+ files: files.map(f => ({
134
+ path: f.path,
135
+ size: f.size,
136
+ mimeType: f.mimeType,
137
+ updatedAt: f.updatedAt,
138
+ })),
139
+ totalFiles: files.length,
140
+ totalSize,
141
+ };
142
+ return JSON.stringify(output, null, 2);
143
+ }
144
+ /**
145
+ * Format file paths only (quiet mode)
146
+ */
147
+ export function formatFilePaths(files) {
148
+ return files.map(f => f.path).join('\n');
149
+ }
150
+ /**
151
+ * Format long/detailed output
152
+ */
153
+ export function formatFileLong(files) {
154
+ if (files.length === 0) {
155
+ return 'No files in workspace.';
156
+ }
157
+ const lines = [];
158
+ // Header
159
+ lines.push('ID PATH SIZE MIME TYPE');
160
+ for (const file of files) {
161
+ const id = file.id.slice(0, 36).padEnd(36);
162
+ const path = file.path.length > 32
163
+ ? '...' + file.path.slice(-29)
164
+ : file.path.padEnd(32);
165
+ const size = formatSize(file.size).padStart(10);
166
+ const mime = file.mimeType;
167
+ lines.push(`${id} ${path} ${size} ${mime}`);
168
+ }
169
+ return lines.join('\n');
170
+ }
171
+ /**
172
+ * Determine MIME type category
173
+ */
174
+ export function getMimeCategory(mimeType) {
175
+ if (mimeType.startsWith('text/')) {
176
+ if (mimeType.includes('typescript') ||
177
+ mimeType.includes('javascript') ||
178
+ mimeType.includes('python') ||
179
+ mimeType.includes('java') ||
180
+ mimeType.includes('c') ||
181
+ mimeType.includes('rust') ||
182
+ mimeType.includes('go')) {
183
+ return 'code';
184
+ }
185
+ return 'text';
186
+ }
187
+ if (mimeType.startsWith('application/json') ||
188
+ mimeType.includes('yaml') ||
189
+ mimeType.includes('xml') ||
190
+ mimeType.includes('csv')) {
191
+ return 'data';
192
+ }
193
+ if (mimeType.includes('javascript') ||
194
+ mimeType.includes('typescript')) {
195
+ return 'code';
196
+ }
197
+ if (mimeType.startsWith('image/') ||
198
+ mimeType.startsWith('audio/') ||
199
+ mimeType.startsWith('video/') ||
200
+ mimeType === 'application/octet-stream') {
201
+ return 'binary';
202
+ }
203
+ return 'unknown';
204
+ }