@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.
Files changed (36) hide show
  1. package/backend/chat/stream-manager.ts +3 -0
  2. package/backend/engine/adapters/claude/stream.ts +2 -1
  3. package/backend/engine/types.ts +9 -0
  4. package/backend/mcp/config.ts +32 -6
  5. package/backend/snapshot/snapshot-service.ts +9 -7
  6. package/backend/terminal/stream-manager.ts +106 -155
  7. package/backend/ws/projects/crud.ts +3 -3
  8. package/backend/ws/snapshot/timeline.ts +6 -2
  9. package/backend/ws/terminal/persistence.ts +19 -33
  10. package/backend/ws/terminal/session.ts +37 -19
  11. package/bin/clopen.ts +376 -99
  12. package/bun.lock +6 -0
  13. package/frontend/components/chat/input/ChatInput.svelte +8 -0
  14. package/frontend/components/chat/input/components/LoadingIndicator.svelte +2 -2
  15. package/frontend/components/chat/input/composables/use-animations.svelte.ts +127 -111
  16. package/frontend/components/chat/input/composables/use-textarea-resize.svelte.ts +11 -1
  17. package/frontend/components/chat/widgets/FloatingTodoList.svelte +2 -2
  18. package/frontend/components/checkpoint/TimelineModal.svelte +3 -1
  19. package/frontend/components/common/overlay/Dialog.svelte +2 -2
  20. package/frontend/components/git/ChangesSection.svelte +104 -13
  21. package/frontend/components/preview/browser/BrowserPreview.svelte +7 -0
  22. package/frontend/components/preview/browser/components/Canvas.svelte +8 -0
  23. package/frontend/components/settings/engines/AIEnginesSettings.svelte +1 -1
  24. package/frontend/components/settings/general/AuthModeSettings.svelte +2 -2
  25. package/frontend/components/terminal/Terminal.svelte +5 -1
  26. package/frontend/components/tunnel/TunnelInactive.svelte +4 -4
  27. package/frontend/services/chat/chat.service.ts +52 -11
  28. package/frontend/services/terminal/project.service.ts +4 -60
  29. package/frontend/services/terminal/terminal.service.ts +18 -27
  30. package/frontend/stores/core/sessions.svelte.ts +6 -0
  31. package/frontend/stores/ui/settings-modal.svelte.ts +1 -1
  32. package/frontend/stores/ui/theme.svelte.ts +11 -11
  33. package/frontend/stores/ui/workspace.svelte.ts +1 -1
  34. package/index.html +2 -2
  35. package/package.json +4 -2
  36. 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
- // Simple loading indicator
50
- let loadingInterval: Timer | null = null;
51
- let currentMessage = '';
50
+ // ============================================================
51
+ // Simple single-line spinner (used for update / clear-data)
52
+ // ============================================================
52
53
 
53
- async function delay(ms: number = 500) {
54
- await new Promise(resolve => setTimeout(resolve, ms));
55
- }
54
+ let spinnerInterval: Timer | null = null;
55
+ let spinnerMessage = '';
56
56
 
57
- function updateLoading(message: string) {
58
- currentMessage = message;
59
- if (!loadingInterval) {
57
+ function startSpinner(message: string) {
58
+ spinnerMessage = message;
59
+ if (!spinnerInterval) {
60
60
  const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
61
61
  let i = 0;
62
- loadingInterval = setInterval(() => {
63
- // Clear line and write new message to avoid text overlap
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 stopLoading() {
71
- if (loadingInterval) {
72
- clearInterval(loadingInterval);
73
- loadingInterval = null;
74
- process.stdout.write('\r\x1b[K'); // Clear line
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 (port ${DEFAULT_PORT})
110
- clopen --port 9145 # Start on port 9145
111
- clopen --host 0.0.0.0 # Bind to all network interfaces
112
- clopen update # Update to the latest version
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
- // Check for latest version
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
- stopLoading();
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
- stopLoading();
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
- stopLoading();
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
- // Run update
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
- stopLoading();
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
- // Always run bun install to ensure dependencies are up to date
297
- // Bun is fast and will skip if nothing changed
298
- updateLoading('Checking dependencies...');
299
- await delay();
576
+ progressStepStart('deps');
300
577
 
301
- const installProc = Bun.spawn(['bun', 'install', '--silent'], {
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
- // If install takes longer than 3 seconds, it's actually installing
308
- const updateMessageTimeout = setTimeout(() => {
309
- updateLoading('Installing dependencies...');
310
- }, 3000);
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
- stopLoading();
317
- // Show error output only if failed
318
- const errorText = await new Response(installProc.stderr).text();
591
+ progressStepFail('deps');
592
+ clearProgress();
593
+ const status = stepStatuses.get('deps');
319
594
  console.error('❌ Dependency installation failed:');
320
- console.error(errorText);
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
- updateLoading('Building...');
344
- await delay();
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
- const exitCode = await buildProc.exited;
619
+ progressStepStart('build');
353
620
 
354
- if (exitCode !== 0) {
355
- stopLoading();
356
- const errorText = await new Response(buildProc.stderr).text();
357
- console.error('❌ Build failed:');
358
- console.error(errorText);
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
- // Write current version to build version file
363
- writeFileSync(BUILD_VERSION_FILE, getVersion());
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
- updateLoading('Starting server...');
369
- await delay();
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
- // 1. Setup environment variables
443
- await setupEnvironment();
714
+ if (options.clearData) {
715
+ await setupEnvironment();
716
+ await clearAllData();
717
+ process.exit(0);
718
+ }
444
719
 
445
- // 2. Install dependencies if needed
446
- await installDependencies();
720
+ // Print header
721
+ console.log(`\n\x1b[36mClopen\x1b[0m v${getVersion()}\n`);
447
722
 
448
- // 3. Verify/build frontend
449
- await verifyBuild();
723
+ // Init multi-step progress
724
+ initProgress(STARTUP_STEPS);
450
725
 
451
- // 4. Start server
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=="],