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,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SynaBun — the PATH of everything SynaBun starts, and where npm puts global commands
|
|
3
|
+
*
|
|
4
|
+
* A packaged application (packaging/) brings its own Node runtime, npm and npx.
|
|
5
|
+
* They are a fallback: on the PATH of every process the application starts,
|
|
6
|
+
* the order is
|
|
7
|
+
*
|
|
8
|
+
* 1. the user's own PATH, as it came
|
|
9
|
+
* 2. the bin folder of npm's global prefix, when the PATH lacks it (where
|
|
10
|
+
* `npm i -g` through SynaBun's npm puts commands: the user's own prefix
|
|
11
|
+
* when they set one, else a per-user folder of SynaBun's, never the app)
|
|
12
|
+
* 3. the packaged runtime, last
|
|
13
|
+
*
|
|
14
|
+
* packagedPathLast() is that order, and the one implementation of it: the
|
|
15
|
+
* entry points of a packaged build (lib/packaged-runtime.js packagedChildEnv:
|
|
16
|
+
* the bootstrap, the hook handler, the headless MCP server) and every PATH the
|
|
17
|
+
* Neural Interface builds (neural-interface/lib/augmented-path.js) use it.
|
|
18
|
+
*
|
|
19
|
+
* npm's global prefix is read here the way npm 10 (@npmcli/config) settles it,
|
|
20
|
+
* from plain files, without starting npm:
|
|
21
|
+
*
|
|
22
|
+
* 1. the environment: npm_config_prefix, in any letter case, when not empty
|
|
23
|
+
* 2. the user's config file: npm_config_userconfig, else ~/.npmrc
|
|
24
|
+
* 3. the global config file: npm_config_globalconfig, else `globalconfig=` in
|
|
25
|
+
* the user's (or the builtin) file, else <the prefix so far>/etc/npmrc
|
|
26
|
+
* 4. the builtin config of that npm (<npm>/npmrc: a packaged build writes one)
|
|
27
|
+
* 5. the default: $PREFIX, else the folder of the node running npm (Windows)
|
|
28
|
+
* or the folder above it
|
|
29
|
+
*
|
|
30
|
+
* (The command line comes first for npm and is never known here; a project's
|
|
31
|
+
* .npmrc is ignored by npm for a global install.) Every value goes through
|
|
32
|
+
* npm's ${VAR} substitution; a path then has a leading `~/` (on Windows also
|
|
33
|
+
* `~\`) replaced by the home folder (HOME, else the account's) and is made
|
|
34
|
+
* absolute. Files are read as npm's ini reader reads them: `;` and `#`
|
|
35
|
+
* comments, quotes, inline comments; a `prefix` inside a [section] is not the
|
|
36
|
+
* prefix. One difference: an empty `prefix=` counts as not set (npm would use
|
|
37
|
+
* the folder it happens to run in, which is no folder to put on a PATH).
|
|
38
|
+
*
|
|
39
|
+
* Only node: modules here: neural-interface/lib/augmented-path.js imports this
|
|
40
|
+
* file, and tests copy both into scratch trees.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { readFileSync } from 'node:fs';
|
|
44
|
+
import { homedir } from 'node:os';
|
|
45
|
+
import path from 'node:path';
|
|
46
|
+
|
|
47
|
+
const MEMO_MS = 5_000;
|
|
48
|
+
let _memo = null; // { key, at, value }
|
|
49
|
+
|
|
50
|
+
const implFor = (platform) => (platform === 'win32' ? path.win32 : path.posix);
|
|
51
|
+
|
|
52
|
+
/** The value of an env's PATH, whatever its spelling (Windows: Path). */
|
|
53
|
+
export function pathEnvValue(env = process.env) {
|
|
54
|
+
return env[Object.keys(env).find(key => key.toUpperCase() === 'PATH')] || '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* `env` with PATH set to `value`. Windows environment names are
|
|
59
|
+
* case-insensitive and JS object keys are not: the base's spelling is kept and
|
|
60
|
+
* every alias removed before an env reaches Node or ConPTY.
|
|
61
|
+
*/
|
|
62
|
+
export function withPathEnv(env = process.env, value = pathEnvValue(env), platform = process.platform) {
|
|
63
|
+
if (platform !== 'win32') return { ...env, PATH: value };
|
|
64
|
+
const keys = Object.keys(env).filter(key => key.toUpperCase() === 'PATH');
|
|
65
|
+
const key = keys[0] || 'Path';
|
|
66
|
+
const out = { ...env };
|
|
67
|
+
for (const alias of keys) delete out[alias];
|
|
68
|
+
out[key] = value;
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* PATH `value` with a packaged application's folders after the user's: each
|
|
74
|
+
* `extra` folder (npm's global bin) appended when the PATH does not have it
|
|
75
|
+
* yet, then every `last` folder (the runtime) moved to the very end. Nothing
|
|
76
|
+
* else moves; each folder stays once (Windows compares without case and
|
|
77
|
+
* trailing separators).
|
|
78
|
+
*/
|
|
79
|
+
export function packagedPathLast(value, { platform = process.platform, last = [], extra = [] } = {}) {
|
|
80
|
+
const delimiter = platform === 'win32' ? ';' : ':';
|
|
81
|
+
const id = (entry) => (platform === 'win32' ? entry.toLowerCase().replace(/[\\/]+$/, '') : entry.replace(/\/+$/, '') || '/');
|
|
82
|
+
const lastIds = new Set(last.filter(Boolean).map(id));
|
|
83
|
+
const seen = new Set();
|
|
84
|
+
const out = [];
|
|
85
|
+
const add = (entry) => {
|
|
86
|
+
if (!entry) return;
|
|
87
|
+
const key = id(entry);
|
|
88
|
+
if (seen.has(key)) return;
|
|
89
|
+
seen.add(key);
|
|
90
|
+
out.push(entry);
|
|
91
|
+
};
|
|
92
|
+
for (const entry of String(value || '').split(delimiter)) if (entry && !lastIds.has(id(entry))) add(entry);
|
|
93
|
+
for (const entry of extra) if (entry && !lastIds.has(id(entry))) add(entry);
|
|
94
|
+
for (const entry of last) add(entry);
|
|
95
|
+
return out.join(delimiter);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Where npm puts the commands of global packages under `prefix`: the prefix itself on Windows, its bin/ elsewhere. */
|
|
99
|
+
export function npmGlobalBinFor(prefix, platform = process.platform) {
|
|
100
|
+
if (!prefix) return null;
|
|
101
|
+
return platform === 'win32' ? prefix : implFor(platform).join(prefix, 'bin');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The builtin npmrc of the npm in a packaged runtime whose executables are in `runtimeBin`. */
|
|
105
|
+
export function bundledNpmrcPath(runtimeBin, platform = process.platform) {
|
|
106
|
+
if (!runtimeBin) return null;
|
|
107
|
+
const impl = implFor(platform);
|
|
108
|
+
return platform === 'win32'
|
|
109
|
+
? impl.join(runtimeBin, 'node_modules', 'npm', 'npmrc')
|
|
110
|
+
: impl.join(runtimeBin, '..', 'lib', 'node_modules', 'npm', 'npmrc');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function envLookup(env, name, platform) {
|
|
114
|
+
if (env[name] !== undefined) return env[name];
|
|
115
|
+
if (platform !== 'win32') return undefined;
|
|
116
|
+
const key = Object.keys(env).find(candidate => candidate.toUpperCase() === name.toUpperCase());
|
|
117
|
+
return key === undefined ? undefined : env[key];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* npm's ${VAR} substitution (@npmcli/config env-replace.js): `${NAME}` is the
|
|
122
|
+
* variable's value, a name that is not set stays as written, and a backslash
|
|
123
|
+
* before `${` escapes it (an even number of them is that many halved, then the value).
|
|
124
|
+
*/
|
|
125
|
+
export function npmEnvReplace(text, env = process.env, platform = process.platform) {
|
|
126
|
+
return String(text).replace(/(?<!\\)(\\*)\$\{([^${}]+)\}/g, (orig, esc, name) => {
|
|
127
|
+
const found = envLookup(env, name, platform);
|
|
128
|
+
const value = found !== undefined ? found : `\${${name}}`;
|
|
129
|
+
if (esc.length % 2) return orig.slice((esc.length + 1) / 2);
|
|
130
|
+
return esc.slice(esc.length / 2) + value;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// The ini reader npm uses (ini 4.x unsafe()): quotes, then `;` / `#` ending the value unless escaped.
|
|
135
|
+
function iniValue(raw) {
|
|
136
|
+
let value = String(raw || '').trim();
|
|
137
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
138
|
+
if (value.charAt(0) === "'") value = value.slice(1, -1);
|
|
139
|
+
try { value = JSON.parse(value); } catch { /* kept as it is */ }
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
let escaped = false;
|
|
143
|
+
let out = '';
|
|
144
|
+
for (const char of value) {
|
|
145
|
+
if (escaped) {
|
|
146
|
+
out += '\\;#'.includes(char) ? char : `\\${char}`;
|
|
147
|
+
escaped = false;
|
|
148
|
+
} else if (char === ';' || char === '#') {
|
|
149
|
+
break;
|
|
150
|
+
} else if (char === '\\') {
|
|
151
|
+
escaped = true;
|
|
152
|
+
} else {
|
|
153
|
+
out += char;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (escaped) out += '\\';
|
|
157
|
+
return out.trim();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The top-level settings of an .npmrc, as npm's ini reader gives them (a key
|
|
162
|
+
* inside a [section] is the section's; `true` / `false` / `null` are those
|
|
163
|
+
* values; `key[]` makes a list; the last of a repeated key wins).
|
|
164
|
+
*/
|
|
165
|
+
export function parseNpmrc(text) {
|
|
166
|
+
const out = Object.create(null);
|
|
167
|
+
for (const line of String(text || '').split(/[\r\n]+/g)) {
|
|
168
|
+
if (!line || /^\s*[;#]/.test(line) || /^\s*$/.test(line)) continue;
|
|
169
|
+
if (/^\[([^\]]*)\]\s*$/.test(line)) break;
|
|
170
|
+
const match = line.match(/^([^=]+)(=(.*))?$/);
|
|
171
|
+
if (!match) continue;
|
|
172
|
+
const rawKey = iniValue(match[1]);
|
|
173
|
+
const isList = rawKey.length > 2 && rawKey.endsWith('[]');
|
|
174
|
+
const key = isList ? rawKey.slice(0, -2) : rawKey;
|
|
175
|
+
if (!key || key === '__proto__') continue;
|
|
176
|
+
const raw = match[2] ? iniValue(match[3]) : true;
|
|
177
|
+
const value = raw === 'true' || raw === 'false' || raw === 'null' ? JSON.parse(raw) : raw;
|
|
178
|
+
if (isList) {
|
|
179
|
+
if (!Array.isArray(out[key])) out[key] = out[key] === undefined ? [] : [out[key]];
|
|
180
|
+
}
|
|
181
|
+
if (Array.isArray(out[key])) out[key].push(value);
|
|
182
|
+
else out[key] = value;
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The npm_config_* settings of an environment, as npm reads them (any letter case, `_` as `-`, empty ones skipped; the last spelling wins). */
|
|
188
|
+
export function npmEnvConfig(env = process.env) {
|
|
189
|
+
const out = Object.create(null);
|
|
190
|
+
for (const [name, value] of Object.entries(env)) {
|
|
191
|
+
if (!/^npm_config_/i.test(name) || value === '' || value === undefined) continue;
|
|
192
|
+
let key = name.slice('npm_config_'.length);
|
|
193
|
+
if (!key.startsWith('//')) key = key.replace(/(?!^)_/g, '-').toLowerCase();
|
|
194
|
+
out[key] = value;
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** A path setting as npm parses it: ${VAR}, then `~/`, then absolute. null for one that is not a usable string. */
|
|
200
|
+
export function npmPathValue(value, { env = process.env, platform = process.platform, home, cwd = process.cwd() } = {}) {
|
|
201
|
+
if (typeof value !== 'string') return null;
|
|
202
|
+
const text = npmEnvReplace(value.trim(), env, platform);
|
|
203
|
+
if (!text) return null;
|
|
204
|
+
const impl = implFor(platform);
|
|
205
|
+
const homePattern = platform === 'win32' ? /^~(\/|\\)/ : /^~\//;
|
|
206
|
+
if (homePattern.test(text) && home) return impl.resolve(home, text.slice(2));
|
|
207
|
+
return impl.resolve(cwd, text);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The home folder npm uses: HOME, else the account's (os.homedir(); on Windows USERPROFILE). */
|
|
211
|
+
export function npmHome(env = process.env, platform = process.platform) {
|
|
212
|
+
if (env.HOME) return env.HOME;
|
|
213
|
+
if (env === process.env) return homedir();
|
|
214
|
+
if (platform === 'win32') return envLookup(env, 'USERPROFILE', platform) || homedir();
|
|
215
|
+
return homedir();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* npm's global prefix, as described at the top. `builtin` is the builtin npmrc
|
|
220
|
+
* of the npm that will run (lib/packaged-runtime.js names the packaged one),
|
|
221
|
+
* `execPath` the node it runs on (for the default); leave both out to get only
|
|
222
|
+
* what the user set (`source` then never says 'builtin' or 'default').
|
|
223
|
+
*
|
|
224
|
+
* Returns { prefix, bin, source, from, userconfig, globalconfig }: `prefix` an
|
|
225
|
+
* absolute folder or null, `source` 'env' | 'userconfig' | 'globalconfig' |
|
|
226
|
+
* 'builtin' | 'default' | null, `from` what set it (the variable or the file).
|
|
227
|
+
*/
|
|
228
|
+
export function resolveNpmPrefix({
|
|
229
|
+
env = process.env,
|
|
230
|
+
platform = process.platform,
|
|
231
|
+
home = npmHome(env, platform),
|
|
232
|
+
cwd = process.cwd(),
|
|
233
|
+
readFile = readFileSync,
|
|
234
|
+
builtin = null,
|
|
235
|
+
execPath = null,
|
|
236
|
+
} = {}) {
|
|
237
|
+
const impl = implFor(platform);
|
|
238
|
+
const parse = { env, platform, home, cwd };
|
|
239
|
+
const read = (file) => {
|
|
240
|
+
if (!file) return Object.create(null);
|
|
241
|
+
try { return parseNpmrc(readFile(file, 'utf8')); } catch { return Object.create(null); }
|
|
242
|
+
};
|
|
243
|
+
const fromEnv = npmEnvConfig(env);
|
|
244
|
+
const envPrefix = npmPathValue(fromEnv.prefix, parse);
|
|
245
|
+
const userconfig = npmPathValue(fromEnv.userconfig, parse) || (home ? impl.resolve(home, '.npmrc') : null);
|
|
246
|
+
const user = read(userconfig);
|
|
247
|
+
const userPrefix = npmPathValue(user.prefix, parse);
|
|
248
|
+
const builtinConfig = read(builtin);
|
|
249
|
+
const builtinPrefix = npmPathValue(builtinConfig.prefix, parse);
|
|
250
|
+
let defaultPrefix = null;
|
|
251
|
+
if (execPath) {
|
|
252
|
+
if (env.PREFIX) defaultPrefix = npmPathValue(env.PREFIX, parse);
|
|
253
|
+
else if (platform === 'win32') defaultPrefix = impl.dirname(execPath);
|
|
254
|
+
else defaultPrefix = env.DESTDIR ? impl.join(env.DESTDIR, impl.dirname(impl.dirname(execPath))) : impl.dirname(impl.dirname(execPath));
|
|
255
|
+
}
|
|
256
|
+
const soFar = envPrefix || userPrefix || builtinPrefix || defaultPrefix;
|
|
257
|
+
const globalconfig = npmPathValue(fromEnv.globalconfig, parse)
|
|
258
|
+
|| npmPathValue(user.globalconfig, parse)
|
|
259
|
+
|| npmPathValue(builtinConfig.globalconfig, parse)
|
|
260
|
+
|| (soFar ? impl.resolve(soFar, 'etc', 'npmrc') : null);
|
|
261
|
+
const globalPrefix = npmPathValue(read(globalconfig).prefix, parse);
|
|
262
|
+
const levels = [
|
|
263
|
+
['env', envPrefix, 'npm_config_prefix'],
|
|
264
|
+
['userconfig', userPrefix, userconfig],
|
|
265
|
+
['globalconfig', globalPrefix, globalconfig],
|
|
266
|
+
['builtin', builtinPrefix, builtin],
|
|
267
|
+
['default', defaultPrefix, execPath],
|
|
268
|
+
];
|
|
269
|
+
const found = levels.find(([, value]) => value);
|
|
270
|
+
const prefix = found ? found[1] : null;
|
|
271
|
+
return {
|
|
272
|
+
prefix,
|
|
273
|
+
bin: npmGlobalBinFor(prefix, platform),
|
|
274
|
+
source: found ? found[0] : null,
|
|
275
|
+
from: found ? found[2] : null,
|
|
276
|
+
userconfig,
|
|
277
|
+
globalconfig,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* resolveNpmPrefix() for this process, kept for a few seconds: every PATH
|
|
283
|
+
* SynaBun builds asks, and a changed ~/.npmrc counts within seconds. Options
|
|
284
|
+
* other than `builtin` and `execPath` (an env that is not process.env, a
|
|
285
|
+
* readFile) are not cached.
|
|
286
|
+
*/
|
|
287
|
+
export function effectiveNpmPrefix(options = {}) {
|
|
288
|
+
const { builtin = null, execPath = null, ...rest } = options;
|
|
289
|
+
const live = Object.keys(rest).every(key => (key === 'env' ? rest.env === process.env : key === 'platform' ? rest.platform === process.platform : false));
|
|
290
|
+
if (!live) return resolveNpmPrefix(options);
|
|
291
|
+
const env = process.env;
|
|
292
|
+
const relevant = Object.entries(env).filter(([name]) => /^npm_config_/i.test(name) || ['HOME', 'USERPROFILE', 'APPDATA', 'PREFIX', 'DESTDIR'].includes(name.toUpperCase()));
|
|
293
|
+
const key = JSON.stringify([builtin, execPath, process.cwd(), relevant]);
|
|
294
|
+
if (_memo && _memo.key === key && Date.now() - _memo.at < MEMO_MS) return _memo.value;
|
|
295
|
+
const value = resolveNpmPrefix({ builtin, execPath });
|
|
296
|
+
_memo = { key, at: Date.now(), value };
|
|
297
|
+
return value;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Forget what effectiveNpmPrefix() keeps (a test, or a settings change that must count at once). */
|
|
301
|
+
export function forgetNpmPrefix() {
|
|
302
|
+
_memo = null;
|
|
303
|
+
}
|
package/lib/claude-hooks.js
CHANGED
|
@@ -78,7 +78,8 @@ export function isSynaBunHookCommand(command, scriptName) {
|
|
|
78
78
|
/**
|
|
79
79
|
* Why this run must not write hook commands, or null when it may. A packaged
|
|
80
80
|
* application with no path that outlives the run (macOS started it from a
|
|
81
|
-
* temporary copy) would leave every handler
|
|
81
|
+
* temporary copy, or from the disk image it came in) would leave every handler
|
|
82
|
+
* pointing at nothing.
|
|
82
83
|
*/
|
|
83
84
|
export function hookInstallBlocker(runtime = packagedRuntime()) {
|
|
84
85
|
if (!runtime || durableEntry(runtime)) return null;
|
|
@@ -52,6 +52,8 @@ function isRuntimeOnlyMigrationFile(relativePath) {
|
|
|
52
52
|
return name === '.DS_Store'
|
|
53
53
|
|| MIGRATION_RUNTIME_ONLY_FILES.has(normalized)
|
|
54
54
|
|| /(?:^|\/)server-(?:stdout|stderr)\.log(?:\.1)?$/i.test(normalized)
|
|
55
|
+
// The desktop app's own log (packaging/desktop) and the launcher's rotated one, written from the first start on.
|
|
56
|
+
|| /^data\/(?:desktop\.log(?:\.1)?|launcher\.log\.1)$/i.test(normalized)
|
|
55
57
|
|| /(?:^|\/)(?:restart-requested|opencode-managed\.pid)$/i.test(normalized);
|
|
56
58
|
}
|
|
57
59
|
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SynaBun — the desktop app's server
|
|
3
|
+
*
|
|
4
|
+
* The Electron window of a packaged build (packaging/desktop/) does not decide
|
|
5
|
+
* anything about the server itself: it runs `<entry> app-backend --json`
|
|
6
|
+
* (packaging/runtime/bootstrap.mjs), which comes here. This module finds the
|
|
7
|
+
* server the window should show, and starts one through the same Start Server
|
|
8
|
+
* launcher an npm install uses (lib/start-launcher.js runLauncher: the launch
|
|
9
|
+
* lock, the supervisor record, the health check), so two starts never race.
|
|
10
|
+
*
|
|
11
|
+
* reuse a server of this install (same entry, same version) answers:
|
|
12
|
+
* on the port the window used last, where this data home's
|
|
13
|
+
* supervisor says it listens, or on the configured port
|
|
14
|
+
* replace a server of this install from before an update answers: it is
|
|
15
|
+
* asked to shut down, and the new version starts in its place
|
|
16
|
+
* refuse another installation of SynaBun answers on the port with this
|
|
17
|
+
* same data home (or cannot say which one it uses): it is never
|
|
18
|
+
* stopped or adopted, and the window says so
|
|
19
|
+
* move the port is taken by something else (a program, a SynaBun on
|
|
20
|
+
* another data home): this install starts on a free port
|
|
21
|
+
*
|
|
22
|
+
* Every step is reported as an event ({ type: 'plan' | 'existing' | 'step' |
|
|
23
|
+
* 'supervisor' | 'ready' | 'conflict' | 'failed', ... }); the window shows them.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { createHash } from 'node:crypto';
|
|
27
|
+
import { realpathSync, readFileSync } from 'node:fs';
|
|
28
|
+
import { join, resolve } from 'node:path';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Which data home a server uses, without saying where it is: the first 16 hex
|
|
32
|
+
* characters of a SHA-256 of its real path (lower case on Windows, where case
|
|
33
|
+
* does not tell paths apart). /api/health reports it, so the app can tell a
|
|
34
|
+
* second copy on the same data from one on another.
|
|
35
|
+
*/
|
|
36
|
+
export function dataHomeId(dataHome, platform = process.platform) {
|
|
37
|
+
let path = resolve(String(dataHome || ''));
|
|
38
|
+
try { path = realpathSync.native(path); } catch {}
|
|
39
|
+
if (platform === 'win32') path = path.toLowerCase();
|
|
40
|
+
return createHash('sha256').update(path).digest('hex').slice(0, 16);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function samePath(a, b, platform) {
|
|
44
|
+
const norm = (value) => {
|
|
45
|
+
let path = String(value || '').replace(/\\/g, '/');
|
|
46
|
+
try { path = realpathSync.native(path).replace(/\\/g, '/'); } catch {}
|
|
47
|
+
return platform === 'win32' ? path.toLowerCase() : path;
|
|
48
|
+
};
|
|
49
|
+
return !!a && !!b && norm(a) === norm(b);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* What answers on a port, for this install:
|
|
54
|
+
* 'none' nothing answers
|
|
55
|
+
* 'other' something answers that is not a SynaBun server
|
|
56
|
+
* 'ours' this install, this version
|
|
57
|
+
* 'ours-stale' this install, another version (from before an update)
|
|
58
|
+
* 'foreign' another installation, on another data home
|
|
59
|
+
* 'foreign-same-data' another installation on this data home, or one that does not say
|
|
60
|
+
*/
|
|
61
|
+
export function classifyServer(health, { entry, packageRoot, version, dataHomeId: ours, platform = process.platform, answered = true } = {}) {
|
|
62
|
+
if (!health) return { kind: answered ? 'other' : 'none' };
|
|
63
|
+
if (health.ok !== true || health.storage !== 'sqlite' || typeof health.projectDir !== 'string') return { kind: 'other', health };
|
|
64
|
+
// This install: the entry it runs from (installEntry; `entry` is only the path that outlives the run, empty for a
|
|
65
|
+
// copy on its disk image or a translocated one), or the very same application folder. An AppImage's folder is a
|
|
66
|
+
// new mount every run, and its entry (the image file) is what matches.
|
|
67
|
+
const theirEntry = health.installEntry || health.entry;
|
|
68
|
+
if ((entry && theirEntry && samePath(theirEntry, entry, platform)) || (packageRoot && samePath(health.projectDir, packageRoot, platform))) {
|
|
69
|
+
return { kind: String(health.version || '') === String(version) ? 'ours' : 'ours-stale', health };
|
|
70
|
+
}
|
|
71
|
+
if (health.dataHomeId && health.dataHomeId !== ours) return { kind: 'foreign', health };
|
|
72
|
+
// Said so (the same data home), or said nothing (a server from before 2026-10-17 reports no data home).
|
|
73
|
+
return { kind: 'foreign-same-data', health, known: !!health.dataHomeId };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A server that answers its health check completely: ready for the window. */
|
|
77
|
+
export function serverReady(health) {
|
|
78
|
+
return !!health && health.ok === true && health.storage === 'sqlite' && typeof health.projectDir === 'string' && !!health.startLauncher;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function defaultHealth(port, { fetchImpl = globalThis.fetch, timeoutMs = 1500 } = {}) {
|
|
82
|
+
try {
|
|
83
|
+
const response = await fetchImpl(`http://127.0.0.1:${port}/api/health`, { signal: AbortSignal.timeout(timeoutMs), headers: { accept: 'application/json' } });
|
|
84
|
+
if (!response.ok) return { status: response.status };
|
|
85
|
+
return await response.json();
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function readJson(path) {
|
|
92
|
+
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Find or start the server for the desktop app. Resolves with the final event
|
|
97
|
+
* ('ready', 'conflict' or 'failed'); `emit` receives every event on the way.
|
|
98
|
+
* Everything that touches the system is a parameter, for the tests.
|
|
99
|
+
*/
|
|
100
|
+
export async function runAppBackend({
|
|
101
|
+
env = process.env,
|
|
102
|
+
runtime,
|
|
103
|
+
packageRoot,
|
|
104
|
+
dataHome,
|
|
105
|
+
preferPort = null,
|
|
106
|
+
platform = process.platform,
|
|
107
|
+
emit = () => {},
|
|
108
|
+
launcher, // lib/start-launcher.js (runLauncher, resolvePort, portAccepts, launcherStatePaths, pidAlive)
|
|
109
|
+
backgroundCommand, // lib/desktop-pwa.js: the background start of a packaged build
|
|
110
|
+
spawn,
|
|
111
|
+
health = defaultHealth,
|
|
112
|
+
shutdown = null,
|
|
113
|
+
sleep = (ms) => new Promise(done => setTimeout(done, ms)),
|
|
114
|
+
now = () => Date.now(),
|
|
115
|
+
readyWaitMs = 5 * 60 * 1000,
|
|
116
|
+
searchSpan = 20,
|
|
117
|
+
} = {}) {
|
|
118
|
+
const ours = { entry: runtime?.entry || null, packageRoot: packageRoot || runtime?.packageRoot || null, version: runtime?.version || '', dataHomeId: dataHomeId(dataHome, platform), platform };
|
|
119
|
+
const { runLauncher, resolvePort, portAccepts, launcherStatePaths, pidAlive } = launcher;
|
|
120
|
+
const configured = resolvePort({ env, dataHome });
|
|
121
|
+
const paths = launcherStatePaths(dataHome);
|
|
122
|
+
const supervisorRecord = () => readJson(paths.supervisor);
|
|
123
|
+
const look = async (port) => {
|
|
124
|
+
const answer = await health(port);
|
|
125
|
+
if (answer && answer.ok === undefined && answer.status) return classifyServer(null, { ...ours, answered: true });
|
|
126
|
+
if (answer) return classifyServer(answer, ours);
|
|
127
|
+
return classifyServer(null, { ...ours, answered: await portAccepts(port).catch(() => false) });
|
|
128
|
+
};
|
|
129
|
+
const ready = (port, extra) => {
|
|
130
|
+
const record = supervisorRecord();
|
|
131
|
+
return { type: 'ready', port, supervisorPid: record && !record.endedAt && pidAlive(record.pid) ? record.pid : null, ...extra };
|
|
132
|
+
};
|
|
133
|
+
const conflict = (port, found) => ({ type: 'conflict', port, projectDir: found.health?.projectDir || null, install: found.health?.install || null, version: found.health?.version || null });
|
|
134
|
+
|
|
135
|
+
emit({ type: 'plan', dataHome, port: configured, preferredPort: configured, dataHomeId: ours.dataHomeId });
|
|
136
|
+
|
|
137
|
+
// This install's server from before an update: its files are gone or replaced. It goes; the new one starts.
|
|
138
|
+
const stopStale = async (stalePort, staleVersion) => {
|
|
139
|
+
const asked = shutdown ? await shutdown(stalePort) : false;
|
|
140
|
+
const deadline = now() + 20000;
|
|
141
|
+
while (await portAccepts(stalePort).catch(() => false)) {
|
|
142
|
+
if (now() >= deadline) return { type: 'failed', code: 'STALE_SERVER', message: `An older SynaBun server of this copy (v${staleVersion}) still runs on port ${stalePort} and did not stop${asked ? '' : ' when asked'}.`, log: paths.log };
|
|
143
|
+
await sleep(300);
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// 1. A server of this install that is already up: where the window was last,
|
|
149
|
+
// or where this data home's supervisor says its server listens. Another
|
|
150
|
+
// installation on this data home there is never a reason to start a second one.
|
|
151
|
+
const record = supervisorRecord();
|
|
152
|
+
const recordPort = record && !record.endedAt && pidAlive(record.pid) && record.phase === 'listening' ? Number(record.detail?.port) || null : null;
|
|
153
|
+
for (const earlier of [...new Set([preferPort, recordPort].filter(value => value && value !== configured))]) {
|
|
154
|
+
const seen = await look(earlier);
|
|
155
|
+
if (seen.kind === 'none') continue;
|
|
156
|
+
emit({ type: 'existing', port: earlier, kind: seen.kind, version: seen.health?.version || null });
|
|
157
|
+
if (seen.kind === 'ours') return finish(ready(earlier, { reused: true, started: false, version: seen.health.version }));
|
|
158
|
+
// Another installation on this data home, or one too old to say which data home it uses (every server from
|
|
159
|
+
// before 2026-10-17): the window is told where it runs. Starting this install beside it could mean two
|
|
160
|
+
// servers on one data home.
|
|
161
|
+
if (seen.kind === 'foreign-same-data') return finish(conflict(earlier, seen));
|
|
162
|
+
if (seen.kind === 'ours-stale') {
|
|
163
|
+
const failed = await stopStale(earlier, seen.health.version);
|
|
164
|
+
if (failed) return finish(failed);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 2. The configured port.
|
|
169
|
+
let port = configured;
|
|
170
|
+
let alternative = false;
|
|
171
|
+
const found = await look(configured);
|
|
172
|
+
if (found.kind !== 'none') emit({ type: 'existing', port: configured, kind: found.kind, version: found.health?.version || null });
|
|
173
|
+
if (found.kind === 'ours') return finish(ready(configured, { reused: true, started: false, version: found.health.version }));
|
|
174
|
+
if (found.kind === 'foreign-same-data') return finish(conflict(configured, found));
|
|
175
|
+
if (found.kind === 'ours-stale') {
|
|
176
|
+
const failed = await stopStale(configured, found.health.version);
|
|
177
|
+
if (failed) return finish(failed);
|
|
178
|
+
} else if (found.kind === 'foreign' || found.kind === 'other') {
|
|
179
|
+
const free = await freePort([preferPort, ...Array.from({ length: searchSpan }, (_, i) => configured + 1 + i)]);
|
|
180
|
+
if (!free) return finish({ type: 'failed', code: 'NO_PORT', message: `Port ${configured} is in use by another program, and no free port was found near it.`, log: paths.log });
|
|
181
|
+
port = free;
|
|
182
|
+
alternative = true;
|
|
183
|
+
emit({ type: 'plan', dataHome, port, preferredPort: configured, alternativePort: true, dataHomeId: ours.dataHomeId });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function freePort(candidates) {
|
|
187
|
+
for (const candidate of [...new Set(candidates.filter(value => Number.isInteger(value) && value > 0 && value < 65536))]) {
|
|
188
|
+
if (!(await portAccepts(candidate).catch(() => true))) return candidate;
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 3. Start, through the launcher (lock, supervisor record, background start).
|
|
194
|
+
let launcherPid = null;
|
|
195
|
+
let lastPhase = null;
|
|
196
|
+
const reportPhase = (current) => {
|
|
197
|
+
if (!current?.phase || current.phase === lastPhase) return;
|
|
198
|
+
lastPhase = current.phase;
|
|
199
|
+
emit({ type: 'supervisor', phase: current.phase, detail: current.detail || null });
|
|
200
|
+
};
|
|
201
|
+
const reporter = () => ({
|
|
202
|
+
get listening() { return false; },
|
|
203
|
+
listen: async () => false,
|
|
204
|
+
step: (id) => emit({ type: 'step', id: String(id) }),
|
|
205
|
+
supervisor: reportPhase,
|
|
206
|
+
finish: () => {},
|
|
207
|
+
linger: async () => {},
|
|
208
|
+
close: () => {},
|
|
209
|
+
});
|
|
210
|
+
const result = await runLauncher({
|
|
211
|
+
argv: ['--port', String(port)],
|
|
212
|
+
env: { ...env, NEURAL_PORT: String(port) },
|
|
213
|
+
packageRoot,
|
|
214
|
+
background: true,
|
|
215
|
+
beacon: reporter,
|
|
216
|
+
spawnImpl: (file, args, options) => {
|
|
217
|
+
const command = backgroundCommand(runtime, options.env);
|
|
218
|
+
const child = spawn(command.file, command.args, { ...options, env: command.env, cwd: command.cwd || options.cwd });
|
|
219
|
+
launcherPid = child.pid || null;
|
|
220
|
+
return child;
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
if (result.outcome === 'failed') {
|
|
224
|
+
const failure = supervisorRecord();
|
|
225
|
+
return finish({ type: 'failed', code: failure?.error?.code || 'START_FAILED', message: failure?.phase === 'failed' && failure.error?.message ? failure.error.message : 'SynaBun\'s server could not be started.', log: paths.log });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 4. Until it answers completely (a first start downloads the automation browser and backs up the data).
|
|
229
|
+
const deadline = now() + readyWaitMs;
|
|
230
|
+
for (;;) {
|
|
231
|
+
const answer = await health(port);
|
|
232
|
+
if (serverReady(answer)) {
|
|
233
|
+
const kind = classifyServer(answer, ours).kind;
|
|
234
|
+
if (kind !== 'ours') return finish(kind === 'foreign-same-data' ? conflict(port, { health: answer }) : { type: 'failed', code: 'NOT_OURS', message: `Another program took port ${port} while SynaBun started.`, log: paths.log });
|
|
235
|
+
return finish(ready(port, { reused: result.outcome !== 'started', started: result.outcome === 'started', launcherPid, version: answer.version, alternativePort: alternative, preferredPort: configured }));
|
|
236
|
+
}
|
|
237
|
+
const current = supervisorRecord();
|
|
238
|
+
reportPhase(current);
|
|
239
|
+
if (current?.phase === 'failed' && current.error) return finish({ type: 'failed', code: current.error.code || 'START_FAILED', message: current.error.message, log: paths.log });
|
|
240
|
+
if (now() >= deadline) return finish({ type: 'failed', code: 'NO_ANSWER', message: `SynaBun's server did not answer within ${Math.round(readyWaitMs / 60000)} minutes.`, log: paths.log });
|
|
241
|
+
await sleep(400);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function finish(event) {
|
|
245
|
+
emit(event);
|
|
246
|
+
return event;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** POST /api/server/shutdown on the loopback address: the server's own graceful stop. */
|
|
251
|
+
export async function requestServerShutdown(port, { fetchImpl = globalThis.fetch } = {}) {
|
|
252
|
+
try {
|
|
253
|
+
const response = await fetchImpl(`http://127.0.0.1:${port}/api/server/shutdown`, {
|
|
254
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}', signal: AbortSignal.timeout(3000),
|
|
255
|
+
});
|
|
256
|
+
return response.ok;
|
|
257
|
+
} catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Where the launcher writes its log for a data home (what "Open logs" shows). */
|
|
263
|
+
export function launcherLogOf(dataHome) {
|
|
264
|
+
return join(dataHome, 'data', 'launcher.log');
|
|
265
|
+
}
|
package/lib/external-tools.js
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
* SynaBun — external tools stay external
|
|
3
3
|
*
|
|
4
4
|
* Claude Code, Codex, OpenCode and Gemini CLI are programs of their own. The
|
|
5
|
-
* user installs them; SynaBun finds what is installed (
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* user installs them; SynaBun finds what is installed (Settings, PATH and the
|
|
6
|
+
* OS PATH, the folders of every install method:
|
|
7
|
+
* neural-interface/lib/cli-resolver.js) and talks to it, through a thin
|
|
8
|
+
* JavaScript SDK where there is one. It never ships or runs a copy of its own.
|
|
9
|
+
* When one is missing, the side panels can run its vendor's official installer
|
|
10
|
+
* on the user's click (neural-interface/lib/cli-install.js), which installs it
|
|
11
|
+
* where that installer always does, as the user's own.
|
|
8
12
|
*
|
|
9
13
|
* npm has no way to say "this SDK, without the program it carries": the Claude
|
|
10
14
|
* Agent SDK brings the Claude Code executable as a per-platform optional
|