dexterm-cli 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/dist/packages/agents/src/audit.js +122 -0
- package/dist/packages/agents/src/bus.js +379 -0
- package/dist/packages/agents/src/executor.js +177 -0
- package/dist/packages/agents/src/gate.js +176 -0
- package/dist/packages/agents/src/identity.js +179 -0
- package/dist/packages/agents/src/index.js +15 -0
- package/dist/packages/agents/src/proposals.js +153 -0
- package/dist/packages/agents/src/schema.js +87 -0
- package/dist/packages/agents/src/sdk.js +140 -0
- package/dist/packages/agents/src/tiers.js +114 -0
- package/dist/packages/agents/src/tools.js +69 -0
- package/dist/packages/agents/src/ws.js +158 -0
- package/dist/packages/cli/assets/claude/analyst-system.md +76 -0
- package/dist/packages/cli/assets/claude/plugin/.claude-plugin/plugin.json +5 -0
- package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +16 -0
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +67 -0
- package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +64 -0
- package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +63 -0
- package/dist/packages/cli/assets/claude/plugin/skills/trench-scan/SKILL.md +44 -0
- package/dist/packages/cli/assets/claude/schemas/larp.schema.json +117 -0
- package/dist/packages/cli/assets/claude/schemas/trench-scan.schema.json +71 -0
- package/dist/packages/cli/assets/claude/schemas/verdict.schema.json +115 -0
- package/dist/packages/cli/bin/dexterm.mjs +11 -0
- package/dist/packages/cli/src/agent-cmd.js +303 -0
- package/dist/packages/cli/src/agent-roster.js +26 -0
- package/dist/packages/cli/src/analyst/backend.js +526 -0
- package/dist/packages/cli/src/analyst/results.js +38 -0
- package/dist/packages/cli/src/analyst/session.js +319 -0
- package/dist/packages/cli/src/audit.js +51 -0
- package/dist/packages/cli/src/backtest-cmd.js +149 -0
- package/dist/packages/cli/src/boot-state.js +62 -0
- package/dist/packages/cli/src/custom.js +261 -0
- package/dist/packages/cli/src/display.js +191 -0
- package/dist/packages/cli/src/doctor.js +361 -0
- package/dist/packages/cli/src/feed.js +634 -0
- package/dist/packages/cli/src/fixtures.js +275 -0
- package/dist/packages/cli/src/follow-cmd.js +221 -0
- package/dist/packages/cli/src/images-config.js +59 -0
- package/dist/packages/cli/src/index.js +31 -0
- package/dist/packages/cli/src/kb-skill-cmd.js +240 -0
- package/dist/packages/cli/src/larp-record.js +21 -0
- package/dist/packages/cli/src/live/copy.js +80 -0
- package/dist/packages/cli/src/live/flow.js +233 -0
- package/dist/packages/cli/src/live/index.js +509 -0
- package/dist/packages/cli/src/live/intel.js +227 -0
- package/dist/packages/cli/src/live/newpairs.js +334 -0
- package/dist/packages/cli/src/live/screens/newpairs-live.js +219 -0
- package/dist/packages/cli/src/live/screens/token-live.js +459 -0
- package/dist/packages/cli/src/live/screens/watchlist-live.js +149 -0
- package/dist/packages/cli/src/live/token.js +585 -0
- package/dist/packages/cli/src/live/trade.js +929 -0
- package/dist/packages/cli/src/live/watchlist.js +178 -0
- package/dist/packages/cli/src/main.js +5583 -0
- package/dist/packages/cli/src/narrative-cmd.js +76 -0
- package/dist/packages/cli/src/pack-cmd.js +323 -0
- package/dist/packages/cli/src/plan-cmd.js +278 -0
- package/dist/packages/cli/src/popup.js +810 -0
- package/dist/packages/cli/src/rules-cmd.js +131 -0
- package/dist/packages/cli/src/rules.js +339 -0
- package/dist/packages/cli/src/scan/command.js +57 -0
- package/dist/packages/cli/src/scan/engine.js +419 -0
- package/dist/packages/cli/src/scan/observe.js +64 -0
- package/dist/packages/cli/src/scan/ui.js +98 -0
- package/dist/packages/cli/src/screener/filter.js +241 -0
- package/dist/packages/cli/src/screener/presets.js +80 -0
- package/dist/packages/cli/src/screener/rows.js +146 -0
- package/dist/packages/cli/src/screener/rug.js +50 -0
- package/dist/packages/cli/src/screener/sort.js +52 -0
- package/dist/packages/cli/src/screener/view.js +183 -0
- package/dist/packages/cli/src/screens/analyst-session.js +343 -0
- package/dist/packages/cli/src/screens/analyst.js +326 -0
- package/dist/packages/cli/src/screens/boot.js +74 -0
- package/dist/packages/cli/src/screens/bubbles.js +255 -0
- package/dist/packages/cli/src/screens/coin.js +33 -0
- package/dist/packages/cli/src/screens/follows.js +201 -0
- package/dist/packages/cli/src/screens/kol.js +90 -0
- package/dist/packages/cli/src/screens/narrative.js +90 -0
- package/dist/packages/cli/src/screens/newpairs.js +150 -0
- package/dist/packages/cli/src/screens/overlay.js +257 -0
- package/dist/packages/cli/src/screens/plans.js +126 -0
- package/dist/packages/cli/src/screens/positions.js +176 -0
- package/dist/packages/cli/src/screens/rules.js +170 -0
- package/dist/packages/cli/src/screens/screener.js +459 -0
- package/dist/packages/cli/src/screens/social-panel.js +107 -0
- package/dist/packages/cli/src/screens/ticket.js +342 -0
- package/dist/packages/cli/src/screens/token.js +312 -0
- package/dist/packages/cli/src/screens/trending.js +181 -0
- package/dist/packages/cli/src/screens/watchlist.js +104 -0
- package/dist/packages/cli/src/social/demo.js +96 -0
- package/dist/packages/cli/src/social/view.js +250 -0
- package/dist/packages/cli/src/state.js +463 -0
- package/dist/packages/cli/src/theme-cmd.js +126 -0
- package/dist/packages/cli/src/trade.js +319 -0
- package/dist/packages/cli/src/voice-hud.js +42 -0
- package/dist/packages/cli/src/wallet-cmd.js +440 -0
- package/dist/packages/cli/src/wallet-command.js +31 -0
- package/dist/packages/cli/src/widgets.js +208 -0
- package/dist/packages/cli/src/workspace-cmd.js +68 -0
- package/dist/packages/cli/src/x-cmd.js +154 -0
- package/dist/packages/custom/src/config.js +213 -0
- package/dist/packages/custom/src/index.js +10 -0
- package/dist/packages/custom/src/keymap.js +107 -0
- package/dist/packages/custom/src/layout.js +235 -0
- package/dist/packages/custom/src/packs.js +633 -0
- package/dist/packages/custom/src/rules.js +704 -0
- package/dist/packages/custom/src/schema.js +515 -0
- package/dist/packages/custom/src/themes.js +176 -0
- package/dist/packages/custom/src/widget-html.js +222 -0
- package/dist/packages/custom/src/workspace.js +141 -0
- package/dist/packages/data/src/analyst-actions.js +133 -0
- package/dist/packages/data/src/budget.js +88 -0
- package/dist/packages/data/src/cache.js +142 -0
- package/dist/packages/data/src/candles.js +168 -0
- package/dist/packages/data/src/clock.js +64 -0
- package/dist/packages/data/src/credits.js +137 -0
- package/dist/packages/data/src/detail.js +97 -0
- package/dist/packages/data/src/fast-lane.js +321 -0
- package/dist/packages/data/src/fixtures.js +135 -0
- package/dist/packages/data/src/health.js +245 -0
- package/dist/packages/data/src/http.js +277 -0
- package/dist/packages/data/src/images/cache.js +141 -0
- package/dist/packages/data/src/images/decode-worker.js +28 -0
- package/dist/packages/data/src/images/decode.js +81 -0
- package/dist/packages/data/src/images/fetch.js +162 -0
- package/dist/packages/data/src/images/gif.js +214 -0
- package/dist/packages/data/src/images/index.js +10 -0
- package/dist/packages/data/src/images/jpeg.js +544 -0
- package/dist/packages/data/src/images/png.js +310 -0
- package/dist/packages/data/src/images/service.js +334 -0
- package/dist/packages/data/src/images/types.js +27 -0
- package/dist/packages/data/src/index.js +208 -0
- package/dist/packages/data/src/intel/helius-history.js +59 -0
- package/dist/packages/data/src/intel/holder-class.js +125 -0
- package/dist/packages/data/src/intel/rules.js +421 -0
- package/dist/packages/data/src/intel/scan.js +170 -0
- package/dist/packages/data/src/larp/evidence.js +288 -0
- package/dist/packages/data/src/larp/links.js +137 -0
- package/dist/packages/data/src/larp/robots.js +90 -0
- package/dist/packages/data/src/larp/safe-fetch.js +389 -0
- package/dist/packages/data/src/limiter.js +341 -0
- package/dist/packages/data/src/model.js +12 -0
- package/dist/packages/data/src/paper.js +331 -0
- package/dist/packages/data/src/rpc-config.js +126 -0
- package/dist/packages/data/src/rpc-pool.js +166 -0
- package/dist/packages/data/src/social/chain.js +83 -0
- package/dist/packages/data/src/social/dedupe.js +205 -0
- package/dist/packages/data/src/social/feed.js +462 -0
- package/dist/packages/data/src/social/firewall.js +110 -0
- package/dist/packages/data/src/social/index.js +11 -0
- package/dist/packages/data/src/social/kol.js +269 -0
- package/dist/packages/data/src/social/model.js +11 -0
- package/dist/packages/data/src/social/rank.js +184 -0
- package/dist/packages/data/src/social/rss.js +113 -0
- package/dist/packages/data/src/social/summary.js +104 -0
- package/dist/packages/data/src/social/venues.js +186 -0
- package/dist/packages/data/src/sources/dexscreener.js +170 -0
- package/dist/packages/data/src/sources/geckoterminal.js +133 -0
- package/dist/packages/data/src/sources/goplus.js +161 -0
- package/dist/packages/data/src/sources/jupiter-swap.js +236 -0
- package/dist/packages/data/src/sources/jupiter.js +198 -0
- package/dist/packages/data/src/sources/pda.js +113 -0
- package/dist/packages/data/src/sources/pump-curve.js +58 -0
- package/dist/packages/data/src/sources/pumpportal.js +247 -0
- package/dist/packages/data/src/sources/rugcheck.js +71 -0
- package/dist/packages/data/src/sources/solana-rpc.js +299 -0
- package/dist/packages/data/src/sources/solana-ws.js +287 -0
- package/dist/packages/data/src/store.js +289 -0
- package/dist/packages/data/src/trade/bridge.js +464 -0
- package/dist/packages/data/src/trade/caps.js +323 -0
- package/dist/packages/data/src/trade/copy/auto.js +385 -0
- package/dist/packages/data/src/trade/copy/follows.js +103 -0
- package/dist/packages/data/src/trade/copy/index.js +9 -0
- package/dist/packages/data/src/trade/copy/mirror.js +91 -0
- package/dist/packages/data/src/trade/copy/stats.js +120 -0
- package/dist/packages/data/src/trade/copy/tracker.js +158 -0
- package/dist/packages/data/src/trade/copy/trades.js +87 -0
- package/dist/packages/data/src/trade/copy/types.js +6 -0
- package/dist/packages/data/src/trade/executor.js +933 -0
- package/dist/packages/data/src/trade/kill-switch.js +47 -0
- package/dist/packages/data/src/trade/link-store.js +52 -0
- package/dist/packages/data/src/trade/plans/confirm.js +157 -0
- package/dist/packages/data/src/trade/plans/engine.js +784 -0
- package/dist/packages/data/src/trade/plans/executor-live.js +220 -0
- package/dist/packages/data/src/trade/plans/executor-paper.js +44 -0
- package/dist/packages/data/src/trade/plans/executor.js +11 -0
- package/dist/packages/data/src/trade/plans/index.js +12 -0
- package/dist/packages/data/src/trade/plans/journal.js +77 -0
- package/dist/packages/data/src/trade/plans/mandate.js +129 -0
- package/dist/packages/data/src/trade/plans/plan.js +179 -0
- package/dist/packages/data/src/trade/plans/service.js +168 -0
- package/dist/packages/data/src/trade/plans/types.js +6 -0
- package/dist/packages/data/src/trade/rpc-exec.js +229 -0
- package/dist/packages/data/src/trade/signer/client.js +197 -0
- package/dist/packages/data/src/trade/signer/fast-wallet.js +154 -0
- package/dist/packages/data/src/trade/signer/host.js +97 -0
- package/dist/packages/data/src/trade/signer/main.js +121 -0
- package/dist/packages/data/src/trade/signer/probe-builder.js +85 -0
- package/dist/packages/data/src/trade/signer/protocol.js +92 -0
- package/dist/packages/data/src/trade/signer/service.js +400 -0
- package/dist/packages/data/src/trade/signer/sol-usd-feed.js +98 -0
- package/dist/packages/data/src/trade/sol-usd.js +86 -0
- package/dist/packages/data/src/trade/swap-build.js +108 -0
- package/dist/packages/data/src/trade/token-delta.js +201 -0
- package/dist/packages/data/src/trade/tx-decode.js +632 -0
- package/dist/packages/data/src/util.js +24 -0
- package/dist/packages/data/src/watchlist.js +176 -0
- package/dist/packages/data/src/x/api.js +256 -0
- package/dist/packages/data/src/x/budget.js +66 -0
- package/dist/packages/data/src/x/config.js +49 -0
- package/dist/packages/data/src/x/index.js +10 -0
- package/dist/packages/data/src/x/mock-server.js +217 -0
- package/dist/packages/data/src/x/oauth.js +242 -0
- package/dist/packages/data/src/x/pkce.js +21 -0
- package/dist/packages/data/src/x/source.js +285 -0
- package/dist/packages/data/src/x/token-store.js +134 -0
- package/dist/packages/data/src/x/transport.js +71 -0
- package/dist/packages/data/vendor/solana-web3/LICENSE +20 -0
- package/dist/packages/data/vendor/solana-web3/NOTICE.md +13 -0
- package/dist/packages/data/vendor/solana-web3/index.iife.min.js +20 -0
- package/dist/packages/mcp/assets/cases.json +1194 -0
- package/dist/packages/mcp/assets/kb/entries/cabal-and-coordinated-launches.md +28 -0
- package/dist/packages/mcp/assets/kb/entries/common-rug-patterns.md +40 -0
- package/dist/packages/mcp/assets/kb/entries/dbc-mechanics.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/dev-wallet-behaviour.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/glossary.md +35 -0
- package/dist/packages/mcp/assets/kb/entries/holder-concentration.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/jito-bundles.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/launchlab-curve.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/lp-burn-vs-lock.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/metadata-and-copycats.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/mint-freeze-authority.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/narrative-lifecycle.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/pump-bonding-curve.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/pumpswap-migration.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/sniping-and-snipers.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/token2022-extensions-risk.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/wash-trading-fake-volume.md +33 -0
- package/dist/packages/mcp/assets/skills/rug-check.md +52 -0
- package/dist/packages/mcp/assets/skills/token-dossier.md +57 -0
- package/dist/packages/mcp/assets/skills/trench-scan.md +40 -0
- package/dist/packages/mcp/bin/dexterm-mcp.mjs +16 -0
- package/dist/packages/mcp/src/dossier.js +78 -0
- package/dist/packages/mcp/src/firewall.js +21 -0
- package/dist/packages/mcp/src/index.js +22 -0
- package/dist/packages/mcp/src/server.js +849 -0
- package/dist/packages/mcp/src/user-content.js +434 -0
- package/dist/packages/packs/src/backtest.js +183 -0
- package/dist/packages/packs/src/digest.js +95 -0
- package/dist/packages/packs/src/firewall.js +57 -0
- package/dist/packages/packs/src/index.js +19 -0
- package/dist/packages/packs/src/larp-eval.js +100 -0
- package/dist/packages/packs/src/narrative.js +309 -0
- package/dist/packages/packs/src/reason.js +135 -0
- package/dist/packages/packs/src/rug-model.js +137 -0
- package/dist/packages/packs/src/schema.js +54 -0
- package/dist/packages/packs/src/token-dossier.js +174 -0
- package/dist/packages/packs/src/verdicts.js +17 -0
- package/dist/packages/packs/src/wallet-dossier.js +105 -0
- package/dist/packages/popup/src/config.js +71 -0
- package/dist/packages/popup/src/dock.js +271 -0
- package/dist/packages/popup/src/host.js +210 -0
- package/dist/packages/popup/src/index.js +12 -0
- package/dist/packages/popup/src/loopback.js +53 -0
- package/dist/packages/popup/src/open.js +183 -0
- package/dist/packages/popup/src/pages/bubbles-lib.js +160 -0
- package/dist/packages/popup/src/pages/bubbles.css +23 -0
- package/dist/packages/popup/src/pages/bubbles.html +36 -0
- package/dist/packages/popup/src/pages/bubbles.js +214 -0
- package/dist/packages/popup/src/pages/chart-lib.js +262 -0
- package/dist/packages/popup/src/pages/chart.css +50 -0
- package/dist/packages/popup/src/pages/chart.html +56 -0
- package/dist/packages/popup/src/pages/chart.js +554 -0
- package/dist/packages/popup/src/pages/common.css +34 -0
- package/dist/packages/popup/src/pages/common.js +187 -0
- package/dist/packages/popup/src/pages/feeds.html +20 -0
- package/dist/packages/popup/src/pages/feeds.js +77 -0
- package/dist/packages/popup/src/pages/narrative.html +20 -0
- package/dist/packages/popup/src/pages/narrative.js +61 -0
- package/dist/packages/popup/src/pages/plans.css +30 -0
- package/dist/packages/popup/src/pages/plans.html +26 -0
- package/dist/packages/popup/src/pages/plans.js +82 -0
- package/dist/packages/popup/src/pages/roster.css +41 -0
- package/dist/packages/popup/src/pages/roster.html +25 -0
- package/dist/packages/popup/src/pages/roster.js +75 -0
- package/dist/packages/popup/src/pages/token.html +20 -0
- package/dist/packages/popup/src/pages/token.js +112 -0
- package/dist/packages/popup/src/pages/widget-sdk.js +224 -0
- package/dist/packages/popup/src/pages/widget.css +5 -0
- package/dist/packages/popup/src/pages/widget.html +21 -0
- package/dist/packages/popup/src/pages/widget.js +85 -0
- package/dist/packages/popup/src/protocol.js +31 -0
- package/dist/packages/popup/src/selftest.js +156 -0
- package/dist/packages/popup/src/server.js +582 -0
- package/dist/packages/popup/src/sinkproxy.js +167 -0
- package/dist/packages/popup/src/widget-html.js +231 -0
- package/dist/packages/popup/vendor/lightweight-charts/LICENSE +201 -0
- package/dist/packages/popup/vendor/lightweight-charts/NOTICE +2 -0
- package/dist/packages/popup/vendor/lightweight-charts/README.md +30 -0
- package/dist/packages/popup/vendor/lightweight-charts/VERSION +1 -0
- package/dist/packages/popup/vendor/lightweight-charts/lightweight-charts.standalone.production.js +7 -0
- package/dist/packages/recorder/bin/dexterm-recorder.mjs +11 -0
- package/dist/packages/recorder/src/cli.js +113 -0
- package/dist/packages/recorder/src/export.js +71 -0
- package/dist/packages/recorder/src/index.js +20 -0
- package/dist/packages/recorder/src/labels.js +151 -0
- package/dist/packages/recorder/src/open.js +29 -0
- package/dist/packages/recorder/src/recorder.js +553 -0
- package/dist/packages/recorder/src/schedule.js +43 -0
- package/dist/packages/recorder/src/schema.js +135 -0
- package/dist/packages/recorder/src/snapshot.js +191 -0
- package/dist/packages/recorder/src/sweep.js +52 -0
- package/dist/packages/recorder/src/verdicts.js +52 -0
- package/dist/packages/tui/src/bubbles.js +415 -0
- package/dist/packages/tui/src/canvas.js +370 -0
- package/dist/packages/tui/src/chrome.js +85 -0
- package/dist/packages/tui/src/color.js +133 -0
- package/dist/packages/tui/src/flush.js +62 -0
- package/dist/packages/tui/src/fmt.js +188 -0
- package/dist/packages/tui/src/fx-trade.js +278 -0
- package/dist/packages/tui/src/fx.js +527 -0
- package/dist/packages/tui/src/images/blocks.js +141 -0
- package/dist/packages/tui/src/images/coin.js +22 -0
- package/dist/packages/tui/src/images/detect.js +140 -0
- package/dist/packages/tui/src/images/index.js +7 -0
- package/dist/packages/tui/src/images/layer.js +146 -0
- package/dist/packages/tui/src/images/pixels.js +157 -0
- package/dist/packages/tui/src/images/protocols.js +87 -0
- package/dist/packages/tui/src/index.js +15 -0
- package/dist/packages/tui/src/keymap.js +355 -0
- package/dist/packages/tui/src/layout.js +11 -0
- package/dist/packages/tui/src/sound.js +177 -0
- package/dist/packages/tui/src/theme.js +182 -0
- package/dist/packages/tui/src/widgets.js +598 -0
- package/dist/packages/voice/src/controller.js +395 -0
- package/dist/packages/voice/src/doctor.js +63 -0
- package/dist/packages/voice/src/engines.js +44 -0
- package/dist/packages/voice/src/index.js +16 -0
- package/dist/packages/voice/src/router.js +502 -0
- package/dist/packages/voice/src/stt/apple-helper.swift.js +103 -0
- package/dist/packages/voice/src/stt/apple.js +179 -0
- package/dist/packages/voice/src/stt/fake.js +34 -0
- package/dist/packages/voice/src/stt/whisper.js +102 -0
- package/dist/packages/voice/src/summary.js +44 -0
- package/dist/packages/voice/src/tts/fake.js +30 -0
- package/dist/packages/voice/src/tts/say.js +96 -0
- package/dist/packages/voice/src/types.js +3 -0
- package/package.json +42 -4
- package/README.md +0 -3
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Popup host configuration (task-1290). decision: keep the spike's hotkey chord and rain cadence
|
|
2
|
+
// defaults, but make both configurable in a config file. The file is ~/.dexterm/popup.json (DEXTERM_HOME
|
|
3
|
+
// overrides the directory, same convention as state.json / helius.key). Everything is optional; a missing
|
|
4
|
+
// or malformed file degrades to the defaults and never throws — a popup must open even with a bad config.
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
/** The spike's global-hotkey chord (⌃⌥⌘⇧B). Accelerator syntax: `Mod+Mod+Code`, matched by the shell. */
|
|
9
|
+
export const DEFAULT_HOTKEY = 'Control+Alt+Super+Shift+KeyB';
|
|
10
|
+
/** task-1325: the global chord that closes every popup window (⌃⌥⌘⇧W). */
|
|
11
|
+
export const DEFAULT_HOTKEY_CLOSE = 'Control+Alt+Super+Shift+KeyW';
|
|
12
|
+
export const DEFAULT_DOCK = { enabled: true, width: 560, follow: true, followEvery: 0.5 };
|
|
13
|
+
/** The rain cadence approved in the spike. speedScale 1 = the bubble-map default fall speed. */
|
|
14
|
+
export const DEFAULT_RAIN = { enabled: true, speedScale: 1, density: 0.55 };
|
|
15
|
+
export const DEFAULT_CONFIG = { hotkey: DEFAULT_HOTKEY, hotkeyClose: DEFAULT_HOTKEY_CLOSE, dock: { ...DEFAULT_DOCK }, rain: { ...DEFAULT_RAIN }, fpsCap: 60, source: 'defaults' };
|
|
16
|
+
/** `DEXTERM_HOME` overrides `~/.dexterm` (tests point it at a temp dir). */
|
|
17
|
+
export function dexHome(env = process.env) {
|
|
18
|
+
return env.DEXTERM_HOME && env.DEXTERM_HOME.trim() ? env.DEXTERM_HOME : join(homedir(), '.dexterm');
|
|
19
|
+
}
|
|
20
|
+
export function configPath(env = process.env) {
|
|
21
|
+
return join(dexHome(env), 'popup.json');
|
|
22
|
+
}
|
|
23
|
+
function num(v, lo, hi, dflt) {
|
|
24
|
+
const n = typeof v === 'number' ? v : NaN;
|
|
25
|
+
return Number.isFinite(n) ? Math.max(lo, Math.min(hi, n)) : dflt;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Validate a parsed config object into a PopupConfig, clamping every field and keeping the defaults for
|
|
29
|
+
* anything missing or out of range. The hotkey must be a short printable accelerator; an unexpected value
|
|
30
|
+
* is ignored so a bad file can never register a nonsense chord or an oversized string.
|
|
31
|
+
*/
|
|
32
|
+
export function normalizeConfig(raw, source = 'file') {
|
|
33
|
+
const o = (raw && typeof raw === 'object' ? raw : {});
|
|
34
|
+
const rrain = (o.rain && typeof o.rain === 'object' ? o.rain : {});
|
|
35
|
+
// empty = disabled; otherwise a short accelerator of letters/digits/+ only (no shell metacharacters)
|
|
36
|
+
const chord = (v, dflt) => {
|
|
37
|
+
if (typeof v !== 'string')
|
|
38
|
+
return dflt;
|
|
39
|
+
const h = v.trim();
|
|
40
|
+
return h === '' ? '' : (/^[A-Za-z0-9+]{1,64}$/.test(h) ? h : dflt);
|
|
41
|
+
};
|
|
42
|
+
const hotkey = chord(o.hotkey, DEFAULT_HOTKEY);
|
|
43
|
+
const rdock = (o.dock && typeof o.dock === 'object' ? o.dock : {});
|
|
44
|
+
return {
|
|
45
|
+
hotkey,
|
|
46
|
+
hotkeyClose: chord(o.hotkeyClose, DEFAULT_HOTKEY_CLOSE),
|
|
47
|
+
dock: {
|
|
48
|
+
enabled: typeof rdock.enabled === 'boolean' ? rdock.enabled : DEFAULT_DOCK.enabled,
|
|
49
|
+
width: Math.round(num(rdock.width, 360, 1600, DEFAULT_DOCK.width)),
|
|
50
|
+
follow: typeof rdock.follow === 'boolean' ? rdock.follow : DEFAULT_DOCK.follow,
|
|
51
|
+
followEvery: num(rdock.followEvery, 0.25, 5, DEFAULT_DOCK.followEvery),
|
|
52
|
+
},
|
|
53
|
+
rain: {
|
|
54
|
+
enabled: typeof rrain.enabled === 'boolean' ? rrain.enabled : DEFAULT_RAIN.enabled,
|
|
55
|
+
speedScale: num(rrain.speedScale, 0.1, 5, DEFAULT_RAIN.speedScale),
|
|
56
|
+
density: num(rrain.density, 0, 1, DEFAULT_RAIN.density),
|
|
57
|
+
},
|
|
58
|
+
fpsCap: Math.round(num(o.fpsCap, 15, 120, DEFAULT_CONFIG.fpsCap)),
|
|
59
|
+
source,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Load ~/.dexterm/popup.json, or the defaults when it is missing/unreadable/malformed. Never throws. */
|
|
63
|
+
export function loadConfig(env = process.env) {
|
|
64
|
+
try {
|
|
65
|
+
const text = readFileSync(configPath(env), 'utf8');
|
|
66
|
+
return normalizeConfig(JSON.parse(text), 'file');
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return { ...DEFAULT_CONFIG, dock: { ...DEFAULT_DOCK }, rain: { ...DEFAULT_RAIN } };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// dexterm popup — dock the chart window beside the terminal (task-1325).
|
|
2
|
+
//
|
|
3
|
+
// macOS only (elsewhere every function degrades to "no docking": the shell's default geometry is used).
|
|
4
|
+
//
|
|
5
|
+
// * Which terminal: TERM_PROGRAM (Apple_Terminal | iTerm.app), else LC_TERMINAL=iTerm2 (survives tmux/ssh).
|
|
6
|
+
// * Which WINDOW: the one whose tab (Terminal.app) / session (iTerm2) has OUR controlling tty, found once and
|
|
7
|
+
// then addressed by window id; the front window if no tty matches (e.g. inside tmux).
|
|
8
|
+
// * One `osascript -l JavaScript` process does the work. It prints the screens first (AppKit, no Apple Events,
|
|
9
|
+
// no permission), then the terminal window's bounds (Apple Events to the terminal app: from inside that
|
|
10
|
+
// terminal this is the app talking to itself). With follow on it stays alive, re-checks every ~0.5 s, prints
|
|
11
|
+
// only when something changed, and exits by itself when its parent (the TUI) is gone.
|
|
12
|
+
// * Placement: right edge of the terminal, same top and height; left edge when the right side has no room;
|
|
13
|
+
// overlapping the screen's right edge (always-on-top) when neither side has room; and for an unknown
|
|
14
|
+
// terminal, the right edge of the main screen (always-on-top), full visible height.
|
|
15
|
+
//
|
|
16
|
+
// Nothing here reads keys, URLs or tickets: the script only carries our tty name and the terminal kind.
|
|
17
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
18
|
+
/** The terminal app we run in, from the environment it sets. */
|
|
19
|
+
export function detectTerminal(env = process.env) {
|
|
20
|
+
const tp = env.TERM_PROGRAM ?? '';
|
|
21
|
+
if (tp === 'Apple_Terminal')
|
|
22
|
+
return 'terminal';
|
|
23
|
+
if (tp === 'iTerm.app' || env.LC_TERMINAL === 'iTerm2')
|
|
24
|
+
return 'iterm';
|
|
25
|
+
return 'unknown';
|
|
26
|
+
}
|
|
27
|
+
/** Our controlling tty as the terminal apps report it (`/dev/ttys003`), or null. */
|
|
28
|
+
export function controllingTty(pid = process.pid, run = spawnSync) {
|
|
29
|
+
try {
|
|
30
|
+
const r = run('ps', ['-o', 'tty=', '-p', String(pid)], { encoding: 'utf8', timeout: 1500 });
|
|
31
|
+
const t = String(r.stdout ?? '').trim();
|
|
32
|
+
return /^ttys?\d+$/.test(t) ? `/dev/${t}` : null;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** The JXA probe. `tty`/`kind` are JSON-encoded into the script (never interpolated raw). */
|
|
39
|
+
export function dockScript(kind, tty, follow, everyS = 0.5) {
|
|
40
|
+
const every = Math.max(0.25, Math.min(5, Number.isFinite(everyS) ? everyS : 0.5));
|
|
41
|
+
return `ObjC.import('AppKit');
|
|
42
|
+
ObjC.bindFunction('getppid', ['int', []]);
|
|
43
|
+
var KIND = ${JSON.stringify(kind)}, TTY = ${JSON.stringify(tty)}, FOLLOW = ${follow ? 'true' : 'false'}, EVERY = ${every};
|
|
44
|
+
var PARENT = $.getppid(), winId = null;
|
|
45
|
+
function rect(b) {
|
|
46
|
+
if (!b) return null;
|
|
47
|
+
if (Array.isArray(b) && b.length === 4) return [b[0], b[1], b[2] - b[0], b[3] - b[1]];
|
|
48
|
+
if (typeof b.width === 'number') return [b.x, b.y, b.width, b.height];
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function screens() {
|
|
52
|
+
var out = [], all = $.NSScreen.screens, mainH = all.objectAtIndex(0).frame.size.height;
|
|
53
|
+
for (var i = 0; i < all.count; i++) {
|
|
54
|
+
var s = all.objectAtIndex(i), f = s.frame, v = s.visibleFrame;
|
|
55
|
+
out.push({ frame: [f.origin.x, mainH - f.origin.y - f.size.height, f.size.width, f.size.height],
|
|
56
|
+
visible: [v.origin.x, mainH - v.origin.y - v.size.height, v.size.width, v.size.height], scale: s.backingScaleFactor });
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
function find(app, tabsOf) {
|
|
61
|
+
if (!app.running()) return null;
|
|
62
|
+
if (winId !== null) { try { return rect(app.windows.byId(winId).bounds()); } catch (e) { winId = null; } }
|
|
63
|
+
var ws = app.windows();
|
|
64
|
+
for (var i = 0; i < ws.length; i++) { if (TTY && tabsOf(ws[i])) { winId = ws[i].id(); return rect(ws[i].bounds()); } }
|
|
65
|
+
return ws.length ? rect(ws[0].bounds()) : null;
|
|
66
|
+
}
|
|
67
|
+
function term() {
|
|
68
|
+
try {
|
|
69
|
+
if (KIND === 'terminal') return find(Application('com.apple.Terminal'), function (w) {
|
|
70
|
+
var t = w.tabs(); for (var j = 0; j < t.length; j++) if (t[j].tty() === TTY) return true; return false; });
|
|
71
|
+
if (KIND === 'iterm') return find(Application('com.googlecode.iterm2'), function (w) {
|
|
72
|
+
var t = w.tabs(); for (var j = 0; j < t.length; j++) { var s = t[j].sessions(); for (var k = 0; k < s.length; k++) if (s[k].tty() === TTY) return true; } return false; });
|
|
73
|
+
} catch (e) { return null; }
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
function out(s) { $.NSFileHandle.fileHandleWithStandardOutput.writeData($(s + '\\n').dataUsingEncoding($.NSUTF8StringEncoding)); }
|
|
77
|
+
function run() {
|
|
78
|
+
var sc = screens(), last = '';
|
|
79
|
+
out(JSON.stringify({ screens: sc, term: null, pending: KIND !== 'unknown' }));
|
|
80
|
+
for (;;) {
|
|
81
|
+
var s = JSON.stringify({ screens: screens(), term: term(), pending: false });
|
|
82
|
+
if (s !== last) { out(s); last = s; }
|
|
83
|
+
if (!FOLLOW) return '';
|
|
84
|
+
var pp = $.getppid(); if (pp !== PARENT || pp === 1) return '';
|
|
85
|
+
delay(EVERY);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
90
|
+
const finite = (n) => typeof n === 'number' && Number.isFinite(n);
|
|
91
|
+
const toRect = (a) => Array.isArray(a) && a.length === 4 && a.every(finite) && a[2] > 0 && a[3] > 0 ? { x: a[0], y: a[1], w: a[2], h: a[3] } : null;
|
|
92
|
+
/** Validate one probe line. Anything malformed → null (the caller keeps its last placement). */
|
|
93
|
+
export function parseDockProbe(line) {
|
|
94
|
+
let o;
|
|
95
|
+
try {
|
|
96
|
+
o = JSON.parse(line);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
if (!o || typeof o !== 'object')
|
|
102
|
+
return null;
|
|
103
|
+
const r = o;
|
|
104
|
+
if (!Array.isArray(r.screens) || r.screens.length === 0 || r.screens.length > 16)
|
|
105
|
+
return null;
|
|
106
|
+
const screens = [];
|
|
107
|
+
for (const s of r.screens) {
|
|
108
|
+
const so = (s ?? {});
|
|
109
|
+
const frame = toRect(so.frame), visible = toRect(so.visible);
|
|
110
|
+
if (!frame || !visible)
|
|
111
|
+
return null;
|
|
112
|
+
screens.push({ frame, visible, scale: finite(so.scale) ? so.scale : 1 });
|
|
113
|
+
}
|
|
114
|
+
return { screens, term: toRect(r.term), pending: r.pending === true };
|
|
115
|
+
}
|
|
116
|
+
const contains = (r, x, y) => x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h;
|
|
117
|
+
/**
|
|
118
|
+
* Where the popup goes. Pure: unit-tested with synthetic screens/terminals.
|
|
119
|
+
* right — the terminal's right edge, same top + height (a narrower window if 360–width pt fit)
|
|
120
|
+
* left — mirrored, when the right side has no room
|
|
121
|
+
* overlap — neither side fits: the screen's right edge, always-on-top, over part of the terminal
|
|
122
|
+
* screen-right — unknown terminal: the main screen's right edge, full visible height, always-on-top
|
|
123
|
+
*/
|
|
124
|
+
export function dockRect(term, screens, kind, opts = {}) {
|
|
125
|
+
const want = Math.max(360, Math.round(opts.width ?? 560));
|
|
126
|
+
const gap = Math.max(0, Math.round(opts.gap ?? 4));
|
|
127
|
+
const main = screens[0]?.visible ?? { x: 0, y: 25, w: 1440, h: 875 };
|
|
128
|
+
if (!term) {
|
|
129
|
+
const w = Math.min(want, Math.round(main.w * 0.5));
|
|
130
|
+
return { rect: { x: main.x + main.w - w, y: main.y, w, h: main.h }, mode: 'screen-right', ontop: true, kind };
|
|
131
|
+
}
|
|
132
|
+
const cx = term.x + term.w / 2, cy = term.y + Math.min(term.h / 2, 40);
|
|
133
|
+
const V = (screens.find((s) => contains(s.frame, cx, cy)) ?? screens[0])?.visible ?? main;
|
|
134
|
+
const h = Math.max(200, Math.min(term.h, V.h));
|
|
135
|
+
const y = Math.max(V.y, Math.min(term.y, V.y + V.h - h));
|
|
136
|
+
const right = V.x + V.w - (term.x + term.w) - gap;
|
|
137
|
+
const left = term.x - V.x - gap;
|
|
138
|
+
if (right >= want)
|
|
139
|
+
return { rect: { x: term.x + term.w + gap, y, w: want, h }, mode: 'right', ontop: false, kind };
|
|
140
|
+
if (right >= 360)
|
|
141
|
+
return { rect: { x: term.x + term.w + gap, y, w: right, h }, mode: 'right', ontop: false, kind };
|
|
142
|
+
if (left >= want)
|
|
143
|
+
return { rect: { x: term.x - gap - want, y, w: want, h }, mode: 'left', ontop: false, kind };
|
|
144
|
+
const w = Math.min(want, V.w);
|
|
145
|
+
return { rect: { x: V.x + V.w - w, y, w, h }, mode: 'overlap', ontop: true, kind };
|
|
146
|
+
}
|
|
147
|
+
const same = (a, b) => !!a && a.mode === b.mode && Math.abs(a.rect.x - b.rect.x) < 1 && Math.abs(a.rect.y - b.rect.y) < 1 && Math.abs(a.rect.w - b.rect.w) < 1 && Math.abs(a.rect.h - b.rect.h) < 1;
|
|
148
|
+
/** Probes and follows the terminal window. Every failure degrades to "no docking"; it never throws. */
|
|
149
|
+
export class TerminalDock {
|
|
150
|
+
kind;
|
|
151
|
+
enabled;
|
|
152
|
+
tty;
|
|
153
|
+
opts;
|
|
154
|
+
child = null;
|
|
155
|
+
last = null;
|
|
156
|
+
onExit = () => this.stop();
|
|
157
|
+
constructor(opts = {}) {
|
|
158
|
+
this.opts = opts;
|
|
159
|
+
this.kind = detectTerminal(opts.env ?? process.env);
|
|
160
|
+
this.enabled = (opts.platform ?? process.platform) === 'darwin';
|
|
161
|
+
this.tty = opts.tty !== undefined ? opts.tty : (this.enabled && this.kind !== 'unknown' ? controllingTty() : null);
|
|
162
|
+
}
|
|
163
|
+
get following() { return this.child !== null; }
|
|
164
|
+
run(follow) {
|
|
165
|
+
const args = ['-l', 'JavaScript', '-e', dockScript(this.kind, this.tty, follow, this.opts.followEvery)];
|
|
166
|
+
return this.opts.spawnImpl ? this.opts.spawnImpl('osascript', args) : spawn('osascript', args, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
167
|
+
}
|
|
168
|
+
place1(p) {
|
|
169
|
+
return dockRect(p.term, p.screens, p.term ? this.kind : (this.kind === 'unknown' ? 'unknown' : this.kind), { width: this.opts.width, gap: this.opts.gap });
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* One probe, at most `timeoutMs`. Resolves with the terminal-docked placement, or — when the terminal did
|
|
173
|
+
* not answer in time (first-run Automation prompt, app busy) — the screen-right placement from the screens
|
|
174
|
+
* line, or null off macOS / when even that failed.
|
|
175
|
+
*/
|
|
176
|
+
place(timeoutMs = 1500) {
|
|
177
|
+
if (!this.enabled)
|
|
178
|
+
return Promise.resolve(null);
|
|
179
|
+
return new Promise((resolve) => {
|
|
180
|
+
let best = null, buf = '', done = false;
|
|
181
|
+
let child;
|
|
182
|
+
try {
|
|
183
|
+
child = this.run(false);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
resolve(null);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const finish = () => {
|
|
190
|
+
if (done)
|
|
191
|
+
return;
|
|
192
|
+
done = true;
|
|
193
|
+
clearTimeout(t);
|
|
194
|
+
try {
|
|
195
|
+
child.kill('SIGKILL');
|
|
196
|
+
}
|
|
197
|
+
catch { /* gone */ }
|
|
198
|
+
const p = best ? this.place1(best) : null;
|
|
199
|
+
if (p)
|
|
200
|
+
this.last = p;
|
|
201
|
+
resolve(p);
|
|
202
|
+
};
|
|
203
|
+
const t = setTimeout(finish, timeoutMs);
|
|
204
|
+
child.on('error', finish);
|
|
205
|
+
child.on('exit', finish);
|
|
206
|
+
child.stdout?.on('data', (d) => {
|
|
207
|
+
buf += d.toString();
|
|
208
|
+
let i;
|
|
209
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
210
|
+
const p = parseDockProbe(buf.slice(0, i));
|
|
211
|
+
buf = buf.slice(i + 1);
|
|
212
|
+
if (p) {
|
|
213
|
+
best = p;
|
|
214
|
+
if (!p.pending)
|
|
215
|
+
finish();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/** Follow the terminal: `onPlace` fires on every changed placement. Idempotent; stop() ends it. */
|
|
222
|
+
follow(onPlace) {
|
|
223
|
+
if (!this.enabled || this.child || this.kind === 'unknown')
|
|
224
|
+
return;
|
|
225
|
+
let buf = '';
|
|
226
|
+
try {
|
|
227
|
+
this.child = this.run(true);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
this.child = null;
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const child = this.child;
|
|
234
|
+
child.on('error', () => { if (this.child === child)
|
|
235
|
+
this.child = null; });
|
|
236
|
+
child.on('exit', () => { if (this.child === child)
|
|
237
|
+
this.child = null; });
|
|
238
|
+
child.stdout?.on('data', (d) => {
|
|
239
|
+
buf += d.toString();
|
|
240
|
+
let i;
|
|
241
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
242
|
+
const p = parseDockProbe(buf.slice(0, i));
|
|
243
|
+
buf = buf.slice(i + 1);
|
|
244
|
+
if (!p || p.pending || !p.term)
|
|
245
|
+
continue; // never jump to screen-right while following
|
|
246
|
+
const pl = this.place1(p);
|
|
247
|
+
if (!same(this.last, pl)) {
|
|
248
|
+
this.last = pl;
|
|
249
|
+
try {
|
|
250
|
+
onPlace(pl);
|
|
251
|
+
}
|
|
252
|
+
catch { /* the window may be gone */ }
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
process.once('exit', this.onExit);
|
|
257
|
+
}
|
|
258
|
+
stop() {
|
|
259
|
+
const c = this.child;
|
|
260
|
+
this.child = null;
|
|
261
|
+
process.removeListener('exit', this.onExit);
|
|
262
|
+
if (c) {
|
|
263
|
+
try {
|
|
264
|
+
c.kill('SIGTERM');
|
|
265
|
+
}
|
|
266
|
+
catch { /* gone */ }
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** The child's pid while following (for the CPU measurement). */
|
|
270
|
+
get pid() { return this.child?.pid; }
|
|
271
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// dexterm popup host — the window layer behind the PopupHost interface (task-1290).
|
|
2
|
+
//
|
|
3
|
+
// Two implementations, chosen at runtime:
|
|
4
|
+
// * ShellPopupHost — spawns the frameless wry shell (shell/wry) and speaks the stdio control protocol.
|
|
5
|
+
// Opens chromeless, transparent, always-on-top HUD windows. native = true.
|
|
6
|
+
// * BrowserPopupHost — graceful fallback when the shell binary or a display is unavailable: opens the
|
|
7
|
+
// same loopback URL in the OS default browser (`open` / `xdg-open`). native = false.
|
|
8
|
+
//
|
|
9
|
+
// The popup URL (which carries a single-use ticket) is handed to the shell over STDIN, never argv, so `ps`
|
|
10
|
+
// from another local user cannot read it. The browser fallback necessarily passes it as an argument — that
|
|
11
|
+
// tradeoff is documented in the README threat notes; the ticket is single-use and loopback-only.
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { EventEmitter } from 'node:events';
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { DEFAULT_HOTKEY, DEFAULT_HOTKEY_CLOSE } from "./config.js";
|
|
18
|
+
import { isLoopbackUrl } from "./loopback.js";
|
|
19
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
/** The built wry shell binary. `DEXTERM_POPUP_SHELL` overrides (e.g. a packaged location). */
|
|
21
|
+
export function shellBinaryPath(env = process.env) {
|
|
22
|
+
if (env.DEXTERM_POPUP_SHELL && env.DEXTERM_POPUP_SHELL.trim())
|
|
23
|
+
return env.DEXTERM_POPUP_SHELL;
|
|
24
|
+
return path.join(here, '..', 'shell', 'wry', 'target', 'release', 'dexterm-shell-wry');
|
|
25
|
+
}
|
|
26
|
+
/** Whether a GUI display is reachable (a frameless window can be created). Browser fallback otherwise. */
|
|
27
|
+
export function displayAvailable(env = process.env, platform = process.platform) {
|
|
28
|
+
if (platform === 'darwin')
|
|
29
|
+
return true; // macOS GUI session (a locked screen still has a display)
|
|
30
|
+
if (platform === 'win32')
|
|
31
|
+
return true;
|
|
32
|
+
return !!(env.DISPLAY || env.WAYLAND_DISPLAY); // X11 / Wayland on Linux; empty in a headless/CI box
|
|
33
|
+
}
|
|
34
|
+
/** Whether `createPopupHost` would pick the native shell (binary present + display), without starting anything. */
|
|
35
|
+
export function nativeShellAvailable(env = process.env, platform = process.platform) {
|
|
36
|
+
return existsSync(shellBinaryPath(env)) && displayAvailable(env, platform);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Decide and construct the host without starting it. The shell is chosen only when its binary exists AND a
|
|
40
|
+
* display is available AND the browser was not forced; otherwise the browser fallback is returned with a
|
|
41
|
+
* reason the caller can show the user ("wry shell not built", "no display", ...).
|
|
42
|
+
*/
|
|
43
|
+
export function createPopupHost(opts = {}) {
|
|
44
|
+
const env = opts.env ?? process.env;
|
|
45
|
+
const platform = opts.platform ?? process.platform;
|
|
46
|
+
if (opts.forceBrowser)
|
|
47
|
+
return { host: new BrowserPopupHost(platform, opts.port), native: false, reason: 'browser requested' };
|
|
48
|
+
const bin = shellBinaryPath(env);
|
|
49
|
+
if (!existsSync(bin))
|
|
50
|
+
return { host: new BrowserPopupHost(platform, opts.port), native: false, reason: `wry shell not built (${bin}); run \`npm run build:shell\` in @dexterm/popup` };
|
|
51
|
+
if (!displayAvailable(env, platform))
|
|
52
|
+
return { host: new BrowserPopupHost(platform, opts.port), native: false, reason: 'no display available' };
|
|
53
|
+
return { host: new ShellPopupHost({ bin, hotkey: opts.hotkey ?? DEFAULT_HOTKEY, hotkeyClose: opts.hotkeyClose ?? DEFAULT_HOTKEY_CLOSE, env, ...(opts.proxyPort ? { proxyPort: opts.proxyPort } : {}) }), native: true, reason: 'wry shell' };
|
|
54
|
+
}
|
|
55
|
+
export class ShellPopupHost extends EventEmitter {
|
|
56
|
+
native = true;
|
|
57
|
+
proc = null;
|
|
58
|
+
spawnedAt = 0;
|
|
59
|
+
bin;
|
|
60
|
+
hotkey;
|
|
61
|
+
env;
|
|
62
|
+
hotkeyClose;
|
|
63
|
+
proxyPort;
|
|
64
|
+
/** Set from shell.ready (task-1325). */
|
|
65
|
+
hotkeyCloseRegistered = false;
|
|
66
|
+
/** task-1358 r4: the sink proxy the shell confirmed in shell.ready (`127.0.0.1:<port>`), or null. */
|
|
67
|
+
sinkProxy = null;
|
|
68
|
+
constructor(deps) { super(); this.bin = deps.bin; this.hotkey = deps.hotkey; this.env = deps.env; this.hotkeyClose = deps.hotkeyClose ?? DEFAULT_HOTKEY_CLOSE; this.proxyPort = deps.proxyPort; }
|
|
69
|
+
async start() {
|
|
70
|
+
if (this.proc)
|
|
71
|
+
return { hotkeyRegistered: false, spawnToReadyMs: 0, sinkProxy: this.sinkProxy };
|
|
72
|
+
this.spawnedAt = performance.now();
|
|
73
|
+
// The hotkey chord (and, r4, the sink proxy address) reach the shell via the environment, never a
|
|
74
|
+
// shell-interpolated command line.
|
|
75
|
+
const env = { ...this.env, DEXTERM_POPUP_HOTKEY: this.hotkey, DEXTERM_POPUP_HOTKEY_CLOSE: this.hotkeyClose, ...(this.proxyPort ? { DEXTERM_POPUP_PROXY: `127.0.0.1:${this.proxyPort}` } : {}) };
|
|
76
|
+
this.proc = spawn(this.bin, [], { stdio: ['pipe', 'pipe', 'pipe'], env });
|
|
77
|
+
let buf = '';
|
|
78
|
+
this.proc.stdout.on('data', (d) => {
|
|
79
|
+
buf += d.toString();
|
|
80
|
+
let i;
|
|
81
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
82
|
+
const line = buf.slice(0, i);
|
|
83
|
+
buf = buf.slice(i + 1);
|
|
84
|
+
if (!line.trim())
|
|
85
|
+
continue;
|
|
86
|
+
let ev;
|
|
87
|
+
try {
|
|
88
|
+
ev = JSON.parse(line);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
this.emit('stderr', line);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
this.emit('shell', ev);
|
|
95
|
+
if (ev.ev)
|
|
96
|
+
this.emit(ev.ev, ev);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
this.proc.stderr.on('data', (d) => this.emit('stderr', d.toString().trimEnd()));
|
|
100
|
+
this.proc.on('exit', (code, sig) => { this.proc = null; this.emit('exit', { code, sig }); });
|
|
101
|
+
this.proc.on('error', (err) => this.emit('error', err));
|
|
102
|
+
const ready = await this.waitFor('shell.ready', 20_000);
|
|
103
|
+
this.hotkeyCloseRegistered = !!ready.hotkey_close_registered;
|
|
104
|
+
this.sinkProxy = typeof ready.sink_proxy === 'string' ? ready.sink_proxy : null;
|
|
105
|
+
return { hotkeyRegistered: !!ready.hotkey_registered, spawnToReadyMs: +(performance.now() - this.spawnedAt).toFixed(1), sinkProxy: this.sinkProxy };
|
|
106
|
+
}
|
|
107
|
+
sendOp(op) { if (!this.proc)
|
|
108
|
+
throw new Error('shell not running'); this.proc.stdin.write(JSON.stringify(op) + '\n'); }
|
|
109
|
+
open(id, url, geom = {}) {
|
|
110
|
+
// The URL reaching here is already pinned to our server origin (assertLoopbackUrl / a server ticket).
|
|
111
|
+
// The shell pins every later navigation of this window to the same origin; `port` tells it which.
|
|
112
|
+
let port;
|
|
113
|
+
try {
|
|
114
|
+
const u = new URL(url);
|
|
115
|
+
port = Number(u.port || '80');
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
port = undefined;
|
|
119
|
+
}
|
|
120
|
+
this.sendOp({ op: 'open', id, url, port, x: geom.x ?? 80, y: geom.y ?? 80, w: geom.w ?? 960, h: geom.h ?? 640, ontop: geom.ontop ?? true, focus: geom.focus ?? false });
|
|
121
|
+
}
|
|
122
|
+
close(id) { this.sendOp({ op: 'close', id }); }
|
|
123
|
+
show(id) { this.sendOp({ op: 'show', id }); }
|
|
124
|
+
hide(id) { this.sendOp({ op: 'hide', id }); }
|
|
125
|
+
focus(id) { this.sendOp({ op: 'focus', id }); }
|
|
126
|
+
alwaysOnTop(id, on) { this.sendOp({ op: 'ontop', id, on }); }
|
|
127
|
+
clickThrough(id, on) { this.sendOp({ op: 'clickthrough', id, on }); }
|
|
128
|
+
move(id, x, y) { this.sendOp({ op: 'move', id, x, y }); }
|
|
129
|
+
setBounds(id, r) { this.sendOp({ op: 'bounds', id, x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.w), h: Math.round(r.h) }); }
|
|
130
|
+
snapshot(id, file) { this.sendOp({ op: 'snapshot', id, path: file }); }
|
|
131
|
+
quit() {
|
|
132
|
+
if (!this.proc)
|
|
133
|
+
return;
|
|
134
|
+
try {
|
|
135
|
+
this.sendOp({ op: 'quit' });
|
|
136
|
+
}
|
|
137
|
+
catch { /* already gone */ }
|
|
138
|
+
const p = this.proc;
|
|
139
|
+
setTimeout(() => { try {
|
|
140
|
+
p.kill('SIGKILL');
|
|
141
|
+
}
|
|
142
|
+
catch { /* ignore */ } }, 1500).unref();
|
|
143
|
+
}
|
|
144
|
+
get pid() { return this.proc?.pid; }
|
|
145
|
+
waitFor(ev, timeoutMs = 10_000, pred = () => true) {
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
const t = setTimeout(() => { this.off(ev, h); reject(new Error(`timeout waiting for ${ev}`)); }, timeoutMs);
|
|
148
|
+
const h = (e) => { if (!pred(e))
|
|
149
|
+
return; clearTimeout(t); this.off(ev, h); resolve(e); };
|
|
150
|
+
this.on(ev, h);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// ------------------------------------------------------------------ browser fallback host
|
|
155
|
+
/**
|
|
156
|
+
* Opens popups as plain tabs/windows in the OS default browser. Everything else is a no-op: a browser tab
|
|
157
|
+
* cannot be made always-on-top / click-through / frameless, and closing it is the user's job. The loopback
|
|
158
|
+
* URL and its single-use ticket are identical to the shell path, so the security model is unchanged.
|
|
159
|
+
*/
|
|
160
|
+
export class BrowserPopupHost extends EventEmitter {
|
|
161
|
+
native = false;
|
|
162
|
+
platform;
|
|
163
|
+
port;
|
|
164
|
+
opened = new Set();
|
|
165
|
+
/** `port`: when known, only http://127.0.0.1:<port> URLs are ever launched (the manager passes the server's). */
|
|
166
|
+
constructor(platform = process.platform, port) { super(); this.platform = platform; this.port = port; }
|
|
167
|
+
start() { return Promise.resolve(null); }
|
|
168
|
+
open(id, url) {
|
|
169
|
+
if (!openInBrowser(url, this.platform, this.port)) {
|
|
170
|
+
this.emit('error', new Error(`refused to open ${url} in a browser on ${this.platform} (not our loopback origin, or no launcher)`));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.opened.add(id);
|
|
174
|
+
this.emit('window.created', { ev: 'window.created', id, t_ms: 0, create_ms: 0 });
|
|
175
|
+
}
|
|
176
|
+
close(id) { this.opened.delete(id); /* a browser tab cannot be closed from here */ }
|
|
177
|
+
show(_id) { }
|
|
178
|
+
hide(_id) { }
|
|
179
|
+
focus(_id) { }
|
|
180
|
+
alwaysOnTop(_id, _on) { }
|
|
181
|
+
clickThrough(_id, _on) { }
|
|
182
|
+
move(_id, _x, _y) { }
|
|
183
|
+
setBounds(_id, _r) { }
|
|
184
|
+
quit() { this.opened.clear(); }
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Launch a URL in the OS browser: `open` (macOS) or `xdg-open` (Linux); `cmd /c start "" <url>` on Windows.
|
|
188
|
+
* Only http://127.0.0.1 URLs are ever launched — enforced here by a parsed comparison (see loopback.ts),
|
|
189
|
+
* not just assumed: https, remote hosts, userinfo (`http://127.0.0.1:1@evil.example/`), `localhost`,
|
|
190
|
+
* `[::1]` and non-http schemes are refused so a remote page can never be opened via the fallback (ROADMAP
|
|
191
|
+
* "no remote content"); with `port` given, any other loopback port is refused too. The URL is a single
|
|
192
|
+
* argument — no shell interpolation. Returns false when refused or when no launcher is available.
|
|
193
|
+
*/
|
|
194
|
+
export function openInBrowser(url, platform = process.platform, port) {
|
|
195
|
+
if (!isLoopbackUrl(url, port))
|
|
196
|
+
return false;
|
|
197
|
+
const href = new URL(url).toString();
|
|
198
|
+
const cmd = platform === 'darwin' ? ['open', href]
|
|
199
|
+
: platform === 'win32' ? ['cmd', '/c', 'start', '', href]
|
|
200
|
+
: ['xdg-open', href];
|
|
201
|
+
try {
|
|
202
|
+
const p = spawn(cmd[0], cmd.slice(1), { stdio: 'ignore' });
|
|
203
|
+
p.on('error', () => { });
|
|
204
|
+
p.unref?.();
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @dexterm/popup — loopback popup host: a frameless HUD-window layer (wry) with a graceful OS-browser
|
|
2
|
+
// fallback, driven by a loopback-only server and a typed protocol (task-1290). The terminal is the hub;
|
|
3
|
+
// heavy views (chart, token detail, feeds) open as chromeless always-on-top windows over the same data.
|
|
4
|
+
export * from "./protocol.js";
|
|
5
|
+
export { createPopupServer, POPUP_EVENTS, IMAGE_ROUTE_RE, IMAGE_ROUTE_PREFIX } from "./server.js";
|
|
6
|
+
export { auditWidgetHtml, inlineHashes, normalizeWidgetHtml, cspHash, WIDGET_INLINE_MAX } from "./widget-html.js";
|
|
7
|
+
export { createPopupHost, ShellPopupHost, BrowserPopupHost, openInBrowser, shellBinaryPath, displayAvailable, nativeShellAvailable } from "./host.js";
|
|
8
|
+
export { createPopupManager, assertLoopbackUrl } from "./open.js";
|
|
9
|
+
export { loopbackUrlProblem, isLoopbackUrl } from "./loopback.js";
|
|
10
|
+
export { loadConfig, normalizeConfig, configPath, dexHome, DEFAULT_CONFIG, DEFAULT_HOTKEY, DEFAULT_HOTKEY_CLOSE, DEFAULT_DOCK, DEFAULT_RAIN } from "./config.js";
|
|
11
|
+
export { detectTerminal, controllingTty, dockScript, parseDockProbe, dockRect, TerminalDock } from "./dock.js";
|
|
12
|
+
export { runSelfTest, renderSelfTest, fixtureChartVM, fixtureRosterVM } from "./selftest.js";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// The one URL pin every popup path goes through (task-1290 follow-up): a popup may only load, navigate to,
|
|
2
|
+
// or be launched in a browser at OUR loopback server origin, http://127.0.0.1:<port>/… — never a remote
|
|
3
|
+
// page (ROADMAP "no remote content"), never another local service.
|
|
4
|
+
//
|
|
5
|
+
// This is a parsed comparison, not a string prefix: `http://127.0.0.1:1@evil.example/` has host
|
|
6
|
+
// evil.example (the "127.0.0.1:1" is userinfo), so a prefix match on "http://127.0.0.1:" passes it and a
|
|
7
|
+
// parsed one refuses it. Any userinfo is refused outright, even to our own host, so the launched URL can
|
|
8
|
+
// never carry a credential-shaped prefix. The hostname must be exactly 127.0.0.1 after WHATWG
|
|
9
|
+
// normalisation (so `0x7f.1`, `2130706433`, `127.0.0.%31` and `HTTP://` all resolve to our host and
|
|
10
|
+
// pass — they ARE 127.0.0.1 — while `localhost`, `[::1]`, `127.0.0.1.evil.example` and `evil.example`
|
|
11
|
+
// are refused: the server only answers `Host: 127.0.0.1:<port>`). The port must equal the server's
|
|
12
|
+
// (an http:// URL with no explicit port means 80).
|
|
13
|
+
//
|
|
14
|
+
// The wry shell applies the same rule in Rust (shell/wry/src/main.rs `nav_allowed`) as defence in depth.
|
|
15
|
+
/** Why `raw` is not an allowed popup URL for the server on `port`, or null when it is allowed. */
|
|
16
|
+
export function loopbackUrlProblem(raw, port) {
|
|
17
|
+
let u;
|
|
18
|
+
try {
|
|
19
|
+
u = new URL(raw);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return `popup url is not a valid URL: ${raw}`;
|
|
23
|
+
}
|
|
24
|
+
if (u.protocol !== 'http:')
|
|
25
|
+
return `popup url must be an http:// loopback url, got ${u.protocol}//`;
|
|
26
|
+
if (u.username !== '' || u.password !== '')
|
|
27
|
+
return 'popup url must not carry userinfo (user:pass@)';
|
|
28
|
+
if (u.hostname !== '127.0.0.1')
|
|
29
|
+
return `popup url host must be 127.0.0.1, got ${u.hostname || '(empty)'}`;
|
|
30
|
+
if ((u.port || '80') !== String(port))
|
|
31
|
+
return `popup url port must be the popup server's (${port}), got ${u.port || '(default)'}`;
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/** The normalised URL when allowed (see loopbackUrlProblem); throws otherwise. */
|
|
35
|
+
export function assertLoopbackUrl(raw, port) {
|
|
36
|
+
const problem = loopbackUrlProblem(raw, port);
|
|
37
|
+
if (problem)
|
|
38
|
+
throw new Error(problem);
|
|
39
|
+
return new URL(raw).toString();
|
|
40
|
+
}
|
|
41
|
+
/** True when `raw` is an http:// URL at exactly 127.0.0.1 with no userinfo (and on `port`, when given). */
|
|
42
|
+
export function isLoopbackUrl(raw, port) {
|
|
43
|
+
let u;
|
|
44
|
+
try {
|
|
45
|
+
u = new URL(raw);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (u.protocol !== 'http:' || u.username !== '' || u.password !== '' || u.hostname !== '127.0.0.1')
|
|
51
|
+
return false;
|
|
52
|
+
return port === undefined || (u.port || '80') === String(port);
|
|
53
|
+
}
|