bincode-cli 1.0.1
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/AGENTS.md +27 -0
- package/README.md +15 -0
- package/bin/bincode +98 -0
- package/bunfig.toml +4 -0
- package/package.json +124 -0
- package/parsers-config.ts +239 -0
- package/script/build.ts +167 -0
- package/script/postinstall.mjs +206 -0
- package/script/publish.ts +99 -0
- package/script/schema.ts +47 -0
- package/src/acp/README.md +164 -0
- package/src/acp/agent.ts +1051 -0
- package/src/acp/session.ts +101 -0
- package/src/acp/types.ts +22 -0
- package/src/agent/agent.ts +398 -0
- package/src/agent/generate.txt +75 -0
- package/src/agent/prompt/compaction.txt +12 -0
- package/src/agent/prompt/explore.txt +18 -0
- package/src/agent/prompt/summary.txt +10 -0
- package/src/agent/prompt/title.txt +36 -0
- package/src/auth/bineric-login.ts +506 -0
- package/src/auth/index.ts +70 -0
- package/src/bun/index.ts +114 -0
- package/src/bus/bus-event.ts +43 -0
- package/src/bus/global.ts +10 -0
- package/src/bus/index.ts +105 -0
- package/src/cli/auth-check.ts +61 -0
- package/src/cli/bootstrap.ts +21 -0
- package/src/cli/cmd/acp.ts +88 -0
- package/src/cli/cmd/agent.ts +256 -0
- package/src/cli/cmd/auth.ts +436 -0
- package/src/cli/cmd/cmd.ts +7 -0
- package/src/cli/cmd/debug/config.ts +15 -0
- package/src/cli/cmd/debug/file.ts +91 -0
- package/src/cli/cmd/debug/index.ts +43 -0
- package/src/cli/cmd/debug/lsp.ts +48 -0
- package/src/cli/cmd/debug/ripgrep.ts +83 -0
- package/src/cli/cmd/debug/scrap.ts +15 -0
- package/src/cli/cmd/debug/skill.ts +15 -0
- package/src/cli/cmd/debug/snapshot.ts +48 -0
- package/src/cli/cmd/export.ts +88 -0
- package/src/cli/cmd/generate.ts +38 -0
- package/src/cli/cmd/github.ts +1399 -0
- package/src/cli/cmd/import.ts +98 -0
- package/src/cli/cmd/login.ts +112 -0
- package/src/cli/cmd/logout.ts +38 -0
- package/src/cli/cmd/mcp.ts +654 -0
- package/src/cli/cmd/models.ts +77 -0
- package/src/cli/cmd/pr.ts +112 -0
- package/src/cli/cmd/run.ts +368 -0
- package/src/cli/cmd/serve.ts +31 -0
- package/src/cli/cmd/session.ts +106 -0
- package/src/cli/cmd/stats.ts +298 -0
- package/src/cli/cmd/tui/app.tsx +669 -0
- package/src/cli/cmd/tui/attach.ts +30 -0
- package/src/cli/cmd/tui/component/border.tsx +21 -0
- package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-command.tsx +123 -0
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
- package/src/cli/cmd/tui/component/dialog-model.tsx +223 -0
- package/src/cli/cmd/tui/component/dialog-provider.tsx +224 -0
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +102 -0
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-status.tsx +162 -0
- package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
- package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
- package/src/cli/cmd/tui/component/logo.tsx +32 -0
- package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +560 -0
- package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
- package/src/cli/cmd/tui/component/prompt/index.tsx +1052 -0
- package/src/cli/cmd/tui/context/args.tsx +14 -0
- package/src/cli/cmd/tui/context/directory.ts +13 -0
- package/src/cli/cmd/tui/context/exit.tsx +23 -0
- package/src/cli/cmd/tui/context/helper.tsx +25 -0
- package/src/cli/cmd/tui/context/keybind.tsx +101 -0
- package/src/cli/cmd/tui/context/kv.tsx +49 -0
- package/src/cli/cmd/tui/context/local.tsx +339 -0
- package/src/cli/cmd/tui/context/prompt.tsx +18 -0
- package/src/cli/cmd/tui/context/route.tsx +46 -0
- package/src/cli/cmd/tui/context/sdk.tsx +74 -0
- package/src/cli/cmd/tui/context/sync.tsx +372 -0
- package/src/cli/cmd/tui/context/theme/aura.json +69 -0
- package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
- package/src/cli/cmd/tui/context/theme/bincode.json +245 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
- package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
- package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
- package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
- package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
- package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
- package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
- package/src/cli/cmd/tui/context/theme/github.json +233 -0
- package/src/cli/cmd/tui/context/theme/gruvbox.json +95 -0
- package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
- package/src/cli/cmd/tui/context/theme/lucent-orng.json +227 -0
- package/src/cli/cmd/tui/context/theme/material.json +235 -0
- package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
- package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
- package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
- package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
- package/src/cli/cmd/tui/context/theme/nord.json +223 -0
- package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
- package/src/cli/cmd/tui/context/theme/orng.json +245 -0
- package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
- package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
- package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
- package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
- package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
- package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
- package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
- package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
- package/src/cli/cmd/tui/context/theme.tsx +1109 -0
- package/src/cli/cmd/tui/event.ts +40 -0
- package/src/cli/cmd/tui/routes/home.tsx +105 -0
- package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +64 -0
- package/src/cli/cmd/tui/routes/session/dialog-message.tsx +109 -0
- package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
- package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
- package/src/cli/cmd/tui/routes/session/footer.tsx +88 -0
- package/src/cli/cmd/tui/routes/session/header.tsx +141 -0
- package/src/cli/cmd/tui/routes/session/index.tsx +1888 -0
- package/src/cli/cmd/tui/routes/session/sidebar.tsx +321 -0
- package/src/cli/cmd/tui/spawn.ts +60 -0
- package/src/cli/cmd/tui/thread.ts +120 -0
- package/src/cli/cmd/tui/ui/dialog-alert.tsx +57 -0
- package/src/cli/cmd/tui/ui/dialog-confirm.tsx +83 -0
- package/src/cli/cmd/tui/ui/dialog-help.tsx +38 -0
- package/src/cli/cmd/tui/ui/dialog-prompt.tsx +77 -0
- package/src/cli/cmd/tui/ui/dialog-select.tsx +330 -0
- package/src/cli/cmd/tui/ui/dialog.tsx +170 -0
- package/src/cli/cmd/tui/ui/spinner.ts +368 -0
- package/src/cli/cmd/tui/ui/toast.tsx +100 -0
- package/src/cli/cmd/tui/util/clipboard.ts +127 -0
- package/src/cli/cmd/tui/util/editor.ts +32 -0
- package/src/cli/cmd/tui/util/terminal.ts +114 -0
- package/src/cli/cmd/tui/worker.ts +63 -0
- package/src/cli/cmd/uninstall.ts +344 -0
- package/src/cli/cmd/upgrade.ts +67 -0
- package/src/cli/cmd/web.ts +84 -0
- package/src/cli/error.ts +55 -0
- package/src/cli/ui.ts +84 -0
- package/src/cli/upgrade.ts +25 -0
- package/src/command/index.ts +80 -0
- package/src/command/template/initialize.txt +10 -0
- package/src/command/template/review.txt +97 -0
- package/src/config/config.ts +995 -0
- package/src/config/markdown.ts +41 -0
- package/src/env/index.ts +26 -0
- package/src/file/ignore.ts +83 -0
- package/src/file/index.ts +328 -0
- package/src/file/ripgrep.ts +393 -0
- package/src/file/time.ts +64 -0
- package/src/file/watcher.ts +103 -0
- package/src/flag/flag.ts +46 -0
- package/src/format/formatter.ts +315 -0
- package/src/format/index.ts +137 -0
- package/src/global/index.ts +52 -0
- package/src/id/id.ts +73 -0
- package/src/ide/index.ts +76 -0
- package/src/index.ts +217 -0
- package/src/installation/index.ts +196 -0
- package/src/lsp/client.ts +229 -0
- package/src/lsp/index.ts +485 -0
- package/src/lsp/language.ts +116 -0
- package/src/lsp/server.ts +1895 -0
- package/src/mcp/auth.ts +135 -0
- package/src/mcp/index.ts +654 -0
- package/src/mcp/oauth-callback.ts +200 -0
- package/src/mcp/oauth-provider.ts +154 -0
- package/src/patch/index.ts +622 -0
- package/src/permission/index.ts +199 -0
- package/src/plugin/index.ts +101 -0
- package/src/project/bootstrap.ts +31 -0
- package/src/project/instance.ts +78 -0
- package/src/project/project.ts +221 -0
- package/src/project/state.ts +65 -0
- package/src/project/vcs.ts +76 -0
- package/src/provider/auth.ts +143 -0
- package/src/provider/models-macro.ts +11 -0
- package/src/provider/models.ts +106 -0
- package/src/provider/provider.ts +1071 -0
- package/src/provider/sdk/openai-compatible/src/README.md +5 -0
- package/src/provider/sdk/openai-compatible/src/index.ts +2 -0
- package/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts +101 -0
- package/src/provider/sdk/openai-compatible/src/responses/convert-to-openai-responses-input.ts +303 -0
- package/src/provider/sdk/openai-compatible/src/responses/map-openai-responses-finish-reason.ts +22 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-config.ts +18 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-error.ts +22 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-responses-api-types.ts +207 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +1713 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-responses-prepare-tools.ts +177 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-responses-settings.ts +1 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/code-interpreter.ts +88 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/file-search.ts +128 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/image-generation.ts +115 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/local-shell.ts +65 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/web-search-preview.ts +104 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/web-search.ts +103 -0
- package/src/provider/transform.ts +455 -0
- package/src/pty/index.ts +231 -0
- package/src/server/error.ts +36 -0
- package/src/server/project.ts +79 -0
- package/src/server/server.ts +2642 -0
- package/src/server/tui.ts +71 -0
- package/src/session/compaction.ts +223 -0
- package/src/session/index.ts +458 -0
- package/src/session/llm.ts +201 -0
- package/src/session/message-v2.ts +659 -0
- package/src/session/message.ts +189 -0
- package/src/session/processor.ts +409 -0
- package/src/session/prompt/anthropic-20250930.txt +166 -0
- package/src/session/prompt/anthropic.txt +104 -0
- package/src/session/prompt/anthropic_spoof.txt +1 -0
- package/src/session/prompt/beast.txt +147 -0
- package/src/session/prompt/build-switch.txt +5 -0
- package/src/session/prompt/codex.txt +318 -0
- package/src/session/prompt/copilot-gpt-5.txt +143 -0
- package/src/session/prompt/gemini.txt +155 -0
- package/src/session/prompt/max-steps.txt +16 -0
- package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
- package/src/session/prompt/plan.txt +26 -0
- package/src/session/prompt/polaris.txt +106 -0
- package/src/session/prompt/qwen.txt +109 -0
- package/src/session/prompt.ts +1446 -0
- package/src/session/retry.ts +86 -0
- package/src/session/revert.ts +108 -0
- package/src/session/status.ts +76 -0
- package/src/session/summary.ts +194 -0
- package/src/session/system.ts +120 -0
- package/src/session/todo.ts +37 -0
- package/src/share/share-next.ts +194 -0
- package/src/share/share.ts +87 -0
- package/src/shell/shell.ts +67 -0
- package/src/skill/index.ts +1 -0
- package/src/skill/skill.ts +83 -0
- package/src/snapshot/index.ts +197 -0
- package/src/storage/storage.ts +226 -0
- package/src/tool/bash.ts +306 -0
- package/src/tool/bash.txt +158 -0
- package/src/tool/batch.ts +175 -0
- package/src/tool/batch.txt +24 -0
- package/src/tool/codesearch.ts +138 -0
- package/src/tool/codesearch.txt +12 -0
- package/src/tool/edit.ts +675 -0
- package/src/tool/edit.txt +10 -0
- package/src/tool/glob.ts +65 -0
- package/src/tool/glob.txt +6 -0
- package/src/tool/grep.ts +121 -0
- package/src/tool/grep.txt +8 -0
- package/src/tool/invalid.ts +17 -0
- package/src/tool/ls.ts +110 -0
- package/src/tool/ls.txt +1 -0
- package/src/tool/lsp-diagnostics.ts +26 -0
- package/src/tool/lsp-diagnostics.txt +1 -0
- package/src/tool/lsp-hover.ts +31 -0
- package/src/tool/lsp-hover.txt +1 -0
- package/src/tool/lsp.ts +87 -0
- package/src/tool/lsp.txt +19 -0
- package/src/tool/multiedit.ts +46 -0
- package/src/tool/multiedit.txt +41 -0
- package/src/tool/patch.ts +233 -0
- package/src/tool/patch.txt +1 -0
- package/src/tool/read.ts +219 -0
- package/src/tool/read.txt +12 -0
- package/src/tool/registry.ts +162 -0
- package/src/tool/skill.ts +100 -0
- package/src/tool/task.ts +136 -0
- package/src/tool/task.txt +60 -0
- package/src/tool/todo.ts +39 -0
- package/src/tool/todoread.txt +14 -0
- package/src/tool/todowrite.txt +167 -0
- package/src/tool/tool.ts +71 -0
- package/src/tool/webfetch.ts +187 -0
- package/src/tool/webfetch.txt +13 -0
- package/src/tool/websearch.ts +150 -0
- package/src/tool/websearch.txt +11 -0
- package/src/tool/write.ts +110 -0
- package/src/tool/write.txt +8 -0
- package/src/util/archive.ts +16 -0
- package/src/util/color.ts +19 -0
- package/src/util/context.ts +25 -0
- package/src/util/defer.ts +12 -0
- package/src/util/eventloop.ts +20 -0
- package/src/util/filesystem.ts +83 -0
- package/src/util/fn.ts +11 -0
- package/src/util/iife.ts +3 -0
- package/src/util/keybind.ts +102 -0
- package/src/util/lazy.ts +11 -0
- package/src/util/locale.ts +81 -0
- package/src/util/lock.ts +98 -0
- package/src/util/log.ts +180 -0
- package/src/util/queue.ts +32 -0
- package/src/util/rpc.ts +42 -0
- package/src/util/scrap.ts +10 -0
- package/src/util/signal.ts +12 -0
- package/src/util/timeout.ts +14 -0
- package/src/util/token.ts +7 -0
- package/src/util/wildcard.ts +54 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
import { Log } from "../util/log"
|
|
2
|
+
import open from "open"
|
|
3
|
+
import { Filesystem } from "../util/filesystem"
|
|
4
|
+
import { Instance } from "../project/instance"
|
|
5
|
+
import * as prompts from "@clack/prompts"
|
|
6
|
+
import { UI } from "../cli/ui"
|
|
7
|
+
import path from "path"
|
|
8
|
+
import { Env } from "../env"
|
|
9
|
+
|
|
10
|
+
const log = Log.create({ service: "auth.bineric-login" })
|
|
11
|
+
|
|
12
|
+
const OAUTH_CALLBACK_PORT = 19877
|
|
13
|
+
const OAUTH_CALLBACK_PATH = "/auth/bineric/callback"
|
|
14
|
+
|
|
15
|
+
interface PendingAuth {
|
|
16
|
+
resolve: (data: { token: string }) => void
|
|
17
|
+
reject: (error: Error) => void
|
|
18
|
+
timeout: ReturnType<typeof setTimeout>
|
|
19
|
+
createdAt: number
|
|
20
|
+
isTimedOut?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export namespace BinericLogin {
|
|
24
|
+
let server: ReturnType<typeof Bun.serve> | undefined
|
|
25
|
+
const pendingAuths = new Map<string, PendingAuth>()
|
|
26
|
+
|
|
27
|
+
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes
|
|
28
|
+
const GRACE_PERIOD_MS = 3 * 60 * 1000 // 3 minutes grace period after timeout
|
|
29
|
+
|
|
30
|
+
// Minimal HTML that immediately tries to close the window
|
|
31
|
+
// Shows nothing visible - just a blank page that closes itself
|
|
32
|
+
const HTML_SUCCESS = `<!DOCTYPE html>
|
|
33
|
+
<html>
|
|
34
|
+
<head>
|
|
35
|
+
<title>Login Complete</title>
|
|
36
|
+
<meta charset="utf-8">
|
|
37
|
+
<style>
|
|
38
|
+
body {
|
|
39
|
+
margin: 0;
|
|
40
|
+
padding: 0;
|
|
41
|
+
background: #1a1a2e;
|
|
42
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
43
|
+
}
|
|
44
|
+
.msg {
|
|
45
|
+
display: none;
|
|
46
|
+
position: fixed;
|
|
47
|
+
top: 50%;
|
|
48
|
+
left: 50%;
|
|
49
|
+
transform: translate(-50%, -50%);
|
|
50
|
+
text-align: center;
|
|
51
|
+
color: #4ade80;
|
|
52
|
+
font-size: 1.2rem;
|
|
53
|
+
}
|
|
54
|
+
.msg.show { display: block; }
|
|
55
|
+
</style>
|
|
56
|
+
</head>
|
|
57
|
+
<body>
|
|
58
|
+
<div class="msg" id="msg">✓ Login successful. You can close this tab.</div>
|
|
59
|
+
<script>
|
|
60
|
+
(function() {
|
|
61
|
+
// Try to close immediately - multiple attempts
|
|
62
|
+
function tryClose() {
|
|
63
|
+
try { window.close(); } catch(e) {}
|
|
64
|
+
try { self.close(); } catch(e) {}
|
|
65
|
+
try { window.open('', '_self').close(); } catch(e) {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Immediate attempts
|
|
69
|
+
tryClose();
|
|
70
|
+
setTimeout(tryClose, 50);
|
|
71
|
+
setTimeout(tryClose, 100);
|
|
72
|
+
setTimeout(tryClose, 200);
|
|
73
|
+
setTimeout(tryClose, 500);
|
|
74
|
+
|
|
75
|
+
// After 600ms, if still open, show minimal message
|
|
76
|
+
setTimeout(function() {
|
|
77
|
+
document.getElementById('msg').classList.add('show');
|
|
78
|
+
}, 600);
|
|
79
|
+
})();
|
|
80
|
+
</script>
|
|
81
|
+
</body>
|
|
82
|
+
</html>`
|
|
83
|
+
|
|
84
|
+
const HTML_ERROR = (error: string) => `<!DOCTYPE html>
|
|
85
|
+
<html>
|
|
86
|
+
<head>
|
|
87
|
+
<title>Bineric - Login Failed</title>
|
|
88
|
+
<style>
|
|
89
|
+
body { font-family: system-ui, -apple-system, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a2e; color: #eee; }
|
|
90
|
+
.container { text-align: center; padding: 2rem; }
|
|
91
|
+
h1 { color: #f87171; margin-bottom: 1rem; }
|
|
92
|
+
p { color: #aaa; }
|
|
93
|
+
.error { color: #fca5a5; font-family: monospace; margin-top: 1rem; padding: 1rem; background: rgba(248,113,113,0.1); border-radius: 0.5rem; }
|
|
94
|
+
</style>
|
|
95
|
+
</head>
|
|
96
|
+
<body>
|
|
97
|
+
<div class="container">
|
|
98
|
+
<h1>Login Failed</h1>
|
|
99
|
+
<p>An error occurred during login.</p>
|
|
100
|
+
<div class="error">${error}</div>
|
|
101
|
+
</div>
|
|
102
|
+
</body>
|
|
103
|
+
</html>`
|
|
104
|
+
|
|
105
|
+
export async function ensureServer(): Promise<void> {
|
|
106
|
+
if (server) return
|
|
107
|
+
|
|
108
|
+
const running = await isPortInUse()
|
|
109
|
+
if (running) {
|
|
110
|
+
log.info("oauth callback server already running on another instance", { port: OAUTH_CALLBACK_PORT })
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
server = Bun.serve({
|
|
115
|
+
port: OAUTH_CALLBACK_PORT,
|
|
116
|
+
fetch(req) {
|
|
117
|
+
const url = new URL(req.url)
|
|
118
|
+
|
|
119
|
+
if (url.pathname !== OAUTH_CALLBACK_PATH) {
|
|
120
|
+
return new Response("Not found", { status: 404 })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const token = url.searchParams.get("token")
|
|
124
|
+
const state = url.searchParams.get("state")
|
|
125
|
+
const error = url.searchParams.get("error")
|
|
126
|
+
const errorDescription = url.searchParams.get("error_description")
|
|
127
|
+
|
|
128
|
+
log.info("received oauth callback", { hasToken: !!token, state, error })
|
|
129
|
+
|
|
130
|
+
// Enforce state parameter presence
|
|
131
|
+
if (!state) {
|
|
132
|
+
const errorMsg = "Missing required state parameter"
|
|
133
|
+
log.error("oauth callback missing state parameter", { url: url.toString() })
|
|
134
|
+
return new Response(HTML_ERROR(errorMsg), {
|
|
135
|
+
status: 400,
|
|
136
|
+
headers: { "Content-Type": "text/html" },
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (error) {
|
|
141
|
+
const errorMsg = errorDescription || error
|
|
142
|
+
if (pendingAuths.has(state)) {
|
|
143
|
+
const pending = pendingAuths.get(state)!
|
|
144
|
+
clearTimeout(pending.timeout)
|
|
145
|
+
pendingAuths.delete(state)
|
|
146
|
+
pending.reject(new Error(errorMsg))
|
|
147
|
+
}
|
|
148
|
+
return new Response(HTML_ERROR(errorMsg), {
|
|
149
|
+
headers: { "Content-Type": "text/html" },
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!token) {
|
|
154
|
+
return new Response(HTML_ERROR("Missing token"), {
|
|
155
|
+
status: 400,
|
|
156
|
+
headers: { "Content-Type": "text/html" },
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Validate state parameter
|
|
161
|
+
if (!pendingAuths.has(state)) {
|
|
162
|
+
const errorMsg = "Invalid or expired state parameter. Please run the login command again."
|
|
163
|
+
log.error("oauth callback with invalid state", { state, pendingStates: Array.from(pendingAuths.keys()) })
|
|
164
|
+
return new Response(HTML_ERROR(errorMsg), {
|
|
165
|
+
status: 400,
|
|
166
|
+
headers: { "Content-Type": "text/html" },
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const pending = pendingAuths.get(state)!
|
|
171
|
+
const now = Date.now()
|
|
172
|
+
const age = now - pending.createdAt
|
|
173
|
+
const isAfterTimeout = age > CALLBACK_TIMEOUT_MS
|
|
174
|
+
const isAfterGracePeriod = age > (CALLBACK_TIMEOUT_MS + GRACE_PERIOD_MS)
|
|
175
|
+
|
|
176
|
+
// If callback came after grace period, reject it
|
|
177
|
+
if (isAfterGracePeriod) {
|
|
178
|
+
const errorMsg = "Login session expired. Please run the login command again."
|
|
179
|
+
log.warn("oauth callback received after grace period", { state, age: Math.floor(age / 1000) + "s" })
|
|
180
|
+
pendingAuths.delete(state)
|
|
181
|
+
return new Response(HTML_ERROR(errorMsg), {
|
|
182
|
+
status: 400,
|
|
183
|
+
headers: { "Content-Type": "text/html" },
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// If callback came after timeout but within grace period, accept it with warning
|
|
188
|
+
if (isAfterTimeout) {
|
|
189
|
+
log.warn("oauth callback received after timeout but within grace period", {
|
|
190
|
+
state,
|
|
191
|
+
timeoutAge: Math.floor((age - CALLBACK_TIMEOUT_MS) / 1000) + "s"
|
|
192
|
+
})
|
|
193
|
+
// Still accept the token, but mark as timed out
|
|
194
|
+
pending.isTimedOut = true
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
clearTimeout(pending.timeout)
|
|
198
|
+
pendingAuths.delete(state)
|
|
199
|
+
|
|
200
|
+
// Try to resolve, but if promise already rejected (timeout), that's ok
|
|
201
|
+
// The token will still be returned and can be saved
|
|
202
|
+
try {
|
|
203
|
+
pending.resolve({ token })
|
|
204
|
+
} catch (e) {
|
|
205
|
+
// Promise already rejected (timeout), but we still have the token
|
|
206
|
+
// This is handled in the login function
|
|
207
|
+
log.info("callback received after timeout, token available for manual save", { state })
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return new Response(HTML_SUCCESS, {
|
|
211
|
+
headers: { "Content-Type": "text/html" },
|
|
212
|
+
})
|
|
213
|
+
},
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT })
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function waitForCallback(state: string): Promise<{ token: string }> {
|
|
220
|
+
return new Promise((resolve, reject) => {
|
|
221
|
+
const timeout = setTimeout(() => {
|
|
222
|
+
if (pendingAuths.has(state)) {
|
|
223
|
+
const pending = pendingAuths.get(state)!
|
|
224
|
+
// Mark as timed out but don't reject yet - wait for grace period
|
|
225
|
+
pending.isTimedOut = true
|
|
226
|
+
log.warn("login timeout reached, but keeping state for grace period", { state })
|
|
227
|
+
|
|
228
|
+
// Set another timeout for grace period
|
|
229
|
+
setTimeout(() => {
|
|
230
|
+
if (pendingAuths.has(state)) {
|
|
231
|
+
const pendingAfterGrace = pendingAuths.get(state)!
|
|
232
|
+
if (pendingAfterGrace.isTimedOut) {
|
|
233
|
+
// Grace period also expired, now reject
|
|
234
|
+
pendingAuths.delete(state)
|
|
235
|
+
clearTimeout(pendingAfterGrace.timeout)
|
|
236
|
+
pendingAfterGrace.reject(new Error("Login timeout - authorization took too long. Please run the login command again."))
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}, GRACE_PERIOD_MS)
|
|
240
|
+
}
|
|
241
|
+
}, CALLBACK_TIMEOUT_MS)
|
|
242
|
+
|
|
243
|
+
pendingAuths.set(state, {
|
|
244
|
+
resolve,
|
|
245
|
+
reject,
|
|
246
|
+
timeout,
|
|
247
|
+
createdAt: Date.now()
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function isPortInUse(): Promise<boolean> {
|
|
253
|
+
return new Promise((resolve) => {
|
|
254
|
+
Bun.connect({
|
|
255
|
+
hostname: "127.0.0.1",
|
|
256
|
+
port: OAUTH_CALLBACK_PORT,
|
|
257
|
+
socket: {
|
|
258
|
+
open(socket) {
|
|
259
|
+
socket.end()
|
|
260
|
+
resolve(true)
|
|
261
|
+
},
|
|
262
|
+
error() {
|
|
263
|
+
resolve(false)
|
|
264
|
+
},
|
|
265
|
+
data() {},
|
|
266
|
+
close() {},
|
|
267
|
+
},
|
|
268
|
+
}).catch(() => {
|
|
269
|
+
resolve(false)
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export async function stop(): Promise<void> {
|
|
275
|
+
if (server) {
|
|
276
|
+
server.stop()
|
|
277
|
+
server = undefined
|
|
278
|
+
log.info("oauth callback server stopped")
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for (const [state, pending] of pendingAuths) {
|
|
282
|
+
clearTimeout(pending.timeout)
|
|
283
|
+
pending.reject(new Error("OAuth callback server stopped"))
|
|
284
|
+
}
|
|
285
|
+
pendingAuths.clear()
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Check if Bineric provider has a token (CLI token) in bincode.json
|
|
290
|
+
*/
|
|
291
|
+
export async function hasApiKey(): Promise<boolean> {
|
|
292
|
+
try {
|
|
293
|
+
const configFiles = ["bincode.jsonc", "bincode.json"]
|
|
294
|
+
for (const file of configFiles) {
|
|
295
|
+
const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
|
|
296
|
+
for (const configPath of found) {
|
|
297
|
+
const config = await Bun.file(configPath).json().catch(() => null)
|
|
298
|
+
// Check for token (preferred) or apiKey (legacy)
|
|
299
|
+
const token = config?.provider?.Bineric?.options?.token
|
|
300
|
+
const apiKey = config?.provider?.Bineric?.options?.apiKey
|
|
301
|
+
|
|
302
|
+
if (token && token.length > 10) {
|
|
303
|
+
return true
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Legacy: check apiKey if token not found
|
|
307
|
+
if (apiKey && apiKey !== "not-needed" && apiKey.length > 10) {
|
|
308
|
+
return true
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return false
|
|
313
|
+
} catch (error) {
|
|
314
|
+
log.error("error checking token/api key", { error })
|
|
315
|
+
return false
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get the base URL from environment variable, config file, or default
|
|
321
|
+
* Priority: BINERIC_API_URL env var > bincode.json config > default
|
|
322
|
+
*/
|
|
323
|
+
export async function getBaseUrl(): Promise<string> {
|
|
324
|
+
// Check environment variable first
|
|
325
|
+
const envUrl = Env.get("BINERIC_API_URL")
|
|
326
|
+
if (envUrl && envUrl.trim()) {
|
|
327
|
+
return envUrl.trim()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const configFiles = ["bincode.jsonc", "bincode.json"]
|
|
332
|
+
for (const file of configFiles) {
|
|
333
|
+
const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
|
|
334
|
+
for (const configPath of found) {
|
|
335
|
+
const config = await Bun.file(configPath).json().catch(() => null)
|
|
336
|
+
const configUrl = config?.provider?.Bineric?.options?.baseURL
|
|
337
|
+
if (configUrl && configUrl.trim()) {
|
|
338
|
+
return configUrl.trim()
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// No default - must be set via env var or config
|
|
343
|
+
throw new Error("BINERIC_API_URL environment variable or baseURL in bincode.json must be set")
|
|
344
|
+
} catch (error) {
|
|
345
|
+
log.error("error getting base url", { error })
|
|
346
|
+
throw new Error("BINERIC_API_URL environment variable or baseURL in bincode.json must be set")
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export async function updateConfig(token: string): Promise<void> {
|
|
351
|
+
try {
|
|
352
|
+
const configFiles = ["bincode.jsonc", "bincode.json"]
|
|
353
|
+
let configPath: string | null = null
|
|
354
|
+
let config: any = null
|
|
355
|
+
|
|
356
|
+
// Find the config file
|
|
357
|
+
for (const file of configFiles) {
|
|
358
|
+
const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
|
|
359
|
+
if (found.length > 0) {
|
|
360
|
+
configPath = found[0]
|
|
361
|
+
config = await Bun.file(configPath).json().catch(() => null)
|
|
362
|
+
if (config) break
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Get baseURL from env var - required
|
|
367
|
+
const defaultBaseURL = Env.get("BINERIC_API_URL")
|
|
368
|
+
if (!defaultBaseURL || !defaultBaseURL.trim()) {
|
|
369
|
+
throw new Error("BINERIC_API_URL environment variable must be set")
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// If no config found, create one in the current directory
|
|
373
|
+
if (!config) {
|
|
374
|
+
configPath = path.join(Instance.directory, "bincode.json")
|
|
375
|
+
config = {
|
|
376
|
+
$schema: "https://bincode.ai/config.json",
|
|
377
|
+
enabled_providers: ["Bineric"],
|
|
378
|
+
provider: {
|
|
379
|
+
Bineric: {
|
|
380
|
+
name: "Bineric",
|
|
381
|
+
npm: "@ai-sdk/openai-compatible",
|
|
382
|
+
options: {
|
|
383
|
+
baseURL: defaultBaseURL,
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Update the config
|
|
391
|
+
if (!config.provider) {
|
|
392
|
+
config.provider = {}
|
|
393
|
+
}
|
|
394
|
+
if (!config.provider.Bineric) {
|
|
395
|
+
config.provider.Bineric = {
|
|
396
|
+
name: "Bineric",
|
|
397
|
+
npm: "@ai-sdk/openai-compatible",
|
|
398
|
+
options: {
|
|
399
|
+
baseURL: defaultBaseURL,
|
|
400
|
+
},
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (!config.provider.Bineric.options) {
|
|
404
|
+
config.provider.Bineric.options = {}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Store token (contains API key and email)
|
|
408
|
+
config.provider.Bineric.options.token = token
|
|
409
|
+
|
|
410
|
+
await Bun.write(configPath!, JSON.stringify(config, null, 2))
|
|
411
|
+
log.info("updated config with token", { configPath })
|
|
412
|
+
} catch (error) {
|
|
413
|
+
log.error("error updating config", { error })
|
|
414
|
+
throw error
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export async function removeToken(): Promise<void> {
|
|
419
|
+
try {
|
|
420
|
+
const configFiles = ["bincode.jsonc", "bincode.json"]
|
|
421
|
+
let configPath: string | null = null
|
|
422
|
+
let config: any = null
|
|
423
|
+
|
|
424
|
+
for (const file of configFiles) {
|
|
425
|
+
const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
|
|
426
|
+
if (found.length > 0) {
|
|
427
|
+
configPath = found[0]
|
|
428
|
+
config = await Bun.file(configPath).json().catch(() => null)
|
|
429
|
+
if (config) break
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (!config || !config.provider?.Bineric?.options) {
|
|
434
|
+
return
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
delete config.provider.Bineric.options.token
|
|
438
|
+
delete config.provider.Bineric.options.email
|
|
439
|
+
delete config.provider.Bineric.options.apiKey
|
|
440
|
+
|
|
441
|
+
await Bun.write(configPath!, JSON.stringify(config, null, 2))
|
|
442
|
+
log.info("removed token from config", { configPath })
|
|
443
|
+
} catch (error) {
|
|
444
|
+
log.error("error removing token", { error })
|
|
445
|
+
throw error
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export async function login(baseUrl?: string): Promise<{ token: string }> {
|
|
450
|
+
// Get frontend URL from env var - required
|
|
451
|
+
const apiWebUrl = Env.get("BINERIC_FRONTEND_URL") || Env.get("NEXT_PUBLIC_APIWEB_URL")
|
|
452
|
+
if (!apiWebUrl || !apiWebUrl.trim()) {
|
|
453
|
+
throw new Error("BINERIC_FRONTEND_URL environment variable must be set for Bineric login")
|
|
454
|
+
}
|
|
455
|
+
const state = crypto.randomUUID()
|
|
456
|
+
const callbackUrl = `http://127.0.0.1:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`
|
|
457
|
+
|
|
458
|
+
const loginUrl = new URL(`${apiWebUrl}/auth/cli-login`)
|
|
459
|
+
loginUrl.searchParams.set("redirect_uri", callbackUrl)
|
|
460
|
+
loginUrl.searchParams.set("state", state)
|
|
461
|
+
|
|
462
|
+
await ensureServer()
|
|
463
|
+
|
|
464
|
+
prompts.log.info("Opening browser for login...")
|
|
465
|
+
prompts.log.info(`Login URL: ${loginUrl.toString()}`)
|
|
466
|
+
prompts.log.info("If browser doesn't open automatically, copy the URL above and open it in your browser")
|
|
467
|
+
|
|
468
|
+
await open(loginUrl.toString())
|
|
469
|
+
|
|
470
|
+
const spinner = prompts.spinner()
|
|
471
|
+
spinner.start("Waiting for authorization...")
|
|
472
|
+
|
|
473
|
+
// Set up timeout warning
|
|
474
|
+
const timeoutWarning = setTimeout(() => {
|
|
475
|
+
if (pendingAuths.has(state)) {
|
|
476
|
+
spinner.message("Still waiting... You can complete login in browser (3 more minutes)")
|
|
477
|
+
}
|
|
478
|
+
}, CALLBACK_TIMEOUT_MS)
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
const { token } = await waitForCallback(state)
|
|
482
|
+
clearTimeout(timeoutWarning)
|
|
483
|
+
|
|
484
|
+
if (!token) {
|
|
485
|
+
throw new Error("No token received from login")
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
spinner.stop("Login successful!")
|
|
489
|
+
return { token }
|
|
490
|
+
} catch (error) {
|
|
491
|
+
clearTimeout(timeoutWarning)
|
|
492
|
+
spinner.stop("Login timeout", 1)
|
|
493
|
+
|
|
494
|
+
// Check if token was saved despite timeout (callback came in grace period)
|
|
495
|
+
const hasToken = await hasApiKey()
|
|
496
|
+
if (hasToken) {
|
|
497
|
+
prompts.log.success("Login completed after timeout. Token has been saved successfully!")
|
|
498
|
+
return { token: "saved" } // Return dummy token, actual token is already saved
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
prompts.log.warn("Login timeout. If you complete login in browser now, you may need to run the login command again.")
|
|
502
|
+
throw error
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { Global } from "../global"
|
|
3
|
+
import fs from "fs/promises"
|
|
4
|
+
import z from "zod"
|
|
5
|
+
|
|
6
|
+
export namespace Auth {
|
|
7
|
+
export const Oauth = z
|
|
8
|
+
.object({
|
|
9
|
+
type: z.literal("oauth"),
|
|
10
|
+
refresh: z.string(),
|
|
11
|
+
access: z.string(),
|
|
12
|
+
expires: z.number(),
|
|
13
|
+
enterpriseUrl: z.string().optional(),
|
|
14
|
+
})
|
|
15
|
+
.meta({ ref: "OAuth" })
|
|
16
|
+
|
|
17
|
+
export const Api = z
|
|
18
|
+
.object({
|
|
19
|
+
type: z.literal("api"),
|
|
20
|
+
key: z.string(),
|
|
21
|
+
})
|
|
22
|
+
.meta({ ref: "ApiAuth" })
|
|
23
|
+
|
|
24
|
+
export const WellKnown = z
|
|
25
|
+
.object({
|
|
26
|
+
type: z.literal("wellknown"),
|
|
27
|
+
key: z.string(),
|
|
28
|
+
token: z.string(),
|
|
29
|
+
})
|
|
30
|
+
.meta({ ref: "WellKnownAuth" })
|
|
31
|
+
|
|
32
|
+
export const Info = z.discriminatedUnion("type", [Oauth, Api, WellKnown]).meta({ ref: "Auth" })
|
|
33
|
+
export type Info = z.infer<typeof Info>
|
|
34
|
+
|
|
35
|
+
const filepath = path.join(Global.Path.data, "auth.json")
|
|
36
|
+
|
|
37
|
+
export async function get(providerID: string) {
|
|
38
|
+
const auth = await all()
|
|
39
|
+
return auth[providerID]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function all(): Promise<Record<string, Info>> {
|
|
43
|
+
const file = Bun.file(filepath)
|
|
44
|
+
const data = await file.json().catch(() => ({}) as Record<string, unknown>)
|
|
45
|
+
return Object.entries(data).reduce(
|
|
46
|
+
(acc, [key, value]) => {
|
|
47
|
+
const parsed = Info.safeParse(value)
|
|
48
|
+
if (!parsed.success) return acc
|
|
49
|
+
acc[key] = parsed.data
|
|
50
|
+
return acc
|
|
51
|
+
},
|
|
52
|
+
{} as Record<string, Info>,
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function set(key: string, info: Info) {
|
|
57
|
+
const file = Bun.file(filepath)
|
|
58
|
+
const data = await all()
|
|
59
|
+
await Bun.write(file, JSON.stringify({ ...data, [key]: info }, null, 2))
|
|
60
|
+
await fs.chmod(file.name!, 0o600)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function remove(key: string) {
|
|
64
|
+
const file = Bun.file(filepath)
|
|
65
|
+
const data = await all()
|
|
66
|
+
delete data[key]
|
|
67
|
+
await Bun.write(file, JSON.stringify(data, null, 2))
|
|
68
|
+
await fs.chmod(file.name!, 0o600)
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/bun/index.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import z from "zod"
|
|
2
|
+
import { Global } from "../global"
|
|
3
|
+
import { Log } from "../util/log"
|
|
4
|
+
import path from "path"
|
|
5
|
+
import { NamedError } from "@bincode-ai/util/error"
|
|
6
|
+
import { readableStreamToText } from "bun"
|
|
7
|
+
import { createRequire } from "module"
|
|
8
|
+
import { Lock } from "../util/lock"
|
|
9
|
+
|
|
10
|
+
export namespace BunProc {
|
|
11
|
+
const log = Log.create({ service: "bun" })
|
|
12
|
+
const req = createRequire(import.meta.url)
|
|
13
|
+
|
|
14
|
+
export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject<any, any, any>) {
|
|
15
|
+
log.info("running", {
|
|
16
|
+
cmd: [which(), ...cmd],
|
|
17
|
+
...options,
|
|
18
|
+
})
|
|
19
|
+
const result = Bun.spawn([which(), ...cmd], {
|
|
20
|
+
...options,
|
|
21
|
+
stdout: "pipe",
|
|
22
|
+
stderr: "pipe",
|
|
23
|
+
env: {
|
|
24
|
+
...process.env,
|
|
25
|
+
...options?.env,
|
|
26
|
+
BUN_BE_BUN: "1",
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
const code = await result.exited
|
|
30
|
+
const stdout = result.stdout
|
|
31
|
+
? typeof result.stdout === "number"
|
|
32
|
+
? result.stdout
|
|
33
|
+
: await readableStreamToText(result.stdout)
|
|
34
|
+
: undefined
|
|
35
|
+
const stderr = result.stderr
|
|
36
|
+
? typeof result.stderr === "number"
|
|
37
|
+
? result.stderr
|
|
38
|
+
: await readableStreamToText(result.stderr)
|
|
39
|
+
: undefined
|
|
40
|
+
log.info("done", {
|
|
41
|
+
code,
|
|
42
|
+
stdout,
|
|
43
|
+
stderr,
|
|
44
|
+
})
|
|
45
|
+
if (code !== 0) {
|
|
46
|
+
throw new Error(`Command failed with exit code ${result.exitCode}`)
|
|
47
|
+
}
|
|
48
|
+
return result
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function which() {
|
|
52
|
+
return process.execPath
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const InstallFailedError = NamedError.create(
|
|
56
|
+
"BunInstallFailedError",
|
|
57
|
+
z.object({
|
|
58
|
+
pkg: z.string(),
|
|
59
|
+
version: z.string(),
|
|
60
|
+
}),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
export async function install(pkg: string, version = "latest") {
|
|
64
|
+
// Use lock to ensure only one install at a time
|
|
65
|
+
using _ = await Lock.write("bun-install")
|
|
66
|
+
|
|
67
|
+
const mod = path.join(Global.Path.cache, "node_modules", pkg)
|
|
68
|
+
const pkgjson = Bun.file(path.join(Global.Path.cache, "package.json"))
|
|
69
|
+
const parsed = await pkgjson.json().catch(async () => {
|
|
70
|
+
const result = { dependencies: {} }
|
|
71
|
+
await Bun.write(pkgjson.name!, JSON.stringify(result, null, 2))
|
|
72
|
+
return result
|
|
73
|
+
})
|
|
74
|
+
if (parsed.dependencies[pkg] === version) return mod
|
|
75
|
+
|
|
76
|
+
// Build command arguments
|
|
77
|
+
const args = ["add", "--force", "--exact", "--cwd", Global.Path.cache, pkg + "@" + version]
|
|
78
|
+
|
|
79
|
+
// Let Bun handle registry resolution:
|
|
80
|
+
// - If .npmrc files exist, Bun will use them automatically
|
|
81
|
+
// - If no .npmrc files exist, Bun will default to https://registry.npmjs.org
|
|
82
|
+
// - No need to pass --registry flag
|
|
83
|
+
log.info("installing package using Bun's default registry resolution", {
|
|
84
|
+
pkg,
|
|
85
|
+
version,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
await BunProc.run(args, {
|
|
89
|
+
cwd: Global.Path.cache,
|
|
90
|
+
}).catch((e) => {
|
|
91
|
+
throw new InstallFailedError(
|
|
92
|
+
{ pkg, version },
|
|
93
|
+
{
|
|
94
|
+
cause: e,
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// Resolve actual version from installed package when using "latest"
|
|
100
|
+
// This ensures subsequent starts use the cached version until explicitly updated
|
|
101
|
+
let resolvedVersion = version
|
|
102
|
+
if (version === "latest") {
|
|
103
|
+
const installedPkgJson = Bun.file(path.join(mod, "package.json"))
|
|
104
|
+
const installedPkg = await installedPkgJson.json().catch(() => null)
|
|
105
|
+
if (installedPkg?.version) {
|
|
106
|
+
resolvedVersion = installedPkg.version
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
parsed.dependencies[pkg] = resolvedVersion
|
|
111
|
+
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2))
|
|
112
|
+
return mod
|
|
113
|
+
}
|
|
114
|
+
}
|