@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,247 +0,0 @@
1
- // Terminal agent status detection — monitors PTY output for known AI agent
2
- // prompt patterns and status indicators. This is status display only;
3
- // no content is captured, stored, or transmitted.
4
-
5
- export interface AgentEvent {
6
- agent: string;
7
- status: 'completed' | 'waiting' | 'running' | 'error';
8
- message: string;
9
- }
10
-
11
- export interface CriticalEvent {
12
- action: string;
13
- riskLevel: 'review' | 'critical';
14
- }
15
-
16
- type AgentEventCallback = (event: AgentEvent) => void;
17
- type CriticalEventCallback = (event: CriticalEvent) => void;
18
-
19
- interface AgentPattern {
20
- agent: string;
21
- patterns: { regex: RegExp; status: AgentEvent['status']; message: string }[];
22
- }
23
-
24
- // ---------------------------------------------------------------------------
25
- // Common cross-agent terminal patterns
26
- // ---------------------------------------------------------------------------
27
-
28
- /** Shared completion indicators (✓ ✔ Done Complete Finished Success) */
29
- const COMMON_COMPLETE: AgentPattern['patterns'] = [
30
- { regex: /[✓✔]\s+(.+)/, status: 'completed', message: 'Task completed' },
31
- { regex: /\b(Done|Complete(?:d)?|Finished|Success(?:ful)?)\b/, status: 'completed', message: 'Task completed' },
32
- ];
33
-
34
- /** Shared error indicators (✗ ✘ Error Failed error:) */
35
- const COMMON_ERROR: AgentPattern['patterns'] = [
36
- { regex: /[✗✘]\s+(.+)/, status: 'error', message: 'Error occurred' },
37
- { regex: /\bFailed\b/, status: 'error', message: 'Task failed' },
38
- { regex: /\berror:\s+(.+)/i, status: 'error', message: 'Error occurred' },
39
- ];
40
-
41
- /** Shared waiting indicators (? Waiting for Press y/n [Y/n]) */
42
- const COMMON_WAITING: AgentPattern['patterns'] = [
43
- { regex: /\?\s+(.+)/, status: 'waiting', message: 'Waiting for input' },
44
- { regex: /Waiting for\s+(.+)/i, status: 'waiting', message: 'Waiting for input' },
45
- { regex: /Press\s+.+\s+to\s+/i, status: 'waiting', message: 'Waiting for key press' },
46
- { regex: /\[Y\/n\]|\(y\/n\)/i, status: 'waiting', message: 'Waiting for confirmation' },
47
- ];
48
-
49
- // ---------------------------------------------------------------------------
50
- // Per-agent patterns
51
- // ---------------------------------------------------------------------------
52
-
53
- const AGENT_PATTERNS: AgentPattern[] = [
54
- // ── Claude Code ────────────────────────────────────────────────────────────
55
- {
56
- agent: 'Claude Code',
57
- patterns: [
58
- { regex: /⏳\s+(.+)/, status: 'running', message: 'Processing...' },
59
- { regex: /❌\s+(.+)/, status: 'error', message: 'Error occurred' },
60
- { regex: /Do you want to/, status: 'waiting', message: 'Waiting for confirmation' },
61
- ...COMMON_COMPLETE,
62
- ...COMMON_ERROR,
63
- ...COMMON_WAITING,
64
- ],
65
- },
66
-
67
- // ── Cursor Agent ──────────────────────────────────────────────────────────
68
- {
69
- agent: 'Cursor Agent',
70
- patterns: [
71
- { regex: /Applied \d+ changes?/, status: 'completed', message: 'Changes applied' },
72
- { regex: /Thinking\.\.\./, status: 'running', message: 'Thinking...' },
73
- ...COMMON_COMPLETE,
74
- ...COMMON_ERROR,
75
- ...COMMON_WAITING,
76
- ],
77
- },
78
-
79
- // ── Aider ─────────────────────────────────────────────────────────────────
80
- {
81
- agent: 'Aider',
82
- patterns: [
83
- { regex: /Applied edit to/, status: 'completed', message: 'Edit applied' },
84
- { regex: /aider>/, status: 'waiting', message: 'Waiting for input' },
85
- ...COMMON_COMPLETE,
86
- ...COMMON_ERROR,
87
- ...COMMON_WAITING,
88
- ],
89
- },
90
-
91
- // ── Codex CLI ─────────────────────────────────────────────────────────────
92
- {
93
- agent: 'Codex CLI',
94
- patterns: [
95
- { regex: /codex>/, status: 'waiting', message: 'Waiting for input' },
96
- { regex: /Codex:\s+(.+)/, status: 'running', message: 'Processing...' },
97
- ...COMMON_COMPLETE,
98
- ...COMMON_ERROR,
99
- ...COMMON_WAITING,
100
- ],
101
- },
102
-
103
- // ── Gemini CLI ────────────────────────────────────────────────────────────
104
- {
105
- agent: 'Gemini CLI',
106
- patterns: [
107
- { regex: /gemini>/, status: 'waiting', message: 'Waiting for input' },
108
- { regex: /Gemini:\s+(.+)/, status: 'running', message: 'Processing...' },
109
- ...COMMON_COMPLETE,
110
- ...COMMON_ERROR,
111
- ...COMMON_WAITING,
112
- ],
113
- },
114
-
115
- // ── OpenCode ──────────────────────────────────────────────────────────────
116
- {
117
- agent: 'OpenCode',
118
- patterns: [
119
- { regex: /opencode>/, status: 'waiting', message: 'Waiting for input' },
120
- ...COMMON_COMPLETE,
121
- ...COMMON_ERROR,
122
- ...COMMON_WAITING,
123
- ],
124
- },
125
-
126
- // ── GitHub Copilot CLI ────────────────────────────────────────────────────
127
- {
128
- agent: 'GitHub Copilot CLI',
129
- patterns: [
130
- { regex: /copilot>/, status: 'waiting', message: 'Waiting for input' },
131
- { regex: /gh copilot\s+(.+)/, status: 'running', message: 'Processing...' },
132
- ...COMMON_COMPLETE,
133
- ...COMMON_ERROR,
134
- ...COMMON_WAITING,
135
- ],
136
- },
137
- ];
138
-
139
- // ---------------------------------------------------------------------------
140
- // Critical action patterns — require approval before execution
141
- // ---------------------------------------------------------------------------
142
-
143
- interface CriticalPattern {
144
- regex: RegExp;
145
- riskLevel: 'review' | 'critical';
146
- label: string;
147
- }
148
-
149
- const CRITICAL_PATTERNS: CriticalPattern[] = [
150
- // Destructive git operations
151
- { regex: /git\s+push\s+(?:.*--force|-f)\b/i, riskLevel: 'critical', label: 'git push --force' },
152
- { regex: /git\s+reset\s+--hard\b/i, riskLevel: 'critical', label: 'git reset --hard' },
153
- { regex: /git\s+clean\s+.*-f\b/i, riskLevel: 'critical', label: 'git clean -f' },
154
- // File system wipe
155
- { regex: /\brm\s+(?:.*-r.*-f|-f.*-r|-rf|-fr)\s+/i, riskLevel: 'critical', label: 'rm -rf' },
156
- { regex: /\brmdir\s+\/[sS]\s+/, riskLevel: 'critical', label: 'rmdir /S' },
157
- // Database destructive
158
- { regex: /\bDROP\s+(?:TABLE|DATABASE|SCHEMA)\b/i, riskLevel: 'critical', label: 'DROP TABLE/DATABASE' },
159
- { regex: /\bDELETE\s+FROM\b/i, riskLevel: 'review', label: 'DELETE FROM' },
160
- { regex: /\bTRUNCATE\s+TABLE\b/i, riskLevel: 'critical', label: 'TRUNCATE TABLE' },
161
- // NPM publishing
162
- { regex: /\bnpm\s+publish\b/i, riskLevel: 'critical', label: 'npm publish' },
163
- { regex: /\bnpx\s+.*--publish\b/i, riskLevel: 'review', label: 'npx publish' },
164
- // Cloud resource destruction
165
- { regex: /\bterraform\s+destroy\b/i, riskLevel: 'critical', label: 'terraform destroy' },
166
- { regex: /\bkubectl\s+delete\b/i, riskLevel: 'review', label: 'kubectl delete' },
167
- { regex: /\baws\s+.*\s+delete\b/i, riskLevel: 'review', label: 'aws delete' },
168
- // Disk formatting
169
- { regex: /\bformat\s+[A-Za-z]:\\/i, riskLevel: 'critical', label: 'format disk' },
170
- { regex: /\bmkfs\b/i, riskLevel: 'critical', label: 'mkfs' },
171
- ];
172
-
173
- const MAX_BUFFER = 16 * 1024; // 16 KB
174
-
175
- export class AgentDetector {
176
- private callbacks: AgentEventCallback[] = [];
177
- private criticalCallbacks: CriticalEventCallback[] = [];
178
- private lineBuffer = '';
179
- private lastEmittedKey = '';
180
-
181
- onEvent(callback: AgentEventCallback): void {
182
- this.callbacks.push(callback);
183
- }
184
-
185
- onCritical(callback: CriticalEventCallback): void {
186
- this.criticalCallbacks.push(callback);
187
- }
188
-
189
- feed(data: string): void {
190
- // Accumulate lines
191
- this.lineBuffer += data;
192
- // Prevent unbounded buffer growth
193
- if (this.lineBuffer.length > MAX_BUFFER) {
194
- this.lineBuffer = this.lineBuffer.slice(-MAX_BUFFER);
195
- }
196
- const lines = this.lineBuffer.split(/\r?\n/);
197
- // Keep the last incomplete line in buffer
198
- this.lineBuffer = lines.pop() || '';
199
-
200
- for (const line of lines) {
201
- this.processLine(line);
202
- }
203
- }
204
-
205
- private processLine(line: string): void {
206
- // Strip ANSI escape codes for pattern matching
207
- const clean = line.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').trim();
208
- if (!clean) return;
209
-
210
- // Check critical patterns first
211
- for (const cp of CRITICAL_PATTERNS) {
212
- if (cp.regex.test(clean)) {
213
- const key = `critical:${cp.label}:${clean.slice(0, 80)}`;
214
- if (key !== this.lastEmittedKey) {
215
- this.lastEmittedKey = key;
216
- const event: CriticalEvent = { action: cp.label, riskLevel: cp.riskLevel };
217
- for (const cb of this.criticalCallbacks) {
218
- cb(event);
219
- }
220
- }
221
- return;
222
- }
223
- }
224
-
225
- for (const ap of AGENT_PATTERNS) {
226
- for (const p of ap.patterns) {
227
- const match = clean.match(p.regex);
228
- if (match) {
229
- // Deduplicate: don't emit the same event twice in a row
230
- const key = `${ap.agent}:${p.status}:${match[0]}`;
231
- if (key === this.lastEmittedKey) return;
232
- this.lastEmittedKey = key;
233
-
234
- const event: AgentEvent = {
235
- agent: ap.agent,
236
- status: p.status,
237
- message: match[1] || p.message,
238
- };
239
- for (const cb of this.callbacks) {
240
- cb(event);
241
- }
242
- return;
243
- }
244
- }
245
- }
246
- }
247
- }
@@ -1,81 +0,0 @@
1
- export interface OscEvent {
2
- code: number;
3
- data: string;
4
- }
5
-
6
- export type OscCallback = (event: OscEvent) => void;
7
-
8
- /**
9
- * Parses OSC (Operating System Command) sequences from terminal data.
10
- * Handles OSC 7 (CWD), OSC 9/99/777 (notifications).
11
- */
12
- const MAX_BUFFER = 64 * 1024; // 64 KB
13
-
14
- export class OscParser {
15
- private buffer = '';
16
- private inOsc = false;
17
- private callbacks: OscCallback[] = [];
18
-
19
- onOsc(callback: OscCallback): void {
20
- this.callbacks.push(callback);
21
- }
22
-
23
- /**
24
- * Process terminal data, extract OSC sequences, return cleaned data.
25
- */
26
- process(data: string): string {
27
- let result = '';
28
- let i = 0;
29
-
30
- while (i < data.length) {
31
- if (this.inOsc) {
32
- // Look for ST (String Terminator): BEL (\x07) or ESC \ (\x1b\x5c)
33
- if (data[i] === '\x07') {
34
- this.emitOsc(this.buffer);
35
- this.buffer = '';
36
- this.inOsc = false;
37
- i++;
38
- } else if (data[i] === '\x1b' && i + 1 < data.length && data[i + 1] === '\\') {
39
- this.emitOsc(this.buffer);
40
- this.buffer = '';
41
- this.inOsc = false;
42
- i += 2;
43
- } else {
44
- this.buffer += data[i];
45
- // Prevent unbounded buffer growth
46
- if (this.buffer.length > MAX_BUFFER) {
47
- this.buffer = '';
48
- this.inOsc = false;
49
- }
50
- i++;
51
- }
52
- } else if (data[i] === '\x1b' && i + 1 < data.length && data[i + 1] === ']') {
53
- // OSC start: ESC ]
54
- this.inOsc = true;
55
- this.buffer = '';
56
- i += 2;
57
- } else {
58
- result += data[i];
59
- i++;
60
- }
61
- }
62
-
63
- return result;
64
- }
65
-
66
- private emitOsc(raw: string): void {
67
- // OSC format: code;data
68
- const semicolonIdx = raw.indexOf(';');
69
- if (semicolonIdx === -1) return;
70
-
71
- const codeStr = raw.substring(0, semicolonIdx);
72
- const code = parseInt(codeStr, 10);
73
- if (isNaN(code)) return;
74
-
75
- const data = raw.substring(semicolonIdx + 1);
76
-
77
- for (const cb of this.callbacks) {
78
- cb({ code, data });
79
- }
80
- }
81
- }
@@ -1,88 +0,0 @@
1
- import { BrowserWindow } from 'electron';
2
- import { PTYManager } from './PTYManager';
3
- import { OscParser } from './OscParser';
4
- import { AgentDetector } from './AgentDetector';
5
- import { ToastManager } from '../notification/ToastManager';
6
- import { IPC } from '../../shared/constants';
7
-
8
- export class PTYBridge {
9
- private oscParsers = new Map<string, OscParser>();
10
- private agentDetectors = new Map<string, AgentDetector>();
11
- private toastManager = new ToastManager();
12
-
13
- constructor(
14
- private ptyManager: PTYManager,
15
- private getWindow: () => BrowserWindow | null,
16
- ) {}
17
-
18
- setupDataForwarding(ptyId: string): void {
19
- const instance = this.ptyManager.get(ptyId);
20
- if (!instance) return;
21
-
22
- const oscParser = new OscParser();
23
- this.oscParsers.set(ptyId, oscParser);
24
-
25
- const agentDetector = new AgentDetector();
26
- this.agentDetectors.set(ptyId, agentDetector);
27
-
28
- // Handle OSC events
29
- oscParser.onOsc((event) => {
30
- const win = this.getWindow();
31
- if (!win || win.isDestroyed()) return;
32
-
33
- switch (event.code) {
34
- case 7: {
35
- // CWD changed — data is typically file://host/path
36
- const cwd = event.data.replace(/^file:\/\/[^/]*/, '');
37
- win.webContents.send(IPC.CWD_CHANGED, ptyId, cwd);
38
- break;
39
- }
40
- case 9: // Windows Terminal notification
41
- case 99: // iTerm2 notification
42
- case 777: // rxvt-unicode notification
43
- // Silently ignore — no notification, no sound
44
- break;
45
- }
46
- });
47
-
48
- // Handle agent detection events — status tracking only, no notification/sound
49
- agentDetector.onEvent(() => {
50
- // Agent status is tracked internally by AgentDetector.
51
- // No notification or sound — these fire too frequently and flood the UI.
52
- });
53
-
54
- // Handle critical action events — send approval request to renderer
55
- agentDetector.onCritical((criticalEvent) => {
56
- const win = this.getWindow();
57
- if (!win || win.isDestroyed()) return;
58
-
59
- win.webContents.send(IPC.APPROVAL_REQUEST, ptyId, {
60
- action: criticalEvent.action,
61
- riskLevel: criticalEvent.riskLevel,
62
- });
63
- });
64
-
65
- instance.process.onData((data: string) => {
66
- const win = this.getWindow();
67
- if (win && !win.isDestroyed()) {
68
- // Process data through OscParser (strips OSC sequences)
69
- oscParser.process(data);
70
- // Feed data to AgentDetector
71
- agentDetector.feed(data);
72
- // Forward raw data to renderer (xterm handles OSC itself)
73
- win.webContents.send(IPC.PTY_DATA, ptyId, data);
74
- }
75
- });
76
-
77
- instance.process.onExit(({ exitCode }) => {
78
- const win = this.getWindow();
79
- if (win && !win.isDestroyed()) {
80
- win.webContents.send(IPC.PTY_EXIT, ptyId, exitCode);
81
- }
82
- this.oscParsers.delete(ptyId);
83
- this.agentDetectors.delete(ptyId);
84
- // Process already exited — remove from map without calling kill()
85
- this.ptyManager.remove(ptyId);
86
- });
87
- }
88
- }
@@ -1,104 +0,0 @@
1
- import * as pty from 'node-pty';
2
- import os from 'node:os';
3
- import { getPipeName, ENV_KEYS } from '../../shared/constants';
4
-
5
- export interface PTYInstance {
6
- id: string;
7
- process: pty.IPty;
8
- shell: string;
9
- }
10
-
11
- export class PTYManager {
12
- private instances = new Map<string, PTYInstance>();
13
- private nextId = 0;
14
- private _authToken: string | undefined;
15
-
16
- setAuthToken(token: string): void {
17
- this._authToken = token;
18
- }
19
-
20
- create(options?: {
21
- shell?: string;
22
- cwd?: string;
23
- cols?: number;
24
- rows?: number;
25
- workspaceId?: string;
26
- surfaceId?: string;
27
- authToken?: string;
28
- }): PTYInstance {
29
- const id = `pty-${++this.nextId}`;
30
- const shell = options?.shell || this.getDefaultShell();
31
- const cwd = options?.cwd || os.homedir();
32
-
33
- // Filter out ELECTRON_ variables to prevent leaking internal state to child processes
34
- const env: Record<string, string> = {};
35
- for (const [key, value] of Object.entries(globalThis.process.env)) {
36
- if (value === undefined) continue;
37
- if (key.startsWith('ELECTRON_')) continue;
38
- env[key] = value;
39
- }
40
- env[ENV_KEYS.SOCKET_PATH] = getPipeName();
41
- if (options?.workspaceId) env[ENV_KEYS.WORKSPACE_ID] = options.workspaceId;
42
- if (options?.surfaceId) env[ENV_KEYS.SURFACE_ID] = options.surfaceId;
43
- const authToken = options?.authToken || this._authToken;
44
- if (authToken) env[ENV_KEYS.AUTH_TOKEN] = authToken;
45
-
46
- const process = pty.spawn(shell, [], {
47
- name: 'xterm-256color',
48
- cols: options?.cols || 80,
49
- rows: options?.rows || 24,
50
- cwd,
51
- env,
52
- useConpty: true,
53
- });
54
-
55
- const instance: PTYInstance = { id, process, shell };
56
- this.instances.set(id, instance);
57
- return instance;
58
- }
59
-
60
- write(id: string, data: string): void {
61
- const instance = this.instances.get(id);
62
- if (instance) {
63
- instance.process.write(data);
64
- }
65
- }
66
-
67
- resize(id: string, cols: number, rows: number): void {
68
- const instance = this.instances.get(id);
69
- if (instance) {
70
- instance.process.resize(cols, rows);
71
- }
72
- }
73
-
74
- dispose(id: string): void {
75
- const instance = this.instances.get(id);
76
- if (instance) {
77
- try { instance.process.kill(); } catch { /* already dead */ }
78
- this.instances.delete(id);
79
- }
80
- }
81
-
82
- /** Remove an entry from the map without killing — use when the process has already exited. */
83
- remove(id: string): void {
84
- this.instances.delete(id);
85
- }
86
-
87
- get(id: string): PTYInstance | undefined {
88
- return this.instances.get(id);
89
- }
90
-
91
- disposeAll(): void {
92
- for (const [id] of this.instances) {
93
- this.dispose(id);
94
- }
95
- }
96
-
97
- private getDefaultShell(): string {
98
- if (process.platform === 'win32') {
99
- // PowerShell 우선 (cd로 드라이브 전환 자동 지원)
100
- return 'powershell.exe';
101
- }
102
- return process.env.SHELL || '/bin/bash';
103
- }
104
- }
@@ -1,63 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
-
4
- export interface ShellInfo {
5
- name: string;
6
- path: string;
7
- args?: string[];
8
- }
9
-
10
- export class ShellDetector {
11
- detect(): ShellInfo[] {
12
- const shells: ShellInfo[] = [];
13
-
14
- // PowerShell 7+ (pwsh)
15
- const pwshPaths = [
16
- 'C:\\Program Files\\PowerShell\\7\\pwsh.exe',
17
- path.join(process.env.LOCALAPPDATA || '', 'Microsoft\\WindowsApps\\pwsh.exe'),
18
- ];
19
- for (const p of pwshPaths) {
20
- if (fs.existsSync(p)) {
21
- shells.push({ name: 'PowerShell 7', path: p });
22
- break;
23
- }
24
- }
25
-
26
- // Windows PowerShell 5.1
27
- const ps5 = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32\\WindowsPowerShell\\v1.0\\powershell.exe');
28
- if (fs.existsSync(ps5)) {
29
- shells.push({ name: 'Windows PowerShell', path: ps5 });
30
- }
31
-
32
- // Git Bash
33
- const gitBashPaths = [
34
- 'C:\\Program Files\\Git\\bin\\bash.exe',
35
- 'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
36
- ];
37
- for (const p of gitBashPaths) {
38
- if (fs.existsSync(p)) {
39
- shells.push({ name: 'Git Bash', path: p, args: ['--login', '-i'] });
40
- break;
41
- }
42
- }
43
-
44
- // WSL
45
- const wslPath = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32\\wsl.exe');
46
- if (fs.existsSync(wslPath)) {
47
- shells.push({ name: 'WSL', path: wslPath });
48
- }
49
-
50
- // cmd.exe
51
- const cmd = process.env.COMSPEC || path.join(process.env.SystemRoot || 'C:\\Windows', 'System32\\cmd.exe');
52
- if (fs.existsSync(cmd)) {
53
- shells.push({ name: 'Command Prompt', path: cmd });
54
- }
55
-
56
- return shells;
57
- }
58
-
59
- getDefault(): string {
60
- const shells = this.detect();
61
- return shells.length > 0 ? shells[0].path : 'powershell.exe';
62
- }
63
- }
@@ -1,53 +0,0 @@
1
- import { app } from 'electron';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import type { SessionData } from '../../shared/types';
5
-
6
- export class SessionManager {
7
- private filePath: string;
8
-
9
- constructor() {
10
- this.filePath = path.join(app.getPath('userData'), 'session.json');
11
- }
12
-
13
- save(data: SessionData): void {
14
- try {
15
- const dir = path.dirname(this.filePath);
16
- if (!fs.existsSync(dir)) {
17
- fs.mkdirSync(dir, { recursive: true });
18
- }
19
- fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2), 'utf-8');
20
- } catch (err) {
21
- console.error('Failed to save session:', err);
22
- }
23
- }
24
-
25
- load(): SessionData | null {
26
- try {
27
- if (!fs.existsSync(this.filePath)) return null;
28
- const raw = fs.readFileSync(this.filePath, 'utf-8');
29
-
30
- // Guard against prototype pollution via JSON reviver
31
- const parsed: unknown = JSON.parse(raw, (key, value) => {
32
- if (key === '__proto__' || key === 'constructor' || key === 'prototype') return undefined;
33
- return value;
34
- });
35
-
36
- // Basic schema validation
37
- if (
38
- typeof parsed !== 'object' ||
39
- parsed === null ||
40
- !Array.isArray((parsed as Record<string, unknown>)['workspaces']) ||
41
- typeof (parsed as Record<string, unknown>)['activeWorkspaceId'] !== 'string'
42
- ) {
43
- console.warn('[SessionManager] Session file failed schema validation — discarding.');
44
- return null;
45
- }
46
-
47
- return parsed as SessionData;
48
- } catch (err) {
49
- console.error('Failed to load session:', err);
50
- return null;
51
- }
52
- }
53
- }