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.
Files changed (277) hide show
  1. package/.smartconfig.json +55 -0
  2. package/changelog.md +1026 -0
  3. package/cli.js +14 -0
  4. package/dist_serve/bundle.js +44778 -0
  5. package/dist_serve/index.html +13 -0
  6. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  7. package/dist_ts/00_commitinfo_data.js +9 -0
  8. package/dist_ts/classes.authmodels.d.ts +126 -0
  9. package/dist_ts/classes.authmodels.js +1422 -0
  10. package/dist_ts/classes.authstore.d.ts +160 -0
  11. package/dist_ts/classes.authstore.js +2483 -0
  12. package/dist_ts/classes.browserresourcehost.d.ts +85 -0
  13. package/dist_ts/classes.browserresourcehost.js +912 -0
  14. package/dist_ts/classes.cli.d.ts +21 -0
  15. package/dist_ts/classes.cli.js +1082 -0
  16. package/dist_ts/classes.config.d.ts +23 -0
  17. package/dist_ts/classes.config.js +267 -0
  18. package/dist_ts/classes.controller.d.ts +523 -0
  19. package/dist_ts/classes.controller.js +10611 -0
  20. package/dist_ts/classes.controllermanagement.d.ts +32 -0
  21. package/dist_ts/classes.controllermanagement.js +222 -0
  22. package/dist_ts/classes.draftmanager.d.ts +30 -0
  23. package/dist_ts/classes.draftmanager.js +179 -0
  24. package/dist_ts/classes.embeddeddb.d.ts +30 -0
  25. package/dist_ts/classes.embeddeddb.js +159 -0
  26. package/dist_ts/classes.flexframedtransport.d.ts +56 -0
  27. package/dist_ts/classes.flexframedtransport.js +275 -0
  28. package/dist_ts/classes.flexmodels.private.d.ts +231 -0
  29. package/dist_ts/classes.flexmodels.private.js +985 -0
  30. package/dist_ts/classes.flexmodels.public.d.ts +207 -0
  31. package/dist_ts/classes.flexmodels.public.js +885 -0
  32. package/dist_ts/classes.flexprojectionreader.d.ts +89 -0
  33. package/dist_ts/classes.flexprojectionreader.js +580 -0
  34. package/dist_ts/classes.flexservice.d.ts +196 -0
  35. package/dist_ts/classes.flexservice.js +3704 -0
  36. package/dist_ts/classes.flexstore.d.ts +174 -0
  37. package/dist_ts/classes.flexstore.js +3400 -0
  38. package/dist_ts/classes.flexsupervisor.d.ts +105 -0
  39. package/dist_ts/classes.flexsupervisor.js +1191 -0
  40. package/dist_ts/classes.gitreversion.d.ts +431 -0
  41. package/dist_ts/classes.gitreversion.js +10592 -0
  42. package/dist_ts/classes.harnessoperationmanager.d.ts +113 -0
  43. package/dist_ts/classes.harnessoperationmanager.js +602 -0
  44. package/dist_ts/classes.opencodeclient.d.ts +146 -0
  45. package/dist_ts/classes.opencodeclient.js +3003 -0
  46. package/dist_ts/classes.opencodesupervisor.d.ts +60 -0
  47. package/dist_ts/classes.opencodesupervisor.js +1003 -0
  48. package/dist_ts/classes.passkeymanager.d.ts +34 -0
  49. package/dist_ts/classes.passkeymanager.js +456 -0
  50. package/dist_ts/classes.processinspection.d.ts +50 -0
  51. package/dist_ts/classes.processinspection.js +484 -0
  52. package/dist_ts/classes.resourcecoordinator.d.ts +64 -0
  53. package/dist_ts/classes.resourcecoordinator.js +294 -0
  54. package/dist_ts/classes.sessionintelligencereader.d.ts +13 -0
  55. package/dist_ts/classes.sessionintelligencereader.js +40 -0
  56. package/dist_ts/classes.systemmetricssampler.d.ts +53 -0
  57. package/dist_ts/classes.systemmetricssampler.js +168 -0
  58. package/dist_ts/classes.terminalmanager.d.ts +95 -0
  59. package/dist_ts/classes.terminalmanager.js +981 -0
  60. package/dist_ts/classes.terminalresourcehost.d.ts +20 -0
  61. package/dist_ts/classes.terminalresourcehost.js +84 -0
  62. package/dist_ts/classes.upgradecoordinator.d.ts +244 -0
  63. package/dist_ts/classes.upgradecoordinator.js +2155 -0
  64. package/dist_ts/classes.upgradetransaction.d.ts +92 -0
  65. package/dist_ts/classes.upgradetransaction.js +2125 -0
  66. package/dist_ts/classes.uploadmanager.d.ts +24 -0
  67. package/dist_ts/classes.uploadmanager.js +210 -0
  68. package/dist_ts/classes.validation.d.ts +91 -0
  69. package/dist_ts/classes.validation.js +1087 -0
  70. package/dist_ts/flexharness.child.d.ts +1 -0
  71. package/dist_ts/flexharness.child.js +28 -0
  72. package/dist_ts/functions.canonicaldirectory.d.ts +21 -0
  73. package/dist_ts/functions.canonicaldirectory.js +144 -0
  74. package/dist_ts/functions.controllerdataroot.d.ts +15 -0
  75. package/dist_ts/functions.controllerdataroot.js +76 -0
  76. package/dist_ts/functions.embeddeddb.d.ts +2 -0
  77. package/dist_ts/functions.embeddeddb.js +21 -0
  78. package/dist_ts/functions.flexcredentialstore.d.ts +16 -0
  79. package/dist_ts/functions.flexcredentialstore.js +93 -0
  80. package/dist_ts/functions.opencodeauth.d.ts +3 -0
  81. package/dist_ts/functions.opencodeauth.js +31 -0
  82. package/dist_ts/functions.runtimeenvironment.d.ts +2 -0
  83. package/dist_ts/functions.runtimeenvironment.js +79 -0
  84. package/dist_ts/functions.upgradefollower.d.ts +3 -0
  85. package/dist_ts/functions.upgradefollower.js +95 -0
  86. package/dist_ts/index.d.ts +24 -0
  87. package/dist_ts/index.js +25 -0
  88. package/dist_ts/interfaces.auth.d.ts +181 -0
  89. package/dist_ts/interfaces.auth.js +82 -0
  90. package/dist_ts/interfaces.config.d.ts +34 -0
  91. package/dist_ts/interfaces.config.js +2 -0
  92. package/dist_ts/interfaces.flexipc.d.ts +939 -0
  93. package/dist_ts/interfaces.flexipc.js +1492 -0
  94. package/dist_ts/interfaces.gitreversion.d.ts +246 -0
  95. package/dist_ts/interfaces.gitreversion.js +2 -0
  96. package/dist_ts/interfaces.lifecycle.d.ts +37 -0
  97. package/dist_ts/interfaces.lifecycle.js +2 -0
  98. package/dist_ts/interfaces.opencode.d.ts +101 -0
  99. package/dist_ts/interfaces.opencode.js +3 -0
  100. package/dist_ts/interfaces.projects.d.ts +156 -0
  101. package/dist_ts/interfaces.projects.js +5 -0
  102. package/dist_ts/plugins.d.ts +36 -0
  103. package/dist_ts/plugins.js +40 -0
  104. package/dist_ts_interfaces/index.d.ts +2 -0
  105. package/dist_ts_interfaces/index.js +3 -0
  106. package/dist_ts_interfaces/interfaces.d.ts +747 -0
  107. package/dist_ts_interfaces/interfaces.js +47 -0
  108. package/dist_ts_interfaces/plugins.d.ts +4 -0
  109. package/dist_ts_interfaces/plugins.js +3 -0
  110. package/dist_ts_interfaces/requests.d.ts +806 -0
  111. package/dist_ts_interfaces/requests.js +2 -0
  112. package/dist_ts_migration/classes.documentmigrationrunner.d.ts +8 -0
  113. package/dist_ts_migration/classes.documentmigrationrunner.js +24 -0
  114. package/dist_ts_migration/classes.flexmigrationrunner.d.ts +11 -0
  115. package/dist_ts_migration/classes.flexmigrationrunner.js +25 -0
  116. package/dist_ts_migration/classes.flexpostharnessmigrationrunner.d.ts +10 -0
  117. package/dist_ts_migration/classes.flexpostharnessmigrationrunner.js +15 -0
  118. package/dist_ts_migration/classes.migrationrunner.d.ts +8 -0
  119. package/dist_ts_migration/classes.migrationrunner.js +34 -0
  120. package/dist_ts_migration/index.d.ts +12 -0
  121. package/dist_ts_migration/index.js +13 -0
  122. package/dist_ts_migration/v10_flexharnessv3migration.d.ts +11 -0
  123. package/dist_ts_migration/v10_flexharnessv3migration.js +23 -0
  124. package/dist_ts_migration/v13_flexprojectionorder.d.ts +11 -0
  125. package/dist_ts_migration/v13_flexprojectionorder.js +23 -0
  126. package/dist_ts_migration/v16_flexprovidercredentials.d.ts +22 -0
  127. package/dist_ts_migration/v16_flexprovidercredentials.js +149 -0
  128. package/dist_ts_migration/v18_resourcelayout.d.ts +22 -0
  129. package/dist_ts_migration/v18_resourcelayout.js +23 -0
  130. package/dist_ts_migration/v18_resourcelayoutmigration.d.ts +6 -0
  131. package/dist_ts_migration/v18_resourcelayoutmigration.js +61 -0
  132. package/dist_ts_migration/v19_flexprojectmanagement.d.ts +11 -0
  133. package/dist_ts_migration/v19_flexprojectmanagement.js +21 -0
  134. package/dist_ts_migration/v1_embeddeddatabaselocation.d.ts +28 -0
  135. package/dist_ts_migration/v1_embeddeddatabaselocation.js +415 -0
  136. package/dist_ts_migration/v22_projectfilesystemidentitymigration.d.ts +7 -0
  137. package/dist_ts_migration/v22_projectfilesystemidentitymigration.js +105 -0
  138. package/dist_ts_migration/v2_controllerdataroot.d.ts +130 -0
  139. package/dist_ts_migration/v2_controllerdataroot.js +1887 -0
  140. package/dist_ts_migration/v6_protocoldocumentmigration.d.ts +7 -0
  141. package/dist_ts_migration/v6_protocoldocumentmigration.js +100 -0
  142. package/dist_ts_migration/v6_protocoldocuments.d.ts +7 -0
  143. package/dist_ts_migration/v6_protocoldocuments.js +138 -0
  144. package/html/index.html +13 -0
  145. package/{LICENSE → license.md} +2 -2
  146. package/package.json +86 -48
  147. package/readme.md +480 -0
  148. package/readme.plan.md +188 -0
  149. package/ts/00_commitinfo_data.ts +8 -0
  150. package/ts/classes.authmodels.ts +1245 -0
  151. package/ts/classes.authstore.ts +3274 -0
  152. package/ts/classes.browserresourcehost.ts +1205 -0
  153. package/ts/classes.cli.ts +1254 -0
  154. package/ts/classes.config.ts +351 -0
  155. package/ts/classes.controller.ts +14426 -0
  156. package/ts/classes.controllermanagement.ts +330 -0
  157. package/ts/classes.draftmanager.ts +232 -0
  158. package/ts/classes.embeddeddb.ts +166 -0
  159. package/ts/classes.flexframedtransport.ts +283 -0
  160. package/ts/classes.flexmodels.private.ts +823 -0
  161. package/ts/classes.flexmodels.public.ts +732 -0
  162. package/ts/classes.flexprojectionreader.ts +816 -0
  163. package/ts/classes.flexservice.ts +4302 -0
  164. package/ts/classes.flexstore.ts +4565 -0
  165. package/ts/classes.flexsupervisor.ts +1453 -0
  166. package/ts/classes.gitreversion.ts +13353 -0
  167. package/ts/classes.harnessoperationmanager.ts +870 -0
  168. package/ts/classes.opencodeclient.ts +4110 -0
  169. package/ts/classes.opencodesupervisor.ts +1280 -0
  170. package/ts/classes.passkeymanager.ts +570 -0
  171. package/ts/classes.processinspection.ts +623 -0
  172. package/ts/classes.resourcecoordinator.ts +436 -0
  173. package/ts/classes.sessionintelligencereader.ts +52 -0
  174. package/ts/classes.systemmetricssampler.ts +215 -0
  175. package/ts/classes.terminalmanager.ts +1207 -0
  176. package/ts/classes.terminalresourcehost.ts +122 -0
  177. package/ts/classes.upgradecoordinator.ts +2675 -0
  178. package/ts/classes.upgradetransaction.ts +2900 -0
  179. package/ts/classes.uploadmanager.ts +240 -0
  180. package/ts/classes.validation.ts +1409 -0
  181. package/ts/flexharness.child.ts +35 -0
  182. package/ts/functions.canonicaldirectory.ts +213 -0
  183. package/ts/functions.controllerdataroot.ts +107 -0
  184. package/ts/functions.embeddeddb.ts +22 -0
  185. package/ts/functions.flexcredentialstore.ts +128 -0
  186. package/ts/functions.opencodeauth.ts +34 -0
  187. package/ts/functions.runtimeenvironment.ts +90 -0
  188. package/ts/functions.upgradefollower.ts +115 -0
  189. package/ts/index.ts +36 -6
  190. package/ts/interfaces.auth.ts +316 -0
  191. package/ts/interfaces.config.ts +37 -0
  192. package/ts/interfaces.flexipc.ts +2586 -0
  193. package/ts/interfaces.gitreversion.ts +336 -0
  194. package/ts/interfaces.lifecycle.ts +46 -0
  195. package/ts/interfaces.opencode.ts +153 -0
  196. package/ts/interfaces.projects.ts +182 -0
  197. package/ts/plugins.ts +64 -0
  198. package/ts_interfaces/index.ts +2 -0
  199. package/ts_interfaces/interfaces.ts +827 -0
  200. package/ts_interfaces/plugins.ts +15 -0
  201. package/ts_interfaces/requests.ts +1186 -0
  202. package/ts_migration/classes.documentmigrationrunner.ts +32 -0
  203. package/ts_migration/classes.flexmigrationrunner.ts +33 -0
  204. package/ts_migration/classes.flexpostharnessmigrationrunner.ts +22 -0
  205. package/ts_migration/classes.migrationrunner.ts +40 -0
  206. package/ts_migration/index.ts +12 -0
  207. package/ts_migration/v10_flexharnessv3migration.ts +35 -0
  208. package/ts_migration/v13_flexprojectionorder.ts +33 -0
  209. package/ts_migration/v16_flexprovidercredentials.ts +196 -0
  210. package/ts_migration/v18_resourcelayout.ts +55 -0
  211. package/ts_migration/v18_resourcelayoutmigration.ts +68 -0
  212. package/ts_migration/v19_flexprojectmanagement.ts +28 -0
  213. package/ts_migration/v1_embeddeddatabaselocation.ts +560 -0
  214. package/ts_migration/v22_projectfilesystemidentitymigration.ts +121 -0
  215. package/ts_migration/v2_controllerdataroot.ts +2349 -0
  216. package/ts_migration/v6_protocoldocumentmigration.ts +144 -0
  217. package/ts_migration/v6_protocoldocuments.ts +198 -0
  218. package/ts_web/00_commitinfo_data.ts +8 -0
  219. package/ts_web/classes.controllersocketclient.ts +517 -0
  220. package/ts_web/classes.sessiondraftsync.ts +390 -0
  221. package/ts_web/elements.harnesscontrollerapp.ts +11494 -0
  222. package/ts_web/index.ts +2 -0
  223. package/ts_web/plugins.ts +31 -0
  224. package/.gitlab-ci.yml +0 -71
  225. package/.npmignore +0 -5
  226. package/README.md +0 -40
  227. package/dist/cli.js +0 -2
  228. package/dist/index.d.ts +0 -0
  229. package/dist/index.js +0 -9
  230. package/dist/mod_bundle/index.d.ts +0 -1
  231. package/dist/mod_bundle/index.js +0 -18
  232. package/dist/mod_bundle/mod.plugins.d.ts +0 -4
  233. package/dist/mod_bundle/mod.plugins.js +0 -11
  234. package/dist/mod_install/index.d.ts +0 -4
  235. package/dist/mod_install/index.js +0 -21
  236. package/dist/mod_install/mod.plugins.d.ts +0 -1
  237. package/dist/mod_install/mod.plugins.js +0 -7
  238. package/dist/mod_standard/index.d.ts +0 -4
  239. package/dist/mod_standard/index.js +0 -21
  240. package/dist/mod_standard/mod.browser.d.ts +0 -6
  241. package/dist/mod_standard/mod.browser.js +0 -14
  242. package/dist/mod_standard/mod.clean.d.ts +0 -4
  243. package/dist/mod_standard/mod.clean.js +0 -12
  244. package/dist/mod_standard/mod.compile.d.ts +0 -4
  245. package/dist/mod_standard/mod.compile.js +0 -21
  246. package/dist/mod_standard/mod.plugins.d.ts +0 -8
  247. package/dist/mod_standard/mod.plugins.js +0 -19
  248. package/dist/mod_standard/mod.test.d.ts +0 -4
  249. package/dist/mod_standard/mod.test.js +0 -12
  250. package/dist/plr.cli.d.ts +0 -3
  251. package/dist/plr.cli.js +0 -57
  252. package/dist/plr.config.d.ts +0 -11
  253. package/dist/plr.config.js +0 -26
  254. package/dist/plr.mods.d.ts +0 -17
  255. package/dist/plr.mods.js +0 -17
  256. package/dist/plr.paths.d.ts +0 -19
  257. package/dist/plr.paths.js +0 -26
  258. package/dist/plr.plugins.d.ts +0 -8
  259. package/dist/plr.plugins.js +0 -16
  260. package/npmextra.json +0 -10
  261. package/ts/mod_bundle/index.ts +0 -20
  262. package/ts/mod_bundle/mod.plugins.ts +0 -9
  263. package/ts/mod_install/index.ts +0 -20
  264. package/ts/mod_install/mod.plugins.ts +0 -1
  265. package/ts/mod_standard/index.ts +0 -23
  266. package/ts/mod_standard/mod.browser.ts +0 -17
  267. package/ts/mod_standard/mod.clean.ts +0 -13
  268. package/ts/mod_standard/mod.compile.ts +0 -23
  269. package/ts/mod_standard/mod.plugins.ts +0 -17
  270. package/ts/mod_standard/mod.test.ts +0 -13
  271. package/ts/plr.cli.ts +0 -60
  272. package/ts/plr.config.ts +0 -36
  273. package/ts/plr.mods.ts +0 -25
  274. package/ts/plr.paths.ts +0 -29
  275. package/ts/plr.plugins.ts +0 -17
  276. package/tslint.json +0 -3
  277. 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
+ };