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.
- package/bin/deckide.js +377 -53
- 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
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 [
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
//
|
|
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
|
}
|