@wong2kim/wmux 1.0.0
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/README.md +157 -0
- package/assets/icon.ico +0 -0
- package/assets/icon.svg +6 -0
- package/dist/cli/cli/client.js +102 -0
- package/dist/cli/cli/commands/browser.js +137 -0
- package/dist/cli/cli/commands/input.js +80 -0
- package/dist/cli/cli/commands/notify.js +28 -0
- package/dist/cli/cli/commands/pane.js +88 -0
- package/dist/cli/cli/commands/surface.js +98 -0
- package/dist/cli/cli/commands/system.js +98 -0
- package/dist/cli/cli/commands/workspace.js +117 -0
- package/dist/cli/cli/index.js +140 -0
- package/dist/cli/cli/utils.js +47 -0
- package/dist/cli/shared/constants.js +54 -0
- package/dist/cli/shared/rpc.js +33 -0
- package/dist/cli/shared/types.js +79 -0
- package/dist/mcp/mcp/index.js +60 -0
- package/dist/mcp/mcp/wmux-client.js +146 -0
- package/dist/mcp/shared/constants.js +54 -0
- package/dist/mcp/shared/rpc.js +33 -0
- package/dist/mcp/shared/types.js +79 -0
- package/forge.config.ts +61 -0
- package/index.html +12 -0
- package/package.json +84 -0
- package/postcss.config.js +6 -0
- package/src/cli/client.ts +76 -0
- package/src/cli/commands/browser.ts +128 -0
- package/src/cli/commands/input.ts +72 -0
- package/src/cli/commands/notify.ts +29 -0
- package/src/cli/commands/pane.ts +90 -0
- package/src/cli/commands/surface.ts +102 -0
- package/src/cli/commands/system.ts +95 -0
- package/src/cli/commands/workspace.ts +116 -0
- package/src/cli/index.ts +145 -0
- package/src/cli/utils.ts +44 -0
- package/src/main/index.ts +86 -0
- package/src/main/ipc/handlers/clipboard.handler.ts +20 -0
- package/src/main/ipc/handlers/metadata.handler.ts +56 -0
- package/src/main/ipc/handlers/pty.handler.ts +69 -0
- package/src/main/ipc/handlers/session.handler.ts +17 -0
- package/src/main/ipc/handlers/shell.handler.ts +11 -0
- package/src/main/ipc/registerHandlers.ts +31 -0
- package/src/main/mcp/McpRegistrar.ts +156 -0
- package/src/main/metadata/MetadataCollector.ts +58 -0
- package/src/main/notification/ToastManager.ts +32 -0
- package/src/main/pipe/PipeServer.ts +190 -0
- package/src/main/pipe/RpcRouter.ts +46 -0
- package/src/main/pipe/handlers/_bridge.ts +40 -0
- package/src/main/pipe/handlers/browser.rpc.ts +132 -0
- package/src/main/pipe/handlers/input.rpc.ts +120 -0
- package/src/main/pipe/handlers/meta.rpc.ts +59 -0
- package/src/main/pipe/handlers/notify.rpc.ts +53 -0
- package/src/main/pipe/handlers/pane.rpc.ts +39 -0
- package/src/main/pipe/handlers/surface.rpc.ts +43 -0
- package/src/main/pipe/handlers/system.rpc.ts +36 -0
- package/src/main/pipe/handlers/workspace.rpc.ts +52 -0
- package/src/main/pty/AgentDetector.ts +247 -0
- package/src/main/pty/OscParser.ts +81 -0
- package/src/main/pty/PTYBridge.ts +88 -0
- package/src/main/pty/PTYManager.ts +104 -0
- package/src/main/pty/ShellDetector.ts +63 -0
- package/src/main/session/SessionManager.ts +53 -0
- package/src/main/updater/AutoUpdater.ts +132 -0
- package/src/main/window/createWindow.ts +71 -0
- package/src/mcp/README.md +56 -0
- package/src/mcp/index.ts +153 -0
- package/src/mcp/wmux-client.ts +127 -0
- package/src/preload/index.ts +111 -0
- package/src/preload/preload.ts +108 -0
- package/src/renderer/App.tsx +5 -0
- package/src/renderer/components/Browser/BrowserPanel.tsx +219 -0
- package/src/renderer/components/Browser/BrowserToolbar.tsx +253 -0
- package/src/renderer/components/Company/ApprovalDialog.tsx +3 -0
- package/src/renderer/components/Company/CompanyView.tsx +7 -0
- package/src/renderer/components/Company/MessageFeedPanel.tsx +3 -0
- package/src/renderer/components/Layout/AppLayout.tsx +234 -0
- package/src/renderer/components/Notification/NotificationPanel.tsx +129 -0
- package/src/renderer/components/Palette/CommandPalette.tsx +409 -0
- package/src/renderer/components/Palette/PaletteItem.tsx +55 -0
- package/src/renderer/components/Pane/Pane.tsx +122 -0
- package/src/renderer/components/Pane/PaneContainer.tsx +41 -0
- package/src/renderer/components/Pane/SurfaceTabs.tsx +46 -0
- package/src/renderer/components/Settings/SettingsPanel.tsx +886 -0
- package/src/renderer/components/Sidebar/MiniSidebar.tsx +67 -0
- package/src/renderer/components/Sidebar/Sidebar.tsx +84 -0
- package/src/renderer/components/Sidebar/WorkspaceItem.tsx +241 -0
- package/src/renderer/components/StatusBar/StatusBar.tsx +93 -0
- package/src/renderer/components/Terminal/SearchBar.tsx +126 -0
- package/src/renderer/components/Terminal/Terminal.tsx +102 -0
- package/src/renderer/components/Terminal/ViCopyMode.tsx +104 -0
- package/src/renderer/hooks/useKeyboard.ts +310 -0
- package/src/renderer/hooks/useNotificationListener.ts +80 -0
- package/src/renderer/hooks/useNotificationSound.ts +75 -0
- package/src/renderer/hooks/useRpcBridge.ts +451 -0
- package/src/renderer/hooks/useT.ts +11 -0
- package/src/renderer/hooks/useTerminal.ts +349 -0
- package/src/renderer/hooks/useViCopyMode.ts +320 -0
- package/src/renderer/i18n/index.ts +69 -0
- package/src/renderer/i18n/locales/en.ts +157 -0
- package/src/renderer/i18n/locales/ja.ts +155 -0
- package/src/renderer/i18n/locales/ko.ts +155 -0
- package/src/renderer/i18n/locales/zh.ts +155 -0
- package/src/renderer/index.tsx +6 -0
- package/src/renderer/stores/index.ts +19 -0
- package/src/renderer/stores/slices/notificationSlice.ts +56 -0
- package/src/renderer/stores/slices/paneSlice.ts +141 -0
- package/src/renderer/stores/slices/surfaceSlice.ts +122 -0
- package/src/renderer/stores/slices/uiSlice.ts +247 -0
- package/src/renderer/stores/slices/workspaceSlice.ts +120 -0
- package/src/renderer/styles/globals.css +150 -0
- package/src/renderer/themes.ts +99 -0
- package/src/shared/constants.ts +53 -0
- package/src/shared/electron.d.ts +11 -0
- package/src/shared/rpc.ts +71 -0
- package/src/shared/types.ts +176 -0
- package/tailwind.config.js +11 -0
- package/tsconfig.cli.json +24 -0
- package/tsconfig.json +21 -0
- package/tsconfig.mcp.json +25 -0
- package/vite.main.config.ts +14 -0
- package/vite.preload.config.ts +9 -0
- package/vite.renderer.config.ts +6 -0
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
process.on('SIGINT', () => {
|
|
4
|
+
process.exit(130);
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
import { hasFlag } from './utils';
|
|
8
|
+
import { handleWorkspace } from './commands/workspace';
|
|
9
|
+
import { handleSurface } from './commands/surface';
|
|
10
|
+
import { handlePane } from './commands/pane';
|
|
11
|
+
import { handleInput } from './commands/input';
|
|
12
|
+
import { handleNotify } from './commands/notify';
|
|
13
|
+
import { handleSystem } from './commands/system';
|
|
14
|
+
import { handleBrowser } from './commands/browser';
|
|
15
|
+
|
|
16
|
+
const HELP_TEXT = `
|
|
17
|
+
wmux CLI
|
|
18
|
+
|
|
19
|
+
USAGE
|
|
20
|
+
wmux <command> [options]
|
|
21
|
+
|
|
22
|
+
WORKSPACE COMMANDS
|
|
23
|
+
list-workspaces List all workspaces
|
|
24
|
+
new-workspace [--name <name>] Create a new workspace
|
|
25
|
+
focus-workspace <id> Focus a workspace by ID
|
|
26
|
+
close-workspace <id> Close a workspace by ID
|
|
27
|
+
current-workspace Show the active workspace
|
|
28
|
+
|
|
29
|
+
SURFACE COMMANDS
|
|
30
|
+
list-surfaces List all surfaces in the active workspace
|
|
31
|
+
new-surface Open a new surface (terminal tab)
|
|
32
|
+
focus-surface <id> Focus a surface by ID
|
|
33
|
+
close-surface <id> Close a surface by ID
|
|
34
|
+
|
|
35
|
+
PANE COMMANDS
|
|
36
|
+
list-panes List all panes in the active workspace
|
|
37
|
+
focus-pane <id> Focus a pane by ID
|
|
38
|
+
split [--direction right|down] Split the active pane (default: right)
|
|
39
|
+
|
|
40
|
+
INPUT COMMANDS
|
|
41
|
+
send <text> Send text to the active terminal
|
|
42
|
+
send-key <keystroke> Send a key (e.g. Enter, ctrl-c, Tab)
|
|
43
|
+
read-screen Read the current terminal screen content
|
|
44
|
+
|
|
45
|
+
NOTIFICATION COMMANDS
|
|
46
|
+
notify --title <title> --body <body> Show a notification in wmux
|
|
47
|
+
|
|
48
|
+
SYSTEM COMMANDS
|
|
49
|
+
set-status <text> Set a status message on the active workspace
|
|
50
|
+
set-progress <0-100> Set a progress value on the active workspace
|
|
51
|
+
identify Show wmux app info
|
|
52
|
+
capabilities List all supported RPC methods
|
|
53
|
+
|
|
54
|
+
BROWSER COMMANDS
|
|
55
|
+
browser snapshot Return the full page HTML of the active browser surface
|
|
56
|
+
browser click <selector> Click an element by CSS selector
|
|
57
|
+
browser fill <selector> <text> Fill an input field by CSS selector
|
|
58
|
+
browser eval <code> Execute JavaScript in the browser context
|
|
59
|
+
browser navigate <url> Navigate the browser surface to a URL
|
|
60
|
+
|
|
61
|
+
GLOBAL FLAGS
|
|
62
|
+
--json Output raw JSON (useful for scripting)
|
|
63
|
+
--help Show this help text
|
|
64
|
+
|
|
65
|
+
EXAMPLES
|
|
66
|
+
wmux list-workspaces
|
|
67
|
+
wmux new-workspace --name dev
|
|
68
|
+
wmux send "echo hello"
|
|
69
|
+
wmux notify --title "Done" --body "Build finished"
|
|
70
|
+
wmux identify --json
|
|
71
|
+
wmux browser snapshot
|
|
72
|
+
wmux browser navigate "https://example.com"
|
|
73
|
+
wmux browser click "#login-btn"
|
|
74
|
+
`.trimStart();
|
|
75
|
+
|
|
76
|
+
const WORKSPACE_CMDS = new Set([
|
|
77
|
+
'list-workspaces',
|
|
78
|
+
'new-workspace',
|
|
79
|
+
'focus-workspace',
|
|
80
|
+
'close-workspace',
|
|
81
|
+
'current-workspace',
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const SURFACE_CMDS = new Set([
|
|
85
|
+
'list-surfaces',
|
|
86
|
+
'new-surface',
|
|
87
|
+
'focus-surface',
|
|
88
|
+
'close-surface',
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
const PANE_CMDS = new Set(['list-panes', 'focus-pane', 'split']);
|
|
92
|
+
|
|
93
|
+
const INPUT_CMDS = new Set(['send', 'send-key', 'read-screen']);
|
|
94
|
+
|
|
95
|
+
const SYSTEM_CMDS = new Set([
|
|
96
|
+
'identify',
|
|
97
|
+
'capabilities',
|
|
98
|
+
'set-status',
|
|
99
|
+
'set-progress',
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
async function main(): Promise<void> {
|
|
103
|
+
// process.argv = ['node', 'index.js', ...userArgs]
|
|
104
|
+
const argv = process.argv.slice(2);
|
|
105
|
+
|
|
106
|
+
if (argv.length === 0 || hasFlag(argv, '--help') || hasFlag(argv, '-h')) {
|
|
107
|
+
process.stdout.write(HELP_TEXT);
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const jsonMode = hasFlag(argv, '--json');
|
|
112
|
+
|
|
113
|
+
// Strip global flags so commands see clean args
|
|
114
|
+
const args = argv.filter((a) => a !== '--json' && a !== '--help' && a !== '-h');
|
|
115
|
+
|
|
116
|
+
const cmd = args[0];
|
|
117
|
+
const rest = args.slice(1);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
if (WORKSPACE_CMDS.has(cmd)) {
|
|
121
|
+
await handleWorkspace(cmd, rest, jsonMode);
|
|
122
|
+
} else if (SURFACE_CMDS.has(cmd)) {
|
|
123
|
+
await handleSurface(cmd, rest, jsonMode);
|
|
124
|
+
} else if (PANE_CMDS.has(cmd)) {
|
|
125
|
+
await handlePane(cmd, rest, jsonMode);
|
|
126
|
+
} else if (INPUT_CMDS.has(cmd)) {
|
|
127
|
+
await handleInput(cmd, rest, jsonMode);
|
|
128
|
+
} else if (cmd === 'notify') {
|
|
129
|
+
await handleNotify(rest, jsonMode);
|
|
130
|
+
} else if (SYSTEM_CMDS.has(cmd)) {
|
|
131
|
+
await handleSystem(cmd, rest, jsonMode);
|
|
132
|
+
} else if (cmd === 'browser') {
|
|
133
|
+
await handleBrowser(rest, jsonMode);
|
|
134
|
+
} else {
|
|
135
|
+
console.error(`Unknown command: "${cmd}". Run 'wmux --help' for usage.`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
} catch (err: unknown) {
|
|
139
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
140
|
+
console.error(message);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
main();
|
package/src/cli/utils.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { RpcResponse } from '../shared/rpc';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Print the result field of a successful RPC response as JSON.
|
|
5
|
+
* If the response contains an error, the error is printed to stderr and
|
|
6
|
+
* the process exits with code 1.
|
|
7
|
+
*/
|
|
8
|
+
export function printResult(response: RpcResponse): void {
|
|
9
|
+
if (!response.ok) {
|
|
10
|
+
printError(response);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
console.log(JSON.stringify(response.result, null, 2));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Print the error field of a failed RPC response to stderr and exit with 1.
|
|
18
|
+
*/
|
|
19
|
+
export function printError(response: RpcResponse): void {
|
|
20
|
+
const msg = !response.ok ? response.error : 'Unknown error from wmux';
|
|
21
|
+
console.error(`Error: ${msg}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse a named flag value from an argv array.
|
|
27
|
+
* e.g. parseFlag(['--name', 'dev'], '--name') => 'dev'
|
|
28
|
+
* Returns undefined when the flag is not present.
|
|
29
|
+
*/
|
|
30
|
+
export function parseFlag(args: string[], flag: string): string | undefined {
|
|
31
|
+
const idx = args.indexOf(flag);
|
|
32
|
+
if (idx === -1) return undefined;
|
|
33
|
+
const value = args[idx + 1];
|
|
34
|
+
if (value === undefined || value.startsWith('-')) return undefined;
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check whether a bare flag is present in argv.
|
|
40
|
+
* e.g. hasFlag(['--json', 'identify'], '--json') => true
|
|
41
|
+
*/
|
|
42
|
+
export function hasFlag(args: string[], flag: string): boolean {
|
|
43
|
+
return args.includes(flag);
|
|
44
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
process.on('unhandledRejection', (reason) => {
|
|
2
|
+
console.error('[Main] Unhandled rejection:', reason);
|
|
3
|
+
});
|
|
4
|
+
process.on('uncaughtException', (err) => {
|
|
5
|
+
console.error('[Main] Uncaught exception:', err);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
import { app, BrowserWindow } from 'electron';
|
|
9
|
+
import started from 'electron-squirrel-startup';
|
|
10
|
+
import { createWindow } from './window/createWindow';
|
|
11
|
+
import { PTYManager } from './pty/PTYManager';
|
|
12
|
+
import { PTYBridge } from './pty/PTYBridge';
|
|
13
|
+
import { registerAllHandlers } from './ipc/registerHandlers';
|
|
14
|
+
import { RpcRouter } from './pipe/RpcRouter';
|
|
15
|
+
import { PipeServer } from './pipe/PipeServer';
|
|
16
|
+
import { registerWorkspaceRpc } from './pipe/handlers/workspace.rpc';
|
|
17
|
+
import { registerSurfaceRpc } from './pipe/handlers/surface.rpc';
|
|
18
|
+
import { registerPaneRpc } from './pipe/handlers/pane.rpc';
|
|
19
|
+
import { registerInputRpc } from './pipe/handlers/input.rpc';
|
|
20
|
+
import { registerNotifyRpc } from './pipe/handlers/notify.rpc';
|
|
21
|
+
import { registerMetaRpc } from './pipe/handlers/meta.rpc';
|
|
22
|
+
import { registerSystemRpc } from './pipe/handlers/system.rpc';
|
|
23
|
+
import { registerBrowserRpc } from './pipe/handlers/browser.rpc';
|
|
24
|
+
import { AutoUpdater } from './updater/AutoUpdater';
|
|
25
|
+
import { McpRegistrar } from './mcp/McpRegistrar';
|
|
26
|
+
|
|
27
|
+
if (started) {
|
|
28
|
+
app.quit();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ptyManager = new PTYManager();
|
|
32
|
+
let mainWindow: BrowserWindow | null = null;
|
|
33
|
+
const ptyBridge = new PTYBridge(ptyManager, () => mainWindow);
|
|
34
|
+
const autoUpdater = new AutoUpdater(() => mainWindow);
|
|
35
|
+
|
|
36
|
+
const rpcRouter = new RpcRouter();
|
|
37
|
+
const pipeServer = new PipeServer(rpcRouter);
|
|
38
|
+
const mcpRegistrar = new McpRegistrar();
|
|
39
|
+
|
|
40
|
+
const cleanupHandlers = registerAllHandlers(ptyManager, ptyBridge, () => mainWindow);
|
|
41
|
+
registerWorkspaceRpc(rpcRouter, () => mainWindow);
|
|
42
|
+
registerSurfaceRpc(rpcRouter, () => mainWindow);
|
|
43
|
+
registerPaneRpc(rpcRouter, () => mainWindow);
|
|
44
|
+
registerInputRpc(rpcRouter, ptyManager, () => mainWindow);
|
|
45
|
+
registerNotifyRpc(rpcRouter, () => mainWindow);
|
|
46
|
+
registerMetaRpc(rpcRouter, () => mainWindow);
|
|
47
|
+
registerSystemRpc(rpcRouter);
|
|
48
|
+
registerBrowserRpc(rpcRouter, () => mainWindow);
|
|
49
|
+
|
|
50
|
+
app.on('ready', () => {
|
|
51
|
+
console.log('[Main] App ready, creating window...');
|
|
52
|
+
mainWindow = createWindow();
|
|
53
|
+
console.log('[Main] Window created:', !!mainWindow);
|
|
54
|
+
mainWindow.on('closed', () => {
|
|
55
|
+
mainWindow = null;
|
|
56
|
+
});
|
|
57
|
+
mainWindow.webContents.on('did-fail-load', (_e, code, desc) => {
|
|
58
|
+
console.error('[Main] Page failed to load:', code, desc);
|
|
59
|
+
});
|
|
60
|
+
mainWindow.webContents.on('did-finish-load', () => {
|
|
61
|
+
console.log('[Main] Page loaded successfully');
|
|
62
|
+
});
|
|
63
|
+
pipeServer.start();
|
|
64
|
+
const authToken = pipeServer.getAuthToken();
|
|
65
|
+
ptyManager.setAuthToken(authToken);
|
|
66
|
+
mcpRegistrar.register(authToken);
|
|
67
|
+
autoUpdater.start();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
app.on('window-all-closed', () => {
|
|
71
|
+
app.quit();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
app.on('before-quit', () => {
|
|
75
|
+
cleanupHandlers();
|
|
76
|
+
ptyManager.disposeAll();
|
|
77
|
+
pipeServer.stop();
|
|
78
|
+
mcpRegistrar.unregister();
|
|
79
|
+
autoUpdater.stop();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
app.on('activate', () => {
|
|
83
|
+
if (BrowserWindow.getAllWindows().length === 0) {
|
|
84
|
+
mainWindow = createWindow();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ipcMain, clipboard } from 'electron';
|
|
2
|
+
import { IPC } from '../../../shared/constants';
|
|
3
|
+
|
|
4
|
+
export function registerClipboardHandlers(): void {
|
|
5
|
+
// Remove any previously registered handlers before re-registering.
|
|
6
|
+
// ipcMain.handle() throws if the same channel is registered twice (e.g.
|
|
7
|
+
// during dev HMR reloads), which silently kills clipboard IPC.
|
|
8
|
+
ipcMain.removeHandler(IPC.CLIPBOARD_WRITE);
|
|
9
|
+
ipcMain.removeHandler(IPC.CLIPBOARD_READ);
|
|
10
|
+
|
|
11
|
+
ipcMain.handle(IPC.CLIPBOARD_WRITE, (_event, text: string) => {
|
|
12
|
+
if (typeof text !== 'string') return;
|
|
13
|
+
if (text.length > 1_000_000) return; // 1MB limit
|
|
14
|
+
clipboard.writeText(text);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
ipcMain.handle(IPC.CLIPBOARD_READ, () => {
|
|
18
|
+
return clipboard.readText();
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
}
|