deckide 3.5.9 → 3.5.10

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.
@@ -4,7 +4,7 @@ import { spawn } from 'node-pty';
4
4
  import { TERMINAL_BUFFER_LIMIT } from '../config.js';
5
5
  import { createHttpError, handleError, readJson } from '../utils/error.js';
6
6
  import { getDefaultShell } from '../utils/shell.js';
7
- import { saveTerminal, deleteTerminal as deleteTerminalFromDb } from '../utils/database.js';
7
+ import { saveTerminal, deleteTerminal as deleteTerminalFromDb, updateTerminalBuffer, loadTerminals } from '../utils/database.js';
8
8
  // Track terminal index per deck for unique naming
9
9
  const deckTerminalCounters = new Map();
10
10
  export function createTerminalRouter(db, decks, terminals) {
@@ -143,6 +143,106 @@ export function createTerminalRouter(db, decks, terminals) {
143
143
  terminals.set(id, session);
144
144
  return session;
145
145
  }
146
+ // Restore persisted terminals from database (re-spawn PTY processes)
147
+ function restoreTerminals() {
148
+ const saved = loadTerminals(db);
149
+ for (const row of saved) {
150
+ if (!decks.has(row.deckId)) {
151
+ // Deck no longer exists, clean up
152
+ deleteTerminalFromDb(db, row.id);
153
+ continue;
154
+ }
155
+ const deck = decks.get(row.deckId);
156
+ let shell;
157
+ let shellArgs = [];
158
+ if (row.command) {
159
+ shell = getDefaultShell();
160
+ if (process.platform === 'win32') {
161
+ if (shell.toLowerCase().includes('powershell')) {
162
+ shellArgs = ['-NoExit', '-Command', row.command];
163
+ }
164
+ else {
165
+ shellArgs = ['/K', row.command];
166
+ }
167
+ }
168
+ else {
169
+ shellArgs = ['-c', row.command];
170
+ }
171
+ }
172
+ else {
173
+ shell = getDefaultShell();
174
+ }
175
+ const env = {};
176
+ for (const [key, value] of Object.entries(process.env)) {
177
+ if (value !== undefined)
178
+ env[key] = value;
179
+ }
180
+ env.TERM = env.TERM || 'xterm-256color';
181
+ env.COLORTERM = 'truecolor';
182
+ env.TERM_PROGRAM = 'xterm.js';
183
+ env.TERM_PROGRAM_VERSION = '5.0.0';
184
+ env.LANG = env.LANG || 'en_US.UTF-8';
185
+ env.LC_ALL = env.LC_ALL || 'en_US.UTF-8';
186
+ env.LC_CTYPE = env.LC_CTYPE || 'en_US.UTF-8';
187
+ try {
188
+ const isWindows = process.platform === 'win32';
189
+ const term = spawn(shell, shellArgs, {
190
+ cwd: deck.root,
191
+ cols: 120,
192
+ rows: 32,
193
+ env,
194
+ encoding: 'utf8',
195
+ ...(isWindows ? { useConpty: true } : {}),
196
+ });
197
+ const session = {
198
+ id: row.id,
199
+ deckId: row.deckId,
200
+ title: row.title,
201
+ command: row.command,
202
+ createdAt: row.createdAt,
203
+ sockets: new Set(),
204
+ buffer: row.buffer,
205
+ lastActive: Date.now(),
206
+ write: (data) => { try {
207
+ term.write(data);
208
+ }
209
+ catch { /* terminal may be dying */ } },
210
+ resize: (cols, rows) => { try {
211
+ term.resize(cols, rows);
212
+ }
213
+ catch { /* terminal may be dying */ } },
214
+ kill: () => { try {
215
+ term.kill();
216
+ }
217
+ catch { /* already dead */ } },
218
+ };
219
+ term.onData((data) => {
220
+ appendToTerminalBuffer(session, data);
221
+ session.lastActive = Date.now();
222
+ broadcastToSockets(session, data);
223
+ });
224
+ term.onExit(() => {
225
+ handleTerminalExit(row.id);
226
+ });
227
+ terminals.set(row.id, session);
228
+ console.log(`[TERMINAL] Restored terminal ${row.id}: shell=${shell}, cwd=${deck.root}, pid=${term.pid}`);
229
+ }
230
+ catch (error) {
231
+ console.error(`[TERMINAL] Failed to restore terminal ${row.id}:`, error);
232
+ deleteTerminalFromDb(db, row.id);
233
+ }
234
+ }
235
+ }
236
+ restoreTerminals();
237
+ // Periodically save terminal buffers to database
238
+ const bufferSaveInterval = setInterval(() => {
239
+ terminals.forEach((session) => {
240
+ try {
241
+ updateTerminalBuffer(db, session.id, session.buffer);
242
+ }
243
+ catch { /* ignore */ }
244
+ });
245
+ }, 10_000); // Every 10 seconds
146
246
  router.get('/', (c) => {
147
247
  const deckId = c.req.query('deckId');
148
248
  if (!deckId) {
package/dist/server.js CHANGED
@@ -11,7 +11,7 @@ import { PORT, HOST, NODE_ENV, BASIC_AUTH_USER, BASIC_AUTH_PASSWORD, CORS_ORIGIN
11
11
  import { securityHeaders } from './middleware/security.js';
12
12
  import { corsMiddleware } from './middleware/cors.js';
13
13
  import { basicAuthMiddleware, generateWsToken, isBasicAuthEnabled } from './middleware/auth.js';
14
- import { checkDatabaseIntegrity, handleDatabaseCorruption, initializeDatabase, loadPersistedState, } from './utils/database.js';
14
+ import { checkDatabaseIntegrity, handleDatabaseCorruption, initializeDatabase, loadPersistedState, updateTerminalBuffer, } from './utils/database.js';
15
15
  import { createWorkspaceRouter, getConfigHandler } from './routes/workspaces.js';
16
16
  import { createDeckRouter } from './routes/decks.js';
17
17
  import { createFileRouter } from './routes/files.js';
@@ -118,8 +118,12 @@ export async function createServer() {
118
118
  if (shutdownPromise)
119
119
  return shutdownPromise;
120
120
  shutdownPromise = (async () => {
121
- // Kill all terminals
121
+ // Save terminal buffers and kill all terminals
122
122
  terminals.forEach((session) => {
123
+ try {
124
+ updateTerminalBuffer(db, session.id, session.buffer);
125
+ }
126
+ catch { /* ignore */ }
123
127
  session.sockets.forEach((socket) => {
124
128
  try {
125
129
  socket.close(1000, 'Server shutting down');
@@ -103,8 +103,23 @@ export function loadPersistedState(db, workspaces, workspacePathIndex, decks) {
103
103
  });
104
104
  }
105
105
  export function saveTerminal(db, id, deckId, title, command, createdAt) {
106
- const stmt = db.prepare('INSERT OR REPLACE INTO terminals (id, deck_id, title, command, created_at) VALUES (?, ?, ?, ?, ?)');
107
- stmt.run(id, deckId, title, command, createdAt);
106
+ const stmt = db.prepare('INSERT OR REPLACE INTO terminals (id, deck_id, title, command, buffer, created_at) VALUES (?, ?, ?, ?, ?, ?)');
107
+ stmt.run(id, deckId, title, command, '', createdAt);
108
+ }
109
+ export function updateTerminalBuffer(db, id, buffer) {
110
+ const stmt = db.prepare('UPDATE terminals SET buffer = ? WHERE id = ?');
111
+ stmt.run(buffer, id);
112
+ }
113
+ export function loadTerminals(db) {
114
+ const rows = db.prepare('SELECT id, deck_id, title, command, buffer, created_at FROM terminals ORDER BY created_at ASC').all();
115
+ return rows.map((row) => ({
116
+ id: String(row.id),
117
+ deckId: String(row.deck_id),
118
+ title: String(row.title),
119
+ command: row.command ? String(row.command) : null,
120
+ buffer: String(row.buffer ?? ''),
121
+ createdAt: String(row.created_at),
122
+ }));
108
123
  }
109
124
  export function deleteTerminal(db, id) {
110
125
  const stmt = db.prepare('DELETE FROM terminals WHERE id = ?');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deckide",
3
- "version": "3.5.9",
3
+ "version": "3.5.10",
4
4
  "description": "Deck IDE - Browser-based IDE with terminal, file explorer, and git integration",
5
5
  "type": "module",
6
6
  "bin": {