@myrialabs/clopen 0.2.12 → 0.2.14
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/backend/chat/stream-manager.ts +3 -0
- package/backend/engine/adapters/claude/stream.ts +2 -1
- package/backend/engine/types.ts +9 -0
- package/backend/mcp/config.ts +32 -6
- package/backend/snapshot/snapshot-service.ts +9 -7
- package/backend/terminal/stream-manager.ts +106 -155
- package/backend/ws/projects/crud.ts +3 -3
- package/backend/ws/snapshot/timeline.ts +6 -2
- package/backend/ws/terminal/persistence.ts +19 -33
- package/backend/ws/terminal/session.ts +37 -19
- package/bin/clopen.ts +376 -99
- package/bun.lock +6 -0
- package/frontend/components/chat/input/ChatInput.svelte +8 -0
- package/frontend/components/chat/input/components/LoadingIndicator.svelte +2 -2
- package/frontend/components/chat/input/composables/use-animations.svelte.ts +127 -111
- package/frontend/components/chat/input/composables/use-textarea-resize.svelte.ts +11 -1
- package/frontend/components/chat/widgets/FloatingTodoList.svelte +2 -2
- package/frontend/components/checkpoint/TimelineModal.svelte +3 -1
- package/frontend/components/common/overlay/Dialog.svelte +2 -2
- package/frontend/components/git/ChangesSection.svelte +104 -13
- package/frontend/components/preview/browser/BrowserPreview.svelte +7 -0
- package/frontend/components/preview/browser/components/Canvas.svelte +8 -0
- package/frontend/components/settings/engines/AIEnginesSettings.svelte +1 -1
- package/frontend/components/settings/general/AuthModeSettings.svelte +2 -2
- package/frontend/components/terminal/Terminal.svelte +5 -1
- package/frontend/components/tunnel/TunnelInactive.svelte +4 -4
- package/frontend/services/chat/chat.service.ts +52 -11
- package/frontend/services/terminal/project.service.ts +4 -60
- package/frontend/services/terminal/terminal.service.ts +18 -27
- package/frontend/stores/core/sessions.svelte.ts +6 -0
- package/frontend/stores/ui/settings-modal.svelte.ts +1 -1
- package/frontend/stores/ui/theme.svelte.ts +11 -11
- package/frontend/stores/ui/workspace.svelte.ts +1 -1
- package/index.html +2 -2
- package/package.json +4 -2
- package/shared/utils/anonymous-user.ts +4 -4
package/bin/clopen.ts
CHANGED
|
@@ -33,6 +33,7 @@ interface CLIOptions {
|
|
|
33
33
|
version?: boolean;
|
|
34
34
|
update?: boolean;
|
|
35
35
|
resetPat?: boolean;
|
|
36
|
+
clearData?: boolean;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
// Get version from package.json
|
|
@@ -46,47 +47,274 @@ function getVersion(): string {
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
// ============================================================
|
|
51
|
+
// Simple single-line spinner (used for update / clear-data)
|
|
52
|
+
// ============================================================
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
54
|
+
let spinnerInterval: Timer | null = null;
|
|
55
|
+
let spinnerMessage = '';
|
|
56
56
|
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
if (!
|
|
57
|
+
function startSpinner(message: string) {
|
|
58
|
+
spinnerMessage = message;
|
|
59
|
+
if (!spinnerInterval) {
|
|
60
60
|
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
61
61
|
let i = 0;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
process.stdout.write(`\r\x1b[K${frames[i]} ${currentMessage}`);
|
|
62
|
+
spinnerInterval = setInterval(() => {
|
|
63
|
+
process.stdout.write(`\r\x1b[K${frames[i]} ${spinnerMessage}`);
|
|
65
64
|
i = (i + 1) % frames.length;
|
|
66
65
|
}, 80);
|
|
67
66
|
}
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
function updateSpinner(message: string) {
|
|
70
|
+
spinnerMessage = message;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function stopSpinner() {
|
|
74
|
+
if (spinnerInterval) {
|
|
75
|
+
clearInterval(spinnerInterval);
|
|
76
|
+
spinnerInterval = null;
|
|
77
|
+
process.stdout.write('\r\x1b[K');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ============================================================
|
|
82
|
+
// Multi-step progress display (used for startup sequence)
|
|
83
|
+
// ============================================================
|
|
84
|
+
|
|
85
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
86
|
+
const MAX_OUTPUT_LINES = 5;
|
|
87
|
+
const OUTPUT_TRUNCATE = 72;
|
|
88
|
+
|
|
89
|
+
type StepState = 'pending' | 'running' | 'done' | 'skipped' | 'error';
|
|
90
|
+
|
|
91
|
+
interface ProgressStep {
|
|
92
|
+
id: string;
|
|
93
|
+
label: string;
|
|
94
|
+
activeLabel: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface StepStatus {
|
|
98
|
+
state: StepState;
|
|
99
|
+
startTime?: number;
|
|
100
|
+
outputLines: string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const isTTY = process.stdout.isTTY === true;
|
|
104
|
+
let progressSteps: ProgressStep[] = [];
|
|
105
|
+
const stepStatuses = new Map<string, StepStatus>();
|
|
106
|
+
let frameIdx = 0;
|
|
107
|
+
let renderTimer: Timer | null = null;
|
|
108
|
+
let drawnLines = 0;
|
|
109
|
+
|
|
110
|
+
function stripAnsi(str: string): string {
|
|
111
|
+
return str.replace(/\x1b\[[0-9;]*[mGKHFABCDJhls]/g, '');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function trunc(str: string, max: number): string {
|
|
115
|
+
if (str.length <= max) return str;
|
|
116
|
+
return str.slice(0, max - 1) + '…';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function initProgress(steps: ProgressStep[]) {
|
|
120
|
+
progressSteps = steps;
|
|
121
|
+
stepStatuses.clear();
|
|
122
|
+
drawnLines = 0;
|
|
123
|
+
frameIdx = 0;
|
|
124
|
+
for (const s of steps) {
|
|
125
|
+
stepStatuses.set(s.id, { state: 'pending', outputLines: [] });
|
|
126
|
+
}
|
|
127
|
+
if (isTTY) {
|
|
128
|
+
process.stdout.write('\x1b[?25l'); // hide cursor
|
|
129
|
+
renderTimer = setInterval(renderProgress, 80);
|
|
130
|
+
renderProgress();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function renderProgress() {
|
|
135
|
+
if (!isTTY) return;
|
|
136
|
+
|
|
137
|
+
if (drawnLines > 0) {
|
|
138
|
+
process.stdout.write(`\x1b[${drawnLines}A\x1b[J`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let output = '';
|
|
142
|
+
let lines = 0;
|
|
143
|
+
|
|
144
|
+
for (const step of progressSteps) {
|
|
145
|
+
const status = stepStatuses.get(step.id)!;
|
|
146
|
+
let icon: string;
|
|
147
|
+
let label: string;
|
|
148
|
+
|
|
149
|
+
switch (status.state) {
|
|
150
|
+
case 'pending':
|
|
151
|
+
icon = '\x1b[90m·\x1b[0m';
|
|
152
|
+
label = `\x1b[90m${step.label}\x1b[0m`;
|
|
153
|
+
break;
|
|
154
|
+
case 'running': {
|
|
155
|
+
const elapsed = Math.floor((Date.now() - (status.startTime ?? Date.now())) / 1000);
|
|
156
|
+
icon = `\x1b[36m${SPINNER_FRAMES[frameIdx]}\x1b[0m`;
|
|
157
|
+
const timer = elapsed >= 3 ? ` \x1b[90m(${elapsed}s)\x1b[0m` : '';
|
|
158
|
+
label = `${step.activeLabel}${timer}`;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
case 'done':
|
|
162
|
+
icon = '\x1b[32m✓\x1b[0m';
|
|
163
|
+
label = step.label;
|
|
164
|
+
break;
|
|
165
|
+
case 'skipped':
|
|
166
|
+
icon = '\x1b[90m✓\x1b[0m';
|
|
167
|
+
label = `\x1b[90m${step.label} (up to date)\x1b[0m`;
|
|
168
|
+
break;
|
|
169
|
+
case 'error':
|
|
170
|
+
icon = '\x1b[31m✗\x1b[0m';
|
|
171
|
+
label = `\x1b[31m${step.label}\x1b[0m`;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
output += `\x1b[K ${icon} ${label}\n`;
|
|
176
|
+
lines++;
|
|
177
|
+
|
|
178
|
+
// Show last N output lines for the currently running step
|
|
179
|
+
if (status.state === 'running' && status.outputLines.length > 0) {
|
|
180
|
+
const visible = status.outputLines.slice(-MAX_OUTPUT_LINES);
|
|
181
|
+
for (const line of visible) {
|
|
182
|
+
output += `\x1b[K \x1b[90m│ ${trunc(line, OUTPUT_TRUNCATE)}\x1b[0m\n`;
|
|
183
|
+
lines++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
process.stdout.write(output);
|
|
189
|
+
drawnLines = lines;
|
|
190
|
+
frameIdx = (frameIdx + 1) % SPINNER_FRAMES.length;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function stopProgress() {
|
|
194
|
+
if (renderTimer) {
|
|
195
|
+
clearInterval(renderTimer);
|
|
196
|
+
renderTimer = null;
|
|
197
|
+
}
|
|
198
|
+
if (isTTY) {
|
|
199
|
+
renderProgress(); // final render
|
|
200
|
+
process.stdout.write('\x1b[?25h'); // show cursor
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function clearProgress() {
|
|
205
|
+
if (renderTimer) {
|
|
206
|
+
clearInterval(renderTimer);
|
|
207
|
+
renderTimer = null;
|
|
208
|
+
}
|
|
209
|
+
if (isTTY) {
|
|
210
|
+
if (drawnLines > 0) {
|
|
211
|
+
process.stdout.write(`\x1b[${drawnLines}A\x1b[J`);
|
|
212
|
+
drawnLines = 0;
|
|
213
|
+
}
|
|
214
|
+
process.stdout.write('\x1b[?25h');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function progressStepStart(id: string) {
|
|
219
|
+
const s = stepStatuses.get(id);
|
|
220
|
+
if (!s) return;
|
|
221
|
+
s.state = 'running';
|
|
222
|
+
s.startTime = Date.now();
|
|
223
|
+
s.outputLines = [];
|
|
224
|
+
if (!isTTY) {
|
|
225
|
+
const step = progressSteps.find(p => p.id === id);
|
|
226
|
+
process.stdout.write(` ${step?.activeLabel ?? id}...\n`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function progressStepDone(id: string) {
|
|
231
|
+
const s = stepStatuses.get(id);
|
|
232
|
+
if (!s) return;
|
|
233
|
+
s.state = 'done';
|
|
234
|
+
if (!isTTY) {
|
|
235
|
+
const step = progressSteps.find(p => p.id === id);
|
|
236
|
+
process.stdout.write(` ✓ ${step?.label ?? id}\n`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function progressStepSkip(id: string) {
|
|
241
|
+
const s = stepStatuses.get(id);
|
|
242
|
+
if (!s) return;
|
|
243
|
+
s.state = 'skipped';
|
|
244
|
+
if (!isTTY) {
|
|
245
|
+
const step = progressSteps.find(p => p.id === id);
|
|
246
|
+
process.stdout.write(` ✓ ${step?.label ?? id} (up to date)\n`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function progressStepFail(id: string) {
|
|
251
|
+
const s = stepStatuses.get(id);
|
|
252
|
+
if (!s) return;
|
|
253
|
+
s.state = 'error';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function progressAppendOutput(id: string, line: string) {
|
|
257
|
+
const s = stepStatuses.get(id);
|
|
258
|
+
if (!s || s.state !== 'running') return;
|
|
259
|
+
s.outputLines.push(stripAnsi(line).trim());
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function streamSubprocess(stream: ReadableStream<Uint8Array>, stepId: string): Promise<void> {
|
|
263
|
+
const reader = stream.getReader();
|
|
264
|
+
const decoder = new TextDecoder();
|
|
265
|
+
let buffer = '';
|
|
266
|
+
try {
|
|
267
|
+
for (;;) {
|
|
268
|
+
const { done, value } = await reader.read();
|
|
269
|
+
if (done) break;
|
|
270
|
+
buffer += decoder.decode(value, { stream: true });
|
|
271
|
+
const parts = buffer.split('\n');
|
|
272
|
+
buffer = parts.pop() ?? '';
|
|
273
|
+
for (const part of parts) {
|
|
274
|
+
const line = part.trim();
|
|
275
|
+
if (line) progressAppendOutput(stepId, line);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (buffer.trim()) progressAppendOutput(stepId, buffer.trim());
|
|
279
|
+
} catch {
|
|
280
|
+
// stream closed or error — fine
|
|
281
|
+
} finally {
|
|
282
|
+
reader.releaseLock();
|
|
75
283
|
}
|
|
76
284
|
}
|
|
77
285
|
|
|
286
|
+
// Restore cursor on unexpected exit
|
|
287
|
+
process.on('exit', () => {
|
|
288
|
+
if (renderTimer) clearInterval(renderTimer);
|
|
289
|
+
if (spinnerInterval) clearInterval(spinnerInterval);
|
|
290
|
+
if (isTTY) process.stdout.write('\x1b[?25h');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// ============================================================
|
|
294
|
+
// Constants
|
|
295
|
+
// ============================================================
|
|
296
|
+
|
|
78
297
|
const __dirname = join(import.meta.dir, '..');
|
|
79
298
|
const ENV_EXAMPLE = join(__dirname, '.env.example');
|
|
80
299
|
const ENV_FILE = join(__dirname, '.env');
|
|
81
300
|
const DIST_DIR = join(__dirname, 'dist');
|
|
82
301
|
const BUILD_VERSION_FILE = join(DIST_DIR, '.build-version');
|
|
83
302
|
|
|
84
|
-
// Default values
|
|
85
303
|
const DEFAULT_PORT = 9141;
|
|
86
304
|
const DEFAULT_HOST = 'localhost';
|
|
87
305
|
const MIN_PORT = 1024;
|
|
88
306
|
const MAX_PORT = 65535;
|
|
89
307
|
|
|
308
|
+
const STARTUP_STEPS: ProgressStep[] = [
|
|
309
|
+
{ id: 'deps', label: 'Dependencies', activeLabel: 'Installing dependencies' },
|
|
310
|
+
{ id: 'build', label: 'Build', activeLabel: 'Building' },
|
|
311
|
+
{ id: 'server', label: 'Server', activeLabel: 'Starting server' },
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
// ============================================================
|
|
315
|
+
// CLI helpers
|
|
316
|
+
// ============================================================
|
|
317
|
+
|
|
90
318
|
function showHelp() {
|
|
91
319
|
console.log(`
|
|
92
320
|
Clopen - All-in-one web workspace for Claude Code & OpenCode
|
|
@@ -98,6 +326,7 @@ USAGE:
|
|
|
98
326
|
COMMANDS:
|
|
99
327
|
update Update clopen to the latest version
|
|
100
328
|
reset-pat Regenerate admin Personal Access Token
|
|
329
|
+
clear-data Delete all projects, sessions, and settings
|
|
101
330
|
|
|
102
331
|
OPTIONS:
|
|
103
332
|
-p, --port <number> Port to run the server on (default: ${DEFAULT_PORT})
|
|
@@ -106,12 +335,10 @@ OPTIONS:
|
|
|
106
335
|
-h, --help Show this help message
|
|
107
336
|
|
|
108
337
|
EXAMPLES:
|
|
109
|
-
clopen # Start with default settings
|
|
110
|
-
clopen --port 9145 # Start on port
|
|
111
|
-
clopen
|
|
112
|
-
clopen update # Update to
|
|
113
|
-
clopen reset-pat # Regenerate admin login token
|
|
114
|
-
clopen --version # Show version
|
|
338
|
+
clopen # Start with default settings
|
|
339
|
+
clopen --port 9145 # Start on custom port
|
|
340
|
+
clopen -v # Show version
|
|
341
|
+
clopen update # Update to latest version
|
|
115
342
|
|
|
116
343
|
For more information, visit: https://github.com/myrialabs/clopen
|
|
117
344
|
`);
|
|
@@ -175,6 +402,10 @@ function parseArguments(): CLIOptions {
|
|
|
175
402
|
options.resetPat = true;
|
|
176
403
|
break;
|
|
177
404
|
|
|
405
|
+
case 'clear-data':
|
|
406
|
+
options.clearData = true;
|
|
407
|
+
break;
|
|
408
|
+
|
|
178
409
|
default:
|
|
179
410
|
console.error(`❌ Error: Unknown option "${arg}"`);
|
|
180
411
|
console.log('Run "clopen --help" for usage information');
|
|
@@ -199,30 +430,33 @@ function isNewerVersion(current: string, latest: string): boolean {
|
|
|
199
430
|
return false;
|
|
200
431
|
}
|
|
201
432
|
|
|
433
|
+
// ============================================================
|
|
434
|
+
// Commands
|
|
435
|
+
// ============================================================
|
|
436
|
+
|
|
202
437
|
async function runUpdate() {
|
|
203
438
|
const currentVersion = getVersion();
|
|
204
439
|
console.log(`\x1b[36mClopen\x1b[0m v${currentVersion}\n`);
|
|
205
440
|
|
|
206
|
-
|
|
207
|
-
updateLoading('Checking for updates...');
|
|
441
|
+
startSpinner('Checking for updates...');
|
|
208
442
|
|
|
209
443
|
let latestVersion: string;
|
|
210
444
|
try {
|
|
211
445
|
const response = await fetch('https://registry.npmjs.org/@myrialabs/clopen/latest');
|
|
212
446
|
if (!response.ok) {
|
|
213
|
-
|
|
447
|
+
stopSpinner();
|
|
214
448
|
console.error(`❌ Failed to check for updates (HTTP ${response.status})`);
|
|
215
449
|
process.exit(1);
|
|
216
450
|
}
|
|
217
451
|
const data = await response.json() as { version: string };
|
|
218
452
|
latestVersion = data.version;
|
|
219
453
|
} catch (err) {
|
|
220
|
-
|
|
454
|
+
stopSpinner();
|
|
221
455
|
console.error('❌ Failed to reach npm registry:', err instanceof Error ? err.message : err);
|
|
222
456
|
process.exit(1);
|
|
223
457
|
}
|
|
224
458
|
|
|
225
|
-
|
|
459
|
+
stopSpinner();
|
|
226
460
|
|
|
227
461
|
if (!isNewerVersion(currentVersion, latestVersion)) {
|
|
228
462
|
console.log(`✓ Already up to date (v${currentVersion})`);
|
|
@@ -231,8 +465,7 @@ async function runUpdate() {
|
|
|
231
465
|
|
|
232
466
|
console.log(` New version available: v${currentVersion} → \x1b[32mv${latestVersion}\x1b[0m\n`);
|
|
233
467
|
|
|
234
|
-
|
|
235
|
-
updateLoading(`Updating to v${latestVersion}...`);
|
|
468
|
+
startSpinner(`Updating to v${latestVersion}...`);
|
|
236
469
|
|
|
237
470
|
const proc = Bun.spawn(['bun', 'add', '-g', '@myrialabs/clopen@latest'], {
|
|
238
471
|
stdout: 'pipe',
|
|
@@ -245,7 +478,7 @@ async function runUpdate() {
|
|
|
245
478
|
]);
|
|
246
479
|
|
|
247
480
|
const exitCode = await proc.exited;
|
|
248
|
-
|
|
481
|
+
stopSpinner();
|
|
249
482
|
|
|
250
483
|
if (exitCode !== 0) {
|
|
251
484
|
const output = (stdout + '\n' + stderr).trim();
|
|
@@ -262,7 +495,6 @@ async function recoverAdminToken() {
|
|
|
262
495
|
const version = getVersion();
|
|
263
496
|
console.log(`\x1b[36mClopen\x1b[0m v${version} — Admin Token Recovery\n`);
|
|
264
497
|
|
|
265
|
-
// Initialize database (import dynamically to avoid loading full backend)
|
|
266
498
|
const { initializeDatabase } = await import('../backend/database/index');
|
|
267
499
|
const { listUsers, regeneratePAT } = await import('../backend/auth/auth-service');
|
|
268
500
|
|
|
@@ -283,8 +515,56 @@ async function recoverAdminToken() {
|
|
|
283
515
|
console.log(`\n Use this token to log in. Keep it safe — it won't be shown again.`);
|
|
284
516
|
}
|
|
285
517
|
|
|
518
|
+
async function clearAllData() {
|
|
519
|
+
const version = getVersion();
|
|
520
|
+
console.log(`\x1b[36mClopen\x1b[0m v${version} — Clear All Data\n`);
|
|
521
|
+
|
|
522
|
+
console.log('\x1b[31m⚠ WARNING: This will permanently delete all projects, sessions, and settings.\x1b[0m');
|
|
523
|
+
console.log(' This action cannot be undone.\n');
|
|
524
|
+
|
|
525
|
+
process.stdout.write('Are you sure? Type "yes" to confirm: ');
|
|
526
|
+
|
|
527
|
+
const response = await new Promise<string>(resolve => {
|
|
528
|
+
const chunks: Buffer[] = [];
|
|
529
|
+
process.stdin.once('data', (data: Buffer) => {
|
|
530
|
+
chunks.push(data);
|
|
531
|
+
resolve(Buffer.concat(chunks).toString().trim());
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
if (response !== 'yes') {
|
|
536
|
+
console.log('\nAborted. No data was deleted.');
|
|
537
|
+
process.exit(0);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
console.log('');
|
|
541
|
+
startSpinner('Clearing all data...');
|
|
542
|
+
|
|
543
|
+
const { initializeDatabase, closeDatabase } = await import('../backend/database/index');
|
|
544
|
+
const { getClopenDir } = await import('../backend/utils/paths');
|
|
545
|
+
const { resetEnvironment } = await import('../backend/engine/adapters/claude/environment');
|
|
546
|
+
const fs = await import('node:fs/promises');
|
|
547
|
+
|
|
548
|
+
await initializeDatabase();
|
|
549
|
+
closeDatabase();
|
|
550
|
+
|
|
551
|
+
const clopenDir = getClopenDir();
|
|
552
|
+
await fs.rm(clopenDir, { recursive: true, force: true });
|
|
553
|
+
|
|
554
|
+
resetEnvironment();
|
|
555
|
+
|
|
556
|
+
await initializeDatabase();
|
|
557
|
+
|
|
558
|
+
stopSpinner();
|
|
559
|
+
console.log('✓ All data cleared successfully.');
|
|
560
|
+
console.log(' Database has been reinitialized with a fresh state.');
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ============================================================
|
|
564
|
+
// Startup sequence
|
|
565
|
+
// ============================================================
|
|
566
|
+
|
|
286
567
|
async function setupEnvironment() {
|
|
287
|
-
// Check if .env exists, if not copy from .env.example
|
|
288
568
|
if (!existsSync(ENV_FILE)) {
|
|
289
569
|
if (existsSync(ENV_EXAMPLE)) {
|
|
290
570
|
copyFileSync(ENV_EXAMPLE, ENV_FILE);
|
|
@@ -293,43 +573,35 @@ async function setupEnvironment() {
|
|
|
293
573
|
}
|
|
294
574
|
|
|
295
575
|
async function installDependencies() {
|
|
296
|
-
|
|
297
|
-
// Bun is fast and will skip if nothing changed
|
|
298
|
-
updateLoading('Checking dependencies...');
|
|
299
|
-
await delay();
|
|
576
|
+
progressStepStart('deps');
|
|
300
577
|
|
|
301
|
-
const
|
|
578
|
+
const proc = Bun.spawn(['bun', 'install'], {
|
|
302
579
|
cwd: __dirname,
|
|
303
580
|
stdout: 'pipe',
|
|
304
|
-
stderr: 'pipe'
|
|
581
|
+
stderr: 'pipe',
|
|
305
582
|
});
|
|
306
583
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const exitCode = await installProc.exited;
|
|
313
|
-
clearTimeout(updateMessageTimeout);
|
|
584
|
+
const [exitCode] = await Promise.all([
|
|
585
|
+
proc.exited,
|
|
586
|
+
streamSubprocess(proc.stdout as ReadableStream<Uint8Array>, 'deps'),
|
|
587
|
+
streamSubprocess(proc.stderr as ReadableStream<Uint8Array>, 'deps'),
|
|
588
|
+
]);
|
|
314
589
|
|
|
315
590
|
if (exitCode !== 0) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
591
|
+
progressStepFail('deps');
|
|
592
|
+
clearProgress();
|
|
593
|
+
const status = stepStatuses.get('deps');
|
|
319
594
|
console.error('❌ Dependency installation failed:');
|
|
320
|
-
console.error(
|
|
595
|
+
if (status?.outputLines.length) console.error(status.outputLines.join('\n'));
|
|
321
596
|
process.exit(exitCode);
|
|
322
597
|
}
|
|
598
|
+
|
|
599
|
+
progressStepDone('deps');
|
|
323
600
|
}
|
|
324
601
|
|
|
325
602
|
function needsBuild(): boolean {
|
|
326
|
-
// No dist directory — must build
|
|
327
603
|
if (!existsSync(DIST_DIR)) return true;
|
|
328
|
-
|
|
329
|
-
// No build version file — must build
|
|
330
604
|
if (!existsSync(BUILD_VERSION_FILE)) return true;
|
|
331
|
-
|
|
332
|
-
// Compare built version with current version
|
|
333
605
|
try {
|
|
334
606
|
const builtVersion = readFileSync(BUILD_VERSION_FILE, 'utf-8').trim();
|
|
335
607
|
return builtVersion !== getVersion();
|
|
@@ -339,44 +611,46 @@ function needsBuild(): boolean {
|
|
|
339
611
|
}
|
|
340
612
|
|
|
341
613
|
async function verifyBuild() {
|
|
342
|
-
if (needsBuild()) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const buildProc = Bun.spawn(['bun', 'run', 'build'], {
|
|
347
|
-
cwd: __dirname,
|
|
348
|
-
stdout: 'pipe',
|
|
349
|
-
stderr: 'pipe'
|
|
350
|
-
});
|
|
614
|
+
if (!needsBuild()) {
|
|
615
|
+
progressStepSkip('build');
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
351
618
|
|
|
352
|
-
|
|
619
|
+
progressStepStart('build');
|
|
353
620
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
process.exit(exitCode);
|
|
360
|
-
}
|
|
621
|
+
const proc = Bun.spawn(['bun', 'run', 'build'], {
|
|
622
|
+
cwd: __dirname,
|
|
623
|
+
stdout: 'pipe',
|
|
624
|
+
stderr: 'pipe',
|
|
625
|
+
});
|
|
361
626
|
|
|
362
|
-
|
|
363
|
-
|
|
627
|
+
const [exitCode] = await Promise.all([
|
|
628
|
+
proc.exited,
|
|
629
|
+
streamSubprocess(proc.stdout as ReadableStream<Uint8Array>, 'build'),
|
|
630
|
+
streamSubprocess(proc.stderr as ReadableStream<Uint8Array>, 'build'),
|
|
631
|
+
]);
|
|
632
|
+
|
|
633
|
+
if (exitCode !== 0) {
|
|
634
|
+
progressStepFail('build');
|
|
635
|
+
clearProgress();
|
|
636
|
+
const status = stepStatuses.get('build');
|
|
637
|
+
console.error('❌ Build failed:');
|
|
638
|
+
if (status?.outputLines.length) console.error(status.outputLines.join('\n'));
|
|
639
|
+
process.exit(exitCode);
|
|
364
640
|
}
|
|
641
|
+
|
|
642
|
+
writeFileSync(BUILD_VERSION_FILE, getVersion());
|
|
643
|
+
progressStepDone('build');
|
|
365
644
|
}
|
|
366
645
|
|
|
367
646
|
async function startServer(options: CLIOptions) {
|
|
368
|
-
|
|
369
|
-
|
|
647
|
+
progressStepStart('server');
|
|
648
|
+
progressStepDone('server');
|
|
649
|
+
stopProgress();
|
|
650
|
+
console.log('');
|
|
370
651
|
|
|
371
|
-
// Delegate to scripts/start.ts — handles port resolution (IPv4 + IPv6
|
|
372
|
-
// zombie detection) and starts backend in a single consistent path.
|
|
373
652
|
const startScript = join(__dirname, 'scripts/start.ts');
|
|
374
653
|
|
|
375
|
-
stopLoading();
|
|
376
|
-
|
|
377
|
-
// Overlay clopen's own .env on top of process.env to override any
|
|
378
|
-
// pollution from a .env file in the directory where `clopen` was invoked.
|
|
379
|
-
// CLI args take highest priority on top of that.
|
|
380
654
|
const env = { ...process.env, ...loadEnvFile(ENV_FILE) };
|
|
381
655
|
if (options.port) env.PORT = options.port.toString();
|
|
382
656
|
if (options.host) env.HOST = options.host;
|
|
@@ -386,19 +660,20 @@ async function startServer(options: CLIOptions) {
|
|
|
386
660
|
stdout: 'inherit',
|
|
387
661
|
stderr: 'inherit',
|
|
388
662
|
stdin: 'inherit',
|
|
389
|
-
env
|
|
663
|
+
env,
|
|
390
664
|
});
|
|
391
665
|
|
|
392
|
-
// Wait for server process
|
|
393
666
|
await serverProc.exited;
|
|
394
667
|
}
|
|
395
668
|
|
|
669
|
+
// ============================================================
|
|
670
|
+
// Main
|
|
671
|
+
// ============================================================
|
|
672
|
+
|
|
396
673
|
async function main() {
|
|
397
674
|
try {
|
|
398
|
-
// Parse CLI arguments
|
|
399
675
|
const options = parseArguments();
|
|
400
676
|
|
|
401
|
-
// Show version if requested
|
|
402
677
|
if (options.version) {
|
|
403
678
|
const currentVersion = getVersion();
|
|
404
679
|
console.log(`v${currentVersion}`);
|
|
@@ -420,42 +695,44 @@ async function main() {
|
|
|
420
695
|
process.exit(0);
|
|
421
696
|
}
|
|
422
697
|
|
|
423
|
-
// Show help if requested
|
|
424
698
|
if (options.help) {
|
|
425
699
|
showHelp();
|
|
426
700
|
process.exit(0);
|
|
427
701
|
}
|
|
428
702
|
|
|
429
|
-
// Run update if requested
|
|
430
703
|
if (options.update) {
|
|
431
704
|
await runUpdate();
|
|
432
705
|
process.exit(0);
|
|
433
706
|
}
|
|
434
707
|
|
|
435
|
-
// Recover admin token if requested
|
|
436
708
|
if (options.resetPat) {
|
|
437
709
|
await setupEnvironment();
|
|
438
710
|
await recoverAdminToken();
|
|
439
711
|
process.exit(0);
|
|
440
712
|
}
|
|
441
713
|
|
|
442
|
-
|
|
443
|
-
|
|
714
|
+
if (options.clearData) {
|
|
715
|
+
await setupEnvironment();
|
|
716
|
+
await clearAllData();
|
|
717
|
+
process.exit(0);
|
|
718
|
+
}
|
|
444
719
|
|
|
445
|
-
//
|
|
446
|
-
|
|
720
|
+
// Print header
|
|
721
|
+
console.log(`\n\x1b[36mClopen\x1b[0m v${getVersion()}\n`);
|
|
447
722
|
|
|
448
|
-
//
|
|
449
|
-
|
|
723
|
+
// Init multi-step progress
|
|
724
|
+
initProgress(STARTUP_STEPS);
|
|
450
725
|
|
|
451
|
-
|
|
726
|
+
await setupEnvironment();
|
|
727
|
+
await installDependencies();
|
|
728
|
+
await verifyBuild();
|
|
452
729
|
await startServer(options);
|
|
453
730
|
|
|
454
731
|
} catch (error) {
|
|
732
|
+
clearProgress();
|
|
455
733
|
console.error('❌ Failed to start Clopen:', error);
|
|
456
734
|
process.exit(1);
|
|
457
735
|
}
|
|
458
736
|
}
|
|
459
737
|
|
|
460
|
-
// Run CLI
|
|
461
738
|
main();
|
package/bun.lock
CHANGED
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
"@xterm/addon-clipboard": "^0.2.0",
|
|
17
17
|
"@xterm/addon-fit": "^0.11.0",
|
|
18
18
|
"@xterm/addon-ligatures": "^0.10.0",
|
|
19
|
+
"@xterm/addon-serialize": "^0.14.0",
|
|
19
20
|
"@xterm/addon-unicode11": "^0.9.0",
|
|
20
21
|
"@xterm/addon-web-links": "^0.12.0",
|
|
22
|
+
"@xterm/headless": "^6.0.0",
|
|
21
23
|
"@xterm/xterm": "^6.0.0",
|
|
22
24
|
"bun-pty": "^0.4.2",
|
|
23
25
|
"cloudflared": "^0.7.1",
|
|
@@ -348,10 +350,14 @@
|
|
|
348
350
|
|
|
349
351
|
"@xterm/addon-ligatures": ["@xterm/addon-ligatures@0.10.0", "", { "dependencies": { "font-finder": "^1.1.0", "font-ligatures": "^1.4.1" } }, "sha512-/Few8ZSHMib7sGjRJoc5l7bCtEB9XJfkNofvPpOcWADxKaUl8og8P172j67OoACSNJAXqeCLIuvj8WFCBkcTxg=="],
|
|
350
352
|
|
|
353
|
+
"@xterm/addon-serialize": ["@xterm/addon-serialize@0.14.0", "", {}, "sha512-uteyTU1EkrQa2Ux6P/uFl2fzmXI46jy5uoQMKEOM0fKTyiW7cSn0WrFenHm5vO5uEXX/GpwW/FgILvv3r0WbkA=="],
|
|
354
|
+
|
|
351
355
|
"@xterm/addon-unicode11": ["@xterm/addon-unicode11@0.9.0", "", {}, "sha512-FxDnYcyuXhNl+XSqGZL/t0U9eiNb/q3EWT5rYkQT/zuig8Gz/VagnQANKHdDWFM2lTMk9ly0EFQxxxtZUoRetw=="],
|
|
352
356
|
|
|
353
357
|
"@xterm/addon-web-links": ["@xterm/addon-web-links@0.12.0", "", {}, "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw=="],
|
|
354
358
|
|
|
359
|
+
"@xterm/headless": ["@xterm/headless@6.0.0", "", {}, "sha512-5Yj1QINYCyzrZtf8OFIHi47iQtI+0qYFPHmouEfG8dHNxbZ9Tb9YGSuLcsEwj9Z+OL75GJqPyJbyoFer80a2Hw=="],
|
|
360
|
+
|
|
355
361
|
"@xterm/xterm": ["@xterm/xterm@6.0.0", "", {}, "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg=="],
|
|
356
362
|
|
|
357
363
|
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|