synabun 2.0.5 → 2.0.9
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 +78 -0
- package/README.md +2 -1
- package/THIRD-PARTY-LICENSES.md +13 -0
- package/docs/whatsapp.md +151 -16
- package/hooks/claude-code/post-plan.mjs +6 -30
- package/hooks/claude-code/session-start.mjs +1 -4
- package/hooks/claude-code/shared.mjs +112 -75
- 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/package-processes.js +670 -0
- package/lib/packaged-runtime.js +138 -11
- package/lib/runtime-dependencies.js +225 -0
- 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/browser-bench.d.ts +1 -1
- package/mcp-server/dist/services/browser-bench.d.ts.map +1 -1
- package/mcp-server/dist/services/browser-bench.js +1 -1
- package/mcp-server/dist/services/browser-bench.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/categories.d.ts +35 -2
- package/mcp-server/dist/services/categories.d.ts.map +1 -1
- package/mcp-server/dist/services/categories.js +77 -33
- package/mcp-server/dist/services/categories.js.map +1 -1
- 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/desktop-bench.d.ts +7 -7
- package/mcp-server/dist/services/desktop-bench.d.ts.map +1 -1
- package/mcp-server/dist/services/desktop-bench.js +1 -1
- package/mcp-server/dist/services/desktop-bench.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 +11 -8
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +35 -14
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/bluesky.js +1 -1
- package/mcp-server/dist/tools/bluesky.js.map +1 -1
- package/mcp-server/dist/tools/browser-advanced.d.ts +1 -1
- package/mcp-server/dist/tools/browser-advanced.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-advanced.js +1 -1
- package/mcp-server/dist/tools/browser-advanced.js.map +1 -1
- package/mcp-server/dist/tools/browser-batch.d.ts +1 -1
- package/mcp-server/dist/tools/browser-batch.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-batch.js +1 -1
- package/mcp-server/dist/tools/browser-batch.js.map +1 -1
- package/mcp-server/dist/tools/browser-cheatsheet.d.ts +1 -1
- package/mcp-server/dist/tools/browser-cheatsheet.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-cheatsheet.js +1 -1
- package/mcp-server/dist/tools/browser-cheatsheet.js.map +1 -1
- package/mcp-server/dist/tools/browser-interact.d.ts +1 -1
- package/mcp-server/dist/tools/browser-interact.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-interact.js +1 -1
- package/mcp-server/dist/tools/browser-interact.js.map +1 -1
- package/mcp-server/dist/tools/browser-navigate.d.ts +1 -1
- package/mcp-server/dist/tools/browser-navigate.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-navigate.js +1 -1
- package/mcp-server/dist/tools/browser-navigate.js.map +1 -1
- package/mcp-server/dist/tools/browser-observe.d.ts +1 -1
- package/mcp-server/dist/tools/browser-observe.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-observe.js +1 -1
- package/mcp-server/dist/tools/browser-observe.js.map +1 -1
- package/mcp-server/dist/tools/card-tools.d.ts +1 -1
- package/mcp-server/dist/tools/card-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/card-tools.js +1 -1
- package/mcp-server/dist/tools/card-tools.js.map +1 -1
- package/mcp-server/dist/tools/category.d.ts +1 -1
- package/mcp-server/dist/tools/category.d.ts.map +1 -1
- package/mcp-server/dist/tools/category.js +5 -1
- package/mcp-server/dist/tools/category.js.map +1 -1
- package/mcp-server/dist/tools/choice.d.ts +1 -1
- package/mcp-server/dist/tools/choice.d.ts.map +1 -1
- package/mcp-server/dist/tools/choice.js +1 -1
- package/mcp-server/dist/tools/choice.js.map +1 -1
- package/mcp-server/dist/tools/computer.d.ts +1 -1
- package/mcp-server/dist/tools/computer.d.ts.map +1 -1
- package/mcp-server/dist/tools/computer.js +1 -1
- package/mcp-server/dist/tools/computer.js.map +1 -1
- package/mcp-server/dist/tools/discord-channel.d.ts +1 -1
- package/mcp-server/dist/tools/discord-channel.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-channel.js +1 -1
- package/mcp-server/dist/tools/discord-channel.js.map +1 -1
- package/mcp-server/dist/tools/discord-guild.d.ts +1 -1
- package/mcp-server/dist/tools/discord-guild.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-guild.js +1 -1
- package/mcp-server/dist/tools/discord-guild.js.map +1 -1
- package/mcp-server/dist/tools/discord-member.d.ts +1 -1
- package/mcp-server/dist/tools/discord-member.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-member.js +1 -1
- package/mcp-server/dist/tools/discord-member.js.map +1 -1
- package/mcp-server/dist/tools/discord-message.d.ts +1 -1
- package/mcp-server/dist/tools/discord-message.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-message.js +1 -1
- package/mcp-server/dist/tools/discord-message.js.map +1 -1
- package/mcp-server/dist/tools/discord-onboarding.d.ts +1 -1
- package/mcp-server/dist/tools/discord-onboarding.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-onboarding.js +1 -1
- package/mcp-server/dist/tools/discord-onboarding.js.map +1 -1
- package/mcp-server/dist/tools/discord-role.d.ts +1 -1
- package/mcp-server/dist/tools/discord-role.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-role.js +1 -1
- package/mcp-server/dist/tools/discord-role.js.map +1 -1
- package/mcp-server/dist/tools/discord-thread.d.ts +1 -1
- package/mcp-server/dist/tools/discord-thread.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-thread.js +1 -1
- package/mcp-server/dist/tools/discord-thread.js.map +1 -1
- package/mcp-server/dist/tools/discord-webhook.d.ts +1 -1
- package/mcp-server/dist/tools/discord-webhook.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-webhook.js +1 -1
- package/mcp-server/dist/tools/discord-webhook.js.map +1 -1
- package/mcp-server/dist/tools/fb-groups.d.ts +1 -1
- package/mcp-server/dist/tools/fb-groups.d.ts.map +1 -1
- package/mcp-server/dist/tools/fb-groups.js +1 -1
- package/mcp-server/dist/tools/fb-groups.js.map +1 -1
- package/mcp-server/dist/tools/forget.d.ts +1 -1
- package/mcp-server/dist/tools/forget.d.ts.map +1 -1
- package/mcp-server/dist/tools/forget.js +1 -1
- package/mcp-server/dist/tools/forget.js.map +1 -1
- package/mcp-server/dist/tools/git-tools.d.ts +1 -1
- package/mcp-server/dist/tools/git-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/git-tools.js +1 -1
- package/mcp-server/dist/tools/git-tools.js.map +1 -1
- package/mcp-server/dist/tools/gsc/experience.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/experience.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/experience.js +1 -1
- package/mcp-server/dist/tools/gsc/experience.js.map +1 -1
- package/mcp-server/dist/tools/gsc/indexing.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/indexing.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/indexing.js +1 -1
- package/mcp-server/dist/tools/gsc/indexing.js.map +1 -1
- package/mcp-server/dist/tools/gsc/inspect.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/inspect.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/inspect.js +1 -1
- package/mcp-server/dist/tools/gsc/inspect.js.map +1 -1
- package/mcp-server/dist/tools/gsc/navigate.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/navigate.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/navigate.js +1 -1
- package/mcp-server/dist/tools/gsc/navigate.js.map +1 -1
- package/mcp-server/dist/tools/gsc/performance.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/performance.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/performance.js +1 -1
- package/mcp-server/dist/tools/gsc/performance.js.map +1 -1
- package/mcp-server/dist/tools/gsc/settings.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/settings.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/settings.js +1 -1
- package/mcp-server/dist/tools/gsc/settings.js.map +1 -1
- package/mcp-server/dist/tools/image-tools.d.ts +1 -1
- package/mcp-server/dist/tools/image-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/image-tools.js +1 -1
- package/mcp-server/dist/tools/image-tools.js.map +1 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +1 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.js +1 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.js.map +1 -1
- package/mcp-server/dist/tools/loop.d.ts +1 -1
- package/mcp-server/dist/tools/loop.d.ts.map +1 -1
- package/mcp-server/dist/tools/loop.js +1 -1
- package/mcp-server/dist/tools/loop.js.map +1 -1
- package/mcp-server/dist/tools/memories.d.ts +1 -1
- package/mcp-server/dist/tools/memories.d.ts.map +1 -1
- package/mcp-server/dist/tools/memories.js +1 -1
- package/mcp-server/dist/tools/memories.js.map +1 -1
- package/mcp-server/dist/tools/morelogin.d.ts +1 -1
- package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
- package/mcp-server/dist/tools/morelogin.js +1 -1
- package/mcp-server/dist/tools/morelogin.js.map +1 -1
- package/mcp-server/dist/tools/profile.d.ts +1 -1
- package/mcp-server/dist/tools/profile.d.ts.map +1 -1
- package/mcp-server/dist/tools/profile.js +1 -1
- package/mcp-server/dist/tools/profile.js.map +1 -1
- package/mcp-server/dist/tools/recall.d.ts +1 -1
- package/mcp-server/dist/tools/recall.d.ts.map +1 -1
- package/mcp-server/dist/tools/recall.js +1 -1
- package/mcp-server/dist/tools/recall.js.map +1 -1
- package/mcp-server/dist/tools/reflect.d.ts +1 -1
- package/mcp-server/dist/tools/reflect.d.ts.map +1 -1
- package/mcp-server/dist/tools/reflect.js +1 -1
- package/mcp-server/dist/tools/reflect.js.map +1 -1
- package/mcp-server/dist/tools/remember.d.ts +1 -1
- package/mcp-server/dist/tools/remember.d.ts.map +1 -1
- package/mcp-server/dist/tools/remember.js +13 -11
- package/mcp-server/dist/tools/remember.js.map +1 -1
- package/mcp-server/dist/tools/restore.d.ts +1 -1
- package/mcp-server/dist/tools/restore.d.ts.map +1 -1
- package/mcp-server/dist/tools/restore.js +1 -1
- package/mcp-server/dist/tools/restore.js.map +1 -1
- package/mcp-server/dist/tools/style-guide-tools.d.ts +1 -1
- package/mcp-server/dist/tools/style-guide-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/style-guide-tools.js +1 -1
- package/mcp-server/dist/tools/style-guide-tools.js.map +1 -1
- package/mcp-server/dist/tools/sync.d.ts +1 -1
- package/mcp-server/dist/tools/sync.d.ts.map +1 -1
- package/mcp-server/dist/tools/sync.js +1 -1
- package/mcp-server/dist/tools/sync.js.map +1 -1
- package/mcp-server/dist/tools/tictactoe-tools.d.ts +1 -1
- package/mcp-server/dist/tools/tictactoe-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/tictactoe-tools.js +1 -1
- package/mcp-server/dist/tools/tictactoe-tools.js.map +1 -1
- package/mcp-server/dist/tools/utils.d.ts +1 -1
- package/mcp-server/dist/tools/utils.d.ts.map +1 -1
- package/mcp-server/dist/tools/utils.js +1 -1
- package/mcp-server/dist/tools/utils.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/tools/whiteboard-tools.d.ts +3 -3
- package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/whiteboard-tools.js +1 -1
- package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
- package/mcp-server/dist/tools/youtube/analytics.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/analytics.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/analytics.js +1 -1
- package/mcp-server/dist/tools/youtube/analytics.js.map +1 -1
- package/mcp-server/dist/tools/youtube/download.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/download.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/download.js +1 -1
- package/mcp-server/dist/tools/youtube/download.js.map +1 -1
- package/mcp-server/dist/tools/youtube/media.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/media.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/media.js +1 -1
- package/mcp-server/dist/tools/youtube/media.js.map +1 -1
- package/mcp-server/dist/tools/youtube/metadata.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/metadata.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/metadata.js +1 -1
- package/mcp-server/dist/tools/youtube/metadata.js.map +1 -1
- package/mcp-server/dist/tools/youtube/navigate.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/navigate.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/navigate.js +1 -1
- package/mcp-server/dist/tools/youtube/navigate.js.map +1 -1
- package/mcp-server/dist/tools/youtube/playlist.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/playlist.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/playlist.js +1 -1
- package/mcp-server/dist/tools/youtube/playlist.js.map +1 -1
- package/mcp-server/dist/tools/youtube/schedule.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/schedule.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/schedule.js +1 -1
- package/mcp-server/dist/tools/youtube/schedule.js.map +1 -1
- package/mcp-server/dist/tools/youtube/source.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/source.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/source.js +1 -1
- package/mcp-server/dist/tools/youtube/source.js.map +1 -1
- package/mcp-server/dist/tools/youtube/upload.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/upload.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/upload.js +4 -9
- package/mcp-server/dist/tools/youtube/upload.js.map +1 -1
- package/mcp-server/dist/tui.js +18 -5
- 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 +3 -3
- package/mcp-server/package.json +3 -3
- 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/browser-bench.ts +1 -1
- package/mcp-server/src/services/budget-judgments.ts +183 -0
- package/mcp-server/src/services/categories.ts +77 -32
- package/mcp-server/src/services/codex-config-heal.ts +35 -4
- package/mcp-server/src/services/desktop-bench.ts +1 -1
- 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 +34 -15
- package/mcp-server/src/tools/bluesky.ts +1 -1
- package/mcp-server/src/tools/browser-advanced.ts +1 -1
- package/mcp-server/src/tools/browser-batch.ts +1 -1
- package/mcp-server/src/tools/browser-cheatsheet.ts +1 -1
- package/mcp-server/src/tools/browser-interact.ts +1 -1
- package/mcp-server/src/tools/browser-navigate.ts +1 -1
- package/mcp-server/src/tools/browser-observe.ts +1 -1
- package/mcp-server/src/tools/card-tools.ts +1 -1
- package/mcp-server/src/tools/category.ts +6 -1
- package/mcp-server/src/tools/choice.ts +1 -1
- package/mcp-server/src/tools/computer.ts +1 -1
- package/mcp-server/src/tools/discord-channel.ts +1 -1
- package/mcp-server/src/tools/discord-guild.ts +1 -1
- package/mcp-server/src/tools/discord-member.ts +1 -1
- package/mcp-server/src/tools/discord-message.ts +1 -1
- package/mcp-server/src/tools/discord-onboarding.ts +1 -1
- package/mcp-server/src/tools/discord-role.ts +1 -1
- package/mcp-server/src/tools/discord-thread.ts +1 -1
- package/mcp-server/src/tools/discord-webhook.ts +1 -1
- package/mcp-server/src/tools/fb-groups.ts +1 -1
- package/mcp-server/src/tools/forget.ts +1 -1
- package/mcp-server/src/tools/git-tools.ts +1 -1
- package/mcp-server/src/tools/gsc/experience.ts +1 -1
- package/mcp-server/src/tools/gsc/indexing.ts +1 -1
- package/mcp-server/src/tools/gsc/inspect.ts +1 -1
- package/mcp-server/src/tools/gsc/navigate.ts +1 -1
- package/mcp-server/src/tools/gsc/performance.ts +1 -1
- package/mcp-server/src/tools/gsc/settings.ts +1 -1
- package/mcp-server/src/tools/image-tools.ts +1 -1
- package/mcp-server/src/tools/leonardo-browser-tools.ts +1 -1
- package/mcp-server/src/tools/loop.ts +1 -1
- package/mcp-server/src/tools/memories.ts +1 -1
- package/mcp-server/src/tools/morelogin.ts +1 -1
- package/mcp-server/src/tools/profile.ts +1 -1
- package/mcp-server/src/tools/recall.ts +1 -1
- package/mcp-server/src/tools/reflect.ts +1 -1
- package/mcp-server/src/tools/remember.ts +13 -12
- package/mcp-server/src/tools/restore.ts +1 -1
- package/mcp-server/src/tools/style-guide-tools.ts +1 -1
- package/mcp-server/src/tools/sync.ts +1 -1
- package/mcp-server/src/tools/tictactoe-tools.ts +1 -1
- package/mcp-server/src/tools/utils.ts +1 -1
- package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
- package/mcp-server/src/tools/whiteboard-tools.ts +1 -1
- package/mcp-server/src/tools/youtube/analytics.ts +1 -1
- package/mcp-server/src/tools/youtube/download.ts +1 -1
- package/mcp-server/src/tools/youtube/media.ts +1 -1
- package/mcp-server/src/tools/youtube/metadata.ts +1 -1
- package/mcp-server/src/tools/youtube/navigate.ts +1 -1
- package/mcp-server/src/tools/youtube/playlist.ts +1 -1
- package/mcp-server/src/tools/youtube/schedule.ts +1 -1
- package/mcp-server/src/tools/youtube/source.ts +1 -1
- package/mcp-server/src/tools/youtube/upload.ts +4 -12
- package/mcp-server/src/tui.ts +24 -10
- package/mcp-server/src/version.ts +22 -0
- package/neural-interface/i18n/en.json +645 -57
- package/neural-interface/i18n/pt-BR.json +644 -56
- package/neural-interface/lib/account-usage.js +163 -0
- package/neural-interface/lib/agent-sandbox-elevation.js +102 -0
- package/neural-interface/lib/agent-sandbox-setting.js +57 -0
- package/neural-interface/lib/assistant-api.js +274 -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-codex.js +3 -1
- 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 +312 -48
- 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 +180 -0
- package/neural-interface/lib/codex-accounts.js +502 -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 +122 -0
- package/neural-interface/lib/desktop/config.js +3 -0
- package/neural-interface/lib/exec-loop-command.js +3 -1
- 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 +163 -0
- package/neural-interface/lib/mcp-installer.js +14 -2
- package/neural-interface/lib/native-loop-providers.js +22 -4
- 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 +96 -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-indexer.js +3 -1
- package/neural-interface/lib/session-title-generator.js +6 -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 +542 -0
- package/neural-interface/public/shared/account-usage.js +79 -0
- package/neural-interface/public/shared/account-view.js +336 -0
- package/neural-interface/public/shared/agent-runtime-options.js +28 -8
- package/neural-interface/public/shared/assistant/asst-account-menu.js +298 -0
- package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
- package/neural-interface/public/shared/assistant/asst-api.js +39 -2
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +288 -32
- 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 +219 -80
- package/neural-interface/public/shared/assistant/asst-model-chip.js +135 -0
- package/neural-interface/public/shared/assistant/asst-panel.js +87 -16
- 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 +160 -29
- 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 +49 -108
- package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +172 -111
- package/neural-interface/public/shared/cdx/cdx-tabs.js +264 -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-childpanel.js +4 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +36 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +17 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +169 -2
- 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 +490 -0
- package/neural-interface/public/shared/storage.js +30 -3
- package/neural-interface/public/shared/styles.css +281 -32
- package/neural-interface/public/shared/synabun-mascot.js +48 -2
- package/neural-interface/public/shared/ui-claude-panel.js +262 -217
- package/neural-interface/public/shared/ui-cli-banner.js +355 -0
- package/neural-interface/public/shared/ui-explorer.js +2 -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 +8 -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 +598 -7
- 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/scripts/rebuild-pty.js +14 -2
- package/neural-interface/server.js +1384 -679
- package/npm-shrinkwrap.json +4424 -0
- package/package.json +57 -2
- package/postinstall.js +61 -47
- package/preuninstall.js +8 -35
- package/setup.js +80 -47
- package/skills/synabun/modules/idea.md +1 -1
- package/skills/synabun/modules/schedule.md +1 -1
- package/updater.cmd +2 -2
- package/updater.mjs +97 -3
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun — where the user's Claude Code, Codex and OpenCode are
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// One answer for every place that looks for one of these CLIs: the version
|
|
6
|
+
// check behind the side panels' banner and the update badges
|
|
7
|
+
// (GET /api/system/tool-versions), Settings' Detect button, the one-click
|
|
8
|
+
// install's final check (cli-install.js), and every launcher in server.js
|
|
9
|
+
// (getClaudeBin, getCodexBin, the `opencode serve` launches, terminal tabs,
|
|
10
|
+
// update commands). Detection, the banner, launching and updates therefore
|
|
11
|
+
// always mean the same file.
|
|
12
|
+
//
|
|
13
|
+
// Resolution order per tool. The first candidate that exists AND answers
|
|
14
|
+
// `--version` wins:
|
|
15
|
+
//
|
|
16
|
+
// configured cli-config.json, Settings > Terminal: a file path (~ is
|
|
17
|
+
// the home folder), or a command name looked up like PATH
|
|
18
|
+
// path the PATH this process was given, then the OS PATH a new
|
|
19
|
+
// terminal would have now (os-path.js: the registry on
|
|
20
|
+
// Windows, the login shell on macOS and Linux)
|
|
21
|
+
// known-location the folders each install method uses on this system:
|
|
22
|
+
// the vendors' own (toolSpecificFiles) first, then every
|
|
23
|
+
// package manager's (augmented-path.js knownInstallDirs)
|
|
24
|
+
//
|
|
25
|
+
// Never a file inside SynaBun's own install or runtime folders
|
|
26
|
+
// (`isInsideSynabun`), never a dependency's `node_modules/.bin` command, never
|
|
27
|
+
// a Windows app alias in WindowsApps (an older Claude Desktop registers a
|
|
28
|
+
// `claude.exe` there that opens the desktop app). On Windows a folder is
|
|
29
|
+
// searched in PATHEXT order (.exe before .cmd/.bat, then .ps1; an
|
|
30
|
+
// extensionless file is never a Windows program), and an npm command file is
|
|
31
|
+
// followed to the program it runs (claude.exe, Codex's vendored codex.exe,
|
|
32
|
+
// OpenCode's opencode.exe) so that nothing needs a shell to start it; one that
|
|
33
|
+
// cannot be followed is run through cmd.exe with every word escaped.
|
|
34
|
+
//
|
|
35
|
+
// A status has three states: installed (path, version, how it was found, the
|
|
36
|
+
// install method), missing, and broken: found, but `--version` failed, with
|
|
37
|
+
// the real error. Versions are cached per file identity (real path, size,
|
|
38
|
+
// mtime), so an update is probed again by itself; `force` (Re-check) reads the
|
|
39
|
+
// OS PATH again and probes everything again.
|
|
40
|
+
//
|
|
41
|
+
// Every dependency is injectable, so Windows, Linux and macOS resolution is
|
|
42
|
+
// tested from any host. Nothing here throws.
|
|
43
|
+
|
|
44
|
+
import { spawn } from 'node:child_process';
|
|
45
|
+
import { accessSync, closeSync, constants, existsSync, openSync, readdirSync, readFileSync, readSync, realpathSync, statSync } from 'node:fs';
|
|
46
|
+
import os from 'node:os';
|
|
47
|
+
import path from 'node:path';
|
|
48
|
+
import { forgetAugmentedPath, knownInstallDirs, withPathEnv, getAugmentedPath, pathEnvValue } from './augmented-path.js';
|
|
49
|
+
import { detectInstallSourceOf } from './cli-update-planner.js';
|
|
50
|
+
import { windowsClaudeTargets } from './claude-executable.js';
|
|
51
|
+
import { resolveTrustedWindowsCodexBinary, windowsCodexPackageJsonCandidates } from './codex-runtime-path.js';
|
|
52
|
+
import { osPath as defaultOsPath } from './os-path.js';
|
|
53
|
+
|
|
54
|
+
export const CLI_TOOLS = Object.freeze({
|
|
55
|
+
'claude-code': Object.freeze({ key: 'claude-code', command: 'claude', label: 'Claude Code' }),
|
|
56
|
+
codex: Object.freeze({ key: 'codex', command: 'codex', label: 'Codex' }),
|
|
57
|
+
opencode: Object.freeze({ key: 'opencode', command: 'opencode', label: 'OpenCode' }),
|
|
58
|
+
gemini: Object.freeze({ key: 'gemini', command: 'gemini', label: 'Gemini CLI' }),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const PROBE_TIMEOUT_MS = 15_000;
|
|
62
|
+
const LAUNCH_MEMO_MS = 3_000;
|
|
63
|
+
const FAILURE_REPROBE_MS = 60_000; // a failed --version is asked again by a plain check after this
|
|
64
|
+
const FAILURE_KEEP_MS = 10 * 60_000; // launchers skip a known failure for this long (or until the file changes)
|
|
65
|
+
const MAX_PROBES = 6;
|
|
66
|
+
const LAUNCHABLE_EXTENSIONS = ['.com', '.exe', '.bat', '.cmd'];
|
|
67
|
+
|
|
68
|
+
const pathFor = (platform) => (platform === 'win32' ? path.win32 : path.posix);
|
|
69
|
+
|
|
70
|
+
function homeOf(env, platform) {
|
|
71
|
+
if (platform === 'win32') return env.USERPROFILE || env.HOME || os.homedir();
|
|
72
|
+
return env.HOME || os.homedir();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Names, places, launch specs (pure) ─────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The file names a command has on Windows, in the order Windows tries them:
|
|
79
|
+
* PATHEXT's own order, kept to the extensions a program can be started from
|
|
80
|
+
* (.com .exe .bat .cmd), then a PowerShell script.
|
|
81
|
+
*/
|
|
82
|
+
export function windowsCommandNames(command, pathext = '') {
|
|
83
|
+
const listed = String(pathext || '.COM;.EXE;.BAT;.CMD').split(';')
|
|
84
|
+
.map(ext => ext.trim().toLowerCase())
|
|
85
|
+
.filter(ext => LAUNCHABLE_EXTENSIONS.includes(ext));
|
|
86
|
+
const order = [...new Set(listed.length ? listed : LAUNCHABLE_EXTENSIONS)];
|
|
87
|
+
return [...order.map(ext => `${command}${ext}`), `${command}.ps1`];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A PATH folder that is a dependency's commands, never the user's tool. */
|
|
91
|
+
export function isPackageBinDir(dir) {
|
|
92
|
+
return /[\\/]node_modules[\\/]\.bin[\\/]?$/i.test(String(dir || ''));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A Windows app alias folder (zero-byte reparse points that start a Store/desktop app). */
|
|
96
|
+
export function isWindowsAppsDir(file) {
|
|
97
|
+
return /[\\/]microsoft[\\/]windowsapps([\\/]|$)/i.test(String(file || ''));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function listDir(readdir, dir) {
|
|
101
|
+
try { return readdir(dir).map(String).slice(0, 300); } catch { return []; }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* WinGet installs a portable package under
|
|
106
|
+
* %LOCALAPPDATA%\Microsoft\WinGet\Packages\<Publisher.Id>_<source>\ and links
|
|
107
|
+
* it from WinGet\Links (a known folder). The package folder is looked in too,
|
|
108
|
+
* for a link that is missing.
|
|
109
|
+
*/
|
|
110
|
+
function wingetPackageFiles({ localAppData, prefixes, exe, readdir, pathImpl }) {
|
|
111
|
+
const root = pathImpl.join(localAppData, 'Microsoft', 'WinGet', 'Packages');
|
|
112
|
+
const out = [];
|
|
113
|
+
for (const name of listDir(readdir, root)) {
|
|
114
|
+
const lower = name.toLowerCase();
|
|
115
|
+
if (!prefixes.some(prefix => lower.startsWith(prefix))) continue;
|
|
116
|
+
const dir = pathImpl.join(root, name);
|
|
117
|
+
out.push(pathImpl.join(dir, exe));
|
|
118
|
+
for (const sub of listDir(readdir, dir).slice(0, 20)) out.push(pathImpl.join(dir, sub, exe));
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The vendors' own install locations for one tool (before every package
|
|
125
|
+
* manager's folders), checked against their installers and docs on
|
|
126
|
+
* 2026-10-17:
|
|
127
|
+
*
|
|
128
|
+
* Claude Code native installer: ~/.local/bin/claude, %USERPROFILE%\.local\bin\claude.exe
|
|
129
|
+
* (a known folder); the older local install ~/.claude/local/claude;
|
|
130
|
+
* WinGet Anthropic.ClaudeCode
|
|
131
|
+
* Codex standalone installer: $CODEX_INSTALL_DIR, else ~/.local/bin/codex
|
|
132
|
+
* (macOS, Linux) or %LOCALAPPDATA%\Programs\OpenAI\Codex\bin\codex.exe,
|
|
133
|
+
* each a link into $CODEX_HOME/packages/standalone/current/bin
|
|
134
|
+
* OpenCode install script: ~/.opencode/bin/opencode (older scripts:
|
|
135
|
+
* $OPENCODE_INSTALL_DIR, $XDG_BIN_DIR); on Windows the folder
|
|
136
|
+
* SynaBun's own install uses, %USERPROFILE%\.opencode\bin
|
|
137
|
+
*/
|
|
138
|
+
export function toolSpecificFiles(toolKey, {
|
|
139
|
+
platform = process.platform,
|
|
140
|
+
env = process.env,
|
|
141
|
+
home = homeOf(env, platform),
|
|
142
|
+
readdir = readdirSync,
|
|
143
|
+
} = {}) {
|
|
144
|
+
const pathImpl = pathFor(platform);
|
|
145
|
+
const join = (...parts) => pathImpl.join(...parts);
|
|
146
|
+
const windows = platform === 'win32';
|
|
147
|
+
const exe = (name) => (windows ? `${name}.exe` : name);
|
|
148
|
+
const localAppData = env.LOCALAPPDATA || join(home, 'AppData', 'Local');
|
|
149
|
+
const out = [];
|
|
150
|
+
const add = (file, via) => { if (file) out.push({ file, via }); };
|
|
151
|
+
const codexHome = env.CODEX_HOME || join(home, '.codex');
|
|
152
|
+
switch (toolKey) {
|
|
153
|
+
case 'claude-code':
|
|
154
|
+
add(join(home, '.local', 'bin', exe('claude')), 'native');
|
|
155
|
+
if (!windows) add(join(home, '.claude', 'local', 'claude'), 'native');
|
|
156
|
+
if (windows) for (const file of wingetPackageFiles({ localAppData, prefixes: ['anthropic.claudecode'], exe: 'claude.exe', readdir, pathImpl })) add(file, 'winget');
|
|
157
|
+
break;
|
|
158
|
+
case 'codex':
|
|
159
|
+
if (env.CODEX_INSTALL_DIR) add(join(env.CODEX_INSTALL_DIR, exe('codex')), 'standalone');
|
|
160
|
+
add(windows ? join(localAppData, 'Programs', 'OpenAI', 'Codex', 'bin', 'codex.exe') : join(home, '.local', 'bin', 'codex'), 'standalone');
|
|
161
|
+
add(join(codexHome, 'packages', 'standalone', 'current', 'bin', exe('codex')), 'standalone');
|
|
162
|
+
add(join(codexHome, 'packages', 'standalone', 'current', exe('codex')), 'standalone');
|
|
163
|
+
if (windows) for (const file of wingetPackageFiles({ localAppData, prefixes: ['openai.codex'], exe: 'codex.exe', readdir, pathImpl })) add(file, 'winget');
|
|
164
|
+
break;
|
|
165
|
+
case 'opencode':
|
|
166
|
+
add(join(home, '.opencode', 'bin', exe('opencode')), 'standalone');
|
|
167
|
+
if (env.OPENCODE_INSTALL_DIR) add(join(env.OPENCODE_INSTALL_DIR, exe('opencode')), 'standalone');
|
|
168
|
+
if (env.XDG_BIN_DIR) add(join(env.XDG_BIN_DIR, exe('opencode')), 'standalone');
|
|
169
|
+
if (windows) for (const file of wingetPackageFiles({ localAppData, prefixes: ['sst.opencode', 'anomalyco.opencode', 'opencode.'], exe: 'opencode.exe', readdir, pathImpl })) add(file, 'winget');
|
|
170
|
+
break;
|
|
171
|
+
default:
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* What a value in Settings > Terminal means: a file path, a command name, or
|
|
179
|
+
* a command line (which terminal tabs run as written but nothing can resolve
|
|
180
|
+
* to a file). The tool's own name is "not configured".
|
|
181
|
+
*/
|
|
182
|
+
export function describeConfigured(value, toolKey, {
|
|
183
|
+
platform = process.platform,
|
|
184
|
+
env = process.env,
|
|
185
|
+
home = homeOf(env, platform),
|
|
186
|
+
exists = existsSync,
|
|
187
|
+
} = {}) {
|
|
188
|
+
const raw = String(value || '').trim();
|
|
189
|
+
const tool = CLI_TOOLS[toolKey];
|
|
190
|
+
if (!raw || !tool || raw === tool.command) return null;
|
|
191
|
+
const pathImpl = pathFor(platform);
|
|
192
|
+
let text = raw.replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1');
|
|
193
|
+
if (text === '~' || text.startsWith('~/') || text.startsWith('~\\')) text = pathImpl.join(home, text.slice(1));
|
|
194
|
+
if (exists(text)) return { value: raw, kind: 'path', file: pathImpl.normalize(text) };
|
|
195
|
+
if (/\s/.test(text)) return { value: raw, kind: 'command-line' };
|
|
196
|
+
const hasSeparator = text.includes('/') || (platform === 'win32' && text.includes('\\'));
|
|
197
|
+
if (hasSeparator) return { value: raw, kind: 'path', file: pathImpl.normalize(text) };
|
|
198
|
+
return { value: raw, kind: 'command', name: text };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** OpenCode's npm package: its command file runs node_modules\opencode-ai\bin\opencode.exe, the platform build its postinstall put there. */
|
|
202
|
+
export function windowsOpencodeTargets(launcher, arch = process.arch) {
|
|
203
|
+
const pathImpl = path.win32;
|
|
204
|
+
const folder = pathImpl.dirname(String(launcher || ''));
|
|
205
|
+
const roots = [pathImpl.join(folder, 'node_modules')];
|
|
206
|
+
if (pathImpl.basename(folder).toLowerCase() === '.bin') roots.push(pathImpl.dirname(folder));
|
|
207
|
+
const platformPackages = arch === 'x64' ? ['opencode-windows-x64', 'opencode-windows-x64-baseline'] : [`opencode-windows-${arch}`];
|
|
208
|
+
return roots.flatMap(root => [
|
|
209
|
+
pathImpl.join(root, 'opencode-ai', 'bin', 'opencode.exe'),
|
|
210
|
+
...platformPackages.flatMap(name => [
|
|
211
|
+
pathImpl.join(root, 'opencode-ai', 'node_modules', name, 'bin', 'opencode.exe'),
|
|
212
|
+
pathImpl.join(root, name, 'bin', 'opencode.exe'),
|
|
213
|
+
]),
|
|
214
|
+
]);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** A Windows program begins with "MZ" (OpenCode's npm placeholder is a shell script). Reads two bytes: a CLI can be 200 MB. */
|
|
218
|
+
export function isPortableExecutable(file) {
|
|
219
|
+
let fd = null;
|
|
220
|
+
try {
|
|
221
|
+
fd = openSync(file, 'r');
|
|
222
|
+
const head = Buffer.alloc(2);
|
|
223
|
+
return readSync(fd, head, 0, 2, 0) === 2 && head[0] === 0x4d && head[1] === 0x5a;
|
|
224
|
+
} catch {
|
|
225
|
+
return false;
|
|
226
|
+
} finally {
|
|
227
|
+
if (fd != null) { try { closeSync(fd); } catch { /* closed */ } }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* How a found file is started without a shell.
|
|
233
|
+
*
|
|
234
|
+
* direct the file itself (every POSIX file; a Windows .exe/.com; an
|
|
235
|
+
* npm command file followed to the .exe it runs)
|
|
236
|
+
* node-script a JavaScript entry run by the Node running SynaBun (an old
|
|
237
|
+
* npm Claude Code's cli.js)
|
|
238
|
+
* powershell a .ps1, through powershell.exe -File
|
|
239
|
+
* cmd a .cmd/.bat that could not be followed: cmd.exe, escaped
|
|
240
|
+
* unsupported nothing Windows can start (an extensionless shell script)
|
|
241
|
+
*/
|
|
242
|
+
export function launchSpecFor(file, toolKey, {
|
|
243
|
+
platform = process.platform,
|
|
244
|
+
arch = process.arch,
|
|
245
|
+
env = process.env,
|
|
246
|
+
exists = existsSync,
|
|
247
|
+
isExecutableFile = isPortableExecutable,
|
|
248
|
+
resolvePlatformPackage,
|
|
249
|
+
acceptBinary = () => true,
|
|
250
|
+
} = {}) {
|
|
251
|
+
if (platform !== 'win32') return { kind: 'direct', file, launchPath: file };
|
|
252
|
+
const ext = path.win32.extname(file).toLowerCase();
|
|
253
|
+
if (ext === '.exe' || ext === '.com') return { kind: 'direct', file, launchPath: file };
|
|
254
|
+
if (ext === '.ps1') return { kind: 'powershell', file, launchPath: file };
|
|
255
|
+
if (ext !== '.cmd' && ext !== '.bat') return { kind: 'unsupported', file, launchPath: null };
|
|
256
|
+
|
|
257
|
+
let rejectedBinary = false;
|
|
258
|
+
const acceptTarget = (target) => {
|
|
259
|
+
if (acceptBinary(target)) return true;
|
|
260
|
+
rejectedBinary = true;
|
|
261
|
+
return false;
|
|
262
|
+
};
|
|
263
|
+
if (toolKey === 'claude-code') {
|
|
264
|
+
for (const target of windowsClaudeTargets(file)) {
|
|
265
|
+
if (!exists(target) || !acceptTarget(target)) continue;
|
|
266
|
+
if (/\.exe$/i.test(target)) return { kind: 'direct', file, launchPath: target, via: 'npm-package' };
|
|
267
|
+
return { kind: 'node-script', file, launchPath: target, via: 'npm-package' };
|
|
268
|
+
}
|
|
269
|
+
} else if (toolKey === 'codex') {
|
|
270
|
+
const found = resolveTrustedWindowsCodexBinary({
|
|
271
|
+
launchers: [file],
|
|
272
|
+
codexPackageJsonPaths: windowsCodexPackageJsonCandidates({ appData: env.APPDATA, npmConfigPrefix: env.NPM_CONFIG_PREFIX || env.npm_config_prefix }),
|
|
273
|
+
arch,
|
|
274
|
+
exists,
|
|
275
|
+
acceptBinary: acceptTarget,
|
|
276
|
+
...(resolvePlatformPackage ? { resolvePlatformPackage } : {}),
|
|
277
|
+
});
|
|
278
|
+
if (found.path) return { kind: 'direct', file, launchPath: found.path, via: found.source };
|
|
279
|
+
} else if (toolKey === 'opencode') {
|
|
280
|
+
// The package's bin\opencode.exe is a placeholder script until its postinstall ran: only a real program counts.
|
|
281
|
+
const target = windowsOpencodeTargets(file, arch).find(candidate => exists(candidate) && acceptTarget(candidate) && isExecutableFile(candidate));
|
|
282
|
+
if (target) return { kind: 'direct', file, launchPath: target, via: 'npm-package' };
|
|
283
|
+
}
|
|
284
|
+
// Do not fall back to executing a shim whose known target is SynaBun's copy.
|
|
285
|
+
if (rejectedBinary) return { kind: 'unsupported', file, launchPath: null, rejectedReason: 'inside-synabun' };
|
|
286
|
+
return { kind: 'cmd', file, launchPath: file };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// cmd.exe quoting, as cross-spawn does it: every metacharacter escaped with ^,
|
|
290
|
+
// the arguments of a command file twice (the file hands them on with %*).
|
|
291
|
+
const CMD_META = /([()\][%!^"`<>&|;, *?])/g;
|
|
292
|
+
|
|
293
|
+
export function cmdEscapeCommand(file) {
|
|
294
|
+
return String(file).replace(CMD_META, '^$1');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function cmdEscapeArgument(value, doubleEscape = true) {
|
|
298
|
+
let arg = String(value);
|
|
299
|
+
arg = arg.replace(/(?=(\\+?)?)\1"/g, '$1$1\\"');
|
|
300
|
+
arg = arg.replace(/(?=(\\+?)?)\1$/, '$1$1');
|
|
301
|
+
arg = `"${arg}"`.replace(CMD_META, '^$1');
|
|
302
|
+
return doubleEscape ? arg.replace(CMD_META, '^$1') : arg;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** The program, its arguments and the spawn options that start `spec` with `args`, never through a shell of ours. */
|
|
306
|
+
export function spawnArgsFor(spec, args = [], { env = process.env, execPath = process.execPath } = {}) {
|
|
307
|
+
const list = args.map(String);
|
|
308
|
+
switch (spec?.kind) {
|
|
309
|
+
case 'node-script':
|
|
310
|
+
return { command: execPath, args: [spec.launchPath, ...list], options: {} };
|
|
311
|
+
case 'powershell': {
|
|
312
|
+
const root = env.SystemRoot || env.SYSTEMROOT || 'C:\\Windows';
|
|
313
|
+
const shell = path.win32.join(root, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
314
|
+
return { command: shell, args: ['-NoProfile', '-NonInteractive', '-File', spec.launchPath, ...list], options: {} };
|
|
315
|
+
}
|
|
316
|
+
case 'cmd': {
|
|
317
|
+
const root = env.SystemRoot || env.SYSTEMROOT || 'C:\\Windows';
|
|
318
|
+
const comspec = env.ComSpec || env.COMSPEC || path.win32.join(root, 'System32', 'cmd.exe');
|
|
319
|
+
const line = [cmdEscapeCommand(spec.launchPath), ...list.map(arg => cmdEscapeArgument(arg, true))].join(' ');
|
|
320
|
+
return { command: comspec, args: ['/d', '/s', '/c', `"${line}"`], options: { windowsVerbatimArguments: true } };
|
|
321
|
+
}
|
|
322
|
+
default:
|
|
323
|
+
return { command: spec?.launchPath || spec?.file, args: list, options: {} };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── --version ───────────────────────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
/** "2.1.302 (Claude Code)" → "2.1.302"; "codex-cli 0.163.0" → "0.163.0"; a prerelease suffix is kept. */
|
|
330
|
+
export function parseCliVersion(text) {
|
|
331
|
+
const match = String(text ?? '').match(/(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?/);
|
|
332
|
+
return match ? `${match[1]}.${match[2]}.${match[3]}${match[4] || ''}` : null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function firstLines(text, count = 3) {
|
|
336
|
+
return String(text || '').split(/\r?\n/).map(line => line.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '').trim()).filter(Boolean).slice(0, count).join(' · ').slice(0, 400);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Run `<spec> --version` and read the answer: { ok, version, output, error }.
|
|
341
|
+
* `error` is a sentence about what went wrong, with what the CLI printed. The
|
|
342
|
+
* CLI runs in a process group of its own (POSIX); its whole group/tree is stopped at
|
|
343
|
+
* the timeout: a CLI that hangs, or leaves a child holding its output open,
|
|
344
|
+
* never holds a check up longer than `timeoutMs`.
|
|
345
|
+
*/
|
|
346
|
+
export function probeVersion(spec, {
|
|
347
|
+
env = process.env,
|
|
348
|
+
timeoutMs = PROBE_TIMEOUT_MS,
|
|
349
|
+
spawnImpl = spawn,
|
|
350
|
+
execPath = process.execPath,
|
|
351
|
+
cwd = os.tmpdir(),
|
|
352
|
+
platform = process.platform,
|
|
353
|
+
} = {}) {
|
|
354
|
+
return new Promise((resolve) => {
|
|
355
|
+
if (!spec || spec.kind === 'unsupported') {
|
|
356
|
+
resolve({ ok: false, version: null, output: '', error: 'Windows cannot start this file: it has no program extension (.exe, .cmd).' });
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const { command, args, options } = spawnArgsFor(spec, ['--version'], { env, execPath });
|
|
360
|
+
const childEnv = { ...withPathEnv(env, pathEnvValue(env), platform), NO_COLOR: '1', TERM: 'dumb' };
|
|
361
|
+
delete childEnv.FORCE_COLOR;
|
|
362
|
+
let child;
|
|
363
|
+
let out = '';
|
|
364
|
+
let err = '';
|
|
365
|
+
let settled = false;
|
|
366
|
+
let timedOut = false;
|
|
367
|
+
let stopped = false;
|
|
368
|
+
const finish = (result) => {
|
|
369
|
+
if (settled) return;
|
|
370
|
+
settled = true;
|
|
371
|
+
clearTimeout(timer);
|
|
372
|
+
resolve(result);
|
|
373
|
+
};
|
|
374
|
+
const stop = () => {
|
|
375
|
+
if (stopped || !child?.pid) return;
|
|
376
|
+
stopped = true;
|
|
377
|
+
const killChild = () => { try { child.kill('SIGKILL'); } catch { /* gone */ } };
|
|
378
|
+
if (platform === 'win32') {
|
|
379
|
+
// Keep cmd.exe alive until taskkill has enumerated the descendants.
|
|
380
|
+
try {
|
|
381
|
+
const killer = spawnImpl('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
|
|
382
|
+
killer.on('error', killChild);
|
|
383
|
+
killer.on('close', (code) => { if (code !== 0) killChild(); });
|
|
384
|
+
} catch { killChild(); }
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (platform !== 'win32') { try { process.kill(-child.pid, 'SIGKILL'); return; } catch { /* not a group */ } }
|
|
388
|
+
killChild();
|
|
389
|
+
};
|
|
390
|
+
const timer = setTimeout(() => {
|
|
391
|
+
timedOut = true;
|
|
392
|
+
stop();
|
|
393
|
+
const said = firstLines(err) || firstLines(out);
|
|
394
|
+
const message = `did not answer --version within ${Math.max(1, Math.round(timeoutMs / 1000))} s`;
|
|
395
|
+
finish({ ok: false, version: parseCliVersion(out), output: said, error: said ? `${message}: ${said}` : message });
|
|
396
|
+
}, timeoutMs);
|
|
397
|
+
timer.unref?.();
|
|
398
|
+
try {
|
|
399
|
+
child = spawnImpl(command, args, {
|
|
400
|
+
...options,
|
|
401
|
+
cwd,
|
|
402
|
+
env: childEnv,
|
|
403
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
404
|
+
windowsHide: true,
|
|
405
|
+
detached: platform !== 'win32',
|
|
406
|
+
});
|
|
407
|
+
} catch (error) {
|
|
408
|
+
finish({ ok: false, version: null, output: '', error: `could not be started (${error?.code || error?.message || error})` });
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const cap = (text, chunk) => (text.length > 64 * 1024 ? text : text + chunk);
|
|
412
|
+
child.stdout?.setEncoding?.('utf8');
|
|
413
|
+
child.stderr?.setEncoding?.('utf8');
|
|
414
|
+
child.stdout?.on('data', (chunk) => { out = cap(out, chunk); });
|
|
415
|
+
child.stderr?.on('data', (chunk) => { err = cap(err, chunk); });
|
|
416
|
+
child.on('error', (error) => {
|
|
417
|
+
const code = error?.code || 'error';
|
|
418
|
+
const message = String(error?.message || '');
|
|
419
|
+
finish({ ok: false, version: null, output: '', error: `could not be started (${message && message !== code ? `${code}: ${message}` : code})` });
|
|
420
|
+
});
|
|
421
|
+
child.on('exit', (code, signal) => {
|
|
422
|
+
// What it printed may still be on its way: read it when the streams end (or soon after: a child of
|
|
423
|
+
// the CLI may hold them open, and is then stopped with the group).
|
|
424
|
+
let fallback = null;
|
|
425
|
+
let read = false;
|
|
426
|
+
const done = () => {
|
|
427
|
+
if (timedOut || read) return;
|
|
428
|
+
read = true;
|
|
429
|
+
clearTimeout(fallback);
|
|
430
|
+
if (code === 0) {
|
|
431
|
+
const version = parseCliVersion(out) || parseCliVersion(err) || firstLines(out, 1).slice(0, 40) || 'unknown';
|
|
432
|
+
finish({ ok: true, version, output: firstLines(out || err), error: null });
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const message = signal
|
|
436
|
+
? `was stopped by ${signal} as it started${signal === 'SIGKILL' ? ' (on macOS: Gatekeeper or a code signature refused it)' : ''}`
|
|
437
|
+
: `exited with code ${code}`;
|
|
438
|
+
const said = firstLines(err) || firstLines(out);
|
|
439
|
+
finish({ ok: false, version: parseCliVersion(out), output: said, error: said ? `${message}: ${said}` : message });
|
|
440
|
+
};
|
|
441
|
+
let pending = 2;
|
|
442
|
+
const one = () => { if (--pending === 0) done(); };
|
|
443
|
+
fallback = setTimeout(() => { stop(); done(); }, 300);
|
|
444
|
+
fallback.unref?.();
|
|
445
|
+
if (child.stdout && !child.stdout.readableEnded) child.stdout.once('end', one); else one();
|
|
446
|
+
if (child.stderr && !child.stderr.readableEnded) child.stderr.once('end', one); else one();
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ── The resolver ────────────────────────────────────────────────────────────
|
|
452
|
+
|
|
453
|
+
function defaultIsLaunchable(file, platform) {
|
|
454
|
+
try {
|
|
455
|
+
if (!statSync(file).isFile()) return false;
|
|
456
|
+
if (platform !== 'win32') accessSync(file, constants.X_OK);
|
|
457
|
+
return true;
|
|
458
|
+
} catch {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** Why a configured file cannot be a candidate, or null when it can. */
|
|
464
|
+
function configuredFileProblem(file, platform, { stat, access }) {
|
|
465
|
+
let info;
|
|
466
|
+
try { info = stat(file); } catch { return 'not found'; }
|
|
467
|
+
if (typeof info?.isFile === 'function' && !info.isFile()) return 'not a file';
|
|
468
|
+
if (platform !== 'win32') {
|
|
469
|
+
try { access(file, constants.X_OK); } catch { return 'not executable by this user'; }
|
|
470
|
+
}
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function fileIdentity(file, { realpath, stat }) {
|
|
475
|
+
try {
|
|
476
|
+
const real = realpath(file);
|
|
477
|
+
const info = stat(real);
|
|
478
|
+
return `${real}|${info.mtimeMs}|${info.size}`;
|
|
479
|
+
} catch {
|
|
480
|
+
return String(file);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* @param {object} deps
|
|
486
|
+
* @param {(file: string) => boolean} [deps.isInsideSynabun] SynaBun's own install/runtime folders
|
|
487
|
+
* @param {(toolKey: string) => (string|null)} [deps.configured] the Settings value (cli-config.json)
|
|
488
|
+
* @param {object} [deps.osPath] os-path.js reader ({ cached, refresh, stale, snapshot })
|
|
489
|
+
*/
|
|
490
|
+
export function createCliResolver({
|
|
491
|
+
platform = process.platform,
|
|
492
|
+
arch = process.arch,
|
|
493
|
+
env = process.env,
|
|
494
|
+
home = homeOf(env, platform),
|
|
495
|
+
isInsideSynabun = () => false,
|
|
496
|
+
configured = () => null,
|
|
497
|
+
osPath = defaultOsPath,
|
|
498
|
+
probe = probeVersion,
|
|
499
|
+
exists = existsSync,
|
|
500
|
+
isLaunchable = defaultIsLaunchable,
|
|
501
|
+
readdir = readdirSync,
|
|
502
|
+
readFile = readFileSync,
|
|
503
|
+
realpath = realpathSync,
|
|
504
|
+
stat = statSync,
|
|
505
|
+
access = accessSync,
|
|
506
|
+
resolvePlatformPackage,
|
|
507
|
+
isExecutableFile,
|
|
508
|
+
now = Date.now,
|
|
509
|
+
probeTimeoutMs = PROBE_TIMEOUT_MS,
|
|
510
|
+
maxProbes = MAX_PROBES,
|
|
511
|
+
log = () => {},
|
|
512
|
+
} = {}) {
|
|
513
|
+
const pathImpl = pathFor(platform);
|
|
514
|
+
const windows = platform === 'win32';
|
|
515
|
+
const versions = new Map(); // launch identity → { at, result }
|
|
516
|
+
const launchMemo = new Map(); // toolKey → { at, value }
|
|
517
|
+
const lastStatus = new Map(); // toolKey → status
|
|
518
|
+
const background = new Map(); // toolKey → promise
|
|
519
|
+
const pathOnly = () => env.SYNABUN_TOOL_DISCOVERY === 'path';
|
|
520
|
+
|
|
521
|
+
const searchDirs = (osEntries) => {
|
|
522
|
+
const delimiter = windows ? ';' : ':';
|
|
523
|
+
const inherited = String(pathEnvValue(env)).split(delimiter);
|
|
524
|
+
const list = pathOnly() ? inherited : [...inherited, ...osEntries];
|
|
525
|
+
const seen = new Set();
|
|
526
|
+
return list.filter((dir) => {
|
|
527
|
+
const text = String(dir || '').trim();
|
|
528
|
+
if (!text || isPackageBinDir(text)) return false;
|
|
529
|
+
const key = windows ? text.toLowerCase().replace(/[\\/]+$/, '') : text.replace(/\/+$/, '');
|
|
530
|
+
if (seen.has(key)) return false;
|
|
531
|
+
seen.add(key);
|
|
532
|
+
return true;
|
|
533
|
+
});
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const namesOf = (command) => (windows ? windowsCommandNames(command, env.PATHEXT) : [command]);
|
|
537
|
+
|
|
538
|
+
/** Every candidate for a tool, in resolution order, each file once (by what it really is). */
|
|
539
|
+
function collect(toolKey, { osEntries = osPath.cached(), configuredValue } = {}) {
|
|
540
|
+
const tool = CLI_TOOLS[toolKey];
|
|
541
|
+
const out = [];
|
|
542
|
+
const rejected = [];
|
|
543
|
+
const seen = new Set();
|
|
544
|
+
const settings = describeConfigured(configuredValue === undefined ? safeConfigured(toolKey) : configuredValue, toolKey, { platform, env, home, exists });
|
|
545
|
+
const consider = (file, foundBy, via) => {
|
|
546
|
+
if (!file) return;
|
|
547
|
+
const normal = pathImpl.normalize(file);
|
|
548
|
+
if (!isLaunchable(normal, platform)) return;
|
|
549
|
+
let id;
|
|
550
|
+
try { id = realpath(normal); } catch { id = normal; }
|
|
551
|
+
if (windows) id = id.toLowerCase();
|
|
552
|
+
if (seen.has(id)) return;
|
|
553
|
+
seen.add(id);
|
|
554
|
+
if (windows && isWindowsAppsDir(normal)) { rejected.push({ path: normal, reason: 'windows-app-alias' }); return; }
|
|
555
|
+
if (isInsideSynabun(normal)) { rejected.push({ path: normal, reason: 'inside-synabun' }); return; }
|
|
556
|
+
out.push({ file: normal, foundBy, via });
|
|
557
|
+
};
|
|
558
|
+
const dirs = searchDirs(osEntries);
|
|
559
|
+
|
|
560
|
+
if (settings?.kind === 'path') consider(settings.file, 'configured', 'settings');
|
|
561
|
+
else if (settings?.kind === 'command') {
|
|
562
|
+
for (const dir of dirs) for (const name of namesOf(settings.name)) consider(pathImpl.join(dir, name), 'configured', 'settings');
|
|
563
|
+
if (!pathOnly()) for (const { dir } of knownInstallDirs({ platform, env, home, readdir, readFile })) for (const name of namesOf(settings.name)) consider(pathImpl.join(dir, name), 'configured', 'settings');
|
|
564
|
+
}
|
|
565
|
+
for (const dir of dirs) for (const name of namesOf(tool.command)) consider(pathImpl.join(dir, name), 'path', 'path');
|
|
566
|
+
if (!pathOnly()) {
|
|
567
|
+
for (const { file, via } of toolSpecificFiles(toolKey, { platform, env, home, readdir })) consider(file, 'known-location', via);
|
|
568
|
+
for (const { dir, via } of knownInstallDirs({ platform, env, home, readdir, readFile })) for (const name of namesOf(tool.command)) consider(pathImpl.join(dir, name), 'known-location', via);
|
|
569
|
+
}
|
|
570
|
+
return { candidates: out, rejected, settings };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function safeConfigured(toolKey) {
|
|
574
|
+
try { return configured(toolKey) ?? null; } catch { return null; }
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const specOf = (candidate, toolKey) => launchSpecFor(candidate.file, toolKey, { platform, arch, env, exists, acceptBinary: (file) => !isInsideSynabun(file), ...(resolvePlatformPackage ? { resolvePlatformPackage } : {}), ...(isExecutableFile ? { isExecutableFile } : {}) });
|
|
578
|
+
|
|
579
|
+
const versionKey = (spec) => `${spec.kind}|${fileIdentity(spec.launchPath || spec.file, { realpath, stat })}|${spec.kind === 'direct' ? '' : fileIdentity(spec.file, { realpath, stat })}`;
|
|
580
|
+
|
|
581
|
+
async function probeCached(spec, { force = false } = {}) {
|
|
582
|
+
const key = versionKey(spec);
|
|
583
|
+
const hit = versions.get(key);
|
|
584
|
+
const age = hit ? now() - hit.at : Infinity;
|
|
585
|
+
if (!force && hit && (hit.result.ok || age < FAILURE_REPROBE_MS)) return hit.result;
|
|
586
|
+
const result = await probe(spec, { env: withPathEnv(env, getAugmentedPath({ platform, env, osPath: osPath.cached() }), platform), timeoutMs: probeTimeoutMs });
|
|
587
|
+
versions.set(key, { at: now(), result });
|
|
588
|
+
return result;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function realOf(file) {
|
|
592
|
+
try { return realpath(file); } catch { return file; }
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function configuredStatus(settings, winner, tried) {
|
|
596
|
+
if (!settings) return null;
|
|
597
|
+
if (settings.kind === 'command-line') {
|
|
598
|
+
return { value: settings.value, ok: null, error: 'a command line: terminal tabs run it as written, everything else looks for the CLI elsewhere' };
|
|
599
|
+
}
|
|
600
|
+
if (winner?.foundBy === 'configured') return { value: settings.value, ok: true, error: null };
|
|
601
|
+
if (settings.kind === 'path') {
|
|
602
|
+
const problem = configuredFileProblem(settings.file, platform, { stat, access });
|
|
603
|
+
if (problem) return { value: settings.value, ok: false, error: problem };
|
|
604
|
+
if (isInsideSynabun(settings.file)) return { value: settings.value, ok: false, error: 'inside SynaBun, not an installation of the CLI' };
|
|
605
|
+
const own = tried.find(item => item.foundBy === 'configured');
|
|
606
|
+
return { value: settings.value, ok: false, error: own?.error || 'did not answer --version' };
|
|
607
|
+
}
|
|
608
|
+
const own = tried.find(item => item.foundBy === 'configured');
|
|
609
|
+
return { value: settings.value, ok: false, error: own?.error || 'not found on PATH or in the known install folders' };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function statusOf(toolKey, state, { candidate = null, spec = null, result = null, settings = null, rejected = [], tried = [] } = {}) {
|
|
613
|
+
const realPath = candidate ? realOf(candidate.file) : null;
|
|
614
|
+
const snapshot = typeof osPath.snapshot === 'function' ? osPath.snapshot() : null;
|
|
615
|
+
return {
|
|
616
|
+
tool: toolKey,
|
|
617
|
+
label: CLI_TOOLS[toolKey]?.label || toolKey,
|
|
618
|
+
state,
|
|
619
|
+
version: result?.version || null,
|
|
620
|
+
path: candidate?.file || null,
|
|
621
|
+
launchPath: spec?.launchPath || null,
|
|
622
|
+
launchKind: spec?.kind || null,
|
|
623
|
+
realPath,
|
|
624
|
+
foundBy: candidate?.foundBy || null,
|
|
625
|
+
via: candidate?.via || null,
|
|
626
|
+
installSource: candidate ? detectInstallSourceOf({ path: candidate.file, realPath }) : 'unknown',
|
|
627
|
+
error: state === 'broken' ? (result?.error || 'did not answer --version') : null,
|
|
628
|
+
configured: configuredStatus(settings, state === 'installed' ? candidate : null, tried),
|
|
629
|
+
candidates: tried.slice(0, 8),
|
|
630
|
+
rejected: rejected.slice(0, 5),
|
|
631
|
+
osPath: snapshot ? { source: snapshot.source, ok: snapshot.ok, error: snapshot.error } : null,
|
|
632
|
+
checkedAt: new Date(now()).toISOString(),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** The full answer for one tool: every candidate in order, probed until one answers. */
|
|
637
|
+
async function resolve(toolKey, { force = false, configured: configuredValue, freshOsPath = false } = {}) {
|
|
638
|
+
if (!CLI_TOOLS[toolKey]) return statusOf(toolKey, 'missing');
|
|
639
|
+
if (force) {
|
|
640
|
+
if (!freshOsPath) { try { await osPath.refresh({ force: true }); } catch { /* the old answer stays */ } }
|
|
641
|
+
forgetAugmentedPath();
|
|
642
|
+
launchMemo.delete(toolKey);
|
|
643
|
+
} else if (typeof osPath.stale === 'function' && osPath.stale()) {
|
|
644
|
+
try { await osPath.refresh(); } catch { /* the old answer stays */ }
|
|
645
|
+
}
|
|
646
|
+
const { candidates, rejected, settings } = collect(toolKey, { osEntries: osPath.cached(), configuredValue });
|
|
647
|
+
const tried = [];
|
|
648
|
+
let failure = null;
|
|
649
|
+
for (const candidate of candidates) {
|
|
650
|
+
if (tried.length >= maxProbes) break;
|
|
651
|
+
const spec = specOf(candidate, toolKey);
|
|
652
|
+
if (spec.rejectedReason) {
|
|
653
|
+
rejected.push({ path: candidate.file, reason: spec.rejectedReason });
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
const result = await probeCached(spec, { force });
|
|
657
|
+
tried.push({ path: candidate.file, foundBy: candidate.foundBy, ok: result.ok, version: result.version || null, error: result.error || null });
|
|
658
|
+
if (result.ok) {
|
|
659
|
+
const status = statusOf(toolKey, 'installed', { candidate, spec, result, settings, rejected, tried });
|
|
660
|
+
if (configuredValue === undefined) { lastStatus.set(toolKey, status); launchMemo.delete(toolKey); }
|
|
661
|
+
return status;
|
|
662
|
+
}
|
|
663
|
+
if (!failure) failure = { candidate, spec, result };
|
|
664
|
+
}
|
|
665
|
+
const status = failure
|
|
666
|
+
? statusOf(toolKey, 'broken', { ...failure, settings, rejected, tried })
|
|
667
|
+
: statusOf(toolKey, 'missing', { settings, rejected, tried });
|
|
668
|
+
if (configuredValue === undefined) { lastStatus.set(toolKey, status); launchMemo.delete(toolKey); }
|
|
669
|
+
return status;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
async function resolveAll(keys = Object.keys(CLI_TOOLS), { force = false } = {}) {
|
|
673
|
+
if (force) {
|
|
674
|
+
try { await osPath.refresh({ force: true }); } catch { /* the old answer stays */ }
|
|
675
|
+
forgetAugmentedPath();
|
|
676
|
+
launchMemo.clear();
|
|
677
|
+
}
|
|
678
|
+
const entries = await Promise.all(keys.map(async key => [key, await resolve(key, { force, freshOsPath: force })]));
|
|
679
|
+
return Object.fromEntries(entries);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function inBackground(toolKey) {
|
|
683
|
+
if (background.has(toolKey)) return;
|
|
684
|
+
const run = resolve(toolKey).catch(() => null).finally(() => background.delete(toolKey));
|
|
685
|
+
background.set(toolKey, run);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* What a launcher starts, at once (no process is started here): the same
|
|
690
|
+
* candidates in the same order, skipping any whose --version is known to
|
|
691
|
+
* fail. A candidate not probed yet is taken as it is, and the full check
|
|
692
|
+
* runs in the background so that the next launch knows. All known to fail:
|
|
693
|
+
* the first is answered as `broken`, so the launch fails with its own error
|
|
694
|
+
* rather than "not installed".
|
|
695
|
+
*/
|
|
696
|
+
function launcher(toolKey) {
|
|
697
|
+
const osKey = osPath.cached().join('\0');
|
|
698
|
+
const memo = launchMemo.get(toolKey);
|
|
699
|
+
if (memo && memo.osKey === osKey && now() - memo.at < LAUNCH_MEMO_MS) return memo.value;
|
|
700
|
+
let value = { tool: toolKey, path: null, launchPath: null, spec: null, foundBy: null, version: null, state: 'missing' };
|
|
701
|
+
if (CLI_TOOLS[toolKey]) {
|
|
702
|
+
const { candidates } = collect(toolKey);
|
|
703
|
+
let firstFailed = null;
|
|
704
|
+
let unprobed = false;
|
|
705
|
+
for (const candidate of candidates) {
|
|
706
|
+
const spec = specOf(candidate, toolKey);
|
|
707
|
+
if (spec.kind === 'unsupported') continue;
|
|
708
|
+
const hit = versions.get(versionKey(spec));
|
|
709
|
+
if (hit && !hit.result.ok && now() - hit.at < FAILURE_KEEP_MS) {
|
|
710
|
+
if (!firstFailed) firstFailed = { candidate, spec, hit };
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
if (!hit) unprobed = true;
|
|
714
|
+
value = { tool: toolKey, path: candidate.file, launchPath: spec.launchPath, spec, foundBy: candidate.foundBy, version: hit?.result?.version || null, state: hit?.result?.ok ? 'installed' : 'unverified' };
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
if (!value.path && firstFailed) {
|
|
718
|
+
value = { tool: toolKey, path: firstFailed.candidate.file, launchPath: firstFailed.spec.launchPath, spec: firstFailed.spec, foundBy: firstFailed.candidate.foundBy, version: firstFailed.hit.result.version || null, state: 'broken', error: firstFailed.hit.result.error };
|
|
719
|
+
}
|
|
720
|
+
if (unprobed) inBackground(toolKey);
|
|
721
|
+
}
|
|
722
|
+
launchMemo.set(toolKey, { at: now(), osKey, value });
|
|
723
|
+
return value;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/** Forget what was found: the next answer reads the OS PATH and probes again. */
|
|
727
|
+
function invalidate() {
|
|
728
|
+
versions.clear();
|
|
729
|
+
launchMemo.clear();
|
|
730
|
+
lastStatus.clear();
|
|
731
|
+
forgetAugmentedPath();
|
|
732
|
+
try { osPath.invalidate?.(); } catch { /* nothing to forget */ }
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return {
|
|
736
|
+
resolve,
|
|
737
|
+
resolveAll,
|
|
738
|
+
launcher,
|
|
739
|
+
invalidate,
|
|
740
|
+
last: (toolKey) => lastStatus.get(toolKey) || null,
|
|
741
|
+
candidates: (toolKey, options) => collect(toolKey, options),
|
|
742
|
+
spawnArgs: (spec, args) => spawnArgsFor(spec, args, { env }),
|
|
743
|
+
};
|
|
744
|
+
}
|