synabun 2.0.5 → 2.0.7
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/CHANGELOG.md +40 -0
- package/README.md +2 -1
- package/THIRD-PARTY-LICENSES.md +13 -0
- package/docs/whatsapp.md +151 -16
- package/hooks/claude-code/shared.mjs +6 -3
- package/lib/child-path.js +303 -0
- package/lib/claude-hooks.js +2 -1
- package/lib/data-home-migration.js +2 -0
- package/lib/desktop-app.js +265 -0
- package/lib/external-tools.js +7 -3
- package/lib/log-redact.js +102 -0
- package/lib/packaged-runtime.js +138 -11
- package/mcp-server/dist/http.d.ts +3 -1
- package/mcp-server/dist/http.d.ts.map +1 -1
- package/mcp-server/dist/http.js +15 -4
- package/mcp-server/dist/http.js.map +1 -1
- package/mcp-server/dist/index.d.ts.map +1 -1
- package/mcp-server/dist/index.js +9 -6
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
- package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/budget-judgments.js +153 -0
- package/mcp-server/dist/services/budget-judgments.js.map +1 -0
- package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.js +41 -4
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
- package/mcp-server/dist/services/identity.d.ts +11 -0
- package/mcp-server/dist/services/identity.d.ts.map +1 -1
- package/mcp-server/dist/services/identity.js +24 -0
- package/mcp-server/dist/services/identity.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +13 -0
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js +34 -2
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
- package/mcp-server/dist/services/typesafe-config.js +4 -1
- package/mcp-server/dist/services/typesafe-config.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +10 -7
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +34 -13
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
- package/mcp-server/dist/tui.js +2 -1
- package/mcp-server/dist/tui.js.map +1 -1
- package/mcp-server/dist/version.d.ts +5 -0
- package/mcp-server/dist/version.d.ts.map +1 -0
- package/mcp-server/dist/version.js +21 -0
- package/mcp-server/dist/version.js.map +1 -0
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/run.mjs +15 -9
- package/mcp-server/src/http.ts +18 -3
- package/mcp-server/src/index.ts +9 -6
- package/mcp-server/src/services/budget-judgments.ts +183 -0
- package/mcp-server/src/services/codex-config-heal.ts +35 -4
- package/mcp-server/src/services/identity.ts +27 -0
- package/mcp-server/src/services/neural-interface.ts +34 -2
- package/mcp-server/src/services/typesafe-config.ts +4 -1
- package/mcp-server/src/tools/agents.ts +33 -14
- package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
- package/mcp-server/src/tui.ts +2 -1
- package/mcp-server/src/version.ts +22 -0
- package/neural-interface/i18n/en.json +627 -57
- package/neural-interface/i18n/pt-BR.json +626 -56
- package/neural-interface/lib/account-usage.js +163 -0
- package/neural-interface/lib/assistant-api.js +255 -52
- package/neural-interface/lib/assistant-brains/claude.js +15 -2
- package/neural-interface/lib/assistant-brains/codex.js +3 -0
- package/neural-interface/lib/assistant-brains/opencode.js +17 -4
- package/neural-interface/lib/assistant-budget-planner.js +390 -0
- package/neural-interface/lib/assistant-budget-split.js +408 -0
- package/neural-interface/lib/assistant-budget.js +94 -95
- package/neural-interface/lib/assistant-config.js +62 -14
- package/neural-interface/lib/assistant-dispatch.js +432 -125
- package/neural-interface/lib/assistant-jev.js +26 -3
- package/neural-interface/lib/assistant-persona.js +68 -11
- package/neural-interface/lib/assistant-plan-permissions.js +8 -1
- package/neural-interface/lib/assistant-route-accounts.js +14 -0
- package/neural-interface/lib/assistant-route-gate.js +19 -3
- package/neural-interface/lib/assistant-router.js +373 -42
- package/neural-interface/lib/assistant-runtime.js +307 -47
- package/neural-interface/lib/assistant-user-writer.js +4 -2
- package/neural-interface/lib/augmented-path.js +317 -53
- package/neural-interface/lib/claude-accounts.js +342 -31
- package/neural-interface/lib/claude-agent-bridge.js +90 -3
- package/neural-interface/lib/claude-executable.js +6 -1
- package/neural-interface/lib/claude-model-catalog.js +3 -3
- package/neural-interface/lib/claude-panel-auth.js +221 -0
- package/neural-interface/lib/claude-profile-api-key.js +62 -0
- package/neural-interface/lib/cli-install-api.js +137 -0
- package/neural-interface/lib/cli-install.js +797 -0
- package/neural-interface/lib/cli-path.js +130 -0
- package/neural-interface/lib/cli-resolver.js +744 -0
- package/neural-interface/lib/cli-update-planner.js +143 -7
- package/neural-interface/lib/codex-account-auth.js +178 -0
- package/neural-interface/lib/codex-accounts.js +489 -0
- package/neural-interface/lib/codex-capabilities.js +2 -1
- package/neural-interface/lib/codex-compat-rpc.js +8 -2
- package/neural-interface/lib/codex-mcp-approvals.js +5 -1
- package/neural-interface/lib/codex-panel-auth.js +62 -0
- package/neural-interface/lib/codex-session-settings.js +1 -1
- package/neural-interface/lib/codex-settings-schema.js +120 -0
- package/neural-interface/lib/desktop/config.js +3 -0
- package/neural-interface/lib/git-bash/bzip2.js +339 -0
- package/neural-interface/lib/git-bash/claude-settings.js +73 -0
- package/neural-interface/lib/git-bash/extract-worker.js +34 -0
- package/neural-interface/lib/git-bash/install.js +529 -0
- package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
- package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
- package/neural-interface/lib/git-bash/manager.js +268 -0
- package/neural-interface/lib/git-bash/pins.js +206 -0
- package/neural-interface/lib/git-bash/pins.json +34 -0
- package/neural-interface/lib/git-bash/resolver.js +194 -0
- package/neural-interface/lib/git-read.js +34 -2
- package/neural-interface/lib/install-details.js +145 -0
- package/neural-interface/lib/mcp-installer.js +14 -2
- package/neural-interface/lib/native-loop-providers.js +16 -3
- package/neural-interface/lib/native-loop-runtime.js +9 -4
- package/neural-interface/lib/os-path.js +319 -0
- package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
- package/neural-interface/lib/provider-panel-terminal-auth.js +95 -0
- package/neural-interface/lib/pty-host/core.js +2 -1
- package/neural-interface/lib/pty-host/manager.js +27 -2
- package/neural-interface/lib/remote-policy.js +20 -11
- package/neural-interface/lib/session-title-generator.js +4 -3
- package/neural-interface/lib/system-about.js +340 -0
- package/neural-interface/lib/system-activity.js +66 -0
- package/neural-interface/lib/terminal-env.js +11 -3
- package/neural-interface/lib/terminal-path.js +55 -0
- package/neural-interface/lib/virtual-socket.js +12 -1
- package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
- package/neural-interface/lib/whatsapp/bridge.js +78 -34
- package/neural-interface/lib/whatsapp/commands.js +13 -14
- package/neural-interface/lib/whatsapp/config.js +134 -20
- package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
- package/neural-interface/lib/whatsapp/host-core.js +168 -35
- package/neural-interface/lib/whatsapp/inbound.js +5 -3
- package/neural-interface/lib/whatsapp/local-file.js +524 -0
- package/neural-interface/lib/whatsapp/manager.js +92 -32
- package/neural-interface/lib/whatsapp/paths.js +11 -1
- package/neural-interface/lib/whatsapp/protocol.js +65 -5
- package/neural-interface/lib/whatsapp/redact.js +63 -0
- package/neural-interface/lib/whatsapp/service.js +343 -91
- package/neural-interface/lib/whatsapp/transcription.js +463 -0
- package/neural-interface/package-lock.json +2 -2
- package/neural-interface/package.json +1 -1
- package/neural-interface/public/index.html +2 -0
- package/neural-interface/public/index2d.html +2 -0
- package/neural-interface/public/offline.html +16 -1
- package/neural-interface/public/onboarding.html +96 -2
- package/neural-interface/public/shared/account-i18n.js +7 -0
- package/neural-interface/public/shared/account-menu.js +539 -0
- package/neural-interface/public/shared/account-usage.js +19 -0
- package/neural-interface/public/shared/account-view.js +335 -0
- package/neural-interface/public/shared/agent-runtime-options.js +28 -8
- package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
- package/neural-interface/public/shared/assistant/asst-api.js +32 -2
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +124 -25
- package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
- package/neural-interface/public/shared/assistant/asst-mascot.js +126 -7
- package/neural-interface/public/shared/assistant/asst-menu.js +15 -3
- package/neural-interface/public/shared/assistant/asst-panel.js +48 -14
- package/neural-interface/public/shared/assistant/asst-render.js +38 -7
- package/neural-interface/public/shared/assistant/asst-route.js +72 -14
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +45 -6
- package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
- package/neural-interface/public/shared/assistant/asst-state.js +1 -1
- package/neural-interface/public/shared/assistant/asst-styles.js +99 -17
- package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
- package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
- package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
- package/neural-interface/public/shared/cdx/cdx-panel.js +48 -107
- package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +128 -107
- package/neural-interface/public/shared/cdx/cdx-tabs.js +261 -664
- package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
- package/neural-interface/public/shared/claude-account-view.js +3 -0
- package/neural-interface/public/shared/cli-install-view.js +392 -0
- package/neural-interface/public/shared/cli-status.js +282 -22
- package/neural-interface/public/shared/cp/cp-events.js +2 -1
- package/neural-interface/public/shared/cp/cp-styles.js +46 -0
- package/neural-interface/public/shared/desktop-bridge.js +39 -0
- package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
- package/neural-interface/public/shared/desktop-titlebar.js +153 -0
- package/neural-interface/public/shared/desktop-update-view.js +462 -0
- package/neural-interface/public/shared/editor/editor-panel.js +1 -1
- package/neural-interface/public/shared/explorer/fx-main.js +1 -1
- package/neural-interface/public/shared/git-bash-view.js +145 -0
- package/neural-interface/public/shared/html-shell.js +1 -0
- package/neural-interface/public/shared/i18n.js +19 -1
- package/neural-interface/public/shared/install-details-view.js +86 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +32 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +17 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +118 -0
- package/neural-interface/public/shared/pwa-install-page.js +3 -1
- package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
- package/neural-interface/public/shared/settings/settings-about.js +254 -0
- package/neural-interface/public/shared/settings/settings-ia.js +15 -0
- package/neural-interface/public/shared/showcase-privacy.js +489 -0
- package/neural-interface/public/shared/storage.js +30 -3
- package/neural-interface/public/shared/styles.css +278 -31
- package/neural-interface/public/shared/synabun-mascot.js +48 -2
- package/neural-interface/public/shared/ui-claude-panel.js +212 -213
- package/neural-interface/public/shared/ui-cli-banner.js +355 -0
- package/neural-interface/public/shared/ui-git-bash.js +124 -0
- package/neural-interface/public/shared/ui-judgments.js +10 -1
- package/neural-interface/public/shared/ui-loading.js +30 -6
- package/neural-interface/public/shared/ui-menubar.js +13 -3
- package/neural-interface/public/shared/ui-navbar.js +3 -1
- package/neural-interface/public/shared/ui-provider-auth.js +104 -0
- package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
- package/neural-interface/public/shared/ui-sessions.js +9 -3
- package/neural-interface/public/shared/ui-settings.js +55 -3
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
- package/neural-interface/public/shared/ui-sync.js +6 -0
- package/neural-interface/public/shared/ui-terminal.js +17 -0
- package/neural-interface/public/shared/ui-tool-updates.js +5 -0
- package/neural-interface/public/shared/ui-update.js +594 -5
- package/neural-interface/public/shared/ui-whatsapp.js +21 -6
- package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
- package/neural-interface/public/shared/worker-end.js +2 -0
- package/neural-interface/public/sw.js +1 -1
- package/neural-interface/public/variant/2d/main.js +10 -1
- package/neural-interface/public/variant/3d/main.js +10 -1
- package/neural-interface/server.js +1219 -608
- package/package.json +4 -1
- package/setup.js +9 -8
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun — the OS PATH a new terminal would get
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// A process keeps the PATH it was started with. SynaBun started from Finder,
|
|
6
|
+
// the Dock, the Start menu or a login item got the desktop's PATH, often
|
|
7
|
+
// without ~/.local/bin or Homebrew; started before a CLI was installed, it
|
|
8
|
+
// never sees the folder the installer added. This reads the PATH the
|
|
9
|
+
// operating system would give a terminal opened now:
|
|
10
|
+
//
|
|
11
|
+
// Windows the machine PATH, then the user PATH, from the registry
|
|
12
|
+
// (`reg query`; REG_EXPAND_SZ expanded with this process's
|
|
13
|
+
// environment), which is how Windows builds a new process's
|
|
14
|
+
// PATH. No PowerShell.
|
|
15
|
+
// macOS, Linux the PATH the user's login shell ends up with
|
|
16
|
+
// (`$SHELL -i -l -c`, which runs .zprofile/.zshrc,
|
|
17
|
+
// .bash_profile/.bashrc: nvm, asdf, mise, fnm, Homebrew's
|
|
18
|
+
// shellenv), started from the launchd default PATH, in a
|
|
19
|
+
// session of its own with no terminal, under a short timeout;
|
|
20
|
+
// on macOS also /etc/paths and /etc/paths.d
|
|
21
|
+
//
|
|
22
|
+
// The answer is cached briefly (`ttlMs`), read again in the background when it
|
|
23
|
+
// goes stale, and handed to augmented-path.js (rememberOsPath), whose PATH
|
|
24
|
+
// every lookup and spawn of a user's CLI uses. Re-check reads it again at once
|
|
25
|
+
// (refresh({ force: true })). A failed read keeps the previous answer and is
|
|
26
|
+
// retried sooner. Nothing here throws.
|
|
27
|
+
|
|
28
|
+
import { execFile, spawn } from 'node:child_process';
|
|
29
|
+
import { randomBytes } from 'node:crypto';
|
|
30
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
31
|
+
import os from 'node:os';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
import { rememberOsPath } from './augmented-path.js';
|
|
34
|
+
|
|
35
|
+
/** Where Windows keeps PATH: the machine's, then the user's (the order Windows joins them in). */
|
|
36
|
+
export const WINDOWS_PATH_KEYS = Object.freeze([
|
|
37
|
+
'HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment',
|
|
38
|
+
'HKCU\\Environment',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/** The PATH launchd gives an app on macOS (and a sane start on Linux): what a new terminal's shell begins from. */
|
|
42
|
+
export const LAUNCHD_PATH = '/usr/bin:/bin:/usr/sbin:/sbin';
|
|
43
|
+
|
|
44
|
+
/** Shells that take `-i -l -c`. Others (csh, nu, xonsh, pwsh …) are read through /bin/sh -l. */
|
|
45
|
+
const INTERACTIVE_LOGIN_SHELLS = new Set(['bash', 'zsh', 'ksh', 'ksh93', 'mksh', 'fish', 'yash']);
|
|
46
|
+
const LOGIN_ONLY_SHELLS = new Set(['sh', 'dash']);
|
|
47
|
+
const MAX_SHELL_OUTPUT = 1024 * 1024;
|
|
48
|
+
|
|
49
|
+
export function splitPathValue(value, platform = process.platform) {
|
|
50
|
+
return String(value || '').split(platform === 'win32' ? ';' : ':')
|
|
51
|
+
.map(entry => platform === 'win32' ? entry.trim().replace(/^"(.*)"$/, '$1').trim() : entry.trim())
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function dedupePath(entries, platform = process.platform) {
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
return entries.filter((entry) => {
|
|
58
|
+
const text = String(entry || '').trim();
|
|
59
|
+
if (!text) return false;
|
|
60
|
+
const key = platform === 'win32' ? text.toLowerCase().replace(/[\\/]+$/, '') : text.replace(/\/+$/, '') || '/';
|
|
61
|
+
if (seen.has(key)) return false;
|
|
62
|
+
seen.add(key);
|
|
63
|
+
return true;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The Path value in what `reg query <key> /v Path` prints:
|
|
69
|
+
*
|
|
70
|
+
* HKEY_CURRENT_USER\Environment
|
|
71
|
+
* Path REG_EXPAND_SZ %USERPROFILE%\AppData\Local\Microsoft\WindowsApps;C:\Users\you\.local\bin
|
|
72
|
+
*
|
|
73
|
+
* → { type: 'REG_EXPAND_SZ', value: '…' }, or null when there is none.
|
|
74
|
+
*/
|
|
75
|
+
export function parseRegQuery(text) {
|
|
76
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
77
|
+
const match = line.match(/^\s+(\S+)\s+(REG_EXPAND_SZ|REG_SZ)(?:\s+(.*))?$/i);
|
|
78
|
+
if (match && match[1].toLowerCase() === 'path') return { type: match[2].toUpperCase(), value: (match[3] || '').trim() };
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** %NAME% replaced from `env` (names compared without case, as Windows does); unknown names stay as written. */
|
|
84
|
+
export function expandWindowsVars(value, env = process.env) {
|
|
85
|
+
const lookup = new Map(Object.entries(env || {}).map(([name, val]) => [name.toUpperCase(), val]));
|
|
86
|
+
return String(value || '').replace(/%([^%;]+)%/g, (whole, name) => {
|
|
87
|
+
const found = lookup.get(name.toUpperCase());
|
|
88
|
+
return found == null || found === '' ? whole : String(found);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function decodeUtf8(buffer) {
|
|
93
|
+
try { return new TextDecoder('utf-8', { fatal: true }).decode(buffer); } catch { return null; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* `reg query <key> /v Path`, its output as text. reg.exe writes in the
|
|
98
|
+
* console's code page; a folder name outside ASCII (C:\Users\João) is then not
|
|
99
|
+
* UTF-8, and the query is asked again through a console switched to UTF-8
|
|
100
|
+
* (chcp 65001). Fixed strings only: nothing of a request reaches either line.
|
|
101
|
+
*/
|
|
102
|
+
export async function runRegQuery(key, { env = process.env, timeoutMs = 3000, execFileImpl = execFile } = {}) {
|
|
103
|
+
const systemRoot = env.SystemRoot || env.SYSTEMROOT || 'C:\\Windows';
|
|
104
|
+
const reg = path.win32.join(systemRoot, 'System32', 'reg.exe');
|
|
105
|
+
const comspec = env.ComSpec || env.COMSPEC || path.win32.join(systemRoot, 'System32', 'cmd.exe');
|
|
106
|
+
const run = (file, args, extra = {}) => new Promise((resolve) => {
|
|
107
|
+
try {
|
|
108
|
+
execFileImpl(file, args, { encoding: 'buffer', timeout: timeoutMs, windowsHide: true, env, maxBuffer: 1024 * 1024, ...extra }, (error, stdout) => {
|
|
109
|
+
resolve({ error, stdout: Buffer.isBuffer(stdout) ? stdout : Buffer.from(String(stdout || '')) });
|
|
110
|
+
});
|
|
111
|
+
} catch (error) {
|
|
112
|
+
resolve({ error, stdout: Buffer.alloc(0) });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
const first = await run(reg, ['query', key, '/v', 'Path']);
|
|
116
|
+
if (!first.stdout.length) throw first.error || new Error('reg.exe printed nothing');
|
|
117
|
+
const text = decodeUtf8(first.stdout);
|
|
118
|
+
if (text !== null) return text;
|
|
119
|
+
const second = await run(comspec, ['/d', '/s', '/c', `"chcp 65001 >nul & "${reg}" query "${key}" /v Path"`], { windowsVerbatimArguments: true });
|
|
120
|
+
return (second.stdout.length && decodeUtf8(second.stdout)) || first.stdout.toString('latin1');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The registry's PATH: machine entries, then user entries, each once. */
|
|
124
|
+
export async function readWindowsPath({ env = process.env, query = runRegQuery, timeoutMs = 3000 } = {}) {
|
|
125
|
+
const entries = [];
|
|
126
|
+
const sources = [];
|
|
127
|
+
const errors = [];
|
|
128
|
+
for (const key of WINDOWS_PATH_KEYS) {
|
|
129
|
+
try {
|
|
130
|
+
const found = parseRegQuery(await query(key, { env, timeoutMs }));
|
|
131
|
+
if (found) {
|
|
132
|
+
const part = splitPathValue(expandWindowsVars(found.value, env), 'win32');
|
|
133
|
+
entries.push(...part);
|
|
134
|
+
sources.push(...part.map(dir => ({ dir, source: key.startsWith('HKLM') ? 'system' : 'user' })));
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
errors.push(`${key}: ${String(error?.message || error).split(/\r?\n/)[0]}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { entries: dedupePath(entries, 'win32'), sources, ok: entries.length > 0, errors };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The user's login shell: $SHELL, else the account's (a launchd- or desktop-started app may have no SHELL). */
|
|
144
|
+
export function loginShellOf({ env = process.env, userInfo = () => os.userInfo() } = {}) {
|
|
145
|
+
if (env.SHELL) return env.SHELL;
|
|
146
|
+
try { return userInfo()?.shell || null; } catch { return null; }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* How to ask `shell` for the PATH a login session ends with. The PATH is
|
|
151
|
+
* printed on a line of its own between two marker lines, so whatever the
|
|
152
|
+
* startup files print around it (banners, warnings) is ignored.
|
|
153
|
+
*/
|
|
154
|
+
export function loginShellCommand(shell, marker) {
|
|
155
|
+
const name = path.posix.basename(String(shell || '')).toLowerCase();
|
|
156
|
+
const script = `printf '%s\\n' '${marker}' "$PATH" '${marker}'`;
|
|
157
|
+
if (INTERACTIVE_LOGIN_SHELLS.has(name)) return { file: shell, args: ['-i', '-l', '-c', script] };
|
|
158
|
+
if (LOGIN_ONLY_SHELLS.has(name)) return { file: shell, args: ['-l', '-c', script] };
|
|
159
|
+
return { file: '/bin/sh', args: ['-l', '-c', script] };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The PATH line between the two markers, or null. */
|
|
163
|
+
export function parseLoginShellOutput(text, marker) {
|
|
164
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
165
|
+
for (let i = 0; i + 2 < lines.length; i++) {
|
|
166
|
+
if (lines[i] === marker && lines[i + 2] === marker) return lines[i + 1];
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The PATH the user's login shell builds. Started in a session of its own
|
|
173
|
+
* (detached: no controlling terminal, so a startup file cannot wait on
|
|
174
|
+
* /dev/tty), from the launchd default PATH, in the home folder; killed with
|
|
175
|
+
* its whole group after `timeoutMs`.
|
|
176
|
+
*/
|
|
177
|
+
export function readLoginShellPath({
|
|
178
|
+
env = process.env,
|
|
179
|
+
platform = process.platform,
|
|
180
|
+
shell = loginShellOf({ env }),
|
|
181
|
+
spawnImpl = spawn,
|
|
182
|
+
timeoutMs = 4000,
|
|
183
|
+
home = env.HOME || os.homedir(),
|
|
184
|
+
} = {}) {
|
|
185
|
+
return new Promise((resolve) => {
|
|
186
|
+
const marker = `SYNABUN_PATH_${randomBytes(6).toString('hex').toUpperCase()}`;
|
|
187
|
+
const command = loginShellCommand(shell || '/bin/sh', marker);
|
|
188
|
+
const childEnv = { ...env, PATH: LAUNCHD_PATH, TERM: 'dumb' };
|
|
189
|
+
let child = null;
|
|
190
|
+
let out = '';
|
|
191
|
+
let settled = false;
|
|
192
|
+
let timer = null;
|
|
193
|
+
const finish = (result) => {
|
|
194
|
+
if (settled) return;
|
|
195
|
+
settled = true;
|
|
196
|
+
clearTimeout(timer);
|
|
197
|
+
resolve({ shell: command.file, ...result });
|
|
198
|
+
};
|
|
199
|
+
const killGroup = () => {
|
|
200
|
+
try { process.kill(-child.pid, 'SIGKILL'); } catch { try { child?.kill('SIGKILL'); } catch { /* gone */ } }
|
|
201
|
+
};
|
|
202
|
+
try {
|
|
203
|
+
child = spawnImpl(command.file, command.args, { env: childEnv, cwd: home, stdio: ['ignore', 'pipe', 'ignore'], detached: platform !== 'win32' });
|
|
204
|
+
} catch (error) {
|
|
205
|
+
return finish({ entries: [], ok: false, error: `${command.file} could not be started: ${error?.message || error}` });
|
|
206
|
+
}
|
|
207
|
+
timer = setTimeout(() => {
|
|
208
|
+
killGroup();
|
|
209
|
+
finish({ entries: [], ok: false, error: `${command.file} did not answer within ${Math.round(timeoutMs / 100) / 10} s` });
|
|
210
|
+
}, timeoutMs);
|
|
211
|
+
timer.unref?.();
|
|
212
|
+
child.on('error', (error) => finish({ entries: [], ok: false, error: `${command.file} could not be started: ${error?.message || error}` }));
|
|
213
|
+
child.stdout?.setEncoding?.('utf8');
|
|
214
|
+
child.stdout?.on('data', (chunk) => {
|
|
215
|
+
out += chunk;
|
|
216
|
+
if (out.length > MAX_SHELL_OUTPUT) {
|
|
217
|
+
killGroup();
|
|
218
|
+
finish({ entries: [], ok: false, error: `${command.file} printed too much` });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
child.on('close', () => {
|
|
222
|
+
const line = parseLoginShellOutput(out, marker);
|
|
223
|
+
if (line == null) return finish({ entries: [], ok: false, error: `${command.file} did not print a PATH` });
|
|
224
|
+
finish({ entries: dedupePath(splitPathValue(line, platform), platform), ok: true, error: null });
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** macOS's /etc/paths, then each file of /etc/paths.d in name order (what path_helper builds a login PATH from). */
|
|
230
|
+
export function readEtcPaths({ readFile = readFileSync, readdir = readdirSync } = {}) {
|
|
231
|
+
const out = [];
|
|
232
|
+
const take = (file) => {
|
|
233
|
+
try {
|
|
234
|
+
for (const line of String(readFile(file, 'utf8')).split(/\r?\n/)) {
|
|
235
|
+
const entry = line.trim();
|
|
236
|
+
if (entry && !entry.startsWith('#')) out.push(entry);
|
|
237
|
+
}
|
|
238
|
+
} catch { /* not there */ }
|
|
239
|
+
};
|
|
240
|
+
take('/etc/paths');
|
|
241
|
+
let names = [];
|
|
242
|
+
try { names = readdir('/etc/paths.d').map(String).sort(); } catch { /* not there */ }
|
|
243
|
+
for (const name of names) take(`/etc/paths.d/${name}`);
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* A cached reader of the OS PATH. `cached()` answers at once (and starts a
|
|
249
|
+
* read in the background when the answer is stale); `refresh()` waits for one.
|
|
250
|
+
*/
|
|
251
|
+
export function createOsPathReader({
|
|
252
|
+
platform = process.platform,
|
|
253
|
+
env = process.env,
|
|
254
|
+
ttlMs = 60_000,
|
|
255
|
+
failureTtlMs = 15_000,
|
|
256
|
+
readWindows = readWindowsPath,
|
|
257
|
+
readLoginShell = readLoginShellPath,
|
|
258
|
+
readEtc = readEtcPaths,
|
|
259
|
+
remember = rememberOsPath,
|
|
260
|
+
now = Date.now,
|
|
261
|
+
} = {}) {
|
|
262
|
+
let state = { entries: [], at: 0, ok: false, source: null, error: null, shell: null };
|
|
263
|
+
let inflight = null;
|
|
264
|
+
|
|
265
|
+
async function read() {
|
|
266
|
+
if (env.SYNABUN_TOOL_DISCOVERY === 'path') return { entries: [], ok: true, source: 'off', error: null, shell: null };
|
|
267
|
+
if (platform === 'win32') {
|
|
268
|
+
const found = await readWindows({ env });
|
|
269
|
+
return { entries: found.entries, sources: found.sources || [], ok: found.ok, source: 'registry', error: found.errors?.length ? found.errors.join('; ') : null, shell: null };
|
|
270
|
+
}
|
|
271
|
+
const found = await readLoginShell({ env, platform });
|
|
272
|
+
const etc = platform === 'darwin' ? readEtc() : [];
|
|
273
|
+
const entries = dedupePath([...(found.entries || []), ...etc], platform);
|
|
274
|
+
return {
|
|
275
|
+
entries,
|
|
276
|
+
ok: !!found.ok,
|
|
277
|
+
source: found.ok ? 'login-shell' : etc.length ? 'etc-paths' : 'none',
|
|
278
|
+
error: found.error || null,
|
|
279
|
+
shell: found.shell || null,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function snapshot() {
|
|
284
|
+
return { entries: state.entries.slice(), sources: (state.sources || []).map(entry => ({ ...entry })), at: state.at, ok: state.ok, source: state.source, error: state.error, shell: state.shell };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function stale() {
|
|
288
|
+
return !state.at || now() - state.at >= (state.ok ? ttlMs : failureTtlMs);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function refresh({ force = false } = {}) {
|
|
292
|
+
if (inflight) return inflight;
|
|
293
|
+
if (!force && !stale()) return Promise.resolve(snapshot());
|
|
294
|
+
inflight = (async () => {
|
|
295
|
+
let result;
|
|
296
|
+
try { result = await read(); } catch (error) { result = { entries: [], ok: false, source: 'none', error: String(error?.message || error), shell: null }; }
|
|
297
|
+
// A read that failed keeps what an earlier one found.
|
|
298
|
+
const entries = result.ok || !state.entries.length ? result.entries : state.entries;
|
|
299
|
+
state = { ...result, entries, at: now() };
|
|
300
|
+
try { remember(entries); } catch { /* nothing to tell */ }
|
|
301
|
+
return snapshot();
|
|
302
|
+
})().finally(() => { inflight = null; });
|
|
303
|
+
return inflight;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function cached() {
|
|
307
|
+
if (stale() && !inflight) refresh().catch(() => {});
|
|
308
|
+
return state.entries.slice();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function invalidate() {
|
|
312
|
+
state = { ...state, at: 0 };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return { refresh, cached, invalidate, snapshot, stale };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** This process's reader (server.js starts it at boot; Re-check forces it). */
|
|
319
|
+
export const osPath = createOsPathReader();
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const API_KEY_ERRORS = {
|
|
2
|
+
invalid_key: { status: 400, message: "That doesn't look like an API key for this provider." },
|
|
3
|
+
oauth_token: { status: 400, message: "That's a Claude sign-in token. Use Sign in with Claude." },
|
|
4
|
+
key_rejected: { status: 401, message: 'The provider rejected that key.' },
|
|
5
|
+
account_unavailable: { status: 404, message: 'This account is no longer available. Select an account again.' },
|
|
6
|
+
account_busy: { status: 409, message: 'Finish sign-in or reconnect this account and try again.' },
|
|
7
|
+
verification_unreachable: { status: 503, message: "Couldn't reach {vendor} to check the key. Try again." },
|
|
8
|
+
verification_unavailable: { status: 503, message: "{vendor} couldn't check the key. Try again shortly." },
|
|
9
|
+
storage_failed: { status: 500, message: "Couldn't save the key for this account." },
|
|
10
|
+
};
|
|
11
|
+
// `provider` names the vendor in the two verification messages (Anthropic for Claude, OpenAI for Codex).
|
|
12
|
+
export function providerApiKeyError(code, provider = 'claude') {
|
|
13
|
+
const { status, message } = Object.hasOwn(API_KEY_ERRORS, code) ? API_KEY_ERRORS[code] : API_KEY_ERRORS.storage_failed;
|
|
14
|
+
return Object.assign(new Error(message.replace('{vendor}', provider === 'codex' ? 'OpenAI' : 'Anthropic')), { status, code });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Return the normalized value: clipboard padding is not part of a credential.
|
|
18
|
+
export function validateProviderApiKey(provider, key) {
|
|
19
|
+
if (!['claude', 'codex'].includes(provider)) throw Object.assign(new Error('Unknown provider'), { status: 404 });
|
|
20
|
+
if (typeof key !== 'string' || key.length > 4096) throw providerApiKeyError('invalid_key');
|
|
21
|
+
key = key.trim();
|
|
22
|
+
// setup-token produces an OAuth token, not an Anthropic API key. Never send
|
|
23
|
+
// it to x-api-key or persist it under the API-key authentication method.
|
|
24
|
+
if (provider === 'claude' && key.startsWith('sk-ant-oat01-')) throw providerApiKeyError('oauth_token');
|
|
25
|
+
const pattern = provider === 'claude' ? /^sk-ant-[A-Za-z0-9_-]{16,}$/ : /^sk-(?!ant-)[A-Za-z0-9_-]{16,}$/;
|
|
26
|
+
if (!pattern.test(key)) throw providerApiKeyError('invalid_key');
|
|
27
|
+
return key;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Defense in depth for native diagnostics/events. Never forward credential fields.
|
|
31
|
+
export function redactProviderSecrets(value) {
|
|
32
|
+
if (typeof value === 'string') return value.replace(/sk-[A-Za-z0-9_-]{16,}/g, '[redacted]');
|
|
33
|
+
if (Array.isArray(value)) return value.map(redactProviderSecrets);
|
|
34
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value)
|
|
35
|
+
.filter(([key]) => !/^(api_?key|access_?token|refresh_?token|id_?token|authorization)$/i.test(key))
|
|
36
|
+
.map(([key, item]) => [key, redactProviderSecrets(item)]));
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function registerProviderPanelApiKeyAuthRoutes(app, { guard, claude, codex }) {
|
|
41
|
+
app.post('/api/provider-panel-auth/:provider/:accountId/api-key', guard, async (req, res) => {
|
|
42
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
43
|
+
const { provider, accountId } = req.params;
|
|
44
|
+
let key = req.body?.key;
|
|
45
|
+
// Express's parsed request body must not outlive this handler with a secret.
|
|
46
|
+
if (req.body) delete req.body.key;
|
|
47
|
+
try {
|
|
48
|
+
key = validateProviderApiKey(provider, key);
|
|
49
|
+
if (!/^[a-z0-9-]{1,64}$/.test(accountId)) throw providerApiKeyError('account_unavailable');
|
|
50
|
+
// Codex keys are kept by SynaBun per account too (lib/codex-accounts.js): an older
|
|
51
|
+
// page may still send the `target` of its live connection, which is not needed now.
|
|
52
|
+
if (provider === 'claude') await claude(accountId, key);
|
|
53
|
+
else await codex(accountId, key);
|
|
54
|
+
res.json({ ok: true });
|
|
55
|
+
} catch (error) {
|
|
56
|
+
// Never serialize provider exceptions; a native error may echo its input.
|
|
57
|
+
const fallback = { 400: 'invalid_key', 401: 'key_rejected', 404: 'account_unavailable', 409: 'account_busy', 503: 'verification_unavailable' }[error.status] || 'storage_failed';
|
|
58
|
+
const code = Object.hasOwn(API_KEY_ERRORS, error.code) ? error.code : fallback;
|
|
59
|
+
const safe = providerApiKeyError(code, provider);
|
|
60
|
+
res.status(safe.status).json({ code, error: safe.message });
|
|
61
|
+
} finally { key = null; }
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Install immediately after the global JSON parser: its SyntaxError may contain
|
|
66
|
+
// a fragment of the secret-bearing body. Never pass that error to Express logs.
|
|
67
|
+
export function providerApiKeyBodyError(error, req, res, next) {
|
|
68
|
+
if (req.method !== 'POST' || !/^\/api\/provider-panel-auth\/[^/]+\/[^/]+\/api-key(?:\?|$)/.test(req.originalUrl || req.url || '')) return next(error);
|
|
69
|
+
delete error.body;
|
|
70
|
+
req.body = undefined;
|
|
71
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
72
|
+
return res.status(error.status === 413 ? 413 : 400).json({ code: 'invalid_key', error: API_KEY_ERRORS.invalid_key.message });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Native stderr arrives in arbitrary chunks. Redact complete lines so a split
|
|
76
|
+
// credential cannot leave an unmasked suffix; omit unterminated/oversized lines.
|
|
77
|
+
export function providerDiagnosticSink(write, maxChars = 65536) {
|
|
78
|
+
let buffer = '', dropping = false;
|
|
79
|
+
return chunk => {
|
|
80
|
+
const parts = String(chunk).split(/\r?\n/);
|
|
81
|
+
for (let i = 0; i < parts.length; i++) {
|
|
82
|
+
if (!dropping) {
|
|
83
|
+
buffer += parts[i];
|
|
84
|
+
if (buffer.length > maxChars) { buffer = ''; dropping = true; }
|
|
85
|
+
}
|
|
86
|
+
if (i < parts.length - 1) {
|
|
87
|
+
if (!dropping && buffer.trim()) write(redactProviderSecrets(buffer.trim()));
|
|
88
|
+
buffer = ''; dropping = false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { isAbsolute } from 'node:path';
|
|
3
|
+
import { claudeAuthEnv } from './claude-panel-auth.js';
|
|
4
|
+
import { applyCodexLaunchEnv } from './codex-accounts.js';
|
|
5
|
+
|
|
6
|
+
// Only server-owned account records and resolved executables enter the PTY.
|
|
7
|
+
export function providerLoginSpec({ provider, accountId, claudeAccounts, codexAccount, claudeExecutable, codexExecutable, env }) {
|
|
8
|
+
if (!['claude', 'codex'].includes(provider) || typeof accountId !== 'string' || !/^[a-z0-9-]{1,64}$/.test(accountId)) {
|
|
9
|
+
throw Object.assign(new Error('Unknown provider account'), { status: 404 });
|
|
10
|
+
}
|
|
11
|
+
const account = provider === 'claude' ? claudeAccounts.profileFor(accountId) : codexAccount(accountId);
|
|
12
|
+
if (!account || account.id !== accountId) throw Object.assign(new Error('Unknown provider account'), { status: 404 });
|
|
13
|
+
const file = provider === 'claude' ? claudeExecutable()?.path : codexExecutable();
|
|
14
|
+
if (!file || !isAbsolute(file) || /\.(cmd|bat)$/i.test(file)) {
|
|
15
|
+
throw new Error(`Could not resolve the ${provider === 'claude' ? 'Claude' : 'Codex'} executable. Check its installation.`);
|
|
16
|
+
}
|
|
17
|
+
let baseEnv = { ...env };
|
|
18
|
+
if (provider === 'codex') {
|
|
19
|
+
// Every Codex account, Default too: `codex login` writes to that account's CODEX_HOME, with no
|
|
20
|
+
// inherited OpenAI/Codex credential around it (lib/codex-accounts.js CODEX_CREDENTIAL_ENV).
|
|
21
|
+
if (!account.home) throw new Error('This Codex account has no home folder.');
|
|
22
|
+
baseEnv = applyCodexLaunchEnv(baseEnv, { env: { CODEX_HOME: account.home } });
|
|
23
|
+
} else if (accountId !== 'default') {
|
|
24
|
+
if (!account.home) throw new Error('This Claude account has no isolated home.');
|
|
25
|
+
baseEnv = claudeAuthEnv(baseEnv, { CLAUDE_CONFIG_DIR: account.home });
|
|
26
|
+
}
|
|
27
|
+
const args = provider === 'claude' ? ['auth', 'login', '--claudeai'] : ['login'];
|
|
28
|
+
return { launch: /\.[cm]?js$/i.test(file) ? { shell: process.execPath, args: [file, ...args] } : { shell: file, args }, baseEnv };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// `onSettled(provider, id, status)` (optional): told once when a sign-in terminal ends ('exited' when the
|
|
32
|
+
// CLI exited 0, else 'error'), so the host can ask the provider what it now says about the account.
|
|
33
|
+
export function createProviderPanelTerminalAuth({ resolveSpec, createSession, whenSpawned, killSession, timeoutMs = 10 * 60_000, onSettled = null }) {
|
|
34
|
+
const operations = new Map();
|
|
35
|
+
const key = (provider, id) => `${provider}:${id}`;
|
|
36
|
+
const snapshot = op => ({ sessionId: op.sessionId, status: op.status, error: op.error || '' });
|
|
37
|
+
function read(provider, id) {
|
|
38
|
+
resolveSpec(provider, id); // Validate deleted accounts and unsupported providers on reads too.
|
|
39
|
+
const op = operations.get(key(provider, id));
|
|
40
|
+
return op ? snapshot(op) : { status: 'idle' };
|
|
41
|
+
}
|
|
42
|
+
async function start(provider, id) {
|
|
43
|
+
const spec = resolveSpec(provider, id);
|
|
44
|
+
const existing = operations.get(key(provider, id));
|
|
45
|
+
if (existing && ['starting', 'pending'].includes(existing.status)) { await existing.ready; return snapshot(existing); }
|
|
46
|
+
const op = { sessionId: randomBytes(16).toString('hex'), status: 'starting' };
|
|
47
|
+
operations.set(key(provider, id), op);
|
|
48
|
+
let timer;
|
|
49
|
+
const finish = (status, error = '') => {
|
|
50
|
+
if (!['starting', 'pending'].includes(op.status)) return;
|
|
51
|
+
clearTimeout(timer); op.status = status; op.error = error;
|
|
52
|
+
if (typeof onSettled === 'function') Promise.resolve().then(() => onSettled(provider, id, status)).catch(() => {});
|
|
53
|
+
};
|
|
54
|
+
op.ready = (async () => {
|
|
55
|
+
try {
|
|
56
|
+
createSession('shell', 120, 30, undefined, { ...spec, sessionId: op.sessionId,
|
|
57
|
+
onExit: ({ exitCode }) => finish(exitCode === 0 ? 'exited' : 'error', exitCode === 0 ? '' : 'Sign-in failed. Try again.'),
|
|
58
|
+
onClosed: () => finish('error', 'Sign-in terminal closed. Try again.'),
|
|
59
|
+
});
|
|
60
|
+
let spawnTimer;
|
|
61
|
+
try {
|
|
62
|
+
await Promise.race([whenSpawned(op.sessionId), new Promise((_, reject) => {
|
|
63
|
+
spawnTimer = setTimeout(() => reject(new Error('Terminal host did not respond')), 15000);
|
|
64
|
+
})]);
|
|
65
|
+
} finally { clearTimeout(spawnTimer); }
|
|
66
|
+
if (op.status === 'starting') {
|
|
67
|
+
op.status = 'pending';
|
|
68
|
+
timer = setTimeout(() => { finish('error', 'Sign-in timed out. Try again.'); killSession(op.sessionId); }, timeoutMs);
|
|
69
|
+
timer.unref?.();
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
finish('error', 'Could not open the sign-in terminal. Try again.');
|
|
73
|
+
killSession(op.sessionId);
|
|
74
|
+
}
|
|
75
|
+
})();
|
|
76
|
+
await op.ready;
|
|
77
|
+
return snapshot(op);
|
|
78
|
+
}
|
|
79
|
+
return { start, read };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function registerProviderPanelTerminalAuthRoutes(app, { guard, auth }) {
|
|
83
|
+
const route = '/api/provider-panel-auth/:provider/:id/terminal';
|
|
84
|
+
app.get(route, guard, (req, res) => {
|
|
85
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
86
|
+
try { res.json({ terminal: auth.read(req.params.provider, req.params.id) }); }
|
|
87
|
+
catch (error) { res.status(error.status || 500).json({ error: error.message }); }
|
|
88
|
+
});
|
|
89
|
+
app.post(route, guard, async (req, res) => {
|
|
90
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
91
|
+
if (Object.keys(req.body || {}).length) return res.status(400).json({ error: 'Only provider and account selection are accepted.' });
|
|
92
|
+
try { res.json({ terminal: await auth.start(req.params.provider, req.params.id) }); }
|
|
93
|
+
catch (error) { res.status(error.status || 500).json({ error: error.message }); }
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -18,6 +18,7 @@ import { createTerminalModel } from './snapshot.js';
|
|
|
18
18
|
import { evaluateFlow } from './flow.js';
|
|
19
19
|
import { EV, OP, HOST_DEFAULTS } from './protocol.js';
|
|
20
20
|
import { createEventLoopMonitor } from '../event-loop-monitor.js';
|
|
21
|
+
import { withPathEnv } from '../augmented-path.js';
|
|
21
22
|
|
|
22
23
|
const IS_WIN = process.platform === 'win32';
|
|
23
24
|
const IS_LINUX = process.platform === 'linux';
|
|
@@ -74,7 +75,7 @@ export function createPtyHostCore({ send, config = {}, pty: injectedPty = null }
|
|
|
74
75
|
}
|
|
75
76
|
const cols = clampInt(spec.cols, 2, 1000) || 120;
|
|
76
77
|
const rows = clampInt(spec.rows, 1, 500) || 30;
|
|
77
|
-
const env =
|
|
78
|
+
const env = withPathEnv(spec.env || {});
|
|
78
79
|
for (const k of Object.keys(env)) if (k.startsWith('NODE_CHANNEL_') || k === 'NODE_UNIQUE_ID') delete env[k];
|
|
79
80
|
|
|
80
81
|
let file = spec.file;
|
|
@@ -38,6 +38,7 @@ export function createPtyHostManager({
|
|
|
38
38
|
} = {}) {
|
|
39
39
|
const handlers = new Map(); // id → { onSpawned, onTap, onResized, onClients, onExit }
|
|
40
40
|
const spawnWaiters = new Map(); // id → waiter
|
|
41
|
+
const preparing = new Map(); // id → operations queued while PATH is read
|
|
41
42
|
const requests = new Map(); // reqId → { resolve, reject, timer }
|
|
42
43
|
let reqSeq = 0;
|
|
43
44
|
let child = null;
|
|
@@ -158,6 +159,7 @@ export function createPtyHostManager({
|
|
|
158
159
|
heldSockets = [];
|
|
159
160
|
queue = [];
|
|
160
161
|
const lost = [...handlers.entries()];
|
|
162
|
+
preparing.clear();
|
|
161
163
|
handlers.clear();
|
|
162
164
|
for (const [id] of lost) settleSpawn(id, new Error('pty-host exited'));
|
|
163
165
|
spawnWaiters.clear();
|
|
@@ -175,6 +177,13 @@ export function createPtyHostManager({
|
|
|
175
177
|
|
|
176
178
|
// ── Main → host ──
|
|
177
179
|
function sendOp(msg) {
|
|
180
|
+
if (preparing.has(msg.id) && msg.op !== OP.SPAWN) {
|
|
181
|
+
if (msg.op === OP.KILL) {
|
|
182
|
+
preparing.delete(msg.id);
|
|
183
|
+
dispatch({ t: EV.SPAWN_ERROR, id: msg.id, message: 'Terminal closed before it started' });
|
|
184
|
+
} else preparing.get(msg.id).push(msg);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
178
187
|
if (mode === 'inproc') {
|
|
179
188
|
if (!core || !ready) { queue.push(msg); return; }
|
|
180
189
|
core.handleMessage(msg);
|
|
@@ -190,11 +199,23 @@ export function createPtyHostManager({
|
|
|
190
199
|
for (const msg of q) sendOp(msg);
|
|
191
200
|
}
|
|
192
201
|
|
|
193
|
-
function spawn(spec, h = {}) {
|
|
202
|
+
function spawn(spec, h = {}, prepare = null) {
|
|
194
203
|
handlers.set(spec.id, h);
|
|
195
204
|
const w = makeWaiter();
|
|
196
205
|
spawnWaiters.set(spec.id, w);
|
|
197
|
-
|
|
206
|
+
if (prepare) {
|
|
207
|
+
const pending = [];
|
|
208
|
+
preparing.set(spec.id, pending);
|
|
209
|
+
Promise.resolve().then(prepare).then(prepared => {
|
|
210
|
+
if (shuttingDown || !handlers.has(spec.id) || preparing.get(spec.id) !== pending) return;
|
|
211
|
+
preparing.delete(spec.id);
|
|
212
|
+
sendOp({ op: OP.SPAWN, ...spec, ...prepared });
|
|
213
|
+
for (const message of pending) sendOp(message);
|
|
214
|
+
}).catch(error => {
|
|
215
|
+
preparing.delete(spec.id);
|
|
216
|
+
dispatch({ t: EV.SPAWN_ERROR, id: spec.id, message: error.message });
|
|
217
|
+
});
|
|
218
|
+
} else sendOp({ op: OP.SPAWN, ...spec });
|
|
198
219
|
return w.promise;
|
|
199
220
|
}
|
|
200
221
|
|
|
@@ -315,6 +336,8 @@ export function createPtyHostManager({
|
|
|
315
336
|
|
|
316
337
|
async function shutdown({ timeoutMs = 1500 } = {}) {
|
|
317
338
|
shuttingDown = true;
|
|
339
|
+
for (const id of preparing.keys()) settleSpawn(id, new Error('Terminal host is shutting down'));
|
|
340
|
+
preparing.clear();
|
|
318
341
|
if (restartTimer) { clearTimeout(restartTimer); restartTimer = null; }
|
|
319
342
|
if (mode === 'inproc') {
|
|
320
343
|
try { core?.shutdownAll(); } catch {}
|
|
@@ -332,6 +355,8 @@ export function createPtyHostManager({
|
|
|
332
355
|
|
|
333
356
|
function killNow() {
|
|
334
357
|
shuttingDown = true;
|
|
358
|
+
for (const id of preparing.keys()) settleSpawn(id, new Error('Terminal host is shutting down'));
|
|
359
|
+
preparing.clear();
|
|
335
360
|
if (restartTimer) { clearTimeout(restartTimer); restartTimer = null; }
|
|
336
361
|
try { child?.kill('SIGTERM'); } catch {}
|
|
337
362
|
try { core?.shutdownAll(); } catch {}
|