@sirenodeck/sirenodeck 0.1.1-alpha.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/LICENSE +21 -0
- package/README.md +69 -0
- package/bin/_wrapper-shared.js +42 -0
- package/bin/dev.js +95 -0
- package/bin/sirenodeck.js +42 -0
- package/config/default-config.yml +117 -0
- package/config/starter-config.yml +54 -0
- package/dist/main.mjs +64702 -0
- package/package.json +140 -0
- package/src/action/executor.ts +156 -0
- package/src/action/index.ts +7 -0
- package/src/addon/api-types.ts +15 -0
- package/src/addon/api.ts +461 -0
- package/src/addon/check-runner.ts +84 -0
- package/src/addon/index.ts +5 -0
- package/src/addon/loader.ts +441 -0
- package/src/addon/registry.ts +230 -0
- package/src/addon/spec.ts +69 -0
- package/src/api/addon.ts +33 -0
- package/src/api/asset-url.ts +24 -0
- package/src/api/index.ts +50 -0
- package/src/api/protocol-internal.ts +656 -0
- package/src/api/protocol.ts +2 -0
- package/src/api/react/index.ts +11 -0
- package/src/api/react/registry.ts +91 -0
- package/src/api/react/use-addon-channel.ts +26 -0
- package/src/api/react/use-button-state.tsx +23 -0
- package/src/api/react/use-deck.ts +24 -0
- package/src/assets/logo.png +0 -0
- package/src/assets/logo72x72.png +0 -0
- package/src/assets/logoFull.png +0 -0
- package/src/builtin-addons/brightness/README.md +51 -0
- package/src/builtin-addons/brightness/buttons/brightness/backend.ts +13 -0
- package/src/builtin-addons/brightness/buttons/brightness/config.ts +10 -0
- package/src/builtin-addons/brightness/buttons/brightness/frontend.tsx +50 -0
- package/src/builtin-addons/brightness/domain/macos.ts +46 -0
- package/src/builtin-addons/brightness/index.d.ts +5 -0
- package/src/builtin-addons/brightness/index.ts +20 -0
- package/src/builtin-addons/brightness/poller.ts +27 -0
- package/src/builtin-addons/brightness/providers/darwin.ts +107 -0
- package/src/builtin-addons/brightness/providers/index.ts +81 -0
- package/src/builtin-addons/brightness/providers/linux.ts +108 -0
- package/src/builtin-addons/brightness/providers/windows.ts +77 -0
- package/src/builtin-addons/brightness/sirenodeck.json +6 -0
- package/src/builtin-addons/coding-agents/assets/claude-code.svg +1 -0
- package/src/builtin-addons/coding-agents/assets/opencode-dark-square.svg +1 -0
- package/src/builtin-addons/coding-agents/buttons/agent/backend.ts +82 -0
- package/src/builtin-addons/coding-agents/buttons/agent/config.ts +13 -0
- package/src/builtin-addons/coding-agents/buttons/agent/frontend.tsx +130 -0
- package/src/builtin-addons/coding-agents/buttons/agent/pages.tsx +150 -0
- package/src/builtin-addons/coding-agents/buttons/summary/backend.ts +48 -0
- package/src/builtin-addons/coding-agents/buttons/summary/config.ts +11 -0
- package/src/builtin-addons/coding-agents/buttons/summary/frontend.tsx +254 -0
- package/src/builtin-addons/coding-agents/decks/agents.ts +69 -0
- package/src/builtin-addons/coding-agents/global/backend.ts +402 -0
- package/src/builtin-addons/coding-agents/global-entry.ts +7 -0
- package/src/builtin-addons/coding-agents/hook/script.ts +123 -0
- package/src/builtin-addons/coding-agents/index.ts +15 -0
- package/src/builtin-addons/coding-agents/manifest.ts +115 -0
- package/src/builtin-addons/coding-agents/providers/claude-code.ts +470 -0
- package/src/builtin-addons/coding-agents/providers/claude-instances.ts +184 -0
- package/src/builtin-addons/coding-agents/providers/opencode-instances.ts +108 -0
- package/src/builtin-addons/coding-agents/providers/opencode.ts +636 -0
- package/src/builtin-addons/coding-agents/providers/registry.ts +120 -0
- package/src/builtin-addons/coding-agents/providers/spawn.ts +112 -0
- package/src/builtin-addons/coding-agents/shared/claude-hook-events.ts +55 -0
- package/src/builtin-addons/coding-agents/shared/claude-status.ts +236 -0
- package/src/builtin-addons/coding-agents/shared/live-count.ts +39 -0
- package/src/builtin-addons/coding-agents/shared/notifier.ts +58 -0
- package/src/builtin-addons/coding-agents/shared/opencode-status.ts +82 -0
- package/src/builtin-addons/coding-agents/shared/snapshot.ts +66 -0
- package/src/builtin-addons/coding-agents/shared/state.ts +72 -0
- package/src/builtin-addons/coding-agents/sirenodeck.json +6 -0
- package/src/builtin-addons/coding-agents/types/types.ts +33 -0
- package/src/builtin-addons/core/README.md +89 -0
- package/src/builtin-addons/core/buttons/action/backend.ts +7 -0
- package/src/builtin-addons/core/buttons/action/config.ts +13 -0
- package/src/builtin-addons/core/buttons/action/frontend.tsx +16 -0
- package/src/builtin-addons/core/buttons/change-deck/backend.ts +18 -0
- package/src/builtin-addons/core/buttons/change-deck/config.ts +15 -0
- package/src/builtin-addons/core/buttons/change-deck/frontend.tsx +18 -0
- package/src/builtin-addons/core/buttons/media-sample/backend.ts +8 -0
- package/src/builtin-addons/core/buttons/media-sample/config.ts +7 -0
- package/src/builtin-addons/core/buttons/media-sample/frontend.tsx +2 -0
- package/src/builtin-addons/core/buttons/page-nav/backend.ts +28 -0
- package/src/builtin-addons/core/buttons/page-nav/config.ts +9 -0
- package/src/builtin-addons/core/buttons/page-nav/frontend.tsx +68 -0
- package/src/builtin-addons/core/buttons/toggle/backend.ts +119 -0
- package/src/builtin-addons/core/buttons/toggle/config.ts +57 -0
- package/src/builtin-addons/core/buttons/toggle/frontend.tsx +57 -0
- package/src/builtin-addons/core/buttons/toggle/global-service.ts +174 -0
- package/src/builtin-addons/core/index.ts +120 -0
- package/src/builtin-addons/core/sirenodeck.json +6 -0
- package/src/builtin-addons/date-time/README.md +73 -0
- package/src/builtin-addons/date-time/buttons/analog-clock/backend.ts +10 -0
- package/src/builtin-addons/date-time/buttons/analog-clock/config.ts +4 -0
- package/src/builtin-addons/date-time/buttons/analog-clock/frontend.tsx +88 -0
- package/src/builtin-addons/date-time/buttons/custom/backend.ts +10 -0
- package/src/builtin-addons/date-time/buttons/custom/config.ts +9 -0
- package/src/builtin-addons/date-time/buttons/custom/frontend.tsx +26 -0
- package/src/builtin-addons/date-time/buttons/date/backend.ts +10 -0
- package/src/builtin-addons/date-time/buttons/date/config.ts +10 -0
- package/src/builtin-addons/date-time/buttons/date/frontend.tsx +42 -0
- package/src/builtin-addons/date-time/buttons/time/backend.ts +10 -0
- package/src/builtin-addons/date-time/buttons/time/config.ts +9 -0
- package/src/builtin-addons/date-time/buttons/time/frontend.tsx +29 -0
- package/src/builtin-addons/date-time/index.d.ts +5 -0
- package/src/builtin-addons/date-time/index.ts +30 -0
- package/src/builtin-addons/date-time/poller.ts +11 -0
- package/src/builtin-addons/date-time/shared/format-date.ts +36 -0
- package/src/builtin-addons/date-time/shared/format.ts +105 -0
- package/src/builtin-addons/date-time/shared/use-now.ts +10 -0
- package/src/builtin-addons/date-time/sirenodeck.json +6 -0
- package/src/builtin-addons/emoji-selector/README.md +45 -0
- package/src/builtin-addons/emoji-selector/assets/launcher.svg +10 -0
- package/src/builtin-addons/emoji-selector/assets/smileys.svg +8 -0
- package/src/builtin-addons/emoji-selector/buttons/category/backend.ts +14 -0
- package/src/builtin-addons/emoji-selector/buttons/category/config.ts +8 -0
- package/src/builtin-addons/emoji-selector/buttons/category/frontend.tsx +19 -0
- package/src/builtin-addons/emoji-selector/buttons/emoji/backend.ts +7 -0
- package/src/builtin-addons/emoji-selector/buttons/emoji/config.ts +9 -0
- package/src/builtin-addons/emoji-selector/buttons/emoji/frontend.tsx +11 -0
- package/src/builtin-addons/emoji-selector/buttons/launcher/backend.ts +13 -0
- package/src/builtin-addons/emoji-selector/buttons/launcher/config.ts +6 -0
- package/src/builtin-addons/emoji-selector/buttons/launcher/frontend.tsx +31 -0
- package/src/builtin-addons/emoji-selector/data/categories.ts +1084 -0
- package/src/builtin-addons/emoji-selector/decks/index.ts +137 -0
- package/src/builtin-addons/emoji-selector/index.d.ts +5 -0
- package/src/builtin-addons/emoji-selector/index.ts +29 -0
- package/src/builtin-addons/emoji-selector/sirenodeck.json +6 -0
- package/src/builtin-addons/emoji-selector/support.ts +49 -0
- package/src/builtin-addons/index.ts +13 -0
- package/src/builtin-addons/internal-settings/README.md +26 -0
- package/src/builtin-addons/internal-settings/backend.ts +58 -0
- package/src/builtin-addons/internal-settings/buttons/app-info/backend.ts +7 -0
- package/src/builtin-addons/internal-settings/buttons/app-info/config.ts +13 -0
- package/src/builtin-addons/internal-settings/buttons/app-info/frontend.tsx +18 -0
- package/src/builtin-addons/internal-settings/buttons/brightness/common/frontend.tsx +50 -0
- package/src/builtin-addons/internal-settings/buttons/brightness-down/backend.ts +16 -0
- package/src/builtin-addons/internal-settings/buttons/brightness-down/config.ts +6 -0
- package/src/builtin-addons/internal-settings/buttons/brightness-down/frontend.tsx +8 -0
- package/src/builtin-addons/internal-settings/buttons/brightness-up/backend.ts +16 -0
- package/src/builtin-addons/internal-settings/buttons/brightness-up/config.ts +6 -0
- package/src/builtin-addons/internal-settings/buttons/brightness-up/frontend.tsx +8 -0
- package/src/builtin-addons/internal-settings/decks/settings.ts +25 -0
- package/src/builtin-addons/internal-settings/index.d.ts +4 -0
- package/src/builtin-addons/internal-settings/index.ts +53 -0
- package/src/builtin-addons/internal-settings/logo72x72.png +0 -0
- package/src/builtin-addons/internal-settings/sirenodeck.json +6 -0
- package/src/builtin-addons/media/README.md +45 -0
- package/src/builtin-addons/media/backend.ts +143 -0
- package/src/builtin-addons/media/buttons/media-player/backend.ts +19 -0
- package/src/builtin-addons/media/buttons/media-player/components/MediaStatus.tsx +25 -0
- package/src/builtin-addons/media/buttons/media-player/components/MediaSurface.tsx +52 -0
- package/src/builtin-addons/media/buttons/media-player/components/ProgressBar.tsx +21 -0
- package/src/builtin-addons/media/buttons/media-player/components/status-meta.ts +36 -0
- package/src/builtin-addons/media/buttons/media-player/config.ts +4 -0
- package/src/builtin-addons/media/buttons/media-player/frontend.tsx +29 -0
- package/src/builtin-addons/media/buttons/volume/common/frontend.tsx +52 -0
- package/src/builtin-addons/media/buttons/volume/mute/backend.ts +13 -0
- package/src/builtin-addons/media/buttons/volume/mute/config.ts +4 -0
- package/src/builtin-addons/media/buttons/volume/mute/frontend.tsx +26 -0
- package/src/builtin-addons/media/buttons/volume/volume-down/backend.ts +14 -0
- package/src/builtin-addons/media/buttons/volume/volume-down/config.ts +6 -0
- package/src/builtin-addons/media/buttons/volume/volume-down/frontend.tsx +10 -0
- package/src/builtin-addons/media/buttons/volume/volume-up/backend.ts +14 -0
- package/src/builtin-addons/media/buttons/volume/volume-up/config.ts +6 -0
- package/src/builtin-addons/media/buttons/volume/volume-up/frontend.tsx +8 -0
- package/src/builtin-addons/media/checks.ts +104 -0
- package/src/builtin-addons/media/executor.ts +36 -0
- package/src/builtin-addons/media/index.d.ts +5 -0
- package/src/builtin-addons/media/index.ts +53 -0
- package/src/builtin-addons/media/progress.ts +16 -0
- package/src/builtin-addons/media/providers/darwin.ts +391 -0
- package/src/builtin-addons/media/providers/index.ts +27 -0
- package/src/builtin-addons/media/providers/linux.ts +196 -0
- package/src/builtin-addons/media/providers/types.ts +48 -0
- package/src/builtin-addons/media/providers/windows.ts +121 -0
- package/src/builtin-addons/media/sirenodeck.json +6 -0
- package/src/builtin-addons/media/state.ts +19 -0
- package/src/builtin-addons/register-builtins.ts +65 -0
- package/src/builtin-addons/session/README.md +14 -0
- package/src/builtin-addons/session/buttons/session-info/backend.ts +8 -0
- package/src/builtin-addons/session/buttons/session-info/config.ts +4 -0
- package/src/builtin-addons/session/buttons/session-info/frontend.tsx +2 -0
- package/src/builtin-addons/session/buttons/time/backend.ts +7 -0
- package/src/builtin-addons/session/buttons/time/config.ts +6 -0
- package/src/builtin-addons/session/buttons/time/frontend.tsx +2 -0
- package/src/builtin-addons/session/index.d.ts +5 -0
- package/src/builtin-addons/session/index.ts +18 -0
- package/src/builtin-addons/session/sirenodeck.json +6 -0
- package/src/builtin-addons/system-status/README.md +169 -0
- package/src/builtin-addons/system-status/buttons/_shared.ts +131 -0
- package/src/builtin-addons/system-status/buttons/system-status/frontend.tsx +224 -0
- package/src/builtin-addons/system-status/buttons/system-status/schemas.ts +67 -0
- package/src/builtin-addons/system-status/domain/catalog.ts +53 -0
- package/src/builtin-addons/system-status/domain/chart-sampler.ts +58 -0
- package/src/builtin-addons/system-status/domain/display-metrics.ts +128 -0
- package/src/builtin-addons/system-status/domain/index.ts +10 -0
- package/src/builtin-addons/system-status/domain/live-metrics.ts +1021 -0
- package/src/builtin-addons/system-status/domain/metric-ids.ts +0 -0
- package/src/builtin-addons/system-status/index.ts +16 -0
- package/src/builtin-addons/system-status/manifest.ts +105 -0
- package/src/builtin-addons/system-status/shared/metrics-catalog.ts +316 -0
- package/src/builtin-addons/system-status/sirenodeck.json +6 -0
- package/src/builtin-addons/test-buildin/decks/action-buttons-test.ts +56 -0
- package/src/builtin-addons/test-buildin/index.d.ts +5 -0
- package/src/builtin-addons/test-buildin/index.ts +16 -0
- package/src/builtin-addons/value-display/README.md +59 -0
- package/src/builtin-addons/value-display/backend.ts +86 -0
- package/src/builtin-addons/value-display/buttons/value-display/backend.ts +17 -0
- package/src/builtin-addons/value-display/buttons/value-display/config.ts +36 -0
- package/src/builtin-addons/value-display/buttons/value-display/frontend.tsx +54 -0
- package/src/builtin-addons/value-display/index.d.ts +5 -0
- package/src/builtin-addons/value-display/index.ts +23 -0
- package/src/builtin-addons/value-display/sirenodeck.json +6 -0
- package/src/builtin-addons/weather/README.md +46 -0
- package/src/builtin-addons/weather/backend.ts +121 -0
- package/src/builtin-addons/weather/buttons/weather/backend.ts +27 -0
- package/src/builtin-addons/weather/buttons/weather/config.ts +29 -0
- package/src/builtin-addons/weather/buttons/weather/frontend.tsx +63 -0
- package/src/builtin-addons/weather/buttons/weather/icons.tsx +88 -0
- package/src/builtin-addons/weather/buttons/weather/pages.tsx +109 -0
- package/src/builtin-addons/weather/domain/codes.ts +35 -0
- package/src/builtin-addons/weather/domain/fetch.ts +47 -0
- package/src/builtin-addons/weather/index.d.ts +5 -0
- package/src/builtin-addons/weather/index.ts +23 -0
- package/src/builtin-addons/weather/poller.ts +70 -0
- package/src/builtin-addons/weather/provider/city-key.ts +9 -0
- package/src/builtin-addons/weather/provider/codes.ts +35 -0
- package/src/builtin-addons/weather/provider/fetch.ts +97 -0
- package/src/builtin-addons/weather/provider/geocode.ts +67 -0
- package/src/builtin-addons/weather/provider/types.ts +42 -0
- package/src/builtin-addons/weather/sirenodeck.json +6 -0
- package/src/cli/coding-agents-onboarding.ts +434 -0
- package/src/cli/commands/addon-decks.ts +441 -0
- package/src/cli/commands/addon-registry.ts +639 -0
- package/src/cli/commands/emulator-mode.ts +289 -0
- package/src/cli/commands/install.ts +128 -0
- package/src/cli/commands/logs.ts +74 -0
- package/src/cli/commands/network-bind.ts +111 -0
- package/src/cli/commands/pipeline/helpers.ts +75 -0
- package/src/cli/commands/pipeline/preflight.ts +20 -0
- package/src/cli/commands/port-identity.ts +152 -0
- package/src/cli/commands/reload.ts +37 -0
- package/src/cli/commands/restart.ts +139 -0
- package/src/cli/commands/run.ts +2270 -0
- package/src/cli/commands/service/install.ts +121 -0
- package/src/cli/commands/service/render-template.ts +158 -0
- package/src/cli/commands/service-manager.ts +184 -0
- package/src/cli/commands/spawn-daemon.ts +251 -0
- package/src/cli/commands/start.ts +1061 -0
- package/src/cli/commands/status.ts +110 -0
- package/src/cli/commands/stop.ts +121 -0
- package/src/cli/commands/subprocess-supervisor.ts +203 -0
- package/src/cli/commands/system-requirements.ts +699 -0
- package/src/cli/cwd.ts +28 -0
- package/src/cli/first-run.ts +149 -0
- package/src/cli/http-server.ts +244 -0
- package/src/cli/index.ts +355 -0
- package/src/cli/install-root.ts +26 -0
- package/src/cli/main.ts +194 -0
- package/src/cli/package-manager.ts +175 -0
- package/src/cli/probe-deps.ts +45 -0
- package/src/cli/prompt.ts +22 -0
- package/src/cli/startup-display.ts +452 -0
- package/src/config/builtin-icons.ts +57 -0
- package/src/config/config-diff.ts +54 -0
- package/src/config/discovery.ts +64 -0
- package/src/config/icon-resolver.ts +90 -0
- package/src/config/include-resolver.ts +202 -0
- package/src/config/index.ts +9 -0
- package/src/config/loader.ts +187 -0
- package/src/config/mutation.ts +514 -0
- package/src/config/reference-expander.ts +84 -0
- package/src/config/schemas.ts +223 -0
- package/src/config/validation.ts +254 -0
- package/src/core/asset-id.ts +21 -0
- package/src/core/gesture-state.ts +344 -0
- package/src/core/icon-asset-registry.ts +127 -0
- package/src/core/icon-source.ts +86 -0
- package/src/core/index.ts +30 -0
- package/src/core/mime.ts +16 -0
- package/src/core/pagination.ts +90 -0
- package/src/core/pub-sub.ts +126 -0
- package/src/core/settings.ts +153 -0
- package/src/core/store.ts +88 -0
- package/src/core/watcher.ts +73 -0
- package/src/deck/addon-handler-bridge.ts +516 -0
- package/src/deck/deck-config.ts +271 -0
- package/src/deck/editor-surfaces.ts +87 -0
- package/src/deck/host-context.ts +18 -0
- package/src/deck/index.ts +102 -0
- package/src/deck/macro-parse.ts +182 -0
- package/src/deck/methods.ts +348 -0
- package/src/deck/paginate-deck.ts +98 -0
- package/src/deck/position-buttons.ts +57 -0
- package/src/deck/runtime/index.ts +5 -0
- package/src/deck/runtime/runtime.ts +1060 -0
- package/src/deck/runtime-subscriptions.ts +45 -0
- package/src/deck/system-back-injection.ts +62 -0
- package/src/deck/system-buttons/registry.tsx +97 -0
- package/src/deck/system-buttons/types.ts +14 -0
- package/src/device/black-frame.ts +73 -0
- package/src/device/index.ts +13 -0
- package/src/device/linux-udev.ts +36 -0
- package/src/device/models.ts +89 -0
- package/src/device/reconnecting.ts +193 -0
- package/src/device/registry.ts +38 -0
- package/src/device/stream-deck.ts +155 -0
- package/src/index.ts +74 -0
- package/src/outputClient/emulator.ts +357 -0
- package/src/outputClient/index.ts +26 -0
- package/src/outputClient/open-browser.ts +26 -0
- package/src/outputClient/real.ts +393 -0
- package/src/outputClient/types.ts +83 -0
- package/src/render/browser-renderer.ts +240 -0
- package/src/render/buffer-hash.ts +20 -0
- package/src/render/editor-handler.ts +197 -0
- package/src/render/emulator-server.ts +128 -0
- package/src/render/icon-source-resolver.ts +69 -0
- package/src/render/index.ts +16 -0
- package/src/render/protocol.ts +61 -0
- package/src/render/push-raw-image.ts +100 -0
- package/src/render/screenshot-cadence.ts +87 -0
- package/src/render/state-publisher.ts +110 -0
- package/src/render/vite-server.ts +138 -0
- package/src/render/ws-bridge.ts +315 -0
- package/src/system/device-selection.ts +80 -0
- package/src/system/glob-match.ts +74 -0
- package/src/system/providers/active-app/darwin.ts +117 -0
- package/src/system/providers/active-app/linux.ts +245 -0
- package/src/system/providers/active-app/wayland-gnome.ts +303 -0
- package/src/system/providers/active-app/windows.ts +94 -0
- package/src/system/providers/active-app.ts +97 -0
- package/src/system/providers/clipboard/darwin.ts +65 -0
- package/src/system/providers/clipboard/linux.ts +163 -0
- package/src/system/providers/clipboard/windows.ts +94 -0
- package/src/system/providers/clipboard.ts +84 -0
- package/src/system/providers/error.ts +16 -0
- package/src/system/providers/key-macro/darwin.ts +221 -0
- package/src/system/providers/key-macro/linux.ts +651 -0
- package/src/system/providers/key-macro/parser.ts +215 -0
- package/src/system/providers/key-macro/windows.ts +476 -0
- package/src/system/providers/key-macro.ts +79 -0
- package/src/system/providers/notification/darwin.ts +36 -0
- package/src/system/providers/notification/linux.ts +115 -0
- package/src/system/providers/notification/windows.ts +72 -0
- package/src/system/providers/notification.ts +75 -0
- package/src/system/providers/session/darwin.ts +82 -0
- package/src/system/providers/session/linux.ts +175 -0
- package/src/system/providers/session/windows.ts +73 -0
- package/src/system/providers/session.ts +89 -0
- package/src/system/providers/shared.ts +65 -0
- package/src/system/requirements.ts +235 -0
- package/src/system/setup-wizard/config-seed.ts +30 -0
- package/src/system/setup-wizard/format.ts +275 -0
- package/src/system/setup-wizard/index.ts +45 -0
- package/src/system/setup-wizard/open-settings.ts +26 -0
- package/src/system/setup-wizard/plan.ts +230 -0
- package/src/system/setup-wizard/probe-cache.ts +34 -0
- package/src/system/setup-wizard/probe.ts +486 -0
- package/src/system/setup-wizard/runtime-features.ts +85 -0
- package/src/system/setup-wizard/sudo.ts +178 -0
- package/src/system/setup-wizard/types.ts +133 -0
- package/src/system/virtual-stream-deck.ts +67 -0
- package/src/themes/css.ts +210 -0
- package/src/themes/default/assets/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/src/themes/default/assets/IBMPlexSans-VariableFont_wdth,wght.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Bold.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-BoldItalic.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-ExtraLight.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-ExtraLightItalic.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Italic.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Light.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-LightItalic.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Medium.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-MediumItalic.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Regular.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-SemiBold.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-SemiBoldItalic.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Thin.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-ThinItalic.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/OFL.txt +93 -0
- package/src/themes/default/assets/OFL.txt +93 -0
- package/src/themes/default/assets/README.txt +106 -0
- package/src/themes/default/assets/static/IBMPlexSans-Bold.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-BoldItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-ExtraLight.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-ExtraLightItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-Italic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-Light.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-LightItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-Medium.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-MediumItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-Regular.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-SemiBold.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-SemiBoldItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-Thin.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-ThinItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-Bold.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-BoldItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-ExtraLight.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-ExtraLightItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-Italic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-Light.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-LightItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-Medium.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-MediumItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-Regular.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-SemiBold.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-SemiBoldItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-Thin.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-ThinItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Bold.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-BoldItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-ExtraLight.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-ExtraLightItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Italic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Light.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-LightItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Medium.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-MediumItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Regular.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-SemiBold.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-SemiBoldItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Thin.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-ThinItalic.ttf +0 -0
- package/src/themes/default/components.css +81 -0
- package/src/themes/default/sirenodeck.json +170 -0
- package/src/themes/index.ts +14 -0
- package/src/themes/light/assets/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/src/themes/light/assets/IBMPlexSans-VariableFont_wdth,wght.ttf +0 -0
- package/src/themes/light/assets/IBM_Plex_Mono/IBMPlexMono-Bold.ttf +0 -0
- package/src/themes/light/assets/IBM_Plex_Mono/IBMPlexMono-Medium.ttf +0 -0
- package/src/themes/light/assets/IBM_Plex_Mono/IBMPlexMono-Regular.ttf +0 -0
- package/src/themes/light/assets/IBM_Plex_Mono/OFL.txt +93 -0
- package/src/themes/light/assets/OFL.txt +93 -0
- package/src/themes/light/assets/README.txt +106 -0
- package/src/themes/light/assets/static/IBMPlexSans-Light.ttf +0 -0
- package/src/themes/light/assets/static/IBMPlexSans-LightItalic.ttf +0 -0
- package/src/themes/light/assets/static/IBMPlexSans-Thin.ttf +0 -0
- package/src/themes/light/components.css +70 -0
- package/src/themes/light/sirenodeck.json +170 -0
- package/src/themes/loader.ts +343 -0
- package/src/themes/manifest.ts +164 -0
- package/src/themes/use-resolved-theme.tsx +32 -0
- package/src/ui/ButtonFrame.tsx +230 -0
- package/src/ui/contexts/AssetCacheContext.tsx +43 -0
- package/src/ui/index.ts +20 -0
- package/src/ui/primitives/Chip.tsx +53 -0
- package/src/ui/primitives/Icon.tsx +209 -0
- package/src/ui/primitives/Label.tsx +82 -0
- package/src/ui/primitives/ProgressBar.tsx +38 -0
- package/src/ui/primitives/TapIndicator.tsx +67 -0
- package/src/ui/primitives/Text.tsx +810 -0
- package/src/ui/primitives/index.ts +6 -0
- package/src/ui/surfaces/BarsSurface.tsx +211 -0
- package/src/ui/surfaces/IconLabelProgressSurface.tsx +133 -0
- package/src/ui/surfaces/IconLabelSurface.tsx +59 -0
- package/src/ui/surfaces/LabelValueListSurface.tsx +202 -0
- package/src/ui/surfaces/PaginatedSurface.tsx +164 -0
- package/src/ui/surfaces/SplitActionSurface.tsx +98 -0
- package/src/ui/surfaces/TemporaryErrorSurface.tsx +68 -0
- package/src/ui/surfaces/ValueChart.tsx +218 -0
- package/src/ui/surfaces/index.ts +32 -0
- package/src/ui/surfaces/paginated-schema.ts +22 -0
- package/src/ui/theme-presentation-builder.ts +184 -0
- package/src/ui/theme-presentation.tsx +146 -0
- package/src/ui/utils/cn.ts +12 -0
- package/src/util/cache-paths.ts +38 -0
- package/src/util/config-diff.ts +16 -0
- package/src/util/daemon-control.ts +85 -0
- package/src/util/daemon.ts +575 -0
- package/src/util/device-config.ts +56 -0
- package/src/util/errors.ts +13 -0
- package/src/util/log-reader.ts +114 -0
- package/src/util/log-tail.ts +103 -0
- package/src/util/logger.ts +417 -0
- package/src/util/process-title.ts +79 -0
- package/src/util/single-instance.ts +202 -0
- package/src/version.ts +10 -0
- package/src/vite/index.ts +6 -0
- package/src/vite/virtual-modules.ts +631 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
import { execFile as execFileCb } from "node:child_process"
|
|
2
|
+
import { existsSync } from "node:fs"
|
|
3
|
+
import { readFile, readdir, statfs } from "node:fs/promises"
|
|
4
|
+
import { cpus, freemem, loadavg, totalmem, uptime as osUptime } from "node:os"
|
|
5
|
+
import { promisify } from "node:util"
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
SYSTEM_METRIC_IDS,
|
|
9
|
+
type SystemMetricId,
|
|
10
|
+
type SystemMetricSnapshot,
|
|
11
|
+
} from "../shared/metrics-catalog"
|
|
12
|
+
|
|
13
|
+
// ponytail: vite-plugin-oxc (vitest transform) doesn't recognise
|
|
14
|
+
// `node:child_process/promises` as a built-in module specifier. The
|
|
15
|
+
// promisify path works on every Node ≥14 with zero extra deps.
|
|
16
|
+
const execFile = promisify(execFileCb)
|
|
17
|
+
|
|
18
|
+
export { SYSTEM_METRIC_IDS }
|
|
19
|
+
|
|
20
|
+
interface ProbeResult {
|
|
21
|
+
available: boolean
|
|
22
|
+
max?: number
|
|
23
|
+
percentage?: number
|
|
24
|
+
unit?: string
|
|
25
|
+
value?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function clampPercent(value: number): number {
|
|
29
|
+
if (!Number.isFinite(value)) return 0
|
|
30
|
+
return Math.min(100, Math.max(0, Math.round(value)))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ponytail: `c.times` from `node:os` are cumulative jiffies since boot — not
|
|
34
|
+
// instantaneous. Two back-to-back readings give nearly identical values
|
|
35
|
+
// because the ratio barely moves; the metric would look frozen. We cache
|
|
36
|
+
// the previous (idle, total) sample and report `(Δtotal - Δidle) / Δtotal`
|
|
37
|
+
// as instant CPU usage over the inter-poll window (~1s). First sample has
|
|
38
|
+
// no baseline → returns 0 to avoid a misleading one-shot.
|
|
39
|
+
let prevCpuSample: { idle: number; total: number } | null = null
|
|
40
|
+
|
|
41
|
+
async function probeCpu(): Promise<ProbeResult> {
|
|
42
|
+
const list = cpus()
|
|
43
|
+
if (list.length === 0) return { available: false }
|
|
44
|
+
let idle = 0
|
|
45
|
+
let total = 0
|
|
46
|
+
for (const c of list) {
|
|
47
|
+
idle += c.times.idle
|
|
48
|
+
total +=
|
|
49
|
+
c.times.user + c.times.nice + c.times.sys + c.times.idle + c.times.irq
|
|
50
|
+
}
|
|
51
|
+
const prev = prevCpuSample
|
|
52
|
+
// Counter reset (boot, container restart) or first sample → re-baseline.
|
|
53
|
+
if (prev === null || total <= prev.total) {
|
|
54
|
+
prevCpuSample = { idle, total }
|
|
55
|
+
return { available: true, max: 100, percentage: 0, unit: "%", value: 0 }
|
|
56
|
+
}
|
|
57
|
+
const dTotal = total - prev.total
|
|
58
|
+
const dIdle = idle - prev.idle
|
|
59
|
+
prevCpuSample = { idle, total }
|
|
60
|
+
const pct = dTotal > 0 ? clampPercent((1 - dIdle / dTotal) * 100) : 0
|
|
61
|
+
return { available: true, max: 100, percentage: pct, unit: "%", value: pct }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function probeRam(): Promise<ProbeResult> {
|
|
65
|
+
const total = totalmem()
|
|
66
|
+
const used = total - freemem()
|
|
67
|
+
if (total <= 0) return { available: false, unit: "%" }
|
|
68
|
+
const pct = clampPercent((used / total) * 100)
|
|
69
|
+
return { available: true, max: total, percentage: pct, unit: "%", value: pct }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ponytail: macOS has no /proc, so the Linux sysfs readers below all bailed
|
|
73
|
+
// and every metric except cpu/ram/disk/uptime/load/frequency reported
|
|
74
|
+
// unavailable — which is most of the default deck's first button. These
|
|
75
|
+
// helpers fill in the ones macOS does expose through documented CLIs. Each
|
|
76
|
+
// returns null on any parse failure so the caller can report unavailable
|
|
77
|
+
// rather than a wrong number.
|
|
78
|
+
const DARWIN_CMD_TIMEOUT_MS = 2_000
|
|
79
|
+
|
|
80
|
+
const darwinRun = async (
|
|
81
|
+
binary: string,
|
|
82
|
+
args: ReadonlyArray<string>,
|
|
83
|
+
): Promise<string | null> => {
|
|
84
|
+
try {
|
|
85
|
+
const { stdout } = await execFile(binary, [...args], {
|
|
86
|
+
timeout: DARWIN_CMD_TIMEOUT_MS,
|
|
87
|
+
signal: AbortSignal.timeout(DARWIN_CMD_TIMEOUT_MS),
|
|
88
|
+
})
|
|
89
|
+
return stdout
|
|
90
|
+
} catch {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// `sysctl -n vm.swapusage` →
|
|
96
|
+
// "total = 15360.00M used = 14146.44M free = 1213.56M (encrypted)"
|
|
97
|
+
const SWAP_FIELD = (name: string): RegExp =>
|
|
98
|
+
new RegExp(`${name}\\s*=\\s*([\\d.]+)([KMG])`, "i")
|
|
99
|
+
|
|
100
|
+
const SWAP_UNIT_BYTES: Readonly<Record<string, number>> = {
|
|
101
|
+
K: 1024,
|
|
102
|
+
M: 1024 ** 2,
|
|
103
|
+
G: 1024 ** 3,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const parseDarwinSwap = (
|
|
107
|
+
raw: string,
|
|
108
|
+
): { totalBytes: number; usedBytes: number } | null => {
|
|
109
|
+
const total = SWAP_FIELD("total").exec(raw)
|
|
110
|
+
const used = SWAP_FIELD("used").exec(raw)
|
|
111
|
+
if (total?.[1] === undefined || used?.[1] === undefined) return null
|
|
112
|
+
const toBytes = (m: RegExpExecArray): number =>
|
|
113
|
+
Number.parseFloat(m[1]!) *
|
|
114
|
+
(SWAP_UNIT_BYTES[(m[2] ?? "M").toUpperCase()] ?? 0)
|
|
115
|
+
const totalBytes = toBytes(total)
|
|
116
|
+
const usedBytes = toBytes(used)
|
|
117
|
+
if (!Number.isFinite(totalBytes) || totalBytes <= 0) return null
|
|
118
|
+
return { totalBytes, usedBytes }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// `pmset -g batt` →
|
|
122
|
+
// " -InternalBattery-0 (id=...)\t100%; charged; 0:00 remaining present: true"
|
|
123
|
+
export const parseDarwinBattery = (raw: string): number | null => {
|
|
124
|
+
const m = /(\d{1,3})%/.exec(raw)
|
|
125
|
+
if (m?.[1] === undefined) return null
|
|
126
|
+
const pct = Number.parseInt(m[1], 10)
|
|
127
|
+
return Number.isFinite(pct) ? pct : null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// `netstat -ibn` — one row per interface/address. The <Link#N> rows carry the
|
|
131
|
+
// per-interface totals; the address rows repeat the same counters, so summing
|
|
132
|
+
// every row would double-count. Skip loopback and virtual interfaces to match
|
|
133
|
+
// the Linux reader's NETWORK_IFACE_SKIP_RE.
|
|
134
|
+
export const parseDarwinNetstat = (
|
|
135
|
+
raw: string,
|
|
136
|
+
): { rx: number; tx: number } | null => {
|
|
137
|
+
const lines = raw.trim().split("\n")
|
|
138
|
+
const header = lines[0]?.trim().split(/\s+/) ?? []
|
|
139
|
+
const ibytes = header.indexOf("Ibytes")
|
|
140
|
+
const obytes = header.indexOf("Obytes")
|
|
141
|
+
if (ibytes === -1 || obytes === -1) return null
|
|
142
|
+
let rx = 0
|
|
143
|
+
let tx = 0
|
|
144
|
+
let counted = false
|
|
145
|
+
for (const line of lines.slice(1)) {
|
|
146
|
+
const cols = line.trim().split(/\s+/)
|
|
147
|
+
const name = cols[0]
|
|
148
|
+
if (name === undefined) continue
|
|
149
|
+
if (NETWORK_IFACE_SKIP_RE.test(name)) continue
|
|
150
|
+
// Only the <Link#N> rows, so per-address duplicates aren't summed twice.
|
|
151
|
+
if (!(cols[2] ?? "").startsWith("<Link")) continue
|
|
152
|
+
const i = Number.parseInt(cols[ibytes] ?? "", 10)
|
|
153
|
+
const o = Number.parseInt(cols[obytes] ?? "", 10)
|
|
154
|
+
if (!Number.isFinite(i) || !Number.isFinite(o)) continue
|
|
155
|
+
rx += i
|
|
156
|
+
tx += o
|
|
157
|
+
counted = true
|
|
158
|
+
}
|
|
159
|
+
return counted ? { rx, tx } : null
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ponytail: Linux reads /proc/meminfo SwapTotal/SwapFree in kB; macOS uses
|
|
163
|
+
// sysctl vm.swapusage. Other platforms report unavailable rather than guess.
|
|
164
|
+
async function probeSwap(): Promise<ProbeResult> {
|
|
165
|
+
if (process.platform === "darwin") {
|
|
166
|
+
const raw = await darwinRun("sysctl", ["-n", "vm.swapusage"])
|
|
167
|
+
const parsed = raw === null ? null : parseDarwinSwap(raw)
|
|
168
|
+
if (parsed === null) return { available: false, unit: "%" }
|
|
169
|
+
const pct = clampPercent((parsed.usedBytes / parsed.totalBytes) * 100)
|
|
170
|
+
return {
|
|
171
|
+
available: true,
|
|
172
|
+
max: parsed.totalBytes,
|
|
173
|
+
percentage: pct,
|
|
174
|
+
unit: "%",
|
|
175
|
+
value: pct,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (process.platform !== "linux") return { available: false, unit: "%" }
|
|
179
|
+
if (!existsSync("/proc/meminfo")) return { available: false, unit: "%" }
|
|
180
|
+
try {
|
|
181
|
+
const raw = await readFile("/proc/meminfo", "utf8")
|
|
182
|
+
const totalKb = Number.parseInt(
|
|
183
|
+
/SwapTotal:\s*(\d+)\s*kB/.exec(raw)?.[1] ?? "",
|
|
184
|
+
10,
|
|
185
|
+
)
|
|
186
|
+
const freeKb = Number.parseInt(
|
|
187
|
+
/SwapFree:\s*(\d+)\s*kB/.exec(raw)?.[1] ?? "",
|
|
188
|
+
10,
|
|
189
|
+
)
|
|
190
|
+
if (!Number.isFinite(totalKb) || totalKb <= 0) {
|
|
191
|
+
return { available: false, unit: "%" }
|
|
192
|
+
}
|
|
193
|
+
const totalBytes = totalKb * 1024
|
|
194
|
+
const usedBytes = Math.max(0, (totalKb - freeKb) * 1024)
|
|
195
|
+
const pct = clampPercent((usedBytes / totalBytes) * 100)
|
|
196
|
+
return {
|
|
197
|
+
available: true,
|
|
198
|
+
max: totalBytes,
|
|
199
|
+
percentage: pct,
|
|
200
|
+
unit: "%",
|
|
201
|
+
value: pct,
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
return { available: false, unit: "%" }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function probeDisk(): Promise<ProbeResult> {
|
|
209
|
+
try {
|
|
210
|
+
const stats = await statfs("/")
|
|
211
|
+
const total = Number(stats.blocks) * Number(stats.bsize)
|
|
212
|
+
const used =
|
|
213
|
+
(Number(stats.blocks) - Number(stats.bfree)) * Number(stats.bsize)
|
|
214
|
+
if (total <= 0) return { available: false, unit: "%" }
|
|
215
|
+
const pct = clampPercent((used / total) * 100)
|
|
216
|
+
return {
|
|
217
|
+
available: true,
|
|
218
|
+
max: total,
|
|
219
|
+
percentage: pct,
|
|
220
|
+
unit: "%",
|
|
221
|
+
value: pct,
|
|
222
|
+
}
|
|
223
|
+
} catch {
|
|
224
|
+
return { available: false, unit: "%" }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function probeNetwork(): Promise<ProbeResult> {
|
|
229
|
+
// ponytail: cheap & portable. Real throughput needs /proc/net/dev per-interface counters.
|
|
230
|
+
const count = cpus().length > 0 ? 1 : 0
|
|
231
|
+
return { available: count > 0, unit: "inter.", value: count }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function probeBattery(): Promise<ProbeResult> {
|
|
235
|
+
if (process.platform === "darwin") {
|
|
236
|
+
const raw = await darwinRun("pmset", ["-g", "batt"])
|
|
237
|
+
const pct = raw === null ? null : parseDarwinBattery(raw)
|
|
238
|
+
if (pct === null) return { available: false, unit: "%" }
|
|
239
|
+
const clamped = clampPercent(pct)
|
|
240
|
+
return {
|
|
241
|
+
available: true,
|
|
242
|
+
max: 100,
|
|
243
|
+
percentage: clamped,
|
|
244
|
+
unit: "%",
|
|
245
|
+
value: clamped,
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Linux: /sys/class/power_supply/BAT0/capacity
|
|
249
|
+
if (process.platform !== "linux") return { available: false, unit: "%" }
|
|
250
|
+
if (!existsSync("/sys/class/power_supply/BAT0/capacity")) {
|
|
251
|
+
return { available: false, unit: "%" }
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
const raw = await readFile("/sys/class/power_supply/BAT0/capacity", "utf8")
|
|
255
|
+
const pct = clampPercent(Number.parseInt(raw.trim(), 10))
|
|
256
|
+
return { available: true, max: 100, percentage: pct, unit: "%", value: pct }
|
|
257
|
+
} catch {
|
|
258
|
+
return { available: false, unit: "%" }
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ponytail: scan /sys/class/hwmon for CPU-package sensors and read
|
|
263
|
+
// temp1_input. Skip acpitz — the thermal_zone0 fallback below covers it,
|
|
264
|
+
// and acpitz often sorts first (hwmon0), shadowing k10temp/coretemp.
|
|
265
|
+
// ponytail: CPU/GPU temperature and fan RPM have no unprivileged source on
|
|
266
|
+
// Apple Silicon from the tools already in use here — `powermetrics` demands
|
|
267
|
+
// root, and the only readable "Temperature" key in ioreg belongs to
|
|
268
|
+
// AppleSmartBattery, which is the battery's, not the SoC's. `macmon` reads
|
|
269
|
+
// them through IOReport without sudo ("Sudoless performance monitoring for
|
|
270
|
+
// Apple Silicon", brew install macmon). It is optional: absent, these metrics
|
|
271
|
+
// keep reporting unavailable exactly as before.
|
|
272
|
+
//
|
|
273
|
+
// Same shape as the media addon preferring `media-control` and brightness
|
|
274
|
+
// preferring `brightness` — an optional helper that unlocks a platform, never
|
|
275
|
+
// a hard dependency.
|
|
276
|
+
interface MacmonSample {
|
|
277
|
+
readonly cpuTemp: number | null
|
|
278
|
+
readonly gpuTemp: number | null
|
|
279
|
+
readonly fanRpm: number | null
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const MACMON_SHARE_MS = 2_000
|
|
283
|
+
let sharedMacmon: { at: number; value: MacmonSample | null } | null = null
|
|
284
|
+
let inFlightMacmon: Promise<MacmonSample | null> | null = null
|
|
285
|
+
|
|
286
|
+
const finite = (v: unknown): number | null =>
|
|
287
|
+
typeof v === "number" && Number.isFinite(v) ? v : null
|
|
288
|
+
|
|
289
|
+
export const parseMacmonSample = (raw: string): MacmonSample | null => {
|
|
290
|
+
// `macmon pipe` emits one JSON object per sample, newline delimited.
|
|
291
|
+
const line = raw.split("\n").find((l) => l.trim().startsWith("{"))
|
|
292
|
+
if (line === undefined) return null
|
|
293
|
+
let data: Record<string, unknown>
|
|
294
|
+
try {
|
|
295
|
+
data = JSON.parse(line) as Record<string, unknown>
|
|
296
|
+
} catch {
|
|
297
|
+
return null
|
|
298
|
+
}
|
|
299
|
+
const temp = (data["temp"] ?? {}) as Record<string, unknown>
|
|
300
|
+
const fans = Array.isArray(data["fans"]) ? data["fans"] : []
|
|
301
|
+
// A machine with several fans reports the fastest — the one that is doing
|
|
302
|
+
// the work. A passively cooled Mac reports an empty array, not zero.
|
|
303
|
+
let fanRpm: number | null = null
|
|
304
|
+
for (const fan of fans) {
|
|
305
|
+
const rpm = finite((fan as Record<string, unknown>)["rpm"])
|
|
306
|
+
if (rpm !== null && rpm > 0) fanRpm = Math.max(fanRpm ?? 0, rpm)
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
cpuTemp: finite(temp["cpu_temp_avg"]),
|
|
310
|
+
gpuTemp: finite(temp["gpu_temp_avg"]),
|
|
311
|
+
fanRpm,
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function readMacmon(): Promise<MacmonSample | null> {
|
|
316
|
+
const now = Date.now()
|
|
317
|
+
if (sharedMacmon !== null && now - sharedMacmon.at < MACMON_SHARE_MS) {
|
|
318
|
+
return sharedMacmon.value
|
|
319
|
+
}
|
|
320
|
+
// Three metrics read one sample; collapse concurrent probes onto one spawn.
|
|
321
|
+
if (inFlightMacmon !== null) return inFlightMacmon
|
|
322
|
+
inFlightMacmon = (async () => {
|
|
323
|
+
// --samples 1 makes it exit on its own; the interval is the settle time
|
|
324
|
+
// before the first reading, well inside darwinRun's 2s ceiling.
|
|
325
|
+
const raw = await darwinRun("macmon", [
|
|
326
|
+
"pipe",
|
|
327
|
+
"--samples",
|
|
328
|
+
"1",
|
|
329
|
+
"--interval",
|
|
330
|
+
"200",
|
|
331
|
+
])
|
|
332
|
+
const value = raw === null ? null : parseMacmonSample(raw)
|
|
333
|
+
sharedMacmon = { at: Date.now(), value }
|
|
334
|
+
return value
|
|
335
|
+
})().finally(() => {
|
|
336
|
+
inFlightMacmon = null
|
|
337
|
+
})
|
|
338
|
+
return inFlightMacmon
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function probeTemperature(): Promise<ProbeResult> {
|
|
342
|
+
if (process.platform === "darwin") {
|
|
343
|
+
const sample = await readMacmon()
|
|
344
|
+
const value = sample?.cpuTemp ?? null
|
|
345
|
+
return value === null
|
|
346
|
+
? { available: false, unit: "°C" }
|
|
347
|
+
: { available: true, value, unit: "°C" }
|
|
348
|
+
}
|
|
349
|
+
if (process.platform !== "linux") return { available: false, unit: "°C" }
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
if (existsSync("/sys/class/hwmon")) {
|
|
353
|
+
const entries = await readdir("/sys/class/hwmon")
|
|
354
|
+
const hwmons: Array<{ dir: string; name: string }> = []
|
|
355
|
+
for (const e of entries) {
|
|
356
|
+
const namePath = `/sys/class/hwmon/${e}/name`
|
|
357
|
+
if (!existsSync(namePath)) continue
|
|
358
|
+
const name = (await readFile(namePath, "utf8")).trim()
|
|
359
|
+
hwmons.push({ dir: `/sys/class/hwmon/${e}`, name })
|
|
360
|
+
}
|
|
361
|
+
const matched = hwmons
|
|
362
|
+
.map((h, i) => ({ h, i }))
|
|
363
|
+
.filter(({ h }) =>
|
|
364
|
+
CPU_HWMON_NAME_PATTERNS.some((re) => re.test(h.name)),
|
|
365
|
+
)
|
|
366
|
+
.filter(({ h }) => !/^acpitz$/i.test(h.name))
|
|
367
|
+
.sort((a, b) => a.i - b.i)[0]
|
|
368
|
+
if (matched) {
|
|
369
|
+
const temp = `${matched.h.dir}/temp1_input`
|
|
370
|
+
if (existsSync(temp)) {
|
|
371
|
+
const raw = await readFile(temp, "utf8")
|
|
372
|
+
const milli = Number.parseInt(raw.trim(), 10)
|
|
373
|
+
if (Number.isFinite(milli) && milli > 0) {
|
|
374
|
+
return {
|
|
375
|
+
available: true,
|
|
376
|
+
unit: "°C",
|
|
377
|
+
value: Math.round(milli / 1000),
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} catch {
|
|
384
|
+
// fall through to thermal_zone0
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Fallback: /sys/class/thermal/thermal_zone0/temp (millidegrees C)
|
|
388
|
+
if (!existsSync("/sys/class/thermal/thermal_zone0/temp")) {
|
|
389
|
+
return { available: false, unit: "°C" }
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
const raw = await readFile("/sys/class/thermal/thermal_zone0/temp", "utf8")
|
|
393
|
+
const milli = Number.parseInt(raw.trim(), 10)
|
|
394
|
+
if (!Number.isFinite(milli)) return { available: false, unit: "°C" }
|
|
395
|
+
return { available: true, unit: "°C", value: Math.round(milli / 1000) }
|
|
396
|
+
} catch {
|
|
397
|
+
return { available: false, unit: "°C" }
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function probeUptime(): Promise<ProbeResult> {
|
|
402
|
+
const sec = Math.round(osUptime())
|
|
403
|
+
return { available: true, value: sec }
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function probeFrequency(): Promise<ProbeResult> {
|
|
407
|
+
// ponytail: os.cpus()[i].speed is unreliable on Linux (often 0). Fall back to /proc/cpuinfo MHz.
|
|
408
|
+
const list = cpus()
|
|
409
|
+
const fromOs = list.find(
|
|
410
|
+
(c) => typeof c.speed === "number" && c.speed > 0,
|
|
411
|
+
)?.speed
|
|
412
|
+
if (typeof fromOs === "number" && fromOs > 0) {
|
|
413
|
+
const ghz = Number((fromOs / 1000).toFixed(2))
|
|
414
|
+
return { available: true, unit: "GHz", value: ghz }
|
|
415
|
+
}
|
|
416
|
+
if (process.platform === "linux" && existsSync("/proc/cpuinfo")) {
|
|
417
|
+
try {
|
|
418
|
+
const raw = await readFile("/proc/cpuinfo", "utf8")
|
|
419
|
+
const m = raw.match(/cpu MHz\s*:\s*([\d.]+)/)
|
|
420
|
+
if (m && m[1]) {
|
|
421
|
+
const mhz = Number.parseFloat(m[1])
|
|
422
|
+
if (Number.isFinite(mhz) && mhz > 0) {
|
|
423
|
+
const ghz = Number((mhz / 1000).toFixed(2))
|
|
424
|
+
return { available: true, unit: "GHz", value: ghz }
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
} catch {
|
|
428
|
+
// fall through
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return { available: false, unit: "GHz" }
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function probeLoad(): Promise<ProbeResult> {
|
|
435
|
+
const [one] = loadavg()
|
|
436
|
+
if (one === undefined || !Number.isFinite(one)) {
|
|
437
|
+
return { available: false }
|
|
438
|
+
}
|
|
439
|
+
return { available: true, value: Number(one.toFixed(2)) }
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function probeProcesses(): Promise<ProbeResult> {
|
|
443
|
+
if (process.platform === "linux" && existsSync("/proc")) {
|
|
444
|
+
try {
|
|
445
|
+
const entries = await readdir("/proc")
|
|
446
|
+
const count = entries.filter((e) => /^\d+$/.test(e)).length
|
|
447
|
+
return { available: count > 0, unit: "procs", value: count }
|
|
448
|
+
} catch {
|
|
449
|
+
return { available: false, unit: "procs" }
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (process.platform === "darwin") {
|
|
453
|
+
const raw = await darwinRun("ps", ["-Ao", "pid="])
|
|
454
|
+
if (raw === null) return { available: false, unit: "procs" }
|
|
455
|
+
const count = raw.split("\n").filter((l) => l.trim().length > 0).length
|
|
456
|
+
return { available: count > 0, unit: "procs", value: count }
|
|
457
|
+
}
|
|
458
|
+
// ponytail: no portable process count without a dep; report unavailable elsewhere.
|
|
459
|
+
return { available: false, unit: "procs" }
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ponytail: cpu-boost has two kernel interfaces. cpufreq/boost is the modern
|
|
463
|
+
// unified path (1 = enabled, 0 = disabled). Older Intel systems use
|
|
464
|
+
// intel_pstate/no_turbo with inverted semantics (0 = boost on). Either
|
|
465
|
+
// wins; otherwise the platform just doesn't expose boost and we return
|
|
466
|
+
// unavailable rather than guess.
|
|
467
|
+
async function probeCpuBoost(): Promise<ProbeResult> {
|
|
468
|
+
// ponytail: deliberately not implemented for darwin. Apple Silicon has no
|
|
469
|
+
// turbo toggle to report — there is no equivalent of cpufreq/boost or
|
|
470
|
+
// intel_pstate/no_turbo — so there is nothing to read, with or without
|
|
471
|
+
// macmon. Same for cpu-voltages below: no exposed voltage rail.
|
|
472
|
+
if (process.platform !== "linux") {
|
|
473
|
+
return { available: false, unit: "" }
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
if (existsSync("/sys/devices/system/cpu/cpufreq/boost")) {
|
|
477
|
+
const raw = await readFile(
|
|
478
|
+
"/sys/devices/system/cpu/cpufreq/boost",
|
|
479
|
+
"utf8",
|
|
480
|
+
)
|
|
481
|
+
const n = Number.parseInt(raw.trim(), 10)
|
|
482
|
+
if (n === 0 || n === 1) {
|
|
483
|
+
return { available: true, unit: "", value: n }
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (existsSync("/sys/devices/system/cpu/intel_pstate/no_turbo")) {
|
|
487
|
+
const raw = await readFile(
|
|
488
|
+
"/sys/devices/system/cpu/intel_pstate/no_turbo",
|
|
489
|
+
"utf8",
|
|
490
|
+
)
|
|
491
|
+
const n = Number.parseInt(raw.trim(), 10)
|
|
492
|
+
if (n === 0 || n === 1) {
|
|
493
|
+
return { available: true, unit: "", value: n === 0 ? 1 : 0 }
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
} catch {
|
|
497
|
+
// fall through
|
|
498
|
+
}
|
|
499
|
+
return { available: false, unit: "" }
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const CPU_HWMON_NAME_PATTERNS = [
|
|
503
|
+
/^k10temp$/i,
|
|
504
|
+
/^zenpower$/i,
|
|
505
|
+
/^coretemp$/i,
|
|
506
|
+
/^cpu\b/i,
|
|
507
|
+
/^acpitz$/i,
|
|
508
|
+
]
|
|
509
|
+
|
|
510
|
+
// ponytail: scanning /sys/class/hwmon and matching by name keeps us vendor-
|
|
511
|
+
// agnostic. AMD (k10temp/zenpower), Intel (coretemp), and ACPI thermal
|
|
512
|
+
// zones all expose CPU voltage on `in0_input` (millivolts). Pick the first
|
|
513
|
+
// match by preference order; if the system has none, report unavailable
|
|
514
|
+
// rather than guess.
|
|
515
|
+
async function probeCpuVoltages(): Promise<ProbeResult> {
|
|
516
|
+
if (process.platform !== "linux") {
|
|
517
|
+
return { available: false, unit: "V" }
|
|
518
|
+
}
|
|
519
|
+
if (!existsSync("/sys/class/hwmon")) {
|
|
520
|
+
return { available: false, unit: "V" }
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
const entries = await readdir("/sys/class/hwmon")
|
|
524
|
+
const hwmons: Array<{ dir: string; name: string }> = []
|
|
525
|
+
for (const e of entries) {
|
|
526
|
+
const namePath = `/sys/class/hwmon/${e}/name`
|
|
527
|
+
if (!existsSync(namePath)) continue
|
|
528
|
+
const name = (await readFile(namePath, "utf8")).trim()
|
|
529
|
+
hwmons.push({ dir: `/sys/class/hwmon/${e}`, name })
|
|
530
|
+
}
|
|
531
|
+
const matched = hwmons
|
|
532
|
+
.map((h, i) => ({ h, i }))
|
|
533
|
+
.filter(({ h }) => CPU_HWMON_NAME_PATTERNS.some((re) => re.test(h.name)))
|
|
534
|
+
.sort((a, b) => a.i - b.i)[0]
|
|
535
|
+
if (!matched) return { available: false, unit: "V" }
|
|
536
|
+
const in0 = `${matched.h.dir}/in0_input`
|
|
537
|
+
if (!existsSync(in0)) return { available: false, unit: "V" }
|
|
538
|
+
const raw = await readFile(in0, "utf8")
|
|
539
|
+
const mv = Number.parseInt(raw.trim(), 10)
|
|
540
|
+
if (!Number.isFinite(mv) || mv <= 0) {
|
|
541
|
+
return { available: false, unit: "V" }
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
available: true,
|
|
545
|
+
unit: "V",
|
|
546
|
+
value: Number((mv / 1000).toFixed(2)),
|
|
547
|
+
}
|
|
548
|
+
} catch {
|
|
549
|
+
return { available: false, unit: "V" }
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ponytail: /proc/diskstats sectors are cumulative per whole disk. We sum
|
|
554
|
+
// across all whole disks (skip partitions, loop, ram, dm-, md, sr*) and
|
|
555
|
+
// report a delta-over-poll-interval byte rate. Same trick as probeCpu —
|
|
556
|
+
// the first sample has no baseline, so it returns 0 to avoid a misleading
|
|
557
|
+
// "all disks went from 0 → N MB in zero seconds" spike.
|
|
558
|
+
const DISKSTATS_SKIP_RE =
|
|
559
|
+
/^(loop|ram|dm-|md|drbd)\d*|^(sr|nbd)\d+$|^[a-z]+\d+_|^dm-/
|
|
560
|
+
let prevDiskIoSample: { ts: number; sectors: number } | null = null
|
|
561
|
+
let prevDarwinDiskIoSample: { ts: number; bytes: number } | null = null
|
|
562
|
+
|
|
563
|
+
async function readDiskstatsSectors(): Promise<number | null> {
|
|
564
|
+
if (!existsSync("/proc/diskstats")) return null
|
|
565
|
+
try {
|
|
566
|
+
const raw = await readFile("/proc/diskstats", "utf8")
|
|
567
|
+
let totalSectors = 0
|
|
568
|
+
for (const line of raw.split("\n")) {
|
|
569
|
+
const parts = line.trim().split(/\s+/)
|
|
570
|
+
if (parts.length < 14) continue
|
|
571
|
+
const name = parts[2]
|
|
572
|
+
if (!name || DISKSTATS_SKIP_RE.test(name)) continue
|
|
573
|
+
if (/^(nvme\d+n\d+)p\d+$/.test(name)) continue
|
|
574
|
+
const readSectors = Number.parseInt(parts[5] ?? "", 10)
|
|
575
|
+
const writtenSectors = Number.parseInt(parts[9] ?? "", 10)
|
|
576
|
+
if (!Number.isFinite(readSectors) || !Number.isFinite(writtenSectors)) {
|
|
577
|
+
continue
|
|
578
|
+
}
|
|
579
|
+
totalSectors += readSectors + writtenSectors
|
|
580
|
+
}
|
|
581
|
+
return totalSectors
|
|
582
|
+
} catch {
|
|
583
|
+
return null
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ponytail: macOS has no /proc/diskstats, but IOBlockStorageDriver carries the
|
|
588
|
+
// equivalent cumulative counters ("Bytes (Read)" / "Bytes (Write)") per device.
|
|
589
|
+
// Sum across devices — ioreg lists several and only the backing store has
|
|
590
|
+
// non-zero totals — and feed the same delta-over-interval maths the Linux
|
|
591
|
+
// reader uses, so both platforms report the same B/s quantity.
|
|
592
|
+
export const parseDarwinDiskBytes = (raw: string): number | null => {
|
|
593
|
+
let total = 0
|
|
594
|
+
let seen = false
|
|
595
|
+
for (const m of raw.matchAll(/"Bytes \((?:Read|Write)\)"\s*=\s*(\d+)/g)) {
|
|
596
|
+
const n = Number.parseInt(m[1] ?? "", 10)
|
|
597
|
+
if (!Number.isFinite(n)) continue
|
|
598
|
+
total += n
|
|
599
|
+
seen = true
|
|
600
|
+
}
|
|
601
|
+
return seen ? total : null
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function readDarwinDiskBytes(): Promise<number | null> {
|
|
605
|
+
const raw = await darwinRun("ioreg", [
|
|
606
|
+
"-c",
|
|
607
|
+
"IOBlockStorageDriver",
|
|
608
|
+
"-r",
|
|
609
|
+
"-d",
|
|
610
|
+
"1",
|
|
611
|
+
"-w",
|
|
612
|
+
"0",
|
|
613
|
+
])
|
|
614
|
+
return raw === null ? null : parseDarwinDiskBytes(raw)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function probeDiskIo(): Promise<ProbeResult> {
|
|
618
|
+
if (process.platform === "darwin") {
|
|
619
|
+
const bytes = await readDarwinDiskBytes()
|
|
620
|
+
if (bytes === null) return { available: false, unit: "B/s" }
|
|
621
|
+
const now = Date.now()
|
|
622
|
+
const prev = prevDarwinDiskIoSample
|
|
623
|
+
if (prev === null || bytes < prev.bytes) {
|
|
624
|
+
prevDarwinDiskIoSample = { ts: now, bytes }
|
|
625
|
+
return { available: true, unit: "B/s", value: 0 }
|
|
626
|
+
}
|
|
627
|
+
const dtSec = Math.max(0.001, (now - prev.ts) / 1000)
|
|
628
|
+
const delta = bytes - prev.bytes
|
|
629
|
+
prevDarwinDiskIoSample = { ts: now, bytes }
|
|
630
|
+
return {
|
|
631
|
+
available: true,
|
|
632
|
+
unit: "B/s",
|
|
633
|
+
value: Math.round(delta / dtSec),
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (process.platform !== "linux") {
|
|
637
|
+
return { available: false, unit: "B/s" }
|
|
638
|
+
}
|
|
639
|
+
const sectors = await readDiskstatsSectors()
|
|
640
|
+
if (sectors === null) return { available: false, unit: "B/s" }
|
|
641
|
+
const now = Date.now()
|
|
642
|
+
const prev = prevDiskIoSample
|
|
643
|
+
if (prev === null || sectors < prev.sectors) {
|
|
644
|
+
prevDiskIoSample = { ts: now, sectors }
|
|
645
|
+
return { available: true, unit: "B/s", value: 0 }
|
|
646
|
+
}
|
|
647
|
+
const dtSec = Math.max(0.001, (now - prev.ts) / 1000)
|
|
648
|
+
const dSectors = sectors - prev.sectors
|
|
649
|
+
prevDiskIoSample = { ts: now, sectors }
|
|
650
|
+
const bytesPerSec = Math.round((dSectors * 512) / dtSec)
|
|
651
|
+
return { available: true, unit: "B/s", value: bytesPerSec }
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// ponytail: hwmon fans expose `fan<N>_input` (RPM). Many systems have none
|
|
655
|
+
// (laptops with passive cooling). Pick the first hwmon with any non-zero
|
|
656
|
+
// fan reading — keeps the metric meaningful (a "0 RPM" fan isn't a fan).
|
|
657
|
+
async function probeFanRpm(): Promise<ProbeResult> {
|
|
658
|
+
if (process.platform === "darwin") {
|
|
659
|
+
const sample = await readMacmon()
|
|
660
|
+
const value = sample?.fanRpm ?? null
|
|
661
|
+
return value === null
|
|
662
|
+
? { available: false, unit: "RPM" }
|
|
663
|
+
: { available: true, value, unit: "RPM" }
|
|
664
|
+
}
|
|
665
|
+
if (process.platform !== "linux") {
|
|
666
|
+
return { available: false, unit: "RPM" }
|
|
667
|
+
}
|
|
668
|
+
if (!existsSync("/sys/class/hwmon")) {
|
|
669
|
+
return { available: false, unit: "RPM" }
|
|
670
|
+
}
|
|
671
|
+
try {
|
|
672
|
+
const entries = await readdir("/sys/class/hwmon")
|
|
673
|
+
for (const e of entries) {
|
|
674
|
+
const dir = `/sys/class/hwmon/${e}`
|
|
675
|
+
const files = await readdir(dir).catch(() => [])
|
|
676
|
+
const fans = files.filter((f) => /^fan\d+_input$/.test(f)).sort()
|
|
677
|
+
for (const f of fans) {
|
|
678
|
+
const raw = await readFile(`${dir}/${f}`, "utf8").catch(() => "")
|
|
679
|
+
const rpm = Number.parseInt(raw.trim(), 10)
|
|
680
|
+
if (Number.isFinite(rpm) && rpm > 0) {
|
|
681
|
+
return { available: true, unit: "RPM", value: rpm }
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return { available: false, unit: "RPM" }
|
|
686
|
+
} catch {
|
|
687
|
+
return { available: false, unit: "RPM" }
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// ponytail: amd/amdgpu publishes busy% on /sys/class/drm/card*/device/
|
|
692
|
+
// gpu_busy_percent and temp on the card's hwmon temp1_input. Both are
|
|
693
|
+
// first-match across cards. NVIDIA users get a one-shot fallback to
|
|
694
|
+
// nvidia-smi (stdlib execFile with a 1.5s ceiling so a missing binary
|
|
695
|
+
// doesn't stall the poller). Other vendors (Intel i915, nouveau) report
|
|
696
|
+
// unavailable rather than spawn a shell — YAGNI on coverage we can't
|
|
697
|
+
// validate in CI.
|
|
698
|
+
async function readAmdGpuBusyPercent(): Promise<number | null> {
|
|
699
|
+
if (!existsSync("/sys/class/drm")) return null
|
|
700
|
+
const cards = (await readdir("/sys/class/drm").catch(() => [])).filter((c) =>
|
|
701
|
+
/^card\d+$/.test(c),
|
|
702
|
+
)
|
|
703
|
+
for (const card of cards) {
|
|
704
|
+
const p = `/sys/class/drm/${card}/device/gpu_busy_percent`
|
|
705
|
+
if (!existsSync(p)) continue
|
|
706
|
+
const raw = await readFile(p, "utf8").catch(() => "")
|
|
707
|
+
const pct = Number.parseInt(raw.trim(), 10)
|
|
708
|
+
if (Number.isFinite(pct) && pct >= 0 && pct <= 100) return pct
|
|
709
|
+
}
|
|
710
|
+
return null
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async function readAmdGpuTempMilliC(): Promise<number | null> {
|
|
714
|
+
if (!existsSync("/sys/class/drm")) return null
|
|
715
|
+
const cards = (await readdir("/sys/class/drm").catch(() => [])).filter((c) =>
|
|
716
|
+
/^card\d+$/.test(c),
|
|
717
|
+
)
|
|
718
|
+
for (const card of cards) {
|
|
719
|
+
const devDir = `/sys/class/drm/${card}/device`
|
|
720
|
+
if (!existsSync(devDir)) continue
|
|
721
|
+
const entries = await readdir(devDir).catch(() => [])
|
|
722
|
+
const hwmons = entries.filter((e) => /^hwmon\d+$/.test(e))
|
|
723
|
+
for (const h of hwmons) {
|
|
724
|
+
const t = `${devDir}/${h}/temp1_input`
|
|
725
|
+
if (!existsSync(t)) continue
|
|
726
|
+
const raw = await readFile(t, "utf8").catch(() => "")
|
|
727
|
+
const milli = Number.parseInt(raw.trim(), 10)
|
|
728
|
+
if (Number.isFinite(milli) && milli > 0) return milli
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return null
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async function tryNvidiaSmi(field: string): Promise<number | null> {
|
|
735
|
+
try {
|
|
736
|
+
const { stdout } = await execFile(
|
|
737
|
+
"nvidia-smi",
|
|
738
|
+
[`--query-gpu=${field}`, "--format=csv,noheader,nounits"],
|
|
739
|
+
{ timeout: 1500, signal: AbortSignal.timeout(1500) },
|
|
740
|
+
)
|
|
741
|
+
const first = stdout.trim().split("\n")[0]?.trim() ?? ""
|
|
742
|
+
const n = Number.parseInt(first, 10)
|
|
743
|
+
return Number.isFinite(n) ? n : null
|
|
744
|
+
} catch {
|
|
745
|
+
return null
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// ponytail: Apple Silicon publishes GPU busy-ness in the IOAccelerator node's
|
|
750
|
+
// PerformanceStatistics dict, readable by any user — no root, no private
|
|
751
|
+
// framework. "Device Utilization %" is the whole-GPU figure; the Renderer/
|
|
752
|
+
// Tiler entries break it down per engine.
|
|
753
|
+
export const parseDarwinGpuUtilization = (raw: string): number | null => {
|
|
754
|
+
const m = /"Device Utilization %"\s*=\s*(\d+)/.exec(raw)
|
|
755
|
+
if (m?.[1] === undefined) return null
|
|
756
|
+
const pct = Number.parseInt(m[1], 10)
|
|
757
|
+
return Number.isFinite(pct) ? pct : null
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async function probeGpuUsage(): Promise<ProbeResult> {
|
|
761
|
+
if (process.platform === "darwin") {
|
|
762
|
+
const raw = await darwinRun("ioreg", [
|
|
763
|
+
"-r",
|
|
764
|
+
"-d",
|
|
765
|
+
"1",
|
|
766
|
+
"-w",
|
|
767
|
+
"0",
|
|
768
|
+
"-c",
|
|
769
|
+
"IOAccelerator",
|
|
770
|
+
])
|
|
771
|
+
const pct = raw === null ? null : parseDarwinGpuUtilization(raw)
|
|
772
|
+
if (pct === null) return { available: false, unit: "%" }
|
|
773
|
+
const clamped = clampPercent(pct)
|
|
774
|
+
return {
|
|
775
|
+
available: true,
|
|
776
|
+
max: 100,
|
|
777
|
+
percentage: clamped,
|
|
778
|
+
unit: "%",
|
|
779
|
+
value: clamped,
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
if (process.platform !== "linux") {
|
|
783
|
+
return { available: false, unit: "%" }
|
|
784
|
+
}
|
|
785
|
+
const pct = await readAmdGpuBusyPercent()
|
|
786
|
+
if (pct !== null) {
|
|
787
|
+
return {
|
|
788
|
+
available: true,
|
|
789
|
+
max: 100,
|
|
790
|
+
percentage: clampPercent(pct),
|
|
791
|
+
unit: "%",
|
|
792
|
+
value: clampPercent(pct),
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
const nvidia = await tryNvidiaSmi("utilization.gpu")
|
|
796
|
+
if (nvidia !== null) {
|
|
797
|
+
return {
|
|
798
|
+
available: true,
|
|
799
|
+
max: 100,
|
|
800
|
+
percentage: clampPercent(nvidia),
|
|
801
|
+
unit: "%",
|
|
802
|
+
value: clampPercent(nvidia),
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return { available: false, unit: "%" }
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async function probeGpuTemp(): Promise<ProbeResult> {
|
|
809
|
+
if (process.platform === "darwin") {
|
|
810
|
+
const sample = await readMacmon()
|
|
811
|
+
const value = sample?.gpuTemp ?? null
|
|
812
|
+
return value === null
|
|
813
|
+
? { available: false, unit: "°C" }
|
|
814
|
+
: { available: true, value, unit: "°C" }
|
|
815
|
+
}
|
|
816
|
+
if (process.platform !== "linux") {
|
|
817
|
+
return { available: false, unit: "°C" }
|
|
818
|
+
}
|
|
819
|
+
const milli = await readAmdGpuTempMilliC()
|
|
820
|
+
if (milli !== null) {
|
|
821
|
+
return {
|
|
822
|
+
available: true,
|
|
823
|
+
unit: "°C",
|
|
824
|
+
value: Math.round(milli / 1000),
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
const nvidia = await tryNvidiaSmi("temperature.gpu")
|
|
828
|
+
if (nvidia !== null && nvidia > 0) {
|
|
829
|
+
return { available: true, unit: "°C", value: nvidia }
|
|
830
|
+
}
|
|
831
|
+
return { available: false, unit: "°C" }
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// ponytail: /sys/class/net/<iface>/statistics/{rx,tx}_bytes is cumulative
|
|
835
|
+
// per interface. Sum across non-loopback, non-virtual ifaces (skip lo,
|
|
836
|
+
// docker*, veth*, br-*, tun*, tap*) and report Δbytes / Δt as a rate. The
|
|
837
|
+
// first sample re-baselines to 0 to avoid a "100 GB/s on boot" spike.
|
|
838
|
+
const NETWORK_IFACE_SKIP_RE =
|
|
839
|
+
/^(lo|docker|veth|br-|tun|tap|virbr|vmnet|vboxnet|awdl|llw|bridge|ipv6tnl|sit|gre|ip6gre|ipip|gretap|erspan)/
|
|
840
|
+
|
|
841
|
+
let prevNetworkSample: { ts: number; rx: number; tx: number } | null = null
|
|
842
|
+
|
|
843
|
+
async function readNetBytes(
|
|
844
|
+
field: "rx_bytes" | "tx_bytes",
|
|
845
|
+
): Promise<number | null> {
|
|
846
|
+
if (!existsSync("/sys/class/net")) return null
|
|
847
|
+
const ifaces = await readdir("/sys/class/net").catch(() => [])
|
|
848
|
+
let total = 0
|
|
849
|
+
let counted = false
|
|
850
|
+
for (const iface of ifaces) {
|
|
851
|
+
if (NETWORK_IFACE_SKIP_RE.test(iface)) continue
|
|
852
|
+
const p = `/sys/class/net/${iface}/statistics/${field}`
|
|
853
|
+
if (!existsSync(p)) continue
|
|
854
|
+
const raw = await readFile(p, "utf8").catch(() => "")
|
|
855
|
+
const n = Number.parseInt(raw.trim(), 10)
|
|
856
|
+
if (Number.isFinite(n) && n >= 0) {
|
|
857
|
+
total += n
|
|
858
|
+
counted = true
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return counted ? total : null
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// ponytail: macOS has no /sys/class/net; `netstat -ibn` carries the same
|
|
865
|
+
// cumulative per-interface byte counters. Both platforms then share the
|
|
866
|
+
// identical delta-over-interval logic below.
|
|
867
|
+
async function readNetTotals(): Promise<{ rx: number; tx: number } | null> {
|
|
868
|
+
if (process.platform === "darwin") {
|
|
869
|
+
const raw = await darwinRun("netstat", ["-ibn"])
|
|
870
|
+
return raw === null ? null : parseDarwinNetstat(raw)
|
|
871
|
+
}
|
|
872
|
+
const rx = await readNetBytes("rx_bytes")
|
|
873
|
+
const tx = await readNetBytes("tx_bytes")
|
|
874
|
+
if (rx === null || tx === null) return null
|
|
875
|
+
return { rx, tx }
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
async function readNetworkDelta(): Promise<{
|
|
879
|
+
dt: number
|
|
880
|
+
rx: number
|
|
881
|
+
tx: number
|
|
882
|
+
} | null> {
|
|
883
|
+
const totals = await readNetTotals()
|
|
884
|
+
if (totals === null) return null
|
|
885
|
+
const { rx, tx } = totals
|
|
886
|
+
const now = Date.now()
|
|
887
|
+
const prev = prevNetworkSample
|
|
888
|
+
if (prev === null || rx < prev.rx || tx < prev.tx) {
|
|
889
|
+
prevNetworkSample = { ts: now, rx, tx }
|
|
890
|
+
return null
|
|
891
|
+
}
|
|
892
|
+
const dt = Math.max(0.001, (now - prev.ts) / 1000)
|
|
893
|
+
prevNetworkSample = { ts: now, rx, tx }
|
|
894
|
+
return { dt, rx: rx - prev.rx, tx: tx - prev.tx }
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// ponytail: network-read and network-write are two metric ids over ONE pair of
|
|
898
|
+
// counters. Each used to call readNetworkDelta() independently, and that call
|
|
899
|
+
// consumes the baseline — so whichever ran second measured the interval since
|
|
900
|
+
// the first, a few microseconds earlier, and reported ~0 B/s. A deck showing
|
|
901
|
+
// both (the default config does) had one of them permanently dead, and which
|
|
902
|
+
// one depended on scheduling order. Share a single delta across any calls that
|
|
903
|
+
// land in the same tick; the window is far below the poll interval, so a real
|
|
904
|
+
// next tick still re-samples.
|
|
905
|
+
const NETWORK_DELTA_SHARE_MS = 250
|
|
906
|
+
let sharedNetworkDelta: {
|
|
907
|
+
at: number
|
|
908
|
+
value: { dt: number; rx: number; tx: number } | null
|
|
909
|
+
} | null = null
|
|
910
|
+
let inFlightNetworkDelta: Promise<{
|
|
911
|
+
dt: number
|
|
912
|
+
rx: number
|
|
913
|
+
tx: number
|
|
914
|
+
} | null> | null = null
|
|
915
|
+
|
|
916
|
+
async function readSharedNetworkDelta(): Promise<{
|
|
917
|
+
dt: number
|
|
918
|
+
rx: number
|
|
919
|
+
tx: number
|
|
920
|
+
} | null> {
|
|
921
|
+
const now = Date.now()
|
|
922
|
+
if (
|
|
923
|
+
sharedNetworkDelta !== null &&
|
|
924
|
+
now - sharedNetworkDelta.at < NETWORK_DELTA_SHARE_MS
|
|
925
|
+
) {
|
|
926
|
+
return sharedNetworkDelta.value
|
|
927
|
+
}
|
|
928
|
+
// Collapse concurrent callers onto one in-flight read as well, so two
|
|
929
|
+
// probes dispatched in the same Promise.all don't both hit the counters.
|
|
930
|
+
if (inFlightNetworkDelta !== null) return inFlightNetworkDelta
|
|
931
|
+
inFlightNetworkDelta = (async () => {
|
|
932
|
+
try {
|
|
933
|
+
const value = await readNetworkDelta()
|
|
934
|
+
sharedNetworkDelta = { at: Date.now(), value }
|
|
935
|
+
return value
|
|
936
|
+
} finally {
|
|
937
|
+
inFlightNetworkDelta = null
|
|
938
|
+
}
|
|
939
|
+
})()
|
|
940
|
+
return inFlightNetworkDelta
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
export function __resetNetworkDeltaCacheForTests(): void {
|
|
944
|
+
sharedNetworkDelta = null
|
|
945
|
+
inFlightNetworkDelta = null
|
|
946
|
+
prevNetworkSample = null
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
async function probeNetworkRead(): Promise<ProbeResult> {
|
|
950
|
+
if (process.platform !== "linux" && process.platform !== "darwin") {
|
|
951
|
+
return { available: false, unit: "B/s" }
|
|
952
|
+
}
|
|
953
|
+
const delta = await readSharedNetworkDelta()
|
|
954
|
+
if (delta === null) return { available: false, unit: "B/s" }
|
|
955
|
+
return {
|
|
956
|
+
available: true,
|
|
957
|
+
unit: "B/s",
|
|
958
|
+
value: Math.round(delta.rx / delta.dt),
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
async function probeNetworkWrite(): Promise<ProbeResult> {
|
|
963
|
+
if (process.platform !== "linux" && process.platform !== "darwin") {
|
|
964
|
+
return { available: false, unit: "B/s" }
|
|
965
|
+
}
|
|
966
|
+
const delta = await readSharedNetworkDelta()
|
|
967
|
+
if (delta === null) return { available: false, unit: "B/s" }
|
|
968
|
+
return {
|
|
969
|
+
available: true,
|
|
970
|
+
unit: "B/s",
|
|
971
|
+
value: Math.round(delta.tx / delta.dt),
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const PROBES: Record<SystemMetricId, () => Promise<ProbeResult>> = {
|
|
976
|
+
cpu: probeCpu,
|
|
977
|
+
ram: probeRam,
|
|
978
|
+
swap: probeSwap,
|
|
979
|
+
disk: probeDisk,
|
|
980
|
+
network: probeNetwork,
|
|
981
|
+
battery: probeBattery,
|
|
982
|
+
temperature: probeTemperature,
|
|
983
|
+
uptime: probeUptime,
|
|
984
|
+
frequency: probeFrequency,
|
|
985
|
+
load: probeLoad,
|
|
986
|
+
processes: probeProcesses,
|
|
987
|
+
"cpu-boost": probeCpuBoost,
|
|
988
|
+
"cpu-voltages": probeCpuVoltages,
|
|
989
|
+
"disk-io": probeDiskIo,
|
|
990
|
+
"fan-rpm": probeFanRpm,
|
|
991
|
+
"gpu-temp": probeGpuTemp,
|
|
992
|
+
"gpu-usage": probeGpuUsage,
|
|
993
|
+
"network-read": probeNetworkRead,
|
|
994
|
+
"network-write": probeNetworkWrite,
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
export async function probeMetric(
|
|
998
|
+
id: SystemMetricId,
|
|
999
|
+
): Promise<SystemMetricSnapshot> {
|
|
1000
|
+
const probe = PROBES[id]
|
|
1001
|
+
try {
|
|
1002
|
+
const r = await probe()
|
|
1003
|
+
return {
|
|
1004
|
+
available: r.available,
|
|
1005
|
+
id,
|
|
1006
|
+
label: id,
|
|
1007
|
+
...(r.max !== undefined ? { max: r.max } : {}),
|
|
1008
|
+
...(r.percentage !== undefined ? { percentage: r.percentage } : {}),
|
|
1009
|
+
...(r.unit !== undefined ? { unit: r.unit } : {}),
|
|
1010
|
+
...(r.value !== undefined ? { value: r.value } : {}),
|
|
1011
|
+
}
|
|
1012
|
+
} catch {
|
|
1013
|
+
return { available: false, id, label: id }
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
export async function probeMetrics(
|
|
1018
|
+
ids: readonly SystemMetricId[],
|
|
1019
|
+
): Promise<SystemMetricSnapshot[]> {
|
|
1020
|
+
return Promise.all(ids.map(probeMetric))
|
|
1021
|
+
}
|