@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.
- package/README.md +72 -0
- package/dist/cli.bundle.js +23743 -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 +8 -3
- 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/lib/auth-server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// glassware[type=implementation, id=
|
|
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
|
-
|
|
91
|
-
|
|
96
|
+
padding: 1rem;
|
|
97
|
+
background: #09090b;
|
|
98
|
+
color: #fafafa;
|
|
92
99
|
}
|
|
93
|
-
.
|
|
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
|
-
|
|
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="
|
|
107
|
-
<div class="
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
.
|
|
189
|
+
.message {
|
|
190
|
+
color: #a1a1aa;
|
|
191
|
+
font-size: 0.875rem;
|
|
131
192
|
text-align: center;
|
|
132
|
-
|
|
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="
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
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>`;
|
package/dist/lib/browser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// glassware[type=implementation, id=
|
|
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
|
/**
|
package/dist/lib/diff.js
ADDED
|
@@ -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
|
+
}
|