opencode-miniterm 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierrc +16 -0
- package/AGENTS.md +243 -0
- package/README.md +5 -0
- package/bun.lock +108 -0
- package/package.json +34 -0
- package/src/ansi.ts +22 -0
- package/src/commands/agents.ts +178 -0
- package/src/commands/debug.ts +74 -0
- package/src/commands/details.ts +17 -0
- package/src/commands/diff.ts +155 -0
- package/src/commands/exit.ts +17 -0
- package/src/commands/init.ts +35 -0
- package/src/commands/kill.ts +33 -0
- package/src/commands/log.ts +24 -0
- package/src/commands/models.ts +218 -0
- package/src/commands/new.ts +42 -0
- package/src/commands/page.ts +78 -0
- package/src/commands/quit.ts +17 -0
- package/src/commands/run.ts +34 -0
- package/src/commands/sessions.ts +257 -0
- package/src/commands/undo.ts +65 -0
- package/src/config.ts +56 -0
- package/src/index.ts +990 -0
- package/src/render.ts +320 -0
- package/src/types.ts +11 -0
- package/test/render.test.ts +390 -0
- package/test/test.ts +115 -0
- package/tsconfig.json +29 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,990 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { createOpencodeClient } from "@opencode-ai/sdk";
|
|
4
|
+
import type { Event, FileDiff, Part, Todo, ToolPart } from "@opencode-ai/sdk";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { mkdir } from "node:fs/promises";
|
|
7
|
+
import { glob } from "node:fs/promises";
|
|
8
|
+
import { stat } from "node:fs/promises";
|
|
9
|
+
import { open } from "node:fs/promises";
|
|
10
|
+
import readline from "node:readline";
|
|
11
|
+
import * as ansi from "./ansi";
|
|
12
|
+
import agentsCommand from "./commands/agents";
|
|
13
|
+
import debugCommand from "./commands/debug";
|
|
14
|
+
import detailsCommand from "./commands/details";
|
|
15
|
+
import diffCommand from "./commands/diff";
|
|
16
|
+
import exitCommand from "./commands/exit";
|
|
17
|
+
import initCommand from "./commands/init";
|
|
18
|
+
import killCommand from "./commands/kill";
|
|
19
|
+
import logCommand, { isLoggingEnabled } from "./commands/log";
|
|
20
|
+
import modelsCommand from "./commands/models";
|
|
21
|
+
import newCommand from "./commands/new";
|
|
22
|
+
import pageCommand from "./commands/page";
|
|
23
|
+
import quitCommand from "./commands/quit";
|
|
24
|
+
import runCommand from "./commands/run";
|
|
25
|
+
import sessionsCommand from "./commands/sessions";
|
|
26
|
+
import undoCommand from "./commands/undo";
|
|
27
|
+
import { config, loadConfig, saveConfig } from "./config";
|
|
28
|
+
import { getActiveDisplay, render, startAnimation, stopAnimation, writePrompt } from "./render";
|
|
29
|
+
|
|
30
|
+
const SERVER_URL = "http://127.0.0.1:4096";
|
|
31
|
+
const AUTH_USERNAME = process.env.OPENCODE_SERVER_USERNAME || "opencode";
|
|
32
|
+
const AUTH_PASSWORD = process.env.OPENCODE_SERVER_PASSWORD || "";
|
|
33
|
+
|
|
34
|
+
const SLASH_COMMANDS = [
|
|
35
|
+
initCommand,
|
|
36
|
+
agentsCommand,
|
|
37
|
+
modelsCommand,
|
|
38
|
+
sessionsCommand,
|
|
39
|
+
newCommand,
|
|
40
|
+
undoCommand,
|
|
41
|
+
detailsCommand,
|
|
42
|
+
diffCommand,
|
|
43
|
+
debugCommand,
|
|
44
|
+
logCommand,
|
|
45
|
+
pageCommand,
|
|
46
|
+
killCommand,
|
|
47
|
+
exitCommand,
|
|
48
|
+
quitCommand,
|
|
49
|
+
runCommand,
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
let client: ReturnType<typeof createOpencodeClient>;
|
|
53
|
+
|
|
54
|
+
let processing = true;
|
|
55
|
+
let retryInterval: ReturnType<typeof setInterval> | null = null;
|
|
56
|
+
let messageAbortController: AbortController | null = null;
|
|
57
|
+
|
|
58
|
+
interface AccumulatedPart {
|
|
59
|
+
key: string;
|
|
60
|
+
title: "thinking" | "response" | "tool" | "files" | "todo";
|
|
61
|
+
text: string;
|
|
62
|
+
active?: boolean;
|
|
63
|
+
durationMs?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface State {
|
|
67
|
+
sessionID: string;
|
|
68
|
+
renderedLines: string[];
|
|
69
|
+
accumulatedResponse: AccumulatedPart[];
|
|
70
|
+
allEvents: Event[];
|
|
71
|
+
write: (text: string) => void;
|
|
72
|
+
lastFileAfter: Map<string, string>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export { updateSessionTitle, setTerminalTitle };
|
|
76
|
+
|
|
77
|
+
let state: State = {
|
|
78
|
+
sessionID: "",
|
|
79
|
+
renderedLines: [],
|
|
80
|
+
accumulatedResponse: [],
|
|
81
|
+
allEvents: [],
|
|
82
|
+
write: (text) => process.stdout.write(text),
|
|
83
|
+
lastFileAfter: new Map(),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
let logFile: Awaited<ReturnType<typeof open>> | null = null;
|
|
87
|
+
let logFilePath: string | null = null;
|
|
88
|
+
|
|
89
|
+
// ====================
|
|
90
|
+
// MAIN ENTRY POINT
|
|
91
|
+
// ====================
|
|
92
|
+
|
|
93
|
+
async function main() {
|
|
94
|
+
loadConfig();
|
|
95
|
+
|
|
96
|
+
const serverProcess = await startOpenCodeServer();
|
|
97
|
+
|
|
98
|
+
client = createOpencodeClient({
|
|
99
|
+
baseUrl: SERVER_URL,
|
|
100
|
+
headers: AUTH_PASSWORD
|
|
101
|
+
? {
|
|
102
|
+
Authorization: `Basic ${Buffer.from(`${AUTH_USERNAME}:${AUTH_PASSWORD}`).toString("base64")}`,
|
|
103
|
+
}
|
|
104
|
+
: undefined,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
let isNewSession = false;
|
|
109
|
+
|
|
110
|
+
const initialSessionID = config.sessionID;
|
|
111
|
+
if (!initialSessionID || !(await validateSession(initialSessionID))) {
|
|
112
|
+
state.sessionID = await createSession();
|
|
113
|
+
isNewSession = true;
|
|
114
|
+
config.sessionID = state.sessionID;
|
|
115
|
+
saveConfig();
|
|
116
|
+
} else {
|
|
117
|
+
state.sessionID = initialSessionID;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
startEventListener();
|
|
121
|
+
|
|
122
|
+
await updateSessionTitle();
|
|
123
|
+
|
|
124
|
+
const activeDisplay = await getActiveDisplay(client);
|
|
125
|
+
|
|
126
|
+
process.stdout.write(`${ansi.CLEAR_SCREEN_UP}${ansi.CLEAR_FROM_CURSOR}`);
|
|
127
|
+
process.stdout.write(ansi.CURSOR_HOME);
|
|
128
|
+
console.log(activeDisplay);
|
|
129
|
+
if (!isNewSession) {
|
|
130
|
+
console.log("Resumed last session");
|
|
131
|
+
}
|
|
132
|
+
console.log();
|
|
133
|
+
console.log(`${ansi.BRIGHT_BLACK}Ask anything...${ansi.RESET}\n`);
|
|
134
|
+
|
|
135
|
+
const rl = readline.createInterface({
|
|
136
|
+
input: process.stdin,
|
|
137
|
+
output: process.stdout,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
readline.emitKeypressEvents(process.stdin);
|
|
141
|
+
if (process.stdin.setRawMode) {
|
|
142
|
+
process.stdin.setRawMode(true);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let inputBuffer = "";
|
|
146
|
+
let cursorPosition = 0;
|
|
147
|
+
let completions: string[] = [];
|
|
148
|
+
let history: string[] = [];
|
|
149
|
+
let historyIndex = -1;
|
|
150
|
+
let selectedCompletion = 0;
|
|
151
|
+
let showCompletions = false;
|
|
152
|
+
let completionCycling = false;
|
|
153
|
+
|
|
154
|
+
const getCompletions = async (text: string): Promise<string[]> => {
|
|
155
|
+
if (text.startsWith("/")) {
|
|
156
|
+
return ["/help", ...SLASH_COMMANDS.map((c) => c.name)].filter((cmd) =>
|
|
157
|
+
cmd.startsWith(text),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const atMatch = text.match(/(@[^\s]*)$/);
|
|
162
|
+
if (atMatch) {
|
|
163
|
+
const prefix = atMatch[0]!;
|
|
164
|
+
const searchPattern = prefix.slice(1);
|
|
165
|
+
const pattern = searchPattern.includes("/")
|
|
166
|
+
? searchPattern + "*"
|
|
167
|
+
: "**/" + searchPattern + "*";
|
|
168
|
+
const files = await getFileCompletions(pattern);
|
|
169
|
+
return files.map((file: string) => text.replace(/@[^\s]*$/, "@" + file));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return [];
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
let oldWrappedRows = 0;
|
|
176
|
+
const renderLine = (): void => {
|
|
177
|
+
const consoleWidth = process.stdout.columns || 80;
|
|
178
|
+
const totalLength = 2 + inputBuffer.length + 1;
|
|
179
|
+
const wrappedRows = Math.floor(totalLength / consoleWidth);
|
|
180
|
+
readline.cursorTo(process.stdout, 0);
|
|
181
|
+
if (oldWrappedRows > 0) {
|
|
182
|
+
readline.moveCursor(process.stdout, 0, -oldWrappedRows);
|
|
183
|
+
}
|
|
184
|
+
readline.clearScreenDown(process.stdout);
|
|
185
|
+
oldWrappedRows = wrappedRows;
|
|
186
|
+
|
|
187
|
+
writePrompt();
|
|
188
|
+
process.stdout.write(inputBuffer);
|
|
189
|
+
|
|
190
|
+
const totalPosition = 2 + cursorPosition;
|
|
191
|
+
const targetRow = Math.floor(totalPosition / consoleWidth);
|
|
192
|
+
const targetCol = totalPosition % consoleWidth;
|
|
193
|
+
|
|
194
|
+
const endCol = (2 + inputBuffer.length) % consoleWidth;
|
|
195
|
+
const endRow = Math.floor((2 + inputBuffer.length) / consoleWidth);
|
|
196
|
+
|
|
197
|
+
const deltaCol = targetCol - endCol;
|
|
198
|
+
let deltaRow = targetRow - endRow;
|
|
199
|
+
if (deltaCol !== 0 && endCol === 0) {
|
|
200
|
+
deltaRow += 1;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
readline.moveCursor(process.stdout, deltaCol, deltaRow);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const handleTab = async (): Promise<void> => {
|
|
207
|
+
const potentialCompletions = await getCompletions(inputBuffer);
|
|
208
|
+
|
|
209
|
+
if (potentialCompletions.length === 0) {
|
|
210
|
+
completionCycling = false;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!completionCycling) {
|
|
215
|
+
completions = potentialCompletions;
|
|
216
|
+
selectedCompletion = 0;
|
|
217
|
+
completionCycling = true;
|
|
218
|
+
inputBuffer = completions[0]!;
|
|
219
|
+
cursorPosition = inputBuffer.length;
|
|
220
|
+
renderLine();
|
|
221
|
+
} else {
|
|
222
|
+
selectedCompletion = (selectedCompletion + 1) % completions.length;
|
|
223
|
+
inputBuffer = completions[selectedCompletion]!;
|
|
224
|
+
cursorPosition = inputBuffer.length;
|
|
225
|
+
renderLine();
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const acceptInput = async (): Promise<void> => {
|
|
230
|
+
process.stdout.write("\n");
|
|
231
|
+
|
|
232
|
+
const input = inputBuffer.trim();
|
|
233
|
+
|
|
234
|
+
inputBuffer = "";
|
|
235
|
+
cursorPosition = 0;
|
|
236
|
+
showCompletions = false;
|
|
237
|
+
completionCycling = false;
|
|
238
|
+
completions = [];
|
|
239
|
+
|
|
240
|
+
if (input) {
|
|
241
|
+
if (history[history.length - 1] !== input) {
|
|
242
|
+
history.push(input);
|
|
243
|
+
}
|
|
244
|
+
historyIndex = history.length;
|
|
245
|
+
try {
|
|
246
|
+
if (input === "/help") {
|
|
247
|
+
const maxCommandLength = Math.max(...SLASH_COMMANDS.map((c) => c.name.length));
|
|
248
|
+
for (const cmd of SLASH_COMMANDS) {
|
|
249
|
+
const padding = " ".repeat(maxCommandLength - cmd.name.length + 2);
|
|
250
|
+
console.log(
|
|
251
|
+
` ${ansi.BRIGHT_WHITE}${cmd.name}${ansi.RESET}${padding}${ansi.BRIGHT_BLACK}${cmd.description}${ansi.RESET}`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
console.log();
|
|
255
|
+
return;
|
|
256
|
+
} else if (input.startsWith("/")) {
|
|
257
|
+
const parts = input.match(/(\/[^\s]+)\s*(.*)/)!;
|
|
258
|
+
if (parts) {
|
|
259
|
+
const commandName = parts[1];
|
|
260
|
+
const extra = parts[2]?.trim();
|
|
261
|
+
for (let command of SLASH_COMMANDS) {
|
|
262
|
+
if (command.name === commandName) {
|
|
263
|
+
await command.run(client, state, extra);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
process.stdout.write(ansi.CURSOR_HIDE);
|
|
272
|
+
startAnimation();
|
|
273
|
+
if (isLoggingEnabled()) {
|
|
274
|
+
console.log(`📝 ${ansi.BRIGHT_BLACK}Logging to ${getLogDir()}\n${ansi.RESET}`);
|
|
275
|
+
}
|
|
276
|
+
await sendMessage(state.sessionID, input);
|
|
277
|
+
} catch (error: any) {
|
|
278
|
+
if (error.message !== "Request cancelled") {
|
|
279
|
+
stopAnimation();
|
|
280
|
+
console.error("Error:", error.message);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!SLASH_COMMANDS.find((c) => c.running)) {
|
|
286
|
+
writePrompt();
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
process.stdin.on("keypress", async (str, key) => {
|
|
291
|
+
for (let command of SLASH_COMMANDS) {
|
|
292
|
+
if (command.running && command.handleKey) {
|
|
293
|
+
await command.handleKey(client, key, str);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
switch (key.name) {
|
|
299
|
+
case "up": {
|
|
300
|
+
if (history.length > 0) {
|
|
301
|
+
if (historyIndex > 0) {
|
|
302
|
+
historyIndex--;
|
|
303
|
+
}
|
|
304
|
+
inputBuffer = history[historyIndex]!;
|
|
305
|
+
cursorPosition = inputBuffer.length;
|
|
306
|
+
renderLine();
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
case "down": {
|
|
311
|
+
if (history.length > 0) {
|
|
312
|
+
if (historyIndex < history.length - 1) {
|
|
313
|
+
historyIndex++;
|
|
314
|
+
} else {
|
|
315
|
+
historyIndex = history.length;
|
|
316
|
+
inputBuffer = "";
|
|
317
|
+
}
|
|
318
|
+
cursorPosition = inputBuffer.length;
|
|
319
|
+
renderLine();
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
case "tab": {
|
|
324
|
+
if (!completionCycling) {
|
|
325
|
+
await handleTab();
|
|
326
|
+
}
|
|
327
|
+
if (completionCycling && completions.length > 0) {
|
|
328
|
+
await handleTab();
|
|
329
|
+
}
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
case "escape": {
|
|
333
|
+
if (messageAbortController) {
|
|
334
|
+
messageAbortController.abort();
|
|
335
|
+
stopAnimation();
|
|
336
|
+
process.stdout.write(ansi.CURSOR_SHOW);
|
|
337
|
+
process.stdout.write(`\r${ansi.BRIGHT_BLACK}Cancelled request${ansi.RESET}\n`);
|
|
338
|
+
} else {
|
|
339
|
+
inputBuffer = "";
|
|
340
|
+
cursorPosition = 0;
|
|
341
|
+
showCompletions = false;
|
|
342
|
+
completionCycling = false;
|
|
343
|
+
completions = [];
|
|
344
|
+
readline.cursorTo(process.stdout, 0);
|
|
345
|
+
readline.clearScreenDown(process.stdout);
|
|
346
|
+
writePrompt();
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
case "return": {
|
|
351
|
+
await acceptInput();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
case "backspace": {
|
|
355
|
+
if (cursorPosition > 0) {
|
|
356
|
+
inputBuffer =
|
|
357
|
+
inputBuffer.slice(0, cursorPosition - 1) + inputBuffer.slice(cursorPosition);
|
|
358
|
+
cursorPosition--;
|
|
359
|
+
}
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
case "delete": {
|
|
363
|
+
if (cursorPosition < inputBuffer.length) {
|
|
364
|
+
inputBuffer =
|
|
365
|
+
inputBuffer.slice(0, cursorPosition) + inputBuffer.slice(cursorPosition + 1);
|
|
366
|
+
}
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
case "left": {
|
|
370
|
+
if (key.meta) {
|
|
371
|
+
cursorPosition = findPreviousWordBoundary(inputBuffer, cursorPosition);
|
|
372
|
+
} else if (cursorPosition > 0) {
|
|
373
|
+
cursorPosition--;
|
|
374
|
+
}
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
case "right": {
|
|
378
|
+
if (key.meta) {
|
|
379
|
+
cursorPosition = findNextWordBoundary(inputBuffer, cursorPosition);
|
|
380
|
+
} else if (cursorPosition < inputBuffer.length) {
|
|
381
|
+
cursorPosition++;
|
|
382
|
+
}
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
default: {
|
|
386
|
+
if (str) {
|
|
387
|
+
inputBuffer =
|
|
388
|
+
inputBuffer.slice(0, cursorPosition) + str + inputBuffer.slice(cursorPosition);
|
|
389
|
+
cursorPosition += str.length;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
showCompletions = false;
|
|
395
|
+
completionCycling = false;
|
|
396
|
+
completions = [];
|
|
397
|
+
renderLine();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
writePrompt();
|
|
401
|
+
} catch (error: any) {
|
|
402
|
+
console.error("Error:", error.message);
|
|
403
|
+
serverProcess.kill();
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ====================
|
|
409
|
+
// SERVER COMMUNICATION
|
|
410
|
+
// ====================
|
|
411
|
+
|
|
412
|
+
async function startOpenCodeServer() {
|
|
413
|
+
const serverProcess = spawn("opencode", ["serve"], {
|
|
414
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
415
|
+
shell: true,
|
|
416
|
+
cwd: process.cwd(),
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
let started = false;
|
|
420
|
+
|
|
421
|
+
console.log(`\n${ansi.BRIGHT_BLACK}Starting OpenCode server...${ansi.RESET}\n`);
|
|
422
|
+
|
|
423
|
+
serverProcess.stdout.on("data", (data) => {
|
|
424
|
+
if (!started) {
|
|
425
|
+
process.stdout.write(`${ansi.CLEAR_SCREEN_UP}${ansi.CLEAR_FROM_CURSOR}`);
|
|
426
|
+
process.stdout.write(ansi.CURSOR_HOME);
|
|
427
|
+
started = true;
|
|
428
|
+
console.log(`${ansi.BRIGHT_BLACK}Server started, connecting...${ansi.RESET}\n`);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
serverProcess.on("error", (error) => {
|
|
433
|
+
console.error("Failed to start OpenCode server:", error.message);
|
|
434
|
+
process.exit(1);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
serverProcess.on("exit", (code) => {
|
|
438
|
+
console.log(`OpenCode server exited with code ${code}`);
|
|
439
|
+
process.exit(0);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
process.on("SIGINT", () => {
|
|
443
|
+
console.log("\nShutting down...");
|
|
444
|
+
saveConfig();
|
|
445
|
+
serverProcess.kill("SIGINT");
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
449
|
+
return serverProcess;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function createSession(): Promise<string> {
|
|
453
|
+
const result = await client.session.create({
|
|
454
|
+
body: {},
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
if (result.error) {
|
|
458
|
+
if (result.response.status === 401 && !AUTH_PASSWORD) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
"Server requires authentication. Set OPENCODE_SERVER_PASSWORD environment variable.",
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
throw new Error(
|
|
464
|
+
`Failed to create session (${result.response.status}): ${JSON.stringify(result.error)}`,
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return result.data.id;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function validateSession(sessionID: string): Promise<boolean> {
|
|
472
|
+
try {
|
|
473
|
+
const result = await client.session.get({
|
|
474
|
+
path: { id: sessionID },
|
|
475
|
+
});
|
|
476
|
+
return !result.error && result.response.status === 200;
|
|
477
|
+
} catch {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function updateSessionTitle(): Promise<void> {
|
|
483
|
+
try {
|
|
484
|
+
const result = await client.session.get({
|
|
485
|
+
path: { id: state.sessionID },
|
|
486
|
+
});
|
|
487
|
+
if (!result.error && result.data?.title) {
|
|
488
|
+
setTerminalTitle(result.data.title);
|
|
489
|
+
} else {
|
|
490
|
+
setTerminalTitle(state.sessionID.substring(0, 8));
|
|
491
|
+
}
|
|
492
|
+
} catch {
|
|
493
|
+
setTerminalTitle(state.sessionID.substring(0, 8));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function startEventListener(): Promise<void> {
|
|
498
|
+
try {
|
|
499
|
+
const { stream } = await client.event.subscribe({
|
|
500
|
+
onSseError: (error) => {
|
|
501
|
+
console.error(
|
|
502
|
+
`\n${ansi.RED}Connection error:${ansi.RESET}`,
|
|
503
|
+
error instanceof Error ? error.message : String(error),
|
|
504
|
+
);
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
for await (const event of stream) {
|
|
509
|
+
try {
|
|
510
|
+
await processEvent(event);
|
|
511
|
+
} catch (error) {
|
|
512
|
+
console.error(
|
|
513
|
+
`\n${ansi.RED}Event processing error:${ansi.RESET}`,
|
|
514
|
+
error instanceof Error ? error.message : String(error),
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
} catch (error) {
|
|
519
|
+
console.error(
|
|
520
|
+
`\n${ansi.RED}Failed to connect to event stream:${ansi.RESET}`,
|
|
521
|
+
error instanceof Error ? error.message : String(error),
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function sendMessage(sessionID: string, message: string) {
|
|
527
|
+
processing = false;
|
|
528
|
+
state.accumulatedResponse = [];
|
|
529
|
+
state.allEvents = [];
|
|
530
|
+
state.renderedLines = [];
|
|
531
|
+
|
|
532
|
+
await createLogFile();
|
|
533
|
+
|
|
534
|
+
await writeToLog(`User: ${message}\n\n`);
|
|
535
|
+
|
|
536
|
+
messageAbortController = new AbortController();
|
|
537
|
+
|
|
538
|
+
const requestStartTime = Date.now();
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
const result = await client.session.prompt({
|
|
542
|
+
path: { id: sessionID },
|
|
543
|
+
body: {
|
|
544
|
+
model: {
|
|
545
|
+
providerID: config.providerID,
|
|
546
|
+
modelID: config.modelID,
|
|
547
|
+
},
|
|
548
|
+
parts: [{ type: "text", text: message }],
|
|
549
|
+
},
|
|
550
|
+
signal: messageAbortController.signal,
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
if (result.error) {
|
|
554
|
+
throw new Error(
|
|
555
|
+
`Failed to send message (${result.response.status}): ${JSON.stringify(result.error)}`,
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Play a chime when request is completed
|
|
560
|
+
process.stdout.write("\x07");
|
|
561
|
+
|
|
562
|
+
stopAnimation();
|
|
563
|
+
|
|
564
|
+
const duration = Date.now() - requestStartTime;
|
|
565
|
+
const durationText = formatDuration(duration);
|
|
566
|
+
console.log(`${ansi.BRIGHT_BLACK}Completed in ${durationText}${ansi.RESET}\n`);
|
|
567
|
+
|
|
568
|
+
writePrompt();
|
|
569
|
+
readline.cursorTo(process.stdout, 2);
|
|
570
|
+
} catch (error: any) {
|
|
571
|
+
if (error.name === "AbortError" || messageAbortController?.signal.aborted) {
|
|
572
|
+
throw new Error("Request cancelled");
|
|
573
|
+
}
|
|
574
|
+
throw error;
|
|
575
|
+
} finally {
|
|
576
|
+
messageAbortController = null;
|
|
577
|
+
await closeLogFile();
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ====================
|
|
582
|
+
// EVENT PROCESSING
|
|
583
|
+
// ====================
|
|
584
|
+
|
|
585
|
+
async function processEvent(event: Event): Promise<void> {
|
|
586
|
+
if (retryInterval && event.type !== "session.status") {
|
|
587
|
+
clearInterval(retryInterval);
|
|
588
|
+
retryInterval = null;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
state.allEvents.push(event);
|
|
592
|
+
|
|
593
|
+
switch (event.type) {
|
|
594
|
+
case "message.part.updated": {
|
|
595
|
+
const part = event.properties.part;
|
|
596
|
+
const delta = event.properties.delta;
|
|
597
|
+
if (part) {
|
|
598
|
+
await processPart(part);
|
|
599
|
+
}
|
|
600
|
+
if (delta !== undefined && part) {
|
|
601
|
+
processDelta(part.id, delta);
|
|
602
|
+
}
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// @ts-ignore this definitely exists
|
|
607
|
+
case "message.part.delta": {
|
|
608
|
+
// @ts-ignore
|
|
609
|
+
const partID = event.properties.partID;
|
|
610
|
+
// @ts-ignore
|
|
611
|
+
const delta = event.properties.delta;
|
|
612
|
+
if (partID !== undefined && delta !== undefined) {
|
|
613
|
+
processDelta(partID, delta);
|
|
614
|
+
}
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
case "session.diff": {
|
|
619
|
+
const diff = event.properties.diff;
|
|
620
|
+
if (diff && diff.length > 0) {
|
|
621
|
+
await processDiff(diff);
|
|
622
|
+
}
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
case "session.idle":
|
|
627
|
+
case "session.status":
|
|
628
|
+
if (event.type === "session.status" && event.properties.status.type === "idle") {
|
|
629
|
+
stopAnimation();
|
|
630
|
+
process.stdout.write(ansi.CURSOR_SHOW);
|
|
631
|
+
if (retryInterval) {
|
|
632
|
+
clearInterval(retryInterval);
|
|
633
|
+
retryInterval = null;
|
|
634
|
+
}
|
|
635
|
+
writePrompt();
|
|
636
|
+
readline.cursorTo(process.stdout, 2);
|
|
637
|
+
}
|
|
638
|
+
if (event.type === "session.status" && event.properties.status.type === "retry") {
|
|
639
|
+
const message = event.properties.status.message;
|
|
640
|
+
const retryTime = event.properties.status.next;
|
|
641
|
+
const sessionID = event.properties.sessionID;
|
|
642
|
+
console.error(`\n${ansi.RED}Error:${ansi.RESET} ${message}`);
|
|
643
|
+
console.error(`${ansi.BRIGHT_BLACK}Session:${ansi.RESET} ${sessionID}`);
|
|
644
|
+
if (retryTime) {
|
|
645
|
+
if (retryInterval) {
|
|
646
|
+
clearInterval(retryInterval);
|
|
647
|
+
}
|
|
648
|
+
const retryDate = new Date(retryTime);
|
|
649
|
+
|
|
650
|
+
let lastSeconds = Math.max(0, Math.ceil((retryDate.getTime() - Date.now()) / 1000));
|
|
651
|
+
console.error(`${ansi.BRIGHT_BLACK}Retrying in ${lastSeconds}s...${ansi.RESET}`);
|
|
652
|
+
|
|
653
|
+
retryInterval = setInterval(() => {
|
|
654
|
+
const remaining = Math.max(0, Math.ceil((retryDate.getTime() - Date.now()) / 1000));
|
|
655
|
+
if (remaining !== lastSeconds) {
|
|
656
|
+
process.stdout.write(
|
|
657
|
+
`\r${ansi.BRIGHT_BLACK}Retrying in ${remaining}s...${ansi.RESET}`,
|
|
658
|
+
);
|
|
659
|
+
lastSeconds = remaining;
|
|
660
|
+
}
|
|
661
|
+
if (remaining === 0) {
|
|
662
|
+
if (retryInterval) {
|
|
663
|
+
clearInterval(retryInterval);
|
|
664
|
+
retryInterval = null;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}, 100);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
break;
|
|
671
|
+
|
|
672
|
+
case "session.updated": {
|
|
673
|
+
const session = event.properties.info;
|
|
674
|
+
if (session && session.id === state.sessionID && session.title) {
|
|
675
|
+
setTerminalTitle(session.title);
|
|
676
|
+
}
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
case "todo.updated": {
|
|
681
|
+
const todos = event.properties.todos;
|
|
682
|
+
if (todos) {
|
|
683
|
+
await processTodos(todos);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
default:
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async function processPart(part: Part): Promise<void> {
|
|
695
|
+
switch (part.type) {
|
|
696
|
+
case "step-start":
|
|
697
|
+
processStepStart();
|
|
698
|
+
break;
|
|
699
|
+
|
|
700
|
+
case "reasoning":
|
|
701
|
+
processReasoning(part);
|
|
702
|
+
break;
|
|
703
|
+
|
|
704
|
+
case "text":
|
|
705
|
+
if (processing) {
|
|
706
|
+
processText(part);
|
|
707
|
+
}
|
|
708
|
+
break;
|
|
709
|
+
|
|
710
|
+
case "step-finish":
|
|
711
|
+
break;
|
|
712
|
+
|
|
713
|
+
case "tool":
|
|
714
|
+
processToolUse(part);
|
|
715
|
+
break;
|
|
716
|
+
|
|
717
|
+
default:
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function processStepStart() {
|
|
723
|
+
processing = true;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
async function processReasoning(part: Part) {
|
|
727
|
+
processing = true;
|
|
728
|
+
let thinkingPart = findLastPart(part.id);
|
|
729
|
+
if (!thinkingPart) {
|
|
730
|
+
thinkingPart = { key: part.id, title: "thinking", text: (part as any).text || "" };
|
|
731
|
+
state.accumulatedResponse.push(thinkingPart);
|
|
732
|
+
} else {
|
|
733
|
+
thinkingPart.text = (part as any).text || "";
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const text = (part as any).text || "";
|
|
737
|
+
const cleanText = ansi.stripAnsiCodes(text.trimStart());
|
|
738
|
+
await writeToLog(`💭 Thinking...\n\n${cleanText}\n\n`);
|
|
739
|
+
|
|
740
|
+
render(state);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async function processText(part: Part) {
|
|
744
|
+
let responsePart = findLastPart(part.id);
|
|
745
|
+
if (!responsePart) {
|
|
746
|
+
responsePart = { key: part.id, title: "response", text: (part as any).text || "" };
|
|
747
|
+
state.accumulatedResponse.push(responsePart);
|
|
748
|
+
} else {
|
|
749
|
+
responsePart.text = (part as any).text || "";
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const text = (part as any).text || "";
|
|
753
|
+
const cleanText = ansi.stripAnsiCodes(text.trimStart());
|
|
754
|
+
await writeToLog(`💬 Response:\n\n${cleanText}\n\n`);
|
|
755
|
+
|
|
756
|
+
render(state);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
async function processToolUse(part: Part) {
|
|
760
|
+
const toolPart = part as ToolPart;
|
|
761
|
+
const toolName = toolPart.tool || "unknown";
|
|
762
|
+
const toolInput = toolPart.state.input["description"] || toolPart.state.input["filePath"] || {};
|
|
763
|
+
const toolText = `🔧 ${toolName}: ${ansi.BRIGHT_BLACK}${toolInput}${ansi.RESET}`;
|
|
764
|
+
|
|
765
|
+
if (state.accumulatedResponse[state.accumulatedResponse.length - 1]?.title === "tool") {
|
|
766
|
+
state.accumulatedResponse[state.accumulatedResponse.length - 1]!.text = toolText;
|
|
767
|
+
} else {
|
|
768
|
+
state.accumulatedResponse.push({ key: part.id, title: "tool", text: toolText });
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const cleanToolText = ansi.stripAnsiCodes(toolText);
|
|
772
|
+
await writeToLog(`${cleanToolText}\n\n`);
|
|
773
|
+
|
|
774
|
+
render(state);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function processDelta(partID: string, delta: string) {
|
|
778
|
+
let responsePart = findLastPart(partID);
|
|
779
|
+
if (responsePart) {
|
|
780
|
+
responsePart.text += delta;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
render(state);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function processDiff(diff: FileDiff[]) {
|
|
787
|
+
let hasChanges = false;
|
|
788
|
+
const parts: string[] = [];
|
|
789
|
+
for (const file of diff) {
|
|
790
|
+
const status = !file.before ? "added" : !file.after ? "deleted" : "modified";
|
|
791
|
+
const statusIcon = status === "added" ? "A" : status === "modified" ? "M" : "D";
|
|
792
|
+
const statusLabel =
|
|
793
|
+
status === "added" ? "added" : status === "modified" ? "modified" : "deleted";
|
|
794
|
+
const addStr = file.additions > 0 ? `${ansi.GREEN}+${file.additions}${ansi.RESET}` : "";
|
|
795
|
+
const delStr = file.deletions > 0 ? `${ansi.RED}-${file.deletions}${ansi.RESET}` : "";
|
|
796
|
+
const stats = [addStr, delStr].filter(Boolean).join(" ");
|
|
797
|
+
const line = ` ${ansi.BLUE}${statusIcon}${ansi.RESET} ${file.file} (${statusLabel}) ${stats}`;
|
|
798
|
+
parts.push(line);
|
|
799
|
+
|
|
800
|
+
const newAfter = file.after ?? "";
|
|
801
|
+
const oldAfter = state.lastFileAfter.get(file.file);
|
|
802
|
+
if (newAfter !== oldAfter) {
|
|
803
|
+
hasChanges = true;
|
|
804
|
+
state.lastFileAfter.set(file.file, newAfter);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (hasChanges) {
|
|
809
|
+
state.accumulatedResponse.push({ key: "diff", title: "files", text: parts.join("\n") });
|
|
810
|
+
|
|
811
|
+
const diffText = ansi.stripAnsiCodes(parts.join("\n"));
|
|
812
|
+
await writeToLog(`${diffText}\n\n`);
|
|
813
|
+
|
|
814
|
+
render(state);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
async function processTodos(todos: Todo[]) {
|
|
819
|
+
let todoListText = "Todo:\n";
|
|
820
|
+
|
|
821
|
+
for (let todo of todos) {
|
|
822
|
+
let todoText = "";
|
|
823
|
+
if (todo.status === "completed") {
|
|
824
|
+
todoText += ansi.STRIKETHROUGH;
|
|
825
|
+
todoText += "- [✓] ";
|
|
826
|
+
} else {
|
|
827
|
+
todoText += "- [ ] ";
|
|
828
|
+
}
|
|
829
|
+
todoText += todo.content;
|
|
830
|
+
if (todo.status === "completed") {
|
|
831
|
+
todoText += ansi.RESET;
|
|
832
|
+
}
|
|
833
|
+
todoListText += todoText + "\n";
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
state.accumulatedResponse.push({ key: "todo", title: "files", text: todoListText });
|
|
837
|
+
|
|
838
|
+
const cleanTodoText = ansi.stripAnsiCodes(todoListText);
|
|
839
|
+
await writeToLog(`${cleanTodoText}\n\n`);
|
|
840
|
+
|
|
841
|
+
render(state);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function findLastPart(title: string) {
|
|
845
|
+
for (let i = state.accumulatedResponse.length - 1; i >= 0; i--) {
|
|
846
|
+
const part = state.accumulatedResponse[i];
|
|
847
|
+
if (part?.key === title) {
|
|
848
|
+
return part;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// ====================
|
|
854
|
+
// USER INTERFACE
|
|
855
|
+
// ====================
|
|
856
|
+
|
|
857
|
+
function setTerminalTitle(sessionName: string): void {
|
|
858
|
+
process.stdout.write(`\x1b]0;OC | ${sessionName}\x07`);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function findPreviousWordBoundary(text: string, pos: number): number {
|
|
862
|
+
if (pos <= 0) return 0;
|
|
863
|
+
|
|
864
|
+
let newPos = pos;
|
|
865
|
+
|
|
866
|
+
while (newPos > 0 && /\s/.test(text[newPos - 1]!)) {
|
|
867
|
+
newPos--;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
while (newPos > 0 && !/\s/.test(text[newPos - 1]!)) {
|
|
871
|
+
newPos--;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return newPos;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function findNextWordBoundary(text: string, pos: number): number {
|
|
878
|
+
if (pos >= text.length) return text.length;
|
|
879
|
+
|
|
880
|
+
let newPos = pos;
|
|
881
|
+
|
|
882
|
+
while (newPos < text.length && !/\s/.test(text[newPos]!)) {
|
|
883
|
+
newPos++;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
while (newPos < text.length && /\s/.test(text[newPos]!)) {
|
|
887
|
+
newPos++;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
return newPos;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function formatDuration(ms: number): string {
|
|
894
|
+
if (ms < 1000) {
|
|
895
|
+
return `${ms}ms`;
|
|
896
|
+
}
|
|
897
|
+
const seconds = ms / 1000;
|
|
898
|
+
if (seconds < 60) {
|
|
899
|
+
return `${seconds.toFixed(1)}s`;
|
|
900
|
+
}
|
|
901
|
+
const minutes = Math.floor(seconds / 60);
|
|
902
|
+
const remainingSeconds = Math.round(seconds % 60);
|
|
903
|
+
if (minutes < 60) {
|
|
904
|
+
return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
|
|
905
|
+
}
|
|
906
|
+
const hours = Math.floor(minutes / 60);
|
|
907
|
+
const remainingMinutes = minutes % 60;
|
|
908
|
+
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// ====================
|
|
912
|
+
// LOGGING
|
|
913
|
+
// ====================
|
|
914
|
+
|
|
915
|
+
export function getLogDir(): string {
|
|
916
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
917
|
+
return `${homeDir}/.local/share/opencode-miniterm/log`;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
async function createLogFile(): Promise<void> {
|
|
921
|
+
if (!isLoggingEnabled()) {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const logDir = getLogDir();
|
|
926
|
+
await mkdir(logDir, { recursive: true });
|
|
927
|
+
|
|
928
|
+
const now = new Date();
|
|
929
|
+
const timestamp = now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
930
|
+
const filename = `${timestamp}.txt`;
|
|
931
|
+
logFilePath = `${logDir}/${filename}`;
|
|
932
|
+
|
|
933
|
+
try {
|
|
934
|
+
logFile = await open(logFilePath, "w");
|
|
935
|
+
} catch (error) {
|
|
936
|
+
console.error("Failed to create log file:", error);
|
|
937
|
+
logFile = null;
|
|
938
|
+
logFilePath = null;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
async function closeLogFile(): Promise<void> {
|
|
943
|
+
if (logFile) {
|
|
944
|
+
try {
|
|
945
|
+
await logFile.close();
|
|
946
|
+
} catch (error) {
|
|
947
|
+
console.error("Failed to close log file:", error);
|
|
948
|
+
}
|
|
949
|
+
logFile = null;
|
|
950
|
+
logFilePath = null;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
async function writeToLog(text: string): Promise<void> {
|
|
955
|
+
if (logFile && isLoggingEnabled()) {
|
|
956
|
+
try {
|
|
957
|
+
await logFile.write(text);
|
|
958
|
+
} catch (error) {
|
|
959
|
+
console.error("Failed to write to log file:", error);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// ====================
|
|
965
|
+
// UTILITIES
|
|
966
|
+
// ====================
|
|
967
|
+
|
|
968
|
+
async function getFileCompletions(pattern: string): Promise<string[]> {
|
|
969
|
+
try {
|
|
970
|
+
const files: string[] = [];
|
|
971
|
+
for await (const file of glob(pattern)) {
|
|
972
|
+
if (
|
|
973
|
+
!file.startsWith("node_modules/") &&
|
|
974
|
+
!file.startsWith(".git/") &&
|
|
975
|
+
!file.startsWith("dist/") &&
|
|
976
|
+
!file.startsWith("build/")
|
|
977
|
+
) {
|
|
978
|
+
const isDir = await stat(file)
|
|
979
|
+
.then((s) => s.isDirectory())
|
|
980
|
+
.catch(() => false);
|
|
981
|
+
files.push(isDir ? file + "/" : file);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return files.sort();
|
|
985
|
+
} catch {
|
|
986
|
+
return [];
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
main().catch(console.error);
|