agl 1.0.17 → 21.0.2
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/.smartconfig.json +55 -0
- package/changelog.md +1026 -0
- package/cli.js +14 -0
- package/dist_serve/bundle.js +44778 -0
- package/dist_serve/index.html +13 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.authmodels.d.ts +126 -0
- package/dist_ts/classes.authmodels.js +1422 -0
- package/dist_ts/classes.authstore.d.ts +160 -0
- package/dist_ts/classes.authstore.js +2483 -0
- package/dist_ts/classes.browserresourcehost.d.ts +85 -0
- package/dist_ts/classes.browserresourcehost.js +912 -0
- package/dist_ts/classes.cli.d.ts +21 -0
- package/dist_ts/classes.cli.js +1082 -0
- package/dist_ts/classes.config.d.ts +23 -0
- package/dist_ts/classes.config.js +267 -0
- package/dist_ts/classes.controller.d.ts +523 -0
- package/dist_ts/classes.controller.js +10611 -0
- package/dist_ts/classes.controllermanagement.d.ts +32 -0
- package/dist_ts/classes.controllermanagement.js +222 -0
- package/dist_ts/classes.draftmanager.d.ts +30 -0
- package/dist_ts/classes.draftmanager.js +179 -0
- package/dist_ts/classes.embeddeddb.d.ts +30 -0
- package/dist_ts/classes.embeddeddb.js +159 -0
- package/dist_ts/classes.flexframedtransport.d.ts +56 -0
- package/dist_ts/classes.flexframedtransport.js +275 -0
- package/dist_ts/classes.flexmodels.private.d.ts +231 -0
- package/dist_ts/classes.flexmodels.private.js +985 -0
- package/dist_ts/classes.flexmodels.public.d.ts +207 -0
- package/dist_ts/classes.flexmodels.public.js +885 -0
- package/dist_ts/classes.flexprojectionreader.d.ts +89 -0
- package/dist_ts/classes.flexprojectionreader.js +580 -0
- package/dist_ts/classes.flexservice.d.ts +196 -0
- package/dist_ts/classes.flexservice.js +3704 -0
- package/dist_ts/classes.flexstore.d.ts +174 -0
- package/dist_ts/classes.flexstore.js +3400 -0
- package/dist_ts/classes.flexsupervisor.d.ts +105 -0
- package/dist_ts/classes.flexsupervisor.js +1191 -0
- package/dist_ts/classes.gitreversion.d.ts +431 -0
- package/dist_ts/classes.gitreversion.js +10592 -0
- package/dist_ts/classes.harnessoperationmanager.d.ts +113 -0
- package/dist_ts/classes.harnessoperationmanager.js +602 -0
- package/dist_ts/classes.opencodeclient.d.ts +146 -0
- package/dist_ts/classes.opencodeclient.js +3003 -0
- package/dist_ts/classes.opencodesupervisor.d.ts +60 -0
- package/dist_ts/classes.opencodesupervisor.js +1003 -0
- package/dist_ts/classes.passkeymanager.d.ts +34 -0
- package/dist_ts/classes.passkeymanager.js +456 -0
- package/dist_ts/classes.processinspection.d.ts +50 -0
- package/dist_ts/classes.processinspection.js +484 -0
- package/dist_ts/classes.resourcecoordinator.d.ts +64 -0
- package/dist_ts/classes.resourcecoordinator.js +294 -0
- package/dist_ts/classes.sessionintelligencereader.d.ts +13 -0
- package/dist_ts/classes.sessionintelligencereader.js +40 -0
- package/dist_ts/classes.systemmetricssampler.d.ts +53 -0
- package/dist_ts/classes.systemmetricssampler.js +168 -0
- package/dist_ts/classes.terminalmanager.d.ts +95 -0
- package/dist_ts/classes.terminalmanager.js +981 -0
- package/dist_ts/classes.terminalresourcehost.d.ts +20 -0
- package/dist_ts/classes.terminalresourcehost.js +84 -0
- package/dist_ts/classes.upgradecoordinator.d.ts +244 -0
- package/dist_ts/classes.upgradecoordinator.js +2155 -0
- package/dist_ts/classes.upgradetransaction.d.ts +92 -0
- package/dist_ts/classes.upgradetransaction.js +2125 -0
- package/dist_ts/classes.uploadmanager.d.ts +24 -0
- package/dist_ts/classes.uploadmanager.js +210 -0
- package/dist_ts/classes.validation.d.ts +91 -0
- package/dist_ts/classes.validation.js +1087 -0
- package/dist_ts/flexharness.child.d.ts +1 -0
- package/dist_ts/flexharness.child.js +28 -0
- package/dist_ts/functions.canonicaldirectory.d.ts +21 -0
- package/dist_ts/functions.canonicaldirectory.js +144 -0
- package/dist_ts/functions.controllerdataroot.d.ts +15 -0
- package/dist_ts/functions.controllerdataroot.js +76 -0
- package/dist_ts/functions.embeddeddb.d.ts +2 -0
- package/dist_ts/functions.embeddeddb.js +21 -0
- package/dist_ts/functions.flexcredentialstore.d.ts +16 -0
- package/dist_ts/functions.flexcredentialstore.js +93 -0
- package/dist_ts/functions.opencodeauth.d.ts +3 -0
- package/dist_ts/functions.opencodeauth.js +31 -0
- package/dist_ts/functions.runtimeenvironment.d.ts +2 -0
- package/dist_ts/functions.runtimeenvironment.js +79 -0
- package/dist_ts/functions.upgradefollower.d.ts +3 -0
- package/dist_ts/functions.upgradefollower.js +95 -0
- package/dist_ts/index.d.ts +24 -0
- package/dist_ts/index.js +25 -0
- package/dist_ts/interfaces.auth.d.ts +181 -0
- package/dist_ts/interfaces.auth.js +82 -0
- package/dist_ts/interfaces.config.d.ts +34 -0
- package/dist_ts/interfaces.config.js +2 -0
- package/dist_ts/interfaces.flexipc.d.ts +939 -0
- package/dist_ts/interfaces.flexipc.js +1492 -0
- package/dist_ts/interfaces.gitreversion.d.ts +246 -0
- package/dist_ts/interfaces.gitreversion.js +2 -0
- package/dist_ts/interfaces.lifecycle.d.ts +37 -0
- package/dist_ts/interfaces.lifecycle.js +2 -0
- package/dist_ts/interfaces.opencode.d.ts +101 -0
- package/dist_ts/interfaces.opencode.js +3 -0
- package/dist_ts/interfaces.projects.d.ts +156 -0
- package/dist_ts/interfaces.projects.js +5 -0
- package/dist_ts/plugins.d.ts +36 -0
- package/dist_ts/plugins.js +40 -0
- package/dist_ts_interfaces/index.d.ts +2 -0
- package/dist_ts_interfaces/index.js +3 -0
- package/dist_ts_interfaces/interfaces.d.ts +747 -0
- package/dist_ts_interfaces/interfaces.js +47 -0
- package/dist_ts_interfaces/plugins.d.ts +4 -0
- package/dist_ts_interfaces/plugins.js +3 -0
- package/dist_ts_interfaces/requests.d.ts +806 -0
- package/dist_ts_interfaces/requests.js +2 -0
- package/dist_ts_migration/classes.documentmigrationrunner.d.ts +8 -0
- package/dist_ts_migration/classes.documentmigrationrunner.js +24 -0
- package/dist_ts_migration/classes.flexmigrationrunner.d.ts +11 -0
- package/dist_ts_migration/classes.flexmigrationrunner.js +25 -0
- package/dist_ts_migration/classes.flexpostharnessmigrationrunner.d.ts +10 -0
- package/dist_ts_migration/classes.flexpostharnessmigrationrunner.js +15 -0
- package/dist_ts_migration/classes.migrationrunner.d.ts +8 -0
- package/dist_ts_migration/classes.migrationrunner.js +34 -0
- package/dist_ts_migration/index.d.ts +12 -0
- package/dist_ts_migration/index.js +13 -0
- package/dist_ts_migration/v10_flexharnessv3migration.d.ts +11 -0
- package/dist_ts_migration/v10_flexharnessv3migration.js +23 -0
- package/dist_ts_migration/v13_flexprojectionorder.d.ts +11 -0
- package/dist_ts_migration/v13_flexprojectionorder.js +23 -0
- package/dist_ts_migration/v16_flexprovidercredentials.d.ts +22 -0
- package/dist_ts_migration/v16_flexprovidercredentials.js +149 -0
- package/dist_ts_migration/v18_resourcelayout.d.ts +22 -0
- package/dist_ts_migration/v18_resourcelayout.js +23 -0
- package/dist_ts_migration/v18_resourcelayoutmigration.d.ts +6 -0
- package/dist_ts_migration/v18_resourcelayoutmigration.js +61 -0
- package/dist_ts_migration/v19_flexprojectmanagement.d.ts +11 -0
- package/dist_ts_migration/v19_flexprojectmanagement.js +21 -0
- package/dist_ts_migration/v1_embeddeddatabaselocation.d.ts +28 -0
- package/dist_ts_migration/v1_embeddeddatabaselocation.js +415 -0
- package/dist_ts_migration/v22_projectfilesystemidentitymigration.d.ts +7 -0
- package/dist_ts_migration/v22_projectfilesystemidentitymigration.js +105 -0
- package/dist_ts_migration/v2_controllerdataroot.d.ts +130 -0
- package/dist_ts_migration/v2_controllerdataroot.js +1887 -0
- package/dist_ts_migration/v6_protocoldocumentmigration.d.ts +7 -0
- package/dist_ts_migration/v6_protocoldocumentmigration.js +100 -0
- package/dist_ts_migration/v6_protocoldocuments.d.ts +7 -0
- package/dist_ts_migration/v6_protocoldocuments.js +138 -0
- package/html/index.html +13 -0
- package/{LICENSE → license.md} +2 -2
- package/package.json +86 -48
- package/readme.md +480 -0
- package/readme.plan.md +188 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.authmodels.ts +1245 -0
- package/ts/classes.authstore.ts +3274 -0
- package/ts/classes.browserresourcehost.ts +1205 -0
- package/ts/classes.cli.ts +1254 -0
- package/ts/classes.config.ts +351 -0
- package/ts/classes.controller.ts +14426 -0
- package/ts/classes.controllermanagement.ts +330 -0
- package/ts/classes.draftmanager.ts +232 -0
- package/ts/classes.embeddeddb.ts +166 -0
- package/ts/classes.flexframedtransport.ts +283 -0
- package/ts/classes.flexmodels.private.ts +823 -0
- package/ts/classes.flexmodels.public.ts +732 -0
- package/ts/classes.flexprojectionreader.ts +816 -0
- package/ts/classes.flexservice.ts +4302 -0
- package/ts/classes.flexstore.ts +4565 -0
- package/ts/classes.flexsupervisor.ts +1453 -0
- package/ts/classes.gitreversion.ts +13353 -0
- package/ts/classes.harnessoperationmanager.ts +870 -0
- package/ts/classes.opencodeclient.ts +4110 -0
- package/ts/classes.opencodesupervisor.ts +1280 -0
- package/ts/classes.passkeymanager.ts +570 -0
- package/ts/classes.processinspection.ts +623 -0
- package/ts/classes.resourcecoordinator.ts +436 -0
- package/ts/classes.sessionintelligencereader.ts +52 -0
- package/ts/classes.systemmetricssampler.ts +215 -0
- package/ts/classes.terminalmanager.ts +1207 -0
- package/ts/classes.terminalresourcehost.ts +122 -0
- package/ts/classes.upgradecoordinator.ts +2675 -0
- package/ts/classes.upgradetransaction.ts +2900 -0
- package/ts/classes.uploadmanager.ts +240 -0
- package/ts/classes.validation.ts +1409 -0
- package/ts/flexharness.child.ts +35 -0
- package/ts/functions.canonicaldirectory.ts +213 -0
- package/ts/functions.controllerdataroot.ts +107 -0
- package/ts/functions.embeddeddb.ts +22 -0
- package/ts/functions.flexcredentialstore.ts +128 -0
- package/ts/functions.opencodeauth.ts +34 -0
- package/ts/functions.runtimeenvironment.ts +90 -0
- package/ts/functions.upgradefollower.ts +115 -0
- package/ts/index.ts +36 -6
- package/ts/interfaces.auth.ts +316 -0
- package/ts/interfaces.config.ts +37 -0
- package/ts/interfaces.flexipc.ts +2586 -0
- package/ts/interfaces.gitreversion.ts +336 -0
- package/ts/interfaces.lifecycle.ts +46 -0
- package/ts/interfaces.opencode.ts +153 -0
- package/ts/interfaces.projects.ts +182 -0
- package/ts/plugins.ts +64 -0
- package/ts_interfaces/index.ts +2 -0
- package/ts_interfaces/interfaces.ts +827 -0
- package/ts_interfaces/plugins.ts +15 -0
- package/ts_interfaces/requests.ts +1186 -0
- package/ts_migration/classes.documentmigrationrunner.ts +32 -0
- package/ts_migration/classes.flexmigrationrunner.ts +33 -0
- package/ts_migration/classes.flexpostharnessmigrationrunner.ts +22 -0
- package/ts_migration/classes.migrationrunner.ts +40 -0
- package/ts_migration/index.ts +12 -0
- package/ts_migration/v10_flexharnessv3migration.ts +35 -0
- package/ts_migration/v13_flexprojectionorder.ts +33 -0
- package/ts_migration/v16_flexprovidercredentials.ts +196 -0
- package/ts_migration/v18_resourcelayout.ts +55 -0
- package/ts_migration/v18_resourcelayoutmigration.ts +68 -0
- package/ts_migration/v19_flexprojectmanagement.ts +28 -0
- package/ts_migration/v1_embeddeddatabaselocation.ts +560 -0
- package/ts_migration/v22_projectfilesystemidentitymigration.ts +121 -0
- package/ts_migration/v2_controllerdataroot.ts +2349 -0
- package/ts_migration/v6_protocoldocumentmigration.ts +144 -0
- package/ts_migration/v6_protocoldocuments.ts +198 -0
- package/ts_web/00_commitinfo_data.ts +8 -0
- package/ts_web/classes.controllersocketclient.ts +517 -0
- package/ts_web/classes.sessiondraftsync.ts +390 -0
- package/ts_web/elements.harnesscontrollerapp.ts +11494 -0
- package/ts_web/index.ts +2 -0
- package/ts_web/plugins.ts +31 -0
- package/.gitlab-ci.yml +0 -71
- package/.npmignore +0 -5
- package/README.md +0 -40
- package/dist/cli.js +0 -2
- package/dist/index.d.ts +0 -0
- package/dist/index.js +0 -9
- package/dist/mod_bundle/index.d.ts +0 -1
- package/dist/mod_bundle/index.js +0 -18
- package/dist/mod_bundle/mod.plugins.d.ts +0 -4
- package/dist/mod_bundle/mod.plugins.js +0 -11
- package/dist/mod_install/index.d.ts +0 -4
- package/dist/mod_install/index.js +0 -21
- package/dist/mod_install/mod.plugins.d.ts +0 -1
- package/dist/mod_install/mod.plugins.js +0 -7
- package/dist/mod_standard/index.d.ts +0 -4
- package/dist/mod_standard/index.js +0 -21
- package/dist/mod_standard/mod.browser.d.ts +0 -6
- package/dist/mod_standard/mod.browser.js +0 -14
- package/dist/mod_standard/mod.clean.d.ts +0 -4
- package/dist/mod_standard/mod.clean.js +0 -12
- package/dist/mod_standard/mod.compile.d.ts +0 -4
- package/dist/mod_standard/mod.compile.js +0 -21
- package/dist/mod_standard/mod.plugins.d.ts +0 -8
- package/dist/mod_standard/mod.plugins.js +0 -19
- package/dist/mod_standard/mod.test.d.ts +0 -4
- package/dist/mod_standard/mod.test.js +0 -12
- package/dist/plr.cli.d.ts +0 -3
- package/dist/plr.cli.js +0 -57
- package/dist/plr.config.d.ts +0 -11
- package/dist/plr.config.js +0 -26
- package/dist/plr.mods.d.ts +0 -17
- package/dist/plr.mods.js +0 -17
- package/dist/plr.paths.d.ts +0 -19
- package/dist/plr.paths.js +0 -26
- package/dist/plr.plugins.d.ts +0 -8
- package/dist/plr.plugins.js +0 -16
- package/npmextra.json +0 -10
- package/ts/mod_bundle/index.ts +0 -20
- package/ts/mod_bundle/mod.plugins.ts +0 -9
- package/ts/mod_install/index.ts +0 -20
- package/ts/mod_install/mod.plugins.ts +0 -1
- package/ts/mod_standard/index.ts +0 -23
- package/ts/mod_standard/mod.browser.ts +0 -17
- package/ts/mod_standard/mod.clean.ts +0 -13
- package/ts/mod_standard/mod.compile.ts +0 -23
- package/ts/mod_standard/mod.plugins.ts +0 -17
- package/ts/mod_standard/mod.test.ts +0 -13
- package/ts/plr.cli.ts +0 -60
- package/ts/plr.config.ts +0 -36
- package/ts/plr.mods.ts +0 -25
- package/ts/plr.paths.ts +0 -29
- package/ts/plr.plugins.ts +0 -17
- package/tslint.json +0 -3
- package/yarn.lock +0 -3464
|
@@ -0,0 +1,1254 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { commitinfo } from './00_commitinfo_data.js';
|
|
3
|
+
import {
|
|
4
|
+
controllerPackageName,
|
|
5
|
+
controllerProtocolVersion,
|
|
6
|
+
controllerUpgradeManagementVersion,
|
|
7
|
+
type IControllerStatus,
|
|
8
|
+
} from '../ts_interfaces/index.js';
|
|
9
|
+
import {
|
|
10
|
+
controllerDataDirectory,
|
|
11
|
+
defaultControllerPort,
|
|
12
|
+
} from './classes.config.js';
|
|
13
|
+
import { ensureControllerDataRoot } from './functions.controllerdataroot.js';
|
|
14
|
+
import type { IControllerRuntimeOverrides } from './interfaces.config.js';
|
|
15
|
+
import type {
|
|
16
|
+
IControllerReadyIpcMessage,
|
|
17
|
+
IControllerStartIpcMessage,
|
|
18
|
+
IControllerStartResult,
|
|
19
|
+
TControllerIpcMessage,
|
|
20
|
+
} from './interfaces.lifecycle.js';
|
|
21
|
+
import { assertProvidedSetupCode, SmartDataAuthStore } from './classes.authstore.js';
|
|
22
|
+
import { OpenCodeController } from './classes.controller.js';
|
|
23
|
+
import {
|
|
24
|
+
assertControllerStatus,
|
|
25
|
+
isLoopbackPortListening,
|
|
26
|
+
queryControllerStatus as queryManagedControllerStatus,
|
|
27
|
+
requestControllerUpgradeLaunch,
|
|
28
|
+
stopVerifiedController,
|
|
29
|
+
tryQueryControllerStatus as tryQueryManagedControllerStatus,
|
|
30
|
+
waitUntilControllerExited,
|
|
31
|
+
} from './classes.controllermanagement.js';
|
|
32
|
+
import {
|
|
33
|
+
inspectControllerProcess,
|
|
34
|
+
listControllerProcessesForCli,
|
|
35
|
+
signalVerifiedControllerProcess,
|
|
36
|
+
} from './classes.processinspection.js';
|
|
37
|
+
import {
|
|
38
|
+
createUpgradeToken,
|
|
39
|
+
parseUpgradeWorkerPayload,
|
|
40
|
+
UpgradeCoordinator,
|
|
41
|
+
upgradePackageTransitionSource,
|
|
42
|
+
upgradePackageTransitionTarget,
|
|
43
|
+
type UpgradeStartLease,
|
|
44
|
+
upgradeTokenEnvironmentVariable,
|
|
45
|
+
} from './classes.upgradecoordinator.js';
|
|
46
|
+
import {
|
|
47
|
+
createUpgradeWorkerPayload,
|
|
48
|
+
launchDetachedUpgradeWorker,
|
|
49
|
+
resolveCurrentCliPath,
|
|
50
|
+
resolveCurrentPnpmGlobalInstallation,
|
|
51
|
+
runUpgradeWorker,
|
|
52
|
+
tryResolveCurrentPnpmGlobalInstallation,
|
|
53
|
+
} from './classes.upgradetransaction.js';
|
|
54
|
+
import { followUpgradeTransaction } from './functions.upgradefollower.js';
|
|
55
|
+
|
|
56
|
+
type TParsedArguments = Record<string, unknown> & { _: Array<string | number> };
|
|
57
|
+
|
|
58
|
+
const currentCliName = commitinfo.name === upgradePackageTransitionSource.packageName
|
|
59
|
+
? upgradePackageTransitionSource.cliName
|
|
60
|
+
: upgradePackageTransitionTarget.cliName;
|
|
61
|
+
const currentRuntimeName = currentCliName === upgradePackageTransitionTarget.cliName
|
|
62
|
+
? 'AGL'
|
|
63
|
+
: currentCliName;
|
|
64
|
+
|
|
65
|
+
const helpText = `
|
|
66
|
+
${currentRuntimeName === 'AGL' ? 'AGL - Agent Gateway Layer' : currentRuntimeName}
|
|
67
|
+
|
|
68
|
+
Usage:
|
|
69
|
+
${currentCliName} start [options]
|
|
70
|
+
${currentCliName} status [--port 4097] [--json]
|
|
71
|
+
${currentCliName} stop [--port 4097]
|
|
72
|
+
${currentCliName} upgrade [--port 4097] [--grace-period-seconds 300] [--continue-sessions] [--json]
|
|
73
|
+
${currentCliName} foreground [options]
|
|
74
|
+
${currentCliName} temp-password [--port 4097] [--ttl-hours 24]
|
|
75
|
+
|
|
76
|
+
Commands:
|
|
77
|
+
start Start ${currentRuntimeName} as a detached process (Linux and macOS)
|
|
78
|
+
status Read live ${currentRuntimeName} and harness status over loopback
|
|
79
|
+
stop Verify and stop the live ${currentRuntimeName} process
|
|
80
|
+
upgrade Upgrade the pnpm-global package and preserve running state
|
|
81
|
+
foreground Run ${currentRuntimeName} attached to this terminal
|
|
82
|
+
temp-password Mint a temporary browser login password (max 24 hours)
|
|
83
|
+
help Show this help
|
|
84
|
+
|
|
85
|
+
First-start options:
|
|
86
|
+
--port <port> Controller port (default: 4097)
|
|
87
|
+
--opencode-port <port> Local-only OpenCode port (default: 4098)
|
|
88
|
+
--directory <path> Fixed OpenCode workspace (default: current directory)
|
|
89
|
+
--projects-root <path> Default base for relative project paths and suggestions
|
|
90
|
+
(default: workspace; explicitly typed absolute projects may
|
|
91
|
+
be outside it; adopted once, then immutable)
|
|
92
|
+
--public-origin <origin> Browser origin used for WebAuthn
|
|
93
|
+
--rp-id <hostname> WebAuthn relying-party ID
|
|
94
|
+
--behind-tls-proxy Trust an explicitly configured TLS reverse proxy
|
|
95
|
+
--setup-code <code> Operator-chosen setup code (4-256 printable ASCII
|
|
96
|
+
characters; short codes weaken brute-force resistance)
|
|
97
|
+
|
|
98
|
+
Upgrade options:
|
|
99
|
+
--grace-period-seconds <n> Maximum graceful pause time (default: 300)
|
|
100
|
+
--continue-sessions Continue sessions paused for the upgrade
|
|
101
|
+
|
|
102
|
+
Temp password options:
|
|
103
|
+
--ttl-hours <hours> Temporary password lifetime in hours (max 24, default 24)
|
|
104
|
+
|
|
105
|
+
Upgrade requirements:
|
|
106
|
+
The command requires the active pnpm-global installation and currently runs
|
|
107
|
+
on Linux and macOS. Normal progress mode prints the detached worker log before
|
|
108
|
+
stopping anything; --json emits it in the terminal document.
|
|
109
|
+
|
|
110
|
+
${currentRuntimeName} stores its data in an embedded database engine by default
|
|
111
|
+
(under $XDG_CONFIG_HOME/hcon/smartdb, default
|
|
112
|
+
~/.config/hcon/smartdb; repository checkouts use ./.nogit/smartdb;
|
|
113
|
+
override the directory
|
|
114
|
+
with HARNESS_CONTROLLER_DB_DIR). Set HARNESS_CONTROLLER_MONGO_URL /
|
|
115
|
+
HARNESS_CONTROLLER_MONGO_DB to use an external MongoDB-compatible server
|
|
116
|
+
instead. temp-password writes to the controller database directly, so run
|
|
117
|
+
it with the same database environment as the running controller; in
|
|
118
|
+
embedded mode it attaches to the running controller's engine socket. The
|
|
119
|
+
password authenticates the browser UI without a passkey until it expires.
|
|
120
|
+
|
|
121
|
+
The first successful start prints a one-time setup code. Runtime configuration
|
|
122
|
+
is immutable after initialization; use the same --port for later management.
|
|
123
|
+
Detached starts append ${currentRuntimeName} errors to a log file under
|
|
124
|
+
$XDG_CONFIG_HOME/harness-controller (default ~/.config/harness-controller;
|
|
125
|
+
repository checkouts use ./.nogit). Upgrade workers write a separate private
|
|
126
|
+
log under that retained legacy root and print its path before work begins.
|
|
127
|
+
In --json mode the path is retained before work begins and emitted at completion.
|
|
128
|
+
`.trim();
|
|
129
|
+
|
|
130
|
+
const publicCliCommands = new Set([
|
|
131
|
+
'start',
|
|
132
|
+
'status',
|
|
133
|
+
'stop',
|
|
134
|
+
'upgrade',
|
|
135
|
+
'foreground',
|
|
136
|
+
'temp-password',
|
|
137
|
+
'help',
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
export const formatCliError = (errorArg: unknown): string => {
|
|
141
|
+
const messages: string[] = [];
|
|
142
|
+
const seen = new Set<unknown>();
|
|
143
|
+
const visit = (valueArg: unknown, depthArg: number): void => {
|
|
144
|
+
if (depthArg > 8 || seen.has(valueArg)) return;
|
|
145
|
+
if ((typeof valueArg === 'object' && valueArg !== null) || typeof valueArg === 'function') {
|
|
146
|
+
seen.add(valueArg);
|
|
147
|
+
}
|
|
148
|
+
if (valueArg instanceof Error) {
|
|
149
|
+
if (valueArg.message && !messages.includes(valueArg.message)) messages.push(valueArg.message);
|
|
150
|
+
if (valueArg instanceof AggregateError) {
|
|
151
|
+
for (const nestedError of valueArg.errors) visit(nestedError, depthArg + 1);
|
|
152
|
+
}
|
|
153
|
+
if (valueArg.cause !== undefined) visit(valueArg.cause, depthArg + 1);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const message = String(valueArg);
|
|
157
|
+
if (message && !messages.includes(message)) messages.push(message);
|
|
158
|
+
};
|
|
159
|
+
visit(errorArg, 0);
|
|
160
|
+
const formatted = messages.join(' Cause: ') || 'An unknown error occurred.';
|
|
161
|
+
return Buffer.byteLength(formatted, 'utf8') <= 8 * 1024
|
|
162
|
+
? formatted
|
|
163
|
+
: `${Buffer.from(formatted, 'utf8').subarray(0, (8 * 1024) - 16).toString('utf8')} [truncated]`;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const readOption = (
|
|
167
|
+
argvArg: TParsedArguments,
|
|
168
|
+
keyArg: string,
|
|
169
|
+
alternateKeyArg?: string,
|
|
170
|
+
): unknown => argvArg[keyArg] ?? (alternateKeyArg ? argvArg[alternateKeyArg] : undefined);
|
|
171
|
+
|
|
172
|
+
const readStringOption = (
|
|
173
|
+
argvArg: TParsedArguments,
|
|
174
|
+
keyArg: string,
|
|
175
|
+
alternateKeyArg?: string,
|
|
176
|
+
): string | undefined => {
|
|
177
|
+
const value = readOption(argvArg, keyArg, alternateKeyArg);
|
|
178
|
+
if (value === undefined) return undefined;
|
|
179
|
+
if (typeof value !== 'string' || !value) throw new Error(`--${alternateKeyArg ?? keyArg} requires a value.`);
|
|
180
|
+
return value;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const readBooleanOption = (
|
|
184
|
+
argvArg: TParsedArguments,
|
|
185
|
+
keyArg: string,
|
|
186
|
+
alternateKeyArg?: string,
|
|
187
|
+
): boolean => {
|
|
188
|
+
const value = readOption(argvArg, keyArg, alternateKeyArg);
|
|
189
|
+
if (value === undefined) return false;
|
|
190
|
+
if (typeof value !== 'boolean') throw new Error(`--${alternateKeyArg ?? keyArg} must be a boolean flag.`);
|
|
191
|
+
return value;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// The parsed argv mangles numeric-looking values (12345 → number, 0123 → 123),
|
|
195
|
+
// so the setup code is read from the raw runtime arguments instead.
|
|
196
|
+
let activeRuntimeArguments: string[] = [];
|
|
197
|
+
|
|
198
|
+
const readProvidedSetupCode = (): string | undefined => {
|
|
199
|
+
for (let index = 0; index < activeRuntimeArguments.length; index++) {
|
|
200
|
+
const token = activeRuntimeArguments[index];
|
|
201
|
+
if (token === '--setup-code') {
|
|
202
|
+
const value = activeRuntimeArguments[index + 1];
|
|
203
|
+
if (value === undefined || value.startsWith('--')) {
|
|
204
|
+
throw new Error('--setup-code requires a value (use --setup-code=<code> for values starting with dashes).');
|
|
205
|
+
}
|
|
206
|
+
return assertProvidedSetupCode(value);
|
|
207
|
+
}
|
|
208
|
+
if (token.startsWith('--setup-code=')) {
|
|
209
|
+
return assertProvidedSetupCode(token.slice('--setup-code='.length));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const readNumberOption = (
|
|
216
|
+
argvArg: TParsedArguments,
|
|
217
|
+
keyArg: string,
|
|
218
|
+
alternateKeyArg: string,
|
|
219
|
+
fallbackArg: number | undefined,
|
|
220
|
+
minimumArg: number,
|
|
221
|
+
maximumArg: number,
|
|
222
|
+
): number | undefined => {
|
|
223
|
+
const value = readOption(argvArg, keyArg, alternateKeyArg);
|
|
224
|
+
if (value === undefined) return fallbackArg;
|
|
225
|
+
const normalized = typeof value === 'number' ? value : Number(value);
|
|
226
|
+
if (!Number.isSafeInteger(normalized) || normalized < minimumArg || normalized > maximumArg) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`--${alternateKeyArg} must be an integer between ${minimumArg} and ${maximumArg}.`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
return normalized;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
export const readUpgradeGracePeriodMs = (argvArg: TParsedArguments): number => (
|
|
235
|
+
readNumberOption(
|
|
236
|
+
argvArg,
|
|
237
|
+
'gracePeriodSeconds',
|
|
238
|
+
'grace-period-seconds',
|
|
239
|
+
300,
|
|
240
|
+
1,
|
|
241
|
+
3_600,
|
|
242
|
+
)! * 1_000
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const readPortOption = (
|
|
246
|
+
argvArg: TParsedArguments,
|
|
247
|
+
keyArg: string,
|
|
248
|
+
alternateKeyArg: string,
|
|
249
|
+
fallbackArg?: number,
|
|
250
|
+
): number | undefined => readNumberOption(
|
|
251
|
+
argvArg,
|
|
252
|
+
keyArg,
|
|
253
|
+
alternateKeyArg,
|
|
254
|
+
fallbackArg,
|
|
255
|
+
1,
|
|
256
|
+
65_535,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
export const parseRuntimeOverrides = (
|
|
260
|
+
argvArg: TParsedArguments,
|
|
261
|
+
): IControllerRuntimeOverrides => ({
|
|
262
|
+
controllerPort: readPortOption(argvArg, 'port', 'port', defaultControllerPort)!,
|
|
263
|
+
publicOrigin: readStringOption(argvArg, 'publicOrigin', 'public-origin'),
|
|
264
|
+
rpId: readStringOption(argvArg, 'rpId', 'rp-id'),
|
|
265
|
+
workspaceDirectory: readStringOption(argvArg, 'directory', 'directory'),
|
|
266
|
+
projectsRoot: readStringOption(argvArg, 'projectsRoot', 'projects-root'),
|
|
267
|
+
opencodePort: readPortOption(argvArg, 'opencodePort', 'opencode-port'),
|
|
268
|
+
behindTlsProxy: readOption(argvArg, 'behindTlsProxy', 'behind-tls-proxy') === undefined
|
|
269
|
+
? undefined
|
|
270
|
+
: readBooleanOption(argvArg, 'behindTlsProxy', 'behind-tls-proxy'),
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const currentStatusExpectation = {
|
|
274
|
+
packageName: controllerPackageName,
|
|
275
|
+
packageVersion: commitinfo.version,
|
|
276
|
+
protocolVersion: controllerProtocolVersion,
|
|
277
|
+
upgradeManagementVersion: controllerUpgradeManagementVersion,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const assertStatus = (statusArg: unknown): IControllerStatus => assertControllerStatus(
|
|
281
|
+
statusArg,
|
|
282
|
+
currentStatusExpectation,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
export const queryControllerStatus = async (
|
|
286
|
+
portArg: number,
|
|
287
|
+
timeoutMsArg = 2_500,
|
|
288
|
+
): Promise<IControllerStatus> => queryManagedControllerStatus(
|
|
289
|
+
portArg,
|
|
290
|
+
currentStatusExpectation,
|
|
291
|
+
timeoutMsArg,
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const tryQueryControllerStatus = async (portArg: number): Promise<IControllerStatus | undefined> => {
|
|
295
|
+
return tryQueryManagedControllerStatus(portArg, currentStatusExpectation);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const statusJson = (statusArg: IControllerStatus): string => JSON.stringify(statusArg, null, 2);
|
|
299
|
+
|
|
300
|
+
export const formatControllerStatus = (statusArg: IControllerStatus): string => {
|
|
301
|
+
const formatHarness = (harnessIdArg: 'opencode' | 'flex', labelArg: string): string => {
|
|
302
|
+
const harness = statusArg.harnesses.find((entryArg) => entryArg.harnessId === harnessIdArg);
|
|
303
|
+
if (!harness) throw new Error(`Controller status is missing the ${labelArg} harness.`);
|
|
304
|
+
const pid = harness.pid ? ` (PID ${harness.pid})` : '';
|
|
305
|
+
return `${labelArg}: ${harness.state}${pid}`;
|
|
306
|
+
};
|
|
307
|
+
return [
|
|
308
|
+
`${currentRuntimeName}: ${statusArg.lifecycleState} (PID ${statusArg.controllerPid}, ${statusArg.processMode})`,
|
|
309
|
+
formatHarness('opencode', 'OpenCode'),
|
|
310
|
+
formatHarness('flex', 'Flex'),
|
|
311
|
+
`Setup: ${statusArg.setupRequired ? 'required' : 'complete'}`,
|
|
312
|
+
].join('\n');
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const printStatus = (statusArg: IControllerStatus): void => {
|
|
316
|
+
process.stdout.write(formatControllerStatus(statusArg) + '\n');
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const printStartResult = (
|
|
320
|
+
resultArg: IControllerStartResult,
|
|
321
|
+
jsonArg: boolean,
|
|
322
|
+
extrasArg: { logFilePath?: string } = {},
|
|
323
|
+
): void => {
|
|
324
|
+
if (jsonArg) {
|
|
325
|
+
process.stdout.write(JSON.stringify({ ...resultArg, ...extrasArg }, null, 2) + '\n');
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
process.stdout.write(`${currentRuntimeName} ready: ${resultArg.publicUrl}\n`);
|
|
329
|
+
printStatus(resultArg.status);
|
|
330
|
+
if (extrasArg.logFilePath) {
|
|
331
|
+
process.stdout.write(`Logs: ${extrasArg.logFilePath}\n`);
|
|
332
|
+
}
|
|
333
|
+
if (resultArg.setupCode) {
|
|
334
|
+
process.stdout.write(`Setup code: ${resultArg.setupCode}\n`);
|
|
335
|
+
process.stdout.write('Keep this code private; it expires after 30 minutes and is accepted only for passkey enrollment.\n');
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const detachedLogRotateBytes = 1024 * 1024;
|
|
340
|
+
|
|
341
|
+
class RetainedControllerStartupError extends Error {}
|
|
342
|
+
|
|
343
|
+
const resolveDetachedLogFilePath = (portArg: number): string => {
|
|
344
|
+
return plugins.path.join(controllerDataDirectory(), `controller-${portArg}.log`);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const openDetachedLogFile = async (
|
|
348
|
+
logFilePathArg: string,
|
|
349
|
+
): Promise<plugins.fs.promises.FileHandle> => {
|
|
350
|
+
await plugins.fs.promises.mkdir(
|
|
351
|
+
plugins.path.dirname(logFilePathArg),
|
|
352
|
+
{ recursive: true, mode: 0o700 },
|
|
353
|
+
);
|
|
354
|
+
try {
|
|
355
|
+
const existing = await plugins.fs.promises.lstat(logFilePathArg);
|
|
356
|
+
if (existing.isFile() && existing.size > detachedLogRotateBytes) {
|
|
357
|
+
await plugins.fs.promises.rename(logFilePathArg, `${logFilePathArg}.old`);
|
|
358
|
+
}
|
|
359
|
+
} catch (error) {
|
|
360
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
361
|
+
}
|
|
362
|
+
// O_NOFOLLOW plus the checks below keep a pre-planted symlink or foreign file
|
|
363
|
+
// from silently receiving controller diagnostics.
|
|
364
|
+
const handle = await plugins.fs.promises.open(
|
|
365
|
+
logFilePathArg,
|
|
366
|
+
plugins.fs.constants.O_WRONLY
|
|
367
|
+
| plugins.fs.constants.O_APPEND
|
|
368
|
+
| plugins.fs.constants.O_CREAT
|
|
369
|
+
| plugins.fs.constants.O_NOFOLLOW,
|
|
370
|
+
0o600,
|
|
371
|
+
);
|
|
372
|
+
try {
|
|
373
|
+
const stats = await handle.stat();
|
|
374
|
+
if (!stats.isFile()) {
|
|
375
|
+
throw new Error(`The controller log path is not a regular file: ${logFilePathArg}`);
|
|
376
|
+
}
|
|
377
|
+
if (typeof process.getuid === 'function' && stats.uid !== process.getuid()) {
|
|
378
|
+
throw new Error(`The controller log file is not owned by the current user: ${logFilePathArg}`);
|
|
379
|
+
}
|
|
380
|
+
if ((stats.mode & 0o077) !== 0) {
|
|
381
|
+
throw new Error(`The controller log file must not be group or world accessible: ${logFilePathArg}`);
|
|
382
|
+
}
|
|
383
|
+
} catch (error) {
|
|
384
|
+
await handle.close();
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
return handle;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const resolveCliPath = async (): Promise<string> => {
|
|
391
|
+
return await resolveCurrentCliPath();
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const acquireStartAdmission = async (
|
|
395
|
+
commandArg: 'start' | 'foreground' | 'temp-password',
|
|
396
|
+
): Promise<UpgradeStartLease | undefined> => {
|
|
397
|
+
if (process.platform === 'win32') return undefined;
|
|
398
|
+
const installation = await tryResolveCurrentPnpmGlobalInstallation();
|
|
399
|
+
if (!installation) return undefined;
|
|
400
|
+
const coordinator = new UpgradeCoordinator(installation.globalRoot);
|
|
401
|
+
return await coordinator.acquireStartLease({
|
|
402
|
+
token: createUpgradeToken(),
|
|
403
|
+
cliPath: installation.cliPath,
|
|
404
|
+
command: commandArg,
|
|
405
|
+
upgradeToken: process.env[upgradeTokenEnvironmentVariable],
|
|
406
|
+
});
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const serializeRuntimeOptions = (overridesArg: IControllerRuntimeOverrides): string[] => {
|
|
410
|
+
const args = ['--port', String(overridesArg.controllerPort)];
|
|
411
|
+
if (overridesArg.publicOrigin !== undefined) args.push('--public-origin', overridesArg.publicOrigin);
|
|
412
|
+
if (overridesArg.rpId !== undefined) args.push('--rp-id', overridesArg.rpId);
|
|
413
|
+
if (overridesArg.workspaceDirectory !== undefined) args.push('--directory', overridesArg.workspaceDirectory);
|
|
414
|
+
if (overridesArg.projectsRoot !== undefined) args.push('--projects-root', overridesArg.projectsRoot);
|
|
415
|
+
if (overridesArg.opencodePort !== undefined) args.push('--opencode-port', String(overridesArg.opencodePort));
|
|
416
|
+
if (overridesArg.behindTlsProxy === true) args.push('--behind-tls-proxy');
|
|
417
|
+
if (overridesArg.behindTlsProxy === false) args.push('--no-behind-tls-proxy');
|
|
418
|
+
return args;
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const stopJustSpawnedProcess = async (
|
|
422
|
+
childArg: plugins.childProcess.ChildProcess,
|
|
423
|
+
cliPathArg: string,
|
|
424
|
+
portArg: number,
|
|
425
|
+
fingerprintArg: string,
|
|
426
|
+
): Promise<void> => {
|
|
427
|
+
if (!childArg.pid) return;
|
|
428
|
+
const releaseParentIpc = (): void => {
|
|
429
|
+
if (childArg.connected) childArg.disconnect();
|
|
430
|
+
childArg.unref();
|
|
431
|
+
};
|
|
432
|
+
const processIdentity = {
|
|
433
|
+
pid: childArg.pid,
|
|
434
|
+
cliPath: cliPathArg,
|
|
435
|
+
port: portArg,
|
|
436
|
+
expectedProcessGroupId: childArg.pid,
|
|
437
|
+
expectedFingerprint: fingerprintArg,
|
|
438
|
+
command: '__serve' as const,
|
|
439
|
+
};
|
|
440
|
+
if (!await inspectControllerProcess(processIdentity)) {
|
|
441
|
+
releaseParentIpc();
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
await signalVerifiedControllerProcess({
|
|
446
|
+
...processIdentity,
|
|
447
|
+
signal: 'SIGTERM',
|
|
448
|
+
signalProcessGroup: true,
|
|
449
|
+
});
|
|
450
|
+
} catch (error) {
|
|
451
|
+
if (!await inspectControllerProcess(processIdentity)) return;
|
|
452
|
+
throw error;
|
|
453
|
+
} finally {
|
|
454
|
+
// Once cleanup has been requested, the parent must release the IPC channel.
|
|
455
|
+
// Otherwise both processes can remain alive solely waiting on each other.
|
|
456
|
+
releaseParentIpc();
|
|
457
|
+
}
|
|
458
|
+
if (await waitUntilControllerExited(processIdentity, 30_000)) return;
|
|
459
|
+
await signalVerifiedControllerProcess({
|
|
460
|
+
...processIdentity,
|
|
461
|
+
signal: 'SIGTERM',
|
|
462
|
+
signalProcessGroup: true,
|
|
463
|
+
});
|
|
464
|
+
if (!await waitUntilControllerExited(processIdentity, 15_000)) {
|
|
465
|
+
throw new Error(
|
|
466
|
+
'The detached controller did not complete cooperative cleanup; it remains available for an exact stop retry.',
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const sendIpcMessage = async (
|
|
472
|
+
childArg: plugins.childProcess.ChildProcess,
|
|
473
|
+
messageArg: TControllerIpcMessage,
|
|
474
|
+
): Promise<void> => {
|
|
475
|
+
if (!childArg.connected) throw new Error('Detached controller IPC channel is unavailable.');
|
|
476
|
+
await new Promise<void>((resolve, reject) => {
|
|
477
|
+
childArg.send(messageArg, (errorArg) => errorArg ? reject(errorArg) : resolve());
|
|
478
|
+
});
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const waitForSpawnedChildExit = async (
|
|
482
|
+
childArg: plugins.childProcess.ChildProcess,
|
|
483
|
+
timeoutMsArg: number,
|
|
484
|
+
): Promise<boolean> => {
|
|
485
|
+
if (childArg.exitCode !== null || childArg.signalCode !== null) return true;
|
|
486
|
+
return await new Promise<boolean>((resolve) => {
|
|
487
|
+
let settled = false;
|
|
488
|
+
const finish = (exitedArg: boolean) => {
|
|
489
|
+
if (settled) return;
|
|
490
|
+
settled = true;
|
|
491
|
+
clearTimeout(timeout);
|
|
492
|
+
childArg.removeListener('exit', onExit);
|
|
493
|
+
resolve(exitedArg);
|
|
494
|
+
};
|
|
495
|
+
const onExit = () => finish(true);
|
|
496
|
+
const timeout = setTimeout(() => finish(false), timeoutMsArg);
|
|
497
|
+
childArg.once('exit', onExit);
|
|
498
|
+
});
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const waitForStartGate = async (): Promise<IControllerStartIpcMessage> => {
|
|
502
|
+
if (!process.connected) {
|
|
503
|
+
throw new Error('The detached controller start gate requires parent IPC.');
|
|
504
|
+
}
|
|
505
|
+
return await new Promise<IControllerStartIpcMessage>((resolve, reject) => {
|
|
506
|
+
let settled = false;
|
|
507
|
+
const finish = (errorArg?: Error, messageArg?: IControllerStartIpcMessage) => {
|
|
508
|
+
if (settled) return;
|
|
509
|
+
settled = true;
|
|
510
|
+
clearTimeout(timeout);
|
|
511
|
+
process.removeListener('message', onMessage);
|
|
512
|
+
process.removeListener('disconnect', onDisconnect);
|
|
513
|
+
if (errorArg) reject(errorArg);
|
|
514
|
+
else resolve(messageArg!);
|
|
515
|
+
};
|
|
516
|
+
const onMessage = (messageArg: unknown) => {
|
|
517
|
+
const message = messageArg as Partial<TControllerIpcMessage>;
|
|
518
|
+
if (message?.type === 'start') {
|
|
519
|
+
finish(undefined, message as IControllerStartIpcMessage);
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
const onDisconnect = () => finish(new Error('Parent disconnected before authorizing startup.'));
|
|
523
|
+
const timeout = setTimeout(() => {
|
|
524
|
+
finish(new Error('Parent did not authorize detached startup within 5 seconds.'));
|
|
525
|
+
}, 5_000);
|
|
526
|
+
process.on('message', onMessage);
|
|
527
|
+
process.once('disconnect', onDisconnect);
|
|
528
|
+
});
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const waitForDetachedReady = async (
|
|
532
|
+
childArg: plugins.childProcess.ChildProcess,
|
|
533
|
+
): Promise<IControllerReadyIpcMessage> => {
|
|
534
|
+
return await new Promise<IControllerReadyIpcMessage>((resolve, reject) => {
|
|
535
|
+
let settled = false;
|
|
536
|
+
const finish = (errorArg?: Error, messageArg?: IControllerReadyIpcMessage) => {
|
|
537
|
+
if (settled) return;
|
|
538
|
+
settled = true;
|
|
539
|
+
clearTimeout(timeout);
|
|
540
|
+
childArg.removeListener('message', onMessage);
|
|
541
|
+
childArg.removeListener('error', onError);
|
|
542
|
+
childArg.removeListener('exit', onExit);
|
|
543
|
+
if (errorArg) reject(errorArg);
|
|
544
|
+
else resolve(messageArg!);
|
|
545
|
+
};
|
|
546
|
+
const onMessage = (messageArg: unknown) => {
|
|
547
|
+
const message = messageArg as Partial<TControllerIpcMessage>;
|
|
548
|
+
if (message?.type === 'startupError' && typeof message.message === 'string') {
|
|
549
|
+
finish(message.retained === true
|
|
550
|
+
? new RetainedControllerStartupError(message.message)
|
|
551
|
+
: new Error(message.message));
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (
|
|
555
|
+
message?.type === 'ready'
|
|
556
|
+
&& typeof message.publicUrl === 'string'
|
|
557
|
+
&& typeof message.status === 'object'
|
|
558
|
+
&& message.status !== null
|
|
559
|
+
&& (message.setupCode === undefined || typeof message.setupCode === 'string')
|
|
560
|
+
) {
|
|
561
|
+
try {
|
|
562
|
+
finish(undefined, {
|
|
563
|
+
type: 'ready',
|
|
564
|
+
publicUrl: message.publicUrl,
|
|
565
|
+
status: assertStatus(message.status),
|
|
566
|
+
...(message.setupCode ? { setupCode: message.setupCode } : {}),
|
|
567
|
+
});
|
|
568
|
+
} catch (error) {
|
|
569
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
const onError = (errorArg: Error) => finish(errorArg);
|
|
574
|
+
const onExit = (codeArg: number | null, signalArg: NodeJS.Signals | null) => {
|
|
575
|
+
finish(new Error(`Detached controller exited during startup (${signalArg ?? codeArg ?? 'unknown'}).`));
|
|
576
|
+
};
|
|
577
|
+
const timeout = setTimeout(() => {
|
|
578
|
+
finish(new Error('Detached controller did not become ready within 45 seconds.'));
|
|
579
|
+
}, 45_000);
|
|
580
|
+
childArg.on('message', onMessage);
|
|
581
|
+
childArg.once('error', onError);
|
|
582
|
+
childArg.once('exit', onExit);
|
|
583
|
+
});
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
const startDetachedWithoutAdmission = async (argvArg: TParsedArguments): Promise<void> => {
|
|
587
|
+
if (process.platform === 'win32') {
|
|
588
|
+
throw new Error('Detached process management is currently supported on Linux and macOS only.');
|
|
589
|
+
}
|
|
590
|
+
const overrides = parseRuntimeOverrides(argvArg);
|
|
591
|
+
const providedSetupCode = readProvidedSetupCode();
|
|
592
|
+
const existing = await tryQueryControllerStatus(overrides.controllerPort);
|
|
593
|
+
const json = readBooleanOption(argvArg, 'json');
|
|
594
|
+
if (existing) {
|
|
595
|
+
if (providedSetupCode !== undefined && !json) {
|
|
596
|
+
process.stdout.write(`${currentRuntimeName} is already running; the provided --setup-code was not used.\n`);
|
|
597
|
+
}
|
|
598
|
+
if (json) process.stdout.write(statusJson(existing) + '\n');
|
|
599
|
+
else printStatus(existing);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const cliPath = await resolveCliPath();
|
|
603
|
+
const existingProcesses = await listControllerProcessesForCli(cliPath);
|
|
604
|
+
const existingProcess = existingProcesses.find((candidateArg) => (
|
|
605
|
+
candidateArg.port === overrides.controllerPort
|
|
606
|
+
));
|
|
607
|
+
if (existingProcess) {
|
|
608
|
+
throw new Error(
|
|
609
|
+
`An ${currentRuntimeName} process is already starting or running (PID ${existingProcess.identity.pid}).`,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
const upgradeToken = process.env[upgradeTokenEnvironmentVariable];
|
|
613
|
+
let upgradeGlobalRoot: string | undefined;
|
|
614
|
+
if (upgradeToken) {
|
|
615
|
+
upgradeGlobalRoot = (await resolveCurrentPnpmGlobalInstallation()).globalRoot;
|
|
616
|
+
}
|
|
617
|
+
const logFilePath = resolveDetachedLogFilePath(overrides.controllerPort);
|
|
618
|
+
const logFileHandle = await openDetachedLogFile(logFilePath);
|
|
619
|
+
let child: plugins.childProcess.ChildProcess;
|
|
620
|
+
try {
|
|
621
|
+
const childEnvironment = { ...process.env };
|
|
622
|
+
delete childEnvironment[upgradeTokenEnvironmentVariable];
|
|
623
|
+
child = plugins.childProcess.spawn(
|
|
624
|
+
process.execPath,
|
|
625
|
+
[cliPath, '__serve', ...serializeRuntimeOptions(overrides)],
|
|
626
|
+
{
|
|
627
|
+
cwd: process.cwd(),
|
|
628
|
+
detached: true,
|
|
629
|
+
env: childEnvironment,
|
|
630
|
+
shell: false,
|
|
631
|
+
// stdout stays discarded: the transport logs every connection there,
|
|
632
|
+
// which would grow the file without bound. stderr carries diagnostics.
|
|
633
|
+
stdio: ['ignore', 'ignore', logFileHandle.fd, 'ipc'],
|
|
634
|
+
},
|
|
635
|
+
);
|
|
636
|
+
} finally {
|
|
637
|
+
await logFileHandle.close();
|
|
638
|
+
}
|
|
639
|
+
if (!child.pid) throw new Error('Detached controller did not provide a process ID.');
|
|
640
|
+
let spawnedIdentity: Awaited<ReturnType<typeof inspectControllerProcess>> = null;
|
|
641
|
+
try {
|
|
642
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
643
|
+
spawnedIdentity = await inspectControllerProcess({
|
|
644
|
+
pid: child.pid,
|
|
645
|
+
cliPath,
|
|
646
|
+
port: overrides.controllerPort,
|
|
647
|
+
expectedProcessGroupId: child.pid,
|
|
648
|
+
command: '__serve',
|
|
649
|
+
});
|
|
650
|
+
if (spawnedIdentity) break;
|
|
651
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
652
|
+
}
|
|
653
|
+
if (!spawnedIdentity) {
|
|
654
|
+
throw new Error('Detached controller process identity could not be established.');
|
|
655
|
+
}
|
|
656
|
+
} catch (error) {
|
|
657
|
+
// The child is blocked on its IPC start gate here and owns no server,
|
|
658
|
+
// database, or OpenCode resources yet. Killing the exact spawned PID cannot
|
|
659
|
+
// orphan a descendant process.
|
|
660
|
+
if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');
|
|
661
|
+
if (!await waitForSpawnedChildExit(child, 2_000)) {
|
|
662
|
+
throw new AggregateError(
|
|
663
|
+
[error],
|
|
664
|
+
'Detached controller identity failed and the gated child did not exit after SIGKILL.',
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
throw error;
|
|
668
|
+
}
|
|
669
|
+
try {
|
|
670
|
+
const readyPromise = waitForDetachedReady(
|
|
671
|
+
child,
|
|
672
|
+
);
|
|
673
|
+
try {
|
|
674
|
+
await sendIpcMessage(child, {
|
|
675
|
+
type: 'start',
|
|
676
|
+
...(providedSetupCode !== undefined ? { setupCode: providedSetupCode } : {}),
|
|
677
|
+
...(upgradeToken === undefined ? {} : { upgradeToken }),
|
|
678
|
+
...(upgradeGlobalRoot === undefined ? {} : { upgradeGlobalRoot }),
|
|
679
|
+
});
|
|
680
|
+
} catch (error) {
|
|
681
|
+
void readyPromise.catch(() => undefined);
|
|
682
|
+
throw error;
|
|
683
|
+
}
|
|
684
|
+
const readyMessage = await readyPromise;
|
|
685
|
+
const status = await queryControllerStatus(overrides.controllerPort);
|
|
686
|
+
child.disconnect();
|
|
687
|
+
child.unref();
|
|
688
|
+
printStartResult({
|
|
689
|
+
status,
|
|
690
|
+
publicUrl: readyMessage.publicUrl,
|
|
691
|
+
...(readyMessage.setupCode ? { setupCode: readyMessage.setupCode } : {}),
|
|
692
|
+
}, json, { logFilePath });
|
|
693
|
+
if (providedSetupCode !== undefined && !readyMessage.setupCode && !json) {
|
|
694
|
+
process.stdout.write('Setup is already complete; the provided --setup-code was not used.\n');
|
|
695
|
+
}
|
|
696
|
+
} catch (error) {
|
|
697
|
+
if (error instanceof RetainedControllerStartupError) {
|
|
698
|
+
if (child.connected) child.disconnect();
|
|
699
|
+
child.unref();
|
|
700
|
+
process.stderr.write(`${error.message}\n`);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
await stopJustSpawnedProcess(
|
|
705
|
+
child,
|
|
706
|
+
cliPath,
|
|
707
|
+
overrides.controllerPort,
|
|
708
|
+
spawnedIdentity.fingerprint,
|
|
709
|
+
);
|
|
710
|
+
} catch (cleanupErrorArg) {
|
|
711
|
+
throw new AggregateError(
|
|
712
|
+
[error, cleanupErrorArg],
|
|
713
|
+
`Detached controller startup failed (${formatCliError(error)}) and cleanup was incomplete (${formatCliError(cleanupErrorArg)}). Logs: ${logFilePath}`,
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
throw new Error(`${formatCliError(error)} Logs: ${logFilePath}`, { cause: error });
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
const startDetached = async (argvArg: TParsedArguments): Promise<void> => {
|
|
721
|
+
const lease = await acquireStartAdmission('start');
|
|
722
|
+
try {
|
|
723
|
+
await startDetachedWithoutAdmission(argvArg);
|
|
724
|
+
} finally {
|
|
725
|
+
await lease?.release();
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
const runForegroundWithAdmission = async (argvArg: TParsedArguments): Promise<void> => {
|
|
730
|
+
const lease = await acquireStartAdmission('foreground');
|
|
731
|
+
delete process.env[upgradeTokenEnvironmentVariable];
|
|
732
|
+
try {
|
|
733
|
+
await runControllerProcess(argvArg, false);
|
|
734
|
+
} finally {
|
|
735
|
+
await lease?.release();
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
const runControllerProcess = async (
|
|
740
|
+
argvArg: TParsedArguments,
|
|
741
|
+
detachedArg: boolean,
|
|
742
|
+
): Promise<void> => {
|
|
743
|
+
if (detachedArg && process.platform === 'win32') {
|
|
744
|
+
throw new Error('Detached process management is currently supported on Linux and macOS only.');
|
|
745
|
+
}
|
|
746
|
+
if (detachedArg && !process.connected) {
|
|
747
|
+
throw new Error('The internal detached server command requires an attached parent IPC channel.');
|
|
748
|
+
}
|
|
749
|
+
let providedSetupCode: string | undefined;
|
|
750
|
+
let upgradeToken: string | undefined;
|
|
751
|
+
let upgradeGlobalRoot: string | undefined;
|
|
752
|
+
let startupRetained = false;
|
|
753
|
+
if (detachedArg) {
|
|
754
|
+
const gateMessage = await waitForStartGate();
|
|
755
|
+
// The gate payload crosses a process boundary; validate rather than trust.
|
|
756
|
+
if (gateMessage.setupCode !== undefined) {
|
|
757
|
+
providedSetupCode = assertProvidedSetupCode(gateMessage.setupCode);
|
|
758
|
+
}
|
|
759
|
+
upgradeToken = gateMessage.upgradeToken;
|
|
760
|
+
upgradeGlobalRoot = gateMessage.upgradeGlobalRoot;
|
|
761
|
+
} else {
|
|
762
|
+
providedSetupCode = readProvidedSetupCode();
|
|
763
|
+
}
|
|
764
|
+
const controller = new OpenCodeController({
|
|
765
|
+
processMode: detachedArg ? 'detached' : 'foreground',
|
|
766
|
+
runtimeOverrides: parseRuntimeOverrides(argvArg),
|
|
767
|
+
...(providedSetupCode !== undefined ? { setupCode: providedSetupCode } : {}),
|
|
768
|
+
...(upgradeToken === undefined ? {} : { upgradeToken }),
|
|
769
|
+
...(upgradeGlobalRoot === undefined ? {} : { upgradeGlobalRoot }),
|
|
770
|
+
...(upgradeToken === undefined
|
|
771
|
+
? {}
|
|
772
|
+
: {
|
|
773
|
+
onStartupBlocked: async (errorArg: Error) => {
|
|
774
|
+
startupRetained = true;
|
|
775
|
+
if (!detachedArg || !process.connected) return;
|
|
776
|
+
await new Promise<void>((resolve) => {
|
|
777
|
+
process.send!({
|
|
778
|
+
type: 'startupError',
|
|
779
|
+
message: `${errorArg.message} The target controller remains fail-closed and will retry.`,
|
|
780
|
+
retained: true,
|
|
781
|
+
}, () => resolve());
|
|
782
|
+
});
|
|
783
|
+
},
|
|
784
|
+
}),
|
|
785
|
+
});
|
|
786
|
+
let ready = false;
|
|
787
|
+
let stopping: Promise<void> | undefined;
|
|
788
|
+
const stop = (): Promise<void> => {
|
|
789
|
+
if (stopping) return stopping;
|
|
790
|
+
let tracked: Promise<void>;
|
|
791
|
+
tracked = controller.stop().then(() => {
|
|
792
|
+
if (detachedArg) {
|
|
793
|
+
process.removeListener('SIGINT', onSignal);
|
|
794
|
+
process.removeListener('SIGTERM', onSignal);
|
|
795
|
+
process.removeListener('disconnect', onDisconnect);
|
|
796
|
+
if (process.connected && typeof process.disconnect === 'function') process.disconnect();
|
|
797
|
+
}
|
|
798
|
+
process.exitCode = 0;
|
|
799
|
+
}, (errorArg) => {
|
|
800
|
+
process.exitCode = 1;
|
|
801
|
+
throw errorArg;
|
|
802
|
+
}).finally(() => {
|
|
803
|
+
if (stopping === tracked) stopping = undefined;
|
|
804
|
+
});
|
|
805
|
+
stopping = tracked;
|
|
806
|
+
return tracked;
|
|
807
|
+
};
|
|
808
|
+
const onSignal = () => {
|
|
809
|
+
void stop().catch((errorArg) => {
|
|
810
|
+
process.exitCode = 1;
|
|
811
|
+
process.stderr.write(`${currentCliName}: ${formatCliError(errorArg)}\n`);
|
|
812
|
+
});
|
|
813
|
+
};
|
|
814
|
+
const onDisconnect = () => {
|
|
815
|
+
if (!ready && !startupRetained) {
|
|
816
|
+
void stop().catch((errorArg) => {
|
|
817
|
+
process.exitCode = 1;
|
|
818
|
+
process.stderr.write(`${currentCliName}: ${formatCliError(errorArg)}\n`);
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
process.on('SIGINT', onSignal);
|
|
823
|
+
process.on('SIGTERM', onSignal);
|
|
824
|
+
if (detachedArg) process.once('disconnect', onDisconnect);
|
|
825
|
+
try {
|
|
826
|
+
const result = await controller.start();
|
|
827
|
+
if (detachedArg) {
|
|
828
|
+
if (process.connected) {
|
|
829
|
+
await new Promise<void>((resolve, reject) => {
|
|
830
|
+
const message: IControllerReadyIpcMessage = {
|
|
831
|
+
type: 'ready',
|
|
832
|
+
status: result.status,
|
|
833
|
+
publicUrl: result.publicUrl,
|
|
834
|
+
...(result.setupCode ? { setupCode: result.setupCode } : {}),
|
|
835
|
+
};
|
|
836
|
+
process.send!(message, (errorArg) => errorArg ? reject(errorArg) : resolve());
|
|
837
|
+
});
|
|
838
|
+
} else if (!startupRetained) {
|
|
839
|
+
throw new Error('Detached controller startup lost its parent IPC channel.');
|
|
840
|
+
}
|
|
841
|
+
ready = true;
|
|
842
|
+
} else {
|
|
843
|
+
ready = true;
|
|
844
|
+
const jsonOutput = readBooleanOption(argvArg, 'json');
|
|
845
|
+
printStartResult(result, jsonOutput);
|
|
846
|
+
if (providedSetupCode !== undefined && !result.setupCode && !jsonOutput) {
|
|
847
|
+
process.stdout.write('Setup is already complete; the provided --setup-code was not used.\n');
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
} catch (error) {
|
|
851
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
852
|
+
let ipcError: unknown;
|
|
853
|
+
if (detachedArg && process.connected) {
|
|
854
|
+
try {
|
|
855
|
+
await new Promise<void>((resolve, reject) => {
|
|
856
|
+
process.send!({ type: 'startupError', message: formatCliError(normalized) }, (errorArg) => (
|
|
857
|
+
errorArg ? reject(errorArg) : resolve()
|
|
858
|
+
));
|
|
859
|
+
});
|
|
860
|
+
} catch (errorArg) {
|
|
861
|
+
ipcError = errorArg;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
let cleanupError: unknown;
|
|
865
|
+
try {
|
|
866
|
+
await stop();
|
|
867
|
+
} catch (errorArg) {
|
|
868
|
+
cleanupError = errorArg;
|
|
869
|
+
}
|
|
870
|
+
const errors = [normalized, ipcError, cleanupError].filter((valueArg) => valueArg !== undefined);
|
|
871
|
+
if (errors.length > 1) {
|
|
872
|
+
throw new AggregateError(
|
|
873
|
+
errors,
|
|
874
|
+
`Detached controller startup failed: ${formatCliError(normalized)}`,
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
throw normalized;
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
const showStatus = async (argvArg: TParsedArguments): Promise<void> => {
|
|
882
|
+
const port = readPortOption(argvArg, 'port', 'port', defaultControllerPort)!;
|
|
883
|
+
const status = await queryControllerStatus(port).catch(() => {
|
|
884
|
+
throw new Error(`No compatible ${currentRuntimeName} instance is responding on 127.0.0.1:${port}.`);
|
|
885
|
+
});
|
|
886
|
+
if (readBooleanOption(argvArg, 'json')) process.stdout.write(statusJson(status) + '\n');
|
|
887
|
+
else printStatus(status);
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
const stopController = async (argvArg: TParsedArguments): Promise<void> => {
|
|
891
|
+
if (process.platform !== 'linux' && process.platform !== 'darwin') {
|
|
892
|
+
throw new Error('CLI process stopping is currently supported on Linux and macOS only.');
|
|
893
|
+
}
|
|
894
|
+
const port = readPortOption(argvArg, 'port', 'port', defaultControllerPort)!;
|
|
895
|
+
const status = await tryQueryControllerStatus(port);
|
|
896
|
+
if (!status) {
|
|
897
|
+
process.stdout.write(`${currentRuntimeName} is not running.\n`);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
const cliPath = await resolveCliPath();
|
|
901
|
+
await stopVerifiedController({ status, cliPath, port });
|
|
902
|
+
process.stdout.write(`${currentRuntimeName} stopped.\n`);
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
export const createCurrentPackageUpgradeTransaction = async (
|
|
906
|
+
coordinatorArg: UpgradeCoordinator,
|
|
907
|
+
optionsArg: {
|
|
908
|
+
token: string;
|
|
909
|
+
port: number;
|
|
910
|
+
sourceVersion: string;
|
|
911
|
+
controllerWasRunning: boolean;
|
|
912
|
+
continueSessions: boolean;
|
|
913
|
+
gracePeriodMs: number;
|
|
914
|
+
},
|
|
915
|
+
) => {
|
|
916
|
+
if (
|
|
917
|
+
commitinfo.name === upgradePackageTransitionSource.packageName
|
|
918
|
+
&& optionsArg.sourceVersion === upgradePackageTransitionSource.version
|
|
919
|
+
) {
|
|
920
|
+
return await coordinatorArg.createPackageTransitionTransaction(optionsArg);
|
|
921
|
+
}
|
|
922
|
+
return await coordinatorArg.createTransaction(optionsArg);
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
const upgradeController = async (argvArg: TParsedArguments): Promise<void> => {
|
|
926
|
+
if (process.platform !== 'linux' && process.platform !== 'darwin') {
|
|
927
|
+
throw new Error(`${currentCliName} upgrade is currently supported on Linux and macOS only.`);
|
|
928
|
+
}
|
|
929
|
+
const port = readPortOption(argvArg, 'port', 'port', defaultControllerPort)!;
|
|
930
|
+
const gracePeriodMs = readUpgradeGracePeriodMs(argvArg);
|
|
931
|
+
const continueSessions = readBooleanOption(argvArg, 'continueSessions', 'continue-sessions');
|
|
932
|
+
const installation = await resolveCurrentPnpmGlobalInstallation();
|
|
933
|
+
const status = await tryQueryControllerStatus(port);
|
|
934
|
+
if (!status && await isLoopbackPortListening(port)) {
|
|
935
|
+
throw new Error(
|
|
936
|
+
`Port ${port} is occupied by an incompatible service or controller version; refusing to upgrade.`,
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
const payload = createUpgradeWorkerPayload({
|
|
940
|
+
port,
|
|
941
|
+
gracePeriodMs,
|
|
942
|
+
continueSessions,
|
|
943
|
+
...(status
|
|
944
|
+
? {
|
|
945
|
+
expectedController: {
|
|
946
|
+
pid: status.controllerPid,
|
|
947
|
+
processGroupId: status.processGroupId,
|
|
948
|
+
processFingerprint: status.processFingerprint,
|
|
949
|
+
},
|
|
950
|
+
}
|
|
951
|
+
: {}),
|
|
952
|
+
});
|
|
953
|
+
const coordinator = new UpgradeCoordinator(installation.globalRoot);
|
|
954
|
+
const invocationToken = createUpgradeToken();
|
|
955
|
+
const invocationLease = await coordinator.acquireUpgradeLock({
|
|
956
|
+
token: invocationToken,
|
|
957
|
+
cliPath: installation.cliPath,
|
|
958
|
+
command: 'upgrade',
|
|
959
|
+
});
|
|
960
|
+
let handoffStarted = false;
|
|
961
|
+
let invocationError: unknown;
|
|
962
|
+
try {
|
|
963
|
+
await createCurrentPackageUpgradeTransaction(coordinator, {
|
|
964
|
+
token: payload.token,
|
|
965
|
+
port,
|
|
966
|
+
sourceVersion: installation.packageVersion,
|
|
967
|
+
controllerWasRunning: status !== undefined,
|
|
968
|
+
continueSessions,
|
|
969
|
+
gracePeriodMs: payload.gracePeriodMs,
|
|
970
|
+
});
|
|
971
|
+
let worker: { workerPid: number; logFilePath: string };
|
|
972
|
+
try {
|
|
973
|
+
if (status) {
|
|
974
|
+
await coordinator.createLaunchGrant({
|
|
975
|
+
token: payload.token,
|
|
976
|
+
port,
|
|
977
|
+
packageName: controllerPackageName,
|
|
978
|
+
packageVersion: commitinfo.version,
|
|
979
|
+
cliPath: installation.cliPath,
|
|
980
|
+
controller: payload.expectedController!,
|
|
981
|
+
});
|
|
982
|
+
try {
|
|
983
|
+
worker = await requestControllerUpgradeLaunch(port, payload.token);
|
|
984
|
+
} finally {
|
|
985
|
+
await coordinator.removeLaunchGrant(payload.token);
|
|
986
|
+
}
|
|
987
|
+
} else {
|
|
988
|
+
const launched = await launchDetachedUpgradeWorker({ installation, payload });
|
|
989
|
+
worker = { workerPid: launched.pid, logFilePath: launched.logFilePath };
|
|
990
|
+
}
|
|
991
|
+
} catch (errorArg) {
|
|
992
|
+
const transaction = await coordinator.readTransaction(payload.token);
|
|
993
|
+
if (transaction.worker?.logFilePath) {
|
|
994
|
+
worker = {
|
|
995
|
+
workerPid: transaction.worker.pid,
|
|
996
|
+
logFilePath: transaction.worker.logFilePath,
|
|
997
|
+
};
|
|
998
|
+
} else {
|
|
999
|
+
await coordinator.finishTransaction(
|
|
1000
|
+
payload.token,
|
|
1001
|
+
false,
|
|
1002
|
+
'The upgrade worker could not be launched.',
|
|
1003
|
+
errorArg instanceof Error ? errorArg.message : String(errorArg),
|
|
1004
|
+
);
|
|
1005
|
+
throw errorArg;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
await coordinator.recordWorkerLaunch(
|
|
1009
|
+
payload.token,
|
|
1010
|
+
worker.workerPid,
|
|
1011
|
+
installation.cliPath,
|
|
1012
|
+
worker.logFilePath,
|
|
1013
|
+
);
|
|
1014
|
+
const json = readBooleanOption(argvArg, 'json');
|
|
1015
|
+
if (!json) {
|
|
1016
|
+
process.stdout.write([
|
|
1017
|
+
`Upgrade worker started (PID ${worker.workerPid}).`,
|
|
1018
|
+
`Logs: ${worker.logFilePath}`,
|
|
1019
|
+
status
|
|
1020
|
+
? `Controller on port ${port} will restart after a successful upgrade.`
|
|
1021
|
+
: 'No running controller was found; it will remain stopped.',
|
|
1022
|
+
].join('\n') + '\n');
|
|
1023
|
+
}
|
|
1024
|
+
const launchedTransaction = await coordinator.readTransaction(payload.token);
|
|
1025
|
+
if (!launchedTransaction.worker) {
|
|
1026
|
+
throw new Error('The launched upgrade worker identity was not retained.');
|
|
1027
|
+
}
|
|
1028
|
+
await coordinator.transferUpgradeLockToWorker({
|
|
1029
|
+
lock: invocationLease,
|
|
1030
|
+
token: payload.token,
|
|
1031
|
+
worker: launchedTransaction.worker,
|
|
1032
|
+
});
|
|
1033
|
+
handoffStarted = true;
|
|
1034
|
+
await coordinator.acknowledgeWorker(payload.token);
|
|
1035
|
+
let finalTransaction: Awaited<ReturnType<UpgradeCoordinator['readTransaction']>>;
|
|
1036
|
+
let followError: unknown;
|
|
1037
|
+
try {
|
|
1038
|
+
finalTransaction = await followUpgradeTransaction(
|
|
1039
|
+
coordinator,
|
|
1040
|
+
payload.token,
|
|
1041
|
+
worker.logFilePath,
|
|
1042
|
+
!json,
|
|
1043
|
+
installation,
|
|
1044
|
+
);
|
|
1045
|
+
} catch (errorArg) {
|
|
1046
|
+
followError = errorArg;
|
|
1047
|
+
finalTransaction = await coordinator.readTransaction(payload.token).catch(() => ({
|
|
1048
|
+
version: 2 as const,
|
|
1049
|
+
tokenHash: '',
|
|
1050
|
+
revision: 0,
|
|
1051
|
+
port,
|
|
1052
|
+
sourceVersion: installation.packageVersion,
|
|
1053
|
+
controllerWasRunning: status !== undefined,
|
|
1054
|
+
continueSessions,
|
|
1055
|
+
gracePeriodMs: payload.gracePeriodMs,
|
|
1056
|
+
phase: 'failed' as const,
|
|
1057
|
+
message: errorArg instanceof Error ? errorArg.message : String(errorArg),
|
|
1058
|
+
createdAt: Date.now(),
|
|
1059
|
+
phaseStartedAt: Date.now(),
|
|
1060
|
+
updatedAt: Date.now(),
|
|
1061
|
+
sessions: [],
|
|
1062
|
+
terminal: {
|
|
1063
|
+
success: false,
|
|
1064
|
+
error: errorArg instanceof Error ? errorArg.message : String(errorArg),
|
|
1065
|
+
},
|
|
1066
|
+
}));
|
|
1067
|
+
}
|
|
1068
|
+
if (json) {
|
|
1069
|
+
const reportedWorker = finalTransaction.worker?.logFilePath
|
|
1070
|
+
? {
|
|
1071
|
+
workerPid: finalTransaction.worker.pid,
|
|
1072
|
+
logFilePath: finalTransaction.worker.logFilePath,
|
|
1073
|
+
}
|
|
1074
|
+
: worker;
|
|
1075
|
+
const sessionSummary = finalTransaction.sessions.reduce((summary, session) => {
|
|
1076
|
+
summary.pause[session.pauseState] = (summary.pause[session.pauseState] ?? 0) + 1;
|
|
1077
|
+
if (session.continueState) {
|
|
1078
|
+
summary.continue[session.continueState]
|
|
1079
|
+
= (summary.continue[session.continueState] ?? 0) + 1;
|
|
1080
|
+
}
|
|
1081
|
+
return summary;
|
|
1082
|
+
}, {
|
|
1083
|
+
pause: {} as Record<string, number>,
|
|
1084
|
+
continue: {} as Record<string, number>,
|
|
1085
|
+
});
|
|
1086
|
+
process.stdout.write(JSON.stringify({
|
|
1087
|
+
accepted: true,
|
|
1088
|
+
...reportedWorker,
|
|
1089
|
+
controllerWasRunning: status !== undefined,
|
|
1090
|
+
fromVersion: finalTransaction.sourceVersion,
|
|
1091
|
+
toVersion: finalTransaction.targetVersion,
|
|
1092
|
+
phase: finalTransaction.phase,
|
|
1093
|
+
success: finalTransaction.terminal?.success === true && followError === undefined,
|
|
1094
|
+
error: finalTransaction.terminal?.error
|
|
1095
|
+
?? (followError instanceof Error ? followError.message : followError === undefined
|
|
1096
|
+
? undefined
|
|
1097
|
+
: String(followError)),
|
|
1098
|
+
sessions: sessionSummary,
|
|
1099
|
+
}, null, 2) + '\n');
|
|
1100
|
+
}
|
|
1101
|
+
if (followError) throw followError;
|
|
1102
|
+
if (!finalTransaction.terminal?.success) {
|
|
1103
|
+
throw new Error(finalTransaction.terminal?.error ?? `${currentCliName} upgrade failed.`);
|
|
1104
|
+
}
|
|
1105
|
+
} catch (errorArg) {
|
|
1106
|
+
invocationError = errorArg;
|
|
1107
|
+
}
|
|
1108
|
+
let releaseError: unknown;
|
|
1109
|
+
if (!handoffStarted) {
|
|
1110
|
+
try {
|
|
1111
|
+
await invocationLease.release();
|
|
1112
|
+
} catch (errorArg) {
|
|
1113
|
+
releaseError = errorArg;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (invocationError !== undefined && releaseError !== undefined) {
|
|
1117
|
+
throw new AggregateError(
|
|
1118
|
+
[invocationError, releaseError],
|
|
1119
|
+
`The ${currentCliName} upgrade invocation failed and its ownership could not be released.`,
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
if (invocationError !== undefined) throw invocationError;
|
|
1123
|
+
if (releaseError !== undefined) throw releaseError;
|
|
1124
|
+
};
|
|
1125
|
+
|
|
1126
|
+
const runHiddenUpgradeWorker = async (): Promise<void> => {
|
|
1127
|
+
if (activeRuntimeArguments.length !== 2 && activeRuntimeArguments.length !== 3) {
|
|
1128
|
+
throw new Error(`The internal ${currentCliName} upgrade worker command requires a payload and optional recovery context.`);
|
|
1129
|
+
}
|
|
1130
|
+
const payload = parseUpgradeWorkerPayload(
|
|
1131
|
+
activeRuntimeArguments[1],
|
|
1132
|
+
process.env[upgradeTokenEnvironmentVariable],
|
|
1133
|
+
);
|
|
1134
|
+
if (activeRuntimeArguments.length === 2) {
|
|
1135
|
+
await runUpgradeWorker(payload);
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
const runWithRecoveryContext = runUpgradeWorker as unknown as (
|
|
1139
|
+
payloadArg: typeof payload,
|
|
1140
|
+
optionsArg: { recoveryContext: string },
|
|
1141
|
+
) => Promise<void>;
|
|
1142
|
+
await runWithRecoveryContext(payload, { recoveryContext: activeRuntimeArguments[2] });
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
const mintTempPasswordWithoutAdmission = async (argvArg: TParsedArguments): Promise<void> => {
|
|
1146
|
+
const port = readPortOption(argvArg, 'port', 'port', defaultControllerPort)!;
|
|
1147
|
+
const ttlValue = readOption(argvArg, 'ttlHours', 'ttl-hours');
|
|
1148
|
+
let ttlHours = 24;
|
|
1149
|
+
if (ttlValue !== undefined) {
|
|
1150
|
+
const normalized = typeof ttlValue === 'number' ? ttlValue : Number(ttlValue);
|
|
1151
|
+
if (!Number.isFinite(normalized) || normalized <= 0 || normalized > 24) {
|
|
1152
|
+
throw new Error('--ttl-hours must be a number above 0 and at most 24.');
|
|
1153
|
+
}
|
|
1154
|
+
ttlHours = normalized;
|
|
1155
|
+
}
|
|
1156
|
+
const authStore = new SmartDataAuthStore((await ensureControllerDataRoot()).databaseConfig);
|
|
1157
|
+
let operationError: unknown;
|
|
1158
|
+
try {
|
|
1159
|
+
await authStore.init();
|
|
1160
|
+
const runtimeConfig = await authStore.getRuntimeConfig(port);
|
|
1161
|
+
if (!runtimeConfig) {
|
|
1162
|
+
throw new Error(
|
|
1163
|
+
`No controller configuration exists for port ${port}. Start the controller first, and run `
|
|
1164
|
+
+ 'temp-password with the same HARNESS_CONTROLLER_MONGO_URL / HARNESS_CONTROLLER_MONGO_DB '
|
|
1165
|
+
+ 'environment as the controller.',
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
const minted = await authStore.createTempPassword(Math.round(ttlHours * 60 * 60 * 1000));
|
|
1169
|
+
await authStore.recordAuditEvent({
|
|
1170
|
+
type: 'temppassword.create',
|
|
1171
|
+
outcome: 'succeeded',
|
|
1172
|
+
credentialId: minted.credentialId,
|
|
1173
|
+
});
|
|
1174
|
+
process.stdout.write([
|
|
1175
|
+
`Temporary password: ${minted.password}`,
|
|
1176
|
+
`Expires: ${minted.expiresAt.toISOString()}`,
|
|
1177
|
+
`Sign in at ${runtimeConfig.publicOrigin} using "CLI temporary password".`,
|
|
1178
|
+
].join('\n') + '\n');
|
|
1179
|
+
} catch (errorArg) {
|
|
1180
|
+
operationError = errorArg;
|
|
1181
|
+
throw errorArg;
|
|
1182
|
+
} finally {
|
|
1183
|
+
try {
|
|
1184
|
+
await authStore.close();
|
|
1185
|
+
} catch (cleanupErrorArg) {
|
|
1186
|
+
if (operationError !== undefined) {
|
|
1187
|
+
throw new AggregateError(
|
|
1188
|
+
[operationError, cleanupErrorArg],
|
|
1189
|
+
'Temporary password operation failed and SmartData cleanup was incomplete.',
|
|
1190
|
+
{ cause: operationError },
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
throw cleanupErrorArg;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
const mintTempPassword = async (argvArg: TParsedArguments): Promise<void> => {
|
|
1199
|
+
const lease = await acquireStartAdmission('temp-password');
|
|
1200
|
+
try {
|
|
1201
|
+
await mintTempPasswordWithoutAdmission(argvArg);
|
|
1202
|
+
} finally {
|
|
1203
|
+
await lease?.release();
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
export const runCli = async (testArgvArg?: string[]): Promise<void> => {
|
|
1208
|
+
activeRuntimeArguments = (testArgvArg ?? process.argv).slice(2);
|
|
1209
|
+
if (
|
|
1210
|
+
activeRuntimeArguments.some((argumentArg) => argumentArg === '--help' || argumentArg === '-h')
|
|
1211
|
+
&& (
|
|
1212
|
+
activeRuntimeArguments[0] === '--help'
|
|
1213
|
+
|| activeRuntimeArguments[0] === '-h'
|
|
1214
|
+
|| publicCliCommands.has(activeRuntimeArguments[0] ?? '')
|
|
1215
|
+
)
|
|
1216
|
+
) {
|
|
1217
|
+
process.stdout.write(helpText + '\n');
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
const cli = new plugins.smartcli.Smartcli();
|
|
1221
|
+
cli.addVersion(commitinfo.version);
|
|
1222
|
+
let commandPromise: Promise<void> = Promise.resolve();
|
|
1223
|
+
let commandDispatched = false;
|
|
1224
|
+
const register = (commandArg: string, handlerArg: (argvArg: TParsedArguments) => Promise<void>) => {
|
|
1225
|
+
cli.addCommand(commandArg).subscribe((argvArg: TParsedArguments) => {
|
|
1226
|
+
commandDispatched = true;
|
|
1227
|
+
commandPromise = handlerArg(argvArg);
|
|
1228
|
+
});
|
|
1229
|
+
};
|
|
1230
|
+
register('start', startDetached);
|
|
1231
|
+
register('status', showStatus);
|
|
1232
|
+
register('stop', stopController);
|
|
1233
|
+
register('upgrade', upgradeController);
|
|
1234
|
+
register('foreground', runForegroundWithAdmission);
|
|
1235
|
+
register('temp-password', mintTempPassword);
|
|
1236
|
+
register('__serve', (argvArg) => runControllerProcess(argvArg, true));
|
|
1237
|
+
register('__upgrade-worker', runHiddenUpgradeWorker);
|
|
1238
|
+
cli.addCommand('help').subscribe(() => {
|
|
1239
|
+
commandDispatched = true;
|
|
1240
|
+
process.stdout.write(helpText + '\n');
|
|
1241
|
+
});
|
|
1242
|
+
cli.standardCommand().subscribe(() => {
|
|
1243
|
+
commandDispatched = true;
|
|
1244
|
+
process.stdout.write(helpText + '\n');
|
|
1245
|
+
});
|
|
1246
|
+
cli.startParse(testArgvArg);
|
|
1247
|
+
const runtimeArguments = activeRuntimeArguments;
|
|
1248
|
+
const versionOnly = runtimeArguments.length > 0
|
|
1249
|
+
&& runtimeArguments.every((argumentArg) => argumentArg === '-v' || argumentArg === '--version');
|
|
1250
|
+
if (!commandDispatched && !versionOnly) {
|
|
1251
|
+
throw new Error(`Unknown command. Run ${currentCliName} help for usage.`);
|
|
1252
|
+
}
|
|
1253
|
+
await commandPromise;
|
|
1254
|
+
};
|