vimd 0.1.6 → 0.1.8

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.
@@ -1 +1 @@
1
- {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/dev.ts"],"names":[],"mappings":"AAWA,UAAU,UAAU;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,IAAI,CAAC,CAwGf"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/dev.ts"],"names":[],"mappings":"AAYA,UAAU,UAAU;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,IAAI,CAAC,CA+If"}
@@ -6,6 +6,7 @@ import { LiveServer } from '../../core/server.js';
6
6
  import { PandocDetector } from '../../core/pandoc-detector.js';
7
7
  import { Logger } from '../../utils/logger.js';
8
8
  import { ProcessManager } from '../../utils/process-manager.js';
9
+ import { SessionManager } from '../../utils/session-manager.js';
9
10
  import * as path from 'path';
10
11
  import fs from 'fs-extra';
11
12
  export async function devCommand(filePath, options) {
@@ -23,44 +24,73 @@ export async function devCommand(filePath, options) {
23
24
  if (options.open !== undefined) {
24
25
  config.open = options.open;
25
26
  }
27
+ let port = config.port;
28
+ // 2. Clean dead sessions first
29
+ const deadCleaned = await SessionManager.cleanDeadSessions();
30
+ if (deadCleaned > 0) {
31
+ Logger.info(`Cleaned ${deadCleaned} stale session(s)`);
32
+ }
33
+ // 3. Check and cleanup previous session on same port
34
+ const cleanup = await SessionManager.cleanupSessionOnPort(port);
35
+ if (cleanup.killed) {
36
+ Logger.info(`Stopped previous session on port ${port}`);
37
+ }
38
+ if (cleanup.htmlRemoved) {
39
+ Logger.info('Removed previous preview file');
40
+ }
41
+ // 4. Check if port is available (might be used by other app)
42
+ if (!(await SessionManager.isPortAvailable(port))) {
43
+ const newPort = await SessionManager.findAvailablePort(port + 1);
44
+ Logger.warn(`Port ${port} is in use by another application`);
45
+ Logger.info(`Using port ${newPort} instead`);
46
+ port = newPort;
47
+ }
26
48
  Logger.info(`Theme: ${config.theme}`);
27
- Logger.info(`Port: ${config.port}`);
28
- // 2. Check pandoc installation
49
+ Logger.info(`Port: ${port}`);
50
+ // 5. Check pandoc installation
29
51
  PandocDetector.ensureInstalled();
30
- // 3. Check file exists
52
+ // 6. Check file exists
31
53
  const absolutePath = path.resolve(filePath);
32
54
  if (!(await fs.pathExists(absolutePath))) {
33
55
  Logger.error(`File not found: ${filePath}`);
34
56
  process.exit(1);
35
57
  }
36
- // 4. Prepare output HTML in source directory
58
+ // 7. Prepare output HTML in source directory
37
59
  const sourceDir = path.dirname(absolutePath);
38
60
  const basename = path.basename(filePath, path.extname(filePath));
39
61
  const htmlFileName = `vimd-preview-${basename}.html`;
40
62
  const htmlPath = path.join(sourceDir, htmlFileName);
41
- // 5. Prepare converter
63
+ // 8. Prepare converter
42
64
  const converter = new MarkdownConverter({
43
65
  theme: config.theme,
44
66
  pandocOptions: config.pandoc,
45
67
  customCSS: config.css,
46
68
  template: config.template,
47
69
  });
48
- // 6. Initial conversion
70
+ // 9. Initial conversion
49
71
  Logger.info('Converting markdown...');
50
72
  const html = await converter.convertWithTemplate(absolutePath);
51
73
  await converter.writeHTML(html, htmlPath);
52
74
  Logger.success('Conversion complete');
53
- // 7. Start live server from source directory
75
+ // 10. Start live server from source directory
54
76
  const server = new LiveServer({
55
- port: config.port,
77
+ port: port,
56
78
  host: config.host,
57
79
  open: config.open,
58
80
  root: sourceDir,
59
81
  });
60
82
  await server.start(htmlPath);
83
+ // 11. Save session
84
+ await SessionManager.saveSession({
85
+ pid: process.pid,
86
+ port: port,
87
+ htmlPath: htmlPath,
88
+ sourcePath: absolutePath,
89
+ startedAt: new Date().toISOString(),
90
+ });
61
91
  Logger.info(`Watching: ${filePath}`);
62
92
  Logger.info('Press Ctrl+C to stop');
63
- // 8. Start file watching
93
+ // 12. Start file watching
64
94
  const watcher = new FileWatcher(absolutePath, config.watch);
65
95
  watcher.onChange(async (changedPath) => {
66
96
  Logger.info('File changed, reconverting...');
@@ -77,7 +107,7 @@ export async function devCommand(filePath, options) {
77
107
  }
78
108
  });
79
109
  watcher.start();
80
- // 9. Register cleanup - remove generated HTML file
110
+ // 13. Register cleanup - remove generated HTML file and session
81
111
  ProcessManager.onExit(async () => {
82
112
  Logger.info('Shutting down...');
83
113
  await watcher.stop();
@@ -90,6 +120,8 @@ export async function devCommand(filePath, options) {
90
120
  catch {
91
121
  // Ignore errors when removing file
92
122
  }
123
+ // Remove session from registry
124
+ await SessionManager.removeSession(port);
93
125
  Logger.info('Cleanup complete');
94
126
  });
95
127
  }
@@ -1,54 +1,524 @@
1
- /* Dark Theme - VS Code inspired */
1
+ /*
2
+ * GitHub Dark Theme for vimd
3
+ * Based on github-markdown-css by Sindre Sorhus
4
+ * https://github.com/sindresorhus/github-markdown-css
5
+ * MIT License
6
+ */
7
+
8
+ /* Full page dark background */
9
+ html {
10
+ background-color: #0d1117;
11
+ }
12
+
13
+ /* Main body styles */
2
14
  body {
3
- font-family: 'Fira Code', Consolas, Monaco, monospace;
4
- line-height: 1.6;
5
- color: #d4d4d4;
6
- background: #1e1e1e;
7
- max-width: 900px;
15
+ color-scheme: dark;
16
+ -ms-text-size-adjust: 100%;
17
+ -webkit-text-size-adjust: 100%;
8
18
  margin: 0 auto;
9
19
  padding: 2rem;
20
+ max-width: 980px;
21
+ color: #f0f6fc;
22
+ background-color: #0d1117;
23
+ font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";
24
+ font-size: 16px;
25
+ line-height: 1.5;
26
+ word-wrap: break-word;
10
27
  }
11
28
 
12
- h1, h2, h3, h4, h5, h6 {
13
- color: #569cd6;
14
- margin-top: 1.5em;
15
- margin-bottom: 0.5em;
16
- }
17
-
18
- a {
19
- color: #569cd6;
29
+ /* Links */
30
+ body a {
31
+ background-color: transparent;
32
+ color: #4493f8;
20
33
  text-decoration: none;
21
34
  }
22
35
 
23
- a:hover {
36
+ body a:hover {
24
37
  text-decoration: underline;
25
38
  }
26
39
 
27
- code {
28
- background: #252526;
29
- color: #ce9178;
30
- padding: 0.2em 0.4em;
31
- border-radius: 3px;
32
- font-family: 'Fira Code', monospace;
40
+ /* Strong / Bold */
41
+ body b,
42
+ body strong {
43
+ font-weight: 600;
33
44
  }
34
45
 
35
- pre {
36
- background: #252526;
37
- padding: 1em;
38
- border-radius: 5px;
39
- overflow-x: auto;
40
- border: 1px solid #333;
46
+ /* Headings */
47
+ body h1,
48
+ body h2,
49
+ body h3,
50
+ body h4,
51
+ body h5,
52
+ body h6 {
53
+ margin-top: 1.5rem;
54
+ margin-bottom: 1rem;
55
+ font-weight: 600;
56
+ line-height: 1.25;
57
+ }
58
+
59
+ body h1 {
60
+ margin: .67em 0;
61
+ font-weight: 600;
62
+ padding-bottom: .3em;
63
+ font-size: 2em;
64
+ border-bottom: 1px solid #3d444db3;
65
+ }
66
+
67
+ body h2 {
68
+ font-weight: 600;
69
+ padding-bottom: .3em;
70
+ font-size: 1.5em;
71
+ border-bottom: 1px solid #3d444db3;
72
+ }
73
+
74
+ body h3 {
75
+ font-weight: 600;
76
+ font-size: 1.25em;
77
+ }
78
+
79
+ body h4 {
80
+ font-weight: 600;
81
+ font-size: 1em;
82
+ }
83
+
84
+ body h5 {
85
+ font-weight: 600;
86
+ font-size: .875em;
87
+ }
88
+
89
+ body h6 {
90
+ font-weight: 600;
91
+ font-size: .85em;
92
+ color: #9198a1;
93
+ }
94
+
95
+ /* Paragraphs */
96
+ body p {
97
+ margin-top: 0;
98
+ margin-bottom: 10px;
99
+ }
100
+
101
+ /* Lists */
102
+ body ul,
103
+ body ol {
104
+ margin-top: 0;
105
+ margin-bottom: 0;
106
+ padding-left: 2em;
107
+ }
108
+
109
+ body li+li {
110
+ margin-top: .25em;
41
111
  }
42
112
 
43
- pre code {
44
- background: none;
45
- color: #d4d4d4;
113
+ /* Blockquote */
114
+ body blockquote {
115
+ margin: 0;
116
+ padding: 0 1em;
117
+ color: #9198a1;
118
+ border-left: .25em solid #3d444d;
119
+ }
120
+
121
+ /* Horizontal Rule */
122
+ body hr {
123
+ box-sizing: content-box;
124
+ overflow: hidden;
125
+ background: transparent;
126
+ border-bottom: 1px solid #3d444db3;
127
+ height: .25em;
46
128
  padding: 0;
129
+ margin: 1.5rem 0;
130
+ background-color: #3d444d;
131
+ border: 0;
132
+ }
133
+
134
+ /* Images */
135
+ body img {
136
+ border-style: none;
137
+ max-width: 100%;
138
+ box-sizing: content-box;
139
+ }
140
+
141
+ /* Tables */
142
+ body table {
143
+ border-spacing: 0;
144
+ border-collapse: collapse;
145
+ display: block;
146
+ width: max-content;
147
+ max-width: 100%;
148
+ overflow: auto;
149
+ }
150
+
151
+ body table th {
152
+ font-weight: 600;
153
+ }
154
+
155
+ body table th,
156
+ body table td {
157
+ padding: 6px 13px;
158
+ border: 1px solid #3d444d;
159
+ }
160
+
161
+ body table tr {
162
+ background-color: #0d1117;
163
+ border-top: 1px solid #3d444db3;
164
+ }
165
+
166
+ body table tr:nth-child(2n) {
167
+ background-color: #151b23;
168
+ }
169
+
170
+ /* Inline Code */
171
+ body code,
172
+ body tt {
173
+ padding: .2em .4em;
174
+ margin: 0;
175
+ font-size: 85%;
176
+ white-space: break-spaces;
177
+ background-color: #656c7633;
178
+ border-radius: 6px;
179
+ font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
180
+ }
181
+
182
+ /* Code Blocks */
183
+ body pre {
184
+ margin-top: 0;
185
+ margin-bottom: 0;
186
+ font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
187
+ font-size: 12px;
188
+ word-wrap: normal;
189
+ }
190
+
191
+ body pre code {
192
+ font-size: 100%;
193
+ }
194
+
195
+ body pre>code {
196
+ padding: 0;
197
+ margin: 0;
198
+ word-break: normal;
199
+ white-space: pre;
200
+ background: transparent;
201
+ border: 0;
202
+ }
203
+
204
+ body .highlight pre,
205
+ body pre {
206
+ padding: 1rem;
207
+ overflow: auto;
208
+ font-size: 85%;
209
+ line-height: 1.45;
210
+ color: #f0f6fc;
211
+ background-color: #151b23;
212
+ border-radius: 6px;
213
+ }
214
+
215
+ body pre code,
216
+ body pre tt {
217
+ display: inline;
218
+ max-width: auto;
219
+ padding: 0;
220
+ margin: 0;
221
+ overflow: visible;
222
+ line-height: inherit;
223
+ word-wrap: normal;
224
+ background-color: transparent;
225
+ border: 0;
226
+ }
227
+
228
+ /* Keyboard */
229
+ body kbd {
230
+ display: inline-block;
231
+ padding: 0.25rem;
232
+ font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
233
+ line-height: 10px;
234
+ color: #f0f6fc;
235
+ vertical-align: middle;
236
+ background-color: #151b23;
237
+ border: solid 1px #3d444db3;
238
+ border-bottom-color: #3d444db3;
239
+ border-radius: 6px;
240
+ box-shadow: inset 0 -1px 0 #3d444db3;
241
+ }
242
+
243
+ /* Mark / Highlight */
244
+ body mark {
245
+ background-color: #bb800926;
246
+ color: #f0f6fc;
247
+ }
248
+
249
+ /* Definition Lists */
250
+ body dl {
251
+ padding: 0;
252
+ }
253
+
254
+ body dl dt {
255
+ padding: 0;
256
+ margin-top: 1rem;
257
+ font-size: 1em;
258
+ font-style: italic;
259
+ font-weight: 600;
260
+ }
261
+
262
+ body dl dd {
263
+ padding: 0 1rem;
264
+ margin-bottom: 1rem;
265
+ }
266
+
267
+ /* Table of Contents (pandoc) */
268
+ nav#TOC {
269
+ background-color: #151b23;
270
+ border: 1px solid #3d444d;
271
+ border-radius: 6px;
272
+ padding: 1rem 1.5rem;
273
+ margin-bottom: 1.5rem;
274
+ }
275
+
276
+ nav#TOC > ul {
277
+ margin: 0;
278
+ padding-left: 0;
279
+ list-style: none;
280
+ }
281
+
282
+ nav#TOC ul {
283
+ padding-left: 1.5em;
284
+ }
285
+
286
+ nav#TOC li {
287
+ margin: 0.4em 0;
288
+ }
289
+
290
+ nav#TOC a {
291
+ color: #4493f8;
292
+ }
293
+
294
+ nav#TOC a:hover {
295
+ color: #58a6ff;
296
+ }
297
+
298
+ /* Pandoc Syntax Highlighting - GitHub Dark Colors */
299
+ div.sourceCode {
300
+ background-color: #151b23;
301
+ border-radius: 6px;
302
+ overflow: auto;
303
+ }
304
+
305
+ pre.sourceCode {
306
+ margin: 0;
307
+ padding: 1rem;
308
+ }
309
+
310
+ code.sourceCode {
311
+ background: transparent;
312
+ color: #f0f6fc;
313
+ }
314
+
315
+ /* Comments */
316
+ code.sourceCode span.co,
317
+ .sourceCode .co {
318
+ color: #9198a1;
319
+ }
320
+
321
+ /* Constants */
322
+ code.sourceCode span.cn,
323
+ code.sourceCode span.c1,
324
+ code.sourceCode span.dv,
325
+ code.sourceCode span.bn,
326
+ code.sourceCode span.fl,
327
+ .sourceCode .cn,
328
+ .sourceCode .c1,
329
+ .sourceCode .dv,
330
+ .sourceCode .bn,
331
+ .sourceCode .fl {
332
+ color: #79c0ff;
333
+ }
334
+
335
+ /* Strings */
336
+ code.sourceCode span.st,
337
+ code.sourceCode span.ch,
338
+ code.sourceCode span.vs,
339
+ .sourceCode .st,
340
+ .sourceCode .ch,
341
+ .sourceCode .vs {
342
+ color: #a5d6ff;
343
+ }
344
+
345
+ /* Keywords */
346
+ code.sourceCode span.kw,
347
+ code.sourceCode span.cf,
348
+ .sourceCode .kw,
349
+ .sourceCode .cf {
350
+ color: #ff7b72;
351
+ }
352
+
353
+ /* Functions */
354
+ code.sourceCode span.fu,
355
+ code.sourceCode span.bu,
356
+ .sourceCode .fu,
357
+ .sourceCode .bu {
358
+ color: #d2a8ff;
359
+ }
360
+
361
+ /* Variables */
362
+ code.sourceCode span.va,
363
+ .sourceCode .va {
364
+ color: #ffa657;
365
+ }
366
+
367
+ /* Data Types */
368
+ code.sourceCode span.dt,
369
+ .sourceCode .dt {
370
+ color: #79c0ff;
371
+ }
372
+
373
+ /* Operators */
374
+ code.sourceCode span.op,
375
+ .sourceCode .op {
376
+ color: #f0f6fc;
377
+ }
378
+
379
+ /* Attributes / Decorators */
380
+ code.sourceCode span.at,
381
+ .sourceCode .at {
382
+ color: #7ee787;
383
+ }
384
+
385
+ /* Preprocessor */
386
+ code.sourceCode span.pp,
387
+ .sourceCode .pp {
388
+ color: #ff7b72;
389
+ }
390
+
391
+ /* Import */
392
+ code.sourceCode span.im,
393
+ .sourceCode .im {
394
+ color: #ff7b72;
395
+ }
396
+
397
+ /* Special characters */
398
+ code.sourceCode span.sc,
399
+ .sourceCode .sc {
400
+ color: #a5d6ff;
401
+ }
402
+
403
+ /* Regular expressions */
404
+ code.sourceCode span.ss,
405
+ .sourceCode .ss {
406
+ color: #a5d6ff;
407
+ }
408
+
409
+ /* Annotations */
410
+ code.sourceCode span.an,
411
+ .sourceCode .an {
412
+ color: #9198a1;
413
+ }
414
+
415
+ /* Documentation */
416
+ code.sourceCode span.do,
417
+ .sourceCode .do {
418
+ color: #9198a1;
419
+ }
420
+
421
+ /* Errors */
422
+ code.sourceCode span.er,
423
+ code.sourceCode span.al,
424
+ .sourceCode .er,
425
+ .sourceCode .al {
426
+ color: #f85149;
427
+ }
428
+
429
+ /* Warnings */
430
+ code.sourceCode span.wa,
431
+ .sourceCode .wa {
432
+ color: #d29922;
433
+ }
434
+
435
+ /* Information */
436
+ code.sourceCode span.in,
437
+ .sourceCode .in {
438
+ color: #9198a1;
439
+ }
440
+
441
+ /* GitHub .pl-* classes (for compatibility) */
442
+ body .pl-c {
443
+ color: #9198a1;
444
+ }
445
+
446
+ body .pl-c1,
447
+ body .pl-s .pl-v {
448
+ color: #79c0ff;
449
+ }
450
+
451
+ body .pl-e,
452
+ body .pl-en {
453
+ color: #d2a8ff;
454
+ }
455
+
456
+ body .pl-smi,
457
+ body .pl-s .pl-s1 {
458
+ color: #f0f6fc;
459
+ }
460
+
461
+ body .pl-ent {
462
+ color: #7ee787;
463
+ }
464
+
465
+ body .pl-k {
466
+ color: #ff7b72;
467
+ }
468
+
469
+ body .pl-s,
470
+ body .pl-pds,
471
+ body .pl-s .pl-pse .pl-s1,
472
+ body .pl-sr,
473
+ body .pl-sr .pl-cce,
474
+ body .pl-sr .pl-sre,
475
+ body .pl-sr .pl-sra {
476
+ color: #a5d6ff;
477
+ }
478
+
479
+ body .pl-v,
480
+ body .pl-smw {
481
+ color: #ffa657;
482
+ }
483
+
484
+ body .pl-bu {
485
+ color: #f85149;
486
+ }
487
+
488
+ /* Selection */
489
+ ::selection {
490
+ background-color: #264f78;
491
+ color: #ffffff;
492
+ }
493
+
494
+ /* Scrollbar (WebKit) */
495
+ ::-webkit-scrollbar {
496
+ width: 10px;
497
+ height: 10px;
498
+ }
499
+
500
+ ::-webkit-scrollbar-track {
501
+ background: #0d1117;
502
+ }
503
+
504
+ ::-webkit-scrollbar-thumb {
505
+ background: #3d444d;
506
+ border-radius: 5px;
507
+ }
508
+
509
+ ::-webkit-scrollbar-thumb:hover {
510
+ background: #4f5761;
511
+ }
512
+
513
+ /* Focus states */
514
+ body a:focus {
515
+ outline: 2px solid #1f6feb;
516
+ outline-offset: -2px;
47
517
  }
48
518
 
49
- blockquote {
50
- border-left: 4px solid #569cd6;
51
- padding-left: 1em;
52
- color: #b5b5b5;
53
- margin-left: 0;
519
+ /* Responsive */
520
+ @media (max-width: 768px) {
521
+ body {
522
+ padding: 1rem;
523
+ }
54
524
  }
@@ -0,0 +1,65 @@
1
+ export interface VimdSession {
2
+ pid: number;
3
+ port: number;
4
+ htmlPath: string;
5
+ sourcePath: string;
6
+ startedAt: string;
7
+ }
8
+ export interface VimdSessions {
9
+ [port: string]: VimdSession;
10
+ }
11
+ export interface CleanupResult {
12
+ killed: boolean;
13
+ htmlRemoved: boolean;
14
+ previousPort?: number;
15
+ previousSource?: string;
16
+ }
17
+ export declare class SessionManager {
18
+ private static readonly SESSIONS_DIR;
19
+ private static readonly SESSIONS_FILE;
20
+ /**
21
+ * Load all sessions from file
22
+ */
23
+ static loadSessions(): Promise<VimdSessions>;
24
+ /**
25
+ * Save sessions to file
26
+ */
27
+ static saveSessions(sessions: VimdSessions): Promise<void>;
28
+ /**
29
+ * Get session for specific port
30
+ */
31
+ static getSession(port: number): Promise<VimdSession | null>;
32
+ /**
33
+ * Save session for specific port
34
+ */
35
+ static saveSession(session: VimdSession): Promise<void>;
36
+ /**
37
+ * Remove session for specific port
38
+ */
39
+ static removeSession(port: number): Promise<void>;
40
+ /**
41
+ * Check if a process is alive
42
+ */
43
+ static isProcessAlive(pid: number): boolean;
44
+ /**
45
+ * Kill a process safely
46
+ */
47
+ static killProcess(pid: number): Promise<boolean>;
48
+ /**
49
+ * Clean up dead sessions (PIDs that no longer exist)
50
+ */
51
+ static cleanDeadSessions(): Promise<number>;
52
+ /**
53
+ * Clean up previous session on the same port
54
+ */
55
+ static cleanupSessionOnPort(port: number): Promise<CleanupResult>;
56
+ /**
57
+ * Check if a port is available
58
+ */
59
+ static isPortAvailable(port: number): Promise<boolean>;
60
+ /**
61
+ * Find an available port starting from startPort
62
+ */
63
+ static findAvailablePort(startPort: number): Promise<number>;
64
+ }
65
+ //# sourceMappingURL=session-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../../src/utils/session-manager.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAkC;IACtE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAGnC;IAEF;;OAEG;WACU,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC;IAWlD;;OAEG;WACU,YAAY,CAAC,QAAQ,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAKhE;;OAEG;WACU,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAKlE;;OAEG;WACU,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAM7D;;OAEG;WACU,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMvD;;OAEG;IACH,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAS3C;;OAEG;WACU,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAgBvD;;OAEG;WACU,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC;IA0BjD;;OAEG;WACU,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAmCvE;;OAEG;IACH,MAAM,CAAC,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAYtD;;OAEG;WACU,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAcnE"}
@@ -0,0 +1,168 @@
1
+ // src/utils/session-manager.ts
2
+ import fs from 'fs-extra';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import * as net from 'net';
6
+ export class SessionManager {
7
+ /**
8
+ * Load all sessions from file
9
+ */
10
+ static async loadSessions() {
11
+ try {
12
+ if (await fs.pathExists(this.SESSIONS_FILE)) {
13
+ return await fs.readJson(this.SESSIONS_FILE);
14
+ }
15
+ }
16
+ catch {
17
+ // Corrupted file, start fresh
18
+ }
19
+ return {};
20
+ }
21
+ /**
22
+ * Save sessions to file
23
+ */
24
+ static async saveSessions(sessions) {
25
+ await fs.ensureDir(this.SESSIONS_DIR);
26
+ await fs.writeJson(this.SESSIONS_FILE, sessions, { spaces: 2 });
27
+ }
28
+ /**
29
+ * Get session for specific port
30
+ */
31
+ static async getSession(port) {
32
+ const sessions = await this.loadSessions();
33
+ return sessions[port.toString()] || null;
34
+ }
35
+ /**
36
+ * Save session for specific port
37
+ */
38
+ static async saveSession(session) {
39
+ const sessions = await this.loadSessions();
40
+ sessions[session.port.toString()] = session;
41
+ await this.saveSessions(sessions);
42
+ }
43
+ /**
44
+ * Remove session for specific port
45
+ */
46
+ static async removeSession(port) {
47
+ const sessions = await this.loadSessions();
48
+ delete sessions[port.toString()];
49
+ await this.saveSessions(sessions);
50
+ }
51
+ /**
52
+ * Check if a process is alive
53
+ */
54
+ static isProcessAlive(pid) {
55
+ try {
56
+ process.kill(pid, 0);
57
+ return true;
58
+ }
59
+ catch {
60
+ return false;
61
+ }
62
+ }
63
+ /**
64
+ * Kill a process safely
65
+ */
66
+ static async killProcess(pid) {
67
+ try {
68
+ process.kill(pid, 'SIGTERM');
69
+ // Wait a bit for graceful shutdown
70
+ await new Promise((resolve) => setTimeout(resolve, 500));
71
+ // Force kill if still alive
72
+ if (this.isProcessAlive(pid)) {
73
+ process.kill(pid, 'SIGKILL');
74
+ }
75
+ return true;
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
81
+ /**
82
+ * Clean up dead sessions (PIDs that no longer exist)
83
+ */
84
+ static async cleanDeadSessions() {
85
+ const sessions = await this.loadSessions();
86
+ let cleaned = 0;
87
+ for (const [port, session] of Object.entries(sessions)) {
88
+ if (!this.isProcessAlive(session.pid)) {
89
+ // Process is dead, clean up HTML if exists
90
+ try {
91
+ if (await fs.pathExists(session.htmlPath)) {
92
+ await fs.remove(session.htmlPath);
93
+ }
94
+ }
95
+ catch {
96
+ // Ignore removal errors
97
+ }
98
+ delete sessions[port];
99
+ cleaned++;
100
+ }
101
+ }
102
+ if (cleaned > 0) {
103
+ await this.saveSessions(sessions);
104
+ }
105
+ return cleaned;
106
+ }
107
+ /**
108
+ * Clean up previous session on the same port
109
+ */
110
+ static async cleanupSessionOnPort(port) {
111
+ const session = await this.getSession(port);
112
+ if (!session) {
113
+ return { killed: false, htmlRemoved: false };
114
+ }
115
+ const result = {
116
+ killed: false,
117
+ htmlRemoved: false,
118
+ previousPort: port,
119
+ previousSource: session.sourcePath,
120
+ };
121
+ // Kill process if alive
122
+ if (this.isProcessAlive(session.pid)) {
123
+ result.killed = await this.killProcess(session.pid);
124
+ }
125
+ // Remove HTML file
126
+ try {
127
+ if (await fs.pathExists(session.htmlPath)) {
128
+ await fs.remove(session.htmlPath);
129
+ result.htmlRemoved = true;
130
+ }
131
+ }
132
+ catch {
133
+ // Ignore removal errors
134
+ }
135
+ // Remove session from registry
136
+ await this.removeSession(port);
137
+ return result;
138
+ }
139
+ /**
140
+ * Check if a port is available
141
+ */
142
+ static isPortAvailable(port) {
143
+ return new Promise((resolve) => {
144
+ const server = net.createServer();
145
+ server.once('error', () => resolve(false));
146
+ server.once('listening', () => {
147
+ server.close();
148
+ resolve(true);
149
+ });
150
+ server.listen(port, '127.0.0.1');
151
+ });
152
+ }
153
+ /**
154
+ * Find an available port starting from startPort
155
+ */
156
+ static async findAvailablePort(startPort) {
157
+ const MAX_ATTEMPTS = 10;
158
+ for (let i = 0; i < MAX_ATTEMPTS; i++) {
159
+ const port = startPort + i;
160
+ if (await this.isPortAvailable(port)) {
161
+ return port;
162
+ }
163
+ }
164
+ throw new Error(`No available port found (tried ${startPort}-${startPort + MAX_ATTEMPTS - 1})`);
165
+ }
166
+ }
167
+ SessionManager.SESSIONS_DIR = path.join(os.tmpdir(), 'vimd');
168
+ SessionManager.SESSIONS_FILE = path.join(SessionManager.SESSIONS_DIR, 'sessions.json');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vimd",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Real-time Markdown preview tool with pandoc (view markdown)",
5
5
  "type": "module",
6
6
  "keywords": [