@geekbeer/minion 3.43.0 → 3.49.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.
@@ -0,0 +1,7 @@
1
+ /**
2
+ * macOS chat routes — re-exports the Linux implementation.
3
+ *
4
+ * Spawns the configured LLM CLI (Claude Code / Gemini / Codex) for SSE
5
+ * streaming. spawn() and PATH resolution work identically on macOS.
6
+ */
7
+ module.exports = require('../../linux/routes/chat')
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Command execution endpoints (macOS)
3
+ *
4
+ * Endpoints:
5
+ * - GET /api/commands - List available commands
6
+ * - POST /api/command - Execute a whitelisted command
7
+ *
8
+ * Identical to linux/routes/commands.js apart from the process-manager source —
9
+ * macOS uses launchd, so we import from mac/lib/ (not linux/lib/).
10
+ */
11
+
12
+ const { exec } = require('child_process')
13
+ const { promisify } = require('util')
14
+ const execAsync = promisify(exec)
15
+
16
+ const { verifyToken } = require('../../core/lib/auth')
17
+ const { detectProcessManager, buildAllowedCommands } = require('../lib/process-manager')
18
+
19
+ const PROC_MGR = detectProcessManager()
20
+ const ALLOWED_COMMANDS = buildAllowedCommands(PROC_MGR)
21
+
22
+ /**
23
+ * Register command routes as Fastify plugin
24
+ * @param {import('fastify').FastifyInstance} fastify
25
+ */
26
+ async function commandRoutes(fastify) {
27
+ fastify.get('/api/commands', async (request, reply) => {
28
+ if (!verifyToken(request)) {
29
+ reply.code(401)
30
+ return { success: false, error: 'Unauthorized' }
31
+ }
32
+
33
+ return {
34
+ commands: Object.entries(ALLOWED_COMMANDS).map(([name, info]) => ({
35
+ name,
36
+ description: info.description,
37
+ })),
38
+ }
39
+ })
40
+
41
+ fastify.post('/api/command', async (request, reply) => {
42
+ if (!verifyToken(request)) {
43
+ reply.code(401)
44
+ return { success: false, error: 'Unauthorized' }
45
+ }
46
+
47
+ const { command } = request.body || {}
48
+
49
+ if (!command) {
50
+ reply.code(400)
51
+ return { success: false, error: 'command is required' }
52
+ }
53
+
54
+ const allowedCommand = ALLOWED_COMMANDS[command]
55
+ if (!allowedCommand) {
56
+ reply.code(403)
57
+ return {
58
+ success: false,
59
+ error: `Command '${command}' is not allowed`,
60
+ allowed_commands: Object.keys(ALLOWED_COMMANDS),
61
+ }
62
+ }
63
+
64
+ console.log(`[Command] Executing: ${command}`)
65
+
66
+ // Deferred commands kill the current process, so respond first.
67
+ if (allowedCommand.deferred) {
68
+ console.log(`[Command] Scheduling deferred command: ${command}`)
69
+ setTimeout(() => {
70
+ exec(allowedCommand.command, { timeout: 60000 }, (err) => {
71
+ if (err) console.error(`[Command] Deferred command failed: ${command} - ${err.message}`)
72
+ else console.log(`[Command] Deferred command completed: ${command}`)
73
+ })
74
+ }, 1000)
75
+
76
+ return {
77
+ success: true,
78
+ command,
79
+ output: 'Command scheduled for execution',
80
+ deferred: true,
81
+ }
82
+ }
83
+
84
+ try {
85
+ const { stdout, stderr } = await execAsync(allowedCommand.command, { timeout: 60000 })
86
+ console.log(`[Command] Success: ${command}`)
87
+ return {
88
+ success: true,
89
+ command,
90
+ output: stdout,
91
+ stderr: stderr || undefined,
92
+ }
93
+ } catch (error) {
94
+ console.error(`[Command] Failed: ${command} - ${error.message}`)
95
+ reply.code(500)
96
+ return {
97
+ success: false,
98
+ command,
99
+ error: error.message,
100
+ stdout: error.stdout,
101
+ stderr: error.stderr,
102
+ }
103
+ }
104
+ })
105
+ }
106
+
107
+ function getProcessManager() {
108
+ return PROC_MGR
109
+ }
110
+
111
+ function getAllowedCommands() {
112
+ return ALLOWED_COMMANDS
113
+ }
114
+
115
+ module.exports = {
116
+ commandRoutes,
117
+ getProcessManager,
118
+ getAllowedCommands,
119
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * macOS config routes — re-exports the Linux implementation.
3
+ *
4
+ * Audited 2026-04: linux/routes/config.js has no Linux-specific paths
5
+ * (no /etc/cloudflared, no systemd). It uses ~/... paths expanded via
6
+ * process.env.HOME and BSD-compatible tar -czf / -xzf flags.
7
+ */
8
+ module.exports = require('../../linux/routes/config')
@@ -0,0 +1,6 @@
1
+ /**
2
+ * macOS directives routes — re-exports the Linux implementation.
3
+ *
4
+ * One-shot skill execution via tmux session (works the same on macOS).
5
+ */
6
+ module.exports = require('../../linux/routes/directives')
@@ -0,0 +1,6 @@
1
+ /**
2
+ * macOS files routes — re-exports the Linux implementation.
3
+ *
4
+ * Pure filesystem operations under ~/files/ — no platform-specific code paths.
5
+ */
6
+ module.exports = require('../../linux/routes/files')
@@ -0,0 +1,7 @@
1
+ /**
2
+ * macOS terminal routes — re-exports the Linux implementation.
3
+ *
4
+ * Linux's terminal.js manages tmux sessions and ttyd processes; both tools
5
+ * are POSIX-compatible and work identically on macOS via Homebrew.
6
+ */
7
+ module.exports = require('../../linux/routes/terminal')
@@ -0,0 +1,4 @@
1
+ /**
2
+ * macOS routine runner — re-exports the Linux implementation.
3
+ */
4
+ module.exports = require('../linux/routine-runner')
package/mac/server.js ADDED
@@ -0,0 +1,413 @@
1
+ /**
2
+ * Minion Agent HTTP Server (macOS)
3
+ *
4
+ * Entry point for the minion agent on macOS.
5
+ * Registers shared routes (from core/) and macOS-specific routes.
6
+ *
7
+ * Architecture notes (macOS):
8
+ * - Runs as a LaunchAgent under the dedicated `minion` user (~/Library/LaunchAgents/).
9
+ * - DATA_DIR is ~/.minion (resolved by core/lib/platform.js).
10
+ * - VNC: native macOS Screen Sharing on port 5900 + websockify front on port 6080.
11
+ * - Terminal: tmux + ttyd (same as Linux), via Homebrew.
12
+ *
13
+ * API surface is identical to the Linux build — most routes are re-exports
14
+ * of the Linux implementation; only commands.js / process-manager.js differ
15
+ * because launchctl replaces systemd/supervisord.
16
+ */
17
+
18
+ const fs = require('fs')
19
+ const path = require('path')
20
+
21
+ const fastify = require('fastify')({ logger: true })
22
+
23
+ // Package root (one level up from mac/)
24
+ const PACKAGE_ROOT = path.join(__dirname, '..')
25
+
26
+ // Core shared modules
27
+ const { config, validate, isHqConfigured } = require('../core/config')
28
+ const { sendHeartbeat } = require('../core/api')
29
+ const { version } = require('../package.json')
30
+
31
+ const workflowStore = require('../core/stores/workflow-store')
32
+ const routineStore = require('../core/stores/routine-store')
33
+
34
+ // Heartbeat interval: fixed at 30s (not user-configurable)
35
+ const HEARTBEAT_INTERVAL_MS = 30_000
36
+ let heartbeatTimer = null
37
+ let lastBeatAt = null
38
+
39
+ // macOS-specific modules (most re-export the Linux implementation)
40
+ const workflowRunner = require('./workflow-runner')
41
+ const routineRunner = require('./routine-runner')
42
+ const boardTaskRunner = require('./board-task-runner')
43
+ const { cleanupTtyd, killStaleTtydProcesses } = require('./routes/terminal')
44
+ const { startTerminalProxy, stopTerminalProxy } = require('./terminal-proxy')
45
+
46
+ // Config warnings (included in heartbeat)
47
+ const { getConfigWarnings } = require('../core/lib/config-warnings')
48
+
49
+ // Pull-model daemons (from core/)
50
+ const stepPoller = require('../core/lib/step-poller')
51
+ const dagStepPoller = require('../core/lib/dag-step-poller')
52
+ const boardTaskPoller = require('../core/lib/board-task-poller')
53
+ const revisionWatcher = require('../core/lib/revision-watcher')
54
+ const reflectionScheduler = require('../core/lib/reflection-scheduler')
55
+ const threadWatcher = require('../core/lib/thread-watcher')
56
+ const runningTasks = require('../core/lib/running-tasks')
57
+
58
+ // Shared routes (from core/)
59
+ const { healthRoutes, setOffline } = require('../core/routes/health')
60
+ const { diagnoseRoutes } = require('../core/routes/diagnose')
61
+ const { skillRoutes } = require('../core/routes/skills')
62
+ const { workflowRoutes } = require('../core/routes/workflows')
63
+ const { routineRoutes } = require('../core/routes/routines')
64
+ const { authRoutes } = require('../core/routes/auth')
65
+ const { variableRoutes } = require('../core/routes/variables')
66
+ const { memoryRoutes } = require('../core/routes/memory')
67
+ const { dailyLogRoutes } = require('../core/routes/daily-logs')
68
+ const { sudoersRoutes } = require('../core/routes/sudoers')
69
+ const { permissionRoutes } = require('../core/routes/permissions')
70
+ const { threadRoutes } = require('../core/routes/threads')
71
+ const { todoRoutes } = require('../core/routes/todos')
72
+ const { emailRoutes } = require('../core/routes/emails')
73
+ const { daemonRoutes } = require('../core/routes/daemons')
74
+ const { llmRoutes } = require('../core/routes/llm')
75
+
76
+ // macOS-specific routes
77
+ const { commandRoutes, getProcessManager, getAllowedCommands } = require('./routes/commands')
78
+ const { terminalRoutes } = require('./routes/terminal')
79
+ const { fileRoutes } = require('./routes/files')
80
+ const { directiveRoutes } = require('./routes/directives')
81
+ const { chatRoutes, runQuickLlmCall } = require('./routes/chat')
82
+ const { configRoutes } = require('./routes/config')
83
+
84
+ // Validate configuration before starting
85
+ validate()
86
+ const PROC_MGR = getProcessManager()
87
+ const ALLOWED_COMMANDS = getAllowedCommands()
88
+ console.log(`[Config] Process manager: ${PROC_MGR}`)
89
+ console.log(`[Config] Available commands: ${Object.keys(ALLOWED_COMMANDS).join(', ')}`)
90
+
91
+ // Graceful shutdown
92
+ async function shutdown(signal) {
93
+ console.log(`[Server] Received ${signal}, shutting down...`)
94
+
95
+ setOffline()
96
+
97
+ // Stop heartbeat timer
98
+ if (heartbeatTimer) {
99
+ clearInterval(heartbeatTimer)
100
+ heartbeatTimer = null
101
+ }
102
+
103
+ // Clear running tasks and send offline heartbeat to HQ (best-effort)
104
+ runningTasks.clear()
105
+ if (isHqConfigured()) {
106
+ try {
107
+ await Promise.race([
108
+ sendHeartbeat({ status: 'offline', running_tasks: [], version }),
109
+ new Promise(resolve => setTimeout(resolve, 3000)),
110
+ ])
111
+ } catch {
112
+ // Best-effort — don't block shutdown
113
+ }
114
+ }
115
+
116
+ // Stop pollers, runners, and scheduler
117
+ stepPoller.stop()
118
+ dagStepPoller.stop()
119
+ boardTaskPoller.stop()
120
+ revisionWatcher.stop()
121
+ reflectionScheduler.stop()
122
+ threadWatcher.stop()
123
+ workflowRunner.stopAll()
124
+ routineRunner.stopAll()
125
+
126
+ // Stop terminal proxy and all ttyd processes
127
+ stopTerminalProxy()
128
+ cleanupTtyd()
129
+
130
+ // Close database and server
131
+ const { closeDb } = require('../core/db')
132
+ closeDb()
133
+ await fastify.close()
134
+ process.exit(0)
135
+ }
136
+
137
+ process.on('SIGTERM', () => shutdown('SIGTERM'))
138
+ process.on('SIGINT', () => shutdown('SIGINT'))
139
+
140
+ /**
141
+ * Sync bundled permissions into ~/.claude/settings.json.
142
+ */
143
+ function syncPermissions() {
144
+ const bundledPath = path.join(PACKAGE_ROOT, 'settings', 'permissions.json')
145
+ const settingsDir = path.join(config.HOME_DIR, '.claude')
146
+ const settingsPath = path.join(settingsDir, 'settings.json')
147
+
148
+ try {
149
+ if (!fs.existsSync(bundledPath)) return
150
+
151
+ const bundled = JSON.parse(fs.readFileSync(bundledPath, 'utf-8'))
152
+
153
+ let settings = {}
154
+ if (fs.existsSync(settingsPath)) {
155
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'))
156
+ }
157
+
158
+ settings.permissions = {
159
+ allow: bundled.allow || [],
160
+ deny: bundled.deny || [],
161
+ }
162
+
163
+ fs.mkdirSync(settingsDir, { recursive: true })
164
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8')
165
+ console.log(`[Permissions] Synced: allow=${bundled.allow.length}, deny=${bundled.deny.length}`)
166
+ } catch (err) {
167
+ console.error(`[Permissions] Failed to sync permissions: ${err.message}`)
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Sync bundled tmux.conf to ~/.tmux.conf.
173
+ */
174
+ function syncTmuxConfig() {
175
+ const bundledPath = path.join(PACKAGE_ROOT, 'settings', 'tmux.conf')
176
+ const destPath = path.join(config.HOME_DIR, '.tmux.conf')
177
+
178
+ try {
179
+ if (!fs.existsSync(bundledPath)) return
180
+
181
+ fs.copyFileSync(bundledPath, destPath)
182
+ console.log('[Tmux] Synced tmux.conf')
183
+ } catch (err) {
184
+ console.error(`[Tmux] Failed to sync tmux.conf: ${err.message}`)
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Sync bundled rules from the package to ~/.claude/rules/.
190
+ */
191
+ function syncBundledRules() {
192
+ const bundledRulesDir = path.join(PACKAGE_ROOT, 'rules')
193
+ const targetRulesDir = path.join(config.HOME_DIR, '.claude', 'rules')
194
+
195
+ try {
196
+ if (!fs.existsSync(bundledRulesDir)) return
197
+
198
+ fs.mkdirSync(targetRulesDir, { recursive: true })
199
+
200
+ const coreSrc = path.join(bundledRulesDir, 'core.md')
201
+ if (fs.existsSync(coreSrc)) {
202
+ fs.copyFileSync(coreSrc, path.join(targetRulesDir, 'core.md'))
203
+ console.log('[Rules] Synced: core.md')
204
+ }
205
+
206
+ for (const legacy of ['minion.md', 'role-pm.md', 'role-engineer.md']) {
207
+ const legacyPath = path.join(targetRulesDir, legacy)
208
+ if (fs.existsSync(legacyPath)) {
209
+ fs.unlinkSync(legacyPath)
210
+ console.log(`[Rules] Removed legacy: ${legacy}`)
211
+ }
212
+ }
213
+ } catch (err) {
214
+ console.error(`[Rules] Failed to sync bundled rules: ${err.message}`)
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Sync bundled role context files to ~/.minion/roles/.
220
+ */
221
+ function syncBundledRoles() {
222
+ const bundledRolesDir = path.join(PACKAGE_ROOT, 'roles')
223
+ const targetRolesDir = path.join(config.HOME_DIR, '.minion', 'roles')
224
+
225
+ try {
226
+ if (!fs.existsSync(bundledRolesDir)) return
227
+
228
+ fs.mkdirSync(targetRolesDir, { recursive: true })
229
+
230
+ for (const file of fs.readdirSync(bundledRolesDir)) {
231
+ if (!file.endsWith('.md')) continue
232
+ const src = path.join(bundledRolesDir, file)
233
+ const dest = path.join(targetRolesDir, file)
234
+ fs.copyFileSync(src, dest)
235
+ console.log(`[Roles] Synced: ${file}`)
236
+ }
237
+ } catch (err) {
238
+ console.error(`[Roles] Failed to sync bundled roles: ${err.message}`)
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Sync bundled documentation files to ~/.minion/docs/.
244
+ */
245
+ function syncBundledDocs() {
246
+ const bundledDocsDir = path.join(PACKAGE_ROOT, 'docs')
247
+ const targetDocsDir = path.join(config.HOME_DIR, '.minion', 'docs')
248
+
249
+ try {
250
+ if (!fs.existsSync(bundledDocsDir)) return
251
+
252
+ fs.mkdirSync(targetDocsDir, { recursive: true })
253
+
254
+ for (const file of fs.readdirSync(bundledDocsDir)) {
255
+ if (!file.endsWith('.md')) continue
256
+ const src = path.join(bundledDocsDir, file)
257
+ const dest = path.join(targetDocsDir, file)
258
+ fs.copyFileSync(src, dest)
259
+ console.log(`[Docs] Synced: ${file}`)
260
+ }
261
+ } catch (err) {
262
+ console.error(`[Docs] Failed to sync bundled docs: ${err.message}`)
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Register all routes (shared + macOS-specific)
268
+ */
269
+ async function registerAllRoutes(app) {
270
+ // Shared routes (from core/) - inject runners via opts
271
+ await app.register(healthRoutes)
272
+ await app.register(diagnoseRoutes)
273
+ await app.register(skillRoutes, { workflowRunner })
274
+ await app.register(workflowRoutes, { workflowRunner })
275
+ await app.register(routineRoutes, { routineRunner })
276
+ await app.register(authRoutes)
277
+ await app.register(variableRoutes)
278
+ await app.register(memoryRoutes)
279
+ await app.register(dailyLogRoutes)
280
+ await app.register(sudoersRoutes)
281
+ await app.register(permissionRoutes)
282
+ await app.register(threadRoutes)
283
+ await app.register(todoRoutes)
284
+ await app.register(emailRoutes)
285
+ await app.register(daemonRoutes, { heartbeatStatus: () => ({ running: !!heartbeatTimer, last_beat_at: lastBeatAt }) })
286
+ await app.register(llmRoutes)
287
+
288
+ // macOS-specific routes
289
+ await app.register(commandRoutes)
290
+ await app.register(terminalRoutes)
291
+ await app.register(fileRoutes)
292
+ await app.register(directiveRoutes)
293
+ await app.register(chatRoutes)
294
+ await app.register(configRoutes)
295
+ }
296
+
297
+ // Start server
298
+ async function start() {
299
+ try {
300
+ // Extend process.env so all child processes (tmux, spawn) inherit correct environment.
301
+ // This eliminates the need for per-invocation PATH building in runners/chat.
302
+ const { buildExtendedPath } = require('../core/lib/platform')
303
+ process.env.PATH = buildExtendedPath(config.HOME_DIR)
304
+ process.env.HOME = config.HOME_DIR
305
+ // No DISPLAY env var on macOS — native Screen Sharing uses WindowServer,
306
+ // not X11. Setting DISPLAY=:99 (Linux pattern) would break GUI child processes.
307
+
308
+ // Load minion secrets into process.env for child process inheritance.
309
+ // Variables are NOT loaded here — they use {{VAR}} template expansion in skill content.
310
+ const variableStore = require('../core/stores/variable-store')
311
+ const minionEnv = variableStore.buildEnv()
312
+ for (const [key, value] of Object.entries(minionEnv)) {
313
+ if (!(key in process.env)) process.env[key] = value
314
+ }
315
+ console.log(`[Server] Loaded ${Object.keys(minionEnv).length} minion secrets into process.env`)
316
+
317
+ // Sync bundled assets
318
+ syncBundledRules()
319
+ syncBundledRoles()
320
+ syncBundledDocs()
321
+ syncPermissions()
322
+ syncTmuxConfig()
323
+
324
+ // Register all routes
325
+ await registerAllRoutes(fastify)
326
+
327
+ // Listen on all interfaces
328
+ await fastify.listen({ port: config.AGENT_PORT, host: '0.0.0.0' })
329
+
330
+ console.log(`[Server] Minion agent listening on port ${config.AGENT_PORT}`)
331
+
332
+ // Kill any stale ttyd processes from a previous run, then start terminal proxy
333
+ await killStaleTtydProcesses()
334
+ try {
335
+ await startTerminalProxy()
336
+ } catch (err) {
337
+ console.error(`[Server] Terminal proxy failed to start: ${err.message}`)
338
+ console.error('[Server] Terminal WebSocket connections will not work')
339
+ }
340
+
341
+ // Load cached workflows and start workflow runner
342
+ try {
343
+ const cachedWorkflows = await workflowStore.load()
344
+ if (cachedWorkflows.length > 0) {
345
+ console.log(`[Server] Loading ${cachedWorkflows.length} cached workflows`)
346
+ workflowRunner.loadWorkflows(cachedWorkflows)
347
+ }
348
+ } catch (err) {
349
+ console.error('[Server] Failed to load cached workflows:', err.message)
350
+ }
351
+
352
+ // Load cached routines and start routine runner
353
+ try {
354
+ const cachedRoutines = await routineStore.load()
355
+ if (cachedRoutines.length > 0) {
356
+ console.log(`[Server] Loading ${cachedRoutines.length} cached routines`)
357
+ routineRunner.loadRoutines(cachedRoutines)
358
+ }
359
+ } catch (err) {
360
+ console.error('[Server] Failed to load cached routines:', err.message)
361
+ }
362
+
363
+ // Start reflection scheduler (self-reflection time)
364
+ reflectionScheduler.start(runQuickLlmCall)
365
+
366
+ if (isHqConfigured()) {
367
+ console.log(`[Server] HQ URL: ${config.HQ_URL}`)
368
+
369
+ // Send initial online heartbeat
370
+ const { getStatus } = require('../core/routes/health')
371
+ const { currentTask } = getStatus()
372
+ const todoStore = require('../core/stores/todo-store')
373
+ const getTodoSummary = () => { try { return todoStore.getSummary() } catch { return null } }
374
+ const workspaceStore = require('../core/stores/workspace-store')
375
+ const syncWorkspaces = (response) => {
376
+ if (response && Array.isArray(response.workspaces)) {
377
+ workspaceStore.upsertAll(response.workspaces)
378
+ }
379
+ }
380
+
381
+ sendHeartbeat({ status: 'online', current_task: currentTask, running_tasks: runningTasks.getAll(), config_warnings: getConfigWarnings(), todo_summary: getTodoSummary(), version }).then(syncWorkspaces).catch(err => {
382
+ console.error('[Heartbeat] Initial heartbeat failed:', err.message)
383
+ })
384
+
385
+ // Start periodic heartbeat
386
+ heartbeatTimer = setInterval(() => {
387
+ const { currentStatus, currentTask } = getStatus()
388
+ sendHeartbeat({ status: currentStatus, current_task: currentTask, running_tasks: runningTasks.getAll(), config_warnings: getConfigWarnings(), todo_summary: getTodoSummary(), version }).then(response => {
389
+ lastBeatAt = new Date().toISOString()
390
+ syncWorkspaces(response)
391
+ }).catch(err => {
392
+ console.error('[Heartbeat] Periodic heartbeat failed:', err.message)
393
+ })
394
+ }, HEARTBEAT_INTERVAL_MS)
395
+ console.log(`[Heartbeat] Sending every ${HEARTBEAT_INTERVAL_MS / 1000}s`)
396
+
397
+ // Start Pull-model daemons
398
+ stepPoller.start()
399
+ dagStepPoller.start()
400
+ boardTaskPoller.setRunner(boardTaskRunner)
401
+ boardTaskPoller.start()
402
+ revisionWatcher.start()
403
+ threadWatcher.start(runQuickLlmCall)
404
+ } else {
405
+ console.log('[Server] Running in standalone mode (no HQ connection)')
406
+ }
407
+ } catch (err) {
408
+ fastify.log.error(err)
409
+ process.exit(1)
410
+ }
411
+ }
412
+
413
+ start()
@@ -0,0 +1,6 @@
1
+ /**
2
+ * macOS terminal proxy — re-exports the Linux implementation.
3
+ *
4
+ * Pure Node.js http/net code; no platform-specific dependencies.
5
+ */
6
+ module.exports = require('../linux/terminal-proxy')