@wong2kim/wmux 1.0.0 → 1.0.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.
Files changed (110) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +209 -157
  3. package/dist/cli/cli/client.js +1 -1
  4. package/dist/cli/cli/commands/browser.js +19 -19
  5. package/dist/cli/cli/index.js +58 -58
  6. package/dist/cli/shared/constants.js +17 -4
  7. package/dist/mcp/shared/constants.js +17 -4
  8. package/package.json +96 -84
  9. package/assets/icon.ico +0 -0
  10. package/assets/icon.svg +0 -6
  11. package/forge.config.ts +0 -61
  12. package/index.html +0 -12
  13. package/postcss.config.js +0 -6
  14. package/src/cli/client.ts +0 -76
  15. package/src/cli/commands/browser.ts +0 -128
  16. package/src/cli/commands/input.ts +0 -72
  17. package/src/cli/commands/notify.ts +0 -29
  18. package/src/cli/commands/pane.ts +0 -90
  19. package/src/cli/commands/surface.ts +0 -102
  20. package/src/cli/commands/system.ts +0 -95
  21. package/src/cli/commands/workspace.ts +0 -116
  22. package/src/cli/index.ts +0 -145
  23. package/src/cli/utils.ts +0 -44
  24. package/src/main/index.ts +0 -86
  25. package/src/main/ipc/handlers/clipboard.handler.ts +0 -20
  26. package/src/main/ipc/handlers/metadata.handler.ts +0 -56
  27. package/src/main/ipc/handlers/pty.handler.ts +0 -69
  28. package/src/main/ipc/handlers/session.handler.ts +0 -17
  29. package/src/main/ipc/handlers/shell.handler.ts +0 -11
  30. package/src/main/ipc/registerHandlers.ts +0 -31
  31. package/src/main/mcp/McpRegistrar.ts +0 -156
  32. package/src/main/metadata/MetadataCollector.ts +0 -58
  33. package/src/main/notification/ToastManager.ts +0 -32
  34. package/src/main/pipe/PipeServer.ts +0 -190
  35. package/src/main/pipe/RpcRouter.ts +0 -46
  36. package/src/main/pipe/handlers/_bridge.ts +0 -40
  37. package/src/main/pipe/handlers/browser.rpc.ts +0 -132
  38. package/src/main/pipe/handlers/input.rpc.ts +0 -120
  39. package/src/main/pipe/handlers/meta.rpc.ts +0 -59
  40. package/src/main/pipe/handlers/notify.rpc.ts +0 -53
  41. package/src/main/pipe/handlers/pane.rpc.ts +0 -39
  42. package/src/main/pipe/handlers/surface.rpc.ts +0 -43
  43. package/src/main/pipe/handlers/system.rpc.ts +0 -36
  44. package/src/main/pipe/handlers/workspace.rpc.ts +0 -52
  45. package/src/main/pty/AgentDetector.ts +0 -247
  46. package/src/main/pty/OscParser.ts +0 -81
  47. package/src/main/pty/PTYBridge.ts +0 -88
  48. package/src/main/pty/PTYManager.ts +0 -104
  49. package/src/main/pty/ShellDetector.ts +0 -63
  50. package/src/main/session/SessionManager.ts +0 -53
  51. package/src/main/updater/AutoUpdater.ts +0 -132
  52. package/src/main/window/createWindow.ts +0 -71
  53. package/src/mcp/README.md +0 -56
  54. package/src/mcp/index.ts +0 -153
  55. package/src/mcp/wmux-client.ts +0 -127
  56. package/src/preload/index.ts +0 -111
  57. package/src/preload/preload.ts +0 -108
  58. package/src/renderer/App.tsx +0 -5
  59. package/src/renderer/components/Browser/BrowserPanel.tsx +0 -219
  60. package/src/renderer/components/Browser/BrowserToolbar.tsx +0 -253
  61. package/src/renderer/components/Company/ApprovalDialog.tsx +0 -3
  62. package/src/renderer/components/Company/CompanyView.tsx +0 -7
  63. package/src/renderer/components/Company/MessageFeedPanel.tsx +0 -3
  64. package/src/renderer/components/Layout/AppLayout.tsx +0 -234
  65. package/src/renderer/components/Notification/NotificationPanel.tsx +0 -129
  66. package/src/renderer/components/Palette/CommandPalette.tsx +0 -409
  67. package/src/renderer/components/Palette/PaletteItem.tsx +0 -55
  68. package/src/renderer/components/Pane/Pane.tsx +0 -122
  69. package/src/renderer/components/Pane/PaneContainer.tsx +0 -41
  70. package/src/renderer/components/Pane/SurfaceTabs.tsx +0 -46
  71. package/src/renderer/components/Settings/SettingsPanel.tsx +0 -886
  72. package/src/renderer/components/Sidebar/MiniSidebar.tsx +0 -67
  73. package/src/renderer/components/Sidebar/Sidebar.tsx +0 -84
  74. package/src/renderer/components/Sidebar/WorkspaceItem.tsx +0 -241
  75. package/src/renderer/components/StatusBar/StatusBar.tsx +0 -93
  76. package/src/renderer/components/Terminal/SearchBar.tsx +0 -126
  77. package/src/renderer/components/Terminal/Terminal.tsx +0 -102
  78. package/src/renderer/components/Terminal/ViCopyMode.tsx +0 -104
  79. package/src/renderer/hooks/useKeyboard.ts +0 -310
  80. package/src/renderer/hooks/useNotificationListener.ts +0 -80
  81. package/src/renderer/hooks/useNotificationSound.ts +0 -75
  82. package/src/renderer/hooks/useRpcBridge.ts +0 -451
  83. package/src/renderer/hooks/useT.ts +0 -11
  84. package/src/renderer/hooks/useTerminal.ts +0 -349
  85. package/src/renderer/hooks/useViCopyMode.ts +0 -320
  86. package/src/renderer/i18n/index.ts +0 -69
  87. package/src/renderer/i18n/locales/en.ts +0 -157
  88. package/src/renderer/i18n/locales/ja.ts +0 -155
  89. package/src/renderer/i18n/locales/ko.ts +0 -155
  90. package/src/renderer/i18n/locales/zh.ts +0 -155
  91. package/src/renderer/index.tsx +0 -6
  92. package/src/renderer/stores/index.ts +0 -19
  93. package/src/renderer/stores/slices/notificationSlice.ts +0 -56
  94. package/src/renderer/stores/slices/paneSlice.ts +0 -141
  95. package/src/renderer/stores/slices/surfaceSlice.ts +0 -122
  96. package/src/renderer/stores/slices/uiSlice.ts +0 -247
  97. package/src/renderer/stores/slices/workspaceSlice.ts +0 -120
  98. package/src/renderer/styles/globals.css +0 -150
  99. package/src/renderer/themes.ts +0 -99
  100. package/src/shared/constants.ts +0 -53
  101. package/src/shared/electron.d.ts +0 -11
  102. package/src/shared/rpc.ts +0 -71
  103. package/src/shared/types.ts +0 -176
  104. package/tailwind.config.js +0 -11
  105. package/tsconfig.cli.json +0 -24
  106. package/tsconfig.json +0 -21
  107. package/tsconfig.mcp.json +0 -25
  108. package/vite.main.config.ts +0 -14
  109. package/vite.preload.config.ts +0 -9
  110. package/vite.renderer.config.ts +0 -6
@@ -1,56 +0,0 @@
1
- import { ipcMain, BrowserWindow } from 'electron';
2
- import { IPC } from '../../../shared/constants';
3
- import { MetadataCollector } from '../../metadata/MetadataCollector';
4
- import { PTYManager } from '../../pty/PTYManager';
5
-
6
- const collector = new MetadataCollector();
7
-
8
- // Track CWD per ptyId (updated via OSC 7 or polling)
9
- const cwdMap = new Map<string, string>();
10
-
11
- export function registerMetadataHandlers(
12
- ptyManager: PTYManager,
13
- getWindow: () => BrowserWindow | null,
14
- ): () => void {
15
- // Handle metadata request from renderer
16
- ipcMain.handle(IPC.METADATA_REQUEST, async (_event, ptyId: string) => {
17
- const cwd = cwdMap.get(ptyId);
18
- return collector.collect(cwd);
19
- });
20
-
21
- // Listen for CWD changes from PTYBridge (via OscParser)
22
- ipcMain.on(IPC.CWD_CHANGED, (_event, ptyId: string, cwd: string) => {
23
- cwdMap.set(ptyId, cwd);
24
- });
25
-
26
- // Periodic metadata polling (every 5 seconds)
27
- const pollingInterval = setInterval(async () => {
28
- const win = getWindow();
29
- if (!win || win.isDestroyed()) return;
30
-
31
- for (const [ptyId] of cwdMap) {
32
- const instance = ptyManager.get(ptyId);
33
- if (!instance) {
34
- cwdMap.delete(ptyId);
35
- continue;
36
- }
37
-
38
- const cwd = cwdMap.get(ptyId);
39
- if (cwd) {
40
- const metadata = await collector.collect(cwd);
41
- win.webContents.send(IPC.METADATA_UPDATE, ptyId, metadata);
42
- }
43
- }
44
- }, 5000);
45
-
46
- // cleanup 함수 반환 — 앱 종료 시 호출
47
- return () => {
48
- clearInterval(pollingInterval);
49
- ipcMain.removeHandler(IPC.METADATA_REQUEST);
50
- ipcMain.removeAllListeners(IPC.CWD_CHANGED);
51
- };
52
- }
53
-
54
- export function updateCwd(ptyId: string, cwd: string): void {
55
- cwdMap.set(ptyId, cwd);
56
- }
@@ -1,69 +0,0 @@
1
- import { ipcMain } from 'electron';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { PTYManager } from '../../pty/PTYManager';
5
- import { PTYBridge } from '../../pty/PTYBridge';
6
- import { IPC } from '../../../shared/constants';
7
-
8
- /**
9
- * Allowed shell basenames (case-insensitive on Windows).
10
- * Only these executables may be spawned via IPC.
11
- */
12
- const ALLOWED_SHELLS = new Set([
13
- 'powershell.exe',
14
- 'pwsh.exe',
15
- 'cmd.exe',
16
- 'bash.exe',
17
- 'wsl.exe',
18
- 'git-bash.exe',
19
- 'sh.exe',
20
- ]);
21
-
22
- function isAllowedShell(shell: string): boolean {
23
- const basename = path.basename(shell).toLowerCase();
24
- return ALLOWED_SHELLS.has(basename);
25
- }
26
-
27
- export function registerPTYHandlers(ptyManager: PTYManager, ptyBridge: PTYBridge): void {
28
- ipcMain.handle(IPC.PTY_CREATE, (_event, options?: { shell?: string; cwd?: string; cols?: number; rows?: number }) => {
29
- if (options?.shell !== undefined && !isAllowedShell(options.shell)) {
30
- throw new Error(`PTY_CREATE: shell not allowed: ${options.shell}`);
31
- }
32
-
33
- // Validate workDir to block UNC paths and non-existent directories
34
- let safeCwd: string | undefined;
35
- if (options?.cwd) {
36
- const resolved = path.resolve(options.cwd);
37
- // Block UNC paths (e.g. \\server\share)
38
- if (!resolved.startsWith('\\\\') && fs.existsSync(resolved)) {
39
- const stat = fs.statSync(resolved);
40
- if (stat.isDirectory()) {
41
- safeCwd = resolved;
42
- }
43
- }
44
- }
45
-
46
- const instance = ptyManager.create(safeCwd !== undefined ? { ...options, cwd: safeCwd } : { ...options, cwd: undefined });
47
- ptyBridge.setupDataForwarding(instance.id);
48
- return { id: instance.id, shell: instance.shell };
49
- });
50
-
51
- ipcMain.handle(IPC.PTY_WRITE, (_event, id: string, data: string) => {
52
- // 세션 복원 시 이전 ptyId가 남아있을 수 있음 — 조용히 무시
53
- if (!ptyManager.get(id)) return;
54
- if (typeof data !== 'string') return;
55
- if (data.length > 100_000) return; // prevent mega-writes
56
- ptyManager.write(id, data);
57
- });
58
-
59
- ipcMain.handle(IPC.PTY_RESIZE, (_event, id: string, cols: number, rows: number) => {
60
- if (!Number.isInteger(cols) || cols <= 0 || !Number.isInteger(rows) || rows <= 0) {
61
- throw new Error(`PTY_RESIZE: cols and rows must be positive integers (got cols=${cols}, rows=${rows})`);
62
- }
63
- ptyManager.resize(id, cols, rows);
64
- });
65
-
66
- ipcMain.handle(IPC.PTY_DISPOSE, (_event, id: string) => {
67
- ptyManager.dispose(id);
68
- });
69
- }
@@ -1,17 +0,0 @@
1
- import { ipcMain } from 'electron';
2
- import { IPC } from '../../../shared/constants';
3
- import { SessionManager } from '../../session/SessionManager';
4
- import type { SessionData } from '../../../shared/types';
5
-
6
- const sessionManager = new SessionManager();
7
-
8
- export function registerSessionHandlers(): void {
9
- ipcMain.handle(IPC.SESSION_SAVE, (_event, data: SessionData) => {
10
- sessionManager.save(data);
11
- return { success: true };
12
- });
13
-
14
- ipcMain.handle(IPC.SESSION_LOAD, () => {
15
- return sessionManager.load();
16
- });
17
- }
@@ -1,11 +0,0 @@
1
- import { ipcMain } from 'electron';
2
- import { ShellDetector } from '../../pty/ShellDetector';
3
- import { IPC } from '../../../shared/constants';
4
-
5
- export function registerShellHandlers(): void {
6
- const detector = new ShellDetector();
7
-
8
- ipcMain.handle(IPC.SHELL_LIST, () => {
9
- return detector.detect();
10
- });
11
- }
@@ -1,31 +0,0 @@
1
- import { ipcMain, type BrowserWindow } from 'electron';
2
- import { PTYManager } from '../pty/PTYManager';
3
- import { PTYBridge } from '../pty/PTYBridge';
4
- import { registerPTYHandlers } from './handlers/pty.handler';
5
- import { registerSessionHandlers } from './handlers/session.handler';
6
- import { registerShellHandlers } from './handlers/shell.handler';
7
- import { registerMetadataHandlers } from './handlers/metadata.handler';
8
- import { registerClipboardHandlers } from './handlers/clipboard.handler';
9
- import { IPC } from '../../shared/constants';
10
- import { toastManager } from '../pipe/handlers/notify.rpc';
11
-
12
- export function registerAllHandlers(
13
- ptyManager: PTYManager,
14
- ptyBridge: PTYBridge,
15
- getWindow: () => BrowserWindow | null,
16
- ): () => void {
17
- registerPTYHandlers(ptyManager, ptyBridge);
18
- registerSessionHandlers();
19
- registerShellHandlers();
20
- const cleanupMetadata = registerMetadataHandlers(ptyManager, getWindow);
21
- registerClipboardHandlers();
22
-
23
- // Sync toast setting from renderer
24
- ipcMain.on(IPC.TOAST_ENABLED, (_event, enabled: boolean) => {
25
- toastManager.enabled = enabled;
26
- });
27
-
28
- return () => {
29
- cleanupMetadata();
30
- };
31
- }
@@ -1,156 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import { app } from 'electron';
4
- import { getAuthTokenPath } from '../../shared/constants';
5
-
6
- /**
7
- * Registers/unregisters the wmux MCP server in Claude Code's config files
8
- * and writes the auth token to a well-known file so the MCP server can read it.
9
- *
10
- * The MCP server uses:
11
- * - Fixed pipe path: \\.\pipe\wmux (from shared/constants)
12
- * - Auth token file: ~/.wmux-auth-token (written here, read by MCP)
13
- *
14
- * Config files written:
15
- * 1. ~/.claude/settings.json (global settings — mcpServers key)
16
- * 2. ~/.claude/.mcp.json (user-level MCP config)
17
- */
18
- export class McpRegistrar {
19
- private readonly settingsPath: string;
20
- private readonly mcpJsonPath: string;
21
- private readonly authTokenPath: string;
22
- private registered = false;
23
-
24
- constructor() {
25
- const home = app.getPath('home');
26
- this.settingsPath = path.join(home, '.claude', 'settings.json');
27
- this.mcpJsonPath = path.join(home, '.claude', '.mcp.json');
28
- this.authTokenPath = getAuthTokenPath();
29
- }
30
-
31
- /**
32
- * Write auth token to file and register MCP server in Claude Code configs.
33
- * Must be called after PipeServer.start().
34
- */
35
- register(authToken: string): void {
36
- try {
37
- // Write auth token to file so MCP server can read it
38
- fs.writeFileSync(this.authTokenPath, authToken, { encoding: 'utf8', mode: 0o600 });
39
- console.log(`[McpRegistrar] Auth token written to ${this.authTokenPath}`);
40
-
41
- const mcpScript = this.getMcpScriptPath();
42
- if (!mcpScript) {
43
- console.warn('[McpRegistrar] Could not determine MCP script path — skipping registration.');
44
- return;
45
- }
46
-
47
- // Use absolute node path to avoid PATH resolution issues
48
- const mcpEntry = {
49
- command: process.execPath,
50
- args: [mcpScript],
51
- };
52
-
53
- this.registerInSettings(mcpEntry);
54
- this.registerInMcpJson(mcpEntry);
55
-
56
- this.registered = true;
57
- console.log(`[McpRegistrar] Registered wmux MCP → ${mcpScript}`);
58
- } catch (err) {
59
- console.error('[McpRegistrar] Failed to register:', err);
60
- }
61
- }
62
-
63
- /**
64
- * Remove wmux MCP server entry and auth token file.
65
- */
66
- unregister(): void {
67
- // Always clean up auth token file
68
- try {
69
- if (fs.existsSync(this.authTokenPath)) {
70
- fs.unlinkSync(this.authTokenPath);
71
- }
72
- } catch { /* ignore */ }
73
-
74
- if (!this.registered) return;
75
-
76
- try {
77
- this.unregisterFromSettings();
78
- this.unregisterFromMcpJson();
79
- console.log('[McpRegistrar] Unregistered wmux MCP.');
80
- } catch (err) {
81
- console.error('[McpRegistrar] Failed to unregister:', err);
82
- }
83
- }
84
-
85
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
- private registerInSettings(mcpEntry: Record<string, any>): void {
87
- const settings = this.readJson(this.settingsPath);
88
- if (!settings.mcpServers) settings.mcpServers = {};
89
- settings.mcpServers['wmux'] = mcpEntry;
90
- this.writeJson(this.settingsPath, settings);
91
- }
92
-
93
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
94
- private registerInMcpJson(mcpEntry: Record<string, any>): void {
95
- const mcpConfig = this.readJson(this.mcpJsonPath);
96
- if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
97
- mcpConfig.mcpServers['wmux'] = mcpEntry;
98
- this.writeJson(this.mcpJsonPath, mcpConfig);
99
- }
100
-
101
- private unregisterFromSettings(): void {
102
- const settings = this.readJson(this.settingsPath);
103
- if (settings.mcpServers?.['wmux']) {
104
- delete settings.mcpServers['wmux'];
105
- if (Object.keys(settings.mcpServers).length === 0) delete settings.mcpServers;
106
- this.writeJson(this.settingsPath, settings);
107
- }
108
- }
109
-
110
- private unregisterFromMcpJson(): void {
111
- const mcpConfig = this.readJson(this.mcpJsonPath);
112
- if (mcpConfig.mcpServers?.['wmux']) {
113
- delete mcpConfig.mcpServers['wmux'];
114
- if (Object.keys(mcpConfig.mcpServers).length === 0) delete mcpConfig.mcpServers;
115
- if (Object.keys(mcpConfig).length === 0) {
116
- try { fs.unlinkSync(this.mcpJsonPath); } catch { /* ignore */ }
117
- } else {
118
- this.writeJson(this.mcpJsonPath, mcpConfig);
119
- }
120
- }
121
- }
122
-
123
- private getMcpScriptPath(): string | null {
124
- if (app.isPackaged) {
125
- const resourcePath = path.join(process.resourcesPath, 'mcp', 'mcp', 'index.js');
126
- if (fs.existsSync(resourcePath)) return resourcePath;
127
- return null;
128
- }
129
-
130
- const appPath = app.getAppPath();
131
- const devPath = path.join(appPath, 'dist', 'mcp', 'mcp', 'index.js');
132
- if (fs.existsSync(devPath)) return devPath;
133
-
134
- return null;
135
- }
136
-
137
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
138
- private readJson(filePath: string): Record<string, any> {
139
- try {
140
- if (fs.existsSync(filePath)) {
141
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
142
- }
143
- } catch { /* corrupted — start fresh */ }
144
-
145
- const dir = path.dirname(filePath);
146
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
147
- return {};
148
- }
149
-
150
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
151
- private writeJson(filePath: string, data: Record<string, any>): void {
152
- const dir = path.dirname(filePath);
153
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
154
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
155
- }
156
- }
@@ -1,58 +0,0 @@
1
- import { execFile } from 'node:child_process';
2
- import { promisify } from 'node:util';
3
-
4
- const execFileAsync = promisify(execFile);
5
-
6
- export class MetadataCollector {
7
- async getGitBranch(cwd: string): Promise<string | undefined> {
8
- try {
9
- const { stdout } = await execFileAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
10
- cwd,
11
- timeout: 3000,
12
- });
13
- const branch = stdout.trim();
14
- return branch || undefined;
15
- } catch {
16
- return undefined;
17
- }
18
- }
19
-
20
- async getListeningPorts(pid?: number): Promise<number[]> {
21
- if (pid !== undefined && !(Number.isInteger(pid) && pid > 0)) {
22
- return [];
23
- }
24
- try {
25
- // Use PowerShell to get listening TCP connections
26
- const script = pid
27
- ? `Get-NetTCPConnection -State Listen -OwningProcess ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty LocalPort`
28
- : `Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -ne 0 -and $_.OwningProcess -ne 4 } | Select-Object -ExpandProperty LocalPort | Sort-Object -Unique | Select-Object -First 20`;
29
-
30
- const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-Command', script], {
31
- timeout: 5000,
32
- });
33
-
34
- const ports = stdout
35
- .trim()
36
- .split(/\r?\n/)
37
- .map((s) => parseInt(s.trim(), 10))
38
- .filter((p) => !isNaN(p) && p > 0);
39
-
40
- return [...new Set(ports)].sort((a, b) => a - b);
41
- } catch {
42
- return [];
43
- }
44
- }
45
-
46
- async collect(cwd?: string): Promise<{ gitBranch?: string; cwd?: string; listeningPorts?: number[] }> {
47
- const [gitBranch, listeningPorts] = await Promise.all([
48
- cwd ? this.getGitBranch(cwd) : Promise.resolve(undefined),
49
- this.getListeningPorts(),
50
- ]);
51
-
52
- return {
53
- gitBranch,
54
- cwd,
55
- listeningPorts: listeningPorts.length > 0 ? listeningPorts : undefined,
56
- };
57
- }
58
- }
@@ -1,32 +0,0 @@
1
- import { Notification, BrowserWindow } from 'electron';
2
-
3
- export class ToastManager {
4
- enabled = true;
5
-
6
- show(title: string, body: string): void {
7
- if (!this.enabled) return;
8
-
9
- // Only show toast when app is not focused
10
- const focusedWindow = BrowserWindow.getFocusedWindow();
11
- if (focusedWindow) return;
12
-
13
- if (!Notification.isSupported()) return;
14
-
15
- const notification = new Notification({
16
- title,
17
- body,
18
- silent: false,
19
- });
20
-
21
- notification.on('click', () => {
22
- // Bring app to front when toast is clicked
23
- const win = BrowserWindow.getAllWindows()[0];
24
- if (win) {
25
- if (win.isMinimized()) win.restore();
26
- win.focus();
27
- }
28
- });
29
-
30
- notification.show();
31
- }
32
- }
@@ -1,190 +0,0 @@
1
- import * as net from 'net';
2
- import * as crypto from 'crypto';
3
- import { getPipeName } from '../../shared/constants';
4
- import type { RpcRequest } from '../../shared/rpc';
5
- import { RpcRouter } from './RpcRouter';
6
-
7
- const MAX_LINE_BUFFER = 1024 * 1024; // 1 MB — prevent OOM from malicious clients
8
-
9
- export class PipeServer {
10
- private server: net.Server | null = null;
11
- private readonly router: RpcRouter;
12
- private readonly connectedSockets = new Set<net.Socket>();
13
- private readonly authToken: string;
14
- private readonly rateLimits = new Map<net.Socket, { count: number; resetAt: number }>();
15
-
16
- constructor(router: RpcRouter) {
17
- this.router = router;
18
- this.authToken = crypto.randomUUID();
19
- }
20
-
21
- getAuthToken(): string {
22
- return this.authToken;
23
- }
24
-
25
- start(): void {
26
- if (this.server) {
27
- return;
28
- }
29
-
30
- this.server = net.createServer((socket) => {
31
- this.connectedSockets.add(socket);
32
- socket.on('close', () => {
33
- this.connectedSockets.delete(socket);
34
- this.rateLimits.delete(socket);
35
- });
36
- this.handleConnection(socket);
37
- });
38
-
39
- this.server.on('error', (err: NodeJS.ErrnoException) => {
40
- if (err.code === 'EADDRINUSE') {
41
- console.warn('[PipeServer] EADDRINUSE — retrying in 1s...');
42
- setTimeout(() => {
43
- if (this.server) {
44
- this.server.close();
45
- this.server.listen(getPipeName());
46
- }
47
- }, 1000);
48
- } else {
49
- console.error('[PipeServer] Server error:', err);
50
- }
51
- });
52
-
53
- const pipeName = getPipeName();
54
- this.server.listen(pipeName, () => {
55
- console.log(`[PipeServer] Listening on ${pipeName}`);
56
- });
57
- }
58
-
59
- stop(): void {
60
- if (!this.server) {
61
- return;
62
- }
63
-
64
- // Destroy all connected sockets
65
- for (const socket of this.connectedSockets) {
66
- socket.destroy();
67
- }
68
- this.connectedSockets.clear();
69
-
70
- this.server.close((err) => {
71
- if (err) {
72
- console.error('[PipeServer] Error closing server:', err);
73
- } else {
74
- console.log('[PipeServer] Server closed.');
75
- }
76
- });
77
-
78
- this.server = null;
79
- }
80
-
81
- private handleConnection(socket: net.Socket): void {
82
- console.log('[PipeServer] Client connected.');
83
-
84
- let buffer = '';
85
-
86
- socket.setEncoding('utf8');
87
-
88
- socket.on('data', (chunk: string) => {
89
- buffer += chunk;
90
-
91
- // Security: prevent OOM from clients that never send newlines
92
- if (buffer.length > MAX_LINE_BUFFER) {
93
- console.warn('[PipeServer] Client exceeded max buffer size — disconnecting.');
94
- socket.destroy();
95
- return;
96
- }
97
-
98
- const lines = buffer.split('\n');
99
- // 마지막 요소는 아직 완성되지 않은 부분 — 다음 청크를 기다림
100
- buffer = lines.pop() ?? '';
101
-
102
- for (const line of lines) {
103
- const trimmed = line.trim();
104
- if (!trimmed) {
105
- continue;
106
- }
107
- this.processLine(socket, trimmed);
108
- }
109
- });
110
-
111
- socket.on('end', () => {
112
- // 연결 종료 시 남은 버퍼 처리
113
- const trimmed = buffer.trim();
114
- if (trimmed) {
115
- this.processLine(socket, trimmed);
116
- }
117
- buffer = '';
118
- console.log('[PipeServer] Client disconnected.');
119
- });
120
-
121
- socket.on('error', (err) => {
122
- console.error('[PipeServer] Socket error:', err);
123
- socket.destroy();
124
- });
125
- }
126
-
127
- private processLine(socket: net.Socket, line: string): void {
128
- let request: RpcRequest;
129
-
130
- try {
131
- request = JSON.parse(line) as RpcRequest;
132
- } catch {
133
- const errorResponse = JSON.stringify({
134
- id: null,
135
- ok: false,
136
- error: 'Invalid JSON',
137
- });
138
- socket.write(errorResponse + '\n');
139
- return;
140
- }
141
-
142
- // Rate limiting: max 50 requests per second per socket
143
- const now = Date.now();
144
- let limit = this.rateLimits.get(socket);
145
- if (!limit || now > limit.resetAt) {
146
- limit = { count: 0, resetAt: now + 1000 };
147
- this.rateLimits.set(socket, limit);
148
- }
149
- limit.count++;
150
- if (limit.count > 50) {
151
- const rateLimitResponse = JSON.stringify({
152
- id: request.id,
153
- ok: false,
154
- error: 'rate limited',
155
- });
156
- socket.write(rateLimitResponse + '\n');
157
- return;
158
- }
159
-
160
- // Authenticate: every request must carry a valid token
161
- if (request.token !== this.authToken) {
162
- const unauthorizedResponse = JSON.stringify({
163
- id: request.id,
164
- ok: false,
165
- error: 'unauthorized',
166
- });
167
- socket.write(unauthorizedResponse + '\n');
168
- return;
169
- }
170
-
171
- this.router
172
- .dispatch(request)
173
- .then((response) => {
174
- if (!socket.destroyed) {
175
- socket.write(JSON.stringify(response) + '\n');
176
- }
177
- })
178
- .catch((err: unknown) => {
179
- console.error('[PipeServer] Dispatch error:', err);
180
- if (!socket.destroyed) {
181
- const errorResponse = JSON.stringify({
182
- id: request.id,
183
- ok: false,
184
- error: 'Internal server error',
185
- });
186
- socket.write(errorResponse + '\n');
187
- }
188
- });
189
- }
190
- }
@@ -1,46 +0,0 @@
1
- import type { RpcMethod, RpcRequest, RpcResponse } from '../../shared/rpc';
2
-
3
- type RpcHandler = (params: Record<string, unknown>) => Promise<unknown>;
4
-
5
- export class RpcRouter {
6
- private readonly handlers = new Map<RpcMethod, RpcHandler>();
7
-
8
- register(method: RpcMethod, handler: RpcHandler): void {
9
- this.handlers.set(method, handler);
10
- }
11
-
12
- async dispatch(request: RpcRequest): Promise<RpcResponse> {
13
- if (!request || typeof request.id !== 'string' || typeof request.method !== 'string') {
14
- return { id: (request as RpcRequest)?.id || '', ok: false, error: 'Invalid RPC request: missing id or method' };
15
- }
16
- if (request.params !== undefined && (typeof request.params !== 'object' || request.params === null)) {
17
- return { id: request.id, ok: false, error: 'Invalid RPC request: params must be an object' };
18
- }
19
-
20
- const handler = this.handlers.get(request.method);
21
-
22
- if (!handler) {
23
- return {
24
- id: request.id,
25
- ok: false,
26
- error: `Unknown method: ${request.method}`,
27
- };
28
- }
29
-
30
- try {
31
- const result = await handler(request.params);
32
- return {
33
- id: request.id,
34
- ok: true,
35
- result,
36
- };
37
- } catch (err) {
38
- const message = err instanceof Error ? err.message : String(err);
39
- return {
40
- id: request.id,
41
- ok: false,
42
- error: message,
43
- };
44
- }
45
- }
46
- }