synabun 2.1.0 → 2.2.0
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 +121 -0
- package/README.md +1 -1
- package/docs/whatsapp.md +11 -11
- package/lib/client-config-repair.js +13 -9
- package/lib/external-tools.js +6 -5
- package/lib/update-snapshot.js +77 -3
- package/lib/virustotal.mjs +12 -1
- package/mcp-server/dist/index.js +1 -1
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -1
- package/mcp-server/dist/services/budget-judgments.js +5 -3
- package/mcp-server/dist/services/budget-judgments.js.map +1 -1
- package/mcp-server/dist/services/identity.d.ts +2 -2
- package/mcp-server/dist/services/identity.js +2 -2
- package/mcp-server/dist/services/memory-sessions.d.ts +1 -1
- package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -1
- package/mcp-server/dist/services/memory-sessions.js +28 -2
- package/mcp-server/dist/services/memory-sessions.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +2 -1
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
- package/mcp-server/dist/services/server-instructions.js +2 -1
- package/mcp-server/dist/services/server-instructions.js.map +1 -1
- package/mcp-server/dist/services/tool-catalog.js +1 -1
- package/mcp-server/dist/services/tool-catalog.js.map +1 -1
- package/mcp-server/dist/services/tool-list.d.ts +1 -1
- package/mcp-server/dist/services/tool-list.d.ts.map +1 -1
- package/mcp-server/dist/services/tool-runtime.d.ts +2 -2
- package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -1
- package/mcp-server/dist/services/tool-runtime.js +1 -1
- package/mcp-server/dist/services/tool-runtime.js.map +1 -1
- package/mcp-server/dist/services/typesafe-config.js +1 -1
- package/mcp-server/dist/services/typesafe-config.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +8 -8
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +10 -9
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/choice.d.ts +6 -0
- package/mcp-server/dist/tools/choice.d.ts.map +1 -1
- package/mcp-server/dist/tools/choice.js +18 -13
- package/mcp-server/dist/tools/choice.js.map +1 -1
- package/mcp-server/dist/tools/git-tools.js +2 -2
- package/mcp-server/dist/tools/git-tools.js.map +1 -1
- package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
- package/mcp-server/dist/tools/morelogin.js +9 -6
- package/mcp-server/dist/tools/morelogin.js.map +1 -1
- package/mcp-server/dist/tools/skill-tools.d.ts +8 -8
- package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/skill-tools.js +11 -8
- package/mcp-server/dist/tools/skill-tools.js.map +1 -1
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/src/index.ts +1 -1
- package/mcp-server/src/services/budget-judgments.ts +5 -3
- package/mcp-server/src/services/identity.ts +2 -2
- package/mcp-server/src/services/memory-sessions.ts +24 -3
- package/mcp-server/src/services/neural-interface.ts +2 -1
- package/mcp-server/src/services/server-instructions.ts +2 -1
- package/mcp-server/src/services/tool-catalog.ts +1 -1
- package/mcp-server/src/services/tool-list.ts +1 -1
- package/mcp-server/src/services/tool-runtime.ts +2 -2
- package/mcp-server/src/services/typesafe-config.ts +1 -1
- package/mcp-server/src/tools/agents.ts +10 -9
- package/mcp-server/src/tools/choice.ts +17 -12
- package/mcp-server/src/tools/git-tools.ts +2 -2
- package/mcp-server/src/tools/morelogin.ts +6 -4
- package/mcp-server/src/tools/skill-tools.ts +11 -8
- package/neural-interface/i18n/en.json +501 -39
- package/neural-interface/i18n/pt-BR.json +501 -39
- package/neural-interface/lib/agent-sandbox-elevation.js +6 -0
- package/neural-interface/lib/antigravity/acp-client-host.js +554 -0
- package/neural-interface/lib/antigravity/acp-connection.js +299 -0
- package/neural-interface/lib/antigravity/bypass.js +100 -0
- package/neural-interface/lib/antigravity/catalog.js +273 -0
- package/neural-interface/lib/antigravity/home.js +260 -0
- package/neural-interface/lib/antigravity/migrate-gemini.js +139 -0
- package/neural-interface/lib/antigravity/panel-bridge.js +1058 -0
- package/neural-interface/lib/antigravity/policy-hook.mjs +78 -0
- package/neural-interface/lib/antigravity/policy.js +74 -0
- package/neural-interface/lib/antigravity/tool-call.js +178 -0
- package/neural-interface/lib/antigravity/transcripts.js +252 -0
- package/neural-interface/lib/antigravity/usage.js +43 -0
- package/neural-interface/lib/assistant-api.js +1 -0
- package/neural-interface/lib/assistant-brains/antigravity.js +423 -0
- package/neural-interface/lib/assistant-budget-planner.js +7 -3
- package/neural-interface/lib/assistant-budget-split.js +42 -15
- package/neural-interface/lib/assistant-catalog.js +76 -9
- package/neural-interface/lib/assistant-config.js +6 -3
- package/neural-interface/lib/assistant-dispatch.js +207 -49
- package/neural-interface/lib/assistant-envelope.js +153 -1
- package/neural-interface/lib/assistant-evidence.js +80 -0
- package/neural-interface/lib/assistant-hidden-models.js +7 -5
- package/neural-interface/lib/assistant-persona.js +16 -11
- package/neural-interface/lib/assistant-plan-permissions.js +15 -0
- package/neural-interface/lib/assistant-router.js +237 -35
- package/neural-interface/lib/assistant-runtime.js +158 -12
- package/neural-interface/lib/assistant-task-prompt.js +10 -8
- package/neural-interface/lib/assistant-tools.js +2 -2
- package/neural-interface/lib/assistant-usage.js +29 -0
- package/neural-interface/lib/backup-service.js +6 -1
- package/neural-interface/lib/browser-guard-cdp.js +5 -4
- package/neural-interface/lib/browser-network-guard.js +274 -82
- package/neural-interface/lib/browser-tool-policy.js +12 -7
- package/neural-interface/lib/claude-agent-bridge.js +7 -0
- package/neural-interface/lib/claude-history-cache.js +210 -0
- package/neural-interface/lib/claude-model-catalog.js +5 -1
- package/neural-interface/lib/cli-install-api.js +1 -1
- package/neural-interface/lib/cli-install.js +160 -31
- package/neural-interface/lib/cli-resolver.js +9 -2
- package/neural-interface/lib/cli-update-planner.js +25 -15
- package/neural-interface/lib/codex-rollout-reader.js +52 -0
- package/neural-interface/lib/codex-worker-app-server.js +25 -6
- package/neural-interface/lib/editor-git-api.js +3 -20
- package/neural-interface/lib/exec-loop-command.js +9 -2
- package/neural-interface/lib/file-explorer-api.js +74 -40
- package/neural-interface/lib/file-explorer-watch.js +9 -3
- package/neural-interface/lib/git-bash/install.js +52 -6
- package/neural-interface/lib/git-bash/manager.js +59 -17
- package/neural-interface/lib/git-bash/pins.js +12 -0
- package/neural-interface/lib/git-read.js +2 -2
- package/neural-interface/lib/git-status-runner.js +390 -0
- package/neural-interface/lib/git-status-watchdog.js +40 -0
- package/neural-interface/lib/greeting/budgets.js +1 -0
- package/neural-interface/lib/greeting/engine.js +2 -2
- package/neural-interface/lib/greeting/sources.js +10 -12
- package/neural-interface/lib/greeting/writer.js +1 -1
- package/neural-interface/lib/mcp-client-config.js +37 -6
- package/neural-interface/lib/mcp-external-servers.js +13 -4
- package/neural-interface/lib/mcp-installer.js +2 -1
- package/neural-interface/lib/memory-session-capture.js +17 -3
- package/neural-interface/lib/morelogin.js +147 -25
- package/neural-interface/lib/native-loop-providers.js +420 -21
- package/neural-interface/lib/native-loop-runtime.js +23 -2
- package/neural-interface/lib/opencode-native-boundary.js +11 -2
- package/neural-interface/lib/owner-authority.js +1 -1
- package/neural-interface/lib/path-confine.js +12 -9
- package/neural-interface/lib/peer-identity.js +32 -2
- package/neural-interface/lib/pty-host/core.js +28 -1
- package/neural-interface/lib/pty-host/lifetime.js +12 -0
- package/neural-interface/lib/pty-host/manager.js +12 -1
- package/neural-interface/lib/remote-policy.js +1 -1
- package/neural-interface/lib/request-gate.js +3 -0
- package/neural-interface/lib/rulesets/api.js +6 -5
- package/neural-interface/lib/rulesets/fs-safe.js +1 -1
- package/neural-interface/lib/rulesets/installer.js +89 -10
- package/neural-interface/lib/rulesets/render.js +1 -1
- package/neural-interface/lib/session-cache-builder.js +64 -1
- package/neural-interface/lib/skills/catalog.js +5 -1
- package/neural-interface/lib/skills/creation-brief.js +1 -1
- package/neural-interface/lib/skills/creation-sources.js +19 -1
- package/neural-interface/lib/skills/deploy.js +9 -4
- package/neural-interface/lib/skills/draft-confinement.js +3 -1
- package/neural-interface/lib/skills/eval-confinement.js +3 -1
- package/neural-interface/lib/skills/eval-trace.js +16 -0
- package/neural-interface/lib/skills/export.js +5 -2
- package/neural-interface/lib/skills/known-bridge-digests.json +58 -4
- package/neural-interface/lib/skills/ownership.js +1 -1
- package/neural-interface/lib/skills/roots.js +10 -3
- package/neural-interface/lib/skills/runtime-checks.js +6 -3
- package/neural-interface/lib/skills/service.js +15 -7
- package/neural-interface/lib/skills/telemetry.js +6 -0
- package/neural-interface/lib/synabun-version.js +1 -1
- package/neural-interface/lib/system-activity.js +1 -1
- package/neural-interface/lib/whatsapp/brain.js +7 -3
- package/neural-interface/lib/whatsapp/cards.js +22 -9
- package/neural-interface/lib/whatsapp/config.js +1 -1
- package/neural-interface/lib/worker-confinement.js +77 -1
- package/neural-interface/lib/worker-question.js +74 -0
- package/neural-interface/lib/worker-resume.js +13 -4
- package/neural-interface/lib/worker-sandbox.js +46 -16
- package/neural-interface/package-lock.json +18 -2
- package/neural-interface/package.json +2 -1
- package/neural-interface/public/onboarding.html +1 -1
- package/neural-interface/public/shared/agent-runtime-options.js +89 -11
- package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
- package/neural-interface/public/shared/agy/agy-client.js +266 -0
- package/neural-interface/public/shared/agy/agy-commands.js +120 -0
- package/neural-interface/public/shared/agy/agy-copy.js +326 -0
- package/neural-interface/public/shared/agy/agy-diff.js +161 -0
- package/neural-interface/public/shared/agy/agy-events.js +469 -0
- package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
- package/neural-interface/public/shared/agy/agy-options.js +232 -0
- package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
- package/neural-interface/public/shared/agy/agy-render.js +392 -0
- package/neural-interface/public/shared/agy/agy-setup.js +149 -0
- package/neural-interface/public/shared/agy/agy-styles.js +604 -0
- package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
- package/neural-interface/public/shared/api.js +4 -4
- package/neural-interface/public/shared/assistant/asst-agents-dock.js +3 -4
- package/neural-interface/public/shared/assistant/asst-api.js +1 -1
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +56 -9
- package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
- package/neural-interface/public/shared/assistant/asst-control.js +48 -0
- package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
- package/neural-interface/public/shared/assistant/asst-limit-dock.js +6 -1
- package/neural-interface/public/shared/assistant/asst-limit-wait.js +38 -5
- package/neural-interface/public/shared/assistant/asst-panel.js +45 -8
- package/neural-interface/public/shared/assistant/asst-render.js +51 -12
- package/neural-interface/public/shared/assistant/asst-route.js +237 -57
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
- package/neural-interface/public/shared/assistant/asst-scenes.js +54 -1
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +51 -7
- package/neural-interface/public/shared/assistant/asst-slash.js +8 -2
- package/neural-interface/public/shared/assistant/asst-state.js +76 -9
- package/neural-interface/public/shared/assistant/asst-styles.js +34 -0
- package/neural-interface/public/shared/assistant/asst-tool-kinds.js +34 -7
- package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
- package/neural-interface/public/shared/cdx/cdx-output.js +26 -3
- package/neural-interface/public/shared/cdx/cdx-panel.js +14 -2
- package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
- package/neural-interface/public/shared/cdx/cdx-protocol.js +11 -10
- package/neural-interface/public/shared/cdx/cdx-render.js +10 -9
- package/neural-interface/public/shared/cdx/cdx-requests.js +11 -1
- package/neural-interface/public/shared/cdx/cdx-styles.js +4 -0
- package/neural-interface/public/shared/cdx/cdx-tabs.js +75 -5
- package/neural-interface/public/shared/cli-install-controller.js +218 -0
- package/neural-interface/public/shared/cli-install-view.js +7 -4
- package/neural-interface/public/shared/cli-status.js +9 -8
- package/neural-interface/public/shared/constants.js +3 -2
- package/neural-interface/public/shared/context-menu-model.js +1 -0
- package/neural-interface/public/shared/cp/cp-restore.js +162 -0
- package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
- package/neural-interface/public/shared/desktop-bridge.js +17 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
- package/neural-interface/public/shared/desktop-titlebar.js +13 -4
- package/neural-interface/public/shared/desktop-update-view.js +1 -0
- package/neural-interface/public/shared/explorer/fx-api.js +4 -4
- package/neural-interface/public/shared/explorer/fx-links.js +3 -3
- package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
- package/neural-interface/public/shared/explorer/fx-main.js +48 -16
- package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
- package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
- package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
- package/neural-interface/public/shared/html-shell.js +21 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +27 -7
- package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +17 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +46 -3
- package/neural-interface/public/shared/provider-icons.js +4 -2
- package/neural-interface/public/shared/settings/settings-rules.js +2 -2
- package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +1 -1
- package/neural-interface/public/shared/settings/settings-skill-folders.js +2 -2
- package/neural-interface/public/shared/showcase-privacy.js +6 -1
- package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
- package/neural-interface/public/shared/skills/ss-creation.js +6 -3
- package/neural-interface/public/shared/skills/ss-dialog.js +5 -2
- package/neural-interface/public/shared/skills/ss-lint.js +9 -1
- package/neural-interface/public/shared/skills/ss-profiles.js +44 -6
- package/neural-interface/public/shared/skills/ss-review-view.js +2 -2
- package/neural-interface/public/shared/skills/studio/sx-actions.js +1 -1
- package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +1 -1
- package/neural-interface/public/shared/skills/studio/sx-create-ai.js +2 -1
- package/neural-interface/public/shared/skills/studio/sx-dom.js +2 -1
- package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
- package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
- package/neural-interface/public/shared/skills/studio/sx-inspector.js +1 -1
- package/neural-interface/public/shared/skills/studio/sx-install-flow.js +9 -6
- package/neural-interface/public/shared/skills/studio/sx-install.js +20 -3
- package/neural-interface/public/shared/skills/studio/sx-library-view.js +144 -35
- package/neural-interface/public/shared/skills/studio/sx-library.js +6 -2
- package/neural-interface/public/shared/skills/studio/sx-main.js +11 -2
- package/neural-interface/public/shared/skills/studio/sx-review-view.js +1 -1
- package/neural-interface/public/shared/skills/studio/sx-subject.js +7 -6
- package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +6 -5
- package/neural-interface/public/shared/state.js +1 -1
- package/neural-interface/public/shared/streaming-markdown.js +58 -0
- package/neural-interface/public/shared/styles.css +354 -34
- package/neural-interface/public/shared/text-cache.js +29 -0
- package/neural-interface/public/shared/tool-list-picker.js +2 -2
- package/neural-interface/public/shared/tool-list-view.js +2 -2
- package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
- package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
- package/neural-interface/public/shared/ui-assistant.js +2 -0
- package/neural-interface/public/shared/ui-automation-studio.js +20 -12
- package/neural-interface/public/shared/ui-claude-panel.js +299 -77
- package/neural-interface/public/shared/ui-cli-banner.js +23 -167
- package/neural-interface/public/shared/ui-file-explorer.js +48 -6
- package/neural-interface/public/shared/ui-file-links.js +2 -2
- package/neural-interface/public/shared/ui-games.js +2 -2
- package/neural-interface/public/shared/ui-judgments.js +1 -1
- package/neural-interface/public/shared/ui-keybinds.js +8 -2
- package/neural-interface/public/shared/ui-link.js +3 -3
- package/neural-interface/public/shared/ui-menubar.js +2 -2
- package/neural-interface/public/shared/ui-native-loop-router.js +4 -0
- package/neural-interface/public/shared/ui-navbar.js +45 -0
- package/neural-interface/public/shared/ui-notifications.js +1 -0
- package/neural-interface/public/shared/ui-resume.js +9 -7
- package/neural-interface/public/shared/ui-schedules-studio.js +41 -10
- package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
- package/neural-interface/public/shared/ui-sessions.js +22 -12
- package/neural-interface/public/shared/ui-settings.js +90 -65
- package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -3
- package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
- package/neural-interface/public/shared/ui-sidepanel-windows.js +3 -3
- package/neural-interface/public/shared/ui-terminal.js +146 -10
- package/neural-interface/public/shared/ui-tool-updates.js +3 -3
- package/neural-interface/public/shared/ui-tooltip.js +71 -46
- package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
- package/neural-interface/public/shared/ui-whiteboard.js +1 -1
- package/neural-interface/public/shared/ui-worker-end.js +1 -1
- package/neural-interface/public/shared/whatsapp/wa-view.js +2 -2
- package/neural-interface/public/shared/worker-questions.js +34 -0
- package/neural-interface/public/sidepanel-session.html +1 -1
- package/neural-interface/server.js +644 -426
- package/neural-interface/templates/rulesets/README.md +1 -1
- package/neural-interface/templates/rulesets/legacy-hashes.json +5 -5
- package/neural-interface/templates/rulesets/manifest.json +1 -1
- package/npm-shrinkwrap.json +18 -2
- package/package.json +2 -1
- package/skills/skill-forge/SKILL.md +4 -4
- package/skills/synabun/SKILL.md +1 -1
- package/skills/synabun/modules/schedule.md +3 -3
- /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ═══════════════════════════════════════════
|
|
2
2
|
// SynaBun — CLI Update Planner
|
|
3
3
|
//
|
|
4
|
-
// Works out HOW each external CLI (Claude Code, Codex, OpenCode,
|
|
4
|
+
// Works out HOW each external CLI (Claude Code, Codex, OpenCode, Antigravity) was
|
|
5
5
|
// installed, and therefore which command actually updates it and which
|
|
6
6
|
// "latest version" it should be compared against.
|
|
7
7
|
//
|
|
@@ -19,12 +19,14 @@
|
|
|
19
19
|
/**
|
|
20
20
|
* npm package name per tool key. OpenCode has none: npm's opencode-ai is
|
|
21
21
|
* OpenCode 1, which SynaBun does not drive, and OpenCode 2 is installed by its
|
|
22
|
-
* own script, Homebrew or WinGet (lib/opencode-protocol.js).
|
|
22
|
+
* own script, Homebrew or WinGet (lib/opencode-protocol.js). Neither has the
|
|
23
|
+
* Antigravity CLI (`agy`): Google ships it as a binary, through its own
|
|
24
|
+
* installer script or the Homebrew cask antigravity-cli, and it updates itself
|
|
25
|
+
* (`agy update`).
|
|
23
26
|
*/
|
|
24
27
|
export const NPM_PKGS = {
|
|
25
28
|
'claude-code': '@anthropic-ai/claude-code',
|
|
26
29
|
'codex': '@openai/codex',
|
|
27
|
-
'gemini': '@google/gemini-cli',
|
|
28
30
|
};
|
|
29
31
|
|
|
30
32
|
/**
|
|
@@ -35,22 +37,23 @@ export const NPM_PKGS = {
|
|
|
35
37
|
export const BREW_FALLBACK_TOKENS = {
|
|
36
38
|
'claude-code': 'claude-code',
|
|
37
39
|
'codex': 'codex',
|
|
38
|
-
'
|
|
40
|
+
'antigravity': 'antigravity-cli',
|
|
39
41
|
// OpenCode 2's own tap (its `opencode` formula there is still OpenCode 1).
|
|
40
42
|
'opencode': 'anomalyco/tap/opencode-v2',
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
46
|
* Whether each tool ships as a Homebrew cask or a formula. This is a property
|
|
45
|
-
* of the package, not of the machine — claude-code
|
|
46
|
-
* casks,
|
|
47
|
+
* of the package, not of the machine — claude-code, codex and antigravity-cli
|
|
48
|
+
* exist ONLY as casks, opencode ONLY as a formula. Needed when a tool is not
|
|
47
49
|
* installed and there is no path to classify: guessing the wrong flavour
|
|
48
|
-
* produces a command that simply errors ("
|
|
50
|
+
* produces a command that simply errors ("No available formula with the name
|
|
51
|
+
* 'antigravity-cli'").
|
|
49
52
|
*/
|
|
50
53
|
export const BREW_KINDS = {
|
|
51
54
|
'claude-code': 'cask',
|
|
52
55
|
'codex': 'cask',
|
|
53
|
-
'
|
|
56
|
+
'antigravity': 'cask',
|
|
54
57
|
'opencode': 'formula',
|
|
55
58
|
};
|
|
56
59
|
|
|
@@ -59,7 +62,7 @@ const SELF_UPDATERS = {
|
|
|
59
62
|
'claude-code': 'claude update',
|
|
60
63
|
'codex': 'codex update',
|
|
61
64
|
'opencode': 'opencode upgrade',
|
|
62
|
-
|
|
65
|
+
'antigravity': 'agy update',
|
|
63
66
|
};
|
|
64
67
|
|
|
65
68
|
/**
|
|
@@ -199,7 +202,7 @@ const TOOL_COMMANDS = {
|
|
|
199
202
|
'claude-code': 'claude',
|
|
200
203
|
'codex': 'codex',
|
|
201
204
|
'opencode': 'opencode',
|
|
202
|
-
'
|
|
205
|
+
'antigravity': 'agy',
|
|
203
206
|
};
|
|
204
207
|
|
|
205
208
|
/**
|
|
@@ -378,8 +381,11 @@ export function installSourceLabel(source) {
|
|
|
378
381
|
* (checked 2026-10-17): Anthropic's native installer (code.claude.com/docs/en/setup),
|
|
379
382
|
* OpenAI's standalone installer (learn.chatgpt.com/docs/codex/cli), OpenCode 2's
|
|
380
383
|
* install script (https://opencode.ai/v2/install, the one `opencode upgrade`
|
|
381
|
-
* runs; on Windows, where the script does not run, WinGet)
|
|
382
|
-
*
|
|
384
|
+
* runs; on Windows, where the script does not run, WinGet), and (checked
|
|
385
|
+
* 2026-10-30) Google's Antigravity CLI installer
|
|
386
|
+
* (antigravity.google/docs/getting-started). These are the same official
|
|
387
|
+
* sources the Install button runs (lib/cli-install.js), which on a Mac with
|
|
388
|
+
* Homebrew installs the Antigravity CLI's cask instead.
|
|
383
389
|
*/
|
|
384
390
|
export function vendorInstallCommand(toolKey, platform = process.platform) {
|
|
385
391
|
const windows = platform === 'win32';
|
|
@@ -392,6 +398,10 @@ export function vendorInstallCommand(toolKey, platform = process.platform) {
|
|
|
392
398
|
: 'curl -fsSL https://chatgpt.com/codex/install.sh | sh';
|
|
393
399
|
case 'opencode':
|
|
394
400
|
return windows ? 'winget install --id Anomaly.OpenCode --exact' : 'curl -fsSL https://opencode.ai/v2/install | bash';
|
|
401
|
+
case 'antigravity':
|
|
402
|
+
// Google's installer: it checks the binary against the SHA-512 of Google's release manifest and puts agy in
|
|
403
|
+
// ~/.local/bin (%LOCALAPPDATA%\agy\bin on Windows). A Homebrew machine is told the cask (buildInstallCommand).
|
|
404
|
+
return windows ? 'irm https://antigravity.google/cli/install.ps1 | iex' : 'curl -fsSL https://antigravity.google/cli/install.sh | bash';
|
|
395
405
|
default:
|
|
396
406
|
return NPM_PKGS[toolKey] ? `npm install -g ${NPM_PKGS[toolKey]}` : '';
|
|
397
407
|
}
|
|
@@ -423,9 +433,9 @@ export function buildInstallCommand(toolKey, installSource = 'unknown', { platfo
|
|
|
423
433
|
? `brew install --cask ${token}`
|
|
424
434
|
: `brew install ${token}`;
|
|
425
435
|
}
|
|
426
|
-
// OpenCode 2
|
|
427
|
-
|
|
428
|
-
if (!npmPkg) return
|
|
436
|
+
// OpenCode 2 and the Antigravity CLI have no npm package: their vendor's installer whatever JavaScript package
|
|
437
|
+
// manager the machine uses.
|
|
438
|
+
if (!npmPkg) return vendorInstallCommand(toolKey, platform || process.platform);
|
|
429
439
|
switch (installSource) {
|
|
430
440
|
case 'bun': return `bun add -g ${npmPkg}`;
|
|
431
441
|
case 'pnpm': return `pnpm add -g ${npmPkg}`;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// ── A Codex rollout, read once for the preview and the history fallback ──
|
|
2
|
+
// The early preview (server.js sendEarlyCodexRolloutPreview) used to readFileSync the
|
|
3
|
+
// whole rollout JSONL before Codex's app-server started, for every thread a tab
|
|
4
|
+
// bootstrapped, and the history fallback read it again right after. This reads it as
|
|
5
|
+
// a stream (the event loop and the app-server's start are not held up) and keeps the
|
|
6
|
+
// parsed items of the last few files by path, size and time, so the fallback that
|
|
7
|
+
// follows reuses them while the file is unchanged.
|
|
8
|
+
|
|
9
|
+
import { createReadStream, statSync } from 'node:fs';
|
|
10
|
+
import { jsonlLines } from './jsonl-lines.js';
|
|
11
|
+
|
|
12
|
+
export function createRolloutReader({
|
|
13
|
+
open = (path) => createReadStream(path, { encoding: 'utf-8' }),
|
|
14
|
+
stat = statSync,
|
|
15
|
+
maxEntries = 4,
|
|
16
|
+
} = {}) {
|
|
17
|
+
const kept = new Map(); // path → { size, mtimeMs, items }
|
|
18
|
+
const running = new Map(); // `${path}|${size}|${mtimeMs}` → promise of items
|
|
19
|
+
const stats = { reads: 0, reused: 0 };
|
|
20
|
+
|
|
21
|
+
/** The items `parse(lines)` makes of the file at `path` (an async iterable of its lines). */
|
|
22
|
+
async function items(path, parse) {
|
|
23
|
+
if (!path) return [];
|
|
24
|
+
let st;
|
|
25
|
+
try { st = stat(path); } catch { return []; }
|
|
26
|
+
const hit = kept.get(path);
|
|
27
|
+
if (hit && hit.size === st.size && hit.mtimeMs === st.mtimeMs) {
|
|
28
|
+
stats.reused++;
|
|
29
|
+
kept.delete(path); kept.set(path, hit);
|
|
30
|
+
return hit.items;
|
|
31
|
+
}
|
|
32
|
+
const key = `${path}|${st.size}|${st.mtimeMs}`;
|
|
33
|
+
if (running.has(key)) { stats.reused++; return running.get(key); }
|
|
34
|
+
const job = (async () => {
|
|
35
|
+
stats.reads++;
|
|
36
|
+
const stream = open(path);
|
|
37
|
+
try {
|
|
38
|
+
const out = await parse(jsonlLines(stream));
|
|
39
|
+
kept.delete(path);
|
|
40
|
+
kept.set(path, { size: st.size, mtimeMs: st.mtimeMs, items: out });
|
|
41
|
+
while (kept.size > maxEntries) kept.delete(kept.keys().next().value);
|
|
42
|
+
return out;
|
|
43
|
+
} finally {
|
|
44
|
+
stream.destroy?.();
|
|
45
|
+
}
|
|
46
|
+
})();
|
|
47
|
+
running.set(key, job);
|
|
48
|
+
try { return await job; } finally { running.delete(key); }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { items, forget: (path) => kept.delete(path), get stats() { return { ...stats }; }, get size() { return kept.size; } };
|
|
52
|
+
}
|
|
@@ -27,7 +27,12 @@ export function codexWorkerConfigOverrides(config) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export class CodexWorkerAppServer {
|
|
30
|
-
constructor({ codexPathOverride, env, config = {}, configArgs = [], apiKey, signal, onProcess = () => {} }) {
|
|
30
|
+
constructor({ codexPathOverride, env, config = {}, configArgs = [], apiKey, signal, onProcess = () => {}, onRequest = null, permissionPolicy = 'auto' }) {
|
|
31
|
+
this.onRequest = onRequest;
|
|
32
|
+
this.approvalPolicy = permissionPolicy === 'ask' ? 'untrusted' : 'never';
|
|
33
|
+
this.requestAbort = new AbortController();
|
|
34
|
+
this.incoming = new Map();
|
|
35
|
+
this.requestAbort.signal.addEventListener('abort', () => { for (const pending of this.incoming.values()) pending.abort(); this.incoming.clear(); }, { once: true });
|
|
31
36
|
this.pending = new Map();
|
|
32
37
|
this.seq = 0;
|
|
33
38
|
this.listener = null;
|
|
@@ -52,9 +57,21 @@ export class CodexWorkerAppServer {
|
|
|
52
57
|
this.pending.delete(msg.id); clearTimeout(p.timer);
|
|
53
58
|
msg.error ? p.reject(new Error(msg.error.message || 'Codex RPC failed')) : p.resolve(msg.result);
|
|
54
59
|
} else if (msg.id != null) {
|
|
55
|
-
|
|
56
|
-
this.
|
|
57
|
-
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
this.incoming.set(msg.id, controller);
|
|
62
|
+
Promise.resolve().then(() => {
|
|
63
|
+
if (!this.onRequest) throw new Error('Worker has no interactive request handler');
|
|
64
|
+
return this.onRequest(msg, controller.signal);
|
|
65
|
+
}).then((reply) => {
|
|
66
|
+
if (!this.failure && !controller.signal.aborted) this.write({ id: msg.id, ...reply });
|
|
67
|
+
}, (error) => {
|
|
68
|
+
if (!this.failure && !controller.signal.aborted) this.write({ id: msg.id, error: { code: -32000, message: error.message } });
|
|
69
|
+
}).finally(() => this.incoming.delete(msg.id));
|
|
70
|
+
} else {
|
|
71
|
+
if (msg.method === 'serverRequest/resolved') this.incoming.get(msg.params?.requestId)?.abort();
|
|
72
|
+
if (msg.method === 'turn/completed') { for (const pending of this.incoming.values()) pending.abort(); this.incoming.clear(); }
|
|
73
|
+
this.listener?.(msg);
|
|
74
|
+
}
|
|
58
75
|
}
|
|
59
76
|
});
|
|
60
77
|
// Drain stderr without exposing credentials/config to the worker transcript.
|
|
@@ -76,6 +93,7 @@ export class CodexWorkerAppServer {
|
|
|
76
93
|
write(value) { if (!this.proc.stdin.destroyed) this.proc.stdin.write(JSON.stringify(value) + '\n'); }
|
|
77
94
|
fail(error) {
|
|
78
95
|
this.failure = error;
|
|
96
|
+
this.requestAbort.abort();
|
|
79
97
|
for (const p of this.pending.values()) { clearTimeout(p.timer); p.reject(error); }
|
|
80
98
|
this.pending.clear();
|
|
81
99
|
this.listener?.({ method: 'synabun/exit', error });
|
|
@@ -99,7 +117,7 @@ export class CodexWorkerAppServer {
|
|
|
99
117
|
const ready = this.ready.then(async () => {
|
|
100
118
|
const reply = await this.rpc(id ? 'thread/resume' : 'thread/start', {
|
|
101
119
|
...(id ? { threadId: id } : {}), cwd: options.workingDirectory,
|
|
102
|
-
model: options.model || null, approvalPolicy:
|
|
120
|
+
model: options.model || null, approvalPolicy: owner.approvalPolicy, sandbox: options.sandboxMode,
|
|
103
121
|
config: { web_search: options.webSearchMode || 'disabled', sandbox_workspace_write: { network_access: !!options.networkAccessEnabled, writable_roots: options.additionalDirectories || [] } },
|
|
104
122
|
});
|
|
105
123
|
const actual = reply?.thread?.id;
|
|
@@ -141,7 +159,7 @@ export class CodexWorkerAppServer {
|
|
|
141
159
|
signal?.addEventListener('abort', abort, { once: true });
|
|
142
160
|
try {
|
|
143
161
|
if (signal?.aborted) throw Object.assign(new Error('Codex turn aborted'), { name: 'AbortError' });
|
|
144
|
-
const reply = await owner.rpc('turn/start', { threadId: id, input: [{ type: 'text', text: String(prompt), text_elements: [] }], approvalPolicy:
|
|
162
|
+
const reply = await owner.rpc('turn/start', { threadId: id, input: [{ type: 'text', text: String(prompt), text_elements: [] }], approvalPolicy: owner.approvalPolicy, sandboxPolicy, effort: options.modelReasoningEffort || null, ...(outputSchema ? { outputSchema } : {}) });
|
|
145
163
|
turnId = reply?.turn?.id;
|
|
146
164
|
if (signal?.aborted) abort();
|
|
147
165
|
} catch (error) { owner.listener = null; signal?.removeEventListener('abort', abort); throw error; }
|
|
@@ -159,6 +177,7 @@ export class CodexWorkerAppServer {
|
|
|
159
177
|
};
|
|
160
178
|
}
|
|
161
179
|
async dispose() {
|
|
180
|
+
this.requestAbort.abort();
|
|
162
181
|
this.startupSignal?.removeEventListener('abort', this.cancelStartup);
|
|
163
182
|
if (this.proc.exitCode !== null || this.proc.signalCode) return;
|
|
164
183
|
if (process.platform === 'win32') {
|
|
@@ -18,13 +18,12 @@
|
|
|
18
18
|
// not an error. Dependencies are injected: the router is tested without server.js.
|
|
19
19
|
|
|
20
20
|
import { Router } from 'express';
|
|
21
|
-
import { execFile } from 'node:child_process';
|
|
22
21
|
import { statSync } from 'node:fs';
|
|
23
22
|
import { dirname, isAbsolute, relative, sep } from 'node:path';
|
|
24
23
|
import { PathConfineError } from './path-confine.js';
|
|
25
24
|
import { MAX_FILE_BYTES, canonicalInside } from './confined-fs.js';
|
|
26
25
|
import { useFileRoot } from './editor-files-api.js';
|
|
27
|
-
import {
|
|
26
|
+
import { gitStatusRunner } from './git-status-runner.js';
|
|
28
27
|
|
|
29
28
|
/** What this route adds to the editor (GET /api/editor/capabilities). */
|
|
30
29
|
export const EDITOR_GIT_CAPABILITIES = Object.freeze(['git-head']);
|
|
@@ -32,9 +31,6 @@ export const GIT_TIMEOUT_MS = 5000;
|
|
|
32
31
|
|
|
33
32
|
const UTF8_BOM = [0xEF, 0xBB, 0xBF];
|
|
34
33
|
const BINARY_PROBE_BYTES = 8192;
|
|
35
|
-
// Variables that would point git at another repository than the one the file is in.
|
|
36
|
-
const REDIRECTING = ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_NAMESPACE', 'GIT_COMMON_DIR', 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_CEILING_DIRECTORIES'];
|
|
37
|
-
|
|
38
34
|
/**
|
|
39
35
|
* Run git for a read and collect what it prints. Never throws. The arguments are the command's own: what keeps a
|
|
40
36
|
* read from starting a program of the repository's is added here (lib/git-read.js).
|
|
@@ -42,21 +38,8 @@ const REDIRECTING = ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_NAMESPAC
|
|
|
42
38
|
* @param {{ cwd: string, maxBuffer: number, timeoutMs?: number, binary?: string, env?: object }} o
|
|
43
39
|
* @returns {Promise<{ ok: boolean, stdout: Buffer, tooLarge: boolean }>} `tooLarge`: it printed more than `maxBuffer` bytes
|
|
44
40
|
*/
|
|
45
|
-
export function runGit(args,
|
|
46
|
-
|
|
47
|
-
for (const name of REDIRECTING) delete clean[name];
|
|
48
|
-
const failed = { ok: false, stdout: Buffer.alloc(0), tooLarge: false };
|
|
49
|
-
return gitRead(cwd, args, { binary, env: clean }).then((read) => new Promise((done) => {
|
|
50
|
-
try {
|
|
51
|
-
execFile(binary, read.args, { cwd, maxBuffer, timeout: timeoutMs, encoding: 'buffer', windowsHide: true, env: read.env, shell: false }, (error, stdout) => {
|
|
52
|
-
const out = Buffer.isBuffer(stdout) ? stdout : Buffer.alloc(0);
|
|
53
|
-
if (!error) done({ ok: true, stdout: out, tooLarge: false });
|
|
54
|
-
else done({ ok: false, stdout: out, tooLarge: error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER' });
|
|
55
|
-
});
|
|
56
|
-
} catch {
|
|
57
|
-
done(failed);
|
|
58
|
-
}
|
|
59
|
-
})).catch(() => failed);
|
|
41
|
+
export function runGit(args, options = {}) {
|
|
42
|
+
return gitStatusRunner.run(args, { timeoutMs: GIT_TIMEOUT_MS, ...options });
|
|
60
43
|
}
|
|
61
44
|
|
|
62
45
|
/**
|
|
@@ -13,9 +13,12 @@ function codexConfigString(value) {
|
|
|
13
13
|
return JSON.stringify(String(value));
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/** The reasoning levels `agy --effort` accepts (agy --help). */
|
|
17
|
+
const ANTIGRAVITY_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
18
|
+
|
|
16
19
|
/**
|
|
17
20
|
* Split the configurable CLI command into an executable and fixed arguments.
|
|
18
|
-
* Settings intentionally supports prefixes such as `wsl -d Ubuntu
|
|
21
|
+
* Settings intentionally supports prefixes such as `wsl -d Ubuntu agy`.
|
|
19
22
|
* An existing path wins before tokenization so unquoted executable paths with
|
|
20
23
|
* spaces keep working; quoted paths are handled by the tokenizer.
|
|
21
24
|
*/
|
|
@@ -107,8 +110,12 @@ export function buildExecInvocation({
|
|
|
107
110
|
args.push('-');
|
|
108
111
|
break;
|
|
109
112
|
}
|
|
110
|
-
case '
|
|
113
|
+
case 'antigravity':
|
|
114
|
+
// Print mode with the task on stdin (the wrapper redirects it), unattended like every exec loop: Antigravity
|
|
115
|
+
// asks before edits and commands, and there is nobody to answer in a terminal that runs a loop.
|
|
116
|
+
args.push('--output-format', 'text', '--dangerously-skip-permissions');
|
|
111
117
|
if (model) args.push('--model', String(model));
|
|
118
|
+
if (effort && ANTIGRAVITY_EFFORTS.has(String(effort))) args.push('--effort', String(effort));
|
|
112
119
|
break;
|
|
113
120
|
case 'opencode':
|
|
114
121
|
// A private server for this run (`--standalone`; without it the CLI uses
|
|
@@ -74,6 +74,7 @@ import { PathConfineError, isInside } from './path-confine.js';
|
|
|
74
74
|
import { canonicalInside, openConfinedStream, removeConfinedEntry } from './confined-fs.js';
|
|
75
75
|
import { matchFileRoots, useFileRoot } from './editor-files-api.js';
|
|
76
76
|
import { runGit } from './editor-git-api.js';
|
|
77
|
+
import { gitStatusRunner, projectStatus, createIgnoredPrefixes, watchRelative, MAX_IGNORE_FOLDERS } from './git-status-runner.js';
|
|
77
78
|
import { TrashUnavailable, createRevealer, createTrash, renameExclusive } from './file-explorer-system.js';
|
|
78
79
|
import { isAllowedWebSocketOrigin } from './http-guards.js';
|
|
79
80
|
import { createTreeWatcher, watchStrategy } from './file-explorer-watch.js';
|
|
@@ -89,8 +90,6 @@ export const MAX_UPLOAD_BYTES = 512 * 1024 * 1024;
|
|
|
89
90
|
export const MAX_RAW_BYTES = 30 * 1024 * 1024;
|
|
90
91
|
export const MAX_BATCH = 500;
|
|
91
92
|
export const MAX_WATCH_SESSIONS = 16;
|
|
92
|
-
const GIT_MAX_BUFFER = 64 * 1024 * 1024;
|
|
93
|
-
const GIT_STATUS_TIMEOUT_MS = 15000;
|
|
94
93
|
const STAT_PARALLEL = 64;
|
|
95
94
|
const COPY_CHUNK_BYTES = 1024 * 1024;
|
|
96
95
|
const O_NOFOLLOW = constants.O_NOFOLLOW || 0;
|
|
@@ -273,7 +272,7 @@ async function mapLimit(items, limit, work) {
|
|
|
273
272
|
* @returns {import('express').Router & { close: () => void, sessions: () => number }}
|
|
274
273
|
*/
|
|
275
274
|
export function createFileExplorerRouter({
|
|
276
|
-
fileRoots, guard = null, origins = {}, git = runGit, trash = createTrash(), revealer = createRevealer(),
|
|
275
|
+
fileRoots, guard = null, origins = {}, git = runGit, statusRunner = gitStatusRunner, trash = createTrash(), revealer = createRevealer(),
|
|
277
276
|
platform = process.platform, watch = watchStrategy(), watchImpl = undefined, watchClock = {}, io = {}, log = (line) => console.warn(line),
|
|
278
277
|
maxUploadBytes = MAX_UPLOAD_BYTES, listingCap = LISTING_CAP,
|
|
279
278
|
} = {}) {
|
|
@@ -292,8 +291,7 @@ export function createFileExplorerRouter({
|
|
|
292
291
|
// The folders only: a single file the editor is allowed by name is not something the explorer manages.
|
|
293
292
|
const roots = () => ({ dirs: (fileRoots()?.dirs || []).filter(Boolean), files: [] });
|
|
294
293
|
const sessions = new Map(); // watch id → { watcher, root, real, res, written: Map<real dir, written dir> }
|
|
295
|
-
const gitKnown = new Map(); //
|
|
296
|
-
const gitFlights = new Map(); // canonical project folder → the status being read
|
|
294
|
+
const gitKnown = new Map(); // bounded visible-folder listings and ignore prefixes, independent of status revisions
|
|
297
295
|
|
|
298
296
|
function answer(res, error) {
|
|
299
297
|
const done = error?.midway || {};
|
|
@@ -438,6 +436,8 @@ export function createFileExplorerRouter({
|
|
|
438
436
|
dirents.sort((a, b) => (a.isDirectory() !== b.isDirectory() ? (a.isDirectory() ? -1 : 1) : compareNames(a.name, b.name)));
|
|
439
437
|
dirents = dirents.slice(0, listingCap);
|
|
440
438
|
}
|
|
439
|
+
const ignoreResult = await ignoredEntries(dir.real, dir.real, dirents, requestSignal(res), true);
|
|
440
|
+
const ignored = new Set(ignoreResult || []);
|
|
441
441
|
const realBase = realpathSync(dir.base);
|
|
442
442
|
const items = await mapLimit(dirents, STAT_PARALLEL, async (entry) => {
|
|
443
443
|
const full = join(dir.real, entry.name);
|
|
@@ -455,46 +455,67 @@ export function createFileExplorerRouter({
|
|
|
455
455
|
try { const st = await lstat(full); return { name: entry.name, type: 'file', size: st.size, mtime: st.mtimeMs }; }
|
|
456
456
|
catch { return { name: entry.name, type: 'file' }; }
|
|
457
457
|
});
|
|
458
|
+
for (const item of items) {
|
|
459
|
+
if (ignored.has(item.name) || ignored.has(item.name + '/')) item.ignored = true;
|
|
460
|
+
else if (ignoreResult === null) item.ignored = null; // still being checked, not known-normal
|
|
461
|
+
}
|
|
458
462
|
items.sort((a, b) => (a.type !== b.type ? (a.type === 'dir' ? -1 : 1) : compareNames(a.name, b.name)));
|
|
459
463
|
res.json({ ok: true, path: dir.written, items, total, truncated });
|
|
460
464
|
}));
|
|
461
465
|
|
|
462
466
|
// ── The git state of a project ──
|
|
463
|
-
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if (named.ok) branch = named.stdout.toString('utf8').trim();
|
|
474
|
-
if (!branch) {
|
|
475
|
-
const short = await git(['rev-parse', '--short', 'HEAD'], { cwd: real, maxBuffer: 1024 * 1024 });
|
|
476
|
-
if (short.ok) branch = short.stdout.toString('utf8').trim();
|
|
467
|
+
function requestSignal(res) {
|
|
468
|
+
const controller = new AbortController();
|
|
469
|
+
res.once('close', () => controller.abort());
|
|
470
|
+
return controller.signal;
|
|
471
|
+
}
|
|
472
|
+
function ignoreData(real) {
|
|
473
|
+
let known = gitKnown.get(real);
|
|
474
|
+
if (!known) {
|
|
475
|
+
if (gitKnown.size >= 64) gitKnown.delete(gitKnown.keys().next().value);
|
|
476
|
+
gitKnown.set(real, known = { folders: new Map(), prefixes: createIgnoredPrefixes(platform), at: Date.now() });
|
|
477
477
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
478
|
+
if (Date.now() - known.at >= 30000) { known.prefixes.clear(); known.folders.clear(); known.at = Date.now(); }
|
|
479
|
+
return known;
|
|
480
|
+
}
|
|
481
|
+
async function ignoredEntries(real, folder, dirents, signal, cacheOnly = false) {
|
|
482
|
+
const known = ignoreData(real);
|
|
483
|
+
const rel = slashes(relative(real, folder));
|
|
484
|
+
const paths = dirents ? dirents.map(e => `${rel ? rel + '/' : ''}${e.name}${e.isDirectory() ? '/' : ''}`) : known.folders.get(folder)?.paths;
|
|
485
|
+
if (!paths) return null;
|
|
486
|
+
const ignored = await statusRunner.ignored(real, paths, { signal, cacheOnly });
|
|
487
|
+
if (known.folders.size >= MAX_IGNORE_FOLDERS && !known.folders.has(folder)) known.folders.delete(known.folders.keys().next().value);
|
|
488
|
+
known.folders.set(folder, { paths });
|
|
489
|
+
for (const path of ignored || []) known.prefixes.add(path);
|
|
490
|
+
return ignored;
|
|
491
|
+
}
|
|
492
|
+
async function readGit(real, signal) {
|
|
493
|
+
const result = projectStatus(await statusRunner.read(real, { signal }), real);
|
|
494
|
+
if (!result.isGit && result.ok) return { isGit: false };
|
|
495
|
+
const dirs = new Set([real]);
|
|
496
|
+
for (const session of sessions.values()) if (session.real === real) for (const folder of session.written.keys()) dirs.add(folder);
|
|
497
|
+
const known = ignoreData(real), ignored = [];
|
|
498
|
+
let ignoreUnknown = false;
|
|
499
|
+
for (const folder of dirs) {
|
|
500
|
+
try {
|
|
501
|
+
// Reuse the folder's entry names between listings/TTL; a tracked edit
|
|
502
|
+
// does not need readdir + check-ignore in every expanded folder.
|
|
503
|
+
const entries = known.folders.has(folder) ? null : (await readdir(folder, { withFileTypes: true })).slice(0, listingCap);
|
|
504
|
+
const checked = await ignoredEntries(real, folder, entries, signal);
|
|
505
|
+
if (checked === null) ignoreUnknown = true;
|
|
506
|
+
else ignored.push(...checked);
|
|
507
|
+
} catch { ignoreUnknown = true; }
|
|
508
|
+
}
|
|
509
|
+
const changes = Object.fromEntries((result.changes || []).map(c => [c.path, c.status]));
|
|
510
|
+
return { isGit: result.isGit, branch: result.branch, changes, ignored: [...new Set(ignored)], ignoreUnknown,
|
|
511
|
+
...(!result.ok || result.partial ? { partial: true, stale: !!result.stale, scanState: result.timedOut ? 'timed-out' : 'failed' } : {}) };
|
|
483
512
|
}
|
|
484
513
|
router.get('/git', route(async (req, res) => {
|
|
485
514
|
const dir = folderOf(req.query.root);
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if (!flight) {
|
|
489
|
-
flight = readGit(dir.real).finally(() => gitFlights.delete(dir.real));
|
|
490
|
-
gitFlights.set(dir.real, flight);
|
|
491
|
-
}
|
|
492
|
-
res.json({ ok: true, root: dir.written, ...(await flight) });
|
|
515
|
+
if (req.query.refresh === '1') statusRunner.invalidate(dir.real, { refresh: true });
|
|
516
|
+
res.json({ ok: true, root: dir.written, ...(await readGit(dir.real, requestSignal(res))) });
|
|
493
517
|
}));
|
|
494
|
-
const ignoredByGit =
|
|
495
|
-
const known = gitKnown.get(real);
|
|
496
|
-
return !!known && (known.files.has(path) || known.dirs.some((dir) => path.startsWith(dir) || `${path}/` === dir));
|
|
497
|
-
};
|
|
518
|
+
const ignoredByGit = real => path => ignoreData(real).prefixes.has(watchRelative(real, path, platform));
|
|
498
519
|
|
|
499
520
|
// ── Entries by name ──
|
|
500
521
|
router.get('/search', route(async (req, res) => {
|
|
@@ -505,8 +526,9 @@ export function createFileExplorerRouter({
|
|
|
505
526
|
const words = query.split(/\s+/);
|
|
506
527
|
const byPath = query.includes('/');
|
|
507
528
|
// What git ignores as a whole folder (node_modules, build output) is not walked; without git, the usual two.
|
|
508
|
-
const
|
|
509
|
-
const
|
|
529
|
+
const signal = requestSignal(res);
|
|
530
|
+
const known = gitKnown.get(dir.real);
|
|
531
|
+
const skipped = (rel, name) => name === '.git' || (known ? known.prefixes.has(rel) : name === 'node_modules');
|
|
510
532
|
const items = [];
|
|
511
533
|
const queue = [''];
|
|
512
534
|
const started = Date.now();
|
|
@@ -515,8 +537,15 @@ export function createFileExplorerRouter({
|
|
|
515
537
|
const rel = queue[at];
|
|
516
538
|
let dirents;
|
|
517
539
|
try { dirents = await readdir(rel ? join(dir.real, rel) : dir.real, { withFileTypes: true }); } catch { continue; }
|
|
540
|
+
if (dirents.length > listingCap) { dirents = dirents.slice(0, listingCap); partial = true; }
|
|
541
|
+
const checked = await ignoredEntries(dir.real, rel ? join(dir.real, rel) : dir.real, dirents, signal);
|
|
542
|
+
// Unknown ignore policy must not send name search into an enormous cache.
|
|
543
|
+
if (checked === null) { partial = true; break; }
|
|
544
|
+
const ignored = new Set(checked);
|
|
545
|
+
if (signal.aborted) break;
|
|
518
546
|
dirents.sort((a, b) => compareNames(a.name, b.name));
|
|
519
547
|
for (const entry of dirents) {
|
|
548
|
+
if (ignored.has(`${rel ? rel + '/' : ''}${entry.name}${entry.isDirectory() ? '/' : ''}`)) continue;
|
|
520
549
|
if (++visited > SEARCH_MAX_VISITED || Date.now() - started > SEARCH_BUDGET_MS) { partial = true; break walk; }
|
|
521
550
|
const path = rel ? `${rel}/${entry.name}` : entry.name;
|
|
522
551
|
const isDir = entry.isDirectory();
|
|
@@ -547,7 +576,12 @@ export function createFileExplorerRouter({
|
|
|
547
576
|
...(watchImpl ? { watch: watchImpl } : {}),
|
|
548
577
|
...watchClock,
|
|
549
578
|
ignoresGit: ignoredByGit(dir.real),
|
|
550
|
-
onChange: ({ dirs, git: gitChanged }) =>
|
|
579
|
+
onChange: ({ dirs, git: gitChanged, ignores }) => {
|
|
580
|
+
if (gitChanged || ignores) statusRunner.invalidate(dir.real, { ignores });
|
|
581
|
+
if (ignores) gitKnown.delete(dir.real);
|
|
582
|
+
else { const known = gitKnown.get(dir.real); for (const folder of dirs) known?.folders.delete(folder); }
|
|
583
|
+
send('change', { dirs: dirs.map((real) => session.written.get(real)).filter(Boolean), git: gitChanged });
|
|
584
|
+
},
|
|
551
585
|
onError: () => send('resync', {}),
|
|
552
586
|
// A stream is not a watch that works: what is really watched is said, and said again when it changes.
|
|
553
587
|
onState: (state) => send('state', state),
|
|
@@ -557,7 +591,7 @@ export function createFileExplorerRouter({
|
|
|
557
591
|
const beat = setInterval(() => { try { res.write(': still here\n\n'); } catch { /* closing */ } }, 25000);
|
|
558
592
|
beat.unref?.();
|
|
559
593
|
// The response's end is the page leaving (a request's own `close` only says its body was read).
|
|
560
|
-
res.on('close', () => { clearInterval(beat); session.watcher.close(); sessions.delete(id); });
|
|
594
|
+
res.on('close', () => { clearInterval(beat); session.watcher.close(); sessions.delete(id); if (![...sessions.values()].some(s => s.real === dir.real)) statusRunner.release(dir.real); });
|
|
561
595
|
}));
|
|
562
596
|
|
|
563
597
|
router.post('/watch/:id', route(async (req, res) => {
|
|
@@ -1033,7 +1067,7 @@ export function createFileExplorerRouter({
|
|
|
1033
1067
|
router.use((req, res) => res.status(404).json({ ok: false, code: 'bad-request', error: 'Unknown explorer route.' }));
|
|
1034
1068
|
|
|
1035
1069
|
/** Release every watch (the server is going down). */
|
|
1036
|
-
router.close = () => { for (const session of sessions.values()) { session.watcher.close(); try { session.res.end(); } catch { /* gone */ } } sessions.clear(); };
|
|
1070
|
+
router.close = () => { for (const session of sessions.values()) { session.watcher.close(); statusRunner.release(session.real); try { session.res.end(); } catch { /* gone */ } } sessions.clear(); };
|
|
1037
1071
|
router.sessions = () => sessions.size;
|
|
1038
1072
|
return router;
|
|
1039
1073
|
}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// that is not there, whatever folders the page shows meanwhile.
|
|
27
27
|
|
|
28
28
|
import { existsSync, watch as fsWatch } from 'node:fs';
|
|
29
|
-
import { dirname, join, relative, sep } from 'node:path';
|
|
29
|
+
import { dirname, isAbsolute, join, relative, sep } from 'node:path';
|
|
30
30
|
|
|
31
31
|
export const MAX_WATCHED_DIRS = 600;
|
|
32
32
|
/** A watch that failed is tried again after this long, then after twice that, and so on. */
|
|
@@ -80,6 +80,7 @@ export function createTreeWatcher({
|
|
|
80
80
|
let said = null; // the `live` that was last reported
|
|
81
81
|
const pending = new Set();
|
|
82
82
|
let pendingGit = false;
|
|
83
|
+
let pendingIgnores = false;
|
|
83
84
|
let timer = null;
|
|
84
85
|
let firstAt = 0;
|
|
85
86
|
|
|
@@ -88,9 +89,10 @@ export function createTreeWatcher({
|
|
|
88
89
|
function flush() {
|
|
89
90
|
timer = null;
|
|
90
91
|
if (closed || (!pending.size && !pendingGit)) return;
|
|
91
|
-
const change = { dirs: [...pending], git: pendingGit };
|
|
92
|
+
const change = { dirs: [...pending], git: pendingGit, ...(pendingIgnores ? { ignores: true } : {}) };
|
|
92
93
|
pending.clear();
|
|
93
94
|
pendingGit = false;
|
|
95
|
+
pendingIgnores = false;
|
|
94
96
|
try { onChange(change); } catch { /* a listener's failure is not the watcher's */ }
|
|
95
97
|
}
|
|
96
98
|
|
|
@@ -107,7 +109,11 @@ export function createTreeWatcher({
|
|
|
107
109
|
// One change, by its path below the root ('' is the root itself).
|
|
108
110
|
function note(rel) {
|
|
109
111
|
if (closed) return;
|
|
110
|
-
|
|
112
|
+
// Native watchers can report an absolute path, or no filename at all.
|
|
113
|
+
// Missing names conservatively dirty status, never all ignore data.
|
|
114
|
+
const path = slashed(isAbsolute(rel || '') ? relative(root, rel) : rel || '');
|
|
115
|
+
if (path === '..' || path.startsWith('../')) return;
|
|
116
|
+
if (path === '.git/info/exclude' || path === '.git/config' || path === '.git/config.worktree' || path === '.gitignore' || path.endsWith('/.gitignore')) { pendingIgnores = true; pendingGit = true; }
|
|
111
117
|
if (path === '.git' || path.startsWith('.git/')) {
|
|
112
118
|
const inside = path.slice(5);
|
|
113
119
|
if (GIT_STATE_FILES.test(inside) || inside.startsWith('refs/')) pendingGit = true;
|