@aopslabs/aops 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +19 -0
- package/NOTICE +7 -0
- package/README.md +203 -0
- package/assets/agent-assets/core/SKILL.md +49 -0
- package/assets/agent-assets/core/references/agentspace/SKILL.md +78 -0
- package/assets/agent-assets/core/references/aops-cli-core/SKILL.md +108 -0
- package/assets/agent-assets/core/references/chatv3/SKILL.md +48 -0
- package/assets/agent-assets/core/references/collaborative-work/SKILL.md +85 -0
- package/assets/agent-assets/core/references/discuss/SKILL.md +66 -0
- package/assets/agent-assets/core/references/docman/SKILL.md +69 -0
- package/assets/agent-assets/core/references/hosted-chat/SKILL.md +64 -0
- package/assets/agent-assets/core/references/mission/SKILL.md +54 -0
- package/assets/agent-assets/core/references/projectman/SKILL.md +86 -0
- package/assets/agent-assets/core/references/sys/SKILL.md +36 -0
- package/assets/agent-assets/core/references/view/SKILL.md +58 -0
- package/assets/agent-assets/core/references/working-disciplines/SKILL.md +69 -0
- package/assets/agent-assets/core/user-guides/agent-assets.md +81 -0
- package/assets/agent-assets/core/user-guides/agentspace.md +1098 -0
- package/assets/agent-assets/core/user-guides/aops-cli.md +1107 -0
- package/assets/agent-assets/core/user-guides/aops-system.md +63 -0
- package/assets/agent-assets/core/user-guides/chatv3.md +113 -0
- package/assets/agent-assets/core/user-guides/docman.md +162 -0
- package/assets/agent-assets/core/user-guides/projectman.md +212 -0
- package/assets/agent-assets/core/user-guides/sys.md +94 -0
- package/assets/agent-assets/core/user-guides/working-disciplines.md +588 -0
- package/assets/agent-assets/gateway/aops/SKILL.md +14 -0
- package/assets/r6-remote-session-baseline.json +19 -0
- package/assets/skills/aops-install/SKILL.md +231 -0
- package/assets/skills/aops-install/agents/openai.yaml +4 -0
- package/dist/commands/activity.js +352 -0
- package/dist/commands/agent-profile.js +653 -0
- package/dist/commands/agent.js +710 -0
- package/dist/commands/agents-md.js +125 -0
- package/dist/commands/api.js +84 -0
- package/dist/commands/archive.js +873 -0
- package/dist/commands/artifact.js +535 -0
- package/dist/commands/assets.js +1414 -0
- package/dist/commands/auth/device-login.js +1 -0
- package/dist/commands/auth/legacy-export.js +177 -0
- package/dist/commands/auth/login.js +426 -0
- package/dist/commands/chat.js +1363 -0
- package/dist/commands/chatv3.js +1684 -0
- package/dist/commands/checkpoint.js +549 -0
- package/dist/commands/commercial-artifact.js +475 -0
- package/dist/commands/commercial-license.js +594 -0
- package/dist/commands/community-auth.js +62 -0
- package/dist/commands/community-cockpit.js +185 -0
- package/dist/commands/community-console.js +109 -0
- package/dist/commands/community-doctor.js +335 -0
- package/dist/commands/community-server.js +2062 -0
- package/dist/commands/community-setup.js +284 -0
- package/dist/commands/discuss.js +1448 -0
- package/dist/commands/doc.js +5219 -0
- package/dist/commands/experience.js +922 -0
- package/dist/commands/host.js +1127 -0
- package/dist/commands/init.js +118 -0
- package/dist/commands/memory.js +3129 -0
- package/dist/commands/mission.js +562 -0
- package/dist/commands/official-catalog.js +134 -0
- package/dist/commands/plan.js +84 -0
- package/dist/commands/playbook.js +415 -0
- package/dist/commands/pm/context.js +6 -0
- package/dist/commands/pm/index.js +701 -0
- package/dist/commands/pm/projectman.js +6266 -0
- package/dist/commands/project.js +1080 -0
- package/dist/commands/prompt.js +1024 -0
- package/dist/commands/repo-sync.js +1907 -0
- package/dist/commands/resource.js +527 -0
- package/dist/commands/skill.js +1211 -0
- package/dist/commands/start-disciplines.js +510 -0
- package/dist/commands/start.js +1343 -0
- package/dist/commands/target.js +309 -0
- package/dist/commands/version.js +42 -0
- package/dist/commands/view.js +1852 -0
- package/dist/launchers/cockpit.js +28 -0
- package/dist/launchers/options.js +74 -0
- package/dist/launchers/server.js +41 -0
- package/dist/lib/agent-assets/envelope.js +83 -0
- package/dist/lib/agent-assets/gateway.js +15 -0
- package/dist/lib/agent-assets/guards.js +23 -0
- package/dist/lib/agent-assets/hosted-discovery.js +148 -0
- package/dist/lib/agent-assets/hosted-package-input.js +154 -0
- package/dist/lib/agent-assets/legacy-pointer-migration.js +677 -0
- package/dist/lib/agent-assets/native-fs.js +589 -0
- package/dist/lib/agent-assets/package-manifest.js +324 -0
- package/dist/lib/agent-assets/portable-path.js +67 -0
- package/dist/lib/agent-assets/release-input.js +177 -0
- package/dist/lib/agent-assets/roots.js +45 -0
- package/dist/lib/agent-assets/runtime-binding-reader.js +545 -0
- package/dist/lib/agent-assets/runtime-targets.js +50 -0
- package/dist/lib/agent-assets/store-reader.js +1212 -0
- package/dist/lib/agent-assets/store-types.js +1 -0
- package/dist/lib/agent-assets/store-writer.js +1484 -0
- package/dist/lib/agent-assets/types.js +1 -0
- package/dist/lib/agent-assets/unicode-case-folding-v15-1.generated.js +1548 -0
- package/dist/lib/commercial-final-readiness.js +504 -0
- package/dist/lib/commercial-lifecycle-smoke-isolation.js +236 -0
- package/dist/lib/commercial-managed-postgres-docker-adapter.js +274 -0
- package/dist/lib/commercial-setup-readiness-adapter.js +179 -0
- package/dist/lib/community-client-contract.js +164 -0
- package/dist/lib/community-cockpit-lifecycle.js +601 -0
- package/dist/lib/community-command-abort.js +59 -0
- package/dist/lib/community-diagnostic.js +237 -0
- package/dist/lib/community-docker-adapter.js +498 -0
- package/dist/lib/community-home.js +31 -0
- package/dist/lib/community-instance-contract.js +148 -0
- package/dist/lib/community-instance-layout.js +97 -0
- package/dist/lib/community-lifecycle.js +1631 -0
- package/dist/lib/community-migration-snapshot.js +770 -0
- package/dist/lib/community-native-application-recovery.js +425 -0
- package/dist/lib/community-native-child.js +240 -0
- package/dist/lib/community-native-database-recovery.js +456 -0
- package/dist/lib/community-native-lifecycle.js +2458 -0
- package/dist/lib/community-native-migration.js +339 -0
- package/dist/lib/community-native-postgres.js +596 -0
- package/dist/lib/community-operation-journal.js +773 -0
- package/dist/lib/community-operation-lock.js +596 -0
- package/dist/lib/community-release-resolver.js +291 -0
- package/dist/lib/community-release-verifier.js +475 -0
- package/dist/lib/community-repo-discovery.js +252 -0
- package/dist/lib/community-setup-server-env.js +203 -0
- package/dist/lib/external-url.js +110 -0
- package/dist/lib/official-catalog-gateway.js +108 -0
- package/dist/lib/official-catalog.js +364 -0
- package/dist/lib/server-bootstrap.js +240 -0
- package/dist/lib/setup-agent-assets-bridge.js +176 -0
- package/dist/lib/setup-agent-assets-release.js +52 -0
- package/dist/lib/setup-external-postgres.js +108 -0
- package/dist/lib/setup-init-orchestrator.js +990 -0
- package/dist/lib/setup-install-guide.js +52 -0
- package/dist/lib/setup-installer-release.js +47 -0
- package/dist/lib/setup-local-postgres.js +329 -0
- package/dist/lib/setup-official-catalog-bridge.js +148 -0
- package/dist/lib/setup-readiness.js +888 -0
- package/dist/lib/tui-launcher.js +59 -0
- package/dist/main.js +150 -0
- package/dist/seeds/agents-md.js +54 -0
- package/dist/utils/agent-gateway.js +275 -0
- package/dist/utils/agents-md.js +281 -0
- package/dist/utils/api.js +255 -0
- package/dist/utils/chatv3-session-store.js +185 -0
- package/dist/utils/command.js +67 -0
- package/dist/utils/community-host-runtime.js +376 -0
- package/dist/utils/config.js +1015 -0
- package/dist/utils/discussion-workspace.js +552 -0
- package/dist/utils/experience-workspace.js +326 -0
- package/dist/utils/guide-paths.js +67 -0
- package/dist/utils/hosted-sugar.js +56 -0
- package/dist/utils/hosted-workspace.js +229 -0
- package/dist/utils/memory-workspace.js +537 -0
- package/dist/utils/operator-runtime.js +199 -0
- package/dist/utils/password-crypto.js +39 -0
- package/dist/utils/playbook-workspace.js +479 -0
- package/dist/utils/project-context.js +255 -0
- package/dist/utils/prompts.js +70 -0
- package/dist/utils/refresh-race.js +28 -0
- package/dist/utils/repo-config.js +109 -0
- package/dist/utils/repo-first-projectman.js +194 -0
- package/dist/utils/repo-first-storage.js +13 -0
- package/dist/utils/repo-first-view.js +305 -0
- package/dist/utils/session-guidance.js +278 -0
- package/dist/utils/session-state.js +137 -0
- package/dist/utils/table.js +39 -0
- package/dist/utils/transient-fs.js +49 -0
- package/dist/utils/workspace-root.js +1 -0
- package/launchers/aops-cockpit.ps1 +15 -0
- package/launchers/aops-cockpit.sh +16 -0
- package/launchers/aops-server.ps1 +15 -0
- package/launchers/aops-server.sh +16 -0
- package/native/tui/win32-x64/aops-tui.exe +0 -0
- package/package.json +81 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { healthCommunityNativeCockpit, inspectCommunityNativeCockpit, readCommunityCockpitLogs, restartCommunityNativeCockpit, startCommunityNativeCockpit, stopCommunityNativeCockpit, } from '../lib/community-cockpit-lifecycle.js';
|
|
3
|
+
import { inspectCommunityNativeInstall, inspectCommunityNativeRuntime, } from '../lib/community-native-lifecycle.js';
|
|
4
|
+
import { communityCommandAbortRuntime, withCommunityCommandAbortScope, } from '../lib/community-command-abort.js';
|
|
5
|
+
import { openExternalUrl } from '../lib/external-url.js';
|
|
6
|
+
import { runCommunityServerStart } from './community-server.js';
|
|
7
|
+
const defaultDependencies = {
|
|
8
|
+
inspectInstall: inspectCommunityNativeInstall,
|
|
9
|
+
inspectServer: inspectCommunityNativeRuntime,
|
|
10
|
+
startServer: runCommunityServerStart,
|
|
11
|
+
startCockpit: startCommunityNativeCockpit,
|
|
12
|
+
openUrl: openExternalUrl,
|
|
13
|
+
};
|
|
14
|
+
function selection(options) {
|
|
15
|
+
return { instanceName: options.instance, dataRoot: options.dataRoot };
|
|
16
|
+
}
|
|
17
|
+
function commandSelection(options) {
|
|
18
|
+
return selection(options);
|
|
19
|
+
}
|
|
20
|
+
function writeResult(value, json = false) {
|
|
21
|
+
if (json)
|
|
22
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
23
|
+
else
|
|
24
|
+
process.stdout.write(`${String(value)}\n`);
|
|
25
|
+
}
|
|
26
|
+
export async function resolveCommunityCockpit(options, dependencies = defaultDependencies, signal) {
|
|
27
|
+
const install = dependencies.inspectInstall(selection(options));
|
|
28
|
+
const requestedInstance = options.instance ?? 'default';
|
|
29
|
+
if (install.status === 'not-installed') {
|
|
30
|
+
throw new Error(`AOPS server instance "${requestedInstance}" is not installed. Run "aops setup init" first.`);
|
|
31
|
+
}
|
|
32
|
+
if (install.status !== 'installed' || !install.state) {
|
|
33
|
+
throw new Error(`AOPS server instance "${requestedInstance}" cannot open Cockpit: ${install.status}` +
|
|
34
|
+
`${install.error ? ` (${install.error})` : ''}. Run "aops doctor".`);
|
|
35
|
+
}
|
|
36
|
+
let server = await dependencies.inspectServer(selection(options));
|
|
37
|
+
let serverAction = 'already-running';
|
|
38
|
+
if (server.runtimeState === 'stopped' || server.runtimeState === 'crashed') {
|
|
39
|
+
await dependencies.startServer({
|
|
40
|
+
instance: install.state.instanceName,
|
|
41
|
+
dataRoot: install.paths.dataRoot,
|
|
42
|
+
detach: true,
|
|
43
|
+
silent: true,
|
|
44
|
+
});
|
|
45
|
+
serverAction = 'started';
|
|
46
|
+
server = await dependencies.inspectServer({
|
|
47
|
+
instanceName: install.state.instanceName,
|
|
48
|
+
dataRoot: install.paths.dataRoot,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (server.runtimeState !== 'running' || server.health !== 'healthy') {
|
|
52
|
+
throw new Error(`AOPS server instance "${install.state.instanceName}" is not ready for Cockpit: ` +
|
|
53
|
+
`${server.runtimeState}${server.reason ? ` (${server.reason})` : ''}. ` +
|
|
54
|
+
'Run "aops server status" and "aops server logs".');
|
|
55
|
+
}
|
|
56
|
+
const cockpit = await dependencies.startCockpit({ ...selection(options), signal });
|
|
57
|
+
const shouldOpen = options.open !== false;
|
|
58
|
+
if (shouldOpen) {
|
|
59
|
+
try {
|
|
60
|
+
await dependencies.openUrl(cockpit.origin);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
throw new Error(`AOPS Cockpit is ready at ${cockpit.origin}, but the default browser could not be opened: ` +
|
|
64
|
+
`${error instanceof Error ? error.message : String(error)}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
status: 'cockpit-ready',
|
|
69
|
+
instance: install.state.instanceName,
|
|
70
|
+
origin: cockpit.origin,
|
|
71
|
+
serverOrigin: cockpit.serverOrigin,
|
|
72
|
+
serverAction,
|
|
73
|
+
cockpitAction: cockpit.action,
|
|
74
|
+
opened: shouldOpen,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export async function runCommunityCockpit(options) {
|
|
78
|
+
const result = await withCommunityCommandAbortScope(communityCommandAbortRuntime, (signal) => resolveCommunityCockpit(options, defaultDependencies, signal));
|
|
79
|
+
if (options.json) {
|
|
80
|
+
writeResult(result, true);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const started = [
|
|
84
|
+
result.serverAction === 'started' ? 'AOPS Server started.' : '',
|
|
85
|
+
result.cockpitAction === 'started' ? 'Cockpit started.' : '',
|
|
86
|
+
].filter(Boolean).join(' ');
|
|
87
|
+
if (started)
|
|
88
|
+
writeResult(started);
|
|
89
|
+
writeResult(`AOPS Cockpit: ${result.origin} (API: ${result.serverOrigin})`);
|
|
90
|
+
writeResult(result.opened ? 'Opened in the default browser.' : 'Browser opening was skipped.');
|
|
91
|
+
}
|
|
92
|
+
export async function runCommunityCockpitStart(options) {
|
|
93
|
+
const result = await withCommunityCommandAbortScope(communityCommandAbortRuntime, (signal) => startCommunityNativeCockpit({ ...commandSelection(options), signal }));
|
|
94
|
+
if (options.json)
|
|
95
|
+
writeResult({ status: 'community-cockpit-running', ...result }, true);
|
|
96
|
+
else
|
|
97
|
+
writeResult(result.action === 'started'
|
|
98
|
+
? `AOPS Cockpit started at ${result.origin}. API proxy: ${result.serverOrigin}`
|
|
99
|
+
: `AOPS Cockpit is already running at ${result.origin}.`);
|
|
100
|
+
}
|
|
101
|
+
export async function runCommunityCockpitStop(options) {
|
|
102
|
+
const result = await withCommunityCommandAbortScope(communityCommandAbortRuntime, (signal) => stopCommunityNativeCockpit({ ...commandSelection(options), signal }));
|
|
103
|
+
if (options.json)
|
|
104
|
+
writeResult({
|
|
105
|
+
status: result.status === 'stopped' ? 'community-cockpit-stopped' : 'community-cockpit-already-stopped',
|
|
106
|
+
instance: result.state.instanceName,
|
|
107
|
+
processStatus: result.process?.status ?? null,
|
|
108
|
+
}, true);
|
|
109
|
+
else
|
|
110
|
+
writeResult(result.status === 'stopped' ? 'AOPS Cockpit stopped.' : 'AOPS Cockpit is already stopped.');
|
|
111
|
+
}
|
|
112
|
+
export async function runCommunityCockpitRestart(options) {
|
|
113
|
+
const result = await withCommunityCommandAbortScope(communityCommandAbortRuntime, (signal) => restartCommunityNativeCockpit({ ...commandSelection(options), signal }));
|
|
114
|
+
if (options.json)
|
|
115
|
+
writeResult({ status: 'community-cockpit-restarted', ...result }, true);
|
|
116
|
+
else
|
|
117
|
+
writeResult(`AOPS Cockpit restarted at ${result.origin}.`);
|
|
118
|
+
}
|
|
119
|
+
export async function runCommunityCockpitStatus(options) {
|
|
120
|
+
const result = await inspectCommunityNativeCockpit(commandSelection(options));
|
|
121
|
+
if (options.json)
|
|
122
|
+
writeResult({ status: 'community-cockpit-status', ...result }, true);
|
|
123
|
+
else
|
|
124
|
+
writeResult(`Cockpit ${result.runtimeState} (${result.health}) at ${result.origin}; API proxy ${result.serverOrigin}.` +
|
|
125
|
+
`${result.reason ? ` Reason: ${result.reason}` : ''}`);
|
|
126
|
+
if (['unhealthy', 'identity-conflict', 'orphaned'].includes(result.runtimeState))
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
}
|
|
129
|
+
export async function runCommunityCockpitHealth(options) {
|
|
130
|
+
const result = await healthCommunityNativeCockpit(commandSelection(options));
|
|
131
|
+
const hostHealthy = result.runtimeState === 'running' && result.health === 'healthy';
|
|
132
|
+
const healthy = hostHealthy && result.apiReady;
|
|
133
|
+
if (options.json)
|
|
134
|
+
writeResult({ status: healthy ? 'healthy' : 'unhealthy', service: 'cockpit', ...result }, true);
|
|
135
|
+
else if (healthy)
|
|
136
|
+
writeResult(`AOPS Cockpit and its API proxy are healthy at ${result.origin}.`);
|
|
137
|
+
else if (hostHealthy)
|
|
138
|
+
writeResult(`AOPS Cockpit host is healthy at ${result.origin}, but its API proxy to ${result.serverOrigin} is unavailable.`);
|
|
139
|
+
else
|
|
140
|
+
writeResult(`AOPS Cockpit is not healthy: ${result.runtimeState}${result.reason ? ` (${result.reason})` : ''}.`);
|
|
141
|
+
if (!healthy)
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
}
|
|
144
|
+
export async function runCommunityCockpitLogs(options) {
|
|
145
|
+
const result = readCommunityCockpitLogs({ ...commandSelection(options), tail: Number(options.tail ?? 100) });
|
|
146
|
+
if (options.json)
|
|
147
|
+
writeResult({ status: 'community-cockpit-logs', ...result }, true);
|
|
148
|
+
else
|
|
149
|
+
writeResult(result.content || 'No detached Cockpit logs are available.');
|
|
150
|
+
}
|
|
151
|
+
function common(command) {
|
|
152
|
+
return command
|
|
153
|
+
.option('--instance <name>', 'Installation instance name', 'default')
|
|
154
|
+
.option('--data-root <path>', 'Absolute Community data root override')
|
|
155
|
+
.option('--json', 'Output JSON');
|
|
156
|
+
}
|
|
157
|
+
export function mergeCommunityCockpitOptions(options, command) {
|
|
158
|
+
return {
|
|
159
|
+
...options,
|
|
160
|
+
...(typeof command.optsWithGlobals === 'function' ? command.optsWithGlobals() : {}),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
export function makeCommunityCockpitCommand() {
|
|
164
|
+
const command = common(new Command('cockpit')
|
|
165
|
+
.description('Open and independently operate AOPS Cockpit on its own loopback port')
|
|
166
|
+
.option('--no-open', 'Prepare Cockpit and print its URL without opening a browser'))
|
|
167
|
+
.action((options, invoked) => runCommunityCockpit(mergeCommunityCockpitOptions(options, invoked)));
|
|
168
|
+
common(command.command('open').description('Start stopped local services as needed and open Cockpit'))
|
|
169
|
+
.option('--no-open', 'Prepare Cockpit and print its URL without opening a browser')
|
|
170
|
+
.action((options, invoked) => runCommunityCockpit(mergeCommunityCockpitOptions(options, invoked)));
|
|
171
|
+
common(command.command('start').description('Start only the Cockpit process; do not start the AOPS Server'))
|
|
172
|
+
.action((options, invoked) => runCommunityCockpitStart(mergeCommunityCockpitOptions(options, invoked)));
|
|
173
|
+
common(command.command('stop').description('Stop only the Cockpit process; do not stop the AOPS Server'))
|
|
174
|
+
.action((options, invoked) => runCommunityCockpitStop(mergeCommunityCockpitOptions(options, invoked)));
|
|
175
|
+
common(command.command('restart').description('Restart only the Cockpit process'))
|
|
176
|
+
.action((options, invoked) => runCommunityCockpitRestart(mergeCommunityCockpitOptions(options, invoked)));
|
|
177
|
+
common(command.command('status').description('Show Cockpit process status without mutation'))
|
|
178
|
+
.action((options, invoked) => runCommunityCockpitStatus(mergeCommunityCockpitOptions(options, invoked)));
|
|
179
|
+
common(command.command('health').description('Check the Cockpit host health without mutation'))
|
|
180
|
+
.action((options, invoked) => runCommunityCockpitHealth(mergeCommunityCockpitOptions(options, invoked)));
|
|
181
|
+
common(command.command('logs').description('Show recent Cockpit logs'))
|
|
182
|
+
.option('--tail <number>', 'Number of log lines', '100')
|
|
183
|
+
.action((options, invoked) => runCommunityCockpitLogs(mergeCommunityCockpitOptions(options, invoked)));
|
|
184
|
+
return command;
|
|
185
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { logInfo } from '@aopslabs/xf-cli-ui';
|
|
3
|
+
import { promptSelect } from '../utils/prompts.js';
|
|
4
|
+
import { runCommercialLicenseStatus } from './commercial-license.js';
|
|
5
|
+
import { runCommunityDoctor } from './community-doctor.js';
|
|
6
|
+
import { runCommunityCockpit, runCommunityCockpitHealth, runCommunityCockpitLogs, runCommunityCockpitRestart, runCommunityCockpitStart, runCommunityCockpitStatus, runCommunityCockpitStop, } from './community-cockpit.js';
|
|
7
|
+
import { runCommunityServerHealth, runCommunityServerLogs, runCommunityServerRestart, runCommunityServerStart, runCommunityServerStatus, runCommunityServerStop, } from './community-server.js';
|
|
8
|
+
export const COMMUNITY_CONSOLE_CHOICES = Object.freeze([
|
|
9
|
+
{ name: 'Open Cockpit — starts stopped services if needed', value: 'cockpit' },
|
|
10
|
+
{ name: 'Show both service statuses', value: 'status' },
|
|
11
|
+
{ name: 'Start AOPS Server — 5900', value: 'server-start' },
|
|
12
|
+
{ name: 'Start AOPS Server in this terminal — live logs, Ctrl+C to stop', value: 'server-start-foreground' },
|
|
13
|
+
{ name: 'Stop AOPS Server — 5900', value: 'server-stop' },
|
|
14
|
+
{ name: 'Restart AOPS Server — 5900', value: 'server-restart' },
|
|
15
|
+
{ name: 'Check AOPS Server health', value: 'server-health' },
|
|
16
|
+
{ name: 'Show AOPS Server logs', value: 'server-logs' },
|
|
17
|
+
{ name: 'Start Cockpit — 5922', value: 'cockpit-start' },
|
|
18
|
+
{ name: 'Stop Cockpit — 5922', value: 'cockpit-stop' },
|
|
19
|
+
{ name: 'Restart Cockpit — 5922', value: 'cockpit-restart' },
|
|
20
|
+
{ name: 'Check Cockpit health', value: 'cockpit-health' },
|
|
21
|
+
{ name: 'Show Cockpit logs', value: 'cockpit-logs' },
|
|
22
|
+
{ name: 'Show commercial license and domain-admission status', value: 'license-status' },
|
|
23
|
+
{ name: 'Run Doctor (read-only)', value: 'doctor' },
|
|
24
|
+
{ name: 'Back', value: 'back' },
|
|
25
|
+
]);
|
|
26
|
+
export async function runCommunityConsoleAction(action, options, dependencies = {}) {
|
|
27
|
+
if (action === 'cockpit')
|
|
28
|
+
return (dependencies.runCockpit ?? runCommunityCockpit)(options);
|
|
29
|
+
if (action === 'status') {
|
|
30
|
+
await (dependencies.runServerStatus ?? runCommunityServerStatus)(options);
|
|
31
|
+
await (dependencies.runCockpitStatus ?? runCommunityCockpitStatus)(options);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (action === 'server-health')
|
|
35
|
+
return (dependencies.runServerHealth ?? runCommunityServerHealth)(options);
|
|
36
|
+
if (action === 'server-start')
|
|
37
|
+
return (dependencies.runServerStart ?? runCommunityServerStart)(options);
|
|
38
|
+
if (action === 'server-start-foreground')
|
|
39
|
+
return (dependencies.runServerStart ?? runCommunityServerStart)({
|
|
40
|
+
...options,
|
|
41
|
+
foreground: true,
|
|
42
|
+
});
|
|
43
|
+
if (action === 'server-restart')
|
|
44
|
+
return (dependencies.runServerRestart ?? runCommunityServerRestart)(options);
|
|
45
|
+
if (action === 'server-stop')
|
|
46
|
+
return (dependencies.runServerStop ?? runCommunityServerStop)(options);
|
|
47
|
+
if (action === 'server-logs')
|
|
48
|
+
return (dependencies.runServerLogs ?? runCommunityServerLogs)(options);
|
|
49
|
+
if (action === 'cockpit-health')
|
|
50
|
+
return (dependencies.runCockpitHealth ?? runCommunityCockpitHealth)(options);
|
|
51
|
+
if (action === 'cockpit-start')
|
|
52
|
+
return (dependencies.runCockpitStart ?? runCommunityCockpitStart)(options);
|
|
53
|
+
if (action === 'cockpit-restart')
|
|
54
|
+
return (dependencies.runCockpitRestart ?? runCommunityCockpitRestart)(options);
|
|
55
|
+
if (action === 'cockpit-stop')
|
|
56
|
+
return (dependencies.runCockpitStop ?? runCommunityCockpitStop)(options);
|
|
57
|
+
if (action === 'cockpit-logs')
|
|
58
|
+
return (dependencies.runCockpitLogs ?? runCommunityCockpitLogs)(options);
|
|
59
|
+
if (action === 'license-status') {
|
|
60
|
+
return (dependencies.runLicenseStatus ?? runCommercialLicenseStatus)({ json: options.json });
|
|
61
|
+
}
|
|
62
|
+
return (dependencies.runDoctor ?? runCommunityDoctor)(options);
|
|
63
|
+
}
|
|
64
|
+
export async function runCommunityConsole(options = {}, dependencies = {}) {
|
|
65
|
+
const interactive = dependencies.isInteractive?.() ??
|
|
66
|
+
(process.stdin.isTTY === true && process.stdout.isTTY === true);
|
|
67
|
+
if (!interactive || options.json === true) {
|
|
68
|
+
console.log(JSON.stringify({
|
|
69
|
+
status: 'needs-input',
|
|
70
|
+
mutationPerformed: false,
|
|
71
|
+
reason: 'community_console_tty_required',
|
|
72
|
+
next: [
|
|
73
|
+
'aops server status --json',
|
|
74
|
+
'aops server start --json',
|
|
75
|
+
'aops server stop --json',
|
|
76
|
+
'aops cockpit status --json',
|
|
77
|
+
'aops cockpit start --json',
|
|
78
|
+
'aops cockpit stop --json',
|
|
79
|
+
'aops license status --json',
|
|
80
|
+
],
|
|
81
|
+
}, null, 2));
|
|
82
|
+
process.exitCode = 2;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const select = dependencies.select ?? promptSelect;
|
|
86
|
+
const info = dependencies.info ?? logInfo;
|
|
87
|
+
info('AOPS Server and Cockpit use separate processes and ports: 5900 and 5922.');
|
|
88
|
+
await (dependencies.runServerStatus ?? runCommunityServerStatus)(options);
|
|
89
|
+
await (dependencies.runCockpitStatus ?? runCommunityCockpitStatus)(options);
|
|
90
|
+
while (true) {
|
|
91
|
+
const action = await select({
|
|
92
|
+
message: 'Server and Cockpit:',
|
|
93
|
+
type: process.env.AOPS_CLI_MENU_STYLE?.toLowerCase() === 'rawlist' ? 'rawlist' : 'select',
|
|
94
|
+
pageSize: COMMUNITY_CONSOLE_CHOICES.length,
|
|
95
|
+
choices: [...COMMUNITY_CONSOLE_CHOICES],
|
|
96
|
+
});
|
|
97
|
+
if (action === 'back')
|
|
98
|
+
return;
|
|
99
|
+
await runCommunityConsoleAction(action, options, dependencies);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function makeCommunityConsoleCommand() {
|
|
103
|
+
return new Command('console')
|
|
104
|
+
.description('Guided server and Cockpit controls; non-TTY returns needs-input without mutation')
|
|
105
|
+
.option('--instance <name>', 'Installation instance name', 'default')
|
|
106
|
+
.option('--data-root <path>', 'Absolute Community data root override')
|
|
107
|
+
.option('--json', 'Return the non-interactive needs-input contract')
|
|
108
|
+
.action((options) => runCommunityConsole(options));
|
|
109
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, statfsSync } from 'node:fs';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { readCommercialIntegrityVerificationSchedule, resolveManagedInstallationLayout, } from '@aopslabs/artifact-trust-contracts';
|
|
7
|
+
import { buildCommunityComposeInvocation, inspectCommunityInstall, readCommunityRuntimePort, } from '../lib/community-lifecycle.js';
|
|
8
|
+
import { buildCommunityInstanceContract } from '../lib/community-instance-contract.js';
|
|
9
|
+
import { resolveCommunityCliIdentity } from '../lib/community-client-contract.js';
|
|
10
|
+
import { communityProcessRuntime } from '../lib/community-docker-adapter.js';
|
|
11
|
+
import { resolveCommunityInstanceLayout } from '../lib/community-instance-layout.js';
|
|
12
|
+
import { inspectCommunityNativeInstall } from '../lib/community-native-lifecycle.js';
|
|
13
|
+
import { inspectCommunityNativePostgres } from '../lib/community-native-postgres.js';
|
|
14
|
+
import { getActiveApiTarget, getApiTarget } from '../utils/config.js';
|
|
15
|
+
import { inspectTargetDoctor } from './target.js';
|
|
16
|
+
function installSelection(options) {
|
|
17
|
+
if (options.dataRoot)
|
|
18
|
+
return { instanceName: options.instance, dataRoot: options.dataRoot };
|
|
19
|
+
const layout = resolveCommunityInstanceLayout({ instanceId: options.instance });
|
|
20
|
+
return { instanceName: layout.instanceId, dataRoot: layout.dataRoot };
|
|
21
|
+
}
|
|
22
|
+
function existingAncestor(candidate) {
|
|
23
|
+
let cursor = path.resolve(candidate);
|
|
24
|
+
while (!existsSync(cursor)) {
|
|
25
|
+
const parent = path.dirname(cursor);
|
|
26
|
+
if (parent === cursor)
|
|
27
|
+
return cursor;
|
|
28
|
+
cursor = parent;
|
|
29
|
+
}
|
|
30
|
+
return cursor;
|
|
31
|
+
}
|
|
32
|
+
function probePort(port) {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const server = net.createServer();
|
|
35
|
+
server.unref();
|
|
36
|
+
server.once('error', () => resolve(false));
|
|
37
|
+
server.listen({ host: '127.0.0.1', port, exclusive: true }, () => server.close(() => resolve(true)));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function inspectCommercialIntegrity() {
|
|
41
|
+
const layout = resolveManagedInstallationLayout();
|
|
42
|
+
const activePointer = path.join(layout.storeRoot, 'state', 'active.json');
|
|
43
|
+
if (!existsSync(activePointer)) {
|
|
44
|
+
return {
|
|
45
|
+
id: 'commercial-integrity',
|
|
46
|
+
status: 'skip',
|
|
47
|
+
detail: 'state=inactive; no managed commercial artifact is active',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const schedule = readCommercialIntegrityVerificationSchedule(layout.stateRoot);
|
|
52
|
+
const status = schedule.state === 'waiting'
|
|
53
|
+
? 'pass'
|
|
54
|
+
: schedule.state === 'due'
|
|
55
|
+
? 'warn'
|
|
56
|
+
: 'fail';
|
|
57
|
+
return {
|
|
58
|
+
id: 'commercial-integrity',
|
|
59
|
+
status,
|
|
60
|
+
detail: `state=${schedule.state}; verifiedAt=${schedule.verifiedAt ?? 'unknown'}; dueAt=${schedule.dueAt ?? 'unknown'}; intervalHours=${schedule.minimumIntervalHours}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return {
|
|
65
|
+
id: 'commercial-integrity',
|
|
66
|
+
status: 'fail',
|
|
67
|
+
detail: `state=invalid; reason=${error instanceof Error ? error.message : 'commercial_integrity_diagnostics_failed'}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function anonymousRegistryCheck(imageDigest, fetchImpl) {
|
|
72
|
+
try {
|
|
73
|
+
const tokenResponse = await fetchImpl('https://ghcr.io/token?service=ghcr.io&scope=repository:eeemzs/aops-community:pull', { signal: AbortSignal.timeout(8_000) });
|
|
74
|
+
if (!tokenResponse.ok)
|
|
75
|
+
return { id: 'anonymous-registry', status: 'fail', detail: `token HTTP ${tokenResponse.status}` };
|
|
76
|
+
const token = String((await tokenResponse.json()).token ?? '');
|
|
77
|
+
if (!token)
|
|
78
|
+
return { id: 'anonymous-registry', status: 'fail', detail: 'anonymous token missing' };
|
|
79
|
+
if (!imageDigest)
|
|
80
|
+
return { id: 'anonymous-registry', status: 'warn', detail: 'anonymous token works; exact release digest unavailable before setup' };
|
|
81
|
+
const manifestResponse = await fetchImpl(`https://ghcr.io/v2/eeemzs/aops-community/manifests/${imageDigest}`, {
|
|
82
|
+
headers: {
|
|
83
|
+
authorization: `Bearer ${token}`,
|
|
84
|
+
accept: 'application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json',
|
|
85
|
+
},
|
|
86
|
+
signal: AbortSignal.timeout(8_000),
|
|
87
|
+
});
|
|
88
|
+
const observed = manifestResponse.headers.get('docker-content-digest');
|
|
89
|
+
return {
|
|
90
|
+
id: 'anonymous-registry',
|
|
91
|
+
status: manifestResponse.ok && observed === imageDigest ? 'pass' : 'fail',
|
|
92
|
+
detail: `manifest HTTP ${manifestResponse.status}; digest=${observed ?? 'missing'}`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
return { id: 'anonymous-registry', status: 'fail', detail: error instanceof Error ? error.message : String(error) };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export async function inspectCommunityDoctor(options = {}, dependencies = {}) {
|
|
100
|
+
const spawn = dependencies.spawn ?? spawnSync;
|
|
101
|
+
const fetchImpl = dependencies.fetchImpl ?? fetch;
|
|
102
|
+
const platform = dependencies.platform ?? process.platform;
|
|
103
|
+
const arch = dependencies.arch ?? process.arch;
|
|
104
|
+
const checks = [];
|
|
105
|
+
const cli = resolveCommunityCliIdentity();
|
|
106
|
+
if (options.target && options.runtime)
|
|
107
|
+
throw new Error('community_doctor_selector_conflict:choose_--target_or_--runtime');
|
|
108
|
+
if (!options.runtime && (options.postgres || options.postgresConfig || options.postgresTls)) {
|
|
109
|
+
throw new Error('community_doctor_postgres_requires_runtime_native');
|
|
110
|
+
}
|
|
111
|
+
const selectedTarget = options.target
|
|
112
|
+
? getApiTarget(options.target)
|
|
113
|
+
: options.runtime
|
|
114
|
+
? undefined
|
|
115
|
+
: getActiveApiTarget();
|
|
116
|
+
if (options.target && !selectedTarget)
|
|
117
|
+
throw new Error(`aops_target_not_found:${options.target}`);
|
|
118
|
+
const selection = installSelection(options);
|
|
119
|
+
const inspection = inspectCommunityInstall(selection);
|
|
120
|
+
const nativeInspection = inspectCommunityNativeInstall(selection);
|
|
121
|
+
const explicitContract = options.runtime
|
|
122
|
+
? buildCommunityInstanceContract({ ...options, port: 5900 })
|
|
123
|
+
: undefined;
|
|
124
|
+
const profile = selectedTarget
|
|
125
|
+
? 'target-only'
|
|
126
|
+
: explicitContract?.profile
|
|
127
|
+
?? (nativeInspection.status === 'installed' ? nativeInspection.state.profile : undefined)
|
|
128
|
+
?? (inspection.status === 'installed' ? 'oci-managed-stack' : 'unconfigured');
|
|
129
|
+
const needsDocker = profile === 'oci-managed-stack' || profile === 'native-container-postgres';
|
|
130
|
+
const needsCompose = profile === 'oci-managed-stack';
|
|
131
|
+
checks.push({ id: 'cli-identity', status: 'pass', detail: `${cli.packageName}@${cli.version}; schema=${cli.commandSchemaVersion}; source=${cli.artifactSource}` });
|
|
132
|
+
checks.push(inspectCommercialIntegrity());
|
|
133
|
+
const osArchSupported = ['win32', 'darwin', 'linux'].includes(platform) && ['x64', 'arm64'].includes(arch);
|
|
134
|
+
checks.push({ id: 'os-arch', status: osArchSupported ? 'pass' : 'fail', detail: `${platform}/${arch}` });
|
|
135
|
+
if (needsDocker) {
|
|
136
|
+
const docker = spawn('docker', ['--version'], { encoding: 'utf8', windowsHide: true });
|
|
137
|
+
checks.push({
|
|
138
|
+
id: 'docker-cli',
|
|
139
|
+
status: docker.status === 0 ? 'pass' : 'fail',
|
|
140
|
+
detail: docker.status === 0 ? String(docker.stdout).trim() : String(docker.stderr || docker.error?.message || 'docker unavailable').trim(),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
else
|
|
144
|
+
checks.push({ id: 'docker-cli', status: 'skip', detail: `${profile} does not require a Docker application runtime.` });
|
|
145
|
+
if (platform === 'win32' && needsDocker) {
|
|
146
|
+
const wsl = spawn('wsl.exe', ['--status'], { encoding: 'utf8', windowsHide: true });
|
|
147
|
+
checks.push({
|
|
148
|
+
id: 'wsl2',
|
|
149
|
+
status: wsl.status === 0 ? 'pass' : 'fail',
|
|
150
|
+
detail: wsl.status === 0 ? String(wsl.stdout).replace(/\0/g, '').trim().slice(-1000) : String(wsl.stderr || wsl.error?.message || 'WSL2 unavailable').replace(/\0/g, '').trim(),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
else
|
|
154
|
+
checks.push({ id: 'wsl2', status: 'skip', detail: needsDocker ? 'WSL2 check applies only to Windows.' : 'No Docker runtime selected.' });
|
|
155
|
+
if (needsCompose) {
|
|
156
|
+
const compose = spawn('docker', ['compose', 'version'], { encoding: 'utf8', windowsHide: true });
|
|
157
|
+
checks.push({
|
|
158
|
+
id: 'docker-compose',
|
|
159
|
+
status: compose.status === 0 ? 'pass' : 'fail',
|
|
160
|
+
detail: compose.status === 0 ? String(compose.stdout).trim() : String(compose.stderr || compose.error?.message || 'docker compose unavailable').trim(),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
else
|
|
164
|
+
checks.push({ id: 'docker-compose', status: 'skip', detail: `${profile} does not require the application Compose stack.` });
|
|
165
|
+
checks.push({
|
|
166
|
+
id: 'sigstore-verifier',
|
|
167
|
+
status: needsCompose ? 'pass' : 'skip',
|
|
168
|
+
detail: needsCompose ? 'bundled sigstore-js verifier; no external cosign installation required' : 'Signed OCI release verification is not used by this profile.',
|
|
169
|
+
});
|
|
170
|
+
if (profile === 'target-only') {
|
|
171
|
+
checks.push({ id: 'disk-space', status: 'skip', detail: 'The selected server is not managed as a local instance.' });
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const filesystem = dependencies.statfs?.(existingAncestor(inspection.paths.dataRoot))
|
|
175
|
+
?? statfsSync(existingAncestor(inspection.paths.dataRoot), { bigint: true });
|
|
176
|
+
const freeBytes = filesystem.bavail * filesystem.bsize;
|
|
177
|
+
const minimumBytes = 2n * 1024n * 1024n * 1024n;
|
|
178
|
+
checks.push({ id: 'disk-space', status: freeBytes >= minimumBytes ? 'pass' : 'fail', detail: `${(Number(freeBytes) / (1024 ** 3)).toFixed(1)} GiB free; 2.0 GiB minimum` });
|
|
179
|
+
}
|
|
180
|
+
checks.push({
|
|
181
|
+
id: 'install-state',
|
|
182
|
+
status: profile === 'target-only'
|
|
183
|
+
? 'skip'
|
|
184
|
+
: nativeInspection.status === 'installed' && profile !== 'oci-managed-stack'
|
|
185
|
+
? 'pass'
|
|
186
|
+
: nativeInspection.status === 'partial' || nativeInspection.status === 'runtime-conflict'
|
|
187
|
+
? 'fail'
|
|
188
|
+
: inspection.status === 'installed' && profile === 'oci-managed-stack'
|
|
189
|
+
? 'pass'
|
|
190
|
+
: inspection.status === 'partial'
|
|
191
|
+
? 'fail'
|
|
192
|
+
: 'warn',
|
|
193
|
+
detail: profile === 'target-only'
|
|
194
|
+
? 'The selected target is independent of any local installation state.'
|
|
195
|
+
: nativeInspection.status === 'installed' && profile !== 'oci-managed-stack'
|
|
196
|
+
? `installed; profile=${nativeInspection.state.profile}; root=${nativeInspection.paths.instanceRoot}`
|
|
197
|
+
: nativeInspection.status === 'partial' || nativeInspection.status === 'runtime-conflict'
|
|
198
|
+
? `${nativeInspection.error ?? nativeInspection.status}; missing=${nativeInspection.missingFiles.join(',') || 'none'}`
|
|
199
|
+
: inspection.status === 'partial'
|
|
200
|
+
? `${inspection.error ?? 'partial install'}; missing=${inspection.missingFiles.join(',') || 'none'}`
|
|
201
|
+
: `${inspection.status}; profile=${profile}; root=${inspection.paths.instanceRoot}`,
|
|
202
|
+
});
|
|
203
|
+
if (profile === 'target-only' && selectedTarget) {
|
|
204
|
+
checks.push({ id: 'release-cache', status: 'skip', detail: 'No local release cache is required for a server target.' });
|
|
205
|
+
checks.push({ id: 'container-status', status: 'skip', detail: 'The target server lifecycle is not owned by this CLI instance.' });
|
|
206
|
+
checks.push({ id: 'port', status: 'skip', detail: `Remote/existing endpoint: ${selectedTarget.apiBaseUrl}` });
|
|
207
|
+
checks.push({ id: 'cockpit-port', status: 'skip', detail: 'The target Cockpit lifecycle is not owned by this CLI instance.' });
|
|
208
|
+
const targetDoctor = await (dependencies.targetDoctor ?? inspectTargetDoctor)(selectedTarget.name, { timeoutMs: options.timeoutMs, json: true });
|
|
209
|
+
const compatibility = targetDoctor.compatibility;
|
|
210
|
+
checks.push({ id: 'server-health', status: targetDoctor.status === 'unavailable' ? 'fail' : 'pass', detail: `target=${selectedTarget.name}; status=${targetDoctor.status}` });
|
|
211
|
+
checks.push({ id: 'compatibility', status: compatibility.compatible ? (compatibility.status === 'warning' ? 'warn' : 'pass') : 'fail', detail: String(compatibility.reason ?? 'unknown') });
|
|
212
|
+
}
|
|
213
|
+
else if (inspection.status === 'installed' && profile === 'oci-managed-stack') {
|
|
214
|
+
const state = inspection.state;
|
|
215
|
+
checks.push({ id: 'release-cache', status: 'pass', detail: `${state.activeRelease.releaseVersion} ${state.activeRelease.imageRef}` });
|
|
216
|
+
const status = await communityProcessRuntime.run(buildCommunityComposeInvocation({ paths: inspection.paths, state, action: 'status' }));
|
|
217
|
+
checks.push({ id: 'container-status', status: status.exitCode === 0 ? 'pass' : 'fail', detail: (status.stdout || status.stderr || `exit=${status.exitCode}`).trim().slice(-2000) });
|
|
218
|
+
const port = readCommunityRuntimePort(inspection.paths);
|
|
219
|
+
checks.push({ id: 'port', status: 'pass', detail: `configured=${port}; installed service is expected to own it` });
|
|
220
|
+
checks.push({ id: 'cockpit-port', status: 'skip', detail: 'OCI compatibility profile serves Cockpit through its combined edge.' });
|
|
221
|
+
try {
|
|
222
|
+
const response = await fetchImpl(`http://127.0.0.1:${port}/api/health`, { signal: AbortSignal.timeout(5_000) });
|
|
223
|
+
checks.push({ id: 'server-health', status: response.ok ? 'pass' : 'fail', detail: `HTTP ${response.status} on port ${port}` });
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
checks.push({ id: 'server-health', status: 'fail', detail: error instanceof Error ? error.message : String(error) });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (nativeInspection.status === 'installed' && nativeInspection.state && profile !== 'oci-managed-stack') {
|
|
230
|
+
const state = nativeInspection.state;
|
|
231
|
+
checks.push({ id: 'release-cache', status: 'skip', detail: `Native source checkout ${state.source.releaseVersion}; no OCI release cache.` });
|
|
232
|
+
if (state.postgres.mode === 'container') {
|
|
233
|
+
try {
|
|
234
|
+
const postgres = await inspectCommunityNativePostgres({
|
|
235
|
+
state: state.postgres,
|
|
236
|
+
instanceName: state.instanceName,
|
|
237
|
+
instanceRoot: nativeInspection.paths.instanceRoot,
|
|
238
|
+
});
|
|
239
|
+
checks.push({
|
|
240
|
+
id: 'container-status',
|
|
241
|
+
status: postgres.container === 'running' && postgres.health === 'healthy' ? 'pass' : 'fail',
|
|
242
|
+
detail: `postgres=${postgres.container}; health=${postgres.health}; image=${postgres.imageRef}`,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
checks.push({ id: 'container-status', status: 'fail', detail: error instanceof Error ? error.message : String(error) });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
checks.push({ id: 'container-status', status: 'skip', detail: 'External PostgreSQL is owned outside this CLI instance.' });
|
|
251
|
+
}
|
|
252
|
+
checks.push({ id: 'port', status: 'pass', detail: `configured=${state.server.port}; installed native service is expected to own it` });
|
|
253
|
+
checks.push({ id: 'cockpit-port', status: 'pass', detail: `configured=${state.cockpit.port}; independent Cockpit service is expected to own it` });
|
|
254
|
+
try {
|
|
255
|
+
const response = await fetchImpl(`http://127.0.0.1:${state.server.port}/api/health`, { signal: AbortSignal.timeout(5_000) });
|
|
256
|
+
checks.push({ id: 'server-health', status: response.ok ? 'pass' : 'fail', detail: `HTTP ${response.status} on port ${state.server.port}` });
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
checks.push({ id: 'server-health', status: 'fail', detail: error instanceof Error ? error.message : String(error) });
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
const cockpitOrigin = `http://127.0.0.1:${state.cockpit.port}`;
|
|
263
|
+
const [hostResponse, proxyResponse] = await Promise.all([
|
|
264
|
+
fetchImpl(`${cockpitOrigin}/_aops/cockpit-health`, { signal: AbortSignal.timeout(5_000) }),
|
|
265
|
+
fetchImpl(`${cockpitOrigin}/api/health`, {
|
|
266
|
+
headers: { origin: cockpitOrigin },
|
|
267
|
+
signal: AbortSignal.timeout(5_000),
|
|
268
|
+
}),
|
|
269
|
+
]);
|
|
270
|
+
checks.push({
|
|
271
|
+
id: 'cockpit-health',
|
|
272
|
+
status: hostResponse.ok && proxyResponse.ok ? 'pass' : 'fail',
|
|
273
|
+
detail: `host HTTP ${hostResponse.status}; API proxy HTTP ${proxyResponse.status}; port ${state.cockpit.port}`,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
checks.push({ id: 'cockpit-health', status: 'fail', detail: error instanceof Error ? error.message : String(error) });
|
|
278
|
+
}
|
|
279
|
+
checks.push({ id: 'compatibility', status: 'skip', detail: 'Local native runtime uses the installed CLI/server contract.' });
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
const selectedServerPort = explicitContract?.server.port ?? 5900;
|
|
283
|
+
const selectedCockpitPort = selectedServerPort <= 65_513 ? selectedServerPort + 22 : selectedServerPort - 22;
|
|
284
|
+
const [available, cockpitAvailable] = await Promise.all([
|
|
285
|
+
(dependencies.portProbe ?? probePort)(selectedServerPort),
|
|
286
|
+
(dependencies.portProbe ?? probePort)(selectedCockpitPort),
|
|
287
|
+
]);
|
|
288
|
+
checks.push({ id: 'release-cache', status: 'skip', detail: 'No complete OCI installation to inspect.' });
|
|
289
|
+
checks.push({ id: 'port', status: available ? 'pass' : 'fail', detail: available ? `${selectedServerPort} is available` : `${selectedServerPort} is already in use` });
|
|
290
|
+
checks.push({ id: 'cockpit-port', status: cockpitAvailable ? 'pass' : 'fail', detail: cockpitAvailable ? `${selectedCockpitPort} is available` : `${selectedCockpitPort} is already in use` });
|
|
291
|
+
checks.push({ id: 'container-status', status: 'skip', detail: 'No complete local installation to inspect.' });
|
|
292
|
+
checks.push({ id: 'server-health', status: 'skip', detail: 'No configured server target or running local instance.' });
|
|
293
|
+
checks.push({ id: 'cockpit-health', status: 'skip', detail: 'No configured local Cockpit instance.' });
|
|
294
|
+
checks.push({ id: 'compatibility', status: 'skip', detail: 'Select a target or start a local instance first.' });
|
|
295
|
+
}
|
|
296
|
+
if (profile === 'oci-managed-stack') {
|
|
297
|
+
checks.push(await anonymousRegistryCheck(inspection.status === 'installed' ? inspection.state.activeRelease.imageIndexDigest : undefined, fetchImpl));
|
|
298
|
+
}
|
|
299
|
+
else
|
|
300
|
+
checks.push({ id: 'anonymous-registry', status: 'skip', detail: `${profile} does not pull the AOPS application image.` });
|
|
301
|
+
return {
|
|
302
|
+
status: checks.some((check) => check.status === 'fail') ? 'attention-required' : 'healthy',
|
|
303
|
+
mutationFree: true,
|
|
304
|
+
profile,
|
|
305
|
+
cli,
|
|
306
|
+
selectedTarget: selectedTarget ?? null,
|
|
307
|
+
checks,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
export async function runCommunityDoctor(options = {}) {
|
|
311
|
+
const result = await inspectCommunityDoctor(options);
|
|
312
|
+
if (options.json)
|
|
313
|
+
console.log(JSON.stringify(result, null, 2));
|
|
314
|
+
else {
|
|
315
|
+
for (const check of result.checks)
|
|
316
|
+
console.log(`${check.status.toUpperCase().padEnd(4)} ${check.id}: ${check.detail}`);
|
|
317
|
+
console.log(`Result: ${result.status}`);
|
|
318
|
+
}
|
|
319
|
+
if (result.status !== 'healthy')
|
|
320
|
+
process.exitCode = 1;
|
|
321
|
+
}
|
|
322
|
+
export function makeCommunityDoctorCommand() {
|
|
323
|
+
return new Command('doctor')
|
|
324
|
+
.description('Run mutation-free, profile-aware CLI/target/local-instance checks')
|
|
325
|
+
.option('--instance <name>', 'Local instance name', 'default')
|
|
326
|
+
.option('--data-root <path>', 'Absolute Community data root override')
|
|
327
|
+
.option('--runtime <native|oci>', 'Preview checks for an explicit application runtime')
|
|
328
|
+
.option('--postgres <external|container>', 'PostgreSQL profile for --runtime native')
|
|
329
|
+
.option('--postgres-config <path>', 'External PostgreSQL config reference')
|
|
330
|
+
.option('--postgres-tls <disable|require|verify-full>', 'External PostgreSQL TLS policy')
|
|
331
|
+
.option('--target <name>', 'Named existing server target')
|
|
332
|
+
.option('--timeout-ms <ms>', 'Target request timeout', (value) => Number.parseInt(value, 10))
|
|
333
|
+
.option('--json', 'Output JSON')
|
|
334
|
+
.action(runCommunityDoctor);
|
|
335
|
+
}
|