@pellux/goodvibes-tui 0.19.48 → 0.19.50
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 +25 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +34 -7
- package/package.json +2 -2
- package/src/input/handler-onboarding.ts +39 -45
- package/src/input/onboarding/onboarding-runtime-status.ts +87 -0
- package/src/runtime/bootstrap.ts +34 -84
- package/src/runtime/ui-services.ts +5 -0
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ All notable changes to GoodVibes TUI.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [0.19.50] — 2026-04-28
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Updated `@pellux/goodvibes-sdk` to `0.26.5`.
|
|
11
|
+
- 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.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- Treats a verified external GoodVibes daemon on the configured host/port as active instead of reporting daemon activation failure during onboarding.
|
|
15
|
+
- Reports SDK-provided blocked/unavailable startup reasons in onboarding runtime verification warnings.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## [0.19.49] — 2026-04-28
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Updated `@pellux/goodvibes-sdk` to `0.26.4` for SDK-owned multipart/raw artifact uploads across artifact storage, knowledge ingest, and Home Assistant Home Graph ingest.
|
|
23
|
+
- Exposed the SDK `storage.artifacts.maxBytes` setting through the normal storage configuration surface.
|
|
24
|
+
- Regenerated foundation operator contract artifacts from the SDK 0.26.4 contract.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- Picked up SDK fixes for large multipart upload cap handling and oversized-upload rejection behavior.
|
|
28
|
+
- Removed the temporary TUI upload-stream normalization shim after SDK 0.26.4 fixed Bun request-reader cleanup failures in raw artifact uploads.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
7
32
|
## [0.19.48] — 2026-04-28
|
|
8
33
|
|
|
9
34
|
### 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
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"product": {
|
|
4
4
|
"id": "goodvibes",
|
|
5
5
|
"surface": "operator",
|
|
6
|
-
"version": "0.26.
|
|
6
|
+
"version": "0.26.5"
|
|
7
7
|
},
|
|
8
8
|
"auth": {
|
|
9
9
|
"modes": [
|
|
@@ -2319,7 +2319,7 @@
|
|
|
2319
2319
|
{
|
|
2320
2320
|
"id": "artifacts.create",
|
|
2321
2321
|
"title": "Create Artifact",
|
|
2322
|
-
"description": "Store a file or attachment artifact for later delivery or
|
|
2322
|
+
"description": "Store a file or attachment artifact for later delivery, analysis, or knowledge ingest. JSON supports small inline bodies and daemon-local path/URI references; HTTP callers can also send multipart/form-data or a raw binary body for large uploads.",
|
|
2323
2323
|
"category": "artifacts",
|
|
2324
2324
|
"source": "builtin",
|
|
2325
2325
|
"access": "authenticated",
|
|
@@ -2486,7 +2486,16 @@
|
|
|
2486
2486
|
],
|
|
2487
2487
|
"additionalProperties": false
|
|
2488
2488
|
},
|
|
2489
|
-
"invokable": true
|
|
2489
|
+
"invokable": true,
|
|
2490
|
+
"metadata": {
|
|
2491
|
+
"uploadModes": [
|
|
2492
|
+
"json-inline",
|
|
2493
|
+
"json-path-or-uri",
|
|
2494
|
+
"multipart-file",
|
|
2495
|
+
"raw-body"
|
|
2496
|
+
],
|
|
2497
|
+
"largeUploadConfigKey": "storage.artifacts.maxBytes"
|
|
2498
|
+
}
|
|
2490
2499
|
},
|
|
2491
2500
|
{
|
|
2492
2501
|
"id": "artifacts.get",
|
|
@@ -26572,7 +26581,7 @@
|
|
|
26572
26581
|
{
|
|
26573
26582
|
"id": "homeassistant.homeGraph.ingestHomeGraphArtifact",
|
|
26574
26583
|
"title": "Ingest Home Graph Artifact",
|
|
26575
|
-
"description": "Index an artifact
|
|
26584
|
+
"description": "Index an existing artifact reference, JSON path/URI reference, multipart file upload, or raw binary upload as a Home Graph document, receipt, warranty, manual, or photo.",
|
|
26576
26585
|
"category": "knowledge",
|
|
26577
26586
|
"source": "builtin",
|
|
26578
26587
|
"access": "admin",
|
|
@@ -27020,7 +27029,16 @@
|
|
|
27020
27029
|
],
|
|
27021
27030
|
"additionalProperties": true
|
|
27022
27031
|
},
|
|
27023
|
-
"invokable": true
|
|
27032
|
+
"invokable": true,
|
|
27033
|
+
"metadata": {
|
|
27034
|
+
"uploadModes": [
|
|
27035
|
+
"json-artifact-reference",
|
|
27036
|
+
"json-path-or-uri",
|
|
27037
|
+
"multipart-file",
|
|
27038
|
+
"raw-body"
|
|
27039
|
+
],
|
|
27040
|
+
"largeUploadConfigKey": "storage.artifacts.maxBytes"
|
|
27041
|
+
}
|
|
27024
27042
|
},
|
|
27025
27043
|
{
|
|
27026
27044
|
"id": "homeassistant.homeGraph.ingestHomeGraphNote",
|
|
@@ -31622,7 +31640,7 @@
|
|
|
31622
31640
|
{
|
|
31623
31641
|
"id": "knowledge.ingest.artifact",
|
|
31624
31642
|
"title": "Ingest Artifact Into Knowledge",
|
|
31625
|
-
"description": "Snapshot
|
|
31643
|
+
"description": "Snapshot an existing artifact, daemon-local path, remote URI, multipart file upload, or raw binary upload into the structured knowledge store and run structured extraction.",
|
|
31626
31644
|
"category": "knowledge",
|
|
31627
31645
|
"source": "builtin",
|
|
31628
31646
|
"access": "admin",
|
|
@@ -31925,7 +31943,16 @@
|
|
|
31925
31943
|
],
|
|
31926
31944
|
"additionalProperties": true
|
|
31927
31945
|
},
|
|
31928
|
-
"invokable": true
|
|
31946
|
+
"invokable": true,
|
|
31947
|
+
"metadata": {
|
|
31948
|
+
"uploadModes": [
|
|
31949
|
+
"json-artifact-reference",
|
|
31950
|
+
"json-path-or-uri",
|
|
31951
|
+
"multipart-file",
|
|
31952
|
+
"raw-body"
|
|
31953
|
+
],
|
|
31954
|
+
"largeUploadConfigKey": "storage.artifacts.maxBytes"
|
|
31955
|
+
}
|
|
31929
31956
|
},
|
|
31930
31957
|
{
|
|
31931
31958
|
"id": "knowledge.ingest.bookmarks",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.50",
|
|
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.5",
|
|
95
95
|
"bash-language-server": "^5.6.0",
|
|
96
96
|
"fuse.js": "^7.1.0",
|
|
97
97
|
"graphql": "^16.13.2",
|
|
@@ -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';
|
|
@@ -295,80 +294,45 @@ export async function bootstrapRuntime(
|
|
|
295
294
|
|
|
296
295
|
const deferredStartup = createDeferredStartupCoordinator();
|
|
297
296
|
|
|
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[] => {
|
|
297
|
+
const formatHostServiceBaseUrl = (host: string, port: number): string => {
|
|
326
298
|
const normalized = host.trim().toLowerCase();
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
299
|
+
const probeHost = normalized === '0.0.0.0'
|
|
300
|
+
? '127.0.0.1'
|
|
301
|
+
: normalized === '::' || normalized === '[::]'
|
|
302
|
+
? '::1'
|
|
303
|
+
: host;
|
|
304
|
+
const urlHost = probeHost.includes(':') && !probeHost.startsWith('[') ? `[${probeHost}]` : probeHost;
|
|
305
|
+
return `http://${urlHost}:${port}`;
|
|
331
306
|
};
|
|
332
307
|
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
308
|
+
const createPendingServiceStatus = (
|
|
309
|
+
service: 'daemon' | 'httpListener',
|
|
310
|
+
): HostServiceStatus => {
|
|
311
|
+
const host = String(configManager.get(service === 'daemon' ? 'controlPlane.host' : 'httpListener.host') ?? '127.0.0.1');
|
|
312
|
+
const port = Number(configManager.get(service === 'daemon' ? 'controlPlane.port' : 'httpListener.port') ?? (service === 'daemon' ? 3421 : 3422));
|
|
313
|
+
return {
|
|
314
|
+
mode: 'unavailable',
|
|
315
|
+
host,
|
|
316
|
+
port,
|
|
317
|
+
baseUrl: formatHostServiceBaseUrl(host, port),
|
|
318
|
+
reason: 'Background service startup has not completed yet',
|
|
341
319
|
};
|
|
320
|
+
};
|
|
342
321
|
|
|
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
|
-
});
|
|
322
|
+
const hostServiceIsActive = (status: HostServiceStatus): boolean => status.mode === 'embedded' || status.mode === 'external';
|
|
349
323
|
|
|
350
|
-
const
|
|
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
|
-
}
|
|
363
|
-
|
|
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
|
-
]);
|
|
324
|
+
const hostServiceIsBlocked = (status: HostServiceStatus): boolean => status.mode === 'blocked';
|
|
368
325
|
|
|
326
|
+
const inspectExternalServices = () => {
|
|
327
|
+
const daemonStatus = externalServices.daemonStatus;
|
|
328
|
+
const httpListenerStatus = externalServices.httpListenerStatus;
|
|
369
329
|
return {
|
|
370
|
-
|
|
371
|
-
|
|
330
|
+
daemonRunning: hostServiceIsActive(daemonStatus),
|
|
331
|
+
daemonPortInUse: hostServiceIsBlocked(daemonStatus),
|
|
332
|
+
httpListenerRunning: hostServiceIsActive(httpListenerStatus),
|
|
333
|
+
httpListenerPortInUse: hostServiceIsBlocked(httpListenerStatus),
|
|
334
|
+
daemonStatus,
|
|
335
|
+
httpListenerStatus,
|
|
372
336
|
};
|
|
373
337
|
};
|
|
374
338
|
|
|
@@ -389,21 +353,12 @@ export async function bootstrapRuntime(
|
|
|
389
353
|
let externalServices: ExternalServicesHandle = {
|
|
390
354
|
daemonServer: null,
|
|
391
355
|
httpListener: null,
|
|
356
|
+
daemonStatus: createPendingServiceStatus('daemon'),
|
|
357
|
+
httpListenerStatus: createPendingServiceStatus('httpListener'),
|
|
392
358
|
listRecentControlPlaneEvents: () => [],
|
|
393
359
|
async stop(): Promise<void> {},
|
|
394
360
|
};
|
|
395
361
|
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
362
|
const platformExternalServices = uiServices.platform as typeof uiServices.platform & {
|
|
408
363
|
externalServices: NonNullable<typeof uiServices.platform.externalServices>;
|
|
409
364
|
};
|
|
@@ -419,8 +374,6 @@ export async function bootstrapRuntime(
|
|
|
419
374
|
}
|
|
420
375
|
await waitForConfigDrivenRestarts(externalServices);
|
|
421
376
|
await externalServices.stop();
|
|
422
|
-
const previousBindings = externalServiceBindings;
|
|
423
|
-
externalServiceBindings = readExternalServiceBindings();
|
|
424
377
|
const daemonHomeDir = join(services.homeDirectory, '.goodvibes', 'daemon');
|
|
425
378
|
const companionTokenRecord = getOrCreateCompanionToken('tui', { daemonHomeDir });
|
|
426
379
|
externalServicesPromise = startExternalServices(
|
|
@@ -432,7 +385,6 @@ export async function bootstrapRuntime(
|
|
|
432
385
|
);
|
|
433
386
|
externalServices = await externalServicesPromise;
|
|
434
387
|
controlPlaneRecentEventsRef.value = (limit) => externalServices.listRecentControlPlaneEvents(limit);
|
|
435
|
-
externalServicePortState = await inspectExternalPorts([previousBindings, externalServiceBindings]);
|
|
436
388
|
requestRender();
|
|
437
389
|
return inspectExternalServices();
|
|
438
390
|
},
|
|
@@ -487,7 +439,6 @@ export async function bootstrapRuntime(
|
|
|
487
439
|
if (prune.failedPaths.length > 0) {
|
|
488
440
|
logger.warn(`[bootstrap] Failed to prune ${prune.failedPaths.length} stale operator-token file(s) (permission/race): ${prune.failedPaths.join(', ')}`);
|
|
489
441
|
}
|
|
490
|
-
externalServiceBindings = readExternalServiceBindings();
|
|
491
442
|
externalServicesPromise = startExternalServices(
|
|
492
443
|
configManager,
|
|
493
444
|
runtimeBus,
|
|
@@ -497,7 +448,6 @@ export async function bootstrapRuntime(
|
|
|
497
448
|
);
|
|
498
449
|
externalServices = await externalServicesPromise;
|
|
499
450
|
controlPlaneRecentEventsRef.value = (limit) => externalServices.listRecentControlPlaneEvents(limit);
|
|
500
|
-
externalServicePortState = await inspectExternalPorts([externalServiceBindings]);
|
|
501
451
|
requestRender();
|
|
502
452
|
},
|
|
503
453
|
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.50';
|
|
10
10
|
try {
|
|
11
11
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
|
|
12
12
|
_version = pkg.version ?? _version;
|