deckide 3.1.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 +377 -53
  2. package/package.json +1 -1
package/bin/deckide.js CHANGED
@@ -3,76 +3,400 @@
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import path from 'node:path';
5
5
  import os from 'node:os';
6
- import { execSync } from 'node:child_process';
6
+ import fs from 'node:fs';
7
+ import { execSync, spawn } from 'node:child_process';
8
+ import crypto from 'node:crypto';
7
9
 
8
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const dataDir = path.join(os.homedir(), '.deckide');
12
+ const settingsFile = path.join(dataDir, 'settings.json');
13
+ const pidFile = path.join(dataDir, 'server.pid');
14
+ const logFile = path.join(dataDir, 'server.log');
15
+
16
+ // ─── Settings helpers ───────────────────────────────────────────
17
+
18
+ function loadSettings() {
19
+ try {
20
+ return JSON.parse(fs.readFileSync(settingsFile, 'utf-8'));
21
+ } catch {
22
+ return {};
23
+ }
24
+ }
25
+
26
+ function saveSettings(settings) {
27
+ fs.mkdirSync(dataDir, { recursive: true });
28
+ fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
29
+ }
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
+
47
+ // ─── CLI ────────────────────────────────────────────────────────
9
48
 
10
- // Parse CLI arguments
11
49
  const args = process.argv.slice(2);
12
- const options = {
13
- port: 8787,
14
- host: '0.0.0.0',
15
- open: true,
16
- };
17
-
18
- for (let i = 0; i < args.length; i++) {
19
- const arg = args[i];
20
- if ((arg === '--port' || arg === '-p') && args[i + 1]) {
21
- options.port = parseInt(args[i + 1], 10);
22
- i++;
23
- } else if (arg === '--host' && args[i + 1]) {
24
- options.host = args[i + 1];
25
- i++;
26
- } else if (arg === '--no-open') {
27
- options.open = false;
28
- } else if (arg === '--help' || arg === '-h') {
29
- console.log(`
30
- Deck IDE - Browser-based IDE
50
+ const command = args[0];
51
+
52
+ // ── deckide version ──
53
+ if (command === '--version' || command === '-v') {
54
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
55
+ console.log(pkg.version);
56
+ process.exit(0);
57
+ }
58
+
59
+ // ── deckide help ──
60
+ if (command === '--help' || command === '-h' || command === 'help') {
61
+ console.log(`Deck IDE - Browser-based IDE
31
62
 
32
63
  Usage:
33
- deckide [options]
34
-
35
- Options:
36
- -p, --port <port> Port to listen on (default: 8787)
37
- --host <host> Host to bind to (default: 0.0.0.0)
38
- --no-open Don't open browser automatically
39
- -h, --help Show this help message
40
- -v, --version Show version
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
+
71
+ deckide config Show all settings
72
+ deckide config set <key> <val> Set a config value
73
+ deckide config get <key> Get a config value
74
+ deckide config reset Reset all settings
75
+
76
+ deckide auth on [user] [pass] Enable basic auth
77
+ deckide auth off Disable basic auth
78
+ deckide auth status Show auth status
79
+
80
+ Options (for start):
81
+ -p, --port <port> Port (default: 8787)
82
+ --host <host> Host (default: 0.0.0.0)
83
+ --no-open Don't open browser
84
+ --fg Run in foreground
85
+
86
+ Config keys:
87
+ port, host, cors, maxFileSize, trustProxy
41
88
  `);
89
+ process.exit(0);
90
+ }
91
+
92
+ // ── deckide config ──
93
+ if (command === 'config') {
94
+ const sub = args[1];
95
+ const settings = loadSettings();
96
+
97
+ if (!sub || sub === 'list') {
98
+ if (Object.keys(settings).length === 0) {
99
+ console.log('No custom settings. Using defaults.');
100
+ console.log(' port: 8787');
101
+ console.log(' host: 0.0.0.0');
102
+ } else {
103
+ for (const [key, value] of Object.entries(settings)) {
104
+ if (key === 'basicAuthPassword' && value) {
105
+ console.log(` ${key}: ********`);
106
+ } else {
107
+ console.log(` ${key}: ${value}`);
108
+ }
109
+ }
110
+ }
42
111
  process.exit(0);
43
- } else if (arg === '--version' || arg === '-v') {
44
- const pkg = await import(path.join(__dirname, '..', 'package.json'), { with: { type: 'json' } });
45
- console.log(pkg.default.version);
112
+ }
113
+
114
+ if (sub === 'get') {
115
+ const key = args[2];
116
+ if (!key) { console.error('Usage: deckide config get <key>'); process.exit(1); }
117
+ const val = settings[key];
118
+ if (val === undefined) console.log(`${key}: (not set)`);
119
+ else if (key === 'basicAuthPassword') console.log(`${key}: ********`);
120
+ else console.log(`${key}: ${val}`);
121
+ process.exit(0);
122
+ }
123
+
124
+ if (sub === 'set') {
125
+ const key = args[2];
126
+ let value = args[3];
127
+ if (!key || value === undefined) { console.error('Usage: deckide config set <key> <value>'); process.exit(1); }
128
+ if (value === 'true') value = true;
129
+ else if (value === 'false') value = false;
130
+ else if (/^\d+$/.test(value)) value = parseInt(value, 10);
131
+ settings[key] = value;
132
+ saveSettings(settings);
133
+ console.log(`${key} = ${key === 'basicAuthPassword' ? '********' : value}`);
134
+ process.exit(0);
135
+ }
136
+
137
+ if (sub === 'reset') {
138
+ saveSettings({});
139
+ console.log('Settings reset to defaults.');
46
140
  process.exit(0);
47
141
  }
142
+
143
+ console.error(`Unknown config command: ${sub}`);
144
+ process.exit(1);
48
145
  }
49
146
 
50
- // Set data directory to ~/.deckide/
51
- const dataDir = path.join(os.homedir(), '.deckide');
52
- process.env.DECKIDE_DATA_DIR = dataDir;
53
- process.env.PORT = String(options.port);
54
- process.env.HOST = options.host;
147
+ // ── deckide auth ──
148
+ if (command === 'auth') {
149
+ const sub = args[1];
150
+ const settings = loadSettings();
55
151
 
56
- // Import and start the server
57
- const { createServer } = await import(path.join(__dirname, '..', 'dist', 'server.js'));
152
+ if (!sub || sub === 'status') {
153
+ if (settings.basicAuthEnabled) {
154
+ console.log('Basic auth: enabled');
155
+ console.log(` user: ${settings.basicAuthUser || '(not set)'}`);
156
+ console.log(` password: ${settings.basicAuthPassword ? '********' : '(not set)'}`);
157
+ } else {
158
+ console.log('Basic auth: disabled');
159
+ }
160
+ if (!sub) {
161
+ console.log('\nUsage:');
162
+ console.log(' deckide auth on [user] [password]');
163
+ console.log(' deckide auth off');
164
+ }
165
+ process.exit(0);
166
+ }
58
167
 
59
- const server = await createServer();
168
+ if (sub === 'off') {
169
+ settings.basicAuthEnabled = false;
170
+ delete settings.basicAuthUser;
171
+ delete settings.basicAuthPassword;
172
+ saveSettings(settings);
173
+ console.log('Basic auth disabled.');
174
+ if (isServerRunning()) console.log('Run "deckide restart" to apply.');
175
+ process.exit(0);
176
+ }
60
177
 
61
- // Open browser after server starts
62
- if (options.open) {
63
- const url = `http://localhost:${options.port}`;
64
- setTimeout(() => {
178
+ if (sub === 'on') {
179
+ const user = args[2];
180
+ const password = args[3];
181
+ const genUser = user || 'admin';
182
+ const genPassword = password || crypto.randomBytes(16).toString('base64url');
183
+
184
+ if (password && password.length < 8) {
185
+ console.error('Error: password must be at least 8 characters.');
186
+ process.exit(1);
187
+ }
188
+
189
+ settings.basicAuthEnabled = true;
190
+ settings.basicAuthUser = genUser;
191
+ settings.basicAuthPassword = genPassword;
192
+ saveSettings(settings);
193
+ console.log('Basic auth enabled.');
194
+ console.log(` user: ${genUser}`);
195
+ if (!password) console.log(` password: ${genPassword}`);
196
+ if (isServerRunning()) console.log('Run "deckide restart" to apply.');
197
+ process.exit(0);
198
+ }
199
+
200
+ console.error(`Unknown auth command: ${sub}`);
201
+ process.exit(1);
202
+ }
203
+
204
+ // ── deckide status ──
205
+ if (command === 'status') {
206
+ const settings = loadSettings();
207
+ const port = settings.port || 8787;
208
+
209
+ console.log('Deck IDE');
210
+ console.log(` data: ${dataDir}`);
211
+ console.log(` port: ${port}`);
212
+ console.log(` auth: ${settings.basicAuthEnabled ? 'enabled' : 'disabled'}`);
213
+
214
+ if (isServerRunning()) {
215
+ console.log(` server: \x1b[32mrunning\x1b[0m → http://localhost:${port}`);
216
+ } else {
217
+ console.log(' server: \x1b[31mstopped\x1b[0m');
218
+ }
219
+
220
+ // Check PID file
221
+ if (fs.existsSync(pidFile)) {
65
222
  try {
66
- const platform = process.platform;
67
- if (platform === 'darwin') {
68
- execSync(`open ${url}`);
69
- } else if (platform === 'win32') {
70
- execSync(`start ${url}`);
71
- } else {
72
- execSync(`xdg-open ${url}`);
73
- }
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}`);
74
226
  } catch {
75
- // Silently fail if browser can't be opened
227
+ // stale pid file
228
+ }
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 {}
237
+ }
238
+
239
+ process.exit(0);
240
+ }
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
+
261
+ // ── deckide stop ──
262
+ if (command === 'stop') {
263
+ if (!isServerRunning()) {
264
+ console.log('Server is not running.');
265
+ process.exit(0);
266
+ }
267
+ const port = getPort();
268
+ try {
269
+ execSync(`curl -sf -X POST http://localhost:${port}/api/shutdown -H "Content-Type: application/json" -d '{"terminateDaemon":true}'`, {
270
+ timeout: 5000, stdio: 'ignore',
271
+ });
272
+ // Clean up pid file
273
+ try { fs.unlinkSync(pidFile); } catch {}
274
+ console.log('Server stopped.');
275
+ } catch {
276
+ console.error('Failed to stop server.');
277
+ }
278
+ process.exit(0);
279
+ }
280
+
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 ──
304
+
305
+ // Parse start options
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);
320
+ i++;
321
+ } else if (arg === '--host' && startArgs[i + 1]) {
322
+ startOptions.host = startArgs[i + 1];
323
+ i++;
324
+ } else if (arg === '--no-open') {
325
+ startOptions.open = false;
326
+ } else if (arg === '--fg') {
327
+ startOptions.fg = true;
328
+ }
329
+ }
330
+
331
+ const settings = loadSettings();
332
+ const port = startOptions.port || settings.port || 8787;
333
+ const host = startOptions.host || settings.host || '0.0.0.0';
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 {}
76
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) ──
386
+ process.env.DECKIDE_DATA_DIR = dataDir;
387
+ process.env.PORT = String(port);
388
+ process.env.HOST = host;
389
+
390
+ const { createServer } = await import(path.join(__dirname, '..', 'dist', 'server.js'));
391
+ await createServer();
392
+
393
+ if (startOptions.open) {
394
+ const url = `http://localhost:${port}`;
395
+ setTimeout(() => {
396
+ try {
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 {}
77
401
  }, 500);
78
402
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deckide",
3
- "version": "3.1.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": {