@pellux/goodvibes-tui 0.19.24 → 0.19.26
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 +13 -0
- package/README.md +5 -5
- package/bin/goodvibes +10 -0
- package/bin/goodvibes-daemon +10 -0
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +3 -2
- package/src/cli/bundle-command.ts +225 -0
- package/src/cli/completion.ts +90 -0
- package/src/cli/config-overrides.ts +159 -0
- package/src/cli/endpoints.ts +63 -0
- package/src/cli/entrypoint.ts +169 -0
- package/src/cli/help.ts +301 -0
- package/src/cli/index.ts +11 -0
- package/src/cli/management-commands.ts +426 -0
- package/src/cli/management.ts +719 -0
- package/src/cli/network-posture.ts +46 -0
- package/src/cli/package-verification.ts +119 -0
- package/src/cli/parser.ts +369 -0
- package/src/cli/provider-classification.ts +107 -0
- package/src/cli/redaction.ts +105 -0
- package/src/cli/service-command.ts +45 -0
- package/src/cli/service-posture.ts +247 -0
- package/src/cli/status.ts +382 -0
- package/src/cli/surface-command.ts +248 -0
- package/src/cli/tui-startup.ts +32 -0
- package/src/cli/types.ts +69 -0
- package/src/cli-flags.ts +18 -55
- package/src/config/index.ts +1 -1
- package/src/config/secrets.ts +44 -0
- package/src/daemon/cli.ts +62 -11
- package/src/input/command-registry.ts +3 -0
- package/src/input/commands/guidance-runtime.ts +9 -4
- package/src/input/commands/local-runtime.ts +21 -7
- package/src/input/commands/local-setup.ts +31 -38
- package/src/input/commands/onboarding-runtime.ts +14 -0
- package/src/input/commands/runtime-services.ts +9 -0
- package/src/input/commands.ts +2 -0
- package/src/input/feed-context-factory.ts +8 -1
- package/src/input/handler-feed.ts +13 -8
- package/src/input/handler-interactions.ts +266 -0
- package/src/input/handler-modal-stack.ts +23 -3
- package/src/input/handler-modal-token-routes.ts +23 -1
- package/src/input/handler-onboarding.ts +696 -0
- package/src/input/handler-picker-routes.ts +15 -7
- package/src/input/handler-ui-state.ts +58 -0
- package/src/input/handler.ts +120 -246
- package/src/input/onboarding/handler-onboarding-routes.ts +105 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +211 -0
- package/src/input/onboarding/onboarding-wizard-constants.ts +148 -0
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +712 -0
- package/src/input/onboarding/onboarding-wizard-helpers.ts +218 -0
- package/src/input/onboarding/onboarding-wizard-rules.ts +224 -0
- package/src/input/onboarding/onboarding-wizard-state.ts +354 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +642 -0
- package/src/input/onboarding/onboarding-wizard-types.ts +170 -0
- package/src/input/onboarding/onboarding-wizard.ts +594 -0
- package/src/main.ts +32 -39
- package/src/panels/builtin/operations.ts +0 -10
- package/src/panels/index.ts +0 -1
- package/src/renderer/conversation-overlays.ts +6 -0
- package/src/renderer/help-overlay.ts +1 -1
- package/src/renderer/onboarding/onboarding-wizard.ts +533 -0
- package/src/runtime/bootstrap-core.ts +1 -0
- package/src/runtime/bootstrap.ts +123 -0
- package/src/runtime/onboarding/apply.ts +685 -0
- package/src/runtime/onboarding/derivation.ts +495 -0
- package/src/runtime/onboarding/index.ts +7 -0
- package/src/runtime/onboarding/markers.ts +161 -0
- package/src/runtime/onboarding/snapshot.ts +400 -0
- package/src/runtime/onboarding/state.ts +140 -0
- package/src/runtime/onboarding/types.ts +402 -0
- package/src/runtime/onboarding/verify.ts +233 -0
- package/src/runtime/ui-services.ts +16 -0
- package/src/shell/ui-openers.ts +12 -2
- package/src/version.ts +1 -1
- package/src/panels/welcome-panel.ts +0 -64
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export const REDACTED_VALUE = '<redacted>';
|
|
2
|
+
|
|
3
|
+
const SENSITIVE_PATH_PATTERN = /(^|\.)(apiKey|accessToken|botToken|appToken|signingSecret|webhookSecret|verifyToken|verificationToken|secret|password|token|keyFile)$/i;
|
|
4
|
+
const SECRET_LIKE_TEXT_PATTERNS: readonly RegExp[] = [
|
|
5
|
+
/\bsk-[A-Za-z0-9_-]{16,}\b/g,
|
|
6
|
+
/\bghp_[A-Za-z0-9_]{16,}\b/g,
|
|
7
|
+
/\bgho_[A-Za-z0-9_]{16,}\b/g,
|
|
8
|
+
/\bghu_[A-Za-z0-9_]{16,}\b/g,
|
|
9
|
+
/\bghs_[A-Za-z0-9_]{16,}\b/g,
|
|
10
|
+
/\bgithub_pat_[A-Za-z0-9_]{24,}\b/g,
|
|
11
|
+
/\b(?:xoxb|xapp|xoxp|xoxa)-[A-Za-z0-9-]{16,}\b/g,
|
|
12
|
+
/\b[A-Za-z0-9._%+-]+:[A-Za-z0-9._%+-]{8,}@/g,
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export function isSensitiveConfigPath(path: string): boolean {
|
|
16
|
+
return SENSITIVE_PATH_PATTERN.test(path);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isRedactedValue(value: unknown): boolean {
|
|
20
|
+
return value === REDACTED_VALUE;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RedactedConfigResult<T> {
|
|
24
|
+
readonly value: T;
|
|
25
|
+
readonly redactedPaths: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function shouldRedactValue(path: string, value: unknown): boolean {
|
|
29
|
+
if (!isSensitiveConfigPath(path)) return false;
|
|
30
|
+
if (typeof value !== 'string') return value !== undefined && value !== null;
|
|
31
|
+
if (value.trim().length === 0) return false;
|
|
32
|
+
if (value.startsWith('goodvibes://secrets/')) return false;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function redactUnknown(value: unknown, path: string, redactedPaths: string[]): unknown {
|
|
37
|
+
if (shouldRedactValue(path, value)) {
|
|
38
|
+
redactedPaths.push(path);
|
|
39
|
+
return REDACTED_VALUE;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
return value.map((item, index) => redactUnknown(item, `${path}.${index}`, redactedPaths));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (value && typeof value === 'object') {
|
|
47
|
+
const result: Record<string, unknown> = {};
|
|
48
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
49
|
+
const nestedPath = path ? `${path}.${key}` : key;
|
|
50
|
+
result[key] = redactUnknown(nested, nestedPath, redactedPaths);
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function redactConfig<T>(config: T): RedactedConfigResult<T> {
|
|
59
|
+
const redactedPaths: string[] = [];
|
|
60
|
+
return {
|
|
61
|
+
value: redactUnknown(config, '', redactedPaths) as T,
|
|
62
|
+
redactedPaths,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function redactText(input: string): string {
|
|
67
|
+
let output = input;
|
|
68
|
+
for (const pattern of SECRET_LIKE_TEXT_PATTERNS) {
|
|
69
|
+
output = output.replace(pattern, REDACTED_VALUE);
|
|
70
|
+
}
|
|
71
|
+
return output;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function collectSensitiveValues(value: unknown, path: string, values: string[]): void {
|
|
75
|
+
if (shouldRedactValue(path, value) && typeof value === 'string') {
|
|
76
|
+
values.push(value);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(value)) {
|
|
80
|
+
value.forEach((item, index) => collectSensitiveValues(item, `${path}.${index}`, values));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (value && typeof value === 'object') {
|
|
84
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
85
|
+
collectSensitiveValues(nested, path ? `${path}.${key}` : key, values);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function collectSensitiveConfigValues(config: unknown): readonly string[] {
|
|
91
|
+
const values: string[] = [];
|
|
92
|
+
collectSensitiveValues(config, '', values);
|
|
93
|
+
return [...new Set(values)].sort((left, right) => right.length - left.length);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function redactSerializedSecrets(serialized: string, secretValues: readonly string[]): string {
|
|
97
|
+
let output = redactText(serialized);
|
|
98
|
+
for (const secret of secretValues) {
|
|
99
|
+
if (!secret) continue;
|
|
100
|
+
const encoded = JSON.stringify(secret).slice(1, -1);
|
|
101
|
+
output = output.split(encoded).join(REDACTED_VALUE);
|
|
102
|
+
output = output.split(secret).join(REDACTED_VALUE);
|
|
103
|
+
}
|
|
104
|
+
return output;
|
|
105
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { CliCommandRuntime } from './management.ts';
|
|
2
|
+
import { buildCliServicePosture, createPlatformServiceManager, formatCliServicePosture } from './service-posture.ts';
|
|
3
|
+
import type { CliCommandOutput } from './types.ts';
|
|
4
|
+
|
|
5
|
+
function enableServicePosture(runtime: CliCommandRuntime): void {
|
|
6
|
+
runtime.configManager.setDynamic('service.enabled', true);
|
|
7
|
+
runtime.configManager.setDynamic('service.autostart', true);
|
|
8
|
+
runtime.configManager.setDynamic('service.restartOnFailure', true);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function handleServiceCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
|
|
12
|
+
const [sub = 'status'] = runtime.cli.commandArgs;
|
|
13
|
+
const json = runtime.cli.flags.outputFormat === 'json';
|
|
14
|
+
if (sub === 'status' || sub === 'check') {
|
|
15
|
+
const posture = await buildCliServicePosture(runtime, { probe: sub === 'check' });
|
|
16
|
+
return {
|
|
17
|
+
output: formatCliServicePosture(posture, json),
|
|
18
|
+
exitCode: sub === 'check' && posture.issues.length > 0 ? 1 : 0,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (sub === 'install' || sub === 'start' || sub === 'restart' || sub === 'stop' || sub === 'uninstall') {
|
|
22
|
+
const manager = createPlatformServiceManager(runtime);
|
|
23
|
+
if (sub === 'install' || sub === 'start' || sub === 'restart') enableServicePosture(runtime);
|
|
24
|
+
const result =
|
|
25
|
+
sub === 'install' ? manager.install()
|
|
26
|
+
: sub === 'start' ? manager.start()
|
|
27
|
+
: sub === 'restart' ? manager.restart()
|
|
28
|
+
: sub === 'stop' ? manager.stop()
|
|
29
|
+
: manager.uninstall();
|
|
30
|
+
const posture = await buildCliServicePosture(runtime);
|
|
31
|
+
const text = [
|
|
32
|
+
`Service ${sub}: ${result.actionError ? 'failed' : 'ok'}`,
|
|
33
|
+
formatCliServicePosture(posture, false),
|
|
34
|
+
...(result.actionError ? [`actionError: ${result.actionError}`] : []),
|
|
35
|
+
].join('\n');
|
|
36
|
+
return {
|
|
37
|
+
output: json ? JSON.stringify({ action: sub, result, posture }, null, 2) : text,
|
|
38
|
+
exitCode: result.actionError ? 1 : 0,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
output: 'Usage: goodvibes service [status|check|install|start|stop|restart|uninstall]',
|
|
43
|
+
exitCode: 2,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { closeSync, existsSync, openSync, readSync, statSync } from 'node:fs';
|
|
2
|
+
import net from 'node:net';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { PlatformServiceManager } from '@pellux/goodvibes-sdk/platform/daemon/service-manager';
|
|
5
|
+
import type { ManagedServiceStatus } from '@pellux/goodvibes-sdk/platform/daemon/service-manager';
|
|
6
|
+
import type { ConfigManager } from '../config/index.ts';
|
|
7
|
+
import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
8
|
+
import type { RuntimeEndpointBinding, RuntimeEndpointId } from './endpoints.ts';
|
|
9
|
+
import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
|
|
10
|
+
import { redactText } from './redaction.ts';
|
|
11
|
+
|
|
12
|
+
export interface CliServiceRuntime {
|
|
13
|
+
readonly configManager: ConfigManager;
|
|
14
|
+
readonly workingDirectory: string;
|
|
15
|
+
readonly homeDirectory: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CliServiceEndpointPosture {
|
|
19
|
+
readonly id: RuntimeEndpointId;
|
|
20
|
+
readonly label: string;
|
|
21
|
+
readonly enabled: boolean;
|
|
22
|
+
readonly binding: RuntimeEndpointBinding;
|
|
23
|
+
readonly bindPosture: ReturnType<typeof classifyBindPosture>;
|
|
24
|
+
readonly networkFacing: boolean;
|
|
25
|
+
readonly reachable?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CliServiceLogPosture {
|
|
29
|
+
readonly path: string | null;
|
|
30
|
+
readonly exists: boolean;
|
|
31
|
+
readonly size: number;
|
|
32
|
+
readonly modifiedAt: number | null;
|
|
33
|
+
readonly tail?: string;
|
|
34
|
+
readonly readError?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CliServicePosture {
|
|
38
|
+
readonly config: {
|
|
39
|
+
readonly enabled: boolean;
|
|
40
|
+
readonly autostart: boolean;
|
|
41
|
+
readonly restartOnFailure: boolean;
|
|
42
|
+
readonly daemonEnabled: boolean;
|
|
43
|
+
};
|
|
44
|
+
readonly managed: ManagedServiceStatus & {
|
|
45
|
+
readonly pidPath: string;
|
|
46
|
+
readonly lastError: string | null;
|
|
47
|
+
};
|
|
48
|
+
readonly endpoints: readonly CliServiceEndpointPosture[];
|
|
49
|
+
readonly log: CliServiceLogPosture;
|
|
50
|
+
readonly issues: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const ENDPOINTS: readonly { readonly id: RuntimeEndpointId; readonly label: string; readonly enabledKey: string }[] = [
|
|
54
|
+
{ id: 'controlPlane', label: 'control plane', enabledKey: 'controlPlane.enabled' },
|
|
55
|
+
{ id: 'httpListener', label: 'HTTP listener', enabledKey: 'danger.httpListener' },
|
|
56
|
+
{ id: 'web', label: 'web surface', enabledKey: 'web.enabled' },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
function connectHostForBindHost(host: string): string {
|
|
60
|
+
if (host === '0.0.0.0' || host === '::') return '127.0.0.1';
|
|
61
|
+
return host || '127.0.0.1';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function probeTcp(host: string, port: number, timeoutMs = 750): Promise<boolean> {
|
|
65
|
+
return await new Promise<boolean>((resolve) => {
|
|
66
|
+
const socket = net.createConnection({ host: connectHostForBindHost(host), port });
|
|
67
|
+
const finish = (value: boolean) => {
|
|
68
|
+
socket.removeAllListeners();
|
|
69
|
+
socket.destroy();
|
|
70
|
+
resolve(value);
|
|
71
|
+
};
|
|
72
|
+
socket.setTimeout(timeoutMs);
|
|
73
|
+
socket.once('connect', () => finish(true));
|
|
74
|
+
socket.once('timeout', () => finish(false));
|
|
75
|
+
socket.once('error', () => finish(false));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function pidFilePath(runtime: CliServiceRuntime, platform: ManagedServiceStatus['platform']): string {
|
|
80
|
+
return join(runtime.workingDirectory, '.goodvibes', 'tui', 'service', `${platform}.pid`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readLogPosture(path: string | undefined, tailBytes: number): CliServiceLogPosture {
|
|
84
|
+
if (!path) return { path: null, exists: false, size: 0, modifiedAt: null };
|
|
85
|
+
if (!existsSync(path)) return { path, exists: false, size: 0, modifiedAt: null };
|
|
86
|
+
try {
|
|
87
|
+
const stat = statSync(path);
|
|
88
|
+
const length = Math.min(stat.size, Math.max(0, tailBytes));
|
|
89
|
+
if (length === 0) {
|
|
90
|
+
return { path, exists: true, size: stat.size, modifiedAt: stat.mtimeMs, tail: '' };
|
|
91
|
+
}
|
|
92
|
+
const raw = Buffer.alloc(length);
|
|
93
|
+
const fd = openSync(path, 'r');
|
|
94
|
+
try {
|
|
95
|
+
readSync(fd, raw, 0, length, Math.max(0, stat.size - length));
|
|
96
|
+
} finally {
|
|
97
|
+
closeSync(fd);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
path,
|
|
101
|
+
exists: true,
|
|
102
|
+
size: stat.size,
|
|
103
|
+
modifiedAt: stat.mtimeMs,
|
|
104
|
+
tail: redactText(raw.toString('utf-8')),
|
|
105
|
+
};
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return {
|
|
108
|
+
path,
|
|
109
|
+
exists: true,
|
|
110
|
+
size: 0,
|
|
111
|
+
modifiedAt: null,
|
|
112
|
+
readError: error instanceof Error ? error.message : String(error),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function endpointsConflict(a: CliServiceEndpointPosture, b: CliServiceEndpointPosture): boolean {
|
|
118
|
+
if (a.binding.port !== b.binding.port) return false;
|
|
119
|
+
const hostA = a.binding.host;
|
|
120
|
+
const hostB = b.binding.host;
|
|
121
|
+
return hostA === hostB || hostA === '0.0.0.0' || hostB === '0.0.0.0' || hostA === '::' || hostB === '::';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function createPlatformServiceManager(runtime: CliServiceRuntime): PlatformServiceManager {
|
|
125
|
+
return new PlatformServiceManager(runtime.configManager, {
|
|
126
|
+
workingDirectory: runtime.workingDirectory,
|
|
127
|
+
homeDirectory: runtime.homeDirectory,
|
|
128
|
+
surfaceRoot: 'tui',
|
|
129
|
+
binaryBaseName: 'goodvibes-daemon',
|
|
130
|
+
defaultServiceName: 'goodvibes',
|
|
131
|
+
defaultServiceDescription: 'GoodVibes daemon, control-plane, listener, and web host',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function buildCliServicePosture(
|
|
136
|
+
runtime: CliServiceRuntime,
|
|
137
|
+
options: { readonly probe?: boolean; readonly logTailBytes?: number } = {},
|
|
138
|
+
): Promise<CliServicePosture> {
|
|
139
|
+
const manager = createPlatformServiceManager(runtime);
|
|
140
|
+
const status = manager.status();
|
|
141
|
+
const endpoints = await Promise.all(ENDPOINTS.map(async (endpoint): Promise<CliServiceEndpointPosture> => {
|
|
142
|
+
const enabled = runtime.configManager.get(endpoint.enabledKey as never) === true;
|
|
143
|
+
const binding = resolveRuntimeEndpointBinding(runtime.configManager, endpoint.id);
|
|
144
|
+
return {
|
|
145
|
+
id: endpoint.id,
|
|
146
|
+
label: endpoint.label,
|
|
147
|
+
enabled,
|
|
148
|
+
binding,
|
|
149
|
+
bindPosture: classifyBindPosture(binding),
|
|
150
|
+
networkFacing: isNetworkFacing(enabled, binding),
|
|
151
|
+
...(options.probe && enabled ? { reachable: await probeTcp(binding.host, binding.port) } : {}),
|
|
152
|
+
};
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
const config = {
|
|
156
|
+
enabled: runtime.configManager.get('service.enabled') === true,
|
|
157
|
+
autostart: runtime.configManager.get('service.autostart') === true,
|
|
158
|
+
restartOnFailure: runtime.configManager.get('service.restartOnFailure') === true,
|
|
159
|
+
daemonEnabled: runtime.configManager.get('danger.daemon') === true,
|
|
160
|
+
};
|
|
161
|
+
const serverBackedEnabled = config.daemonEnabled || endpoints.some((endpoint) => endpoint.enabled);
|
|
162
|
+
const issues: string[] = [];
|
|
163
|
+
|
|
164
|
+
if (serverBackedEnabled && !config.enabled) {
|
|
165
|
+
issues.push('Server-backed surfaces are enabled but service mode is off.');
|
|
166
|
+
}
|
|
167
|
+
if (config.enabled && !config.autostart) {
|
|
168
|
+
issues.push('Service mode is enabled but autostart is off.');
|
|
169
|
+
}
|
|
170
|
+
if (config.enabled && !config.restartOnFailure) {
|
|
171
|
+
issues.push('Service mode is enabled but restart-on-failure is off.');
|
|
172
|
+
}
|
|
173
|
+
if (config.enabled && !status.installed) {
|
|
174
|
+
issues.push('Service mode is enabled but no platform service definition is installed.');
|
|
175
|
+
}
|
|
176
|
+
if (config.enabled && !status.running) {
|
|
177
|
+
issues.push('Service mode is enabled but the managed service is not running.');
|
|
178
|
+
}
|
|
179
|
+
if (status.actionError) {
|
|
180
|
+
issues.push(`Service manager reported an error: ${status.actionError}`);
|
|
181
|
+
}
|
|
182
|
+
for (const endpoint of endpoints) {
|
|
183
|
+
if (endpoint.enabled && options.probe && endpoint.reachable === false) {
|
|
184
|
+
issues.push(`${endpoint.label} is enabled but not reachable on ${endpoint.binding.host}:${endpoint.binding.port}.`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const enabledEndpoints = endpoints.filter((endpoint) => endpoint.enabled);
|
|
188
|
+
for (let outer = 0; outer < enabledEndpoints.length; outer += 1) {
|
|
189
|
+
for (let inner = outer + 1; inner < enabledEndpoints.length; inner += 1) {
|
|
190
|
+
const left = enabledEndpoints[outer]!;
|
|
191
|
+
const right = enabledEndpoints[inner]!;
|
|
192
|
+
if (endpointsConflict(left, right)) {
|
|
193
|
+
issues.push(`${left.label} and ${right.label} are configured to bind the same host/port envelope (${left.binding.host}:${left.binding.port}, ${right.binding.host}:${right.binding.port}).`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const log = readLogPosture(status.logPath, options.logTailBytes ?? 4096);
|
|
198
|
+
if (log.readError) {
|
|
199
|
+
issues.push(`Service log exists but could not be read: ${log.readError}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
config,
|
|
204
|
+
managed: {
|
|
205
|
+
...status,
|
|
206
|
+
pidPath: pidFilePath(runtime, status.platform),
|
|
207
|
+
lastError: status.actionError ?? null,
|
|
208
|
+
},
|
|
209
|
+
endpoints,
|
|
210
|
+
log,
|
|
211
|
+
issues,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function yesNo(value: boolean): string {
|
|
216
|
+
return value ? 'yes' : 'no';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function formatCliServicePosture(posture: CliServicePosture, json = false): string {
|
|
220
|
+
if (json) return JSON.stringify(posture, null, 2);
|
|
221
|
+
return [
|
|
222
|
+
'GoodVibes service',
|
|
223
|
+
` enabled: ${yesNo(posture.config.enabled)}`,
|
|
224
|
+
` autostart: ${yesNo(posture.config.autostart)}`,
|
|
225
|
+
` restartOnFailure: ${yesNo(posture.config.restartOnFailure)}`,
|
|
226
|
+
` daemon flag: ${yesNo(posture.config.daemonEnabled)}`,
|
|
227
|
+
'',
|
|
228
|
+
'Managed service:',
|
|
229
|
+
` platform: ${posture.managed.platform}`,
|
|
230
|
+
` installed: ${yesNo(posture.managed.installed)}`,
|
|
231
|
+
` running: ${yesNo(posture.managed.running)}`,
|
|
232
|
+
` pid: ${posture.managed.pid ?? 'n/a'}`,
|
|
233
|
+
` definition: ${posture.managed.path}`,
|
|
234
|
+
` pid file: ${posture.managed.pidPath}`,
|
|
235
|
+
` log: ${posture.log.path ?? 'n/a'} (${posture.log.exists ? 'present' : 'missing'})`,
|
|
236
|
+
...(posture.log.readError ? [` log read error: ${posture.log.readError}`] : []),
|
|
237
|
+
` command: ${posture.managed.commandPreview}`,
|
|
238
|
+
'',
|
|
239
|
+
'Endpoints:',
|
|
240
|
+
...posture.endpoints.map((endpoint) =>
|
|
241
|
+
` ${endpoint.label}: enabled=${yesNo(endpoint.enabled)} ${endpoint.binding.hostMode} ${endpoint.binding.host}:${endpoint.binding.port} posture=${endpoint.bindPosture.label}${endpoint.reachable === undefined ? '' : ` reachable=${yesNo(endpoint.reachable)}`}`,
|
|
242
|
+
),
|
|
243
|
+
'',
|
|
244
|
+
posture.issues.length === 0 ? 'Readiness: ready' : 'Readiness: needs attention',
|
|
245
|
+
...posture.issues.map((issue) => ` - ${issue}`),
|
|
246
|
+
].join('\n');
|
|
247
|
+
}
|