upfynai-code 2.9.1 → 2.9.2
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 +91 -66
- package/client/dist/api-docs.html +838 -0
- package/client/dist/assets/AppContent-BXZDeSIC.js +545 -0
- package/client/dist/assets/CanvasFullScreen-mnpCnLZ9.js +1 -0
- package/client/dist/assets/CanvasWorkspace-4CqmjAVQ.js +163 -0
- package/client/dist/assets/DashboardPanel-zFIFlw56.js +1 -0
- package/client/dist/assets/FileTree-B0c_GaB3.js +1 -0
- package/client/dist/assets/GitPanel-DUP4zVU4.js +2 -0
- package/client/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/client/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/client/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/client/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/client/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/client/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/client/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/client/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/client/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/client/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/client/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/client/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/client/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/client/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/client/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/client/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/client/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/client/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/client/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/client/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/client/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/client/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/client/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/client/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/client/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/client/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/client/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/client/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/client/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/client/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/client/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/client/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/client/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/client/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/client/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/client/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/client/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/client/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/client/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/client/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/client/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/client/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/client/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/client/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/client/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/client/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/client/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/client/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/client/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/client/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/client/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/client/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/client/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/client/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/client/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/client/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/client/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/client/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/client/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/client/dist/assets/LoginModal-BRycfsyD.js +13 -0
- package/client/dist/assets/MarkdownPreview-DHmk3qzu.js +1 -0
- package/client/dist/assets/MermaidBlock-BuBc_G-F.js +2 -0
- package/client/dist/assets/Onboarding-BcnaZZ0o.js +1 -0
- package/client/dist/assets/PreviewPanel-CqCa92Tf.js +32 -0
- package/client/dist/assets/SetupForm-S0g6u5yT.js +1 -0
- package/client/dist/assets/WorkflowsPanel-CouH9JDO.js +1 -0
- package/client/dist/assets/index-BFuqS0tY.css +1 -0
- package/client/dist/assets/index-CNDcVl2g.js +68 -0
- package/client/dist/assets/pdf-CE_K4jFx.js +12 -0
- package/client/dist/assets/vendor-canvas-BZV40eAE.css +1 -0
- package/client/dist/assets/vendor-canvas-D39yWul6.js +49 -0
- package/client/dist/assets/vendor-codemirror-CbtmxxaB.js +35 -0
- package/client/dist/assets/vendor-diff-DNQpbhrT.js +69 -0
- package/client/dist/assets/vendor-i18n-DCFGyhQR.js +1 -0
- package/client/dist/assets/vendor-icons-BaD0x9SL.js +711 -0
- package/client/dist/assets/vendor-markdown-CimbIo6Y.js +296 -0
- package/client/dist/assets/vendor-mermaid-CH7SGc99.js +2556 -0
- package/client/dist/assets/vendor-react-96lCPsRK.js +67 -0
- package/client/dist/assets/vendor-syntax-DuHI9Ok6.js +16 -0
- package/client/dist/assets/vendor-xterm-CZq1hqo1.js +66 -0
- package/client/dist/assets/vendor-xterm-qxJ8_QYu.css +32 -0
- package/client/dist/clear-cache.html +85 -0
- package/client/dist/convert-icons.md +53 -0
- package/client/dist/favicon.png +0 -0
- package/client/dist/favicon.svg +5 -0
- package/client/dist/generate-icons.js +49 -0
- package/client/dist/icons/claude-ai-icon.svg +1 -0
- package/client/dist/icons/codex-white.svg +3 -0
- package/client/dist/icons/codex.svg +3 -0
- package/client/dist/icons/cursor-white.svg +12 -0
- package/client/dist/icons/cursor.svg +1 -0
- package/client/dist/icons/icon-128x128.png +0 -0
- package/client/dist/icons/icon-128x128.svg +5 -0
- package/client/dist/icons/icon-144x144.png +0 -0
- package/client/dist/icons/icon-144x144.svg +5 -0
- package/client/dist/icons/icon-152x152.png +0 -0
- package/client/dist/icons/icon-152x152.svg +5 -0
- package/client/dist/icons/icon-192x192.png +0 -0
- package/client/dist/icons/icon-192x192.svg +5 -0
- package/client/dist/icons/icon-384x384.png +0 -0
- package/client/dist/icons/icon-384x384.svg +5 -0
- package/client/dist/icons/icon-512x512.png +0 -0
- package/client/dist/icons/icon-512x512.svg +5 -0
- package/client/dist/icons/icon-72x72.png +0 -0
- package/client/dist/icons/icon-72x72.svg +5 -0
- package/client/dist/icons/icon-96x96.png +0 -0
- package/client/dist/icons/icon-96x96.svg +5 -0
- package/client/dist/icons/icon-template.svg +5 -0
- package/client/dist/index.html +119 -0
- package/client/dist/logo-128.png +0 -0
- package/client/dist/logo-256.png +0 -0
- package/client/dist/logo-32.png +0 -0
- package/client/dist/logo-512.png +0 -0
- package/client/dist/logo-64.png +0 -0
- package/client/dist/logo.svg +14 -0
- package/client/dist/manifest.json +61 -0
- package/client/dist/mcp-docs.html +108 -0
- package/client/dist/offline.html +84 -0
- package/client/dist/screenshots/cli-selection.png +0 -0
- package/client/dist/screenshots/desktop-main.png +0 -0
- package/client/dist/screenshots/mobile-chat.png +0 -0
- package/client/dist/screenshots/tools-modal.png +0 -0
- package/client/dist/sw.js +82 -0
- package/commands/upfynai-connect.md +59 -0
- package/commands/upfynai-disconnect.md +31 -0
- package/commands/upfynai-doctor.md +99 -0
- package/commands/upfynai-export.md +49 -0
- package/commands/upfynai-local.md +82 -0
- package/commands/upfynai-status.md +75 -0
- package/commands/upfynai-stop.md +49 -0
- package/commands/upfynai-uninstall.md +58 -0
- package/commands/upfynai.md +69 -0
- package/package.json +143 -82
- package/scripts/build-client.js +17 -0
- package/scripts/fix-node-pty.js +67 -0
- package/scripts/install-commands.js +78 -0
- package/server/agent-loop.js +242 -0
- package/server/auto-compact.js +99 -0
- package/server/claude-sdk.js +797 -0
- package/server/cli-ui.js +785 -0
- package/server/cli.js +596 -0
- package/server/constants/config.js +31 -0
- package/server/cursor-cli.js +270 -0
- package/server/database/auth.db +0 -0
- package/server/database/db.js +1391 -0
- package/server/database/init.sql +70 -0
- package/server/index.js +3799 -0
- package/server/load-env.js +26 -0
- package/server/mcp-server.js +621 -0
- package/server/middleware/auth.js +176 -0
- package/server/middleware/relayHelpers.js +44 -0
- package/server/middleware/sandboxRouter.js +174 -0
- package/server/openai-codex.js +403 -0
- package/server/openrouter.js +137 -0
- package/server/projects.js +1807 -0
- package/server/provider-factory.js +174 -0
- package/server/relay-client.js +379 -0
- package/server/routes/agent.js +1226 -0
- package/server/routes/auth.js +554 -0
- package/server/routes/canvas.js +53 -0
- package/server/routes/cli-auth.js +263 -0
- package/server/routes/codex.js +396 -0
- package/server/routes/commands.js +707 -0
- package/server/routes/composio.js +176 -0
- package/server/routes/cursor.js +770 -0
- package/server/routes/dashboard.js +295 -0
- package/server/routes/git.js +1208 -0
- package/server/routes/keys.js +34 -0
- package/server/routes/mcp-utils.js +48 -0
- package/server/routes/mcp.js +661 -0
- package/server/routes/payments.js +227 -0
- package/server/routes/projects.js +655 -0
- package/server/routes/sessions.js +146 -0
- package/server/routes/settings.js +261 -0
- package/server/routes/taskmaster.js +1928 -0
- package/server/routes/user.js +106 -0
- package/server/routes/vapi-chat.js +624 -0
- package/server/routes/voice.js +235 -0
- package/server/routes/webhooks.js +166 -0
- package/server/routes/workflows.js +312 -0
- package/server/sandbox.js +120 -0
- package/server/services/composio.js +204 -0
- package/server/services/sessionRegistry.js +139 -0
- package/server/services/whisperService.js +84 -0
- package/server/services/workflowScheduler.js +206 -0
- package/server/tests/relay-flow.test.js +570 -0
- package/server/tests/sessions.test.js +259 -0
- package/server/utils/commandParser.js +303 -0
- package/server/utils/email.js +61 -0
- package/server/utils/gitConfig.js +24 -0
- package/server/utils/mcp-detector.js +198 -0
- package/server/utils/taskmaster-websocket.js +129 -0
- package/shared/integrationCatalog.d.ts +12 -0
- package/shared/integrationCatalog.js +172 -0
- package/shared/modelConstants.js +96 -0
- package/bin/cli.js +0 -97
- package/dist/agents/claude.js +0 -229
- package/dist/agents/codex.js +0 -48
- package/dist/agents/cursor.js +0 -48
- package/dist/agents/detect.js +0 -51
- package/dist/agents/exec.js +0 -31
- package/dist/agents/files.js +0 -105
- package/dist/agents/git.js +0 -18
- package/dist/agents/gitagent.js +0 -67
- package/dist/agents/index.js +0 -88
- package/dist/agents/shell.js +0 -38
- package/dist/agents/utils.js +0 -136
- package/scripts/postinstall.js +0 -9
- package/scripts/prepublish.js +0 -58
- package/src/animation.js +0 -228
- package/src/auth.js +0 -122
- package/src/config.js +0 -40
- package/src/connect.js +0 -416
- package/src/launch.js +0 -78
- package/src/mcp.js +0 -57
- package/src/permissions.js +0 -140
- package/src/persistent-shell.js +0 -261
- package/src/server.js +0 -54
- /package/{dist → shared}/gitagent/index.js +0 -0
- /package/{dist → shared}/gitagent/parser.js +0 -0
- /package/{dist → shared}/gitagent/prompt-builder.js +0 -0
package/src/permissions.js
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Permission-Gated Tool Execution
|
|
3
|
-
* Ported from opencode-ai/opencode internal/permission/permission.go
|
|
4
|
-
*
|
|
5
|
-
* Splits relay actions into safe (auto-approve) and dangerous (require browser approval).
|
|
6
|
-
* Permission requests are sent to the server → browser, and we wait for the response.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
// Safe actions — auto-approved, read-only (opencode pattern: tools without permission flag)
|
|
10
|
-
const SAFE_ACTIONS = new Set([
|
|
11
|
-
'file-read',
|
|
12
|
-
'file-tree',
|
|
13
|
-
'browse-dirs',
|
|
14
|
-
'validate-path',
|
|
15
|
-
'detect-agents',
|
|
16
|
-
]);
|
|
17
|
-
|
|
18
|
-
// Dangerous actions — require user approval (opencode pattern: tools with permission flag)
|
|
19
|
-
const DANGEROUS_ACTIONS = new Set([
|
|
20
|
-
'shell-command',
|
|
21
|
-
'file-write',
|
|
22
|
-
'create-folder',
|
|
23
|
-
'git-operation',
|
|
24
|
-
]);
|
|
25
|
-
|
|
26
|
-
// Safe shell command prefixes — auto-approved even within shell-command
|
|
27
|
-
// (opencode pattern: safe bash commands bypass permission)
|
|
28
|
-
const SAFE_SHELL_PREFIXES = [
|
|
29
|
-
'ls', 'dir', 'pwd', 'echo', 'cat', 'head', 'tail', 'wc',
|
|
30
|
-
'git status', 'git log', 'git diff', 'git branch', 'git remote',
|
|
31
|
-
'node --version', 'npm --version', 'python --version',
|
|
32
|
-
'which', 'where', 'type', 'whoami', 'hostname',
|
|
33
|
-
'date', 'uptime', 'df', 'free',
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
// Pending permission requests: requestId → { resolve }
|
|
37
|
-
const pendingPermissions = new Map();
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Check if an action needs user permission.
|
|
41
|
-
* @param {string} action - Relay action type
|
|
42
|
-
* @param {object} payload - Action payload
|
|
43
|
-
* @returns {boolean}
|
|
44
|
-
*/
|
|
45
|
-
export function needsPermission(action, payload) {
|
|
46
|
-
if (SAFE_ACTIONS.has(action)) return false;
|
|
47
|
-
if (!DANGEROUS_ACTIONS.has(action)) return false; // unknown actions handled elsewhere
|
|
48
|
-
|
|
49
|
-
// Shell commands: check if it's a safe read-only command
|
|
50
|
-
if (action === 'shell-command' && payload?.command) {
|
|
51
|
-
const cmd = payload.command.trim().toLowerCase();
|
|
52
|
-
if (SAFE_SHELL_PREFIXES.some(prefix => cmd.startsWith(prefix))) {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Request permission from the browser via the relay WebSocket.
|
|
62
|
-
* Blocks until the user approves or denies.
|
|
63
|
-
*
|
|
64
|
-
* @param {WebSocket} ws - Relay WebSocket connection
|
|
65
|
-
* @param {string} requestId - Relay request ID
|
|
66
|
-
* @param {string} action - Action being requested
|
|
67
|
-
* @param {object} payload - Action payload
|
|
68
|
-
* @param {number} timeoutMs - Timeout for waiting (default 60s)
|
|
69
|
-
* @returns {Promise<boolean>} - true if approved, false if denied
|
|
70
|
-
*/
|
|
71
|
-
export function requestPermission(ws, requestId, action, payload, timeoutMs = 60000) {
|
|
72
|
-
return new Promise((resolve) => {
|
|
73
|
-
const permId = `perm-${requestId}`;
|
|
74
|
-
|
|
75
|
-
const timeout = setTimeout(() => {
|
|
76
|
-
pendingPermissions.delete(permId);
|
|
77
|
-
resolve(false); // Timeout = deny
|
|
78
|
-
}, timeoutMs);
|
|
79
|
-
|
|
80
|
-
pendingPermissions.set(permId, {
|
|
81
|
-
resolve: (approved) => {
|
|
82
|
-
clearTimeout(timeout);
|
|
83
|
-
pendingPermissions.delete(permId);
|
|
84
|
-
resolve(approved);
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// Send permission request to server → browser
|
|
89
|
-
ws.send(JSON.stringify({
|
|
90
|
-
type: 'relay-permission-request',
|
|
91
|
-
requestId,
|
|
92
|
-
permissionId: permId,
|
|
93
|
-
action,
|
|
94
|
-
description: describeAction(action, payload),
|
|
95
|
-
payload: sanitizePayload(action, payload),
|
|
96
|
-
}));
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Handle a permission response from the server.
|
|
102
|
-
* @param {object} data - { permissionId, approved }
|
|
103
|
-
*/
|
|
104
|
-
export function handlePermissionResponse(data) {
|
|
105
|
-
const pending = pendingPermissions.get(data.permissionId);
|
|
106
|
-
if (pending) {
|
|
107
|
-
pending.resolve(Boolean(data.approved));
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Generate a human-readable description of the action.
|
|
113
|
-
*/
|
|
114
|
-
function describeAction(action, payload) {
|
|
115
|
-
switch (action) {
|
|
116
|
-
case 'shell-command':
|
|
117
|
-
return `Execute command: ${payload?.command || '(unknown)'}`;
|
|
118
|
-
case 'file-write':
|
|
119
|
-
return `Write to file: ${payload?.filePath || '(unknown)'}`;
|
|
120
|
-
case 'create-folder':
|
|
121
|
-
return `Create folder: ${payload?.folderPath || '(unknown)'}`;
|
|
122
|
-
case 'git-operation':
|
|
123
|
-
return `Run git: ${payload?.gitCommand || '(unknown)'}`;
|
|
124
|
-
default:
|
|
125
|
-
return `${action}: ${JSON.stringify(payload || {}).slice(0, 200)}`;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Sanitize payload for display — remove large content fields.
|
|
131
|
-
*/
|
|
132
|
-
function sanitizePayload(action, payload) {
|
|
133
|
-
if (!payload) return {};
|
|
134
|
-
const safe = { ...payload };
|
|
135
|
-
// Don't send file content to browser for permission display
|
|
136
|
-
if (safe.content && safe.content.length > 500) {
|
|
137
|
-
safe.content = safe.content.slice(0, 500) + `... (${safe.content.length} chars total)`;
|
|
138
|
-
}
|
|
139
|
-
return safe;
|
|
140
|
-
}
|
package/src/persistent-shell.js
DELETED
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Persistent Shell Singleton
|
|
3
|
-
* Ported from opencode-ai/opencode internal/llm/tools/shell/shell.go
|
|
4
|
-
*
|
|
5
|
-
* Maintains a single long-running shell process across commands.
|
|
6
|
-
* - Commands are queued and executed sequentially
|
|
7
|
-
* - Environment and cwd persist between commands
|
|
8
|
-
* - Child processes can be killed on abort
|
|
9
|
-
* - Shell respawns automatically if it dies
|
|
10
|
-
*/
|
|
11
|
-
import { spawn, execSync } from 'child_process';
|
|
12
|
-
import os from 'os';
|
|
13
|
-
import path from 'path';
|
|
14
|
-
import { promises as fs } from 'fs';
|
|
15
|
-
|
|
16
|
-
let shellInstance = null;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Get or create the persistent shell singleton.
|
|
20
|
-
* @param {string} workingDir - Initial working directory
|
|
21
|
-
* @returns {PersistentShell}
|
|
22
|
-
*/
|
|
23
|
-
export function getPersistentShell(workingDir) {
|
|
24
|
-
if (!shellInstance || !shellInstance.isAlive) {
|
|
25
|
-
shellInstance = new PersistentShell(workingDir || os.homedir());
|
|
26
|
-
}
|
|
27
|
-
return shellInstance;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
class PersistentShell {
|
|
31
|
-
constructor(cwd) {
|
|
32
|
-
this.cwd = cwd;
|
|
33
|
-
this.isAlive = false;
|
|
34
|
-
this.commandQueue = [];
|
|
35
|
-
this.processing = false;
|
|
36
|
-
this.proc = null;
|
|
37
|
-
this.stdin = null;
|
|
38
|
-
this._start();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
_start() {
|
|
42
|
-
const isWin = process.platform === 'win32';
|
|
43
|
-
const shellPath = isWin
|
|
44
|
-
? process.env.COMSPEC || 'cmd.exe'
|
|
45
|
-
: process.env.SHELL || '/bin/bash';
|
|
46
|
-
const shellArgs = isWin ? ['/Q'] : ['-l'];
|
|
47
|
-
|
|
48
|
-
this.proc = spawn(shellPath, shellArgs, {
|
|
49
|
-
cwd: this.cwd,
|
|
50
|
-
env: { ...process.env, GIT_EDITOR: 'true' },
|
|
51
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
52
|
-
shell: false,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
this.stdin = this.proc.stdin;
|
|
56
|
-
this.isAlive = true;
|
|
57
|
-
|
|
58
|
-
this.proc.on('close', () => {
|
|
59
|
-
this.isAlive = false;
|
|
60
|
-
// Reject any queued commands
|
|
61
|
-
for (const queued of this.commandQueue) {
|
|
62
|
-
queued.reject(new Error('Shell process died'));
|
|
63
|
-
}
|
|
64
|
-
this.commandQueue = [];
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
this.proc.on('error', () => {
|
|
68
|
-
this.isAlive = false;
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Execute a command in the persistent shell.
|
|
74
|
-
* Queued and executed sequentially (opencode pattern: commandQueue channel).
|
|
75
|
-
*
|
|
76
|
-
* @param {string} command - Shell command to execute
|
|
77
|
-
* @param {object} opts - { timeoutMs, abortSignal }
|
|
78
|
-
* @returns {Promise<{ stdout, stderr, exitCode, interrupted, cwd }>}
|
|
79
|
-
*/
|
|
80
|
-
exec(command, opts = {}) {
|
|
81
|
-
return new Promise((resolve, reject) => {
|
|
82
|
-
this.commandQueue.push({ command, opts, resolve, reject });
|
|
83
|
-
this._processNext();
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async _processNext() {
|
|
88
|
-
if (this.processing || this.commandQueue.length === 0) return;
|
|
89
|
-
this.processing = true;
|
|
90
|
-
|
|
91
|
-
const { command, opts, resolve, reject } = this.commandQueue.shift();
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
const result = await this._execCommand(command, opts);
|
|
95
|
-
resolve(result);
|
|
96
|
-
} catch (err) {
|
|
97
|
-
reject(err);
|
|
98
|
-
} finally {
|
|
99
|
-
this.processing = false;
|
|
100
|
-
// Process next in queue
|
|
101
|
-
if (this.commandQueue.length > 0) {
|
|
102
|
-
this._processNext();
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async _execCommand(command, { timeoutMs = 60000, abortSignal } = {}) {
|
|
108
|
-
if (!this.isAlive) {
|
|
109
|
-
throw new Error('Shell is not alive');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const isWin = process.platform === 'win32';
|
|
113
|
-
const tmpDir = os.tmpdir();
|
|
114
|
-
const ts = Date.now() + '-' + Math.random().toString(36).slice(2, 8);
|
|
115
|
-
const stdoutFile = path.join(tmpDir, `uc-stdout-${ts}`);
|
|
116
|
-
const stderrFile = path.join(tmpDir, `uc-stderr-${ts}`);
|
|
117
|
-
const statusFile = path.join(tmpDir, `uc-status-${ts}`);
|
|
118
|
-
const cwdFile = path.join(tmpDir, `uc-cwd-${ts}`);
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
// Build the shell command that redirects output to temp files
|
|
122
|
-
// Exact same pattern as opencode's shell.go execCommand
|
|
123
|
-
let fullCommand;
|
|
124
|
-
if (isWin) {
|
|
125
|
-
// Windows cmd.exe variant
|
|
126
|
-
fullCommand = [
|
|
127
|
-
`${command} > "${stdoutFile}" 2> "${stderrFile}"`,
|
|
128
|
-
`echo %ERRORLEVEL% > "${statusFile}"`,
|
|
129
|
-
`cd > "${cwdFile}"`,
|
|
130
|
-
].join(' & ');
|
|
131
|
-
} else {
|
|
132
|
-
// Unix bash variant (identical to opencode)
|
|
133
|
-
const escaped = command.replace(/'/g, "'\\''");
|
|
134
|
-
fullCommand = [
|
|
135
|
-
`eval '${escaped}' < /dev/null > '${stdoutFile}' 2> '${stderrFile}'`,
|
|
136
|
-
`EXEC_EXIT_CODE=$?`,
|
|
137
|
-
`pwd > '${cwdFile}'`,
|
|
138
|
-
`echo $EXEC_EXIT_CODE > '${statusFile}'`,
|
|
139
|
-
].join('\n');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
this.stdin.write(fullCommand + '\n');
|
|
143
|
-
|
|
144
|
-
// Poll for status file (same polling pattern as opencode)
|
|
145
|
-
let interrupted = false;
|
|
146
|
-
const startTime = Date.now();
|
|
147
|
-
|
|
148
|
-
await new Promise((done, fail) => {
|
|
149
|
-
const poll = setInterval(async () => {
|
|
150
|
-
// Check abort
|
|
151
|
-
if (abortSignal?.aborted) {
|
|
152
|
-
this.killChildren();
|
|
153
|
-
interrupted = true;
|
|
154
|
-
clearInterval(poll);
|
|
155
|
-
done();
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Check timeout
|
|
160
|
-
if (timeoutMs > 0 && Date.now() - startTime > timeoutMs) {
|
|
161
|
-
this.killChildren();
|
|
162
|
-
interrupted = true;
|
|
163
|
-
clearInterval(poll);
|
|
164
|
-
done();
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Check if status file exists and has content
|
|
169
|
-
try {
|
|
170
|
-
const stat = await fs.stat(statusFile);
|
|
171
|
-
if (stat.size > 0) {
|
|
172
|
-
clearInterval(poll);
|
|
173
|
-
done();
|
|
174
|
-
}
|
|
175
|
-
} catch {
|
|
176
|
-
// File doesn't exist yet
|
|
177
|
-
}
|
|
178
|
-
}, 50);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// Read results from temp files
|
|
182
|
-
const stdout = await this._readFileOrEmpty(stdoutFile);
|
|
183
|
-
const stderr = await this._readFileOrEmpty(stderrFile);
|
|
184
|
-
const exitCodeStr = await this._readFileOrEmpty(statusFile);
|
|
185
|
-
const newCwd = await this._readFileOrEmpty(cwdFile);
|
|
186
|
-
|
|
187
|
-
let exitCode = 0;
|
|
188
|
-
if (exitCodeStr.trim()) {
|
|
189
|
-
exitCode = parseInt(exitCodeStr.trim(), 10) || 0;
|
|
190
|
-
} else if (interrupted) {
|
|
191
|
-
exitCode = 143;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (newCwd.trim()) {
|
|
195
|
-
this.cwd = newCwd.trim();
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return {
|
|
199
|
-
stdout: stdout,
|
|
200
|
-
stderr: interrupted ? stderr + '\nCommand execution timed out or was interrupted' : stderr,
|
|
201
|
-
exitCode,
|
|
202
|
-
interrupted,
|
|
203
|
-
cwd: this.cwd,
|
|
204
|
-
};
|
|
205
|
-
} finally {
|
|
206
|
-
// Cleanup temp files
|
|
207
|
-
await Promise.all([
|
|
208
|
-
fs.unlink(stdoutFile).catch(() => {}),
|
|
209
|
-
fs.unlink(stderrFile).catch(() => {}),
|
|
210
|
-
fs.unlink(statusFile).catch(() => {}),
|
|
211
|
-
fs.unlink(cwdFile).catch(() => {}),
|
|
212
|
-
]);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Kill child processes of the shell (opencode pattern: killChildren).
|
|
218
|
-
* On Unix: pgrep -P <pid> → SIGTERM each child.
|
|
219
|
-
* On Windows: taskkill /PID /T.
|
|
220
|
-
*/
|
|
221
|
-
killChildren() {
|
|
222
|
-
if (!this.proc?.pid) return;
|
|
223
|
-
try {
|
|
224
|
-
if (process.platform === 'win32') {
|
|
225
|
-
execSync(`taskkill /PID ${this.proc.pid} /T /F`, { stdio: 'ignore', timeout: 5000 });
|
|
226
|
-
// Respawn since taskkill kills the parent too on Windows
|
|
227
|
-
this._start();
|
|
228
|
-
} else {
|
|
229
|
-
const output = execSync(`pgrep -P ${this.proc.pid}`, { encoding: 'utf8', timeout: 5000 });
|
|
230
|
-
for (const line of output.split('\n')) {
|
|
231
|
-
const pid = parseInt(line.trim(), 10);
|
|
232
|
-
if (pid > 0) {
|
|
233
|
-
try { process.kill(pid, 'SIGTERM'); } catch { /* already dead */ }
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
} catch {
|
|
238
|
-
// pgrep/taskkill failed — no children to kill
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Close the persistent shell.
|
|
244
|
-
*/
|
|
245
|
-
close() {
|
|
246
|
-
if (!this.isAlive) return;
|
|
247
|
-
try {
|
|
248
|
-
this.stdin.write('exit\n');
|
|
249
|
-
this.proc.kill('SIGTERM');
|
|
250
|
-
} catch { /* ignore */ }
|
|
251
|
-
this.isAlive = false;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async _readFileOrEmpty(filePath) {
|
|
255
|
-
try {
|
|
256
|
-
return await fs.readFile(filePath, 'utf8');
|
|
257
|
-
} catch {
|
|
258
|
-
return '';
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
package/src/server.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath } from 'url';
|
|
2
|
-
import { dirname, join } from 'path';
|
|
3
|
-
import { existsSync } from 'fs';
|
|
4
|
-
import express from 'express';
|
|
5
|
-
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
6
|
-
import open from 'open';
|
|
7
|
-
import chalk from 'chalk';
|
|
8
|
-
|
|
9
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const DIST_DIR = join(__dirname, '..', 'dist');
|
|
11
|
-
|
|
12
|
-
export async function startServer(port, serverUrl, token) {
|
|
13
|
-
if (!existsSync(DIST_DIR)) {
|
|
14
|
-
console.log(chalk.red('\n Error: No built frontend found at dist/.'));
|
|
15
|
-
console.log(chalk.dim(' Run the build-frontend script first, or use the default mode (no --local flag).\n'));
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const app = express();
|
|
20
|
-
|
|
21
|
-
// Proxy API calls to the remote Vercel backend
|
|
22
|
-
app.use('/api', createProxyMiddleware({
|
|
23
|
-
target: serverUrl,
|
|
24
|
-
changeOrigin: true,
|
|
25
|
-
headers: {
|
|
26
|
-
Authorization: `Bearer ${token}`,
|
|
27
|
-
},
|
|
28
|
-
}));
|
|
29
|
-
|
|
30
|
-
// Serve the built React frontend
|
|
31
|
-
app.use(express.static(DIST_DIR));
|
|
32
|
-
|
|
33
|
-
// SPA fallback — serve index.html for all non-API, non-static routes
|
|
34
|
-
app.get('*', (req, res) => {
|
|
35
|
-
res.sendFile(join(DIST_DIR, 'index.html'));
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
return new Promise((resolve) => {
|
|
39
|
-
const server = app.listen(port, async () => {
|
|
40
|
-
const url = `http://localhost:${port}?token=${encodeURIComponent(token)}`;
|
|
41
|
-
console.log(chalk.green(`\n Local server running at ${chalk.bold(`http://localhost:${port}`)}`));
|
|
42
|
-
console.log(chalk.dim(' API proxy active. Press Ctrl+C to stop.\n'));
|
|
43
|
-
await open(url);
|
|
44
|
-
resolve(server);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const shutdown = () => {
|
|
48
|
-
console.log(chalk.dim('\n Shutting down...'));
|
|
49
|
-
server.close(() => process.exit(0));
|
|
50
|
-
};
|
|
51
|
-
process.on('SIGINT', shutdown);
|
|
52
|
-
process.on('SIGTERM', shutdown);
|
|
53
|
-
});
|
|
54
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|