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
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
//
|
|
11
11
|
// resolveClaudeLauncher what a terminal or a shell would start: the
|
|
12
12
|
// user's configured command, else the first
|
|
13
|
-
// `claude` on PATH that is not inside SynaBun
|
|
13
|
+
// `claude` on PATH that is not inside SynaBun.
|
|
14
|
+
// server.js asks lib/cli-resolver.js instead
|
|
15
|
+
// (Settings, PATH and the OS PATH, the known
|
|
16
|
+
// install folders, a --version that answers),
|
|
17
|
+
// which follows a Windows `claude.cmd` with
|
|
18
|
+
// windowsClaudeTargets below
|
|
14
19
|
// resolveClaudeSdkExecutable what the SDK can start. It spawns with
|
|
15
20
|
// shell:false, so an npm command file on Windows
|
|
16
21
|
// is followed to the executable behind it
|
|
@@ -14,7 +14,7 @@ import { spawn } from 'node:child_process';
|
|
|
14
14
|
import { dirname, join, sep } from 'node:path';
|
|
15
15
|
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
16
16
|
import { createRequire } from 'node:module';
|
|
17
|
-
import { getAugmentedPath } from './augmented-path.js';
|
|
17
|
+
import { withPathEnv, getAugmentedPath } from './augmented-path.js';
|
|
18
18
|
|
|
19
19
|
// Effort ladder the Claude Code CLI accepts for --effort. Models advertise their
|
|
20
20
|
// own subset via supportedEffortLevels; this is the superset used as a fallback
|
|
@@ -240,7 +240,7 @@ export function queryClaudeCliModels(claudeBin, { cwd, timeoutMs = DISCOVERY_TIM
|
|
|
240
240
|
try {
|
|
241
241
|
child = spawnImpl(shellCommand(bin, useShell), args, {
|
|
242
242
|
cwd: cwd || process.cwd(),
|
|
243
|
-
env: { ...process.env,
|
|
243
|
+
env: { ...withPathEnv(process.env, getAugmentedPath()), CLAUDE_CODE_ENTRYPOINT: 'sdk-cli' },
|
|
244
244
|
stdio: ['pipe', 'pipe', 'ignore'],
|
|
245
245
|
shell: useShell,
|
|
246
246
|
});
|
|
@@ -400,7 +400,7 @@ export function queryClaudeCliVersion(claudeBin, { timeoutMs = 10000 } = {}) {
|
|
|
400
400
|
|
|
401
401
|
try {
|
|
402
402
|
child = spawn(shellCommand(bin, useShell), args, {
|
|
403
|
-
env:
|
|
403
|
+
env: withPathEnv(process.env, getAugmentedPath()),
|
|
404
404
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
405
405
|
shell: useShell,
|
|
406
406
|
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Claude owns OAuth credentials and refresh. SynaBun owns only pasted keys
|
|
2
|
+
// in a separate private profile file; this adapter exposes safe auth status.
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { verifyClaudeApiKey } from './claude-profile-api-key.js';
|
|
9
|
+
import { validateProviderApiKey, redactProviderSecrets } from './provider-panel-api-key-auth.js';
|
|
10
|
+
|
|
11
|
+
export function claudeAuthEnv(env, accountEnv) {
|
|
12
|
+
const clean = { ...env };
|
|
13
|
+
for (const key of Object.keys(clean)) {
|
|
14
|
+
if (/^(CLAUDECODE|CLAUDE_CONFIG_DIR|CLAUDE_CODE_OAUTH_TOKEN|CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR|ANTHROPIC_AUTH_TOKEN|ANTHROPIC_API_KEY|ANTHROPIC_API_KEY_FILE_DESCRIPTOR|CLAUDE_CODE_USE_BEDROCK|CLAUDE_CODE_USE_VERTEX|CLAUDE_CODE_USE_FOUNDRY)$/.test(key)) delete clean[key];
|
|
15
|
+
}
|
|
16
|
+
return { ...clean, ...accountEnv, NO_COLOR: '1', FORCE_COLOR: '0' };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function claudeAuthorizationUrl(text) {
|
|
20
|
+
for (const match of text.matchAll(/https:\/\/[^\s<>"\x1b]+/g)) {
|
|
21
|
+
try {
|
|
22
|
+
const url = new URL(match[0]);
|
|
23
|
+
if (['claude.ai', 'console.anthropic.com', 'platform.claude.com'].includes(url.hostname)
|
|
24
|
+
&& !url.username && !url.password && url.searchParams.has('client_id') && url.searchParams.has('code_challenge')) return url.href;
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createClaudePanelAuth({ accounts, executable, env = () => process.env, spawnProcess = spawn, timeoutMs = 10 * 60_000, statusTimeoutMs = 15_000, verifyKey = verifyClaudeApiKey, changeCredentials = async (_id, change) => change() } = {}) {
|
|
31
|
+
const operations = new Map();
|
|
32
|
+
function account(id = 'default') {
|
|
33
|
+
if (typeof id !== 'string' || !/^[a-z0-9-]{1,64}$/.test(id)) throw Object.assign(new Error('Unknown Claude account'), { status: 404 });
|
|
34
|
+
const row = accounts.profileFor ? accounts.profileFor(id) : accounts.get(id);
|
|
35
|
+
if (!row || row.id !== id) throw Object.assign(new Error('Unknown Claude account'), { status: 404 });
|
|
36
|
+
return row;
|
|
37
|
+
}
|
|
38
|
+
function launch(id, args, extraEnv = {}) {
|
|
39
|
+
account(id);
|
|
40
|
+
const found = executable();
|
|
41
|
+
if (!found?.path) throw new Error(found?.reason || 'Claude CLI not found. Install Claude Code to sign in.');
|
|
42
|
+
const script = /\.[cm]?js$/i.test(found.path);
|
|
43
|
+
const proc = spawnProcess(script ? process.execPath : found.path, script ? [found.path, ...args] : args, {
|
|
44
|
+
env: { ...claudeAuthEnv(env(), accounts.envFor(id)), ...extraEnv }, stdio: ['pipe', 'pipe', 'pipe'], shell: false, windowsHide: true,
|
|
45
|
+
});
|
|
46
|
+
proc.stdin?.on('error', () => {}); // A native exit can race a manual-code write.
|
|
47
|
+
return proc;
|
|
48
|
+
}
|
|
49
|
+
// Claude Code's own answer about the account's login (never a credential).
|
|
50
|
+
function nativeStatus(id = 'default') {
|
|
51
|
+
account(id);
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
let proc;
|
|
54
|
+
try { proc = launch(id, ['auth', 'status', '--json']); } catch (error) { reject(error); return; }
|
|
55
|
+
let output = '', settled = false;
|
|
56
|
+
const finish = (error, result) => { if (settled) return; settled = true; clearTimeout(timer); error ? reject(error) : resolve(result); };
|
|
57
|
+
const timer = setTimeout(() => { proc.kill(); finish(new Error('Claude authentication status timed out. Try Refresh.')); }, statusTimeoutMs);
|
|
58
|
+
proc.stdout.on('data', chunk => { output = (output + chunk).slice(-65536); });
|
|
59
|
+
proc.stderr.on('data', () => {}); // Never forward CLI output or credential-bearing errors.
|
|
60
|
+
proc.on('error', () => finish(new Error('Could not start Claude Code. Check its installation.')));
|
|
61
|
+
proc.on('close', () => {
|
|
62
|
+
try {
|
|
63
|
+
const data = JSON.parse(output);
|
|
64
|
+
if (typeof data.loggedIn !== 'boolean') throw new Error();
|
|
65
|
+
finish(null, {
|
|
66
|
+
loggedIn: data.loggedIn, email: redactProviderSecrets(data.email || null), orgName: redactProviderSecrets(data.orgName || null),
|
|
67
|
+
authMethod: redactProviderSecrets(data.authMethod || null), subscriptionType: redactProviderSecrets(data.subscriptionType || null),
|
|
68
|
+
});
|
|
69
|
+
} catch { finish(new Error('Could not read Claude authentication. Update Claude Code to a version supporting auth status --json.')); }
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// The account as every surface shows it (lib/claude-accounts.js describe()),
|
|
74
|
+
// after asking Claude Code about its subscription login. A failed check of an
|
|
75
|
+
// account that still has a usable API key is not an error: the key is used.
|
|
76
|
+
async function read(id = 'default') {
|
|
77
|
+
account(id);
|
|
78
|
+
// An account running on its API key is signed in whatever its subscription
|
|
79
|
+
// says: no process is started for the answer. The subscription half is
|
|
80
|
+
// asked in the background, so the account list can show it later.
|
|
81
|
+
const current = accounts.describeForClient?.(id);
|
|
82
|
+
if (current?.mode === 'apiKey' && current.signedIn === true) {
|
|
83
|
+
Promise.resolve(accounts.refreshStatus?.([id])).catch(() => {});
|
|
84
|
+
return { accountId: id, account: current, state: snapshot(id) };
|
|
85
|
+
}
|
|
86
|
+
let status = null, failure = null;
|
|
87
|
+
try { status = await nativeStatus(id); } catch (error) { failure = error; }
|
|
88
|
+
account(id); // A profile removed meanwhile is gone, whatever the CLI said.
|
|
89
|
+
if (status) accounts.noteNativeStatus?.(id, status);
|
|
90
|
+
const row = accounts.describeForClient ? accounts.describeForClient(id) : null;
|
|
91
|
+
if (failure && !(row?.mode === 'apiKey' && row.signedIn)) throw failure;
|
|
92
|
+
return { accountId: id, account: row || legacyAccount(id, status), state: snapshot(id) };
|
|
93
|
+
}
|
|
94
|
+
// A host without the account model (an older caller, a test double).
|
|
95
|
+
function legacyAccount(id, status) {
|
|
96
|
+
if (accounts.readApiKey?.(id)) return { loggedIn: true, email: null, authMethod: 'apiKey', subscriptionType: null };
|
|
97
|
+
return { loggedIn: status.loggedIn, email: status.email, authMethod: status.authMethod, subscriptionType: status.subscriptionType };
|
|
98
|
+
}
|
|
99
|
+
const keyPending = new Set();
|
|
100
|
+
async function signInApiKey(id, key) {
|
|
101
|
+
account(id); key = validateProviderApiKey('claude', key);
|
|
102
|
+
if (keyPending.has(id) || ['starting', 'pending'].includes(snapshot(id).status)) throw Object.assign(new Error('Sign-in already pending'), { status: 409 });
|
|
103
|
+
keyPending.add(id);
|
|
104
|
+
try {
|
|
105
|
+
await verifyKey(key);
|
|
106
|
+
account(id); // A removed profile must never be recreated by a late reply.
|
|
107
|
+
await changeCredentials(id, () => { account(id); accounts.saveApiKey(id, key); });
|
|
108
|
+
operations.set(id, { status: 'success', authUrl: '' });
|
|
109
|
+
} finally { keyPending.delete(id); key = null; }
|
|
110
|
+
}
|
|
111
|
+
// `credential`: 'apiKey' removes the key SynaBun keeps, 'subscription' runs
|
|
112
|
+
// Claude Code's own logout in the account's config directory, 'all' both.
|
|
113
|
+
async function signOut(id, credential = 'all') {
|
|
114
|
+
account(id);
|
|
115
|
+
if (!['all', 'apiKey', 'subscription'].includes(credential)) throw Object.assign(new Error('Choose which sign-in to remove.'), { status: 400 });
|
|
116
|
+
if (keyPending.has(id)) throw Object.assign(new Error('Sign-in already pending'), { status: 409 });
|
|
117
|
+
const op = operations.get(id);
|
|
118
|
+
if (op && ['starting', 'pending'].includes(op.status) && credential !== 'apiKey') cancel(id, op.operationId);
|
|
119
|
+
keyPending.add(id);
|
|
120
|
+
try {
|
|
121
|
+
await changeCredentials(id, async () => {
|
|
122
|
+
if (credential !== 'subscription') accounts.clearApiKey(id);
|
|
123
|
+
if (credential === 'apiKey') return;
|
|
124
|
+
operations.delete(id);
|
|
125
|
+
await new Promise((resolve, reject) => {
|
|
126
|
+
let proc, timer;
|
|
127
|
+
try { proc = launch(id, ['auth', 'logout']); } catch { reject(new Error('Could not sign out of Claude. Try again.')); return; }
|
|
128
|
+
let settled = false;
|
|
129
|
+
const finish = ok => { if (settled) return; settled = true; clearTimeout(timer); ok ? resolve() : reject(new Error('Could not sign out of Claude. Try again.')); };
|
|
130
|
+
proc.stdout.on('data', () => {}); proc.stderr.on('data', () => {});
|
|
131
|
+
proc.on('error', () => finish(false)); proc.on('close', code => finish(code === 0));
|
|
132
|
+
timer = setTimeout(() => { proc.kill(); finish(false); }, statusTimeoutMs);
|
|
133
|
+
});
|
|
134
|
+
accounts.noteNativeStatus?.(id, { loggedIn: false });
|
|
135
|
+
});
|
|
136
|
+
} finally { keyPending.delete(id); }
|
|
137
|
+
}
|
|
138
|
+
// What the account's sessions use from now on; its idle panel processes end
|
|
139
|
+
// so the next message starts under the new credential.
|
|
140
|
+
async function setMode(id, mode) {
|
|
141
|
+
account(id);
|
|
142
|
+
if (keyPending.has(id) || ['starting', 'pending'].includes(snapshot(id).status)) throw Object.assign(new Error('Finish sign-in first.'), { status: 409 });
|
|
143
|
+
return changeCredentials(id, () => accounts.setMode(id, mode));
|
|
144
|
+
}
|
|
145
|
+
function snapshot(id) {
|
|
146
|
+
const op = operations.get(id);
|
|
147
|
+
return op ? { accountId: id, operationId: op.operationId, status: op.status, authUrl: op.authUrl, error: op.error || '' } : { accountId: id, status: 'idle' };
|
|
148
|
+
}
|
|
149
|
+
function start(id = 'default') {
|
|
150
|
+
account(id);
|
|
151
|
+
if (keyPending.has(id)) throw Object.assign(new Error('Sign-in already pending'), { status: 409 });
|
|
152
|
+
const existing = operations.get(id);
|
|
153
|
+
if (existing && ['starting', 'pending'].includes(existing.status)) return snapshot(id);
|
|
154
|
+
// BROWSER is a supported Claude Code handoff. Suppress its OS browser launch:
|
|
155
|
+
// the panel opens the URL printed by the native command instead.
|
|
156
|
+
const dir = mkdtempSync(join(tmpdir(), 'synabun-claude-oauth-'));
|
|
157
|
+
const browser = join(dir, process.platform === 'win32' ? 'browser.cmd' : 'browser');
|
|
158
|
+
writeFileSync(browser, process.platform === 'win32' ? '@exit /b 0\r\n' : '#!/bin/sh\nexit 0\n', { mode: 0o700 });
|
|
159
|
+
const op = { operationId: randomUUID(), status: 'starting', authUrl: '', process: null };
|
|
160
|
+
operations.set(id, op);
|
|
161
|
+
let timer, forceTimer, output = '';
|
|
162
|
+
const cleanup = () => { clearTimeout(timer); clearTimeout(forceTimer); rmSync(dir, { recursive: true, force: true }); };
|
|
163
|
+
const finish = (status, error = '') => {
|
|
164
|
+
if (!['starting', 'pending'].includes(op.status)) return;
|
|
165
|
+
op.status = status; op.error = error; op.authUrl = '';
|
|
166
|
+
clearTimeout(timer);
|
|
167
|
+
};
|
|
168
|
+
op.stop = (status, error) => {
|
|
169
|
+
finish(status, error);
|
|
170
|
+
op.process?.kill();
|
|
171
|
+
forceTimer = setTimeout(() => op.process?.kill('SIGKILL'), 2000); forceTimer.unref?.();
|
|
172
|
+
};
|
|
173
|
+
try {
|
|
174
|
+
op.process = launch(id, ['auth', 'login', '--claudeai'], { BROWSER: browser });
|
|
175
|
+
const collect = chunk => {
|
|
176
|
+
output = (output + chunk).slice(-65536);
|
|
177
|
+
if (['starting', 'pending'].includes(op.status)) {
|
|
178
|
+
const url = claudeAuthorizationUrl(output.slice(0, output.lastIndexOf('\n') + 1));
|
|
179
|
+
if (url) { op.authUrl = url; op.status = 'pending'; }
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
op.process.stdout.on('data', collect); op.process.stderr.on('data', collect);
|
|
183
|
+
op.process.on('error', () => { finish('error', 'Could not start Claude Code. Check its installation.'); cleanup(); });
|
|
184
|
+
op.process.on('close', async code => {
|
|
185
|
+
cleanup();
|
|
186
|
+
if (!['starting', 'pending'].includes(op.status)) return;
|
|
187
|
+
if (code !== 0) { finish('error', 'Claude sign-in failed. Retry, or update Claude Code if auth login is unavailable.'); return; }
|
|
188
|
+
try {
|
|
189
|
+
const status = await nativeStatus(id);
|
|
190
|
+
if (status.loggedIn) {
|
|
191
|
+
// A subscription just signed into is what the account uses from now on.
|
|
192
|
+
if (accounts.noteSubscriptionSignIn) accounts.noteSubscriptionSignIn(id, status);
|
|
193
|
+
else accounts.recordIdentity?.(id);
|
|
194
|
+
finish('success');
|
|
195
|
+
} else finish('error', 'Claude Code finished without confirming sign-in. Try again.');
|
|
196
|
+
} catch (error) { finish('error', error.message); }
|
|
197
|
+
});
|
|
198
|
+
timer = setTimeout(() => op.stop('error', 'Claude sign-in timed out. Try again.'), timeoutMs); timer.unref?.();
|
|
199
|
+
} catch (error) { finish('error', error.message); cleanup(); }
|
|
200
|
+
return snapshot(id);
|
|
201
|
+
}
|
|
202
|
+
function cancel(id, operationId) {
|
|
203
|
+
account(id);
|
|
204
|
+
const op = operations.get(id);
|
|
205
|
+
if (!op || op.operationId !== operationId) throw Object.assign(new Error('This sign-in attempt is no longer active. Refresh its status.'), { status: 409 });
|
|
206
|
+
if (['starting', 'pending'].includes(op.status)) op.stop('cancelled', '');
|
|
207
|
+
return snapshot(id);
|
|
208
|
+
}
|
|
209
|
+
function submitCode(id, operationId, code) {
|
|
210
|
+
account(id);
|
|
211
|
+
const op = operations.get(id);
|
|
212
|
+
if (!op || op.operationId !== operationId || op.status !== 'pending') throw Object.assign(new Error('This sign-in attempt is no longer active. Refresh its status.'), { status: 409 });
|
|
213
|
+
if (typeof code !== 'string' || !code.trim() || code.length > 4096 || /[\r\n\0]/.test(code)) throw Object.assign(new Error('Paste a single sign-in code from Claude.'), { status: 400 });
|
|
214
|
+
if (!op.process?.stdin?.writable) throw new Error('Claude Code is no longer accepting a sign-in code. Retry sign-in.');
|
|
215
|
+
op.process.stdin.write(`${code.trim()}\n`, error => {
|
|
216
|
+
if (error && op.status === 'pending') op.stop('error', 'Claude Code stopped accepting the sign-in code. Retry sign-in.');
|
|
217
|
+
});
|
|
218
|
+
return snapshot(id);
|
|
219
|
+
}
|
|
220
|
+
return { start, read, nativeStatus, cancel, submitCode, snapshot, signInApiKey, signOut, setMode, close: () => { for (const [id, op] of operations) if (['starting', 'pending'].includes(op.status)) cancel(id, op.operationId); } };
|
|
221
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// SynaBun's own file, never Claude's native credential store. Atomic 0600 writes
|
|
2
|
+
// work on every supported platform without a Keychain prompt or service naming.
|
|
3
|
+
import { constants, openSync, closeSync, fstatSync, readFileSync, writeFileSync, mkdirSync, renameSync, rmSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { validateProviderApiKey, providerApiKeyError } from './provider-panel-api-key-auth.js';
|
|
7
|
+
export const CLAUDE_API_KEY_FILE = '.synabun-api-key';
|
|
8
|
+
// The same file holds an account's key for either provider (Claude: an
|
|
9
|
+
// Anthropic key in its CLAUDE_CONFIG_DIR; Codex: an OpenAI key in its
|
|
10
|
+
// CODEX_HOME, lib/codex-accounts.js), checked against that provider's format.
|
|
11
|
+
export function readProfileApiKey(home, provider = 'claude') {
|
|
12
|
+
let fd;
|
|
13
|
+
try {
|
|
14
|
+
fd = openSync(join(home, CLAUDE_API_KEY_FILE), constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
|
|
15
|
+
const stat = fstatSync(fd);
|
|
16
|
+
if (!stat.isFile() || (process.platform !== 'win32' && (stat.mode & 0o077))) throw new Error('Unsafe credential permissions');
|
|
17
|
+
return validateProviderApiKey(provider, readFileSync(fd, 'utf8'));
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (error.code === 'ENOENT') return null;
|
|
20
|
+
throw new Error('Could not read the saved API key. Sign in again.');
|
|
21
|
+
} finally { if (fd !== undefined) closeSync(fd); }
|
|
22
|
+
}
|
|
23
|
+
export function readClaudeProfileApiKey(home) { return readProfileApiKey(home, 'claude'); }
|
|
24
|
+
export function saveProfileApiKey(home, key, provider = 'claude') {
|
|
25
|
+
key = validateProviderApiKey(provider, key);
|
|
26
|
+
mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
27
|
+
const target = join(home, CLAUDE_API_KEY_FILE), temp = `${target}.${randomUUID()}.tmp`;
|
|
28
|
+
try { writeFileSync(temp, key, { mode: 0o600, flag: 'wx' }); renameSync(temp, target); }
|
|
29
|
+
finally { rmSync(temp, { force: true }); }
|
|
30
|
+
}
|
|
31
|
+
export function saveClaudeProfileApiKey(home, key) { saveProfileApiKey(home, key, 'claude'); }
|
|
32
|
+
export function clearProfileApiKey(home) { rmSync(join(home, CLAUDE_API_KEY_FILE), { force: true }); }
|
|
33
|
+
export function clearClaudeProfileApiKey(home) { clearProfileApiKey(home); }
|
|
34
|
+
export async function verifyClaudeApiKey(key, fetchRequest = fetch) {
|
|
35
|
+
let response;
|
|
36
|
+
try {
|
|
37
|
+
response = await fetchRequest('https://api.anthropic.com/v1/models?limit=1', {
|
|
38
|
+
headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' }, signal: AbortSignal.timeout(15000), redirect: 'error',
|
|
39
|
+
});
|
|
40
|
+
} catch { throw providerApiKeyError('verification_unreachable'); }
|
|
41
|
+
finally { key = null; }
|
|
42
|
+
// Discard the body without letting a cleanup failure mask the HTTP result.
|
|
43
|
+
// Neither provider response bodies nor exception text are surfaced or logged.
|
|
44
|
+
try { await response.body?.cancel(); } catch {}
|
|
45
|
+
if ([401, 403].includes(response.status)) throw providerApiKeyError('key_rejected');
|
|
46
|
+
if (!response.ok) throw providerApiKeyError('verification_unavailable');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// OpenAI's read-only model list answers whether a key is accepted. The key goes
|
|
50
|
+
// in the Authorization header only; the body and any error text are dropped.
|
|
51
|
+
export async function verifyOpenAiApiKey(key, fetchRequest = fetch) {
|
|
52
|
+
let response;
|
|
53
|
+
try {
|
|
54
|
+
response = await fetchRequest('https://api.openai.com/v1/models', {
|
|
55
|
+
headers: { authorization: `Bearer ${key}` }, signal: AbortSignal.timeout(15000), redirect: 'error',
|
|
56
|
+
});
|
|
57
|
+
} catch { throw providerApiKeyError('verification_unreachable', 'codex'); }
|
|
58
|
+
finally { key = null; }
|
|
59
|
+
try { await response.body?.cancel(); } catch {}
|
|
60
|
+
if ([401, 403].includes(response.status)) throw providerApiKeyError('key_rejected', 'codex');
|
|
61
|
+
if (!response.ok) throw providerApiKeyError('verification_unavailable', 'codex');
|
|
62
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun — /api/cli/install: the side panels' Install button
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// GET /api/cli/install { ok, jobs: { [tool]: { state, step, code, error, log, … } } }
|
|
6
|
+
// POST /api/cli/install/:tool NDJSON: start, step, progress, log … done (lib/cli-install.js)
|
|
7
|
+
// GET /api/cli/git-bash Git Bash for Claude Code on Windows (lib/git-bash/), and
|
|
8
|
+
// POST /api/cli/git-bash/install|recheck its download and a new look (createGitBashApi below)
|
|
9
|
+
//
|
|
10
|
+
// Installing software is the owner's own act on this computer, so these routes
|
|
11
|
+
// take the guards of SynaBun's other local mutating endpoints (lib/http-guards.js,
|
|
12
|
+
// as /api/whatsapp): a loopback socket and no proxy header, a localhost Host,
|
|
13
|
+
// no invite guest; a POST also its own Origin, a JSON body, X-SynaBun-UI: 1 and
|
|
14
|
+
// no agent header. (`/api/cli` is admin-only in lib/request-gate.js too.) A
|
|
15
|
+
// request names a tool and nothing else: what runs is fixed by
|
|
16
|
+
// lib/cli-install.js for this system.
|
|
17
|
+
//
|
|
18
|
+
// Refusals before anything starts are JSON: 400 UNKNOWN_TOOL, 409 BUSY (that
|
|
19
|
+
// tool is being installed), 422 UNSUPPORTED (no one-click method here). A tab
|
|
20
|
+
// that goes away does not stop an install; its result is in GET and in the next
|
|
21
|
+
// /api/system/tool-versions.
|
|
22
|
+
|
|
23
|
+
import { Router } from 'express';
|
|
24
|
+
import { guestForbidden, noStore, requireLocal, requireLocalHost, requireSameOriginJson, sendGuardError, uiOnly } from './http-guards.js';
|
|
25
|
+
import { openNdjsonStream } from './whatsapp/api.js';
|
|
26
|
+
|
|
27
|
+
export const INSTALLABLE_TOOLS = Object.freeze(['claude-code', 'codex', 'opencode']);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {object} deps
|
|
31
|
+
* @param {ReturnType<import('./cli-install.js').createCliInstaller>} deps.installer
|
|
32
|
+
* @param {(req) => boolean} [deps.isGuestRequest]
|
|
33
|
+
* @param {number|(() => number)} [deps.port] the port the page is served from (Host / Origin checks)
|
|
34
|
+
*/
|
|
35
|
+
function localCliRouter({ isGuestRequest = () => false, port = 3344 } = {}) {
|
|
36
|
+
const router = Router();
|
|
37
|
+
const portOf = () => Number(typeof port === 'function' ? port() : port);
|
|
38
|
+
const built = new Map();
|
|
39
|
+
const perPort = (name, factory) => (req, res, next) => {
|
|
40
|
+
const key = `${name}:${portOf()}`;
|
|
41
|
+
let guard = built.get(key);
|
|
42
|
+
if (!guard) {
|
|
43
|
+
try { guard = factory(portOf()); } catch { return sendGuardError(res, 500, 'BAD_PORT', 'The install API has no valid port.'); }
|
|
44
|
+
built.set(key, guard);
|
|
45
|
+
}
|
|
46
|
+
return guard(req, res, next);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
router.use(noStore, guestForbidden(isGuestRequest), requireLocal, perPort('host', requireLocalHost));
|
|
50
|
+
const mutating = [perPort('origin', requireSameOriginJson), uiOnly];
|
|
51
|
+
return { router, mutating };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createCliInstallApi({ installer, isGuestRequest = () => false, port = 3344, log = () => {} } = {}) {
|
|
55
|
+
if (!installer) throw new Error('createCliInstallApi needs the installer');
|
|
56
|
+
const { router, mutating } = localCliRouter({ isGuestRequest, port });
|
|
57
|
+
|
|
58
|
+
router.get('/', (req, res) => {
|
|
59
|
+
res.json({ ok: true, jobs: installer.jobs() });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
router.post('/:tool', ...mutating, (req, res) => {
|
|
63
|
+
const tool = String(req.params.tool || '');
|
|
64
|
+
if (!INSTALLABLE_TOOLS.includes(tool)) {
|
|
65
|
+
return res.status(400).json({ ok: false, code: 'UNKNOWN_TOOL', error: `Unknown CLI: ${tool.slice(0, 40)}` });
|
|
66
|
+
}
|
|
67
|
+
let stream = null;
|
|
68
|
+
const onEvent = (event) => {
|
|
69
|
+
if (!stream) stream = openNdjsonStream(res);
|
|
70
|
+
stream.write(event);
|
|
71
|
+
if (event?.type === 'done') stream.end();
|
|
72
|
+
};
|
|
73
|
+
let started;
|
|
74
|
+
try {
|
|
75
|
+
started = installer.start(tool, { onEvent });
|
|
76
|
+
} catch (error) {
|
|
77
|
+
const code = error?.code === 'busy' ? 'BUSY' : error?.code === 'unsupported' ? 'UNSUPPORTED' : 'INTERNAL';
|
|
78
|
+
if (code === 'INTERNAL') log(`[cli-install] ${tool} could not start: ${error?.message || error}`);
|
|
79
|
+
if (stream) return stream.end({ type: 'done', ok: false, code: 'internal', error: String(error?.message || error) });
|
|
80
|
+
return res.status(code === 'BUSY' ? 409 : code === 'UNSUPPORTED' ? 422 : 500).json({ ok: false, code, error: String(error?.message || error) });
|
|
81
|
+
}
|
|
82
|
+
if (!stream) stream = openNdjsonStream(res);
|
|
83
|
+
stream.onAbort(() => started.unsubscribe());
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return router;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* /api/cli/git-bash: the Git Bash every Claude Code SynaBun starts gets on Windows (lib/git-bash/manager.js),
|
|
91
|
+
* beside the CLIs' install and PATH, with the same guards.
|
|
92
|
+
*
|
|
93
|
+
* GET / { ok, state: unsupported | system | ready | missing | downloading | failed, … }
|
|
94
|
+
* POST /install start the download of SynaBun's copy now (Settings: Download, Try again); answers at once,
|
|
95
|
+
* the page follows it with GET. 422 UNSUPPORTED where there is no Windows Git to fetch
|
|
96
|
+
* POST /recheck look again (a Git for Windows installed meanwhile), then the status
|
|
97
|
+
*
|
|
98
|
+
* A request names nothing: what is downloaded is the pinned release (lib/git-bash/pins.json), and a body is refused.
|
|
99
|
+
*/
|
|
100
|
+
export function createGitBashApi({ manager, onRecheck = async () => {}, isGuestRequest, port } = {}) {
|
|
101
|
+
if (!manager) throw new Error('createGitBashApi needs the Git Bash manager');
|
|
102
|
+
const { router, mutating } = localCliRouter({ isGuestRequest, port });
|
|
103
|
+
const noBody = (req, res, next) => (Object.keys(req.body || {}).length ? res.status(400).json({ ok: false, code: 'UNEXPECTED_BODY' }) : next());
|
|
104
|
+
router.get('/', (req, res) => { res.json({ ok: true, ...manager.status() }); });
|
|
105
|
+
router.post('/install', ...mutating, noBody, (req, res) => {
|
|
106
|
+
const before = manager.status();
|
|
107
|
+
if (before.state === 'unsupported') return res.status(422).json({ ok: false, code: 'UNSUPPORTED', ...before });
|
|
108
|
+
manager.ensure({ reason: 'settings' });
|
|
109
|
+
res.json({ ok: true, ...manager.status() });
|
|
110
|
+
});
|
|
111
|
+
router.post('/recheck', ...mutating, noBody, async (req, res) => {
|
|
112
|
+
manager.refresh();
|
|
113
|
+
try { await onRecheck(); } catch { /* the status says what is known */ }
|
|
114
|
+
res.json({ ok: true, ...manager.status() });
|
|
115
|
+
});
|
|
116
|
+
return router;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The same owner/localhost/Origin/UI guards as Install. Never accepts a folder. */
|
|
120
|
+
export function createCliPathApi({ pathManager, resolver, diagnostics = async () => ({}), isGuestRequest, port } = {}) {
|
|
121
|
+
const { router, mutating } = localCliRouter({ isGuestRequest, port });
|
|
122
|
+
router.get('/', async (req, res) => {
|
|
123
|
+
try { res.json({ ok: true, ...await diagnostics() }); }
|
|
124
|
+
catch (error) { res.status(500).json({ ok: false, error: error.message }); }
|
|
125
|
+
});
|
|
126
|
+
router.post('/:tool', ...mutating, async (req, res) => {
|
|
127
|
+
if (!INSTALLABLE_TOOLS.includes(req.params.tool)) return res.status(400).json({ ok: false, code: 'UNKNOWN_TOOL' });
|
|
128
|
+
if (Object.keys(req.body || {}).length) return res.status(400).json({ ok: false, code: 'UNEXPECTED_BODY' });
|
|
129
|
+
try {
|
|
130
|
+
const status = await resolver.resolve(req.params.tool, { force: true });
|
|
131
|
+
const result = await pathManager.add(status);
|
|
132
|
+
if (!result.changed) return res.status(409).json({ ok: false, code: 'NOT_NEEDED', ...result });
|
|
133
|
+
res.json({ ok: true, ...result });
|
|
134
|
+
} catch (error) { res.status(error.status || 500).json({ ok: false, error: error.message }); }
|
|
135
|
+
});
|
|
136
|
+
return router;
|
|
137
|
+
}
|