deckide 3.2.0 → 3.3.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.
Files changed (2) hide show
  1. package/bin/deckide.js +202 -129
  2. package/package.json +1 -1
package/bin/deckide.js CHANGED
@@ -4,12 +4,14 @@ import { fileURLToPath } from 'node:url';
4
4
  import path from 'node:path';
5
5
  import os from 'node:os';
6
6
  import fs from 'node:fs';
7
- import { execSync } from 'node:child_process';
7
+ import { execSync, spawn } from 'node:child_process';
8
8
  import crypto from 'node:crypto';
9
9
 
10
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
11
  const dataDir = path.join(os.homedir(), '.deckide');
12
12
  const settingsFile = path.join(dataDir, 'settings.json');
13
+ const pidFile = path.join(dataDir, 'server.pid');
14
+ const logFile = path.join(dataDir, 'server.log');
13
15
 
14
16
  // ─── Settings helpers ───────────────────────────────────────────
15
17
 
@@ -26,6 +28,22 @@ function saveSettings(settings) {
26
28
  fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
27
29
  }
28
30
 
31
+ function getPort() {
32
+ return loadSettings().port || 8787;
33
+ }
34
+
35
+ function isServerRunning() {
36
+ const port = getPort();
37
+ try {
38
+ execSync(`curl -sf -o /dev/null http://localhost:${port}/health`, {
39
+ timeout: 2000, stdio: 'ignore',
40
+ });
41
+ return true;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
29
47
  // ─── CLI ────────────────────────────────────────────────────────
30
48
 
31
49
  const args = process.argv.slice(2);
@@ -40,32 +58,33 @@ if (command === '--version' || command === '-v') {
40
58
 
41
59
  // ── deckide help ──
42
60
  if (command === '--help' || command === '-h' || command === 'help') {
43
- console.log(`
44
- Deck IDE - Browser-based IDE
61
+ console.log(`Deck IDE - Browser-based IDE
45
62
 
46
63
  Usage:
47
- deckide Start the server
64
+ deckide [start] Start server (background)
65
+ deckide start --fg Start server (foreground)
66
+ deckide stop Stop server
67
+ deckide restart Restart server
68
+ deckide status Show server status
69
+ deckide logs Show server logs
70
+
48
71
  deckide config Show all settings
49
72
  deckide config set <key> <val> Set a config value
50
73
  deckide config get <key> Get a config value
51
74
  deckide config reset Reset all settings
52
- deckide auth on Enable basic auth (interactive)
75
+
76
+ deckide auth on [user] [pass] Enable basic auth
53
77
  deckide auth off Disable basic auth
54
78
  deckide auth status Show auth status
55
- deckide status Show server status
56
- deckide stop Stop running server
57
79
 
58
- Start options:
59
- -p, --port <port> Port to listen on
60
- --host <host> Host to bind to
80
+ Options (for start):
81
+ -p, --port <port> Port (default: 8787)
82
+ --host <host> Host (default: 0.0.0.0)
61
83
  --no-open Don't open browser
84
+ --fg Run in foreground
62
85
 
63
86
  Config keys:
64
- port Server port (default: 8787)
65
- host Bind host (default: 0.0.0.0)
66
- cors CORS origin
67
- maxFileSize Max file size in bytes
68
- trustProxy Trust proxy headers (true/false)
87
+ port, host, cors, maxFileSize, trustProxy
69
88
  `);
70
89
  process.exit(0);
71
90
  }
@@ -76,7 +95,6 @@ if (command === 'config') {
76
95
  const settings = loadSettings();
77
96
 
78
97
  if (!sub || sub === 'list') {
79
- // Show all config
80
98
  if (Object.keys(settings).length === 0) {
81
99
  console.log('No custom settings. Using defaults.');
82
100
  console.log(' port: 8787');
@@ -95,33 +113,21 @@ if (command === 'config') {
95
113
 
96
114
  if (sub === 'get') {
97
115
  const key = args[2];
98
- if (!key) {
99
- console.error('Usage: deckide config get <key>');
100
- process.exit(1);
101
- }
116
+ if (!key) { console.error('Usage: deckide config get <key>'); process.exit(1); }
102
117
  const val = settings[key];
103
- if (val === undefined) {
104
- console.log(`${key}: (not set)`);
105
- } else if (key === 'basicAuthPassword') {
106
- console.log(`${key}: ********`);
107
- } else {
108
- console.log(`${key}: ${val}`);
109
- }
118
+ if (val === undefined) console.log(`${key}: (not set)`);
119
+ else if (key === 'basicAuthPassword') console.log(`${key}: ********`);
120
+ else console.log(`${key}: ${val}`);
110
121
  process.exit(0);
111
122
  }
112
123
 
113
124
  if (sub === 'set') {
114
125
  const key = args[2];
115
126
  let value = args[3];
116
- if (!key || value === undefined) {
117
- console.error('Usage: deckide config set <key> <value>');
118
- process.exit(1);
119
- }
120
- // Type coercion
127
+ if (!key || value === undefined) { console.error('Usage: deckide config set <key> <value>'); process.exit(1); }
121
128
  if (value === 'true') value = true;
122
129
  else if (value === 'false') value = false;
123
130
  else if (/^\d+$/.test(value)) value = parseInt(value, 10);
124
-
125
131
  settings[key] = value;
126
132
  saveSettings(settings);
127
133
  console.log(`${key} = ${key === 'basicAuthPassword' ? '********' : value}`);
@@ -143,7 +149,7 @@ if (command === 'auth') {
143
149
  const sub = args[1];
144
150
  const settings = loadSettings();
145
151
 
146
- if (sub === 'status') {
152
+ if (!sub || sub === 'status') {
147
153
  if (settings.basicAuthEnabled) {
148
154
  console.log('Basic auth: enabled');
149
155
  console.log(` user: ${settings.basicAuthUser || '(not set)'}`);
@@ -151,6 +157,11 @@ if (command === 'auth') {
151
157
  } else {
152
158
  console.log('Basic auth: disabled');
153
159
  }
160
+ if (!sub) {
161
+ console.log('\nUsage:');
162
+ console.log(' deckide auth on [user] [password]');
163
+ console.log(' deckide auth off');
164
+ }
154
165
  process.exit(0);
155
166
  }
156
167
 
@@ -160,58 +171,29 @@ if (command === 'auth') {
160
171
  delete settings.basicAuthPassword;
161
172
  saveSettings(settings);
162
173
  console.log('Basic auth disabled.');
174
+ if (isServerRunning()) console.log('Run "deckide restart" to apply.');
163
175
  process.exit(0);
164
176
  }
165
177
 
166
178
  if (sub === 'on') {
167
179
  const user = args[2];
168
180
  const password = args[3];
181
+ const genUser = user || 'admin';
182
+ const genPassword = password || crypto.randomBytes(16).toString('base64url');
169
183
 
170
- if (!user || !password) {
171
- // Generate random password if not provided
172
- const genUser = user || 'admin';
173
- const genPassword = crypto.randomBytes(16).toString('base64url');
174
- settings.basicAuthEnabled = true;
175
- settings.basicAuthUser = genUser;
176
- settings.basicAuthPassword = genPassword;
177
- saveSettings(settings);
178
- console.log('Basic auth enabled.');
179
- console.log(` user: ${genUser}`);
180
- console.log(` password: ${genPassword}`);
181
- console.log('');
182
- console.log('Restart the server for changes to take effect.');
183
- process.exit(0);
184
- }
185
-
186
- if (password.length < 8) {
184
+ if (password && password.length < 8) {
187
185
  console.error('Error: password must be at least 8 characters.');
188
186
  process.exit(1);
189
187
  }
190
188
 
191
189
  settings.basicAuthEnabled = true;
192
- settings.basicAuthUser = user;
193
- settings.basicAuthPassword = password;
190
+ settings.basicAuthUser = genUser;
191
+ settings.basicAuthPassword = genPassword;
194
192
  saveSettings(settings);
195
193
  console.log('Basic auth enabled.');
196
- console.log(` user: ${user}`);
197
- console.log('Restart the server for changes to take effect.');
198
- process.exit(0);
199
- }
200
-
201
- if (!sub) {
202
- // Default to status
203
- const enabled = settings.basicAuthEnabled;
204
- if (enabled) {
205
- console.log('Basic auth: enabled');
206
- console.log(` user: ${settings.basicAuthUser || '(not set)'}`);
207
- } else {
208
- console.log('Basic auth: disabled');
209
- }
210
- console.log('');
211
- console.log('Usage:');
212
- console.log(' deckide auth on [user] [password] Enable auth');
213
- console.log(' deckide auth off Disable auth');
214
- console.log(' deckide auth status Show status');
194
+ console.log(` user: ${genUser}`);
195
+ if (!password) console.log(` password: ${genPassword}`);
196
+ if (isServerRunning()) console.log('Run "deckide restart" to apply.');
215
197
  process.exit(0);
216
198
  }
217
199
 
@@ -222,108 +204,199 @@ if (command === 'auth') {
222
204
  // ── deckide status ──
223
205
  if (command === 'status') {
224
206
  const settings = loadSettings();
225
- const daemonInfoPath = path.join(dataDir, 'pty-daemon.json');
207
+ const port = settings.port || 8787;
226
208
 
227
- console.log('Deck IDE status');
228
- console.log(` data dir: ${dataDir}`);
229
- console.log(` port: ${settings.port || 8787}`);
230
- console.log(` auth: ${settings.basicAuthEnabled ? 'enabled' : 'disabled'}`);
209
+ console.log('Deck IDE');
210
+ console.log(` data: ${dataDir}`);
211
+ console.log(` port: ${port}`);
212
+ console.log(` auth: ${settings.basicAuthEnabled ? 'enabled' : 'disabled'}`);
231
213
 
232
- // Check if server is running
233
- const port = settings.port || 8787;
234
- try {
235
- const res = execSync(`curl -s -o /dev/null -w "%{http_code}" http://localhost:${port}/health`, {
236
- timeout: 3000,
237
- }).toString().trim();
238
- console.log(` server: running (port ${port})`);
239
- } catch {
240
- console.log(' server: not running');
214
+ if (isServerRunning()) {
215
+ console.log(` server: \x1b[32mrunning\x1b[0m http://localhost:${port}`);
216
+ } else {
217
+ console.log(' server: \x1b[31mstopped\x1b[0m');
241
218
  }
242
219
 
243
- // Check PTY daemon
244
- if (fs.existsSync(daemonInfoPath)) {
220
+ // Check PID file
221
+ if (fs.existsSync(pidFile)) {
245
222
  try {
246
- const info = JSON.parse(fs.readFileSync(daemonInfoPath, 'utf-8'));
247
- console.log(` pty daemon: running (pid ${info.pid}, port ${info.port})`);
223
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
224
+ process.kill(pid, 0); // Check if process exists
225
+ console.log(` pid: ${pid}`);
248
226
  } catch {
249
- console.log(' pty daemon: unknown');
227
+ // stale pid file
250
228
  }
251
- } else {
252
- console.log(' pty daemon: not running');
229
+ }
230
+
231
+ const daemonInfoPath = path.join(dataDir, 'pty-daemon.json');
232
+ if (fs.existsSync(daemonInfoPath)) {
233
+ try {
234
+ const info = JSON.parse(fs.readFileSync(daemonInfoPath, 'utf-8'));
235
+ console.log(` pty: pid ${info.pid}, port ${info.port}`);
236
+ } catch {}
253
237
  }
254
238
 
255
239
  process.exit(0);
256
240
  }
257
241
 
242
+ // ── deckide logs ──
243
+ if (command === 'logs') {
244
+ if (!fs.existsSync(logFile)) {
245
+ console.log('No logs found.');
246
+ process.exit(0);
247
+ }
248
+ const follow = args.includes('-f') || args.includes('--follow');
249
+ if (follow) {
250
+ const tail = spawn('tail', ['-f', logFile], { stdio: 'inherit' });
251
+ tail.on('exit', () => process.exit(0));
252
+ } else {
253
+ const lines = fs.readFileSync(logFile, 'utf-8');
254
+ // Show last 50 lines
255
+ const arr = lines.split('\n');
256
+ console.log(arr.slice(-51).join('\n'));
257
+ }
258
+ if (!args.includes('-f') && !args.includes('--follow')) process.exit(0);
259
+ }
260
+
258
261
  // ── deckide stop ──
259
262
  if (command === 'stop') {
260
- const settings = loadSettings();
261
- const port = settings.port || 8787;
263
+ if (!isServerRunning()) {
264
+ console.log('Server is not running.');
265
+ process.exit(0);
266
+ }
267
+ const port = getPort();
262
268
  try {
263
- execSync(`curl -s -X POST http://localhost:${port}/api/shutdown -H "Content-Type: application/json" -d '{"terminateDaemon":true}'`, {
264
- timeout: 5000,
269
+ execSync(`curl -sf -X POST http://localhost:${port}/api/shutdown -H "Content-Type: application/json" -d '{"terminateDaemon":true}'`, {
270
+ timeout: 5000, stdio: 'ignore',
265
271
  });
272
+ // Clean up pid file
273
+ try { fs.unlinkSync(pidFile); } catch {}
266
274
  console.log('Server stopped.');
267
275
  } catch {
268
- console.log('Server is not running or could not be reached.');
276
+ console.error('Failed to stop server.');
269
277
  }
270
278
  process.exit(0);
271
279
  }
272
280
 
273
- // ── deckide (start server) ──
281
+ // ── deckide restart ──
282
+ if (command === 'restart') {
283
+ if (isServerRunning()) {
284
+ const port = getPort();
285
+ try {
286
+ execSync(`curl -sf -X POST http://localhost:${port}/api/shutdown -H "Content-Type: application/json" -d '{"terminateDaemon":true}'`, {
287
+ timeout: 5000, stdio: 'ignore',
288
+ });
289
+ try { fs.unlinkSync(pidFile); } catch {}
290
+ console.log('Server stopped.');
291
+ } catch {}
292
+ // Wait a moment for port to free
293
+ await new Promise(r => setTimeout(r, 1000));
294
+ }
295
+ // Re-exec as start (background)
296
+ const restartArgs = ['start', ...args.slice(1)];
297
+ const child = spawn(process.execPath, [fileURLToPath(import.meta.url), ...restartArgs], {
298
+ stdio: 'inherit',
299
+ });
300
+ child.on('exit', (code) => process.exit(code));
301
+ }
302
+
303
+ // ── deckide / deckide start ──
274
304
 
275
305
  // Parse start options
276
- const startOptions = {
277
- port: null,
278
- host: null,
279
- open: true,
280
- };
281
-
282
- for (let i = 0; i < args.length; i++) {
283
- const arg = args[i];
284
- if ((arg === '--port' || arg === '-p') && args[i + 1]) {
285
- startOptions.port = parseInt(args[i + 1], 10);
306
+ const isStart = command === 'start' || !command;
307
+ if (!isStart) {
308
+ console.error(`Unknown command: ${command}`);
309
+ console.error('Run "deckide help" for usage.');
310
+ process.exit(1);
311
+ }
312
+
313
+ const startArgs = command === 'start' ? args.slice(1) : args;
314
+ const startOptions = { port: null, host: null, open: true, fg: false };
315
+
316
+ for (let i = 0; i < startArgs.length; i++) {
317
+ const arg = startArgs[i];
318
+ if ((arg === '--port' || arg === '-p') && startArgs[i + 1]) {
319
+ startOptions.port = parseInt(startArgs[i + 1], 10);
286
320
  i++;
287
- } else if (arg === '--host' && args[i + 1]) {
288
- startOptions.host = args[i + 1];
321
+ } else if (arg === '--host' && startArgs[i + 1]) {
322
+ startOptions.host = startArgs[i + 1];
289
323
  i++;
290
324
  } else if (arg === '--no-open') {
291
325
  startOptions.open = false;
292
- } else if (arg && !arg.startsWith('-')) {
293
- console.error(`Unknown command: ${arg}`);
294
- console.error('Run "deckide help" for usage.');
295
- process.exit(1);
326
+ } else if (arg === '--fg') {
327
+ startOptions.fg = true;
296
328
  }
297
329
  }
298
330
 
299
- // Load settings and apply CLI overrides
300
331
  const settings = loadSettings();
301
332
  const port = startOptions.port || settings.port || 8787;
302
333
  const host = startOptions.host || settings.host || '0.0.0.0';
303
334
 
335
+ // Check if already running
336
+ if (isServerRunning()) {
337
+ console.log(`Server is already running on http://localhost:${port}`);
338
+ process.exit(0);
339
+ }
340
+
341
+ // ── Background mode (default) ──
342
+ if (!startOptions.fg) {
343
+ fs.mkdirSync(dataDir, { recursive: true });
344
+
345
+ const out = fs.openSync(logFile, 'a');
346
+ const err = fs.openSync(logFile, 'a');
347
+
348
+ const fgArgs = ['start', '--fg', '--no-open', '-p', String(port), '--host', host];
349
+ const child = spawn(process.execPath, [fileURLToPath(import.meta.url), ...fgArgs], {
350
+ detached: true,
351
+ stdio: ['ignore', out, err],
352
+ env: { ...process.env, DECKIDE_DATA_DIR: dataDir },
353
+ });
354
+
355
+ // Write PID file
356
+ fs.writeFileSync(pidFile, String(child.pid));
357
+ child.unref();
358
+
359
+ // Wait for server to be ready
360
+ const startTime = Date.now();
361
+ let ready = false;
362
+ while (Date.now() - startTime < 8000) {
363
+ await new Promise(r => setTimeout(r, 300));
364
+ if (isServerRunning()) { ready = true; break; }
365
+ }
366
+
367
+ if (ready) {
368
+ const url = `http://localhost:${port}`;
369
+ console.log(`Deck IDE running at ${url} (pid: ${child.pid})`);
370
+
371
+ if (startOptions.open) {
372
+ try {
373
+ if (process.platform === 'darwin') execSync(`open ${url}`);
374
+ else if (process.platform === 'win32') execSync(`start ${url}`);
375
+ else execSync(`xdg-open ${url}`);
376
+ } catch {}
377
+ }
378
+ } else {
379
+ console.error('Server failed to start. Check logs: deckide logs');
380
+ }
381
+
382
+ process.exit(0);
383
+ }
384
+
385
+ // ── Foreground mode (--fg) ──
304
386
  process.env.DECKIDE_DATA_DIR = dataDir;
305
387
  process.env.PORT = String(port);
306
388
  process.env.HOST = host;
307
389
 
308
- // Import and start the server
309
390
  const { createServer } = await import(path.join(__dirname, '..', 'dist', 'server.js'));
310
391
  await createServer();
311
392
 
312
- // Open browser after server starts
313
393
  if (startOptions.open) {
314
394
  const url = `http://localhost:${port}`;
315
395
  setTimeout(() => {
316
396
  try {
317
- const platform = process.platform;
318
- if (platform === 'darwin') {
319
- execSync(`open ${url}`);
320
- } else if (platform === 'win32') {
321
- execSync(`start ${url}`);
322
- } else {
323
- execSync(`xdg-open ${url}`);
324
- }
325
- } catch {
326
- // Silently fail if browser can't be opened
327
- }
397
+ if (process.platform === 'darwin') execSync(`open ${url}`);
398
+ else if (process.platform === 'win32') execSync(`start ${url}`);
399
+ else execSync(`xdg-open ${url}`);
400
+ } catch {}
328
401
  }, 500);
329
402
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deckide",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "Deck IDE - Browser-based IDE with terminal, file explorer, and git integration",
5
5
  "type": "module",
6
6
  "bin": {