deckide 3.2.0 → 3.3.1
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
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
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
-p, --port <port> Port
|
|
60
|
-
--host <host> Host
|
|
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
|
|
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
|
-
|
|
105
|
-
|
|
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 (
|
|
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 =
|
|
193
|
-
settings.basicAuthPassword =
|
|
190
|
+
settings.basicAuthUser = genUser;
|
|
191
|
+
settings.basicAuthPassword = genPassword;
|
|
194
192
|
saveSettings(settings);
|
|
195
193
|
console.log('Basic auth enabled.');
|
|
196
|
-
console.log(` user: ${
|
|
197
|
-
console.log(
|
|
198
|
-
|
|
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
|
|
207
|
+
const port = settings.port || 8787;
|
|
226
208
|
|
|
227
|
-
console.log('Deck IDE
|
|
228
|
-
console.log(` data
|
|
229
|
-
console.log(` port:
|
|
230
|
-
console.log(` auth:
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
244
|
-
if (fs.existsSync(
|
|
220
|
+
// Check PID file
|
|
221
|
+
if (fs.existsSync(pidFile)) {
|
|
245
222
|
try {
|
|
246
|
-
const
|
|
247
|
-
|
|
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
|
-
|
|
227
|
+
// stale pid file
|
|
250
228
|
}
|
|
251
|
-
}
|
|
252
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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 -
|
|
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.
|
|
276
|
+
console.error('Failed to stop server.');
|
|
269
277
|
}
|
|
270
278
|
process.exit(0);
|
|
271
279
|
}
|
|
272
280
|
|
|
273
|
-
// ── deckide
|
|
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
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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' &&
|
|
288
|
-
startOptions.host =
|
|
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
|
|
293
|
-
|
|
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
|
-
|
|
318
|
-
if (platform === '
|
|
319
|
-
|
|
320
|
-
|
|
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
|
}
|