vimd 0.1.6 → 0.1.7

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
  }
@@ -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.7",
4
4
  "description": "Real-time Markdown preview tool with pandoc (view markdown)",
5
5
  "type": "module",
6
6
  "keywords": [