@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,1061 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, readlinkSync } from "node:fs"
|
|
2
|
+
import { execFileSync } from "node:child_process"
|
|
3
|
+
import { dirname, join, resolve as resolvePath } from "node:path"
|
|
4
|
+
import { fileURLToPath } from "node:url"
|
|
5
|
+
|
|
6
|
+
import { confirm, isCancel, select } from "@/cli/prompt"
|
|
7
|
+
import type pino from "pino"
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
cmdlineMentionsCliRoot,
|
|
11
|
+
isOrphan,
|
|
12
|
+
isOurDaemon,
|
|
13
|
+
isOurViteChild,
|
|
14
|
+
readProcCmdline,
|
|
15
|
+
} from "./port-identity"
|
|
16
|
+
|
|
17
|
+
import { resolveConfigPath as resolveRunConfigPath } from "./pipeline/helpers"
|
|
18
|
+
import type { ResolveConfigPathResult } from "./pipeline/helpers"
|
|
19
|
+
import {
|
|
20
|
+
acquireStartLock,
|
|
21
|
+
generateToken,
|
|
22
|
+
isRunning,
|
|
23
|
+
pruneStaleChildren,
|
|
24
|
+
readConfigPath,
|
|
25
|
+
readPid,
|
|
26
|
+
removeChildrenFile,
|
|
27
|
+
removePidFile,
|
|
28
|
+
removeRuntimeStateFile,
|
|
29
|
+
removeStartLock,
|
|
30
|
+
resolveDaemonPaths,
|
|
31
|
+
terminateChildren,
|
|
32
|
+
writeChildren,
|
|
33
|
+
writeConfigPath,
|
|
34
|
+
writeFlags,
|
|
35
|
+
writePid,
|
|
36
|
+
type RuntimeFlags,
|
|
37
|
+
} from "@/util/daemon"
|
|
38
|
+
import {
|
|
39
|
+
acquireInstanceLock,
|
|
40
|
+
instanceLockPath,
|
|
41
|
+
isSocketLive,
|
|
42
|
+
} from "@/util/single-instance"
|
|
43
|
+
|
|
44
|
+
import { startHttpServer, type RunningHttpServer } from "../http-server"
|
|
45
|
+
import { removeDaemonControl, startDaemonControl } from "@/util/daemon-control"
|
|
46
|
+
import { tailLogs } from "@/util/log-tail"
|
|
47
|
+
import {
|
|
48
|
+
builtinDir,
|
|
49
|
+
collectBuiltinAddonRegistry,
|
|
50
|
+
type ScannedAddon,
|
|
51
|
+
} from "./addon-registry"
|
|
52
|
+
import {
|
|
53
|
+
ensureInstalled,
|
|
54
|
+
invokeManager,
|
|
55
|
+
isUnitInstalled,
|
|
56
|
+
} from "./service-manager"
|
|
57
|
+
import { isUnderServiceManager, spawnDetached } from "./spawn-daemon"
|
|
58
|
+
import { runPipeline, type RunOptions, type SignalProvider } from "./run"
|
|
59
|
+
import { preflight } from "./pipeline/preflight"
|
|
60
|
+
import {
|
|
61
|
+
type SystemReport,
|
|
62
|
+
probeAllCached,
|
|
63
|
+
summarizeReport,
|
|
64
|
+
} from "@/system/setup-wizard"
|
|
65
|
+
import { systemRequirements } from "./system-requirements"
|
|
66
|
+
import { buildStandardProbeDeps } from "@/cli/probe-deps"
|
|
67
|
+
import { onboardCodingAgents } from "@/cli/coding-agents-onboarding"
|
|
68
|
+
import { resolveUserPath } from "@/cli/cwd"
|
|
69
|
+
|
|
70
|
+
export interface StartOptions {
|
|
71
|
+
readonly config?: string
|
|
72
|
+
readonly port?: number
|
|
73
|
+
readonly emulator?: boolean
|
|
74
|
+
readonly remote?: boolean
|
|
75
|
+
readonly deviceModel?: string
|
|
76
|
+
readonly frontendUrl?: string
|
|
77
|
+
readonly intervalMs?: number
|
|
78
|
+
readonly xdgConfigHome?: string
|
|
79
|
+
readonly homeDir?: string
|
|
80
|
+
readonly httpPort?: number
|
|
81
|
+
readonly logs?: boolean
|
|
82
|
+
readonly system?: boolean
|
|
83
|
+
readonly noAutoOpen?: boolean
|
|
84
|
+
readonly signals?: SignalProvider
|
|
85
|
+
readonly logger: pino.Logger
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const toRunOptions = (
|
|
89
|
+
options: StartOptions,
|
|
90
|
+
onChildren: (pids: ReadonlyArray<number>) => void,
|
|
91
|
+
onAddonsUpdate?: (addons: ReadonlyArray<ScannedAddon>) => void,
|
|
92
|
+
): RunOptions => ({
|
|
93
|
+
logger: options.logger,
|
|
94
|
+
config: options.config,
|
|
95
|
+
port: options.port,
|
|
96
|
+
emulator: options.emulator,
|
|
97
|
+
remote: options.remote,
|
|
98
|
+
deviceModel: options.deviceModel,
|
|
99
|
+
frontendUrl: options.frontendUrl,
|
|
100
|
+
intervalMs: options.intervalMs,
|
|
101
|
+
xdgConfigHome: options.xdgConfigHome,
|
|
102
|
+
homeDir: options.homeDir,
|
|
103
|
+
signals: options.signals,
|
|
104
|
+
noAutoOpen: options.noAutoOpen === true,
|
|
105
|
+
onChildren,
|
|
106
|
+
onAddonsUpdate,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const resolveFrontendDist = (): string => {
|
|
110
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
111
|
+
return resolvePath(here, "../../../frontend/dist")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const isDevInvocation = (): boolean => {
|
|
115
|
+
return (process.argv[1] ?? "").endsWith(".ts")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ponytail: kill every process currently listening on the daemon's expected
|
|
119
|
+
// ports — but ONLY if the identity gate (cmdline + orphan check) says it's
|
|
120
|
+
// ours. Otherwise the port collision stays and the new daemon's preflight
|
|
121
|
+
// surfaces a clear EADDRINUSE, which is the correct signal to the user.
|
|
122
|
+
|
|
123
|
+
// ponytail: each backend distinguishes "tool ran, found nothing" (the common
|
|
124
|
+
// preflight case when no daemon is up) from "tool unavailable / crashed"
|
|
125
|
+
// (rare — binary missing on PATH, permission denied, etc.). Only the latter
|
|
126
|
+
// surfaces as a WARN; empty results stay silent so a clean `p dev start`
|
|
127
|
+
// doesn't flood with 4 false-positive "all backends failed" lines.
|
|
128
|
+
export type PortScanResult =
|
|
129
|
+
| { readonly ok: true; readonly pids: ReadonlyArray<number> }
|
|
130
|
+
| { readonly ok: false; readonly reason: string }
|
|
131
|
+
|
|
132
|
+
const trySs = (port: number): PortScanResult => {
|
|
133
|
+
const ssLineRegex = (p: number): RegExp =>
|
|
134
|
+
new RegExp(`:${p}\\b[\\s\\S]*?users:\\([^)]*?pid=(\\d+)[,\\)]`)
|
|
135
|
+
try {
|
|
136
|
+
const out = execFileSync("ss", ["-ltnp"], {
|
|
137
|
+
encoding: "utf8",
|
|
138
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
139
|
+
})
|
|
140
|
+
const pids: number[] = []
|
|
141
|
+
for (const line of out.split("\n")) {
|
|
142
|
+
const m = line.match(ssLineRegex(port))
|
|
143
|
+
if (m && m[1]) pids.push(Number.parseInt(m[1], 10))
|
|
144
|
+
}
|
|
145
|
+
return { ok: true, pids }
|
|
146
|
+
} catch (err) {
|
|
147
|
+
return { ok: false, reason: describeExecFailure(err, "ss") }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const parseLsofPids = (out: string): ReadonlyArray<number> =>
|
|
152
|
+
out
|
|
153
|
+
.split("\n")
|
|
154
|
+
.map((line) => Number.parseInt(line.trim(), 10))
|
|
155
|
+
.filter((n) => Number.isFinite(n) && n > 0)
|
|
156
|
+
|
|
157
|
+
const tryLsof = (port: number): PortScanResult => {
|
|
158
|
+
try {
|
|
159
|
+
const out = execFileSync("lsof", [`-iTCP:${port}`, "-sTCP:LISTEN", "-t"], {
|
|
160
|
+
encoding: "utf8",
|
|
161
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
162
|
+
})
|
|
163
|
+
return { ok: true, pids: parseLsofPids(out) }
|
|
164
|
+
} catch (err) {
|
|
165
|
+
// ponytail: lsof exits 1 with EMPTY output when simply nothing matches the
|
|
166
|
+
// filter — its documented "no results" status, not a failure. execFileSync
|
|
167
|
+
// throws on any non-zero exit, so the common "port is free" case was being
|
|
168
|
+
// recorded as "backend unavailable". On macOS that was every call (ss and
|
|
169
|
+
// /proc/net/tcp don't exist there), so detectPortPids concluded it had no
|
|
170
|
+
// working backend at all, warned on every start, and orphan cleanup went
|
|
171
|
+
// blind — leaving stale vites holding :5180/:52937 after an unclean exit.
|
|
172
|
+
// Treat exit 1 with no stdout as a clean empty result; a missing binary
|
|
173
|
+
// (ENOENT) or any other status stays a real failure.
|
|
174
|
+
const e = err as NodeJS.ErrnoException & {
|
|
175
|
+
status?: number | null
|
|
176
|
+
stdout?: string | Buffer
|
|
177
|
+
}
|
|
178
|
+
const stdout =
|
|
179
|
+
typeof e.stdout === "string" ? e.stdout : (e.stdout?.toString() ?? "")
|
|
180
|
+
if (e.code !== "ENOENT" && e.status === 1) {
|
|
181
|
+
return { ok: true, pids: parseLsofPids(stdout) }
|
|
182
|
+
}
|
|
183
|
+
return { ok: false, reason: describeExecFailure(err, "lsof") }
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const tryProcNet = (port: number): PortScanResult => {
|
|
188
|
+
if (process.platform !== "linux") {
|
|
189
|
+
return { ok: false, reason: "/proc/net/tcp: not on linux" }
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const hex = port.toString(16).toUpperCase().padStart(4, "0")
|
|
193
|
+
const out = readFileSync("/proc/net/tcp", "utf8")
|
|
194
|
+
const pids = new Set<number>()
|
|
195
|
+
const inodes: string[] = []
|
|
196
|
+
for (const line of out.split("\n").slice(1)) {
|
|
197
|
+
const cols = line.trim().split(/\s+/)
|
|
198
|
+
if (cols.length < 10) continue
|
|
199
|
+
const localAddr = cols[1] ?? ""
|
|
200
|
+
const [, localPortHex] = localAddr.split(":")
|
|
201
|
+
if (localPortHex !== hex) continue
|
|
202
|
+
const inode = cols[9]
|
|
203
|
+
if (inode) inodes.push(inode)
|
|
204
|
+
}
|
|
205
|
+
const procDirs = readdirSyncSync("/proc")
|
|
206
|
+
for (const pidStr of procDirs) {
|
|
207
|
+
if (!/^\d+$/.test(pidStr)) continue
|
|
208
|
+
try {
|
|
209
|
+
const fdList = readdirSyncSync(`/proc/${pidStr}/fd`)
|
|
210
|
+
for (const fd of fdList) {
|
|
211
|
+
try {
|
|
212
|
+
const link = readlinkSyncFn(`/proc/${pidStr}/fd/${fd}`)
|
|
213
|
+
for (const inode of inodes) {
|
|
214
|
+
if (link === `socket:[${inode}]`) {
|
|
215
|
+
pids.add(Number.parseInt(pidStr, 10))
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
// ignore
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
// ignore
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return { ok: true, pids: Array.from(pids) }
|
|
227
|
+
} catch (err) {
|
|
228
|
+
const e = err as NodeJS.ErrnoException
|
|
229
|
+
return {
|
|
230
|
+
ok: false,
|
|
231
|
+
reason: `/proc/net/tcp: ${e.code ?? e.message}`,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const describeExecFailure = (err: unknown, tool: string): string => {
|
|
237
|
+
const e = err as NodeJS.ErrnoException
|
|
238
|
+
if (e.code === "ENOENT") return `${tool}: binary not found on PATH`
|
|
239
|
+
if (typeof e.code === "string") return `${tool}: ${e.code}`
|
|
240
|
+
return `${tool}: ${e.message}`
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const readdirSyncSync = (dir: string): string[] => {
|
|
244
|
+
try {
|
|
245
|
+
return readdirSync(dir)
|
|
246
|
+
} catch {
|
|
247
|
+
return []
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const readlinkSyncFn = (path: string): string | null => {
|
|
252
|
+
try {
|
|
253
|
+
return readlinkSync(path)
|
|
254
|
+
} catch {
|
|
255
|
+
return null
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export const detectPortPids = (
|
|
260
|
+
port: number,
|
|
261
|
+
logger: pino.Logger,
|
|
262
|
+
): ReadonlyArray<number> => {
|
|
263
|
+
const ss = trySs(port)
|
|
264
|
+
if (ss.ok && ss.pids.length > 0) return ss.pids
|
|
265
|
+
const lsof = tryLsof(port)
|
|
266
|
+
if (lsof.ok && lsof.pids.length > 0) {
|
|
267
|
+
logger.debug({ port }, "port detection: ss failed, used lsof")
|
|
268
|
+
return lsof.pids
|
|
269
|
+
}
|
|
270
|
+
const procNet = tryProcNet(port)
|
|
271
|
+
if (procNet.ok && procNet.pids.length > 0) {
|
|
272
|
+
logger.debug({ port }, "port detection: ss/lsof failed, used /proc/net/tcp")
|
|
273
|
+
return procNet.pids
|
|
274
|
+
}
|
|
275
|
+
// ponytail: only WARN when EVERY backend is unavailable. "nothing listening"
|
|
276
|
+
// (the common preflight case) is a clean empty result, not a failure.
|
|
277
|
+
// On macOS /proc/net/tcp is absent by design — ss+lsof are still consulted
|
|
278
|
+
// and a clean empty from both means "no listener", not "backend down".
|
|
279
|
+
if (ss.ok || lsof.ok || procNet.ok) return []
|
|
280
|
+
logger.warn(
|
|
281
|
+
{
|
|
282
|
+
port,
|
|
283
|
+
ss: ss.ok ? undefined : ss.reason,
|
|
284
|
+
lsof: lsof.ok ? undefined : lsof.reason,
|
|
285
|
+
procNet: procNet.ok ? undefined : procNet.reason,
|
|
286
|
+
},
|
|
287
|
+
"port detection: no port-detection backends available — orphan cleanup may miss stale vites",
|
|
288
|
+
)
|
|
289
|
+
return []
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ponytail: kill a previous-session daemon that's still bound to the WS port
|
|
293
|
+
// (52937). The pid file is the primary source of truth for "is there a
|
|
294
|
+
// daemon running" but it's stale-prone — a SIGKILL'd daemon leaves its
|
|
295
|
+
// pid file behind, while a daemon that crashed during preflight never
|
|
296
|
+
// wrote one. The Linux process title (`/proc/<pid>/comm`) is set by
|
|
297
|
+
// main.ts:23 to `sirenodeck:dm` and survives across pid-file loss. Cross-
|
|
298
|
+
// check the cmdline fingerprint (isOurDaemon) so an unrelated binary
|
|
299
|
+
// named "sirenodeck:dm" by its supervisor doesn't get reaped.
|
|
300
|
+
//
|
|
301
|
+
// We only run this for the WS port — that's the one the new daemon
|
|
302
|
+
// binds FIRST, and the one whose collision most reliably breaks startup.
|
|
303
|
+
// HTTP and frontend ports are bound after; if the daemon reaps itself
|
|
304
|
+
// cleanly the vites go with it (they're parented to the daemon, get
|
|
305
|
+
// reparented to init on exit, and killPortListeners picks them up).
|
|
306
|
+
export const reapStaleDaemon = async (
|
|
307
|
+
wsPort: number,
|
|
308
|
+
logger: pino.Logger,
|
|
309
|
+
): Promise<void> => {
|
|
310
|
+
const pids = detectPortPids(wsPort, logger)
|
|
311
|
+
const daemonPids = pids.filter(isOurDaemon)
|
|
312
|
+
if (daemonPids.length === 0) return
|
|
313
|
+
for (const pid of daemonPids) {
|
|
314
|
+
try {
|
|
315
|
+
process.kill(pid, "SIGTERM")
|
|
316
|
+
logger.warn(
|
|
317
|
+
{ pid, port: wsPort },
|
|
318
|
+
"start: SIGTERM to stale daemon holding WS port (self-heal)",
|
|
319
|
+
)
|
|
320
|
+
} catch (err) {
|
|
321
|
+
logger.warn(
|
|
322
|
+
{ err, pid },
|
|
323
|
+
"start: SIGTERM to stale daemon failed (may already be dead)",
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const deadline = Date.now() + 3_000
|
|
328
|
+
while (Date.now() < deadline) {
|
|
329
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
330
|
+
if (detectPortPids(wsPort, logger).length === 0) return
|
|
331
|
+
}
|
|
332
|
+
// SIGKILL stragglers (only our own daemons — anything else is left
|
|
333
|
+
// alone with a warning so the user can intervene manually).
|
|
334
|
+
for (const pid of detectPortPids(wsPort, logger)) {
|
|
335
|
+
if (isOurDaemon(pid)) {
|
|
336
|
+
try {
|
|
337
|
+
process.kill(pid, "SIGKILL")
|
|
338
|
+
logger.warn(
|
|
339
|
+
{ pid, port: wsPort },
|
|
340
|
+
"start: SIGKILL to stale daemon that ignored SIGTERM",
|
|
341
|
+
)
|
|
342
|
+
} catch {
|
|
343
|
+
// already dead
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
logger.warn(
|
|
347
|
+
{ pid, port: wsPort },
|
|
348
|
+
"start: WS port held by a non-sirenodeck daemon — leaving it alone",
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const killPortListeners = async (
|
|
355
|
+
ports: ReadonlyArray<number>,
|
|
356
|
+
logger: pino.Logger,
|
|
357
|
+
): Promise<void> => {
|
|
358
|
+
const daemonPid = readPid()
|
|
359
|
+
for (const port of ports) {
|
|
360
|
+
const pids = new Set(detectPortPids(port, logger))
|
|
361
|
+
for (const pid of pids) {
|
|
362
|
+
if (!isOurViteChild(pid)) {
|
|
363
|
+
logger.warn(
|
|
364
|
+
{ pid, port },
|
|
365
|
+
"start: port in use by a process that is NOT a sirenodeck child — leaving it alone",
|
|
366
|
+
)
|
|
367
|
+
continue
|
|
368
|
+
}
|
|
369
|
+
if (!cmdlineMentionsCliRoot(readProcCmdline(pid) ?? "")) {
|
|
370
|
+
logger.warn(
|
|
371
|
+
{ pid, port },
|
|
372
|
+
"start: port in use by a vite, but not under packages/cli/ — leaving it alone",
|
|
373
|
+
)
|
|
374
|
+
continue
|
|
375
|
+
}
|
|
376
|
+
if (!isOrphan(pid, daemonPid)) {
|
|
377
|
+
logger.warn(
|
|
378
|
+
{ pid, port },
|
|
379
|
+
"start: port in use by a live (non-orphan) sirenodeck vite — leaving it alone",
|
|
380
|
+
)
|
|
381
|
+
continue
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
process.kill(pid, "SIGTERM")
|
|
385
|
+
logger.warn(
|
|
386
|
+
{ pid, port },
|
|
387
|
+
"start: killed orphan sirenodeck vite bound to daemon port",
|
|
388
|
+
)
|
|
389
|
+
} catch {
|
|
390
|
+
// already dead
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// brief grace so SIGTERM takes effect before the new daemon binds
|
|
395
|
+
await new Promise((r) => setTimeout(r, 500))
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const promptConflict = async (
|
|
399
|
+
pid: number | null,
|
|
400
|
+
): Promise<"restart" | "cancel"> => {
|
|
401
|
+
const who = pid === null ? "" : ` with pid ${pid}`
|
|
402
|
+
if (!process.stdin.isTTY) {
|
|
403
|
+
throw new Error(
|
|
404
|
+
`Daemon already running${who} (non-interactive: not stopping)`,
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
const answer = await select<"restart" | "cancel">({
|
|
408
|
+
message: `Daemon already running${who}.`,
|
|
409
|
+
options: [
|
|
410
|
+
{
|
|
411
|
+
value: "restart",
|
|
412
|
+
label: "restart",
|
|
413
|
+
hint: "Stop the existing daemon and start a new one",
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
value: "cancel",
|
|
417
|
+
label: "cancel",
|
|
418
|
+
hint: "Exit without changes",
|
|
419
|
+
},
|
|
420
|
+
],
|
|
421
|
+
})
|
|
422
|
+
if (isCancel(answer)) return "cancel"
|
|
423
|
+
return answer
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const stopExisting = async (
|
|
427
|
+
pid: number,
|
|
428
|
+
logger: pino.Logger,
|
|
429
|
+
): Promise<void> => {
|
|
430
|
+
if (!isRunning(pid)) {
|
|
431
|
+
logger.warn({ pid }, "existing pid file is stale, removing")
|
|
432
|
+
removePidFile()
|
|
433
|
+
await terminateChildren({ logger, timeoutMs: 2_000 })
|
|
434
|
+
removeChildrenFile()
|
|
435
|
+
removeStartLock()
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
logger.info({ pid }, "stopping existing daemon")
|
|
439
|
+
try {
|
|
440
|
+
process.kill(pid, "SIGTERM")
|
|
441
|
+
} catch (err) {
|
|
442
|
+
logger.warn({ err, pid }, "failed to send SIGTERM to existing daemon")
|
|
443
|
+
}
|
|
444
|
+
const deadline = Date.now() + 5_000
|
|
445
|
+
while (Date.now() < deadline && isRunning(pid)) {
|
|
446
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
447
|
+
}
|
|
448
|
+
if (isRunning(pid)) {
|
|
449
|
+
logger.warn({ pid }, "existing daemon did not exit in 5s, sending SIGKILL")
|
|
450
|
+
try {
|
|
451
|
+
process.kill(pid, "SIGKILL")
|
|
452
|
+
} catch (err) {
|
|
453
|
+
logger.warn({ err, pid }, "failed to send SIGKILL to existing daemon")
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
await terminateChildren({ logger, timeoutMs: 2_000 })
|
|
457
|
+
removePidFile()
|
|
458
|
+
removeDaemonControl(resolveDaemonPaths())
|
|
459
|
+
removeRuntimeStateFile()
|
|
460
|
+
removeChildrenFile()
|
|
461
|
+
removeStartLock()
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const resolveConfigPath = (options: StartOptions): ResolveConfigPathResult => {
|
|
465
|
+
// ponytail: an explicit --config always wins. The cache below exists so
|
|
466
|
+
// `restart` / in-place edits keep the same config the session launched
|
|
467
|
+
// with, but a user (or tooling) that passes --config clearly wants THAT
|
|
468
|
+
// file — honoring the cache over an explicit arg silently swapped in the
|
|
469
|
+
// user's ~/.config/sirenodeck/config.yml instead (the bug that made
|
|
470
|
+
// --config appear to "do nothing").
|
|
471
|
+
if (options.config !== undefined) {
|
|
472
|
+
const explicit = resolveUserPath(options.config)
|
|
473
|
+
if (!existsSync(explicit)) {
|
|
474
|
+
throw new Error(
|
|
475
|
+
`Config file not found: ${explicit}\n` +
|
|
476
|
+
` Fix: pass a valid --config path.`,
|
|
477
|
+
)
|
|
478
|
+
}
|
|
479
|
+
return { path: explicit, source: "cli" }
|
|
480
|
+
}
|
|
481
|
+
// ponytail: the daemon honors the cached pointer first so the running
|
|
482
|
+
// session keeps editing the same config it was launched with. When the
|
|
483
|
+
// cached path is gone (e.g. worktree removed) we fall through to the
|
|
484
|
+
// shared precedence: cli arg → XDG → run folder.
|
|
485
|
+
const cached = readConfigPath()
|
|
486
|
+
const cachedUsable = cached !== null && existsSync(cached) ? cached : null
|
|
487
|
+
if (cachedUsable !== null) {
|
|
488
|
+
return { path: cachedUsable, source: "cli" }
|
|
489
|
+
}
|
|
490
|
+
return resolveRunConfigPath({
|
|
491
|
+
...(options.xdgConfigHome !== undefined
|
|
492
|
+
? { xdgConfigHome: options.xdgConfigHome }
|
|
493
|
+
: {}),
|
|
494
|
+
...(options.homeDir !== undefined ? { homeDir: options.homeDir } : {}),
|
|
495
|
+
logger: options.logger,
|
|
496
|
+
})
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const buildRuntimeFlags = (options: StartOptions): RuntimeFlags => ({
|
|
500
|
+
emulator: options.emulator === true || options.remote === true,
|
|
501
|
+
remote: options.remote,
|
|
502
|
+
httpPort: options.httpPort ?? 3939,
|
|
503
|
+
...(options.config !== undefined ? { config: options.config } : {}),
|
|
504
|
+
...(options.deviceModel !== undefined
|
|
505
|
+
? { deviceModel: options.deviceModel }
|
|
506
|
+
: {}),
|
|
507
|
+
...(options.port !== undefined ? { port: options.port } : {}),
|
|
508
|
+
...(options.noAutoOpen !== undefined
|
|
509
|
+
? { noAutoOpen: options.noAutoOpen }
|
|
510
|
+
: {}),
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
const runInProcess = async (options: StartOptions): Promise<void> => {
|
|
514
|
+
const { logger } = options
|
|
515
|
+
|
|
516
|
+
// ponytail: the single-instance lock comes FIRST — before the start lock,
|
|
517
|
+
// before preflight, and above all before killPortListeners. Everything below
|
|
518
|
+
// this point assumes the ports, the children and the pid file belong to us;
|
|
519
|
+
// a daemon that does not hold the lock has no business cleaning any of them
|
|
520
|
+
// up, because they belong to the daemon that does. Holding the lock for the
|
|
521
|
+
// whole process lifetime is what makes a second daemon impossible rather
|
|
522
|
+
// than merely unlikely: the kernel, not a file we wrote, is the referee.
|
|
523
|
+
const instance = await acquireInstanceLock(resolveDaemonPaths(), logger)
|
|
524
|
+
if (instance.kind === "busy") {
|
|
525
|
+
throw new Error(
|
|
526
|
+
instance.holderPid === null
|
|
527
|
+
? "another sirenodeck daemon is already running"
|
|
528
|
+
: `another sirenodeck daemon is already running (pid ${instance.holderPid}) — stop it with \`sireno stop\`, or restart it with \`sireno restart\``,
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const startLock = acquireStartLock()
|
|
533
|
+
if (startLock === null) {
|
|
534
|
+
instance.lock.release()
|
|
535
|
+
throw new Error("another start is already in progress")
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const releaseAll = (): void => {
|
|
539
|
+
startLock.release()
|
|
540
|
+
instance.lock.release()
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ponytail: the start lock is only released by the post-runPipeline
|
|
544
|
+
// .finally on a clean pipeline exit. But preflight/setup code below
|
|
545
|
+
// (config read, addon scan, http server boot) can throw EADDRINUSE or
|
|
546
|
+
// similar BEFORE runPipeline ever starts, leaving the lock orphaned
|
|
547
|
+
// for 60s — which is exactly the user's "another start is already in
|
|
548
|
+
// progress" error after a previous start crashed. Wrap the setup in a
|
|
549
|
+
// try/catch that releases on throw, and also call release() in the
|
|
550
|
+
// existing .finally so the runtime-exit path stays covered.
|
|
551
|
+
try {
|
|
552
|
+
await runInProcessSetup(options, logger, releaseAll)
|
|
553
|
+
} catch (err) {
|
|
554
|
+
releaseAll()
|
|
555
|
+
throw err
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const runInProcessSetup = async (
|
|
560
|
+
options: StartOptions,
|
|
561
|
+
logger: pino.Logger,
|
|
562
|
+
releaseLock: () => void,
|
|
563
|
+
): Promise<void> => {
|
|
564
|
+
const resolved = resolveConfigPath(options)
|
|
565
|
+
const configPath = resolved.path
|
|
566
|
+
options.logger.info(
|
|
567
|
+
{ configPath, source: resolved.source },
|
|
568
|
+
`start: using config ${configPath}`,
|
|
569
|
+
)
|
|
570
|
+
// ponytail: an explicit `start` always wins — never replay the persisted
|
|
571
|
+
// flags file here. The file exists so `restart` can reuse the last mode;
|
|
572
|
+
// letting it also override a fresh invocation made one past `--emulator`
|
|
573
|
+
// run force every later plain `p dev start` into emulator mode.
|
|
574
|
+
const runtimeFlags = buildRuntimeFlags(options)
|
|
575
|
+
writeConfigPath(configPath)
|
|
576
|
+
writeFlags(runtimeFlags)
|
|
577
|
+
|
|
578
|
+
const builtinAddons = (await collectBuiltinAddonRegistry()).scanned
|
|
579
|
+
const allScanned: ScannedAddon[] = [...builtinAddons]
|
|
580
|
+
|
|
581
|
+
const runOptions = toRunOptions(
|
|
582
|
+
{
|
|
583
|
+
...options,
|
|
584
|
+
config: configPath,
|
|
585
|
+
port: runtimeFlags.port,
|
|
586
|
+
emulator: runtimeFlags.emulator,
|
|
587
|
+
remote: runtimeFlags.remote,
|
|
588
|
+
deviceModel: runtimeFlags.deviceModel,
|
|
589
|
+
httpPort: runtimeFlags.httpPort,
|
|
590
|
+
},
|
|
591
|
+
(pids) => {
|
|
592
|
+
writeChildren({ pids: [...pids] })
|
|
593
|
+
logger.info({ pids }, "daemon: tracked children")
|
|
594
|
+
},
|
|
595
|
+
(addons) => {
|
|
596
|
+
allScanned.length = 0
|
|
597
|
+
allScanned.push(...addons)
|
|
598
|
+
},
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
await preflight(runOptions)
|
|
602
|
+
|
|
603
|
+
writePid(process.pid)
|
|
604
|
+
const token = generateToken()
|
|
605
|
+
const daemonPaths = resolveDaemonPaths()
|
|
606
|
+
const control = await startDaemonControl(token, daemonPaths, logger)
|
|
607
|
+
process.env["SIRENO_TOKEN"] = token
|
|
608
|
+
logger.info({ tokenLen: token.length }, "daemon: pid + token written")
|
|
609
|
+
|
|
610
|
+
let httpServer: RunningHttpServer | null = null
|
|
611
|
+
const distDir = resolveFrontendDist()
|
|
612
|
+
const indexPath = join(distDir, "index.html")
|
|
613
|
+
if (existsSync(indexPath)) {
|
|
614
|
+
try {
|
|
615
|
+
httpServer = await startHttpServer({
|
|
616
|
+
port: runtimeFlags.httpPort,
|
|
617
|
+
distDir,
|
|
618
|
+
getToken: () => token,
|
|
619
|
+
logger,
|
|
620
|
+
getConfigContent: () => {
|
|
621
|
+
try {
|
|
622
|
+
return readFileSync(configPath, "utf8")
|
|
623
|
+
} catch {
|
|
624
|
+
return null
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
getConfigPath: () => configPath,
|
|
628
|
+
getAddons: () =>
|
|
629
|
+
allScanned.map((s) => ({
|
|
630
|
+
name: s.name,
|
|
631
|
+
path: s.path ?? join(builtinDir, s.name),
|
|
632
|
+
internal: s.internal === true,
|
|
633
|
+
source: s.source,
|
|
634
|
+
buttonTypes: Object.entries(s.buttonTypes).map(([type, info]) => ({
|
|
635
|
+
type,
|
|
636
|
+
internal: info.internal,
|
|
637
|
+
})),
|
|
638
|
+
defaultButton: null,
|
|
639
|
+
decks: s.decks.map((d) => ({
|
|
640
|
+
id: d.id,
|
|
641
|
+
isOverlay: false,
|
|
642
|
+
paginated: d.paginated,
|
|
643
|
+
buttons: d.buttons,
|
|
644
|
+
internal: d.internal,
|
|
645
|
+
})),
|
|
646
|
+
})),
|
|
647
|
+
})
|
|
648
|
+
} catch (err) {
|
|
649
|
+
logger.warn(
|
|
650
|
+
{ err },
|
|
651
|
+
"daemon: failed to start http server, continuing without it",
|
|
652
|
+
)
|
|
653
|
+
httpServer = null
|
|
654
|
+
}
|
|
655
|
+
} else if (!isDevInvocation()) {
|
|
656
|
+
logger.warn(
|
|
657
|
+
{ distDir },
|
|
658
|
+
"daemon: frontend dist not found, skipping http server (run `pnpm build` first for the prod HTTP server)",
|
|
659
|
+
)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
void runPipeline(runOptions)
|
|
663
|
+
.catch((err: unknown) => {
|
|
664
|
+
logger.error({ err }, "background run failed")
|
|
665
|
+
})
|
|
666
|
+
.finally(async () => {
|
|
667
|
+
releaseLock()
|
|
668
|
+
if (httpServer !== null) {
|
|
669
|
+
try {
|
|
670
|
+
await httpServer.stop()
|
|
671
|
+
logger.info("daemon: http server stopped")
|
|
672
|
+
} catch (err) {
|
|
673
|
+
logger.warn({ err }, "daemon: http server stop failed")
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
await new Promise<void>((resolve) => control.close(() => resolve()))
|
|
677
|
+
removeDaemonControl(daemonPaths)
|
|
678
|
+
// ponytail: kill tracked children (frontend vite, config UI vite, ...)
|
|
679
|
+
// BEFORE removing the children file. Without this, when the daemon
|
|
680
|
+
// exits cleanly the children outlive it as init-adopted orphans, keep
|
|
681
|
+
// their ports, and the next `start` invocation fails with EADDRINUSE
|
|
682
|
+
// — exactly the user's "the frontend port still in use by children"
|
|
683
|
+
// symptom. TerminateChildren sends SIGTERM (then SIGKILL after 2s)
|
|
684
|
+
// and is awaited so the operation completes before process.exit.
|
|
685
|
+
await terminateChildren({ logger, timeoutMs: 3_000 })
|
|
686
|
+
removePidFile()
|
|
687
|
+
removeDaemonControl(resolveDaemonPaths())
|
|
688
|
+
removeRuntimeStateFile()
|
|
689
|
+
removeChildrenFile()
|
|
690
|
+
removeStartLock()
|
|
691
|
+
logger.info("daemon: shutdown complete")
|
|
692
|
+
// ponytail: explicit exit — without this, lingering handles in the
|
|
693
|
+
// emulator's active-app polling or lingering ws-bridge connections keep
|
|
694
|
+
// the event loop alive and the daemon process never terminates after
|
|
695
|
+
// a startup failure. The systemd / fork-off flows don't go through
|
|
696
|
+
// runInProcess, so this only affects the in-process daemon path.
|
|
697
|
+
process.exit(process.exitCode ?? 0)
|
|
698
|
+
})
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// ponytail: dev-mode `start`. Production goes through systemd/launchd — the
|
|
702
|
+
// OS owns the daemon lifecycle and the wrapper exits as soon as `start` forks
|
|
703
|
+
// the service manager. Dev's `pnpm dev start` mirrors that shape: spawn the
|
|
704
|
+
// daemon via `spawnDetached`, write the pid file, return to the cli. The
|
|
705
|
+
// wrapper exits cleanly so `pnpm dev status | stop | restart | reload | logs`
|
|
706
|
+
// from any other shell work against the same pid-file contract production
|
|
707
|
+
// uses. No auto-restart: the daemon stays dead on crash, the operator runs
|
|
708
|
+
// `pnpm dev restart` to recover — same as production (systemd eventually
|
|
709
|
+
// gives up too).
|
|
710
|
+
const resolveCliRoot = (): string => {
|
|
711
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
712
|
+
return resolvePath(here, "..", "..", "..")
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const buildDetachedArgs = (flags: RuntimeFlags): string[] => {
|
|
716
|
+
const args: string[] = ["start"]
|
|
717
|
+
if (flags.emulator) args.push("--emulator")
|
|
718
|
+
if (flags.remote) args.push("--remote")
|
|
719
|
+
if (flags.port !== undefined) args.push("--port", String(flags.port))
|
|
720
|
+
if (flags.config !== undefined) args.push("--config", flags.config)
|
|
721
|
+
if (flags.noAutoOpen === true) args.push("--no-autoopen")
|
|
722
|
+
if (flags.deviceModel !== undefined) {
|
|
723
|
+
args.push("--device-model", flags.deviceModel)
|
|
724
|
+
}
|
|
725
|
+
if (flags.httpPort !== undefined && flags.httpPort !== 3939) {
|
|
726
|
+
args.push("--http-port", String(flags.httpPort))
|
|
727
|
+
}
|
|
728
|
+
return args
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const startInBackground = async (options: StartOptions): Promise<void> => {
|
|
732
|
+
const { logger } = options
|
|
733
|
+
const resolved = resolveConfigPath(options)
|
|
734
|
+
const configPath = resolved.path
|
|
735
|
+
logger.info(
|
|
736
|
+
{ configPath, source: resolved.source },
|
|
737
|
+
`start: using config ${configPath}`,
|
|
738
|
+
)
|
|
739
|
+
const runtimeFlags = buildRuntimeFlags(options)
|
|
740
|
+
writeConfigPath(configPath)
|
|
741
|
+
writeFlags(runtimeFlags)
|
|
742
|
+
pruneStaleChildren(undefined, logger)
|
|
743
|
+
await terminateChildren({ logger, timeoutMs: 2_000 })
|
|
744
|
+
|
|
745
|
+
// ponytail: in dev mode (pnpm dev), the daemon should run from
|
|
746
|
+
// SOURCE via the dev wrapper (bin/dev.js + tsx), not the bundled
|
|
747
|
+
// dist/main.mjs. The bundle strips runtime assets like the `default`
|
|
748
|
+
// and `light` themes at packages/cli/src/themes/ — the loader's
|
|
749
|
+
// filesystem scan can't find them inside dist/, so any `theme:`
|
|
750
|
+
// reference in config.yml resolves to "not a registered theme, not
|
|
751
|
+
// a path, and not a known npm package" and crashes with
|
|
752
|
+
// unhandledRejection. The dev wrapper runs the source path and
|
|
753
|
+
// keeps the themes on disk for the loader to discover.
|
|
754
|
+
const binPath = resolvePath(
|
|
755
|
+
resolveCliRoot(),
|
|
756
|
+
"bin",
|
|
757
|
+
isDevInvocation() ? "dev.js" : "sirenodeck.js",
|
|
758
|
+
)
|
|
759
|
+
const args = buildDetachedArgs(runtimeFlags)
|
|
760
|
+
const { pid } = spawnDetached({
|
|
761
|
+
binPath,
|
|
762
|
+
args,
|
|
763
|
+
remote: options.remote === true,
|
|
764
|
+
})
|
|
765
|
+
writePid(pid)
|
|
766
|
+
logger.info(
|
|
767
|
+
{ pid, configPath, args },
|
|
768
|
+
"start: daemon spawned, returning to cli",
|
|
769
|
+
)
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const startProduction = async (options: StartOptions): Promise<void> => {
|
|
773
|
+
const { logger } = options
|
|
774
|
+
const resolved = resolveConfigPath(options)
|
|
775
|
+
const configPath = resolved.path
|
|
776
|
+
logger.info(
|
|
777
|
+
{ configPath, source: resolved.source },
|
|
778
|
+
`start: using config ${configPath}`,
|
|
779
|
+
)
|
|
780
|
+
const runtimeFlags = buildRuntimeFlags(options)
|
|
781
|
+
writeConfigPath(configPath)
|
|
782
|
+
writeFlags(runtimeFlags)
|
|
783
|
+
|
|
784
|
+
// ponytail: startup banner (printed by the command handler) replaces what
|
|
785
|
+
// used to be a spinner here. The handler's waitForDaemonReady prints the
|
|
786
|
+
// outro when the daemon is actually serving.
|
|
787
|
+
await ensureInstalled({
|
|
788
|
+
logger,
|
|
789
|
+
...(options.system === true ? { system: true } : {}),
|
|
790
|
+
})
|
|
791
|
+
await invokeManager({ action: "restart", logger })
|
|
792
|
+
|
|
793
|
+
const paths = resolveDaemonPaths()
|
|
794
|
+
const deadline = Date.now() + 5_000
|
|
795
|
+
let pid: number | null = null
|
|
796
|
+
while (Date.now() < deadline) {
|
|
797
|
+
pid = readPid(paths)
|
|
798
|
+
if (pid !== null && isRunning(pid)) break
|
|
799
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
800
|
+
}
|
|
801
|
+
logger.info(
|
|
802
|
+
{ childPid: pid, configPath },
|
|
803
|
+
pid !== null
|
|
804
|
+
? "start: daemon started via service manager"
|
|
805
|
+
: "start: daemon started (pid not yet visible)",
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
if (options.logs === true && process.exitCode !== 1) {
|
|
809
|
+
const logPath = `${paths.runtimeDir}/service.log`
|
|
810
|
+
await tailLogs({ logPath, follow: true, lines: 50 })
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const probeSystemForFirstRun = async (
|
|
815
|
+
options: StartOptions,
|
|
816
|
+
): Promise<{
|
|
817
|
+
report: SystemReport
|
|
818
|
+
summary: ReturnType<typeof summarizeReport>
|
|
819
|
+
} | null> => {
|
|
820
|
+
const home = options.homeDir ?? process.env["HOME"] ?? ""
|
|
821
|
+
const xdgConfigHome =
|
|
822
|
+
options.xdgConfigHome ?? process.env["XDG_CONFIG_HOME"] ?? `${home}/.config`
|
|
823
|
+
const baseDeps = buildStandardProbeDeps()
|
|
824
|
+
// ponytail: probe the config this start will actually use. `--config` and the
|
|
825
|
+
// cached daemon pointer both count; only when neither resolves does the probe
|
|
826
|
+
// fall back to the XDG default (which is also the wizard's seed target).
|
|
827
|
+
let configPath: string | undefined
|
|
828
|
+
try {
|
|
829
|
+
configPath = resolveConfigPath(options).path
|
|
830
|
+
} catch {
|
|
831
|
+
configPath = undefined
|
|
832
|
+
}
|
|
833
|
+
try {
|
|
834
|
+
const report = await probeAllCached({
|
|
835
|
+
...baseDeps,
|
|
836
|
+
homeDir: home !== "" ? home : baseDeps.homeDir,
|
|
837
|
+
xdgConfigHome,
|
|
838
|
+
...(configPath !== undefined ? { configPath } : {}),
|
|
839
|
+
})
|
|
840
|
+
return { report, summary: summarizeReport(report) }
|
|
841
|
+
} catch {
|
|
842
|
+
return null
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const runFirstRunCheckIfNeeded = async (
|
|
847
|
+
options: StartOptions,
|
|
848
|
+
logger: pino.Logger,
|
|
849
|
+
): Promise<boolean> => {
|
|
850
|
+
const probed = await probeSystemForFirstRun(options)
|
|
851
|
+
if (probed === null) return true
|
|
852
|
+
const { summary } = probed
|
|
853
|
+
// Optional host integrations (clipboard and active-app detection) are not
|
|
854
|
+
// startup requirements. Hardware readiness is checked by preflight after
|
|
855
|
+
// this informational probe; config and udev are the only setup blockers.
|
|
856
|
+
const missing = summary.udevMissing || summary.configMissing
|
|
857
|
+
if (!missing) return true
|
|
858
|
+
|
|
859
|
+
if (process.env["SIRENO_SKIP_WIZARD"]) {
|
|
860
|
+
logger.warn(
|
|
861
|
+
{ lines: summary.lines },
|
|
862
|
+
"start: some requirements still missing — run `sirenodeck system-requirements` to fix",
|
|
863
|
+
)
|
|
864
|
+
return false
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (!process.stdin.isTTY) {
|
|
868
|
+
logger.warn(
|
|
869
|
+
{ lines: summary.lines },
|
|
870
|
+
"start: missing requirements and no TTY — run `sirenodeck system-requirements` interactively to fix",
|
|
871
|
+
)
|
|
872
|
+
process.exitCode = 1
|
|
873
|
+
return false
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const shouldRunWizard = await confirm({
|
|
877
|
+
message:
|
|
878
|
+
"It looks like this is your first run (missing config or system capabilities). Run the setup wizard now?",
|
|
879
|
+
initialValue: true,
|
|
880
|
+
})
|
|
881
|
+
if (!shouldRunWizard) {
|
|
882
|
+
logger.warn(
|
|
883
|
+
{ lines: summary.lines },
|
|
884
|
+
"start: requirements missing — continuing anyway. Run `sirenodeck system-requirements` later to fix.",
|
|
885
|
+
)
|
|
886
|
+
return true
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
await systemRequirements({
|
|
890
|
+
logger,
|
|
891
|
+
configPath: resolveConfigPath(options).path,
|
|
892
|
+
...(options.homeDir !== undefined ? { homeDir: options.homeDir } : {}),
|
|
893
|
+
...(options.xdgConfigHome !== undefined
|
|
894
|
+
? { xdgConfigHome: options.xdgConfigHome }
|
|
895
|
+
: {}),
|
|
896
|
+
})
|
|
897
|
+
|
|
898
|
+
const reprobed = await probeSystemForFirstRun(options)
|
|
899
|
+
if (reprobed === null) return false
|
|
900
|
+
const stillMissing =
|
|
901
|
+
reprobed.summary.udevMissing || reprobed.summary.configMissing
|
|
902
|
+
if (stillMissing) {
|
|
903
|
+
logger.warn(
|
|
904
|
+
{ lines: reprobed.summary.lines },
|
|
905
|
+
"start: some requirements still missing — exiting",
|
|
906
|
+
)
|
|
907
|
+
process.exitCode = 1
|
|
908
|
+
return false
|
|
909
|
+
}
|
|
910
|
+
return true
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const start = async (options: StartOptions): Promise<void> => {
|
|
914
|
+
const { logger } = options
|
|
915
|
+
|
|
916
|
+
// ponytail: skip the first-run wizard in the daemon child. The forked
|
|
917
|
+
// daemon has `SIRENO_DAEMON_CHILD=1` and `stdio: ["ignore", "pipe", "pipe"]`
|
|
918
|
+
// — stdin is closed, so the wizard's `!process.stdin.isTTY` branch fires,
|
|
919
|
+
// sets `process.exitCode = 1`, and the daemon's `process.exit(exitCode)`
|
|
920
|
+
// then exits 1. The supervisor reads that as "unexpected exit" and respawns
|
|
921
|
+
// the child, which re-runs the wizard, exits 1, respawns — visible to the
|
|
922
|
+
// operator as the CLI restarting in a tight loop. The parent already ran
|
|
923
|
+
// the wizard before forking, so the child has no business re-running it.
|
|
924
|
+
if (isUnderServiceManager()) {
|
|
925
|
+
await runInProcess(options)
|
|
926
|
+
return
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (!(await runFirstRunCheckIfNeeded(options, logger))) return
|
|
930
|
+
|
|
931
|
+
await onboardCodingAgents(resolveConfigPath(options).path)
|
|
932
|
+
|
|
933
|
+
// ponytail: settle the "is a daemon already running?" question BEFORE any
|
|
934
|
+
// reaping. The old order reaped first and asked afterwards, which is only
|
|
935
|
+
// safe while the reap is accurate — and on macOS it was not: every identity
|
|
936
|
+
// check read /proc, found nothing, and classified the running daemon's own
|
|
937
|
+
// children as strangers. Asking first also means the answer can come from
|
|
938
|
+
// the instance lock, which is the one source that cannot be stale: a daemon
|
|
939
|
+
// holds it for exactly as long as it is alive.
|
|
940
|
+
const existing = readPid()
|
|
941
|
+
const lockHeld = await isSocketLive(instanceLockPath(resolveDaemonPaths()))
|
|
942
|
+
if (lockHeld || (existing !== null && isRunning(existing))) {
|
|
943
|
+
const action = await promptConflict(existing)
|
|
944
|
+
if (action === "cancel") {
|
|
945
|
+
logger.info("start cancelled")
|
|
946
|
+
return
|
|
947
|
+
}
|
|
948
|
+
// ponytail: a supervised daemon cannot be restarted by killing it. launchd
|
|
949
|
+
// with KeepAlive brings it back in about two seconds — measured — and the
|
|
950
|
+
// daemon we would spawn here loses the race for the instance lock and dies
|
|
951
|
+
// with "another sirenodeck daemon is already running". Before the lock
|
|
952
|
+
// existed the same race simply produced TWO daemons, which is how this
|
|
953
|
+
// machine ended up with one from 09:39 and another from 16:54 fighting
|
|
954
|
+
// over the same ports. The manager owns the process, so the manager is
|
|
955
|
+
// what restarts it; the config and flags go through the state files, the
|
|
956
|
+
// same way `startProduction` hands them over.
|
|
957
|
+
if (isUnitInstalled()) {
|
|
958
|
+
const resolvedRestart = resolveConfigPath(options)
|
|
959
|
+
writeConfigPath(resolvedRestart.path)
|
|
960
|
+
writeFlags(buildRuntimeFlags(options))
|
|
961
|
+
logger.info(
|
|
962
|
+
{ configPath: resolvedRestart.path },
|
|
963
|
+
"start: daemon is service-managed — restarting through the service manager",
|
|
964
|
+
)
|
|
965
|
+
await invokeManager({ action: "restart", logger })
|
|
966
|
+
const deadline = Date.now() + 10_000
|
|
967
|
+
let restarted: number | null = null
|
|
968
|
+
while (Date.now() < deadline) {
|
|
969
|
+
await new Promise((r) => setTimeout(r, 200))
|
|
970
|
+
const pid = readPid()
|
|
971
|
+
if (pid !== null && pid !== existing && isRunning(pid)) {
|
|
972
|
+
restarted = pid
|
|
973
|
+
break
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
logger.info(
|
|
977
|
+
{ pid: restarted },
|
|
978
|
+
restarted === null
|
|
979
|
+
? "start: service manager restarted the daemon (pid not yet visible)"
|
|
980
|
+
: "start: daemon restarted by the service manager",
|
|
981
|
+
)
|
|
982
|
+
return
|
|
983
|
+
}
|
|
984
|
+
if (existing !== null) await stopExisting(existing, logger)
|
|
985
|
+
else
|
|
986
|
+
// The lock is held but nothing wrote a pid file. There is no process to
|
|
987
|
+
// signal by pid, so the port reap below is what clears it.
|
|
988
|
+
logger.warn(
|
|
989
|
+
"start: a daemon holds the instance lock but left no pid file — falling back to the port reap",
|
|
990
|
+
)
|
|
991
|
+
removePidFile()
|
|
992
|
+
removeDaemonControl(resolveDaemonPaths())
|
|
993
|
+
removeRuntimeStateFile()
|
|
994
|
+
removeChildrenFile()
|
|
995
|
+
removeStartLock()
|
|
996
|
+
} else {
|
|
997
|
+
// ponytail: previous daemon is dead (or never started cleanly) — kill any
|
|
998
|
+
// children it left behind (frontend vite on :5180, config UI vite on :52938,
|
|
999
|
+
// ws-bridge on :52937). Without this, the children's ports stay bound
|
|
1000
|
+
// by orphans and the new daemon's preflight can't bind them — user's
|
|
1001
|
+
// complaint: "the frontend port still in use by children". terminateChildren
|
|
1002
|
+
// sends SIGTERM, waits 2s, then SIGKILL on stragglers.
|
|
1003
|
+
if (existing !== null) {
|
|
1004
|
+
logger.warn(
|
|
1005
|
+
{ pid: existing },
|
|
1006
|
+
"start: existing daemon pid is dead — cleaning up before starting",
|
|
1007
|
+
)
|
|
1008
|
+
}
|
|
1009
|
+
await terminateChildren({ logger, timeoutMs: 2_000 })
|
|
1010
|
+
pruneStaleChildren(undefined, logger)
|
|
1011
|
+
if (existing !== null) {
|
|
1012
|
+
removePidFile()
|
|
1013
|
+
removeDaemonControl(resolveDaemonPaths())
|
|
1014
|
+
removeRuntimeStateFile()
|
|
1015
|
+
removeChildrenFile()
|
|
1016
|
+
removeStartLock()
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// ponytail: if a previous-session `sirenodeck:dm` daemon is still bound to
|
|
1021
|
+
// the WS port (52937), reap it before spawning. The pid file is stale-
|
|
1022
|
+
// prone (SIGKILL'd daemons leave it behind; crashed preflight daemons
|
|
1023
|
+
// never wrote one). Identity check uses /proc/<pid>/comm === "sirenodeck:dm"
|
|
1024
|
+
// cross-checked with the sirenodeck cmdline fingerprint, so unrelated
|
|
1025
|
+
// processes holding 52937 (a Discord voice call, an `nc -l`, the user's
|
|
1026
|
+
// own server) get left alone. The WS port is the load-bearing one —
|
|
1027
|
+
// EADDRINUSE there causes the daemon's runPipeline to fail before the
|
|
1028
|
+
// HTTP / frontend ports are even reached.
|
|
1029
|
+
await reapStaleDaemon(options.port ?? 52937, logger)
|
|
1030
|
+
|
|
1031
|
+
// ponytail: clear the daemon's expected ports before spawning. This is
|
|
1032
|
+
// the load-bearing fix for "the frontend port is in use by children":
|
|
1033
|
+
// even when the previous daemon's children file is gone (e.g. after a
|
|
1034
|
+
// SIGKILL that left orphans adopted by systemd), the port still binds
|
|
1035
|
+
// the orphan. ss -ltnp → SIGTERM every listener → 500ms grace → start.
|
|
1036
|
+
// Cost: bounded, no-op when the ports are free.
|
|
1037
|
+
await killPortListeners(
|
|
1038
|
+
options.emulator === true
|
|
1039
|
+
? [5180, 3939, 52937, 52938]
|
|
1040
|
+
: [5180, 3939, 52937],
|
|
1041
|
+
logger,
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
// ponytail: a SIGKILL'd or crashed daemon leaves runtime-state.json behind
|
|
1045
|
+
// (no cleanup ran) — and when there was no pid file at all, the branches
|
|
1046
|
+
// above never removed it. The next start's waitForRuntimeState would read
|
|
1047
|
+
// that stale state (old token, old remote flag, old URLs) before the new
|
|
1048
|
+
// daemon writes its own — surfacing e.g. the LAN/QR banner from a previous
|
|
1049
|
+
// `--remote` session during a plain `--emulator` start. Remove it
|
|
1050
|
+
// unconditionally: the daemon writes a fresh file when its pipeline is
|
|
1051
|
+
// ready, so the CLI only ever sees the state of the daemon it spawned.
|
|
1052
|
+
removeRuntimeStateFile()
|
|
1053
|
+
|
|
1054
|
+
if (isDevInvocation()) {
|
|
1055
|
+
await startInBackground(options)
|
|
1056
|
+
} else {
|
|
1057
|
+
await startProduction(options)
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
export default start
|