@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,2062 @@
|
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, rmSync, } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { abandonInterruptedCommunityRecovery, assertCommunityLedgerHasNoNonterminalOperation, assertNoCommunityRecoveryCommitJournal, assertCommunityRecoveryMutationFence, buildCommunityComposeInvocation, inspectCommunityInstall, readCommunityBackupReceipt, reconcileCommunityRecoveryCommit, recoverCommunityUpdate, resolveCommunityInstallPaths, restoreCommunitySourceRuntime, restoreCommunityBackup, rollbackCommunityInstall, setupCommunityInstall, stageCommunityRelease, updateCommunityInstall, verifyStagedCommunityRelease, verifyCommunityBackupRecord, writeCommunityBackupReceipt, } from '../lib/community-lifecycle.js';
|
|
5
|
+
import { communityProcessRuntime, createCommunityDockerAdapter } from '../lib/community-docker-adapter.js';
|
|
6
|
+
import { communityCommandAbortRuntime, throwIfCommunityCommandAborted, withCommunityCommandAbortScope, } from '../lib/community-command-abort.js';
|
|
7
|
+
import { resolveCommunityInstanceLayout } from '../lib/community-instance-layout.js';
|
|
8
|
+
import { assertCommunityOperationJournalFence, captureCommunityOperationDigests, createCommunityOperationJournal, digestsEqual, finishCommunityOperationJournal, inspectCommunityOperationJournalFile, openCommunityOperationJournalForReconciliation, runCommunityJournaledPromotion, runCommunityJournaledSideEffect, } from '../lib/community-operation-journal.js';
|
|
9
|
+
import { COMMUNITY_OPERATION_LOCK_DIRECTORY_NAME, inspectCommunityOperationLock, recoverStaleCommunityOperationLock, withCommunityOperationLock, } from '../lib/community-operation-lock.js';
|
|
10
|
+
import { assertCommunityRepoCandidateCurrent, resolveCommunityRepo, } from '../lib/community-repo-discovery.js';
|
|
11
|
+
import { verifyCommunityReleaseBundle } from '../lib/community-release-verifier.js';
|
|
12
|
+
import { resolveCommunityOfflineRelease, resolveCommunityPublishedRelease, } from '../lib/community-release-resolver.js';
|
|
13
|
+
import { inspectCommunityDoctor } from './community-doctor.js';
|
|
14
|
+
import { buildCommunityInstanceContract, } from '../lib/community-instance-contract.js';
|
|
15
|
+
import { asCommunityDiagnosticError } from '../lib/community-diagnostic.js';
|
|
16
|
+
import { assertCommunityNativePathLayout, attestCommunityNativeExternalSnapshot, inspectCommunityNativeInstall, inspectCommunityNativeRuntime, loadExternalPostgresUrl, loadExternalServerAuthConfig, planCommunityNativeInstalledMigration, readCommunityNativeLogs, rollbackCommunityNativeApplication, resolveCommunityNativeLaunchMode, resolveCommunityNativePaths, setupCommunityNativeInstall, startCommunityNativeInstall, stopCommunityNativeInstall, } from '../lib/community-native-lifecycle.js';
|
|
17
|
+
import { inspectCommunityNativeApplicationRecoveryStatus } from '../lib/community-native-application-recovery.js';
|
|
18
|
+
import { inspectCommunityNativeDatabaseRecoveryStatus, restoreCommunityNativeDatabaseForUpdate, } from '../lib/community-native-database-recovery.js';
|
|
19
|
+
import { inspectCommunityNativePostgres, removeCommunityNativePostgresContainerForReset, removeCommunityNativeManagedPostgres, } from '../lib/community-native-postgres.js';
|
|
20
|
+
import { stopCommunityNativeCockpit } from '../lib/community-cockpit-lifecycle.js';
|
|
21
|
+
function nativePublicOrigin(state) {
|
|
22
|
+
return state.server.host === '127.0.0.1'
|
|
23
|
+
? `http://127.0.0.1:${state.server.publicPort}`
|
|
24
|
+
: null;
|
|
25
|
+
}
|
|
26
|
+
function nativeListenAddress(state) {
|
|
27
|
+
return `${state.server.host}:${state.server.publicPort}`;
|
|
28
|
+
}
|
|
29
|
+
const DEFAULT_COMMUNITY_CLI_VERSION = '0.0.1';
|
|
30
|
+
function isErrno(error, code) {
|
|
31
|
+
return error?.code === code;
|
|
32
|
+
}
|
|
33
|
+
function isSamePhysicalPath(left, right) {
|
|
34
|
+
return path.relative(left, right) === '' && path.relative(right, left) === '';
|
|
35
|
+
}
|
|
36
|
+
function isWithin(root, candidate) {
|
|
37
|
+
const relative = path.relative(root, candidate);
|
|
38
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
39
|
+
}
|
|
40
|
+
function tryLstat(candidate) {
|
|
41
|
+
try {
|
|
42
|
+
return lstatSync(candidate);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (isErrno(error, 'ENOENT'))
|
|
46
|
+
return undefined;
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function assertInstanceDirectory(instanceRoot) {
|
|
51
|
+
const resolved = path.resolve(instanceRoot);
|
|
52
|
+
const stat = tryLstat(resolved);
|
|
53
|
+
if (!stat || stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
54
|
+
throw new Error('community_instance_root_unsafe');
|
|
55
|
+
}
|
|
56
|
+
const canonical = realpathSync.native(resolved);
|
|
57
|
+
if (!isSamePhysicalPath(resolved, canonical)) {
|
|
58
|
+
throw new Error('community_instance_root_alias_refused');
|
|
59
|
+
}
|
|
60
|
+
return canonical;
|
|
61
|
+
}
|
|
62
|
+
function captureInstanceDirectoryIdentity(instanceRoot) {
|
|
63
|
+
const canonical = assertInstanceDirectory(instanceRoot);
|
|
64
|
+
const stat = lstatSync(canonical, { bigint: true });
|
|
65
|
+
return Object.freeze({ path: canonical, device: stat.dev.toString(), inode: stat.ino.toString() });
|
|
66
|
+
}
|
|
67
|
+
function assertInstanceDirectoryIdentity(identity) {
|
|
68
|
+
const canonical = assertInstanceDirectory(identity.path);
|
|
69
|
+
const stat = lstatSync(canonical, { bigint: true });
|
|
70
|
+
if (stat.dev.toString() !== identity.device || stat.ino.toString() !== identity.inode) {
|
|
71
|
+
throw new Error('community_instance_root_identity_changed');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function ensureInstanceDirectory(instanceRoot) {
|
|
75
|
+
const resolvedTarget = path.resolve(instanceRoot);
|
|
76
|
+
const missingSegments = [];
|
|
77
|
+
let existingAncestor = resolvedTarget;
|
|
78
|
+
let stat = tryLstat(existingAncestor);
|
|
79
|
+
while (!stat) {
|
|
80
|
+
const parent = path.dirname(existingAncestor);
|
|
81
|
+
if (isSamePhysicalPath(parent, existingAncestor)) {
|
|
82
|
+
throw new Error('community_instance_ancestor_missing');
|
|
83
|
+
}
|
|
84
|
+
missingSegments.unshift(path.basename(existingAncestor));
|
|
85
|
+
existingAncestor = parent;
|
|
86
|
+
stat = tryLstat(existingAncestor);
|
|
87
|
+
}
|
|
88
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
89
|
+
throw new Error('community_instance_ancestor_unsafe');
|
|
90
|
+
}
|
|
91
|
+
let canonicalParent = realpathSync.native(existingAncestor);
|
|
92
|
+
if (!isSamePhysicalPath(existingAncestor, canonicalParent)) {
|
|
93
|
+
throw new Error('community_instance_ancestor_alias_refused');
|
|
94
|
+
}
|
|
95
|
+
for (const segment of missingSegments) {
|
|
96
|
+
const child = path.join(canonicalParent, segment);
|
|
97
|
+
if (!isSamePhysicalPath(path.dirname(child), canonicalParent)) {
|
|
98
|
+
throw new Error('community_instance_path_segment_escape');
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
mkdirSync(child, { mode: 0o700 });
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (!isErrno(error, 'EEXIST'))
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
const childStat = tryLstat(child);
|
|
108
|
+
if (!childStat || childStat.isSymbolicLink() || !childStat.isDirectory()) {
|
|
109
|
+
throw new Error('community_instance_path_segment_unsafe');
|
|
110
|
+
}
|
|
111
|
+
const canonicalChild = realpathSync.native(child);
|
|
112
|
+
if (!isSamePhysicalPath(child, canonicalChild) ||
|
|
113
|
+
!isSamePhysicalPath(path.dirname(canonicalChild), canonicalParent)) {
|
|
114
|
+
throw new Error('community_instance_path_segment_alias_refused');
|
|
115
|
+
}
|
|
116
|
+
canonicalParent = canonicalChild;
|
|
117
|
+
}
|
|
118
|
+
if (!isSamePhysicalPath(canonicalParent, resolvedTarget)) {
|
|
119
|
+
throw new Error('community_instance_root_alias_refused');
|
|
120
|
+
}
|
|
121
|
+
assertInstanceDirectory(resolvedTarget);
|
|
122
|
+
}
|
|
123
|
+
function assertOwnedInstancePaths(paths, mode) {
|
|
124
|
+
const instanceRoot = assertInstanceDirectory(paths.instanceRoot);
|
|
125
|
+
const ownedPaths = [
|
|
126
|
+
['runtime', paths.runtimeRoot],
|
|
127
|
+
['releases', paths.releaseCacheRoot],
|
|
128
|
+
['backups', paths.backupRoot],
|
|
129
|
+
];
|
|
130
|
+
for (const [name, candidate] of ownedPaths) {
|
|
131
|
+
const resolved = path.resolve(candidate);
|
|
132
|
+
if (!isSamePhysicalPath(path.dirname(resolved), instanceRoot) || !isWithin(instanceRoot, resolved)) {
|
|
133
|
+
throw new Error(`community_instance_owned_path_escape:${name}`);
|
|
134
|
+
}
|
|
135
|
+
let stat = tryLstat(resolved);
|
|
136
|
+
if (!stat && mode === 'create-missing') {
|
|
137
|
+
try {
|
|
138
|
+
mkdirSync(resolved, { mode: 0o700 });
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (!isErrno(error, 'EEXIST'))
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
stat = tryLstat(resolved);
|
|
145
|
+
}
|
|
146
|
+
if (!stat) {
|
|
147
|
+
if (mode === 'require-existing') {
|
|
148
|
+
throw new Error(`community_instance_owned_path_missing:${name}`);
|
|
149
|
+
}
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
153
|
+
throw new Error(`community_instance_owned_path_unsafe:${name}`);
|
|
154
|
+
}
|
|
155
|
+
const canonical = realpathSync.native(resolved);
|
|
156
|
+
if (!isSamePhysicalPath(resolved, canonical) || !isWithin(instanceRoot, canonical)) {
|
|
157
|
+
throw new Error(`community_instance_owned_path_unsafe:${name}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function removeInstanceContentsExceptLock(instanceRoot) {
|
|
162
|
+
const canonicalRoot = assertInstanceDirectory(instanceRoot);
|
|
163
|
+
for (const entry of readdirSync(canonicalRoot, { withFileTypes: true })) {
|
|
164
|
+
if (entry.name === COMMUNITY_OPERATION_LOCK_DIRECTORY_NAME)
|
|
165
|
+
continue;
|
|
166
|
+
const target = path.join(canonicalRoot, entry.name);
|
|
167
|
+
if (path.dirname(target) !== canonicalRoot || !isWithin(canonicalRoot, target)) {
|
|
168
|
+
throw new Error('community_reset_entry_escape');
|
|
169
|
+
}
|
|
170
|
+
const stat = lstatSync(target);
|
|
171
|
+
rmSync(target, { recursive: stat.isDirectory() && !stat.isSymbolicLink(), force: true });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function removeNativeInstanceContentsPreservingPostgres(instanceRoot) {
|
|
175
|
+
const canonicalRoot = assertInstanceDirectory(instanceRoot);
|
|
176
|
+
const runtimeRoot = path.join(canonicalRoot, 'runtime');
|
|
177
|
+
const postgresSecretName = 'native-postgres.env';
|
|
178
|
+
for (const entry of readdirSync(canonicalRoot, { withFileTypes: true })) {
|
|
179
|
+
if (entry.name === COMMUNITY_OPERATION_LOCK_DIRECTORY_NAME)
|
|
180
|
+
continue;
|
|
181
|
+
const target = path.join(canonicalRoot, entry.name);
|
|
182
|
+
if (entry.name !== 'runtime') {
|
|
183
|
+
const stat = lstatSync(target);
|
|
184
|
+
rmSync(target, { recursive: stat.isDirectory() && !stat.isSymbolicLink(), force: true });
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const runtimeStat = lstatSync(target);
|
|
188
|
+
if (!runtimeStat.isDirectory() || runtimeStat.isSymbolicLink() ||
|
|
189
|
+
!isSamePhysicalPath(realpathSync.native(target), runtimeRoot)) {
|
|
190
|
+
throw new Error('community_native_runtime_root_unsafe');
|
|
191
|
+
}
|
|
192
|
+
for (const runtimeEntry of readdirSync(runtimeRoot, { withFileTypes: true })) {
|
|
193
|
+
if (runtimeEntry.name === postgresSecretName)
|
|
194
|
+
continue;
|
|
195
|
+
const runtimeTarget = path.join(runtimeRoot, runtimeEntry.name);
|
|
196
|
+
const stat = lstatSync(runtimeTarget);
|
|
197
|
+
rmSync(runtimeTarget, { recursive: stat.isDirectory() && !stat.isSymbolicLink(), force: true });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function resolveDependencies(dependencies = {}) {
|
|
202
|
+
return {
|
|
203
|
+
verifyReleaseBundle: dependencies.verifyReleaseBundle ?? verifyCommunityReleaseBundle,
|
|
204
|
+
resolvePublishedRelease: dependencies.resolvePublishedRelease ?? resolveCommunityPublishedRelease,
|
|
205
|
+
resolveOfflineRelease: dependencies.resolveOfflineRelease ?? resolveCommunityOfflineRelease,
|
|
206
|
+
inspectDoctor: dependencies.inspectDoctor ?? inspectCommunityDoctor,
|
|
207
|
+
createAdapter: dependencies.createAdapter ?? createCommunityDockerAdapter,
|
|
208
|
+
processRuntime: dependencies.processRuntime ?? communityProcessRuntime,
|
|
209
|
+
withOperationLock: dependencies.withOperationLock ?? withCommunityOperationLock,
|
|
210
|
+
inspectOperationLock: dependencies.inspectOperationLock ?? inspectCommunityOperationLock,
|
|
211
|
+
recoverStaleOperationLock: dependencies.recoverStaleOperationLock ?? recoverStaleCommunityOperationLock,
|
|
212
|
+
removeNativePostgresContainerForReset: dependencies.removeNativePostgresContainerForReset ?? removeCommunityNativePostgresContainerForReset,
|
|
213
|
+
removeNativeManagedPostgres: dependencies.removeNativeManagedPostgres ?? removeCommunityNativeManagedPostgres,
|
|
214
|
+
planNativeMigration: dependencies.planNativeMigration ?? planCommunityNativeInstalledMigration,
|
|
215
|
+
attestNativeExternalSnapshot: dependencies.attestNativeExternalSnapshot ?? attestCommunityNativeExternalSnapshot,
|
|
216
|
+
setupNativeInstall: dependencies.setupNativeInstall ?? setupCommunityNativeInstall,
|
|
217
|
+
rollbackNativeApplication: dependencies.rollbackNativeApplication ?? rollbackCommunityNativeApplication,
|
|
218
|
+
restoreNativeDatabase: dependencies.restoreNativeDatabase ?? restoreCommunityNativeDatabaseForUpdate,
|
|
219
|
+
stopNativeCockpit: dependencies.stopNativeCockpit ?? stopCommunityNativeCockpit,
|
|
220
|
+
ensureInstanceDirectory: dependencies.ensureInstanceDirectory ?? ensureInstanceDirectory,
|
|
221
|
+
commandAbortRuntime: dependencies.commandAbortRuntime ?? communityCommandAbortRuntime,
|
|
222
|
+
cliVersion: dependencies.cliVersion ?? DEFAULT_COMMUNITY_CLI_VERSION,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function installSelection(options) {
|
|
226
|
+
if (options.dataRoot)
|
|
227
|
+
return { instanceName: options.instance, dataRoot: options.dataRoot };
|
|
228
|
+
const layout = resolveCommunityInstanceLayout({ instanceId: options.instance });
|
|
229
|
+
return { instanceName: layout.instanceId, dataRoot: layout.dataRoot };
|
|
230
|
+
}
|
|
231
|
+
function pathsFrom(options) {
|
|
232
|
+
return resolveCommunityInstallPaths(installSelection(options));
|
|
233
|
+
}
|
|
234
|
+
function nativeJournalPathsFrom(options) {
|
|
235
|
+
const selection = installSelection(options);
|
|
236
|
+
const base = resolveCommunityInstallPaths(selection);
|
|
237
|
+
const native = resolveCommunityNativePaths(selection);
|
|
238
|
+
if (!isSamePhysicalPath(base.instanceRoot, native.instanceRoot)) {
|
|
239
|
+
throw new Error('community_native_journal_instance_path_mismatch');
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
...base,
|
|
243
|
+
statePath: native.statePath,
|
|
244
|
+
envPath: native.secretPath,
|
|
245
|
+
ledgerPath: native.processPath,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function journalPathsForMode(options, runtimeMode) {
|
|
249
|
+
return runtimeMode === 'native' ? nativeJournalPathsFrom(options) : pathsFrom(options);
|
|
250
|
+
}
|
|
251
|
+
function inspectFrom(options) {
|
|
252
|
+
return inspectCommunityInstall(installSelection(options));
|
|
253
|
+
}
|
|
254
|
+
function inspectNativeFrom(options) {
|
|
255
|
+
return inspectCommunityNativeInstall(installSelection(options));
|
|
256
|
+
}
|
|
257
|
+
function writeResult(result, json = false, composition = {}) {
|
|
258
|
+
composition.resultSink?.(result);
|
|
259
|
+
if (composition.silent)
|
|
260
|
+
return;
|
|
261
|
+
if (json)
|
|
262
|
+
console.log(JSON.stringify(result, null, 2));
|
|
263
|
+
else if (typeof result === 'string')
|
|
264
|
+
process.stdout.write(result.endsWith('\n') ? result : `${result}\n`);
|
|
265
|
+
else
|
|
266
|
+
console.log(JSON.stringify(result, null, 2));
|
|
267
|
+
}
|
|
268
|
+
function nativeMigrationSummary(receipt) {
|
|
269
|
+
return {
|
|
270
|
+
status: receipt.status,
|
|
271
|
+
completedAt: receipt.completedAt,
|
|
272
|
+
action: receipt.action,
|
|
273
|
+
policyId: receipt.policyId,
|
|
274
|
+
targetLineageId: receipt.targetLineageId,
|
|
275
|
+
resultLineageId: receipt.resultLineageId,
|
|
276
|
+
pendingMigrationCount: receipt.pendingMigrationCount,
|
|
277
|
+
acceptedPlanSha256: receipt.acceptedPlanSha256,
|
|
278
|
+
sourceMigrationStateFingerprintSha256: receipt.sourceMigrationStateFingerprintSha256,
|
|
279
|
+
resultSchemaFingerprintSha256: receipt.resultSchemaFingerprintSha256,
|
|
280
|
+
resultReceiptFingerprintSha256: receipt.resultReceiptFingerprintSha256,
|
|
281
|
+
resultMigrationStateFingerprintSha256: receipt.resultMigrationStateFingerprintSha256,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
async function releaseBundle(options, dependencies, signal) {
|
|
285
|
+
const sourceSelection = selectCommunityReleaseSource(options);
|
|
286
|
+
if (sourceSelection === 'published-version-tag') {
|
|
287
|
+
const verified = await dependencies.resolvePublishedRelease({
|
|
288
|
+
releaseVersion: dependencies.cliVersion,
|
|
289
|
+
certificateIdentity: options.certificateIdentity,
|
|
290
|
+
certificateOidcIssuer: options.certificateOidcIssuer,
|
|
291
|
+
signal,
|
|
292
|
+
});
|
|
293
|
+
return {
|
|
294
|
+
verified,
|
|
295
|
+
assertCurrent: () => { },
|
|
296
|
+
descriptorSource: verified.descriptorSource,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
if (sourceSelection === 'offline-descriptor') {
|
|
300
|
+
const resolved = await dependencies.resolveOfflineRelease({
|
|
301
|
+
descriptorPath: options.releaseDescriptor,
|
|
302
|
+
releaseVersion: dependencies.cliVersion,
|
|
303
|
+
certificateIdentity: options.certificateIdentity,
|
|
304
|
+
certificateOidcIssuer: options.certificateOidcIssuer,
|
|
305
|
+
signal,
|
|
306
|
+
});
|
|
307
|
+
return {
|
|
308
|
+
verified: resolved.verified,
|
|
309
|
+
assertCurrent: resolved.assertCurrent,
|
|
310
|
+
descriptorSource: resolved.descriptorSource,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
let repo = options.repo;
|
|
314
|
+
let expectedReleaseDir;
|
|
315
|
+
if (options.releaseDir) {
|
|
316
|
+
const requested = path.resolve(options.releaseDir);
|
|
317
|
+
const directManifest = tryLstat(path.join(requested, 'release.json'));
|
|
318
|
+
repo = directManifest?.isFile() ? path.dirname(requested) : requested;
|
|
319
|
+
expectedReleaseDir = directManifest?.isFile() ? requested : path.join(requested, 'release');
|
|
320
|
+
}
|
|
321
|
+
const candidate = resolveCommunityRepo({ repo });
|
|
322
|
+
if (expectedReleaseDir && !isSamePhysicalPath(realpathSync.native(expectedReleaseDir), candidate.releaseDir)) {
|
|
323
|
+
throw new Error('community_release_dir_repo_mismatch');
|
|
324
|
+
}
|
|
325
|
+
const verified = await dependencies.verifyReleaseBundle({
|
|
326
|
+
releaseRoot: candidate.releaseDir,
|
|
327
|
+
certificateIdentity: options.certificateIdentity,
|
|
328
|
+
certificateOidcIssuer: options.certificateOidcIssuer,
|
|
329
|
+
verificationMode: options.releaseDir !== undefined ? 'offline' : 'online',
|
|
330
|
+
});
|
|
331
|
+
assertCommunityRepoCandidateCurrent(candidate);
|
|
332
|
+
if (verified.identity.releaseVersion !== candidate.releaseIdentity.releaseVersion) {
|
|
333
|
+
throw new Error('community_repo_verified_release_identity_mismatch');
|
|
334
|
+
}
|
|
335
|
+
if (verified.identity.releaseVersion !== dependencies.cliVersion) {
|
|
336
|
+
throw new Error(`community_cli_release_version_mismatch:cli=${dependencies.cliVersion}:release=${verified.identity.releaseVersion}`);
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
verified,
|
|
340
|
+
assertCurrent: () => assertCommunityRepoCandidateCurrent(candidate),
|
|
341
|
+
descriptorSource: Object.freeze({
|
|
342
|
+
kind: 'local-bundle',
|
|
343
|
+
repository: candidate.repoRoot,
|
|
344
|
+
releaseDirectory: candidate.releaseDir,
|
|
345
|
+
}),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
export function selectCommunityReleaseSource(options) {
|
|
349
|
+
const selectors = [
|
|
350
|
+
options.repo !== undefined ? '--repo' : null,
|
|
351
|
+
options.releaseDir !== undefined ? '--release-dir' : null,
|
|
352
|
+
options.releaseDescriptor !== undefined ? '--release-descriptor' : null,
|
|
353
|
+
].filter((value) => value !== null);
|
|
354
|
+
if (selectors.length > 1) {
|
|
355
|
+
throw new Error(`community_release_selector_conflict:choose_only_one_of_${selectors.join('_')}`);
|
|
356
|
+
}
|
|
357
|
+
if (options.releaseDescriptor !== undefined)
|
|
358
|
+
return 'offline-descriptor';
|
|
359
|
+
return options.repo !== undefined || options.releaseDir !== undefined ? 'local-bundle' : 'published-version-tag';
|
|
360
|
+
}
|
|
361
|
+
function adapter(dependencies = resolveDependencies(), signal) {
|
|
362
|
+
return dependencies.createAdapter({
|
|
363
|
+
verifyRelease: async (release) => verifyStagedCommunityRelease(release),
|
|
364
|
+
signal,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
async function withMutatingCommandScope(dependencies, callback) {
|
|
368
|
+
const runtime = resolveDependencies(dependencies);
|
|
369
|
+
return withCommunityCommandAbortScope(runtime.commandAbortRuntime, async (signal) => {
|
|
370
|
+
throwIfCommunityCommandAborted(signal);
|
|
371
|
+
const result = await callback(runtime, signal);
|
|
372
|
+
throwIfCommunityCommandAborted(signal);
|
|
373
|
+
return result;
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
function createJournaledAdapter(base, journal, signal) {
|
|
377
|
+
let step = 0;
|
|
378
|
+
const effect = (name, callback) => runCommunityJournaledSideEffect({
|
|
379
|
+
handle: journal,
|
|
380
|
+
step: `${name}-${++step}`,
|
|
381
|
+
signal,
|
|
382
|
+
effect: callback,
|
|
383
|
+
});
|
|
384
|
+
return {
|
|
385
|
+
verifyRelease: (release) => effect('verify-release', () => base.verifyRelease(release)),
|
|
386
|
+
createBackup: (params) => effect('create-backup', () => base.createBackup(params)),
|
|
387
|
+
stop: (params) => effect('stop-runtime', () => base.stop(params)),
|
|
388
|
+
pull: (params) => effect('pull-release', () => base.pull(params)),
|
|
389
|
+
start: (params) => effect('start-runtime', () => base.start(params)),
|
|
390
|
+
health: (params) => effect('health-check', () => base.health(params)),
|
|
391
|
+
dataSmoke: (params) => effect('data-smoke', () => base.dataSmoke(params)),
|
|
392
|
+
claimFreshPostgresVolume: (params) => effect('claim-volume', () => base.claimFreshPostgresVolume(params)),
|
|
393
|
+
restoreBackup: (params) => effect('restore-backup', () => base.restoreBackup(params)),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
async function runWithOperationJournal(params) {
|
|
397
|
+
const journal = createCommunityOperationJournal({
|
|
398
|
+
paths: params.paths,
|
|
399
|
+
operation: params.operation,
|
|
400
|
+
receipt: params.receipt,
|
|
401
|
+
sourceState: params.state,
|
|
402
|
+
runtimeMode: params.runtimeMode,
|
|
403
|
+
});
|
|
404
|
+
let lifecycle;
|
|
405
|
+
const context = {
|
|
406
|
+
signal: params.signal,
|
|
407
|
+
journal,
|
|
408
|
+
receipt: params.receipt,
|
|
409
|
+
get lifecycle() {
|
|
410
|
+
lifecycle ??= createJournaledAdapter(adapter(params.runtime, params.signal), journal, params.signal);
|
|
411
|
+
return lifecycle;
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
try {
|
|
415
|
+
throwIfCommunityCommandAborted(params.signal);
|
|
416
|
+
const result = await params.callback(context);
|
|
417
|
+
throwIfCommunityCommandAborted(params.signal);
|
|
418
|
+
finishCommunityOperationJournal(journal, params.paths, 'succeeded');
|
|
419
|
+
return result;
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
if (params.signal.aborted) {
|
|
423
|
+
if (journal.record().status === 'running') {
|
|
424
|
+
finishCommunityOperationJournal(journal, params.paths, 'aborted');
|
|
425
|
+
}
|
|
426
|
+
throw new Error('community_operation_aborted');
|
|
427
|
+
}
|
|
428
|
+
const diagnosticError = asCommunityDiagnosticError(error, {
|
|
429
|
+
code: 'community_server_operation_failed',
|
|
430
|
+
phase: 'operation',
|
|
431
|
+
details: {
|
|
432
|
+
operation: params.operation,
|
|
433
|
+
journalPhase: journal.record().phase,
|
|
434
|
+
journalStep: journal.record().step,
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
if (journal.record().status === 'running') {
|
|
438
|
+
finishCommunityOperationJournal(journal, params.paths, 'failed', diagnosticError.diagnostic);
|
|
439
|
+
}
|
|
440
|
+
throw diagnosticError;
|
|
441
|
+
}
|
|
442
|
+
finally {
|
|
443
|
+
journal.close();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function requireInstalled(options) {
|
|
447
|
+
const native = inspectNativeFrom(options);
|
|
448
|
+
if (native.status !== 'not-installed') {
|
|
449
|
+
throw new Error(`community_oci_operation_refused:native_${native.status}:${native.error ?? 'use_native_lifecycle_command'}`);
|
|
450
|
+
}
|
|
451
|
+
const inspection = inspectFrom(options);
|
|
452
|
+
if (inspection.status === 'not-installed')
|
|
453
|
+
throw new Error('community_not_installed:run_aops-cli_server_setup');
|
|
454
|
+
if (inspection.status === 'partial')
|
|
455
|
+
throw new Error(`community_install_partial:${inspection.error ?? 'unknown'}:run_aops-cli_doctor`);
|
|
456
|
+
return { paths: inspection.paths, state: inspection.state };
|
|
457
|
+
}
|
|
458
|
+
function assertSameInstancePaths(expected, observed) {
|
|
459
|
+
if (!isSamePhysicalPath(path.resolve(expected.instanceRoot), path.resolve(observed.instanceRoot))) {
|
|
460
|
+
throw new Error('community_instance_path_changed_before_operation');
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
async function requireHealthyPreflight(operation, options, runtime) {
|
|
464
|
+
const selection = installSelection(options);
|
|
465
|
+
const preflight = await runtime.inspectDoctor({
|
|
466
|
+
instance: selection.instanceName,
|
|
467
|
+
dataRoot: selection.dataRoot,
|
|
468
|
+
});
|
|
469
|
+
if (preflight.mutationFree !== true) {
|
|
470
|
+
throw new Error(`community_${operation}_preflight_not_mutation_free`);
|
|
471
|
+
}
|
|
472
|
+
const failedChecks = preflight.checks.filter((check) => check.status === 'fail').map((check) => check.id);
|
|
473
|
+
if (failedChecks.length > 0) {
|
|
474
|
+
throw new Error(`community_${operation}_preflight_failed:${failedChecks.join(',')}:run_aops-cli_doctor`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const RECOVERY_TOLERATED_RUNTIME_FAILURES = new Set(['container-status', 'server-health']);
|
|
478
|
+
async function requireRecoveryPreflight(options, runtime) {
|
|
479
|
+
const selection = installSelection(options);
|
|
480
|
+
const preflight = await runtime.inspectDoctor({
|
|
481
|
+
instance: selection.instanceName,
|
|
482
|
+
dataRoot: selection.dataRoot,
|
|
483
|
+
});
|
|
484
|
+
if (preflight.mutationFree !== true) {
|
|
485
|
+
throw new Error('community_recover_preflight_not_mutation_free');
|
|
486
|
+
}
|
|
487
|
+
const blockingChecks = preflight.checks
|
|
488
|
+
.filter((check) => check.status === 'fail' && !RECOVERY_TOLERATED_RUNTIME_FAILURES.has(check.id))
|
|
489
|
+
.map((check) => check.id);
|
|
490
|
+
if (blockingChecks.length > 0) {
|
|
491
|
+
throw new Error(`community_recover_preflight_failed:${blockingChecks.join(',')}:run_aops-cli_doctor`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async function withInstalledOperation(options, operation, runtime, signal, callback) {
|
|
495
|
+
const initial = requireInstalled(options);
|
|
496
|
+
assertOwnedInstancePaths(initial.paths, 'require-existing');
|
|
497
|
+
return runtime.withOperationLock({ instanceDirectory: initial.paths.instanceRoot, operation, signal }, async ({ receipt }) => {
|
|
498
|
+
const locked = requireInstalled(options);
|
|
499
|
+
assertSameInstancePaths(initial.paths, locked.paths);
|
|
500
|
+
assertOwnedInstancePaths(locked.paths, 'require-existing');
|
|
501
|
+
assertCommunityRecoveryMutationFence(locked.paths);
|
|
502
|
+
return runWithOperationJournal({
|
|
503
|
+
paths: locked.paths,
|
|
504
|
+
state: locked.state,
|
|
505
|
+
operation,
|
|
506
|
+
receipt,
|
|
507
|
+
signal,
|
|
508
|
+
runtime,
|
|
509
|
+
callback: (context) => callback(locked, context),
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
async function withNativeOperation(options, operation, runtime, signal, callback) {
|
|
514
|
+
const initial = inspectNativeFrom(options);
|
|
515
|
+
if (initial.status !== 'installed' || !initial.state) {
|
|
516
|
+
throw new Error(`community_native_not_installed:${initial.status}:${initial.error ?? 'run_server_setup'}`);
|
|
517
|
+
}
|
|
518
|
+
const initialRoot = assertInstanceDirectory(initial.paths.instanceRoot);
|
|
519
|
+
const instanceIdentity = captureInstanceDirectoryIdentity(initialRoot);
|
|
520
|
+
return runtime.withOperationLock({ instanceDirectory: initialRoot, operation, signal }, async ({ receipt }) => {
|
|
521
|
+
throwIfCommunityCommandAborted(signal);
|
|
522
|
+
assertInstanceDirectoryIdentity(instanceIdentity);
|
|
523
|
+
const locked = inspectNativeFrom(options);
|
|
524
|
+
if (locked.status !== 'installed' || !locked.state) {
|
|
525
|
+
throw new Error(`community_native_install_changed:${locked.status}:${locked.error ?? 'unknown'}`);
|
|
526
|
+
}
|
|
527
|
+
if (!isSamePhysicalPath(initialRoot, path.resolve(locked.paths.instanceRoot))) {
|
|
528
|
+
throw new Error('community_instance_path_changed_before_operation');
|
|
529
|
+
}
|
|
530
|
+
const oci = inspectFrom(options);
|
|
531
|
+
if (oci.status !== 'not-installed') {
|
|
532
|
+
throw new Error(`community_instance_runtime_conflict:oci_${oci.status}:run_aops-cli_doctor`);
|
|
533
|
+
}
|
|
534
|
+
const basePaths = pathsFrom(options);
|
|
535
|
+
assertOwnedInstancePaths(basePaths, 'allow-missing');
|
|
536
|
+
assertCommunityNativePathLayout(locked.paths, { requireInstanceRoot: true });
|
|
537
|
+
const journalPaths = nativeJournalPathsFrom(options);
|
|
538
|
+
assertCommunityOperationJournalFence(journalPaths);
|
|
539
|
+
return runWithOperationJournal({
|
|
540
|
+
paths: journalPaths,
|
|
541
|
+
state: null,
|
|
542
|
+
operation,
|
|
543
|
+
receipt,
|
|
544
|
+
signal,
|
|
545
|
+
runtime,
|
|
546
|
+
runtimeMode: 'native',
|
|
547
|
+
callback: ({ journal }) => runCommunityJournaledSideEffect({
|
|
548
|
+
handle: journal,
|
|
549
|
+
step: `native-${operation}`,
|
|
550
|
+
signal,
|
|
551
|
+
effect: async () => {
|
|
552
|
+
assertInstanceDirectoryIdentity(instanceIdentity);
|
|
553
|
+
const result = await callback(locked);
|
|
554
|
+
assertInstanceDirectoryIdentity(instanceIdentity);
|
|
555
|
+
return result;
|
|
556
|
+
},
|
|
557
|
+
}),
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
export async function runCommunityServerSetup(options, dependencies = {}) {
|
|
562
|
+
const contract = buildCommunityInstanceContract({
|
|
563
|
+
runtime: options.runtime,
|
|
564
|
+
postgres: options.postgres,
|
|
565
|
+
postgresConfig: options.postgresConfig,
|
|
566
|
+
postgresTls: options.postgresTls,
|
|
567
|
+
auth: options.auth,
|
|
568
|
+
exposure: options.exposure,
|
|
569
|
+
publicPort: options.publicPort,
|
|
570
|
+
instance: options.instance,
|
|
571
|
+
port: options.port,
|
|
572
|
+
});
|
|
573
|
+
if (contract.runtime === 'oci' && (options.sourceRoot || options.foreground === true || options.detach === true)) {
|
|
574
|
+
throw new Error('community_setup_oci_native_options_refused');
|
|
575
|
+
}
|
|
576
|
+
if (options.preview === true && options.apply === true) {
|
|
577
|
+
throw new Error('community_setup_mode_conflict:choose_--preview_or_--apply');
|
|
578
|
+
}
|
|
579
|
+
if (options.preview === true) {
|
|
580
|
+
if (contract.runtime === 'native' && contract.postgres.mode === 'external') {
|
|
581
|
+
loadExternalPostgresUrl(contract.postgres.configRef, contract.postgres.tlsPolicy);
|
|
582
|
+
loadExternalServerAuthConfig(contract.postgres.configRef, contract.server.authProvider);
|
|
583
|
+
}
|
|
584
|
+
writeResult({
|
|
585
|
+
status: 'community-server-setup-preview',
|
|
586
|
+
mutationFree: true,
|
|
587
|
+
contract,
|
|
588
|
+
next: contract.runtime === 'native'
|
|
589
|
+
? 'Re-run with --apply to start the installed npm server package, or build an explicit public checkout supplied with --source-root.'
|
|
590
|
+
: 'Re-run with --apply to verify the signed release and start the OCI stack.',
|
|
591
|
+
}, options.json, options);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (options.apply !== true) {
|
|
595
|
+
throw new Error('community_setup_apply_required:use_--preview_or_--apply');
|
|
596
|
+
}
|
|
597
|
+
if (contract.runtime === 'native') {
|
|
598
|
+
await withMutatingCommandScope(dependencies, async (runtime, signal) => {
|
|
599
|
+
const selection = installSelection({ ...options, instance: contract.instanceId });
|
|
600
|
+
const paths = resolveCommunityInstallPaths(selection);
|
|
601
|
+
runtime.ensureInstanceDirectory(paths.instanceRoot);
|
|
602
|
+
assertOwnedInstancePaths(paths, 'allow-missing');
|
|
603
|
+
const instanceIdentity = captureInstanceDirectoryIdentity(paths.instanceRoot);
|
|
604
|
+
const setup = await runtime.withOperationLock({ instanceDirectory: paths.instanceRoot, operation: 'setup', signal }, async ({ receipt }) => {
|
|
605
|
+
throwIfCommunityCommandAborted(signal);
|
|
606
|
+
assertInstanceDirectoryIdentity(instanceIdentity);
|
|
607
|
+
assertOwnedInstancePaths(paths, 'allow-missing');
|
|
608
|
+
const nativePaths = resolveCommunityNativePaths(selection);
|
|
609
|
+
assertCommunityNativePathLayout(nativePaths, { requireInstanceRoot: true });
|
|
610
|
+
const ociInspection = inspectFrom({ ...options, instance: contract.instanceId });
|
|
611
|
+
if (ociInspection.status !== 'not-installed') {
|
|
612
|
+
throw new Error(`community_instance_runtime_conflict:oci_${ociInspection.status}:run_aops-cli_doctor`);
|
|
613
|
+
}
|
|
614
|
+
const journalPaths = nativeJournalPathsFrom({ ...options, instance: contract.instanceId });
|
|
615
|
+
const nativeInspection = inspectNativeFrom({ ...options, instance: contract.instanceId });
|
|
616
|
+
if (nativeInspection.status === 'partial' || nativeInspection.status === 'runtime-conflict') {
|
|
617
|
+
throw new Error(`community_native_install_${nativeInspection.status}:${nativeInspection.error ?? 'unknown'}:run_doctor`);
|
|
618
|
+
}
|
|
619
|
+
assertCommunityOperationJournalFence(journalPaths);
|
|
620
|
+
return runWithOperationJournal({
|
|
621
|
+
paths: journalPaths,
|
|
622
|
+
state: null,
|
|
623
|
+
operation: 'setup',
|
|
624
|
+
receipt,
|
|
625
|
+
signal,
|
|
626
|
+
runtime,
|
|
627
|
+
runtimeMode: 'native',
|
|
628
|
+
callback: ({ journal }) => runCommunityJournaledSideEffect({
|
|
629
|
+
handle: journal,
|
|
630
|
+
step: 'native-setup',
|
|
631
|
+
signal,
|
|
632
|
+
effect: async () => {
|
|
633
|
+
assertInstanceDirectoryIdentity(instanceIdentity);
|
|
634
|
+
const result = await runtime.setupNativeInstall({
|
|
635
|
+
contract,
|
|
636
|
+
sourceRoot: options.sourceRoot,
|
|
637
|
+
dataRoot: selection.dataRoot,
|
|
638
|
+
mode: resolveCommunityNativeLaunchMode(options),
|
|
639
|
+
createPostgresSecret: options.createPostgresSecret,
|
|
640
|
+
progress: options.progressSink,
|
|
641
|
+
signal,
|
|
642
|
+
});
|
|
643
|
+
assertInstanceDirectoryIdentity(instanceIdentity);
|
|
644
|
+
return result;
|
|
645
|
+
},
|
|
646
|
+
}),
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
throwIfCommunityCommandAborted(signal);
|
|
650
|
+
writeResult({
|
|
651
|
+
status: setup.status === 'created'
|
|
652
|
+
? 'community-server-installed-and-running'
|
|
653
|
+
: 'community-server-refreshed-and-running',
|
|
654
|
+
runtime: 'native',
|
|
655
|
+
profile: setup.state.profile,
|
|
656
|
+
instance: setup.state.instanceName,
|
|
657
|
+
mode: setup.launch.mode,
|
|
658
|
+
pid: setup.launch.process.pid,
|
|
659
|
+
origin: nativePublicOrigin(setup.state),
|
|
660
|
+
listenAddress: nativeListenAddress(setup.state),
|
|
661
|
+
exposure: setup.state.server.exposure,
|
|
662
|
+
authProvider: setup.state.server.authProvider,
|
|
663
|
+
dataRoot: setup.paths.dataRoot,
|
|
664
|
+
sourceRoot: setup.state.source.root,
|
|
665
|
+
sourceFingerprint: setup.state.source.sourceFingerprint,
|
|
666
|
+
buildFingerprint: setup.state.build.buildFingerprint,
|
|
667
|
+
migration: nativeMigrationSummary(setup.launch.migration),
|
|
668
|
+
applicationUpdateId: setup.applicationUpdate?.prepared.updateId ?? null,
|
|
669
|
+
}, options.json, options);
|
|
670
|
+
if (setup.launch.waitForExit)
|
|
671
|
+
await setup.launch.waitForExit();
|
|
672
|
+
});
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
await withMutatingCommandScope(dependencies, async (runtime, signal) => {
|
|
676
|
+
const selection = installSelection(options);
|
|
677
|
+
const nativeExisting = inspectNativeFrom(options);
|
|
678
|
+
if (nativeExisting.status !== 'not-installed') {
|
|
679
|
+
throw new Error(`community_instance_runtime_conflict:native_${nativeExisting.status}:run_aops-cli_doctor`);
|
|
680
|
+
}
|
|
681
|
+
const existing = inspectFrom(options);
|
|
682
|
+
if (existing.status === 'partial') {
|
|
683
|
+
throw new Error(`community_install_partial:${existing.error ?? 'unknown'}:run_aops-cli_doctor_or_server_reset`);
|
|
684
|
+
}
|
|
685
|
+
throwIfCommunityCommandAborted(signal);
|
|
686
|
+
const { assertCurrent, descriptorSource, verified } = await releaseBundle(options, runtime, signal);
|
|
687
|
+
throwIfCommunityCommandAborted(signal);
|
|
688
|
+
await requireHealthyPreflight('setup', options, runtime);
|
|
689
|
+
throwIfCommunityCommandAborted(signal);
|
|
690
|
+
const port = contract.server.port;
|
|
691
|
+
const paths = resolveCommunityInstallPaths(selection);
|
|
692
|
+
runtime.ensureInstanceDirectory(paths.instanceRoot);
|
|
693
|
+
assertOwnedInstancePaths(paths, 'allow-missing');
|
|
694
|
+
const result = await runtime.withOperationLock({ instanceDirectory: paths.instanceRoot, operation: 'setup', signal }, async ({ receipt }) => {
|
|
695
|
+
assertCurrent();
|
|
696
|
+
assertOwnedInstancePaths(paths, 'create-missing');
|
|
697
|
+
const lockedInspection = inspectFrom(options);
|
|
698
|
+
const lockedNativeInspection = inspectNativeFrom(options);
|
|
699
|
+
if (lockedNativeInspection.status !== 'not-installed') {
|
|
700
|
+
throw new Error(`community_instance_runtime_conflict:native_${lockedNativeInspection.status}:run_aops-cli_doctor`);
|
|
701
|
+
}
|
|
702
|
+
if (lockedInspection.status === 'partial') {
|
|
703
|
+
throw new Error(`community_install_partial:${lockedInspection.error ?? 'unknown'}:run_aops-cli_doctor_or_server_reset`);
|
|
704
|
+
}
|
|
705
|
+
assertSameInstancePaths(paths, lockedInspection.paths);
|
|
706
|
+
assertCommunityRecoveryMutationFence(paths);
|
|
707
|
+
return runWithOperationJournal({
|
|
708
|
+
paths,
|
|
709
|
+
state: lockedInspection.status === 'installed' ? lockedInspection.state : null,
|
|
710
|
+
operation: 'setup',
|
|
711
|
+
receipt,
|
|
712
|
+
signal,
|
|
713
|
+
runtime,
|
|
714
|
+
callback: async (context) => {
|
|
715
|
+
const { journal, receipt } = context;
|
|
716
|
+
const setup = await runCommunityJournaledPromotion({
|
|
717
|
+
handle: journal,
|
|
718
|
+
step: 'install-files',
|
|
719
|
+
signal,
|
|
720
|
+
promote: () => setupCommunityInstall({
|
|
721
|
+
manifestContent: verified.manifestContent,
|
|
722
|
+
composeContent: verified.composeContent,
|
|
723
|
+
manifestVerified: true,
|
|
724
|
+
instanceName: selection.instanceName,
|
|
725
|
+
dataRoot: selection.dataRoot,
|
|
726
|
+
port,
|
|
727
|
+
operationJournal: journal,
|
|
728
|
+
operationReceipt: receipt,
|
|
729
|
+
}),
|
|
730
|
+
});
|
|
731
|
+
if (path.resolve(setup.paths.instanceRoot) !== path.resolve(paths.instanceRoot)) {
|
|
732
|
+
throw new Error('community_setup_instance_path_mismatch');
|
|
733
|
+
}
|
|
734
|
+
const lifecycle = context.lifecycle;
|
|
735
|
+
await lifecycle.verifyRelease(setup.state.activeRelease);
|
|
736
|
+
await lifecycle.pull({ paths: setup.paths, state: setup.state, release: setup.state.activeRelease });
|
|
737
|
+
await lifecycle.start({
|
|
738
|
+
paths: setup.paths,
|
|
739
|
+
state: setup.state,
|
|
740
|
+
release: setup.state.activeRelease,
|
|
741
|
+
postgresVolumeName: setup.state.postgresVolumeName,
|
|
742
|
+
});
|
|
743
|
+
await lifecycle.health({ paths: setup.paths, state: setup.state });
|
|
744
|
+
await lifecycle.dataSmoke({ paths: setup.paths, state: setup.state });
|
|
745
|
+
throwIfCommunityCommandAborted(signal);
|
|
746
|
+
return {
|
|
747
|
+
status: setup.status === 'created' ? 'community-server-installed-and-running' : 'community-server-running',
|
|
748
|
+
instance: setup.state.instanceName,
|
|
749
|
+
releaseVersion: setup.state.activeRelease.releaseVersion,
|
|
750
|
+
imageRef: setup.state.activeRelease.imageRef,
|
|
751
|
+
dataRoot: setup.paths.dataRoot,
|
|
752
|
+
releaseDescriptorSource: descriptorSource,
|
|
753
|
+
certificateIdentity: verified.certificateIdentity,
|
|
754
|
+
verifiedArtifactCount: verified.verifiedArtifactCount,
|
|
755
|
+
};
|
|
756
|
+
},
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
throwIfCommunityCommandAborted(signal);
|
|
760
|
+
writeResult(result, options.json, options);
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
export async function runCommunityServerStart(options, dependencies = {}) {
|
|
764
|
+
await withMutatingCommandScope(dependencies, async (runtime, signal) => {
|
|
765
|
+
const native = inspectNativeFrom(options);
|
|
766
|
+
if (native.status === 'installed') {
|
|
767
|
+
const launch = await withNativeOperation(options, 'start', runtime, signal, () => startCommunityNativeInstall({
|
|
768
|
+
...installSelection(options),
|
|
769
|
+
mode: resolveCommunityNativeLaunchMode(options),
|
|
770
|
+
signal,
|
|
771
|
+
}));
|
|
772
|
+
writeResult({
|
|
773
|
+
status: 'community-server-running',
|
|
774
|
+
runtime: 'native',
|
|
775
|
+
profile: launch.state.profile,
|
|
776
|
+
instance: launch.state.instanceName,
|
|
777
|
+
mode: launch.mode,
|
|
778
|
+
pid: launch.process.pid,
|
|
779
|
+
origin: nativePublicOrigin(launch.state),
|
|
780
|
+
listenAddress: nativeListenAddress(launch.state),
|
|
781
|
+
exposure: launch.state.server.exposure,
|
|
782
|
+
authProvider: launch.state.server.authProvider,
|
|
783
|
+
sourceFingerprint: launch.state.source.sourceFingerprint,
|
|
784
|
+
buildFingerprint: launch.state.build.buildFingerprint,
|
|
785
|
+
migration: nativeMigrationSummary(launch.migration),
|
|
786
|
+
}, options.json, options);
|
|
787
|
+
if (launch.waitForExit)
|
|
788
|
+
await launch.waitForExit();
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (native.status !== 'not-installed') {
|
|
792
|
+
throw new Error(`community_native_start_refused:${native.status}:${native.error ?? 'run_doctor'}`);
|
|
793
|
+
}
|
|
794
|
+
if (options.foreground === true || options.detach === true) {
|
|
795
|
+
throw new Error('community_oci_native_options_refused');
|
|
796
|
+
}
|
|
797
|
+
const result = await withInstalledOperation(options, 'start', runtime, signal, async ({ paths, state }, { lifecycle }) => {
|
|
798
|
+
await lifecycle.verifyRelease(state.activeRelease);
|
|
799
|
+
await lifecycle.pull({ paths, state, release: state.activeRelease });
|
|
800
|
+
await lifecycle.start({ paths, state, release: state.activeRelease, postgresVolumeName: state.postgresVolumeName });
|
|
801
|
+
await lifecycle.health({ paths, state });
|
|
802
|
+
await lifecycle.dataSmoke({ paths, state });
|
|
803
|
+
return { status: 'community-server-running', instance: state.instanceName, imageRef: state.activeRelease.imageRef };
|
|
804
|
+
});
|
|
805
|
+
throwIfCommunityCommandAborted(signal);
|
|
806
|
+
writeResult(result, options.json, options);
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
export async function runCommunityServerStop(options, dependencies = {}) {
|
|
810
|
+
await withMutatingCommandScope(dependencies, async (runtime, signal) => {
|
|
811
|
+
const native = inspectNativeFrom(options);
|
|
812
|
+
if (native.status === 'installed') {
|
|
813
|
+
const stopped = await withNativeOperation(options, 'stop', runtime, signal, () => stopCommunityNativeInstall({ ...installSelection(options), signal }));
|
|
814
|
+
writeResult({
|
|
815
|
+
status: stopped.status === 'stopped' ? 'community-server-stopped' : 'community-server-already-stopped',
|
|
816
|
+
runtime: 'native',
|
|
817
|
+
profile: stopped.state.profile,
|
|
818
|
+
instance: stopped.state.instanceName,
|
|
819
|
+
processStatus: stopped.process?.status ?? null,
|
|
820
|
+
}, options.json);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
if (native.status !== 'not-installed') {
|
|
824
|
+
throw new Error(`community_native_stop_refused:${native.status}:${native.error ?? 'run_doctor'}`);
|
|
825
|
+
}
|
|
826
|
+
const result = await withInstalledOperation(options, 'stop', runtime, signal, async ({ paths, state }, { lifecycle }) => {
|
|
827
|
+
await lifecycle.stop({ paths, state });
|
|
828
|
+
return { status: 'community-server-stopped', instance: state.instanceName };
|
|
829
|
+
});
|
|
830
|
+
throwIfCommunityCommandAborted(signal);
|
|
831
|
+
writeResult(result, options.json);
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
export async function runCommunityServerRestart(options, dependencies = {}) {
|
|
835
|
+
await withMutatingCommandScope(dependencies, async (runtime, signal) => {
|
|
836
|
+
const native = inspectNativeFrom(options);
|
|
837
|
+
if (native.status === 'installed') {
|
|
838
|
+
const launch = await withNativeOperation(options, 'restart', runtime, signal, async () => {
|
|
839
|
+
await stopCommunityNativeInstall({ ...installSelection(options), signal });
|
|
840
|
+
return startCommunityNativeInstall({
|
|
841
|
+
...installSelection(options),
|
|
842
|
+
mode: resolveCommunityNativeLaunchMode(options),
|
|
843
|
+
signal,
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
writeResult({
|
|
847
|
+
status: 'community-server-restarted',
|
|
848
|
+
runtime: 'native',
|
|
849
|
+
profile: launch.state.profile,
|
|
850
|
+
instance: launch.state.instanceName,
|
|
851
|
+
mode: launch.mode,
|
|
852
|
+
pid: launch.process.pid,
|
|
853
|
+
hostPid: launch.process.hostPid ?? null,
|
|
854
|
+
origin: nativePublicOrigin(launch.state),
|
|
855
|
+
listenAddress: nativeListenAddress(launch.state),
|
|
856
|
+
exposure: launch.state.server.exposure,
|
|
857
|
+
authProvider: launch.state.server.authProvider,
|
|
858
|
+
migration: nativeMigrationSummary(launch.migration),
|
|
859
|
+
}, options.json);
|
|
860
|
+
if (launch.waitForExit)
|
|
861
|
+
await launch.waitForExit();
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
if (native.status !== 'not-installed') {
|
|
865
|
+
throw new Error(`community_native_restart_refused:${native.status}:${native.error ?? 'run_doctor'}`);
|
|
866
|
+
}
|
|
867
|
+
if (options.foreground === true || options.detach === true) {
|
|
868
|
+
throw new Error('community_oci_native_options_refused');
|
|
869
|
+
}
|
|
870
|
+
const output = await withInstalledOperation(options, 'restart', runtime, signal, async ({ paths, state }, { journal, lifecycle }) => {
|
|
871
|
+
const invocation = buildCommunityComposeInvocation({ paths, state, action: 'restart' });
|
|
872
|
+
const result = await runCommunityJournaledSideEffect({
|
|
873
|
+
handle: journal,
|
|
874
|
+
step: 'restart-runtime',
|
|
875
|
+
signal,
|
|
876
|
+
effect: () => runtime.processRuntime.run({ ...invocation, signal }),
|
|
877
|
+
});
|
|
878
|
+
if (result.exitCode !== 0)
|
|
879
|
+
throw new Error(`community_process_failed:restart:${result.exitCode}`);
|
|
880
|
+
await lifecycle.health({ paths, state });
|
|
881
|
+
await lifecycle.dataSmoke({ paths, state });
|
|
882
|
+
return { status: 'community-server-restarted', instance: state.instanceName };
|
|
883
|
+
});
|
|
884
|
+
throwIfCommunityCommandAborted(signal);
|
|
885
|
+
writeResult(output, options.json);
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
export async function runCommunityServerStatus(options) {
|
|
889
|
+
const native = inspectNativeFrom(options);
|
|
890
|
+
if (native.status !== 'not-installed') {
|
|
891
|
+
const recoveryErrors = [];
|
|
892
|
+
let applicationRecovery = null;
|
|
893
|
+
let databaseRecovery = null;
|
|
894
|
+
try {
|
|
895
|
+
applicationRecovery = inspectCommunityNativeApplicationRecoveryStatus(native.paths);
|
|
896
|
+
}
|
|
897
|
+
catch (error) {
|
|
898
|
+
recoveryErrors.push(error instanceof Error ? error.message : String(error));
|
|
899
|
+
}
|
|
900
|
+
try {
|
|
901
|
+
databaseRecovery = inspectCommunityNativeDatabaseRecoveryStatus(native.paths);
|
|
902
|
+
}
|
|
903
|
+
catch (error) {
|
|
904
|
+
recoveryErrors.push(error instanceof Error ? error.message : String(error));
|
|
905
|
+
}
|
|
906
|
+
const runtime = native.status === 'installed'
|
|
907
|
+
? await inspectCommunityNativeRuntime(installSelection(options))
|
|
908
|
+
: null;
|
|
909
|
+
const postgres = native.status === 'installed' && native.state?.postgres.mode === 'container'
|
|
910
|
+
? await inspectCommunityNativePostgres({
|
|
911
|
+
state: native.state.postgres,
|
|
912
|
+
instanceName: native.state.instanceName,
|
|
913
|
+
instanceRoot: native.paths.instanceRoot,
|
|
914
|
+
})
|
|
915
|
+
: null;
|
|
916
|
+
writeResult({
|
|
917
|
+
status: native.status,
|
|
918
|
+
runtime: 'native',
|
|
919
|
+
profile: native.state?.profile ?? null,
|
|
920
|
+
instanceRoot: native.paths.instanceRoot,
|
|
921
|
+
instance: native.state?.instanceName ?? options.instance ?? 'default',
|
|
922
|
+
origin: native.state ? nativePublicOrigin(native.state) : null,
|
|
923
|
+
listenAddress: native.state ? nativeListenAddress(native.state) : null,
|
|
924
|
+
exposure: native.state?.server.exposure ?? null,
|
|
925
|
+
authProvider: native.state?.server.authProvider ?? null,
|
|
926
|
+
processRecord: runtime?.process ? {
|
|
927
|
+
status: runtime.process.status,
|
|
928
|
+
mode: runtime.process.mode,
|
|
929
|
+
pid: runtime.process.pid,
|
|
930
|
+
hostPid: runtime.process.hostPid ?? null,
|
|
931
|
+
startedAt: runtime.process.startedAt,
|
|
932
|
+
logPath: runtime.process.logPath,
|
|
933
|
+
} : null,
|
|
934
|
+
runtimeState: runtime?.runtimeState ?? null,
|
|
935
|
+
postgres,
|
|
936
|
+
migration: native.migration ? nativeMigrationSummary(native.migration) : null,
|
|
937
|
+
applicationRecovery,
|
|
938
|
+
databaseRecovery,
|
|
939
|
+
recoveryErrors,
|
|
940
|
+
liveness: runtime ? {
|
|
941
|
+
supervisor: runtime.supervisorAlive,
|
|
942
|
+
host: runtime.hostAlive,
|
|
943
|
+
health: runtime.health,
|
|
944
|
+
identityBound: runtime.identity !== null,
|
|
945
|
+
recoverable: runtime.recoverable,
|
|
946
|
+
} : null,
|
|
947
|
+
error: recoveryErrors[0] ?? runtime?.reason ?? native.error ?? null,
|
|
948
|
+
presentFiles: native.presentFiles,
|
|
949
|
+
missingFiles: native.missingFiles,
|
|
950
|
+
}, options.json);
|
|
951
|
+
if (native.status !== 'installed' ||
|
|
952
|
+
recoveryErrors.length > 0 ||
|
|
953
|
+
(runtime && ['unhealthy', 'crashed', 'identity-conflict', 'orphaned'].includes(runtime.runtimeState)))
|
|
954
|
+
process.exitCode = 1;
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
const inspection = inspectFrom(options);
|
|
958
|
+
if (inspection.status !== 'installed') {
|
|
959
|
+
writeResult({
|
|
960
|
+
status: inspection.status,
|
|
961
|
+
instanceRoot: inspection.paths.instanceRoot,
|
|
962
|
+
error: inspection.error ?? null,
|
|
963
|
+
presentFiles: inspection.presentFiles,
|
|
964
|
+
missingFiles: inspection.missingFiles,
|
|
965
|
+
}, options.json);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
const invocation = buildCommunityComposeInvocation({ paths: inspection.paths, state: inspection.state, action: 'status' });
|
|
969
|
+
const result = await communityProcessRuntime.run(invocation);
|
|
970
|
+
if (options.json) {
|
|
971
|
+
writeResult({
|
|
972
|
+
status: result.exitCode === 0 ? 'installed' : 'docker-status-failed',
|
|
973
|
+
instance: inspection.state.instanceName,
|
|
974
|
+
releaseVersion: inspection.state.activeRelease.releaseVersion,
|
|
975
|
+
imageRef: inspection.state.activeRelease.imageRef,
|
|
976
|
+
docker: { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr },
|
|
977
|
+
}, true);
|
|
978
|
+
}
|
|
979
|
+
else {
|
|
980
|
+
writeResult(result.stdout || result.stderr || `AOPS Community ${inspection.state.activeRelease.releaseVersion} is installed.`);
|
|
981
|
+
}
|
|
982
|
+
if (result.exitCode !== 0)
|
|
983
|
+
process.exitCode = 1;
|
|
984
|
+
}
|
|
985
|
+
export async function runCommunityServerHealth(options) {
|
|
986
|
+
const native = inspectNativeFrom(options);
|
|
987
|
+
if (native.status === 'installed' && native.state) {
|
|
988
|
+
const observed = await inspectCommunityNativeRuntime(installSelection(options));
|
|
989
|
+
const healthy = observed.runtimeState === 'running' && observed.health === 'healthy';
|
|
990
|
+
writeResult({
|
|
991
|
+
status: healthy ? 'healthy' : 'unhealthy',
|
|
992
|
+
runtime: 'native',
|
|
993
|
+
profile: native.state.profile,
|
|
994
|
+
instance: native.state.instanceName,
|
|
995
|
+
origin: nativePublicOrigin(native.state),
|
|
996
|
+
listenAddress: nativeListenAddress(native.state),
|
|
997
|
+
exposure: native.state.server.exposure,
|
|
998
|
+
authProvider: native.state.server.authProvider,
|
|
999
|
+
runtimeState: observed.runtimeState,
|
|
1000
|
+
supervisor: observed.supervisorAlive,
|
|
1001
|
+
host: observed.hostAlive,
|
|
1002
|
+
health: observed.health,
|
|
1003
|
+
identityBound: observed.identity !== null,
|
|
1004
|
+
error: observed.reason ?? null,
|
|
1005
|
+
}, options.json);
|
|
1006
|
+
if (!healthy)
|
|
1007
|
+
process.exitCode = 1;
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
if (native.status !== 'not-installed') {
|
|
1011
|
+
throw new Error(`community_native_health_refused:${native.status}:${native.error ?? 'run_doctor'}`);
|
|
1012
|
+
}
|
|
1013
|
+
const inspection = requireInstalled(options);
|
|
1014
|
+
try {
|
|
1015
|
+
const lifecycle = createCommunityDockerAdapter({ verifyRelease: async () => undefined });
|
|
1016
|
+
await lifecycle.health({ paths: inspection.paths, state: inspection.state });
|
|
1017
|
+
writeResult({
|
|
1018
|
+
status: 'healthy',
|
|
1019
|
+
runtime: 'oci',
|
|
1020
|
+
instance: inspection.state.instanceName,
|
|
1021
|
+
releaseVersion: inspection.state.activeRelease.releaseVersion,
|
|
1022
|
+
}, options.json);
|
|
1023
|
+
}
|
|
1024
|
+
catch (error) {
|
|
1025
|
+
writeResult({
|
|
1026
|
+
status: 'unhealthy',
|
|
1027
|
+
runtime: 'oci',
|
|
1028
|
+
instance: inspection.state.instanceName,
|
|
1029
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1030
|
+
}, options.json);
|
|
1031
|
+
process.exitCode = 1;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
export async function runCommunityServerMigrationPlan(options, dependencies = {}) {
|
|
1035
|
+
const runtime = resolveDependencies(dependencies);
|
|
1036
|
+
await withCommunityCommandAbortScope(runtime.commandAbortRuntime, async (signal) => {
|
|
1037
|
+
throwIfCommunityCommandAborted(signal);
|
|
1038
|
+
const result = await runtime.planNativeMigration({ ...installSelection(options), signal });
|
|
1039
|
+
throwIfCommunityCommandAborted(signal);
|
|
1040
|
+
writeResult({
|
|
1041
|
+
surface: 'community-native-migration-plan',
|
|
1042
|
+
mutationFree: true,
|
|
1043
|
+
...result,
|
|
1044
|
+
}, options.json);
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
export async function runCommunityServerAttestExternalSnapshot(options, dependencies = {}) {
|
|
1048
|
+
await withMutatingCommandScope(dependencies, async (runtime, signal) => {
|
|
1049
|
+
const result = await runtime.attestNativeExternalSnapshot({
|
|
1050
|
+
...installSelection(options),
|
|
1051
|
+
expectedPlanSha256: String(options.expectedPlanSha256 ?? ''),
|
|
1052
|
+
provider: String(options.provider ?? ''),
|
|
1053
|
+
snapshotRef: String(options.snapshotRef ?? ''),
|
|
1054
|
+
snapshotDigest: options.snapshotDigest,
|
|
1055
|
+
attestedBy: String(options.attestedBy ?? ''),
|
|
1056
|
+
restoreInstructionsRef: String(options.restoreInstructionsRef ?? ''),
|
|
1057
|
+
preview: options.preview,
|
|
1058
|
+
apply: options.apply,
|
|
1059
|
+
confirmExternalRecoveryOwner: options.confirmExternalRecoveryOwner,
|
|
1060
|
+
signal,
|
|
1061
|
+
});
|
|
1062
|
+
throwIfCommunityCommandAborted(signal);
|
|
1063
|
+
writeResult({
|
|
1064
|
+
surface: 'community-native-external-snapshot-attestation',
|
|
1065
|
+
mutationFree: !result.applied,
|
|
1066
|
+
...result,
|
|
1067
|
+
next: result.applied
|
|
1068
|
+
? 'Optional recovery evidence recorded. Normal migration remains transaction-based and does not require this attestation.'
|
|
1069
|
+
: 'Review this exact plan-bound attestation, then re-run with --apply --confirm-external-recovery-owner.',
|
|
1070
|
+
}, options.json);
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
export async function runCommunityServerLogs(options) {
|
|
1074
|
+
const native = inspectNativeFrom(options);
|
|
1075
|
+
if (native.status === 'installed') {
|
|
1076
|
+
const tail = Number(options.tail ?? 100);
|
|
1077
|
+
const logs = readCommunityNativeLogs({ ...installSelection(options), tail });
|
|
1078
|
+
if (options.json)
|
|
1079
|
+
writeResult({
|
|
1080
|
+
status: 'community-native-logs',
|
|
1081
|
+
lineCount: logs.lineCount,
|
|
1082
|
+
truncated: logs.truncated,
|
|
1083
|
+
logPath: logs.logPath,
|
|
1084
|
+
content: logs.content,
|
|
1085
|
+
}, true);
|
|
1086
|
+
else
|
|
1087
|
+
writeResult(logs.content || 'No detached native server logs are available.');
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
if (native.status !== 'not-installed') {
|
|
1091
|
+
throw new Error(`community_native_logs_refused:${native.status}:${native.error ?? 'run_doctor'}`);
|
|
1092
|
+
}
|
|
1093
|
+
const { paths, state } = requireInstalled(options);
|
|
1094
|
+
const tail = Number(options.tail ?? 100);
|
|
1095
|
+
if (!Number.isSafeInteger(tail) || tail < 1 || tail > 10_000)
|
|
1096
|
+
throw new Error('community_logs_tail_invalid');
|
|
1097
|
+
const result = await communityProcessRuntime.run(buildCommunityComposeInvocation({ paths, state, action: 'logs', logsTail: tail }));
|
|
1098
|
+
if (options.json)
|
|
1099
|
+
writeResult({ exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr }, true);
|
|
1100
|
+
else
|
|
1101
|
+
writeResult(`${result.stdout}${result.stderr}`);
|
|
1102
|
+
if (result.exitCode !== 0)
|
|
1103
|
+
process.exitCode = 1;
|
|
1104
|
+
}
|
|
1105
|
+
export async function runCommunityServerUpdate(options, dependencies = {}) {
|
|
1106
|
+
await withMutatingCommandScope(dependencies, async (runtime, signal) => {
|
|
1107
|
+
const nativeInitial = inspectNativeFrom(options);
|
|
1108
|
+
if (nativeInitial.status === 'installed' && nativeInitial.state) {
|
|
1109
|
+
const setup = await withNativeOperation(options, 'update', runtime, signal, async (locked) => {
|
|
1110
|
+
const state = locked.state;
|
|
1111
|
+
const contract = buildCommunityInstanceContract({
|
|
1112
|
+
runtime: 'native',
|
|
1113
|
+
postgres: state.postgres.mode,
|
|
1114
|
+
postgresConfig: state.postgres.mode === 'external' ? state.postgres.configRef : undefined,
|
|
1115
|
+
postgresTls: state.postgres.mode === 'external' ? state.postgres.tlsPolicy : undefined,
|
|
1116
|
+
exposure: state.server.exposure,
|
|
1117
|
+
auth: state.server.authProvider,
|
|
1118
|
+
publicPort: state.server.publicPort,
|
|
1119
|
+
instance: state.instanceName,
|
|
1120
|
+
port: state.server.port,
|
|
1121
|
+
});
|
|
1122
|
+
return runtime.setupNativeInstall({
|
|
1123
|
+
contract,
|
|
1124
|
+
sourceRoot: options.sourceRoot,
|
|
1125
|
+
dataRoot: locked.paths.dataRoot,
|
|
1126
|
+
mode: resolveCommunityNativeLaunchMode(options),
|
|
1127
|
+
requireApplicationUpdate: true,
|
|
1128
|
+
signal,
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
if (!setup.applicationUpdate)
|
|
1132
|
+
throw new Error('community_native_application_update_record_missing');
|
|
1133
|
+
throwIfCommunityCommandAborted(signal);
|
|
1134
|
+
writeResult({
|
|
1135
|
+
status: 'community-native-application-updated',
|
|
1136
|
+
updateId: setup.applicationUpdate.prepared.updateId,
|
|
1137
|
+
priorReleaseVersion: setup.applicationUpdate.prepared.prior.content.releaseVersion,
|
|
1138
|
+
releaseVersion: setup.applicationUpdate.prepared.target.content.releaseVersion,
|
|
1139
|
+
applicationContentSha256: setup.applicationUpdate.prepared.target.content.applicationContentSha256,
|
|
1140
|
+
databaseAction: setup.launch.migration.action,
|
|
1141
|
+
databasePlanSha256: setup.launch.migration.acceptedPlanSha256,
|
|
1142
|
+
databaseRewound: false,
|
|
1143
|
+
}, options.json);
|
|
1144
|
+
if (setup.launch.waitForExit)
|
|
1145
|
+
await setup.launch.waitForExit();
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
if (nativeInitial.status !== 'not-installed') {
|
|
1149
|
+
throw new Error(`community_native_update_refused:${nativeInitial.status}:${nativeInitial.error ?? 'run_doctor'}`);
|
|
1150
|
+
}
|
|
1151
|
+
const initial = requireInstalled(options);
|
|
1152
|
+
assertOwnedInstancePaths(initial.paths, 'require-existing');
|
|
1153
|
+
const { assertCurrent, descriptorSource, verified } = await releaseBundle(options, runtime, signal);
|
|
1154
|
+
throwIfCommunityCommandAborted(signal);
|
|
1155
|
+
await requireHealthyPreflight('update', options, runtime);
|
|
1156
|
+
throwIfCommunityCommandAborted(signal);
|
|
1157
|
+
const record = await runtime.withOperationLock({ instanceDirectory: initial.paths.instanceRoot, operation: 'update', signal }, async ({ receipt }) => {
|
|
1158
|
+
const locked = requireInstalled(options);
|
|
1159
|
+
assertSameInstancePaths(initial.paths, locked.paths);
|
|
1160
|
+
assertOwnedInstancePaths(locked.paths, 'require-existing');
|
|
1161
|
+
assertCommunityRecoveryMutationFence(locked.paths);
|
|
1162
|
+
assertCurrent();
|
|
1163
|
+
await requireHealthyPreflight('update', options, runtime);
|
|
1164
|
+
throwIfCommunityCommandAborted(signal);
|
|
1165
|
+
assertOwnedInstancePaths(locked.paths, 'require-existing');
|
|
1166
|
+
return runWithOperationJournal({
|
|
1167
|
+
paths: locked.paths,
|
|
1168
|
+
state: locked.state,
|
|
1169
|
+
operation: 'update',
|
|
1170
|
+
receipt,
|
|
1171
|
+
signal,
|
|
1172
|
+
runtime,
|
|
1173
|
+
callback: async (context) => {
|
|
1174
|
+
const { journal, receipt } = context;
|
|
1175
|
+
const targetRelease = await runCommunityJournaledPromotion({
|
|
1176
|
+
handle: journal,
|
|
1177
|
+
step: 'stage-release',
|
|
1178
|
+
signal,
|
|
1179
|
+
promote: () => stageCommunityRelease({
|
|
1180
|
+
paths: locked.paths,
|
|
1181
|
+
manifestContent: verified.manifestContent,
|
|
1182
|
+
composeContent: verified.composeContent,
|
|
1183
|
+
manifestVerified: true,
|
|
1184
|
+
}),
|
|
1185
|
+
});
|
|
1186
|
+
const lifecycle = context.lifecycle;
|
|
1187
|
+
return updateCommunityInstall({
|
|
1188
|
+
paths: locked.paths,
|
|
1189
|
+
targetRelease,
|
|
1190
|
+
adapter: lifecycle,
|
|
1191
|
+
signal,
|
|
1192
|
+
operationJournal: journal,
|
|
1193
|
+
operationReceipt: receipt,
|
|
1194
|
+
});
|
|
1195
|
+
},
|
|
1196
|
+
});
|
|
1197
|
+
});
|
|
1198
|
+
throwIfCommunityCommandAborted(signal);
|
|
1199
|
+
writeResult({
|
|
1200
|
+
status: 'community-server-updated',
|
|
1201
|
+
updateId: record.id,
|
|
1202
|
+
releaseVersion: record.targetRelease.releaseVersion,
|
|
1203
|
+
imageRef: record.targetRelease.imageRef,
|
|
1204
|
+
releaseDescriptorSource: descriptorSource,
|
|
1205
|
+
certificateIdentity: verified.certificateIdentity,
|
|
1206
|
+
verifiedArtifactCount: verified.verifiedArtifactCount,
|
|
1207
|
+
backup: { path: record.backup.path, sha256: record.backup.sha256, byteLength: record.backup.byteLength },
|
|
1208
|
+
}, options.json);
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
export async function runCommunityServerRecover(options, dependencies = {}) {
|
|
1212
|
+
if (!options.updateId)
|
|
1213
|
+
throw new Error('community_recover_update_id_required');
|
|
1214
|
+
await withMutatingCommandScope(dependencies, async (runtime, signal) => {
|
|
1215
|
+
const initial = requireInstalled(options);
|
|
1216
|
+
assertOwnedInstancePaths(initial.paths, 'require-existing');
|
|
1217
|
+
await requireRecoveryPreflight(options, runtime);
|
|
1218
|
+
throwIfCommunityCommandAborted(signal);
|
|
1219
|
+
const record = await runtime.withOperationLock({ instanceDirectory: initial.paths.instanceRoot, operation: 'recover', signal }, async ({ receipt }) => {
|
|
1220
|
+
const locked = requireInstalled(options);
|
|
1221
|
+
assertSameInstancePaths(initial.paths, locked.paths);
|
|
1222
|
+
assertOwnedInstancePaths(locked.paths, 'require-existing');
|
|
1223
|
+
assertCommunityRecoveryMutationFence(locked.paths);
|
|
1224
|
+
await requireRecoveryPreflight(options, runtime);
|
|
1225
|
+
throwIfCommunityCommandAborted(signal);
|
|
1226
|
+
assertOwnedInstancePaths(locked.paths, 'require-existing');
|
|
1227
|
+
return runWithOperationJournal({
|
|
1228
|
+
paths: locked.paths,
|
|
1229
|
+
state: locked.state,
|
|
1230
|
+
operation: 'recover',
|
|
1231
|
+
receipt,
|
|
1232
|
+
signal,
|
|
1233
|
+
runtime,
|
|
1234
|
+
callback: ({ journal, receipt, lifecycle }) => recoverCommunityUpdate({
|
|
1235
|
+
paths: locked.paths,
|
|
1236
|
+
updateId: options.updateId,
|
|
1237
|
+
retryRecoveryId: options.retryRecoveryId,
|
|
1238
|
+
adapter: lifecycle,
|
|
1239
|
+
confirmDataRewind: options.confirmDataRewind === true,
|
|
1240
|
+
signal,
|
|
1241
|
+
operationJournal: journal,
|
|
1242
|
+
operationReceipt: receipt,
|
|
1243
|
+
}),
|
|
1244
|
+
});
|
|
1245
|
+
});
|
|
1246
|
+
throwIfCommunityCommandAborted(signal);
|
|
1247
|
+
writeResult({
|
|
1248
|
+
status: 'community-server-recovered',
|
|
1249
|
+
updateId: record.recoveredUpdateId,
|
|
1250
|
+
recoveryId: record.id,
|
|
1251
|
+
releaseVersion: record.priorRelease.releaseVersion,
|
|
1252
|
+
imageRef: record.priorRelease.imageRef,
|
|
1253
|
+
dataRewound: record.replacementVolumeName !== undefined,
|
|
1254
|
+
postgresVolumeName: record.replacementVolumeName ?? record.sourcePostgresVolumeName,
|
|
1255
|
+
}, options.json);
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
export async function runCommunityServerReconcileRecovery(options, dependencies = {}) {
|
|
1259
|
+
if (!options.updateId || !options.recoveryId) {
|
|
1260
|
+
throw new Error('community_recovery_reconciliation_exact_ids_required');
|
|
1261
|
+
}
|
|
1262
|
+
if (options.reconciliationAction !== 'complete-commit' &&
|
|
1263
|
+
options.reconciliationAction !== 'abandon-interrupted' &&
|
|
1264
|
+
options.reconciliationAction !== 'restore-source-runtime') {
|
|
1265
|
+
throw new Error('community_recovery_reconciliation_action_invalid');
|
|
1266
|
+
}
|
|
1267
|
+
await withMutatingCommandScope(dependencies, async (runtime, signal) => {
|
|
1268
|
+
const initial = requireInstalled(options);
|
|
1269
|
+
assertOwnedInstancePaths(initial.paths, 'require-existing');
|
|
1270
|
+
const record = await runtime.withOperationLock({ instanceDirectory: initial.paths.instanceRoot, operation: 'recover', signal }, async () => {
|
|
1271
|
+
const locked = requireInstalled(options);
|
|
1272
|
+
assertSameInstancePaths(initial.paths, locked.paths);
|
|
1273
|
+
assertOwnedInstancePaths(locked.paths, 'require-existing');
|
|
1274
|
+
if (options.reconciliationAction === 'complete-commit') {
|
|
1275
|
+
return reconcileCommunityRecoveryCommit({
|
|
1276
|
+
paths: locked.paths,
|
|
1277
|
+
updateId: options.updateId,
|
|
1278
|
+
recoveryId: options.recoveryId,
|
|
1279
|
+
confirm: options.confirmRecoveryReconciliation === true,
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
if (options.reconciliationAction === 'abandon-interrupted') {
|
|
1283
|
+
return abandonInterruptedCommunityRecovery({
|
|
1284
|
+
paths: locked.paths,
|
|
1285
|
+
updateId: options.updateId,
|
|
1286
|
+
recoveryId: options.recoveryId,
|
|
1287
|
+
confirm: options.confirmRecoveryReconciliation === true,
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
if (options.confirmRecoveryReconciliation !== true) {
|
|
1291
|
+
throw new Error('community_recovery_reconciliation_confirmation_required');
|
|
1292
|
+
}
|
|
1293
|
+
const restored = await restoreCommunitySourceRuntime({
|
|
1294
|
+
paths: locked.paths,
|
|
1295
|
+
sourceState: locked.state,
|
|
1296
|
+
operation: 'recover',
|
|
1297
|
+
operationStartedAt: new Date(0).toISOString(),
|
|
1298
|
+
expectedUpdateId: options.updateId,
|
|
1299
|
+
expectedRecoveryId: options.recoveryId,
|
|
1300
|
+
adapter: adapter(runtime, signal),
|
|
1301
|
+
confirmDataRewind: options.confirmDataRewind === true,
|
|
1302
|
+
signal,
|
|
1303
|
+
});
|
|
1304
|
+
if (!restored.reconciledRecord) {
|
|
1305
|
+
throw new Error('community_recovery_reconciliation_record_mismatch');
|
|
1306
|
+
}
|
|
1307
|
+
return restored.reconciledRecord;
|
|
1308
|
+
});
|
|
1309
|
+
throwIfCommunityCommandAborted(signal);
|
|
1310
|
+
writeResult({
|
|
1311
|
+
status: options.reconciliationAction === 'complete-commit'
|
|
1312
|
+
? 'community-recovery-commit-completed'
|
|
1313
|
+
: options.reconciliationAction === 'abandon-interrupted'
|
|
1314
|
+
? 'community-recovery-attempt-abandoned'
|
|
1315
|
+
: 'community-recovery-source-runtime-restored',
|
|
1316
|
+
updateId: record.recoveredUpdateId,
|
|
1317
|
+
recoveryId: record.id,
|
|
1318
|
+
recoveryStatus: record.status,
|
|
1319
|
+
retryWithRecoveryId: record.status === 'recovery-failed' ? record.id : undefined,
|
|
1320
|
+
}, options.json);
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
export async function runCommunityServerRollbackApplication(options, dependencies = {}) {
|
|
1324
|
+
if (!options.updateId)
|
|
1325
|
+
throw new Error('community_native_application_rollback_update_id_required');
|
|
1326
|
+
if (!options.sourceRoot)
|
|
1327
|
+
throw new Error('community_native_application_rollback_source_root_required');
|
|
1328
|
+
if (options.confirmDataRewind === true) {
|
|
1329
|
+
throw new Error('community_native_application_rollback_data_rewind_option_refused');
|
|
1330
|
+
}
|
|
1331
|
+
await withMutatingCommandScope(dependencies, async (runtime, signal) => {
|
|
1332
|
+
const result = await withNativeOperation(options, 'rollback', runtime, signal, async (locked) => {
|
|
1333
|
+
return runtime.rollbackNativeApplication({
|
|
1334
|
+
instanceName: locked.state.instanceName,
|
|
1335
|
+
dataRoot: locked.paths.dataRoot,
|
|
1336
|
+
updateId: options.updateId,
|
|
1337
|
+
sourceRoot: options.sourceRoot,
|
|
1338
|
+
mode: resolveCommunityNativeLaunchMode(options),
|
|
1339
|
+
signal,
|
|
1340
|
+
});
|
|
1341
|
+
});
|
|
1342
|
+
throwIfCommunityCommandAborted(signal);
|
|
1343
|
+
const rollback = result.applicationUpdate.rollbackOutcome;
|
|
1344
|
+
if (!rollback || rollback.status !== 'community-native-application-rolled-back') {
|
|
1345
|
+
throw new Error('community_native_application_rollback_receipt_missing');
|
|
1346
|
+
}
|
|
1347
|
+
writeResult({
|
|
1348
|
+
status: rollback.status,
|
|
1349
|
+
updateId: result.applicationUpdate.prepared.updateId,
|
|
1350
|
+
rollbackId: rollback.rollbackId,
|
|
1351
|
+
releaseVersion: result.state.source.releaseVersion,
|
|
1352
|
+
applicationContentSha256: result.applicationUpdate.prepared.prior.content.applicationContentSha256,
|
|
1353
|
+
databaseAction: rollback.migration?.action,
|
|
1354
|
+
databasePlanSha256: rollback.migration?.acceptedPlanSha256,
|
|
1355
|
+
databaseRewound: false,
|
|
1356
|
+
}, options.json);
|
|
1357
|
+
if (result.launch.waitForExit)
|
|
1358
|
+
await result.launch.waitForExit();
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
export async function runCommunityServerRestoreDatabase(options, dependencies = {}) {
|
|
1362
|
+
if (!options.updateId)
|
|
1363
|
+
throw new Error('community_native_database_restore_update_id_required');
|
|
1364
|
+
if (!options.sourceRoot)
|
|
1365
|
+
throw new Error('community_native_database_restore_source_root_required');
|
|
1366
|
+
if (options.confirmDataRewind !== true) {
|
|
1367
|
+
throw new Error('community_native_database_restore_confirmation_required');
|
|
1368
|
+
}
|
|
1369
|
+
await withMutatingCommandScope(dependencies, async (runtime, signal) => {
|
|
1370
|
+
const result = await withNativeOperation(options, 'restore', runtime, signal, async (locked) => {
|
|
1371
|
+
const observed = await inspectCommunityNativeRuntime({
|
|
1372
|
+
instanceName: locked.state.instanceName,
|
|
1373
|
+
dataRoot: locked.paths.dataRoot,
|
|
1374
|
+
});
|
|
1375
|
+
if (observed.runtimeState !== 'stopped' && observed.runtimeState !== 'crashed') {
|
|
1376
|
+
throw new Error(`community_native_process_active:${observed.runtimeState}:run_server_stop_before_database_restore`);
|
|
1377
|
+
}
|
|
1378
|
+
return runtime.restoreNativeDatabase({
|
|
1379
|
+
paths: locked.paths,
|
|
1380
|
+
state: locked.state,
|
|
1381
|
+
updateId: options.updateId,
|
|
1382
|
+
sourceRoot: path.resolve(options.sourceRoot),
|
|
1383
|
+
confirmDataRewind: true,
|
|
1384
|
+
confirmExternalRestoreComplete: options.confirmExternalRestoreComplete === true,
|
|
1385
|
+
signal,
|
|
1386
|
+
});
|
|
1387
|
+
});
|
|
1388
|
+
throwIfCommunityCommandAborted(signal);
|
|
1389
|
+
if (result.actionRequired) {
|
|
1390
|
+
writeResult({
|
|
1391
|
+
status: 'community-native-database-restore-external-action-required',
|
|
1392
|
+
restoreId: result.prepared.restoreId,
|
|
1393
|
+
updateId: result.prepared.updateId,
|
|
1394
|
+
recoveryOwner: result.prepared.recoveryOwner,
|
|
1395
|
+
evidenceKind: result.prepared.evidenceKind,
|
|
1396
|
+
evidenceSha256: result.prepared.evidenceSha256,
|
|
1397
|
+
provider: result.externalAction?.provider,
|
|
1398
|
+
snapshotRef: result.externalAction?.snapshotRef,
|
|
1399
|
+
snapshotDigest: result.externalAction?.snapshotDigest,
|
|
1400
|
+
restoreInstructionsRef: result.externalAction?.restoreInstructionsRef,
|
|
1401
|
+
nextCommand: 'Repeat server restore db with the same arguments plus --confirm-external-restore-complete after the external restore is complete',
|
|
1402
|
+
dataRewound: false,
|
|
1403
|
+
}, options.json);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
if (!result.completed)
|
|
1407
|
+
throw new Error('community_native_database_restore_receipt_missing');
|
|
1408
|
+
writeResult({
|
|
1409
|
+
status: result.completed.status,
|
|
1410
|
+
restoreId: result.completed.restoreId,
|
|
1411
|
+
updateId: result.completed.updateId,
|
|
1412
|
+
recoveryOwner: result.completed.recoveryOwner,
|
|
1413
|
+
evidenceKind: result.completed.evidenceKind,
|
|
1414
|
+
evidenceSha256: result.completed.evidenceSha256,
|
|
1415
|
+
restoredLineageId: result.completed.restoredLineageId,
|
|
1416
|
+
restoredStateFingerprintSha256: result.completed.restoredStateFingerprintSha256,
|
|
1417
|
+
dataRewound: result.dataRewound,
|
|
1418
|
+
applicationChanged: false,
|
|
1419
|
+
nextCommand: 'Run server rollback app with the exact update ID and prior source checkout',
|
|
1420
|
+
}, options.json);
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
export async function runCommunityServerRollback(options, dependencies = {}) {
|
|
1424
|
+
await withMutatingCommandScope(dependencies, async (runtime, signal) => {
|
|
1425
|
+
const initial = requireInstalled(options);
|
|
1426
|
+
assertOwnedInstancePaths(initial.paths, 'require-existing');
|
|
1427
|
+
await requireHealthyPreflight('rollback', options, runtime);
|
|
1428
|
+
throwIfCommunityCommandAborted(signal);
|
|
1429
|
+
const record = await runtime.withOperationLock({ instanceDirectory: initial.paths.instanceRoot, operation: 'rollback', signal }, async ({ receipt }) => {
|
|
1430
|
+
const locked = requireInstalled(options);
|
|
1431
|
+
assertSameInstancePaths(initial.paths, locked.paths);
|
|
1432
|
+
assertOwnedInstancePaths(locked.paths, 'require-existing');
|
|
1433
|
+
assertCommunityRecoveryMutationFence(locked.paths);
|
|
1434
|
+
await requireHealthyPreflight('rollback', options, runtime);
|
|
1435
|
+
throwIfCommunityCommandAborted(signal);
|
|
1436
|
+
assertOwnedInstancePaths(locked.paths, 'require-existing');
|
|
1437
|
+
return runWithOperationJournal({
|
|
1438
|
+
paths: locked.paths,
|
|
1439
|
+
state: locked.state,
|
|
1440
|
+
operation: 'rollback',
|
|
1441
|
+
receipt,
|
|
1442
|
+
signal,
|
|
1443
|
+
runtime,
|
|
1444
|
+
callback: ({ journal, receipt, lifecycle }) => rollbackCommunityInstall({
|
|
1445
|
+
paths: locked.paths,
|
|
1446
|
+
adapter: lifecycle,
|
|
1447
|
+
confirmDataRewind: options.confirmDataRewind === true,
|
|
1448
|
+
signal,
|
|
1449
|
+
operationJournal: journal,
|
|
1450
|
+
operationReceipt: receipt,
|
|
1451
|
+
}),
|
|
1452
|
+
});
|
|
1453
|
+
});
|
|
1454
|
+
throwIfCommunityCommandAborted(signal);
|
|
1455
|
+
writeResult({
|
|
1456
|
+
status: 'community-server-rolled-back',
|
|
1457
|
+
updateId: record.id,
|
|
1458
|
+
releaseVersion: record.priorRelease.releaseVersion,
|
|
1459
|
+
replacementVolumeName: record.replacementVolumeName,
|
|
1460
|
+
}, options.json);
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
export async function runCommunityServerBackup(options, dependencies = {}) {
|
|
1464
|
+
await withMutatingCommandScope(dependencies, async (runtime, signal) => {
|
|
1465
|
+
const output = await withInstalledOperation(options, 'backup', runtime, signal, async ({ paths, state }, { journal, lifecycle }) => {
|
|
1466
|
+
const record = await lifecycle.createBackup({ paths, state });
|
|
1467
|
+
throwIfCommunityCommandAborted(signal);
|
|
1468
|
+
await verifyCommunityBackupRecord(record);
|
|
1469
|
+
throwIfCommunityCommandAborted(signal);
|
|
1470
|
+
const receiptPath = await runCommunityJournaledPromotion({
|
|
1471
|
+
handle: journal,
|
|
1472
|
+
step: 'write-backup-receipt',
|
|
1473
|
+
signal,
|
|
1474
|
+
promote: () => writeCommunityBackupReceipt(paths, record),
|
|
1475
|
+
});
|
|
1476
|
+
return {
|
|
1477
|
+
status: 'community-backup-created',
|
|
1478
|
+
backup: { path: record.path, receiptPath, sha256: record.sha256, byteLength: record.byteLength },
|
|
1479
|
+
sourceRelease: record.sourceRelease.imageRef,
|
|
1480
|
+
};
|
|
1481
|
+
});
|
|
1482
|
+
throwIfCommunityCommandAborted(signal);
|
|
1483
|
+
writeResult(output, options.json);
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
export async function runCommunityServerRestore(options, dependencies = {}) {
|
|
1487
|
+
if (!options.backup)
|
|
1488
|
+
throw new Error('community_restore_backup_path_required');
|
|
1489
|
+
await withMutatingCommandScope(dependencies, async (runtime, signal) => {
|
|
1490
|
+
const output = await withInstalledOperation(options, 'restore', runtime, signal, async ({ paths }, { journal, receipt, lifecycle }) => {
|
|
1491
|
+
const backup = await readCommunityBackupReceipt(paths, path.resolve(options.backup));
|
|
1492
|
+
throwIfCommunityCommandAborted(signal);
|
|
1493
|
+
const result = await restoreCommunityBackup({
|
|
1494
|
+
paths,
|
|
1495
|
+
backup,
|
|
1496
|
+
adapter: lifecycle,
|
|
1497
|
+
confirmDataRewind: options.confirmDataRewind === true,
|
|
1498
|
+
signal,
|
|
1499
|
+
operationJournal: journal,
|
|
1500
|
+
operationReceipt: receipt,
|
|
1501
|
+
});
|
|
1502
|
+
throwIfCommunityCommandAborted(signal);
|
|
1503
|
+
return {
|
|
1504
|
+
status: 'community-backup-restored',
|
|
1505
|
+
backup: { path: backup.path, sha256: backup.sha256, byteLength: backup.byteLength },
|
|
1506
|
+
replacementVolumeName: result.replacementVolumeName,
|
|
1507
|
+
};
|
|
1508
|
+
});
|
|
1509
|
+
throwIfCommunityCommandAborted(signal);
|
|
1510
|
+
writeResult(output, options.json);
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
const COMMUNITY_OPERATION_RECONCILIATION_ACTIONS = new Set([
|
|
1514
|
+
'acknowledge-no-side-effect',
|
|
1515
|
+
'acknowledge-native-runtime-state',
|
|
1516
|
+
'restore-source-runtime',
|
|
1517
|
+
'acknowledge-artifact-preserved',
|
|
1518
|
+
'acknowledge-partial-install-preserved',
|
|
1519
|
+
'complete-reset-preserving-volumes',
|
|
1520
|
+
'complete-native-reset-preserving-volume',
|
|
1521
|
+
]);
|
|
1522
|
+
function requireOperationReconciliationSelection(options) {
|
|
1523
|
+
if (!options.operationId || !options.expectedOperation || !options.reconciliationAction) {
|
|
1524
|
+
throw new Error('community_operation_reconciliation_exact_selection_required:run_server_operation-status');
|
|
1525
|
+
}
|
|
1526
|
+
if (!COMMUNITY_OPERATION_RECONCILIATION_ACTIONS.has(options.reconciliationAction)) {
|
|
1527
|
+
throw new Error('community_operation_reconciliation_action_invalid');
|
|
1528
|
+
}
|
|
1529
|
+
return {
|
|
1530
|
+
operationId: options.operationId,
|
|
1531
|
+
expectedOperation: options.expectedOperation,
|
|
1532
|
+
action: options.reconciliationAction,
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
export async function runCommunityServerOperationStatus(options) {
|
|
1536
|
+
if (!options.operationId)
|
|
1537
|
+
throw new Error('community_operation_status_id_required');
|
|
1538
|
+
const basePaths = pathsFrom(options);
|
|
1539
|
+
const inspection = inspectCommunityOperationJournalFile(basePaths, options.operationId);
|
|
1540
|
+
const paths = journalPathsForMode(options, inspection.record.runtimeMode);
|
|
1541
|
+
const currentDigests = captureCommunityOperationDigests(paths);
|
|
1542
|
+
writeResult({
|
|
1543
|
+
surface: 'community-operation-status',
|
|
1544
|
+
instance: options.instance ?? 'default',
|
|
1545
|
+
operationId: inspection.record.id,
|
|
1546
|
+
operation: inspection.record.operation,
|
|
1547
|
+
runtimeMode: inspection.record.runtimeMode,
|
|
1548
|
+
integrity: inspection.integrity,
|
|
1549
|
+
validBytes: inspection.validBytes,
|
|
1550
|
+
journalSha256: inspection.fileSha256,
|
|
1551
|
+
status: inspection.record.status,
|
|
1552
|
+
phase: inspection.record.phase,
|
|
1553
|
+
step: inspection.record.step,
|
|
1554
|
+
lastRunningPhase: inspection.lastRunningPhase,
|
|
1555
|
+
lastRunningStep: inspection.lastRunningStep,
|
|
1556
|
+
failure: inspection.record.failure ?? null,
|
|
1557
|
+
permittedActions: inspection.record.permittedActions,
|
|
1558
|
+
receipt: inspection.record.receipt,
|
|
1559
|
+
preDigests: inspection.record.preDigests,
|
|
1560
|
+
currentDigests,
|
|
1561
|
+
currentMatchesPre: digestsEqual(currentDigests, inspection.record.preDigests),
|
|
1562
|
+
sourceState: inspection.record.sourceState,
|
|
1563
|
+
recoveryCommitPending: inspection.record.runtimeMode === 'oci' && Boolean(tryLstat(paths.recoveryJournalPath)),
|
|
1564
|
+
}, options.json);
|
|
1565
|
+
}
|
|
1566
|
+
async function applyCommunityOperationReconciliation(params) {
|
|
1567
|
+
const { options, runtime, signal, paths, inspection, action } = params;
|
|
1568
|
+
const record = inspection.record;
|
|
1569
|
+
const currentDigests = captureCommunityOperationDigests(paths);
|
|
1570
|
+
if (action === 'acknowledge-no-side-effect') {
|
|
1571
|
+
const localRuntimeStateAbsent = currentDigests.state === null
|
|
1572
|
+
&& currentDigests.env === null
|
|
1573
|
+
&& currentDigests.ledger === null;
|
|
1574
|
+
const retryableExternalNativeSetupWithoutLocalState = record.runtimeMode === 'native' && record.operation === 'setup' &&
|
|
1575
|
+
options.postgres === 'external' && record.sourceState === null &&
|
|
1576
|
+
inspection.lastRunningPhase === 'side-effect-before' && inspection.lastRunningStep === 'native-setup' &&
|
|
1577
|
+
inspectNativeFrom(options).status === 'not-installed' && localRuntimeStateAbsent;
|
|
1578
|
+
const stillAtPreparedState = inspection.lastRunningPhase === 'prepared' && inspection.lastRunningStep === 'prepared';
|
|
1579
|
+
if ((!stillAtPreparedState && !retryableExternalNativeSetupWithoutLocalState) ||
|
|
1580
|
+
(!digestsEqual(currentDigests, record.preDigests) && !retryableExternalNativeSetupWithoutLocalState)) {
|
|
1581
|
+
throw new Error('community_operation_reconciliation_not_pre_effect');
|
|
1582
|
+
}
|
|
1583
|
+
if (record.runtimeMode === 'oci') {
|
|
1584
|
+
assertNoCommunityRecoveryCommitJournal(paths);
|
|
1585
|
+
assertCommunityLedgerHasNoNonterminalOperation(paths);
|
|
1586
|
+
}
|
|
1587
|
+
return { action, sideEffectAcknowledged: false };
|
|
1588
|
+
}
|
|
1589
|
+
if (action === 'acknowledge-native-runtime-state') {
|
|
1590
|
+
if (record.runtimeMode !== 'native') {
|
|
1591
|
+
throw new Error('community_native_operation_reconciliation_mode_mismatch');
|
|
1592
|
+
}
|
|
1593
|
+
const native = inspectNativeFrom(options);
|
|
1594
|
+
if (native.status !== 'installed' || !native.state) {
|
|
1595
|
+
throw new Error(`community_native_operation_reconciliation_requires_install:${native.status}`);
|
|
1596
|
+
}
|
|
1597
|
+
assertCommunityNativePathLayout(native.paths, { requireInstanceRoot: true });
|
|
1598
|
+
const observed = await inspectCommunityNativeRuntime({
|
|
1599
|
+
...installSelection(options),
|
|
1600
|
+
});
|
|
1601
|
+
if (observed.runtimeState === 'identity-conflict' || observed.runtimeState === 'orphaned') {
|
|
1602
|
+
throw new Error(`community_native_operation_reconciliation_runtime_unsafe:${observed.runtimeState}`);
|
|
1603
|
+
}
|
|
1604
|
+
return {
|
|
1605
|
+
action,
|
|
1606
|
+
nativeProfile: native.state.profile,
|
|
1607
|
+
runtimeState: observed.runtimeState,
|
|
1608
|
+
health: observed.health,
|
|
1609
|
+
statePreserved: true,
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
if (action === 'acknowledge-artifact-preserved') {
|
|
1613
|
+
if (record.operation !== 'backup' || !digestsEqual(currentDigests, record.preDigests) ||
|
|
1614
|
+
inspection.lastRunningStep.startsWith('write-backup-receipt') ||
|
|
1615
|
+
inspection.lastRunningPhase === 'promotion-before' || inspection.lastRunningPhase === 'promotion-after') {
|
|
1616
|
+
throw new Error('community_backup_artifact_reconciliation_precondition_failed');
|
|
1617
|
+
}
|
|
1618
|
+
assertNoCommunityRecoveryCommitJournal(paths);
|
|
1619
|
+
assertCommunityLedgerHasNoNonterminalOperation(paths);
|
|
1620
|
+
return { action, artifactPreserved: true, receiptPromoted: false };
|
|
1621
|
+
}
|
|
1622
|
+
if (action === 'acknowledge-partial-install-preserved') {
|
|
1623
|
+
if (record.operation !== 'setup' || record.sourceState !== null) {
|
|
1624
|
+
throw new Error('community_partial_install_reconciliation_precondition_failed');
|
|
1625
|
+
}
|
|
1626
|
+
const install = inspectFrom(options);
|
|
1627
|
+
assertSameInstancePaths(paths, install.paths);
|
|
1628
|
+
if (install.status !== 'partial') {
|
|
1629
|
+
throw new Error('community_partial_install_reconciliation_requires_partial_install');
|
|
1630
|
+
}
|
|
1631
|
+
assertNoCommunityRecoveryCommitJournal(paths);
|
|
1632
|
+
assertCommunityLedgerHasNoNonterminalOperation(paths);
|
|
1633
|
+
return { action, partialInstallPreserved: true, nextSafeAction: 'reset' };
|
|
1634
|
+
}
|
|
1635
|
+
if (action === 'complete-reset-preserving-volumes') {
|
|
1636
|
+
const instance = (options.instance ?? 'default').trim().toLowerCase();
|
|
1637
|
+
const eligibleReset = record.operation === 'reset' || (record.operation === 'setup' && record.sourceState === null);
|
|
1638
|
+
if (!eligibleReset || options.confirmDataLoss !== true || options.confirmInstance !== instance) {
|
|
1639
|
+
throw new Error('community_operation_reset_reconciliation_confirmation_required');
|
|
1640
|
+
}
|
|
1641
|
+
const install = inspectFrom({ ...options, instance });
|
|
1642
|
+
if (install.status === 'installed') {
|
|
1643
|
+
await adapter(runtime, signal).stop({ paths, state: install.state });
|
|
1644
|
+
}
|
|
1645
|
+
throwIfCommunityCommandAborted(signal);
|
|
1646
|
+
removeInstanceContentsExceptLock(paths.instanceRoot);
|
|
1647
|
+
return { action, instance, rootPreserved: true, dockerVolumesPreserved: true };
|
|
1648
|
+
}
|
|
1649
|
+
if (action === 'complete-native-reset-preserving-volume') {
|
|
1650
|
+
const instance = (options.instance ?? 'default').trim().toLowerCase();
|
|
1651
|
+
if (record.runtimeMode !== 'native' ||
|
|
1652
|
+
options.confirmDataLoss !== true || options.confirmInstance !== instance) {
|
|
1653
|
+
throw new Error('community_native_operation_reset_reconciliation_confirmation_required');
|
|
1654
|
+
}
|
|
1655
|
+
const native = inspectNativeFrom({ ...options, instance });
|
|
1656
|
+
if (native.status === 'runtime-conflict') {
|
|
1657
|
+
throw new Error('community_native_operation_reset_reconciliation_runtime_conflict');
|
|
1658
|
+
}
|
|
1659
|
+
if (native.status === 'installed') {
|
|
1660
|
+
await runtime.stopNativeCockpit({ ...installSelection({ ...options, instance }), signal });
|
|
1661
|
+
const observed = await inspectCommunityNativeRuntime({ ...installSelection({ ...options, instance }) });
|
|
1662
|
+
if (observed.supervisorAlive || observed.hostAlive) {
|
|
1663
|
+
if (observed.runtimeState === 'running' || observed.runtimeState === 'starting' || observed.runtimeState === 'unhealthy') {
|
|
1664
|
+
await stopCommunityNativeInstall({ ...installSelection({ ...options, instance }), signal });
|
|
1665
|
+
}
|
|
1666
|
+
else {
|
|
1667
|
+
throw new Error(`community_native_operation_reset_live_process_refused:${observed.runtimeState}`);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
throwIfCommunityCommandAborted(signal);
|
|
1672
|
+
const hasManagedPostgres = (native.status === 'installed' && native.state?.profile === 'native-container-postgres') ||
|
|
1673
|
+
existsSync(path.join(paths.runtimeRoot, 'native-postgres.env'));
|
|
1674
|
+
const postgres = hasManagedPostgres
|
|
1675
|
+
? await runtime.removeNativePostgresContainerForReset({
|
|
1676
|
+
instanceName: instance,
|
|
1677
|
+
instanceRoot: paths.instanceRoot,
|
|
1678
|
+
signal,
|
|
1679
|
+
})
|
|
1680
|
+
: null;
|
|
1681
|
+
throwIfCommunityCommandAborted(signal);
|
|
1682
|
+
removeNativeInstanceContentsPreservingPostgres(paths.instanceRoot);
|
|
1683
|
+
return {
|
|
1684
|
+
action,
|
|
1685
|
+
instance,
|
|
1686
|
+
rootPreserved: true,
|
|
1687
|
+
postgresContainerPreserved: false,
|
|
1688
|
+
postgresContainer: postgres?.container ?? 'not-managed',
|
|
1689
|
+
postgresContainerName: postgres?.containerName ?? null,
|
|
1690
|
+
postgresVolumePreserved: true,
|
|
1691
|
+
postgresVolumeName: postgres?.volumeName ?? null,
|
|
1692
|
+
postgresSecretPreserved: hasManagedPostgres,
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
if (action === 'restore-source-runtime') {
|
|
1696
|
+
if (!record.sourceState)
|
|
1697
|
+
throw new Error('community_operation_source_state_missing');
|
|
1698
|
+
const restored = await restoreCommunitySourceRuntime({
|
|
1699
|
+
paths,
|
|
1700
|
+
sourceState: record.sourceState,
|
|
1701
|
+
operation: record.operation,
|
|
1702
|
+
operationStartedAt: record.createdAt,
|
|
1703
|
+
adapter: adapter(runtime, signal),
|
|
1704
|
+
confirmDataRewind: options.confirmDataRewind === true,
|
|
1705
|
+
signal,
|
|
1706
|
+
});
|
|
1707
|
+
return {
|
|
1708
|
+
action,
|
|
1709
|
+
releaseVersion: restored.state.activeRelease.releaseVersion,
|
|
1710
|
+
imageRef: restored.state.activeRelease.imageRef,
|
|
1711
|
+
postgresVolumeName: restored.state.postgresVolumeName,
|
|
1712
|
+
reconciledLedgerRecordId: restored.reconciledRecord?.id,
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
throw new Error('community_operation_reconciliation_action_invalid');
|
|
1716
|
+
}
|
|
1717
|
+
export async function runCommunityServerReconcileOperation(options, dependencies = {}) {
|
|
1718
|
+
const selection = requireOperationReconciliationSelection(options);
|
|
1719
|
+
if (options.confirmOperationReconciliation !== true) {
|
|
1720
|
+
throw new Error('community_operation_reconciliation_confirmation_required');
|
|
1721
|
+
}
|
|
1722
|
+
await withMutatingCommandScope(dependencies, async (runtime, signal) => {
|
|
1723
|
+
const basePaths = pathsFrom(options);
|
|
1724
|
+
assertInstanceDirectory(basePaths.instanceRoot);
|
|
1725
|
+
const initial = inspectCommunityOperationJournalFile(basePaths, selection.operationId);
|
|
1726
|
+
const paths = journalPathsForMode(options, initial.record.runtimeMode);
|
|
1727
|
+
if (initial.record.operation !== selection.expectedOperation ||
|
|
1728
|
+
!initial.record.permittedActions.includes(selection.action) ||
|
|
1729
|
+
['succeeded', 'reconciled'].includes(initial.record.status)) {
|
|
1730
|
+
throw new Error('community_operation_reconciliation_identity_mismatch');
|
|
1731
|
+
}
|
|
1732
|
+
const output = await runtime.withOperationLock({ instanceDirectory: paths.instanceRoot, operation: selection.expectedOperation, signal }, async () => {
|
|
1733
|
+
const lockedPaths = journalPathsForMode(options, initial.record.runtimeMode);
|
|
1734
|
+
assertSameInstancePaths(paths, lockedPaths);
|
|
1735
|
+
if (initial.record.runtimeMode === 'oci')
|
|
1736
|
+
assertNoCommunityRecoveryCommitJournal(lockedPaths);
|
|
1737
|
+
else
|
|
1738
|
+
assertCommunityNativePathLayout(resolveCommunityNativePaths(installSelection(options)), { requireInstanceRoot: true });
|
|
1739
|
+
const lockedInspection = inspectCommunityOperationJournalFile(lockedPaths, selection.operationId);
|
|
1740
|
+
if (lockedInspection.record.sequence !== initial.record.sequence ||
|
|
1741
|
+
lockedInspection.lastHash !== initial.lastHash ||
|
|
1742
|
+
lockedInspection.integrity !== initial.integrity ||
|
|
1743
|
+
lockedInspection.validBytes !== initial.validBytes ||
|
|
1744
|
+
lockedInspection.fileSha256 !== initial.fileSha256) {
|
|
1745
|
+
throw new Error('community_operation_reconciliation_generation_changed');
|
|
1746
|
+
}
|
|
1747
|
+
const journal = openCommunityOperationJournalForReconciliation({
|
|
1748
|
+
paths: lockedPaths,
|
|
1749
|
+
operationId: selection.operationId,
|
|
1750
|
+
expectedOperation: selection.expectedOperation,
|
|
1751
|
+
action: selection.action,
|
|
1752
|
+
confirm: true,
|
|
1753
|
+
expectedSequence: lockedInspection.record.sequence,
|
|
1754
|
+
expectedLastHash: lockedInspection.lastHash,
|
|
1755
|
+
expectedFileSha256: lockedInspection.fileSha256,
|
|
1756
|
+
});
|
|
1757
|
+
try {
|
|
1758
|
+
assertCommunityOperationJournalFence(lockedPaths, {
|
|
1759
|
+
handle: journal,
|
|
1760
|
+
operation: selection.expectedOperation,
|
|
1761
|
+
receipt: lockedInspection.record.receipt,
|
|
1762
|
+
reconciliation: true,
|
|
1763
|
+
});
|
|
1764
|
+
throwIfCommunityCommandAborted(signal);
|
|
1765
|
+
const detail = await applyCommunityOperationReconciliation({
|
|
1766
|
+
options,
|
|
1767
|
+
runtime,
|
|
1768
|
+
signal,
|
|
1769
|
+
paths: lockedPaths,
|
|
1770
|
+
inspection: lockedInspection,
|
|
1771
|
+
action: selection.action,
|
|
1772
|
+
});
|
|
1773
|
+
throwIfCommunityCommandAborted(signal);
|
|
1774
|
+
journal.assertOwned();
|
|
1775
|
+
const terminal = finishCommunityOperationJournal(journal, lockedPaths, 'reconciled');
|
|
1776
|
+
return {
|
|
1777
|
+
surface: 'community-operation-reconciliation',
|
|
1778
|
+
status: terminal.status,
|
|
1779
|
+
operationId: terminal.id,
|
|
1780
|
+
operation: terminal.operation,
|
|
1781
|
+
...detail,
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
finally {
|
|
1785
|
+
journal.close();
|
|
1786
|
+
}
|
|
1787
|
+
});
|
|
1788
|
+
throwIfCommunityCommandAborted(signal);
|
|
1789
|
+
writeResult(output, options.json);
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
export async function runCommunityServerLockStatus(options, dependencies = {}) {
|
|
1793
|
+
const runtime = resolveDependencies(dependencies);
|
|
1794
|
+
const paths = pathsFrom(options);
|
|
1795
|
+
assertInstanceDirectory(paths.instanceRoot);
|
|
1796
|
+
const inspection = await runtime.inspectOperationLock({ instanceDirectory: paths.instanceRoot });
|
|
1797
|
+
writeResult({ surface: 'community-operation-lock-status', ...inspection }, options.json);
|
|
1798
|
+
}
|
|
1799
|
+
export async function runCommunityServerRecoverLock(options, dependencies = {}) {
|
|
1800
|
+
if (!options.expectedLockOperation || !options.expectedLockStartedAt ||
|
|
1801
|
+
!options.expectedLockOwnerSha256 || !options.expectedProcessStartIdentity) {
|
|
1802
|
+
throw new Error('community_lock_recovery_exact_receipt_required:run_server_lock-status');
|
|
1803
|
+
}
|
|
1804
|
+
const pid = Number(options.expectedLockPid);
|
|
1805
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
1806
|
+
throw new Error('community_lock_recovery_expected_pid_invalid');
|
|
1807
|
+
}
|
|
1808
|
+
const expectedReceipt = {
|
|
1809
|
+
schemaVersion: 2,
|
|
1810
|
+
pid,
|
|
1811
|
+
operation: options.expectedLockOperation,
|
|
1812
|
+
startedAt: options.expectedLockStartedAt,
|
|
1813
|
+
ownerTokenSha256: options.expectedLockOwnerSha256,
|
|
1814
|
+
processStartIdentity: options.expectedProcessStartIdentity,
|
|
1815
|
+
};
|
|
1816
|
+
const runtime = resolveDependencies(dependencies);
|
|
1817
|
+
const paths = pathsFrom(options);
|
|
1818
|
+
assertInstanceDirectory(paths.instanceRoot);
|
|
1819
|
+
const result = await runtime.recoverStaleOperationLock({
|
|
1820
|
+
instanceDirectory: paths.instanceRoot,
|
|
1821
|
+
expectedReceipt,
|
|
1822
|
+
confirm: options.confirmStaleLockRecovery === true,
|
|
1823
|
+
});
|
|
1824
|
+
writeResult({ surface: 'community-operation-lock-recovery', ...result }, options.json);
|
|
1825
|
+
}
|
|
1826
|
+
export async function runCommunityServerReset(options, dependencies = {}) {
|
|
1827
|
+
const instance = (options.instance ?? 'default').trim().toLowerCase();
|
|
1828
|
+
if (options.confirmDataLoss !== true || options.confirmInstance !== instance) {
|
|
1829
|
+
throw new Error('community_reset_confirmation_required:use_--confirm-data-loss_--confirm-instance');
|
|
1830
|
+
}
|
|
1831
|
+
await withMutatingCommandScope(dependencies, async (runtime, signal) => {
|
|
1832
|
+
const resetOptions = { ...options, instance };
|
|
1833
|
+
const native = inspectNativeFrom(resetOptions);
|
|
1834
|
+
if (native.status === 'installed' && native.state) {
|
|
1835
|
+
if (native.state.profile === 'native-container-postgres' && options.removeManagedPostgres !== true) {
|
|
1836
|
+
throw new Error('community_native_container_reset_refused:use_--remove-managed-postgres_to_delete_the_label_verified_database');
|
|
1837
|
+
}
|
|
1838
|
+
const expectedRoot = pathsFrom(resetOptions).instanceRoot;
|
|
1839
|
+
if (!isSamePhysicalPath(path.resolve(expectedRoot), path.resolve(native.paths.instanceRoot))) {
|
|
1840
|
+
throw new Error('community_reset_path_mismatch');
|
|
1841
|
+
}
|
|
1842
|
+
assertInstanceDirectory(expectedRoot);
|
|
1843
|
+
const output = await runtime.withOperationLock({ instanceDirectory: expectedRoot, operation: 'reset', signal }, async () => {
|
|
1844
|
+
const lockedNative = inspectNativeFrom(resetOptions);
|
|
1845
|
+
if (lockedNative.status !== 'installed' || !lockedNative.state) {
|
|
1846
|
+
throw new Error(`community_native_install_changed:${lockedNative.status}:${lockedNative.error ?? 'unknown'}`);
|
|
1847
|
+
}
|
|
1848
|
+
if (lockedNative.state.profile === 'native-container-postgres' &&
|
|
1849
|
+
options.removeManagedPostgres !== true) {
|
|
1850
|
+
throw new Error('community_native_container_reset_refused:use_--remove-managed-postgres_to_delete_the_label_verified_database');
|
|
1851
|
+
}
|
|
1852
|
+
if (lockedNative.state.profile !== 'native-external-postgres' &&
|
|
1853
|
+
lockedNative.state.profile !== 'native-container-postgres') {
|
|
1854
|
+
throw new Error('community_native_reset_profile_invalid');
|
|
1855
|
+
}
|
|
1856
|
+
const lockedOci = inspectFrom(resetOptions);
|
|
1857
|
+
if (lockedOci.status !== 'not-installed') {
|
|
1858
|
+
throw new Error(`community_instance_runtime_conflict:oci_${lockedOci.status}:run_aops-cli_doctor`);
|
|
1859
|
+
}
|
|
1860
|
+
await runtime.stopNativeCockpit({ ...installSelection(resetOptions), signal });
|
|
1861
|
+
await stopCommunityNativeInstall({ ...installSelection(resetOptions), signal });
|
|
1862
|
+
throwIfCommunityCommandAborted(signal);
|
|
1863
|
+
const postgres = lockedNative.state.profile === 'native-container-postgres'
|
|
1864
|
+
? await runtime.removeNativeManagedPostgres({
|
|
1865
|
+
instanceName: instance,
|
|
1866
|
+
instanceRoot: expectedRoot,
|
|
1867
|
+
signal,
|
|
1868
|
+
})
|
|
1869
|
+
: null;
|
|
1870
|
+
throwIfCommunityCommandAborted(signal);
|
|
1871
|
+
removeInstanceContentsExceptLock(expectedRoot);
|
|
1872
|
+
return {
|
|
1873
|
+
status: 'community-install-reset',
|
|
1874
|
+
runtime: 'native',
|
|
1875
|
+
instance,
|
|
1876
|
+
instanceRoot: expectedRoot,
|
|
1877
|
+
rootPreserved: true,
|
|
1878
|
+
managedPostgresRemoved: postgres !== null,
|
|
1879
|
+
postgres,
|
|
1880
|
+
};
|
|
1881
|
+
});
|
|
1882
|
+
writeResult(output, options.json);
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
if (native.status !== 'not-installed') {
|
|
1886
|
+
throw new Error(`community_native_reset_refused:${native.status}:${native.error ?? 'run_doctor'}`);
|
|
1887
|
+
}
|
|
1888
|
+
const inspection = inspectFrom(resetOptions);
|
|
1889
|
+
const expectedRoot = pathsFrom(resetOptions).instanceRoot;
|
|
1890
|
+
if (path.resolve(expectedRoot) !== path.resolve(inspection.paths.instanceRoot))
|
|
1891
|
+
throw new Error('community_reset_path_mismatch');
|
|
1892
|
+
if (!tryLstat(expectedRoot)) {
|
|
1893
|
+
throwIfCommunityCommandAborted(signal);
|
|
1894
|
+
writeResult({ status: 'community-install-reset', instance, instanceRoot: expectedRoot, alreadyAbsent: true }, options.json);
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
assertInstanceDirectory(expectedRoot);
|
|
1898
|
+
assertOwnedInstancePaths(inspection.paths, 'allow-missing');
|
|
1899
|
+
const output = await runtime.withOperationLock({ instanceDirectory: expectedRoot, operation: 'reset', signal }, async ({ receipt }) => {
|
|
1900
|
+
const locked = inspectFrom(resetOptions);
|
|
1901
|
+
assertSameInstancePaths(inspection.paths, locked.paths);
|
|
1902
|
+
assertOwnedInstancePaths(locked.paths, 'allow-missing');
|
|
1903
|
+
assertCommunityRecoveryMutationFence(locked.paths);
|
|
1904
|
+
return runWithOperationJournal({
|
|
1905
|
+
paths: locked.paths,
|
|
1906
|
+
state: locked.status === 'installed' ? locked.state : null,
|
|
1907
|
+
operation: 'reset',
|
|
1908
|
+
receipt,
|
|
1909
|
+
signal,
|
|
1910
|
+
runtime,
|
|
1911
|
+
callback: async ({ journal, lifecycle }) => {
|
|
1912
|
+
if (locked.status === 'installed') {
|
|
1913
|
+
await lifecycle.stop({ paths: locked.paths, state: locked.state });
|
|
1914
|
+
}
|
|
1915
|
+
assertOwnedInstancePaths(locked.paths, 'allow-missing');
|
|
1916
|
+
await runCommunityJournaledPromotion({
|
|
1917
|
+
handle: journal,
|
|
1918
|
+
step: 'remove-instance-contents',
|
|
1919
|
+
signal,
|
|
1920
|
+
promote: () => removeInstanceContentsExceptLock(expectedRoot),
|
|
1921
|
+
});
|
|
1922
|
+
return { status: 'community-install-reset', instance, instanceRoot: expectedRoot, rootPreserved: true };
|
|
1923
|
+
},
|
|
1924
|
+
});
|
|
1925
|
+
});
|
|
1926
|
+
throwIfCommunityCommandAborted(signal);
|
|
1927
|
+
writeResult(output, options.json);
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
function common(command) {
|
|
1931
|
+
return command
|
|
1932
|
+
.option('--instance <name>', 'Installation instance name', 'default')
|
|
1933
|
+
.option('--data-root <path>', 'Absolute Community data root override')
|
|
1934
|
+
.option('--json', 'Output JSON');
|
|
1935
|
+
}
|
|
1936
|
+
function mergeCommunityCommandOptions(options, command) {
|
|
1937
|
+
return {
|
|
1938
|
+
...(typeof command.optsWithGlobals === 'function' ? command.optsWithGlobals() : {}),
|
|
1939
|
+
...options,
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
function releaseOptions(command) {
|
|
1943
|
+
return command
|
|
1944
|
+
.option('--repo <path>', 'Local tagged aops-community clone override; default is the exact published CLI version tag')
|
|
1945
|
+
.option('--release-dir <path>', 'Local complete signed release-bundle override')
|
|
1946
|
+
.option('--release-descriptor <path>', 'Offline release.json path; verifies only adjacent signature and Compose files without fetching release metadata')
|
|
1947
|
+
.option('--certificate-identity <identity>', 'Trusted GitHub Actions certificate identity')
|
|
1948
|
+
.option('--certificate-oidc-issuer <url>', 'Trusted certificate OIDC issuer');
|
|
1949
|
+
}
|
|
1950
|
+
export function makeCommunityServerCommand(identity = {}) {
|
|
1951
|
+
const dependencies = { cliVersion: identity.cliVersion };
|
|
1952
|
+
const command = new Command('server').description('Configure and operate a named AOPS Community server instance');
|
|
1953
|
+
common(releaseOptions(command.command('setup').description('Configure and start an npm-package, source-checkout, or OCI server profile')))
|
|
1954
|
+
.requiredOption('--runtime <native|oci>', 'Application runtime; no implicit default')
|
|
1955
|
+
.option('--postgres <external|container>', 'PostgreSQL owner for --runtime native')
|
|
1956
|
+
.option('--postgres-config <path>', 'Explicit external PostgreSQL env-file override; default: ~/.aops/aops.server.env (or AOPS_CLI_CONFIG_PATH directory)')
|
|
1957
|
+
.option('--postgres-tls <disable|require|verify-full>', 'Explicit external PostgreSQL TLS policy')
|
|
1958
|
+
.option('--auth <trusted-local>', 'Server authentication provider; Community is trusted-local only')
|
|
1959
|
+
.option('--exposure <loopback|container>', 'Server exposure; native Community remains loopback-only')
|
|
1960
|
+
.option('--public-port <number>', 'Host-side public port used by container origin checks; defaults to --port')
|
|
1961
|
+
.option('--source-root <path>', 'Optional public aops-community checkout root; defaults to the installed @aopslabs/aops-server package')
|
|
1962
|
+
.option('--port <number>', 'Host port', '5900')
|
|
1963
|
+
.option('--foreground', 'Keep the native server attached to this terminal')
|
|
1964
|
+
.option('--detach', 'Start the native server in the background (default)')
|
|
1965
|
+
.option('--preview', 'Validate and print the setup contract without mutation')
|
|
1966
|
+
.option('--apply', 'Apply the selected setup contract')
|
|
1967
|
+
.action((options) => runCommunityServerSetup(options, dependencies));
|
|
1968
|
+
common(command.command('start').alias('up').description('Start the installed native npm/source host or OCI stack'))
|
|
1969
|
+
.option('--foreground', 'Keep the native server attached to this terminal')
|
|
1970
|
+
.option('--detach', 'Start the native server in the background (default)')
|
|
1971
|
+
.action((options) => runCommunityServerStart(options, dependencies));
|
|
1972
|
+
common(command.command('stop').alias('down').description('Stop the server without deleting data'))
|
|
1973
|
+
.action((options) => runCommunityServerStop(options, dependencies));
|
|
1974
|
+
common(command.command('restart').description('Restart the installed server'))
|
|
1975
|
+
.option('--foreground', 'Keep the restarted native server attached to this terminal')
|
|
1976
|
+
.option('--detach', 'Restart the native server in the background (default)')
|
|
1977
|
+
.action((options) => runCommunityServerRestart(options, dependencies));
|
|
1978
|
+
common(command.command('status').description('Show install and runtime status')).action(runCommunityServerStatus);
|
|
1979
|
+
common(command.command('health').description('Check the installed server health without mutation')).action(runCommunityServerHealth);
|
|
1980
|
+
common(command.command('migration-plan').description('Read the exact pending native database migration plan without mutation'))
|
|
1981
|
+
.action((options) => runCommunityServerMigrationPlan(options, dependencies));
|
|
1982
|
+
common(command.command('attest-external-snapshot').description('Optionally record an operator-owned external PostgreSQL snapshot for one risky migration plan'))
|
|
1983
|
+
.requiredOption('--expected-plan-sha256 <digest>', 'Exact raw plan SHA-256 returned by server migration-plan')
|
|
1984
|
+
.requiredOption('--provider <name>', 'External snapshot provider or platform')
|
|
1985
|
+
.requiredOption('--snapshot-ref <reference>', 'Immutable external snapshot reference')
|
|
1986
|
+
.option('--snapshot-digest <sha256:digest>', 'Optional immutable snapshot content digest')
|
|
1987
|
+
.requiredOption('--attested-by <identity>', 'Operator identity accepting external recovery ownership')
|
|
1988
|
+
.requiredOption('--restore-instructions-ref <reference>', 'Durable runbook or restore-instructions reference')
|
|
1989
|
+
.option('--preview', 'Validate and print the exact attestation without writing it')
|
|
1990
|
+
.option('--apply', 'Write the immutable plan-bound attestation')
|
|
1991
|
+
.option('--confirm-external-recovery-owner', 'Confirm that restore execution remains externally owned')
|
|
1992
|
+
.action((options) => runCommunityServerAttestExternalSnapshot(options, dependencies));
|
|
1993
|
+
common(command.command('logs').description('Show recent server logs'))
|
|
1994
|
+
.option('--tail <number>', 'Number of log lines', '100')
|
|
1995
|
+
.action(runCommunityServerLogs);
|
|
1996
|
+
common(releaseOptions(command.command('update').description('Update an installed native application or OCI stack')))
|
|
1997
|
+
.option('--source-root <path>', 'Optional Community checkout for a native update; defaults to the installed official npm runtime')
|
|
1998
|
+
.option('--foreground', 'Keep the updated native server attached to this terminal')
|
|
1999
|
+
.option('--detach', 'Start the updated native server in the background (default)')
|
|
2000
|
+
.action((options) => runCommunityServerUpdate(options, dependencies));
|
|
2001
|
+
common(command.command('recover').description('Recover one exact failed or unhealthy update from its verified backup'))
|
|
2002
|
+
.requiredOption('--update-id <id>', 'Exact update ID recorded by the failed or unhealthy update')
|
|
2003
|
+
.option('--retry-recovery-id <id>', 'Retry only the exact previously failed recovery attempt using a new volume')
|
|
2004
|
+
.option('--confirm-data-rewind', 'Confirm that recovery may rewind data to the pre-update backup')
|
|
2005
|
+
.action((options) => runCommunityServerRecover(options, dependencies));
|
|
2006
|
+
common(command.command('reconcile-recovery').description('Explicitly finish or abandon one exact hard-killed recovery attempt'))
|
|
2007
|
+
.requiredOption('--update-id <id>', 'Exact update ID from the recovery ledger')
|
|
2008
|
+
.requiredOption('--recovery-id <id>', 'Exact interrupted recovery attempt ID')
|
|
2009
|
+
.requiredOption('--reconciliation-action <action>', 'complete-commit, abandon-interrupted, or restore-source-runtime')
|
|
2010
|
+
.option('--confirm-recovery-reconciliation', 'Confirm this exact fail-closed reconciliation')
|
|
2011
|
+
.option('--confirm-data-rewind', 'Confirm switching back to the exact recorded source data volume')
|
|
2012
|
+
.action((options) => runCommunityServerReconcileRecovery(options, dependencies));
|
|
2013
|
+
const rollback = common(command.command('rollback').description('Rollback application code; legacy flat form remains OCI-only until P4'))
|
|
2014
|
+
.option('--confirm-data-rewind', 'Confirm that data will be rewound to the pre-update backup')
|
|
2015
|
+
.action((options) => runCommunityServerRollback(options, dependencies));
|
|
2016
|
+
common(rollback.command('app').description('Rollback only native application code; never restore or rewind PostgreSQL'))
|
|
2017
|
+
.requiredOption('--update-id <id>', 'Exact native application update ID')
|
|
2018
|
+
.requiredOption('--source-root <path>', 'Exact prior public release checkout; the CLI never downloads or changes it')
|
|
2019
|
+
.option('--foreground', 'Keep the rolled-back native server attached to this terminal')
|
|
2020
|
+
.option('--detach', 'Start the rolled-back native server in the background (default)')
|
|
2021
|
+
.action((options, child) => runCommunityServerRollbackApplication(mergeCommunityCommandOptions(options, child), dependencies));
|
|
2022
|
+
common(command.command('backup').description('Create and verify a custom-format PostgreSQL backup plus receipt'))
|
|
2023
|
+
.action((options) => runCommunityServerBackup(options, dependencies));
|
|
2024
|
+
const restore = common(command.command('restore').description('Restore database data; legacy flat form remains OCI-only until P4'))
|
|
2025
|
+
.option('--backup <path>', 'Backup dump path; its JSON receipt must exist beside it')
|
|
2026
|
+
.option('--confirm-data-rewind', 'Confirm that data will be rewound to the selected backup')
|
|
2027
|
+
.action((options) => runCommunityServerRestore(options, dependencies));
|
|
2028
|
+
common(restore.command('db').description('Restore only PostgreSQL for one exact native application update'))
|
|
2029
|
+
.requiredOption('--update-id <id>', 'Exact native application update ID whose snapshot evidence will be used')
|
|
2030
|
+
.requiredOption('--source-root <path>', 'Exact prior public release checkout used to verify the restored database')
|
|
2031
|
+
.option('--confirm-data-rewind', 'Confirm that PostgreSQL data may rewind to the exact pre-update snapshot')
|
|
2032
|
+
.option('--confirm-external-restore-complete', 'Confirm an externally owned restore is complete and request strict verification')
|
|
2033
|
+
.action((options, child) => runCommunityServerRestoreDatabase(mergeCommunityCommandOptions(options, child), dependencies));
|
|
2034
|
+
common(command.command('operation-status').description('Inspect one exact durable operation journal without changing it'))
|
|
2035
|
+
.requiredOption('--operation-id <id>', 'Exact operation journal ID')
|
|
2036
|
+
.action((options) => runCommunityServerOperationStatus(options));
|
|
2037
|
+
common(command.command('reconcile-operation').description('Reconcile one exact interrupted operation under its instance lock'))
|
|
2038
|
+
.requiredOption('--operation-id <id>', 'Exact operation journal ID from server operation-status')
|
|
2039
|
+
.requiredOption('--expected-operation <operation>', 'Exact operation recorded by server operation-status')
|
|
2040
|
+
.requiredOption('--reconciliation-action <action>', 'One exact action permitted by server operation-status')
|
|
2041
|
+
.option('--confirm-operation-reconciliation', 'Confirm reconciliation of this exact operation generation')
|
|
2042
|
+
.option('--confirm-data-rewind', 'Confirm switching back to the exact recorded source data volume')
|
|
2043
|
+
.option('--confirm-data-loss', 'Confirm completion of an interrupted reset')
|
|
2044
|
+
.option('--confirm-instance <name>', 'Repeat the exact instance name for reset reconciliation')
|
|
2045
|
+
.action((options) => runCommunityServerReconcileOperation(options, dependencies));
|
|
2046
|
+
common(command.command('lock-status').description('Inspect the exact same-instance operation lock without changing it'))
|
|
2047
|
+
.action((options) => runCommunityServerLockStatus(options, dependencies));
|
|
2048
|
+
common(command.command('recover-lock').description('Recover one exact stale lock generation proven dead or PID-reused'))
|
|
2049
|
+
.requiredOption('--expected-lock-pid <pid>', 'Exact PID from server lock-status')
|
|
2050
|
+
.requiredOption('--expected-lock-operation <operation>', 'Exact operation from server lock-status')
|
|
2051
|
+
.requiredOption('--expected-lock-started-at <iso>', 'Exact startedAt from server lock-status')
|
|
2052
|
+
.requiredOption('--expected-lock-owner-sha256 <digest>', 'Exact ownerTokenSha256 from server lock-status')
|
|
2053
|
+
.requiredOption('--expected-process-start-identity <digest>', 'Exact processStartIdentity from server lock-status')
|
|
2054
|
+
.option('--confirm-stale-lock-recovery', 'Confirm deletion of only this exact proven-stale generation')
|
|
2055
|
+
.action((options) => runCommunityServerRecoverLock(options, dependencies));
|
|
2056
|
+
common(command.command('reset').description('Remove local installation state; managed PostgreSQL is deleted only when explicitly requested'))
|
|
2057
|
+
.requiredOption('--confirm-instance <name>', 'Repeat the instance name')
|
|
2058
|
+
.option('--confirm-data-loss', 'Confirm removal of the installation state and its active data pointer')
|
|
2059
|
+
.option('--remove-managed-postgres', 'Also delete the exact ownership-label-verified managed PostgreSQL container and volume')
|
|
2060
|
+
.action((options) => runCommunityServerReset(options, dependencies));
|
|
2061
|
+
return command;
|
|
2062
|
+
}
|