@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 +23 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/daemon/cli.ts +8 -1
- package/src/daemon/safe-serve.ts +61 -0
- package/src/input/handler-onboarding.ts +39 -45
- package/src/input/onboarding/onboarding-runtime-status.ts +87 -0
- package/src/runtime/bootstrap.ts +57 -86
- package/src/runtime/ui-services.ts +5 -0
- package/src/version.ts +1 -1
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
|
[](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "0.19.
|
|
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.
|
|
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({
|
|
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
|
|
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
|
-
|
|
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
|
|
615
|
-
|| currentState
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
750
|
-
message:
|
|
751
|
-
? '
|
|
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
|
|
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:
|
|
768
|
-
message:
|
|
769
|
-
? '
|
|
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
|
|
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
|
+
}
|
package/src/runtime/bootstrap.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
371
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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;
|