@pellux/goodvibes-tui 0.19.49 → 0.19.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,29 @@ All notable changes to GoodVibes TUI.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.19.51] — 2026-04-28
8
+
9
+ ### Changed
10
+ - Updated `@pellux/goodvibes-sdk` to `0.26.6`.
11
+ - Embedded and standalone daemon hosts now wrap Bun request handlers so unexpected daemon/listener route exceptions return bounded JSON errors and are logged instead of printing raw stack/source output into the TUI terminal.
12
+
13
+ ### Fixed
14
+ - Picked up SDK Home Assistant Home Graph snapshot sync fixes for Home Assistant-native snake_case registry fields, incomplete registry objects, and JSON error responses from Home Graph admin routes.
15
+
16
+ ---
17
+
18
+ ## [0.19.50] — 2026-04-28
19
+
20
+ ### Changed
21
+ - Updated `@pellux/goodvibes-sdk` to `0.26.5`.
22
+ - TUI external-service inspection now uses SDK `daemonStatus` and `httpListenerStatus` startup modes to distinguish embedded services, verified external daemons, blocked ports, disabled services, and unavailable services.
23
+
24
+ ### Fixed
25
+ - Treats a verified external GoodVibes daemon on the configured host/port as active instead of reporting daemon activation failure during onboarding.
26
+ - Reports SDK-provided blocked/unavailable startup reasons in onboarding runtime verification warnings.
27
+
28
+ ---
29
+
7
30
  ## [0.19.49] — 2026-04-28
8
31
 
9
32
  ### Changed
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml/badge.svg)](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Version](https://img.shields.io/badge/version-0.19.49-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
5
+ [![Version](https://img.shields.io/badge/version-0.19.51-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
6
6
 
7
7
  A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
8
8
 
@@ -3,7 +3,7 @@
3
3
  "product": {
4
4
  "id": "goodvibes",
5
5
  "surface": "operator",
6
- "version": "0.26.4"
6
+ "version": "0.26.6"
7
7
  },
8
8
  "auth": {
9
9
  "modes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "0.19.49",
3
+ "version": "0.19.51",
4
4
  "description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
5
5
  "type": "module",
6
6
  "main": "src/main.ts",
@@ -91,7 +91,7 @@
91
91
  "@anthropic-ai/vertex-sdk": "^0.16.0",
92
92
  "@ast-grep/napi": "^0.42.0",
93
93
  "@aws/bedrock-token-generator": "^1.1.0",
94
- "@pellux/goodvibes-sdk": "0.26.4",
94
+ "@pellux/goodvibes-sdk": "0.26.6",
95
95
  "bash-language-server": "^5.6.0",
96
96
  "fuse.js": "^7.1.0",
97
97
  "graphql": "^16.13.2",
package/src/daemon/cli.ts CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  loadPersistedProviders,
27
27
  persistProviders,
28
28
  } from '@pellux/goodvibes-sdk/platform/discovery/index';
29
+ import { createSafeHostServeFactory } from './safe-serve.ts';
29
30
 
30
31
  import {
31
32
  parseGoodVibesCli,
@@ -195,11 +196,17 @@ async function main(): Promise<void> {
195
196
  });
196
197
 
197
198
  const userAuth = runtimeServices.localUserAuthManager;
198
- const daemon = new DaemonServer({ runtimeBus, userAuth, runtimeServices });
199
+ const daemon = new DaemonServer({
200
+ runtimeBus,
201
+ userAuth,
202
+ runtimeServices,
203
+ serveFactory: createSafeHostServeFactory('Standalone daemon'),
204
+ });
199
205
  const listener = new HttpListener({
200
206
  hookDispatcher: runtimeServices.hookDispatcher,
201
207
  userAuth,
202
208
  configManager: config,
209
+ serveFactory: createSafeHostServeFactory('Standalone HTTP listener'),
203
210
  });
204
211
  const { daemonToken, httpToken } = readDaemonCliTokens(process.env);
205
212
 
@@ -0,0 +1,61 @@
1
+ import { logger } from '@pellux/goodvibes-sdk/platform/utils/logger';
2
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-display';
3
+
4
+ type HostServeFetch = (
5
+ request: Request,
6
+ server: unknown,
7
+ ) => Response | undefined | Promise<Response | undefined>;
8
+
9
+ type HostServeOptions = Parameters<typeof Bun.serve>[0] & {
10
+ fetch?: HostServeFetch;
11
+ };
12
+
13
+ function requestPath(request: Request): string {
14
+ try {
15
+ return new URL(request.url).pathname;
16
+ } catch {
17
+ return request.url;
18
+ }
19
+ }
20
+
21
+ export function createHostRequestFailureResponse(
22
+ surface: string,
23
+ request: Request,
24
+ error: unknown,
25
+ ): Response {
26
+ const message = summarizeError(error);
27
+ logger.error(`${surface}: request handler failed`, {
28
+ method: request.method,
29
+ path: requestPath(request),
30
+ error: message,
31
+ });
32
+ return Response.json({
33
+ error: message,
34
+ code: 'HOST_REQUEST_HANDLER_FAILED',
35
+ }, { status: 500 });
36
+ }
37
+
38
+ export function createSafeHostServeFactory(
39
+ surface: string,
40
+ baseServeFactory: typeof Bun.serve = Bun.serve,
41
+ ): typeof Bun.serve {
42
+ return ((options: HostServeOptions) => {
43
+ const originalFetch = options.fetch;
44
+ if (typeof originalFetch !== 'function') {
45
+ return baseServeFactory(options as Parameters<typeof Bun.serve>[0]);
46
+ }
47
+
48
+ const wrappedFetch: HostServeFetch = async (request, server) => {
49
+ try {
50
+ return await originalFetch(request, server);
51
+ } catch (error) {
52
+ return createHostRequestFailureResponse(surface, request, error);
53
+ }
54
+ };
55
+
56
+ return baseServeFactory({
57
+ ...options,
58
+ fetch: wrappedFetch,
59
+ } as Parameters<typeof Bun.serve>[0]);
60
+ }) as typeof Bun.serve;
61
+ }
@@ -9,6 +9,15 @@ import type { OnboardingApplyRequest, OnboardingVerificationItem } from '../runt
9
9
  import type { ModelPickerTarget } from './model-picker.ts';
10
10
  import { captureOnboardingWizardSnapshot, restoreOnboardingWizardSnapshot } from './handler-ui-state.ts';
11
11
  import type { InputHandler } from './handler.ts';
12
+ import {
13
+ formatRuntimeActiveSuccessMessage,
14
+ getRuntimeEndpointStatus,
15
+ isRuntimeEndpointActive,
16
+ isRuntimeEndpointOccupyingConfiguredPort,
17
+ runtimePortDiagnostic,
18
+ type OnboardingExternalServiceState,
19
+ type OnboardingRuntimeEndpoint,
20
+ } from './onboarding/onboarding-runtime-status.ts';
12
21
 
13
22
  export interface OnboardingRuntimePosture {
14
23
  readonly serviceEnabled: boolean;
@@ -20,15 +29,6 @@ export interface OnboardingRuntimePosture {
20
29
  readonly remoteExposure: boolean;
21
30
  }
22
31
 
23
- interface OnboardingExternalServiceState {
24
- readonly daemonRunning?: boolean;
25
- readonly daemonPortInUse?: boolean;
26
- readonly httpListenerRunning?: boolean;
27
- readonly httpListenerPortInUse?: boolean;
28
- }
29
-
30
- type OnboardingRuntimeEndpoint = 'daemon' | 'httpListener';
31
-
32
32
  function extractAuthorizationCode(input: string): string | null {
33
33
  const trimmed = input.trim();
34
34
  if (!trimmed) return null;
@@ -105,16 +105,6 @@ function getRuntimeEndpointBinding(
105
105
  };
106
106
  }
107
107
 
108
- function runtimePortDiagnostic(
109
- binding: { readonly label: string; readonly host: string; readonly port: number },
110
- portInUse: boolean | undefined,
111
- ): string {
112
- if (portInUse) {
113
- return `The configured port ${binding.host}:${binding.port} is occupied after restart; another GoodVibes process, an overlapping restart, or another service may still own it.`;
114
- }
115
- return `No process is listening on ${binding.host}:${binding.port} after restart.`;
116
- }
117
-
118
108
  function formatRuntimeActiveFailureMessage(
119
109
  handler: InputHandler,
120
110
  request: OnboardingApplyRequest,
@@ -123,20 +113,24 @@ function formatRuntimeActiveFailureMessage(
123
113
  ): string {
124
114
  const binding = getRuntimeEndpointBinding(handler, request, endpoint);
125
115
  const portInUse = endpoint === 'daemon' ? state?.daemonPortInUse : state?.httpListenerPortInUse;
116
+ const status = getRuntimeEndpointStatus(state, endpoint);
126
117
  const impact = endpoint === 'daemon'
127
118
  ? 'browser, LAN, and service-backed GoodVibes surfaces may be unavailable until the daemon is running there.'
128
119
  : 'incoming webhooks and event surfaces will not receive traffic until the listener is running there.';
129
- return `${binding.label} is enabled for ${binding.host}:${binding.port}, but onboarding could not confirm it is running in this TUI instance after restart. ${runtimePortDiagnostic(binding, portInUse)} Settings were saved; ${impact}`;
120
+ return `${binding.label} is enabled for ${binding.host}:${binding.port}, but onboarding could not confirm an embedded or verified external service after restart. ${runtimePortDiagnostic(binding, portInUse, status)} Settings were saved; ${impact}`;
130
121
  }
131
122
 
132
123
  function formatRuntimeStoppedFailureMessage(
133
124
  handler: InputHandler,
134
125
  request: OnboardingApplyRequest,
135
126
  endpoint: OnboardingRuntimeEndpoint,
127
+ state?: OnboardingExternalServiceState,
136
128
  ): string {
137
129
  const binding = getRuntimeEndpointBinding(handler, request, endpoint);
130
+ const status = getRuntimeEndpointStatus(state, endpoint);
138
131
  const disabledSurface = endpoint === 'daemon' ? 'server-backed surfaces' : 'incoming event surfaces';
139
- return `${binding.label} was disabled for ${disabledSurface}, but ${binding.host}:${binding.port} is still occupied. Settings were saved; another GoodVibes process or external service may still be running on that port.`;
132
+ const statusDetail = status ? ` ${runtimePortDiagnostic(binding, undefined, status)}` : '';
133
+ return `${binding.label} was disabled for ${disabledSurface}, but ${binding.host}:${binding.port} is still occupied. Settings were saved; another GoodVibes process or external service may still be running on that port.${statusDetail}`;
140
134
  }
141
135
 
142
136
  function showOnboardingApplyFeedbackForHandler(handler: InputHandler, feedback: OnboardingWizardApplyFeedback): void {
@@ -611,16 +605,14 @@ export async function restartOnboardingExternalServicesIfNeededForHandler(handle
611
605
  }
612
606
 
613
607
  const currentState = externalServices.inspect();
614
- const hasLiveExternalServices = currentState.daemonRunning === true
615
- || currentState.daemonPortInUse === true
616
- || currentState.httpListenerRunning === true
617
- || currentState.httpListenerPortInUse === true;
608
+ const hasLiveExternalServices = isRuntimeEndpointOccupyingConfiguredPort(currentState, 'daemon')
609
+ || isRuntimeEndpointOccupyingConfiguredPort(currentState, 'httpListener');
618
610
  if (!posture.serverBacked && !hasLiveExternalServices) return [];
619
611
 
620
612
  try {
621
613
  const state = await externalServices.restart();
622
614
  const failures: OnboardingVerificationItem[] = [];
623
- if (posture.expectedDaemon && !state.daemonRunning) {
615
+ if (posture.expectedDaemon && !isRuntimeEndpointActive(state, 'daemon')) {
624
616
  failures.push({
625
617
  id: 'runtime:daemon-active',
626
618
  status: 'fail',
@@ -628,15 +620,15 @@ export async function restartOnboardingExternalServicesIfNeededForHandler(handle
628
620
  target: 'service',
629
621
  });
630
622
  }
631
- if (!posture.expectedDaemon && (state.daemonRunning || state.daemonPortInUse)) {
623
+ if (!posture.expectedDaemon && isRuntimeEndpointOccupyingConfiguredPort(state, 'daemon')) {
632
624
  failures.push({
633
625
  id: 'runtime:daemon-stopped',
634
626
  status: 'fail',
635
- message: formatRuntimeStoppedFailureMessage(handler, request, 'daemon'),
627
+ message: formatRuntimeStoppedFailureMessage(handler, request, 'daemon', state),
636
628
  target: 'service',
637
629
  });
638
630
  }
639
- if (posture.expectedHttpListener && !state.httpListenerRunning) {
631
+ if (posture.expectedHttpListener && !isRuntimeEndpointActive(state, 'httpListener')) {
640
632
  failures.push({
641
633
  id: 'runtime:http-listener-active',
642
634
  status: 'fail',
@@ -644,11 +636,11 @@ export async function restartOnboardingExternalServicesIfNeededForHandler(handle
644
636
  target: 'service',
645
637
  });
646
638
  }
647
- if (!posture.expectedHttpListener && (state.httpListenerRunning || state.httpListenerPortInUse)) {
639
+ if (!posture.expectedHttpListener && isRuntimeEndpointOccupyingConfiguredPort(state, 'httpListener')) {
648
640
  failures.push({
649
641
  id: 'runtime:http-listener-stopped',
650
642
  status: 'fail',
651
- message: formatRuntimeStoppedFailureMessage(handler, request, 'httpListener'),
643
+ message: formatRuntimeStoppedFailureMessage(handler, request, 'httpListener', state),
652
644
  target: 'service',
653
645
  });
654
646
  }
@@ -685,19 +677,19 @@ export function verifyOnboardingRuntimePostureForHandler(handler: InputHandler,
685
677
  }
686
678
 
687
679
  const stoppedItems: OnboardingVerificationItem[] = [];
688
- if (externalState?.daemonRunning || externalState?.daemonPortInUse) {
680
+ if (isRuntimeEndpointOccupyingConfiguredPort(externalState, 'daemon')) {
689
681
  stoppedItems.push({
690
682
  id: 'runtime:daemon-stopped',
691
683
  status: 'fail',
692
- message: formatRuntimeStoppedFailureMessage(handler, request, 'daemon'),
684
+ message: formatRuntimeStoppedFailureMessage(handler, request, 'daemon', externalState),
693
685
  target: 'service',
694
686
  });
695
687
  }
696
- if (externalState?.httpListenerRunning || externalState?.httpListenerPortInUse) {
688
+ if (isRuntimeEndpointOccupyingConfiguredPort(externalState, 'httpListener')) {
697
689
  stoppedItems.push({
698
690
  id: 'runtime:http-listener-stopped',
699
691
  status: 'fail',
700
- message: formatRuntimeStoppedFailureMessage(handler, request, 'httpListener'),
692
+ message: formatRuntimeStoppedFailureMessage(handler, request, 'httpListener', externalState),
701
693
  target: 'service',
702
694
  });
703
695
  }
@@ -744,38 +736,40 @@ export function verifyOnboardingRuntimePostureForHandler(handler: InputHandler,
744
736
  }
745
737
 
746
738
  if (posture.expectedDaemon) {
739
+ const daemonActive = isRuntimeEndpointActive(externalState, 'daemon');
747
740
  items.push({
748
741
  id: 'runtime:daemon-active',
749
- status: externalState?.daemonRunning ? 'pass' : 'fail',
750
- message: externalState?.daemonRunning
751
- ? 'The GoodVibes daemon is running with the applied onboarding settings.'
742
+ status: daemonActive ? 'pass' : 'fail',
743
+ message: daemonActive
744
+ ? formatRuntimeActiveSuccessMessage('daemon', externalState)
752
745
  : formatRuntimeActiveFailureMessage(handler, request, 'daemon', externalState),
753
746
  target: 'service',
754
747
  });
755
748
  }
756
- if (!posture.expectedDaemon && (externalState?.daemonRunning || externalState?.daemonPortInUse)) {
749
+ if (!posture.expectedDaemon && isRuntimeEndpointOccupyingConfiguredPort(externalState, 'daemon')) {
757
750
  items.push({
758
751
  id: 'runtime:daemon-stopped',
759
752
  status: 'fail',
760
- message: formatRuntimeStoppedFailureMessage(handler, request, 'daemon'),
753
+ message: formatRuntimeStoppedFailureMessage(handler, request, 'daemon', externalState),
761
754
  target: 'service',
762
755
  });
763
756
  }
764
757
  if (posture.expectedHttpListener) {
758
+ const httpListenerActive = isRuntimeEndpointActive(externalState, 'httpListener');
765
759
  items.push({
766
760
  id: 'runtime:http-listener-active',
767
- status: externalState?.httpListenerRunning ? 'pass' : 'fail',
768
- message: externalState?.httpListenerRunning
769
- ? 'The HTTP listener is running with the applied onboarding settings.'
761
+ status: httpListenerActive ? 'pass' : 'fail',
762
+ message: httpListenerActive
763
+ ? formatRuntimeActiveSuccessMessage('httpListener', externalState)
770
764
  : formatRuntimeActiveFailureMessage(handler, request, 'httpListener', externalState),
771
765
  target: 'service',
772
766
  });
773
767
  }
774
- if (!posture.expectedHttpListener && (externalState?.httpListenerRunning || externalState?.httpListenerPortInUse)) {
768
+ if (!posture.expectedHttpListener && isRuntimeEndpointOccupyingConfiguredPort(externalState, 'httpListener')) {
775
769
  items.push({
776
770
  id: 'runtime:http-listener-stopped',
777
771
  status: 'fail',
778
- message: formatRuntimeStoppedFailureMessage(handler, request, 'httpListener'),
772
+ message: formatRuntimeStoppedFailureMessage(handler, request, 'httpListener', externalState),
779
773
  target: 'service',
780
774
  });
781
775
  }
@@ -0,0 +1,87 @@
1
+ import type { HostServiceStatus } from '@pellux/goodvibes-sdk/platform/runtime/bootstrap-services';
2
+
3
+ export interface OnboardingExternalServiceState {
4
+ readonly daemonRunning?: boolean;
5
+ readonly daemonPortInUse?: boolean;
6
+ readonly httpListenerRunning?: boolean;
7
+ readonly httpListenerPortInUse?: boolean;
8
+ readonly daemonStatus?: HostServiceStatus;
9
+ readonly httpListenerStatus?: HostServiceStatus;
10
+ }
11
+
12
+ export type OnboardingRuntimeEndpoint = 'daemon' | 'httpListener';
13
+
14
+ export function runtimePortDiagnostic(
15
+ binding: { readonly label: string; readonly host: string; readonly port: number },
16
+ portInUse: boolean | undefined,
17
+ status?: HostServiceStatus,
18
+ ): string {
19
+ if (status) {
20
+ const reason = status.reason ? ` ${status.reason}` : '';
21
+ if (status.mode === 'blocked') {
22
+ return `The configured endpoint ${status.baseUrl} is occupied but was not usable by this TUI instance.${reason}`;
23
+ }
24
+ if (status.mode === 'disabled') {
25
+ return `The configured endpoint ${status.baseUrl} is disabled in the runtime service configuration.${reason}`;
26
+ }
27
+ if (status.mode === 'unavailable') {
28
+ return `The configured endpoint ${status.baseUrl} is unavailable after startup or restart.${reason}`;
29
+ }
30
+ if (status.mode === 'external') {
31
+ const version = status.version ? ` version ${status.version}` : '';
32
+ return `An existing GoodVibes service was verified at ${status.baseUrl}${version}.`;
33
+ }
34
+ return `An embedded GoodVibes service is running at ${status.baseUrl}.`;
35
+ }
36
+ if (portInUse) {
37
+ return `The configured port ${binding.host}:${binding.port} is occupied after restart; another GoodVibes process, an overlapping restart, or another service may still own it.`;
38
+ }
39
+ return `No process is listening on ${binding.host}:${binding.port} after restart.`;
40
+ }
41
+
42
+ export function getRuntimeEndpointStatus(
43
+ state: OnboardingExternalServiceState | undefined,
44
+ endpoint: OnboardingRuntimeEndpoint,
45
+ ): HostServiceStatus | undefined {
46
+ return endpoint === 'daemon' ? state?.daemonStatus : state?.httpListenerStatus;
47
+ }
48
+
49
+ export function isRuntimeEndpointActive(
50
+ state: OnboardingExternalServiceState | undefined,
51
+ endpoint: OnboardingRuntimeEndpoint,
52
+ ): boolean {
53
+ const status = getRuntimeEndpointStatus(state, endpoint);
54
+ if (status) return status.mode === 'embedded' || status.mode === 'external';
55
+ return endpoint === 'daemon'
56
+ ? state?.daemonRunning === true
57
+ : state?.httpListenerRunning === true;
58
+ }
59
+
60
+ export function isRuntimeEndpointOccupyingConfiguredPort(
61
+ state: OnboardingExternalServiceState | undefined,
62
+ endpoint: OnboardingRuntimeEndpoint,
63
+ ): boolean {
64
+ const status = getRuntimeEndpointStatus(state, endpoint);
65
+ if (status) return status.mode === 'embedded' || status.mode === 'external' || status.mode === 'blocked';
66
+ return endpoint === 'daemon'
67
+ ? state?.daemonRunning === true || state?.daemonPortInUse === true
68
+ : state?.httpListenerRunning === true || state?.httpListenerPortInUse === true;
69
+ }
70
+
71
+ export function formatRuntimeActiveSuccessMessage(
72
+ endpoint: OnboardingRuntimeEndpoint,
73
+ state: OnboardingExternalServiceState | undefined,
74
+ ): string {
75
+ const status = getRuntimeEndpointStatus(state, endpoint);
76
+ const label = endpoint === 'daemon' ? 'GoodVibes daemon' : 'HTTP listener';
77
+ if (status?.mode === 'external') {
78
+ const version = status.version ? ` version ${status.version}` : '';
79
+ return `${label} is already running as a verified external GoodVibes service at ${status.baseUrl}${version}.`;
80
+ }
81
+ if (status?.mode === 'embedded') {
82
+ return `${label} is running as an embedded service at ${status.baseUrl}.`;
83
+ }
84
+ return endpoint === 'daemon'
85
+ ? 'The GoodVibes daemon is running with the applied onboarding settings.'
86
+ : 'The HTTP listener is running with the applied onboarding settings.';
87
+ }
@@ -10,7 +10,6 @@
10
10
  * - lifecycle.ts: save/shutdown helpers
11
11
  */
12
12
  import { join } from 'node:path';
13
- import net from 'node:net';
14
13
  import { Orchestrator, type OrchestratorUserInputOptions } from '../core/orchestrator.ts';
15
14
  import { AcpManager } from '@pellux/goodvibes-sdk/platform/acp/manager';
16
15
  import { getTierPromptSupplement, getTierForContextWindow } from '@pellux/goodvibes-sdk/platform/providers/tier-prompts';
@@ -36,7 +35,7 @@ import {
36
35
  } from '@pellux/goodvibes-sdk/platform/runtime/session-persistence';
37
36
  import { startBackgroundProviderRegistration } from '@pellux/goodvibes-sdk/platform/runtime/bootstrap-background';
38
37
  import { restoreSavedModel } from '@pellux/goodvibes-sdk/platform/runtime/bootstrap-helpers';
39
- import { startExternalServices, type ExternalServicesHandle } from '@pellux/goodvibes-sdk/platform/runtime/bootstrap-services';
38
+ import { startExternalServices, type ExternalServicesHandle, type HostServiceStatus } from '@pellux/goodvibes-sdk/platform/runtime/bootstrap-services';
40
39
  import { getOrCreateCompanionToken, pruneStaleOperatorTokens } from '@pellux/goodvibes-sdk/platform/pairing/companion-token';
41
40
  import { workspaceOperatorTokenCandidates } from './operator-token-cleanup.ts';
42
41
  import type { UiRuntimeServices } from './ui-services.ts';
@@ -44,6 +43,11 @@ import { createDeferredStartupCoordinator } from '@pellux/goodvibes-sdk/platform
44
43
  import { initializeBootstrapCore } from './bootstrap-core.ts';
45
44
  import { createBootstrapShell } from './bootstrap-shell.ts';
46
45
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-display';
46
+ import { DaemonServer } from '@pellux/goodvibes-sdk/platform/daemon/server';
47
+ import { HttpListener } from '@pellux/goodvibes-sdk/platform/daemon/http-listener';
48
+ import { createSafeHostServeFactory } from '../daemon/safe-serve.ts';
49
+
50
+ type ExternalServiceFactories = NonNullable<Parameters<typeof startExternalServices>[3]>;
47
51
 
48
52
  // ── Bootstrap context type ──────────────────────────────────────────────────
49
53
 
@@ -295,80 +299,45 @@ export async function bootstrapRuntime(
295
299
 
296
300
  const deferredStartup = createDeferredStartupCoordinator();
297
301
 
298
- interface ExternalServiceBindingSnapshot {
299
- readonly daemon: {
300
- readonly host: string;
301
- readonly port: number;
302
- };
303
- readonly httpListener: {
304
- readonly host: string;
305
- readonly port: number;
306
- };
307
- }
308
-
309
- interface ExternalServicePortState {
310
- readonly daemonPortInUse: boolean;
311
- readonly httpListenerPortInUse: boolean;
312
- }
313
-
314
- const readExternalServiceBindings = (): ExternalServiceBindingSnapshot => ({
315
- daemon: {
316
- host: String(configManager.get('controlPlane.host') ?? '127.0.0.1'),
317
- port: Number(configManager.get('controlPlane.port') ?? 3421),
318
- },
319
- httpListener: {
320
- host: String(configManager.get('httpListener.host') ?? '127.0.0.1'),
321
- port: Number(configManager.get('httpListener.port') ?? 3422),
322
- },
323
- });
324
-
325
- const getProbeHosts = (host: string): readonly string[] => {
302
+ const formatHostServiceBaseUrl = (host: string, port: number): string => {
326
303
  const normalized = host.trim().toLowerCase();
327
- if (normalized === '0.0.0.0') return ['127.0.0.1'];
328
- if (normalized === '::' || normalized === '[::]') return ['::1'];
329
- if (normalized.length === 0) return ['127.0.0.1'];
330
- return [host];
304
+ const probeHost = normalized === '0.0.0.0'
305
+ ? '127.0.0.1'
306
+ : normalized === '::' || normalized === '[::]'
307
+ ? '::1'
308
+ : host;
309
+ const urlHost = probeHost.includes(':') && !probeHost.startsWith('[') ? `[${probeHost}]` : probeHost;
310
+ return `http://${urlHost}:${port}`;
331
311
  };
332
312
 
333
- const isTcpPortInUse = async (host: string, port: number): Promise<boolean> => new Promise((resolve) => {
334
- const socket = new net.Socket();
335
- let settled = false;
336
- const finish = (result: boolean): void => {
337
- if (settled) return;
338
- settled = true;
339
- socket.destroy();
340
- resolve(result);
313
+ const createPendingServiceStatus = (
314
+ service: 'daemon' | 'httpListener',
315
+ ): HostServiceStatus => {
316
+ const host = String(configManager.get(service === 'daemon' ? 'controlPlane.host' : 'httpListener.host') ?? '127.0.0.1');
317
+ const port = Number(configManager.get(service === 'daemon' ? 'controlPlane.port' : 'httpListener.port') ?? (service === 'daemon' ? 3421 : 3422));
318
+ return {
319
+ mode: 'unavailable',
320
+ host,
321
+ port,
322
+ baseUrl: formatHostServiceBaseUrl(host, port),
323
+ reason: 'Background service startup has not completed yet',
341
324
  };
325
+ };
342
326
 
343
- socket.setTimeout(250);
344
- socket.once('connect', () => finish(true));
345
- socket.once('timeout', () => finish(false));
346
- socket.once('error', () => finish(false));
347
- socket.connect(port, host);
348
- });
349
-
350
- const inspectExternalPorts = async (
351
- bindings: readonly ExternalServiceBindingSnapshot[],
352
- ): Promise<ExternalServicePortState> => {
353
- const daemonTargets = new Map<string, { readonly host: string; readonly port: number }>();
354
- const listenerTargets = new Map<string, { readonly host: string; readonly port: number }>();
355
- for (const binding of bindings) {
356
- for (const host of getProbeHosts(binding.daemon.host)) {
357
- daemonTargets.set(`${host}:${binding.daemon.port}`, { host, port: binding.daemon.port });
358
- }
359
- for (const host of getProbeHosts(binding.httpListener.host)) {
360
- listenerTargets.set(`${host}:${binding.httpListener.port}`, { host, port: binding.httpListener.port });
361
- }
362
- }
327
+ const hostServiceIsActive = (status: HostServiceStatus): boolean => status.mode === 'embedded' || status.mode === 'external';
363
328
 
364
- const [daemonResults, listenerResults] = await Promise.all([
365
- Promise.all([...daemonTargets.values()].map((target) => isTcpPortInUse(target.host, target.port))),
366
- Promise.all([...listenerTargets.values()].map((target) => isTcpPortInUse(target.host, target.port))),
367
- ]);
329
+ const hostServiceIsBlocked = (status: HostServiceStatus): boolean => status.mode === 'blocked';
368
330
 
331
+ const inspectExternalServices = () => {
332
+ const daemonStatus = externalServices.daemonStatus;
333
+ const httpListenerStatus = externalServices.httpListenerStatus;
369
334
  return {
370
- daemonPortInUse: daemonResults.some(Boolean),
371
- httpListenerPortInUse: listenerResults.some(Boolean),
335
+ daemonRunning: hostServiceIsActive(daemonStatus),
336
+ daemonPortInUse: hostServiceIsBlocked(daemonStatus),
337
+ httpListenerRunning: hostServiceIsActive(httpListenerStatus),
338
+ httpListenerPortInUse: hostServiceIsBlocked(httpListenerStatus),
339
+ daemonStatus,
340
+ httpListenerStatus,
372
341
  };
373
342
  };
374
343
 
@@ -386,24 +355,31 @@ export async function bootstrapRuntime(
386
355
  ]);
387
356
  };
388
357
 
358
+ const createExternalServiceFactories = (sharedDaemonToken: string): ExternalServiceFactories => ({
359
+ sharedDaemonToken,
360
+ createDaemonServer: (bus, userAuth, runtimeServices) => new DaemonServer({
361
+ runtimeBus: bus,
362
+ userAuth,
363
+ runtimeServices,
364
+ serveFactory: createSafeHostServeFactory('Embedded daemon'),
365
+ }),
366
+ createHttpListener: (dispatcher, userAuth, configManager) => new HttpListener({
367
+ hookDispatcher: dispatcher,
368
+ userAuth,
369
+ configManager,
370
+ serveFactory: createSafeHostServeFactory('Embedded HTTP listener'),
371
+ }),
372
+ });
373
+
389
374
  let externalServices: ExternalServicesHandle = {
390
375
  daemonServer: null,
391
376
  httpListener: null,
377
+ daemonStatus: createPendingServiceStatus('daemon'),
378
+ httpListenerStatus: createPendingServiceStatus('httpListener'),
392
379
  listRecentControlPlaneEvents: () => [],
393
380
  async stop(): Promise<void> {},
394
381
  };
395
382
  let externalServicesPromise: Promise<ExternalServicesHandle> | null = null;
396
- let externalServiceBindings = readExternalServiceBindings();
397
- let externalServicePortState: ExternalServicePortState = {
398
- daemonPortInUse: false,
399
- httpListenerPortInUse: false,
400
- };
401
- const inspectExternalServices = () => ({
402
- daemonRunning: externalServices.daemonServer !== null,
403
- daemonPortInUse: externalServicePortState.daemonPortInUse,
404
- httpListenerRunning: externalServices.httpListener !== null,
405
- httpListenerPortInUse: externalServicePortState.httpListenerPortInUse,
406
- });
407
383
  const platformExternalServices = uiServices.platform as typeof uiServices.platform & {
408
384
  externalServices: NonNullable<typeof uiServices.platform.externalServices>;
409
385
  };
@@ -419,20 +395,17 @@ export async function bootstrapRuntime(
419
395
  }
420
396
  await waitForConfigDrivenRestarts(externalServices);
421
397
  await externalServices.stop();
422
- const previousBindings = externalServiceBindings;
423
- externalServiceBindings = readExternalServiceBindings();
424
398
  const daemonHomeDir = join(services.homeDirectory, '.goodvibes', 'daemon');
425
399
  const companionTokenRecord = getOrCreateCompanionToken('tui', { daemonHomeDir });
426
400
  externalServicesPromise = startExternalServices(
427
401
  configManager,
428
402
  runtimeBus,
429
403
  hookDispatcher,
430
- { sharedDaemonToken: companionTokenRecord.token },
404
+ createExternalServiceFactories(companionTokenRecord.token),
431
405
  services,
432
406
  );
433
407
  externalServices = await externalServicesPromise;
434
408
  controlPlaneRecentEventsRef.value = (limit) => externalServices.listRecentControlPlaneEvents(limit);
435
- externalServicePortState = await inspectExternalPorts([previousBindings, externalServiceBindings]);
436
409
  requestRender();
437
410
  return inspectExternalServices();
438
411
  },
@@ -487,17 +460,15 @@ export async function bootstrapRuntime(
487
460
  if (prune.failedPaths.length > 0) {
488
461
  logger.warn(`[bootstrap] Failed to prune ${prune.failedPaths.length} stale operator-token file(s) (permission/race): ${prune.failedPaths.join(', ')}`);
489
462
  }
490
- externalServiceBindings = readExternalServiceBindings();
491
463
  externalServicesPromise = startExternalServices(
492
464
  configManager,
493
465
  runtimeBus,
494
466
  hookDispatcher,
495
- { sharedDaemonToken: companionTokenRecord.token },
467
+ createExternalServiceFactories(companionTokenRecord.token),
496
468
  services,
497
469
  );
498
470
  externalServices = await externalServicesPromise;
499
471
  controlPlaneRecentEventsRef.value = (limit) => externalServices.listRecentControlPlaneEvents(limit);
500
- externalServicePortState = await inspectExternalPorts([externalServiceBindings]);
501
472
  requestRender();
502
473
  },
503
474
  onError: (error) => {
@@ -8,6 +8,7 @@ import type { ControlPlaneRecentEvent } from '@pellux/goodvibes-sdk/platform/con
8
8
  import type { ApprovalBroker } from '@pellux/goodvibes-sdk/platform/control-plane/approval-broker';
9
9
  import type { SharedSessionBroker } from '@pellux/goodvibes-sdk/platform/control-plane/session-broker';
10
10
  import type { ShellPathService } from '@pellux/goodvibes-sdk/platform/runtime/shell-paths';
11
+ import type { HostServiceStatus } from '@pellux/goodvibes-sdk/platform/runtime/bootstrap-services';
11
12
  import type { SecretsManager } from '../config/secrets.ts';
12
13
 
13
14
  export interface UiEnvironmentServices {
@@ -61,12 +62,16 @@ export interface UiPlatformServices {
61
62
  readonly daemonPortInUse?: boolean;
62
63
  readonly httpListenerRunning: boolean;
63
64
  readonly httpListenerPortInUse?: boolean;
65
+ readonly daemonStatus?: HostServiceStatus;
66
+ readonly httpListenerStatus?: HostServiceStatus;
64
67
  };
65
68
  restart(): Promise<{
66
69
  readonly daemonRunning: boolean;
67
70
  readonly daemonPortInUse?: boolean;
68
71
  readonly httpListenerRunning: boolean;
69
72
  readonly httpListenerPortInUse?: boolean;
73
+ readonly daemonStatus?: HostServiceStatus;
74
+ readonly httpListenerStatus?: HostServiceStatus;
70
75
  }>;
71
76
  };
72
77
  }
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '0.19.49';
9
+ let _version = '0.19.51';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  _version = pkg.version ?? _version;