oysterun 1.6.0-beta.2 → 1.6.0-beta.21

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 (242) hide show
  1. package/bin/oysterun.mjs +64 -17
  2. package/dev/client/connectivity/completed-message-neighbor-click.mjs +78 -0
  3. package/dev/client/connectivity/completed-message-neighbor-contract.mjs +205 -0
  4. package/dev/client/connectivity/completed-message-visibility.d.mts +4 -4
  5. package/dev/client/connectivity/completed-message-visibility.mjs +14 -11
  6. package/dev/client/connectivity/desktop-chat-sidebar-preference.mjs +57 -0
  7. package/dev/client/connectivity/host-connection-feedback.mjs +12 -1
  8. package/dev/client/connectivity/managed-host-browser.mjs +10 -0
  9. package/dev/client/connectivity/session-navigation-intent.mjs +189 -0
  10. package/dev/client/connectivity/session-transition-state.mjs +18 -6
  11. package/dev/client/connectivity/test-completed-message-neighbor-click.mjs +180 -0
  12. package/dev/client/web/index.html +5367 -609
  13. package/dev/client/web-chat/dist/assets/{ReactPrism-BwJhidYf.js → ReactPrism-C6YiTZTC.js} +1 -1
  14. package/dev/client/web-chat/dist/assets/{ReactPrism-BwJhidYf.js.map → ReactPrism-C6YiTZTC.js.map} +1 -1
  15. package/dev/client/web-chat/dist/assets/SessionsSetupApp-CKJOSPFP.js +6442 -0
  16. package/dev/client/web-chat/dist/assets/SessionsSetupApp-CKJOSPFP.js.map +1 -0
  17. package/dev/client/web-chat/dist/assets/SessionsSetupApp-Dx09Uz-6.css +1 -0
  18. package/dev/client/web-chat/dist/assets/index-Ae5I4LHp.js +9 -0
  19. package/dev/client/web-chat/dist/assets/index-Ae5I4LHp.js.map +1 -0
  20. package/dev/client/web-chat/dist/assets/index-CN_FoTB1.js +2 -0
  21. package/dev/client/web-chat/dist/assets/index-CN_FoTB1.js.map +1 -0
  22. package/dev/client/web-chat/dist/assets/{index--1xmQ99x.js → index-DCQFwgGf.js} +2 -2
  23. package/dev/client/web-chat/dist/assets/{index--1xmQ99x.js.map → index-DCQFwgGf.js.map} +1 -1
  24. package/dev/client/web-chat/dist/assets/matrix-entry-BV7G38kH.css +10 -0
  25. package/dev/client/web-chat/dist/assets/matrix-entry-abAy3aOo.js +594 -0
  26. package/dev/client/web-chat/dist/assets/matrix-entry-abAy3aOo.js.map +1 -0
  27. package/dev/client/web-chat/dist/assets/{pdf-ZjtalfgI.js → pdf-DN7oVvM6.js} +5 -5
  28. package/dev/client/web-chat/dist/assets/pdf-DN7oVvM6.js.map +1 -0
  29. package/dev/client/web-chat/dist/assets/session-notice-DlyODy1z.css +1 -0
  30. package/dev/client/web-chat/dist/assets/web-BlYuDlOV.js +1 -0
  31. package/dev/client/web-chat/dist/assets/{web-c70YaskK.js.map → web-BlYuDlOV.js.map} +1 -1
  32. package/dev/client/web-chat/dist/assets/web-Dabw6cJi.js +1 -0
  33. package/dev/client/web-chat/dist/assets/{web-BzHcZK2M.js.map → web-Dabw6cJi.js.map} +1 -1
  34. package/dev/client/web-chat/dist/index.html +1 -2
  35. package/docs/routec/dev/cloud_deplyments.md +5 -0
  36. package/docs/routec/dev/npm_native_dependency_install.md +6 -2
  37. package/docs/routec/dev/npmjs_publish.md +55 -19
  38. package/docs/routec/dev/p262_setup_input_file.md +117 -0
  39. package/docs/routec/host2_turn_started_idempotency_diagnostics.md +8 -7
  40. package/docs/routec/host_setup.md +39 -10
  41. package/docs/routec/oysterun_operation_interface_spec.md +78 -2
  42. package/docs/routec/provider_contract.md +35 -5
  43. package/host-service/README.md +36 -0
  44. package/host-service/adapters/claude-code-adapter.mjs +48 -5
  45. package/host-service/adapters/codex-app-server-adapter.mjs +320 -18
  46. package/host-service/adapters/debug-fixture-provider-adapter.mjs +102 -1
  47. package/host-service/adapters/debug-large-tool-spillover-provider-adapter.mjs +2 -0
  48. package/host-service/adapters/debug-p135-codex-replay-provider-adapter.mjs +2 -0
  49. package/host-service/adapters/debug-routec-structural-replay-provider-adapter.mjs +2 -0
  50. package/host-service/agent-config.mjs +16 -5
  51. package/host-service/agent-folder-availability.mjs +41 -0
  52. package/host-service/apns-complete-message-dispatcher.mjs +1 -0
  53. package/host-service/assets/product-skills/Oysterun/modules/oysterun-find-context/SKILL.md +24 -0
  54. package/host-service/assets/product-skills/Oysterun/modules/oysterun-session-chat/SKILL.md +94 -5
  55. package/host-service/assets/product-skills/Oysterun/modules/oysterun-sessions/SKILL.md +103 -0
  56. package/host-service/assets/product-skills/Oysterun/modules/oysterun-website/SKILL.md +7 -0
  57. package/host-service/assets/product-skills/oysterun-find-context/SKILL.md +24 -0
  58. package/host-service/assets/product-skills/oysterun-session-chat/SKILL.md +94 -5
  59. package/host-service/assets/product-skills/oysterun-sessions/SKILL.md +103 -0
  60. package/host-service/assets/product-skills/oysterun-website/SKILL.md +7 -0
  61. package/host-service/callback-durable-store.mjs +354 -47
  62. package/host-service/callback-foundation.mjs +21 -1
  63. package/host-service/callback-host-integration.mjs +345 -30
  64. package/host-service/claude-acp-model-discovery.mjs +23 -18
  65. package/host-service/cli/assert-installer-reinstall-target.mjs +9 -0
  66. package/host-service/cli/installer-recovery-guidance.mjs +20 -0
  67. package/host-service/cli/prepare-restart-restore.mjs +2 -0
  68. package/host-service/cli/product-cli.mjs +34 -33
  69. package/host-service/cli/reconcile-initial-preflight.mjs +19 -0
  70. package/host-service/cli/recover-installer-lifecycle.mjs +24 -0
  71. package/host-service/cli/run-host-lifecycle.mjs +2 -0
  72. package/host-service/cli/run-staged-lifecycle-recovery.mjs +161 -0
  73. package/host-service/codex-model-discovery.mjs +23 -18
  74. package/host-service/complete-cli-uninstall.mjs +176 -0
  75. package/host-service/complete-message-notification-predicate.mjs +8 -6
  76. package/host-service/complete-message-notification-title.mjs +4 -0
  77. package/host-service/completed-session-unread.mjs +425 -42
  78. package/host-service/completed-turn-navigation.mjs +444 -144
  79. package/host-service/completed-turn-notification-observer.mjs +116 -0
  80. package/host-service/completed-turn-service.mjs +445 -0
  81. package/host-service/config.mjs +115 -9
  82. package/host-service/desired-open-session-state.mjs +331 -0
  83. package/host-service/fixtures/p218-completed-turn-notification.json +11 -0
  84. package/host-service/frp-agent.mjs +9 -9
  85. package/host-service/grok-acp-model-discovery.mjs +31 -33
  86. package/host-service/host-lifecycle-recovery.mjs +37 -0
  87. package/host-service/host-port-resolution.mjs +98 -0
  88. package/host-service/host-runtime-diagnostics.mjs +18 -1
  89. package/host-service/initial-preflight-recovery.mjs +71 -0
  90. package/host-service/installer-recovery-guidance.mjs +85 -0
  91. package/host-service/installer-reinstall-bindings.mjs +49 -0
  92. package/host-service/installer-reinstall-target.mjs +49 -0
  93. package/host-service/legacy-continuity-observation.mjs +33 -0
  94. package/host-service/lifecycle-compat-cli.mjs +13 -0
  95. package/host-service/lifecycle-compat-preload.mjs +89 -0
  96. package/host-service/lifecycle-compatibility.mjs +164 -0
  97. package/host-service/lifecycle-continuity-assets.mjs +222 -0
  98. package/host-service/lifecycle-continuity-probe.mjs +97 -0
  99. package/host-service/lifecycle-continuity-projection.mjs +29 -0
  100. package/host-service/lifecycle-continuity-runner.mjs +192 -0
  101. package/host-service/lifecycle-continuity.mjs +493 -0
  102. package/host-service/lifecycle-service-render-bundle.mjs +120 -0
  103. package/host-service/lifecycle-user-path.mjs +34 -0
  104. package/host-service/link-annotations.mjs +10 -0
  105. package/host-service/matrix-credential-issuer.mjs +32 -1
  106. package/host-service/matrix-event-correlator.mjs +166 -27
  107. package/host-service/matrix-facade.mjs +96 -19
  108. package/host-service/matrix-room-binding.mjs +24 -0
  109. package/host-service/matrix-transcript-read-adapter.mjs +17 -3
  110. package/host-service/model-discovery-guardian.mjs +40 -0
  111. package/host-service/model-discovery-process.mjs +141 -0
  112. package/host-service/onboard-lifecycle-status.mjs +28 -0
  113. package/host-service/onboard.mjs +479 -0
  114. package/host-service/package.json +2 -1
  115. package/host-service/portable-scheduler-definitions.mjs +28 -0
  116. package/host-service/product-runtime-host-origin.mjs +22 -0
  117. package/host-service/provider-model-params.mjs +14 -17
  118. package/host-service/provider-model-refresh-runner.mjs +124 -169
  119. package/host-service/provider-model-refresh-schedule.mjs +11 -0
  120. package/host-service/provider-resume-contract.mjs +9 -3
  121. package/host-service/restart-restore-state.mjs +25 -0
  122. package/host-service/retained-host-state-holder.mjs +57 -0
  123. package/host-service/routec-matrix-sqlite-store.mjs +155 -10
  124. package/host-service/routec-matrix-storage-adapter.mjs +208 -25
  125. package/host-service/routec-matrix-storage-paths.mjs +12 -0
  126. package/host-service/routec-request-delay-window.mjs +55 -0
  127. package/host-service/scheduler-acp-provider-runner.mjs +2 -1
  128. package/host-service/scheduler-direct-provider-runner.mjs +71 -9
  129. package/host-service/scheduler-run-timeout-policy.mjs +85 -0
  130. package/host-service/scheduler-service.mjs +96 -9
  131. package/host-service/scheduler-store.mjs +9 -2
  132. package/host-service/server.mjs +2202 -552
  133. package/host-service/service-definition-migration.mjs +10 -3
  134. package/host-service/session-collection-service.mjs +320 -0
  135. package/host-service/session-history.mjs +99 -15
  136. package/host-service/session-list-preview-service.mjs +87 -12
  137. package/host-service/session-list-projection-service.mjs +182 -0
  138. package/host-service/session-loop-definitions.mjs +702 -75
  139. package/host-service/session-manager.mjs +473 -38
  140. package/host-service/session-switcher-projection.mjs +11 -3
  141. package/host-service/session-tags.mjs +451 -0
  142. package/host-service/session-transcript-target.mjs +103 -0
  143. package/host-service/setup-input-file.mjs +91 -0
  144. package/host-service/setup-provider-model-defaults.mjs +53 -0
  145. package/host-service/setup.mjs +133 -101
  146. package/host-service/stopped-user-host-package.mjs +60 -0
  147. package/host-service/test-fixtures/p236-navigation-original.mjs +500 -0
  148. package/host-service/update-reminder-state.mjs +7 -0
  149. package/host-service/voice-input-receipt-store.mjs +60 -0
  150. package/package.json +1 -1
  151. package/static/oysterun-social-preview.png +0 -0
  152. package/static/site/assets/oysterun-social-preview.png +0 -0
  153. package/static/site/blog/en/index.html +17 -0
  154. package/static/site/blog/generated_manifest.json +9 -9
  155. package/static/site/blog/index.html +18 -0
  156. package/static/site/blog/voice-input-model-experiment/en/index.html +17 -0
  157. package/static/site/blog/voice-input-model-experiment/zh/index.html +17 -0
  158. package/static/site/blog/zh/index.html +17 -0
  159. package/static/site/docs/choose-and-recover-a-provider/en/index.html +17 -0
  160. package/static/site/docs/choose-and-recover-a-provider/zh/index.html +17 -0
  161. package/static/site/docs/configure-session-files-and-settings/en/index.html +17 -0
  162. package/static/site/docs/configure-session-files-and-settings/zh/index.html +17 -0
  163. package/static/site/docs/en/index.html +17 -0
  164. package/static/site/docs/generated_manifest.json +60 -60
  165. package/static/site/docs/host-authentication-and-connection/en/index.html +17 -0
  166. package/static/site/docs/host-authentication-and-connection/zh/index.html +17 -0
  167. package/static/site/docs/index.html +18 -0
  168. package/static/site/docs/install-and-use-a-skill/en/index.html +17 -0
  169. package/static/site/docs/install-and-use-a-skill/zh/index.html +17 -0
  170. package/static/site/docs/read-large-tool-results/en/index.html +17 -0
  171. package/static/site/docs/read-large-tool-results/zh/index.html +17 -0
  172. package/static/site/docs/recover-from-a-host-runtime-error/en/index.html +17 -0
  173. package/static/site/docs/recover-from-a-host-runtime-error/zh/index.html +17 -0
  174. package/static/site/docs/restore-work-after-a-host-restart/en/index.html +17 -0
  175. package/static/site/docs/restore-work-after-a-host-restart/zh/index.html +17 -0
  176. package/static/site/docs/send-images-and-media/en/index.html +17 -0
  177. package/static/site/docs/send-images-and-media/zh/index.html +17 -0
  178. package/static/site/docs/set-up-and-understand-a-host/en/index.html +17 -0
  179. package/static/site/docs/set-up-and-understand-a-host/zh/index.html +17 -0
  180. package/static/site/docs/understand-agent-activity/en/index.html +19 -2
  181. package/static/site/docs/understand-agent-activity/en/index.md +2 -2
  182. package/static/site/docs/understand-agent-activity/zh/index.html +19 -2
  183. package/static/site/docs/understand-agent-activity/zh/index.md +2 -2
  184. package/static/site/docs/use-the-oysterun-cli-safely/en/index.html +17 -0
  185. package/static/site/docs/use-the-oysterun-cli-safely/zh/index.html +17 -0
  186. package/static/site/docs/zh/index.html +17 -0
  187. package/static/site/en/index.html +18 -1
  188. package/static/site/en/index.md +1 -1
  189. package/static/site/faq/en/index.html +17 -0
  190. package/static/site/faq/index.html +18 -0
  191. package/static/site/faq/zh/index.html +17 -0
  192. package/static/site/getting-started/en/index.html +23 -1
  193. package/static/site/getting-started/en/index.md +7 -1
  194. package/static/site/getting-started/index.html +18 -0
  195. package/static/site/getting-started/zh/index.html +23 -1
  196. package/static/site/getting-started/zh/index.md +7 -1
  197. package/static/site/index.html +18 -0
  198. package/static/site/install.sh +676 -127
  199. package/static/site/overview/en/index.html +18 -1
  200. package/static/site/overview/en/index.md +2 -2
  201. package/static/site/overview/index.html +18 -0
  202. package/static/site/overview/zh/index.html +18 -1
  203. package/static/site/overview/zh/index.md +2 -2
  204. package/static/site/privacy/en/index.html +17 -0
  205. package/static/site/privacy/index.html +18 -0
  206. package/static/site/privacy/zh/index.html +17 -0
  207. package/static/site/zh/index.html +18 -1
  208. package/static/site/zh/index.md +1 -1
  209. package/static/site_source/content/tutorials/published/understand-agent-activity/en/article.html +2 -2
  210. package/static/site_source/content/tutorials/published/understand-agent-activity/zh/article.html +2 -2
  211. package/static/site_source/downloads/install.sh +676 -127
  212. package/static/site_source/manifests/host_install.json +2 -10
  213. package/static/site_source/pages/getting-started/en.html +6 -1
  214. package/static/site_source/pages/getting-started/zh.html +6 -1
  215. package/static/site_source/pages/home/en.html +1 -1
  216. package/static/site_source/pages/home/zh.html +1 -1
  217. package/static/site_source/pages/home_main.html +3 -2
  218. package/static/site_source/pages/overview/en.html +1 -1
  219. package/static/site_source/pages/overview/zh.html +1 -1
  220. package/tool_scripts/install_oysterun_update.mjs +78 -13
  221. package/tool_scripts/oysterun_continuity_transaction.mjs +438 -0
  222. package/tool_scripts/oysterun_npm_package_manifest.mjs +32 -0
  223. package/tool_scripts/oysterun_release_service.sh +5 -0
  224. package/tool_scripts/oysterun_service_common.sh +59 -24
  225. package/tool_scripts/oysterun_setup_input.sh +97 -0
  226. package/tool_scripts/oysterun_update_restart_runner.mjs +19 -3
  227. package/tool_scripts/p233_legacy_bootstrap.mjs +254 -0
  228. package/tool_scripts/resolve_user_host_port.mjs +15 -0
  229. package/tool_scripts/restart_oysterun.sh +8 -2
  230. package/tool_scripts/stack_dashboard_credentials.mjs +2 -1
  231. package/tool_scripts/status_oysterun_launch_agents.sh +13 -1
  232. package/tool_scripts/sync_host_stack_config.mjs +9 -6
  233. package/tool_scripts/test_environment_remote_cloud_deprovision.mjs +419 -0
  234. package/tool_scripts/uninstall_oysterun_launch_agents.sh +3 -5
  235. package/dev/client/web-chat/dist/assets/index-Ce9NZBZf.js +0 -129
  236. package/dev/client/web-chat/dist/assets/index-Ce9NZBZf.js.map +0 -1
  237. package/dev/client/web-chat/dist/assets/index-DzIClZ3T.js +0 -2
  238. package/dev/client/web-chat/dist/assets/index-DzIClZ3T.js.map +0 -1
  239. package/dev/client/web-chat/dist/assets/index-NYnaSGBb.css +0 -1
  240. package/dev/client/web-chat/dist/assets/pdf-ZjtalfgI.js.map +0 -1
  241. package/dev/client/web-chat/dist/assets/web-BzHcZK2M.js +0 -1
  242. package/dev/client/web-chat/dist/assets/web-c70YaskK.js +0 -1
@@ -0,0 +1,40 @@
1
+ // Discovery-only POSIX group anchor. Never accept a PID or group number from
2
+ // the parent. The guardian remains a member until its final group signal.
3
+ import { spawn } from 'node:child_process';
4
+ let started = false;
5
+ let stopping = false;
6
+ let deadline;
7
+ const send = (message) => {
8
+ if (process.connected) process.send(message, () => {});
9
+ };
10
+ process.on('SIGTERM', () => {});
11
+ function cleanup() {
12
+ if (stopping) return;
13
+ stopping = true;
14
+ clearTimeout(deadline);
15
+ try {
16
+ process.kill(0, 'SIGTERM');
17
+ setTimeout(() => process.kill(0, 'SIGKILL'), 300);
18
+ } catch (error) {
19
+ send({ type: 'control_error', code: error.code });
20
+ process.exit(1);
21
+ }
22
+ }
23
+ process.on('disconnect', cleanup);
24
+ process.on('message', (message) => {
25
+ if (message?.type === 'cleanup') return cleanup();
26
+ if (message?.type !== 'start' || started || stopping) return;
27
+ started = true;
28
+ clearTimeout(deadline);
29
+ try {
30
+ const child = spawn(message.command, message.args, { stdio: [0, 1, 2] });
31
+ child.once('spawn', () => send({ type: 'root_spawn' }));
32
+ child.once('error', (error) => send({ type: 'root_error', code: error.code, message: error.message }));
33
+ child.once('exit', (code, signal) => send({ type: 'root_exit', code, signal }));
34
+ } catch (error) {
35
+ send({ type: 'root_error', code: error.code, message: error.message });
36
+ }
37
+ });
38
+ // No provider is launched before the parent has established the channel.
39
+ deadline = setTimeout(cleanup, 5000);
40
+ send({ type: 'ready' });
@@ -0,0 +1,141 @@
1
+ import { spawn, execFileSync } from 'node:child_process';
2
+ import { EventEmitter } from 'node:events';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ // Read-only fixture diagnostics. Process-table rows are never signal authority.
6
+ export function readDiscoveryProcessTable() {
7
+ return execFileSync('ps', ['-axo', 'pid=,ppid=,pgid=,lstart=,stat='], {
8
+ encoding: 'utf8', env: { ...process.env, LC_ALL: 'C' },
9
+ }).trim().split('\n').filter(Boolean).map((line) => {
10
+ const m = line.trim().match(/^(\d+)\s+(\d+)\s+(\d+)\s+(.{24})\s+(\S+)/);
11
+ if (!m) throw new Error('model_discovery_process_identity_unavailable');
12
+ return { pid: +m[1], ppid: +m[2], group: +m[3], generation: m[4], state: m[5] };
13
+ });
14
+ }
15
+ const owners = new WeakMap();
16
+ function incomplete(reason, extra = {}) {
17
+ const error = new Error('Model discovery cleanup cannot prove complete descendant absence');
18
+ error.code = 'model_discovery_cleanup_failed';
19
+ error.details = { process_cleanup: { status: 'unknown', proof_scope: 'anchored_process_group_only', reason, survivor_count: null, ...extra } };
20
+ return error;
21
+ }
22
+
23
+ export function ownDiscoveryProcess(proc) {
24
+ return owners.get(proc) || { cleanup: async () => { throw incomplete('no_guardian_authority'); } };
25
+ }
26
+
27
+ export function spawnDiscoveryProcess(command, args, options = {}, transport = spawn) {
28
+ const { signal, ...spawnOptions } = options;
29
+ const guardian = transport(process.execPath, [fileURLToPath(new URL('./model-discovery-guardian.mjs', import.meta.url))], {
30
+ ...spawnOptions, detached: true, stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
31
+ });
32
+ const proc = new EventEmitter();
33
+ // Provider bytes never pass through IPC and the provider does not inherit
34
+ // the private control descriptor. Root lifecycle is forwarded separately.
35
+ Object.assign(proc, { stdin: guardian.stdin, stdout: guardian.stdout, stderr: guardian.stderr });
36
+ let exited = false, ready = false, lost = false, rootSpawned = false, rootFailed = false;
37
+ let closing = false, terminal = false, controlFailed = false;
38
+ let cleanupPromise, resolveExit;
39
+ const exit = new Promise((resolve) => { resolveExit = resolve; });
40
+ const emitError = (error) => {
41
+ if (terminal) return;
42
+ terminal = true;
43
+ clearTimeout(startup);
44
+ proc.emit('error', error);
45
+ };
46
+ const emitExit = (code, exitSignal) => {
47
+ if (terminal) return;
48
+ terminal = true;
49
+ proc.emit('exit', code, exitSignal);
50
+ };
51
+ const controlLost = (cause) => {
52
+ const unexpected = !closing && !terminal;
53
+ controlFailed ||= unexpected || Boolean(cause);
54
+ lost = true;
55
+ closing = true;
56
+ if (unexpected) {
57
+ const error = new Error('Model discovery guardian control lost', { cause });
58
+ error.code = 'model_discovery_guardian_control_lost';
59
+ emitError(error);
60
+ }
61
+ // The guardian's disconnect handler owns shutdown; never use its PID.
62
+ if (guardian.connected) guardian.disconnect();
63
+ };
64
+ const send = (message) => {
65
+ if (exited || lost || !guardian.connected) return false;
66
+ try {
67
+ guardian.send(message, (error) => { if (error) controlLost(error); });
68
+ return !lost;
69
+ } catch (error) { controlLost(error); return false; }
70
+ };
71
+ const abort = () => {
72
+ closing = true;
73
+ send({ type: 'cleanup' });
74
+ const error = new Error('Model discovery aborted'); error.name = 'AbortError'; error.code = 'ABORT_ERR';
75
+ emitError(error);
76
+ };
77
+ const startup = setTimeout(() => {
78
+ closing = true;
79
+ send({ type: 'cleanup' });
80
+ const error = new Error('Model discovery guardian startup timed out');
81
+ error.code = 'model_discovery_guardian_startup_timeout';
82
+ emitError(error);
83
+ }, 5000);
84
+ guardian.on('message', (message) => {
85
+ if (message?.type === 'ready' && !ready) {
86
+ ready = true; clearTimeout(startup);
87
+ if (closing || terminal || lost || exited) return;
88
+ if (signal?.aborted) return abort();
89
+ send({ type: 'start', command, args });
90
+ } else if (message?.type === 'root_spawn') rootSpawned = true;
91
+ else if (message?.type === 'root_error') {
92
+ rootFailed = true;
93
+ const error = new Error(message.message); error.code = message.code; emitError(error);
94
+ } else if (message?.type === 'root_exit') emitExit(message.code, message.signal);
95
+ else if (message?.type === 'control_error') controlLost(new Error('Guardian shutdown failed'));
96
+ });
97
+ guardian.once('error', (error) => { lost = true; controlFailed = true; emitError(error); });
98
+ guardian.once('disconnect', () => { if (!lost) controlLost(); });
99
+ for (const stream of [proc.stdin, proc.stdout, proc.stderr]) {
100
+ stream?.on('error', emitError);
101
+ }
102
+ guardian.once('exit', (code, exitSignal) => {
103
+ exited = true; clearTimeout(startup); signal?.removeEventListener('abort', abort);
104
+ resolveExit({ code, signal: exitSignal });
105
+ if (!closing) emitExit(code, exitSignal);
106
+ });
107
+ signal?.addEventListener('abort', abort, { once: true });
108
+ owners.set(proc, { cleanup() {
109
+ return cleanupPromise ||= (async () => {
110
+ closing = true;
111
+ const requested = send({ type: 'cleanup' });
112
+ proc.stdin?.end();
113
+ let timer;
114
+ const ended = await Promise.race([exit, new Promise((resolve) => { timer = setTimeout(() => resolve(null), 2000); })]);
115
+ clearTimeout(timer); clearTimeout(startup); signal?.removeEventListener('abort', abort);
116
+ if (!ended) {
117
+ // Disconnect triggers the guardian's own shutdown. Never fall back to
118
+ // guardian.kill(), process.kill(savedPid), or a sampled process table.
119
+ if (guardian.connected) guardian.disconnect();
120
+ throw incomplete('guardian_shutdown_unconfirmed');
121
+ }
122
+ if (rootFailed && !rootSpawned && requested && !controlFailed && ended.signal === 'SIGKILL') return { status: 'not_spawned', proof_scope: 'provider_spawn_failed' };
123
+ throw incomplete(requested && !controlFailed && ended.signal === 'SIGKILL' ? 'detached_descendants_unprovable' : 'guardian_control_lost', {
124
+ guardian_reaped: true, group_shutdown_requested: requested && !controlFailed, root_spawned: rootSpawned,
125
+ });
126
+ })();
127
+ } });
128
+ return proc;
129
+ }
130
+
131
+ // Catalog/protocol outcome and cleanup evidence are independent. Callers attach
132
+ // this bounded diagnostic to either their valid result or original error.
133
+ export async function collectDiscoveryCleanup(owner) {
134
+ try { return await owner.cleanup(); }
135
+ catch (error) {
136
+ return error.details?.process_cleanup || {
137
+ status: 'failed', proof_scope: 'unknown',
138
+ reason: 'cleanup_diagnostic_unavailable', survivor_count: null,
139
+ };
140
+ }
141
+ }
@@ -0,0 +1,28 @@
1
+ function normalizeString(value) {
2
+ return typeof value === "string" && value.trim() ? value.trim() : null;
3
+ }
4
+
5
+ export function resolveOnboardLifecycleObservation(payload, expectedOperationId) {
6
+ const operation =
7
+ payload?.update_operation && typeof payload.update_operation === "object"
8
+ ? payload.update_operation
9
+ : payload?.operation && typeof payload.operation === "object"
10
+ ? payload.operation
11
+ : null;
12
+ const operationId = normalizeString(
13
+ operation?.operation_id || payload?.operation_id
14
+ );
15
+ if (operationId && operationId !== expectedOperationId) {
16
+ throw new Error(
17
+ `another lifecycle operation replaced ${expectedOperationId}: ${operationId}`
18
+ );
19
+ }
20
+ return {
21
+ operation_id: operationId,
22
+ status:
23
+ normalizeString(operation?.status) ||
24
+ normalizeString(payload?.operation_status) ||
25
+ normalizeString(payload?.status) ||
26
+ "unknown",
27
+ };
28
+ }
@@ -0,0 +1,479 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readSetupInputFile } from "./setup-input-file.mjs";
4
+
5
+ import { spawnSync } from "node:child_process";
6
+ import { existsSync } from "node:fs";
7
+ import { dirname, join, resolve } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ import {
11
+ buildCloudApiUrl,
12
+ getConfigPath,
13
+ readCloudIdentity,
14
+ readConfig,
15
+ readRawConfigSource,
16
+ resolveCloudBackendStage,
17
+ resolveCloudBackendUrl,
18
+ writeConfig,
19
+ } from "./config.mjs";
20
+ import { resolveOnboardLifecycleObservation } from "./onboard-lifecycle-status.mjs";
21
+
22
+ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
23
+ const setupScript =
24
+ process.env.OYSTERUN_ONBOARD_SETUP_SCRIPT ||
25
+ join(repoRoot, "host-service", "setup.mjs");
26
+ const releaseServiceScript =
27
+ process.env.OYSTERUN_ONBOARD_RELEASE_SERVICE_SCRIPT ||
28
+ join(repoRoot, "tool_scripts", "oysterun_release_service.sh");
29
+ const stagedRecoveryScript =
30
+ process.env.OYSTERUN_ONBOARD_STAGED_RECOVERY_SCRIPT ||
31
+ join(repoRoot, "host-service", "cli", "run-staged-lifecycle-recovery.mjs");
32
+
33
+ const SUCCESSFUL_LIFECYCLE_STATUSES = new Set([
34
+ "restart_complete",
35
+ "update_complete",
36
+ ]);
37
+ const FAILED_LIFECYCLE_STATUSES = new Set([
38
+ "update_recovery_required",
39
+ "restart_recovery_required",
40
+ "native_abi_mismatch",
41
+ "runtime_validation_failed",
42
+ "config_compatibility_failed",
43
+ "activation_failed",
44
+ "preparation_failed",
45
+ "staging_install_failed",
46
+ "restart_schedule_failed",
47
+ "post_restart_health_failed",
48
+ "rollback_failed",
49
+ "failed",
50
+ "install_failed",
51
+ "version_mismatch",
52
+ ]);
53
+
54
+ function normalizeString(value) {
55
+ return typeof value === "string" && value.trim() ? value.trim() : null;
56
+ }
57
+
58
+ function parseArgs(argv) {
59
+ const options = {
60
+ packageChanged: false,
61
+ updateRequired: false,
62
+ repairRequired: false,
63
+ channel: "latest",
64
+ };
65
+ for (let index = 0; index < argv.length; index += 1) {
66
+ const arg = argv[index];
67
+ if (arg === "--package-changed") options.packageChanged = true;
68
+ else if (arg === "--update-required") options.updateRequired = true;
69
+ else if (arg === "--repair-required") options.repairRequired = true;
70
+ else if (arg === "--setup-input-file") {
71
+ options.setupInputFile=argv[++index];
72
+ if (!options.setupInputFile || options.setupInputFile.startsWith("--")) throw new Error("--setup-input-file requires an absolute path");
73
+ }
74
+ else if (arg === "--channel") options.channel = argv[++index] || "";
75
+ else throw new Error(`unknown onboard argument: ${arg}`);
76
+ }
77
+ if (
78
+ !new Set(["latest", "beta"]).has(options.channel) &&
79
+ !/^exact:[0-9A-Za-z.+-]+$/.test(options.channel)
80
+ ) {
81
+ throw new Error("--channel must be latest, beta, or exact:<version>");
82
+ }
83
+ if (options.updateRequired && options.repairRequired) {
84
+ throw new Error("--update-required and --repair-required cannot be combined");
85
+ }
86
+ return options;
87
+ }
88
+
89
+ function run(command, args, { capture = false, allowFailure = false, setupInputDescriptor } = {}) {
90
+ const childEnv = { ...process.env };
91
+ delete childEnv.OYSTERUN_SETUP_INPUT_FD;
92
+ delete childEnv.OYSTERUN_SETUP_INPUT_NEXT_FD;
93
+ if (setupInputDescriptor !== undefined) childEnv.OYSTERUN_SETUP_INPUT_FD = "3";
94
+ const result = spawnSync(command, args, {
95
+ cwd: repoRoot,
96
+ env: {
97
+ ...childEnv,
98
+ OYSTERUN_BOOTSTRAP_NODE_BIN: process.execPath,
99
+ OYSTERUN_NODE_BIN: process.execPath,
100
+ PATH: `${dirname(process.execPath)}:${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}`,
101
+ },
102
+ encoding: capture ? "utf8" : undefined,
103
+ stdio: setupInputDescriptor !== undefined ? ["inherit", "inherit", "inherit", setupInputDescriptor] : capture ? ["ignore", "pipe", "pipe"] : "inherit",
104
+ });
105
+ if (result.error) throw result.error;
106
+ if (!allowFailure && (result.status ?? 1) !== 0) {
107
+ const detail = capture
108
+ ? String(result.stderr || result.stdout || "").trim()
109
+ : "";
110
+ throw new Error(
111
+ `command failed (${result.status ?? 1}): ${command}${
112
+ detail ? `: ${detail}` : ""
113
+ }`
114
+ );
115
+ }
116
+ return result;
117
+ }
118
+
119
+ function service(command, args = [], options = {}) {
120
+ return run(releaseServiceScript, [command, ...args], options);
121
+ }
122
+
123
+ function parseKeyValueStatus(output) {
124
+ const status = {};
125
+ for (const line of String(output || "").split(/\r?\n/)) {
126
+ const match = line.match(/^([a-z_]+)=(.*)$/);
127
+ if (match) status[match[1]] = match[2].trim();
128
+ }
129
+ return status;
130
+ }
131
+
132
+ function readServiceStatus() {
133
+ const result = service("status", [], { capture: true, allowFailure: true });
134
+ if ((result.status ?? 1) !== 0) {
135
+ return {
136
+ available: false,
137
+ healthy: false,
138
+ running: false,
139
+ managed: false,
140
+ authenticated: false,
141
+ raw: {},
142
+ };
143
+ }
144
+ const raw = parseKeyValueStatus(result.stdout);
145
+ const pid = raw.pid && raw.pid !== "-" ? raw.pid : null;
146
+ const healthy = raw.health === "yes";
147
+ const managed = raw.loaded === "yes" && Boolean(pid);
148
+ let authenticated = false;
149
+
150
+ // A Host installed by an older Oysterun release can still be healthy while
151
+ // its process is absent from the current service manager's PID/label view.
152
+ // Prove that such a listener is this user's Host through the loopback-only
153
+ // service-control token before treating it as running. An unrelated process
154
+ // with a /health endpoint remains untrusted and is never stopped or adopted.
155
+ if (healthy && !managed) {
156
+ const control = service("lifecycle-status", [], {
157
+ capture: true,
158
+ allowFailure: true,
159
+ });
160
+ authenticated =
161
+ (control.status ?? 1) === 0 &&
162
+ Boolean(parseJsonOutput(control.stdout));
163
+ }
164
+ return {
165
+ available: true,
166
+ healthy,
167
+ running: managed || authenticated,
168
+ managed,
169
+ authenticated,
170
+ raw,
171
+ };
172
+ }
173
+
174
+ function readLifecycleStatus() {
175
+ const result = service("lifecycle-status", [], {
176
+ capture: true,
177
+ allowFailure: true,
178
+ });
179
+ return (result.status ?? 1) === 0 ? parseJsonOutput(result.stdout) : null;
180
+ }
181
+
182
+ function recoverRetainedLifecycleWithStagedDriver() {
183
+ run(process.execPath, [stagedRecoveryScript]);
184
+ }
185
+
186
+ function isConfigReady(raw, identity) {
187
+ const port = Number(raw.port);
188
+ const common =
189
+ Number.isInteger(port) &&
190
+ port > 0 &&
191
+ normalizeString(raw.dashboard_password_hash) &&
192
+ normalizeString(raw.dashboard_user) &&
193
+ normalizeString(raw.default_browse_path);
194
+ if (!common) return false;
195
+ if (raw.connection_mode === "direct") {
196
+ return Boolean(
197
+ normalizeString(raw.direct_host_url) ||
198
+ normalizeString(raw.public_base_url)
199
+ );
200
+ }
201
+ return Boolean(
202
+ normalizeString(raw.managed_endpoint) &&
203
+ normalizeString(identity.device_id || identity.host_id) &&
204
+ normalizeString(identity.device_token || identity.host_credential)
205
+ );
206
+ }
207
+
208
+ function parseJsonOutput(value) {
209
+ try {
210
+ return JSON.parse(String(value || ""));
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+
216
+ function wait(ms) {
217
+ return new Promise((resolveWait) => setTimeout(resolveWait, ms));
218
+ }
219
+
220
+ async function waitForLifecycle(operationId) {
221
+ const timeoutMs = Number(
222
+ process.env.OYSTERUN_ONBOARD_LIFECYCLE_TIMEOUT_MS || 180_000
223
+ );
224
+ const pollMs = Number(process.env.OYSTERUN_ONBOARD_POLL_MS || 1_000);
225
+ const deadline = Date.now() + timeoutMs;
226
+ let lastStatus = "unavailable";
227
+ while (Date.now() < deadline) {
228
+ const result = service("lifecycle-status", [], {
229
+ capture: true,
230
+ allowFailure: true,
231
+ });
232
+ if ((result.status ?? 1) === 0) {
233
+ const payload = parseJsonOutput(result.stdout);
234
+ if (payload) {
235
+ const observation = resolveOnboardLifecycleObservation(
236
+ payload,
237
+ operationId
238
+ );
239
+ lastStatus = observation.status;
240
+ if (SUCCESSFUL_LIFECYCLE_STATUSES.has(lastStatus)) return payload;
241
+ if (FAILED_LIFECYCLE_STATUSES.has(lastStatus)) {
242
+ throw new Error(
243
+ payload.safe_message ||
244
+ `Host lifecycle did not complete successfully (${lastStatus})`
245
+ );
246
+ }
247
+ }
248
+ }
249
+ await wait(pollMs);
250
+ }
251
+ throw new Error(
252
+ `timed out waiting for Host lifecycle ${operationId} (last status: ${lastStatus})`
253
+ );
254
+ }
255
+
256
+ async function restartThroughLifecycle() {
257
+ const result = service("restart", ["--restore-sessions"], {
258
+ capture: true,
259
+ });
260
+ const payload = parseJsonOutput(result.stdout);
261
+ const operationId = normalizeString(payload?.operation_id);
262
+ if (!operationId) {
263
+ throw new Error("Host lifecycle restart did not return an operation ID");
264
+ }
265
+ console.log(` Host lifecycle operation: ${operationId}`);
266
+ await waitForLifecycle(operationId);
267
+ }
268
+
269
+ async function runLifecycleMutation(command, args = []) {
270
+ const result = service(command, args, { capture: true });
271
+ const payload = parseJsonOutput(result.stdout);
272
+ const operationId = normalizeString(payload?.operation_id);
273
+ if (!operationId) {
274
+ throw new Error(`Host lifecycle ${command} did not return an operation ID`);
275
+ }
276
+ console.log(` Host lifecycle operation: ${operationId}`);
277
+ await waitForLifecycle(operationId);
278
+ }
279
+
280
+ async function waitForLocalHealth() {
281
+ const timeoutMs = Number(process.env.OYSTERUN_ONBOARD_HEALTH_TIMEOUT_MS || 90_000);
282
+ const pollMs = Number(process.env.OYSTERUN_ONBOARD_POLL_MS || 1_000);
283
+ const deadline = Date.now() + timeoutMs;
284
+ while (Date.now() < deadline) {
285
+ const status = readServiceStatus();
286
+ if (status.healthy && status.running) return status;
287
+ await wait(pollMs);
288
+ }
289
+ throw new Error("Oysterun Host did not become healthy before the onboarding timeout");
290
+ }
291
+
292
+ async function verifyManagedRoute(config, identity) {
293
+ if (config.connection_mode === "direct") return;
294
+ const endpoint = normalizeString(config.managed_endpoint);
295
+ const token = normalizeString(identity.device_token || identity.host_credential);
296
+ if (!endpoint || !token) {
297
+ throw new Error("Managed Host identity or endpoint is incomplete");
298
+ }
299
+ const backendUrl = resolveCloudBackendUrl(config);
300
+ const backendStage = resolveCloudBackendStage(config);
301
+ const timeoutMs = Number(process.env.OYSTERUN_ONBOARD_ROUTE_TIMEOUT_MS || 90_000);
302
+ const pollMs = Number(process.env.OYSTERUN_ONBOARD_POLL_MS || 1_000);
303
+ const deadline = Date.now() + timeoutMs;
304
+ let lastReason = "Managed Endpoint status was unavailable";
305
+ while (Date.now() < deadline) {
306
+ try {
307
+ const response = await fetch(
308
+ buildCloudApiUrl(
309
+ backendUrl,
310
+ "/api/connectivity/host/route",
311
+ backendStage
312
+ ),
313
+ {
314
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
315
+ signal: AbortSignal.timeout(Math.min(10_000, Math.max(1, deadline - Date.now()))),
316
+ }
317
+ );
318
+ if (response.ok) {
319
+ const route = await response.json();
320
+ if (
321
+ route?.healthy === true &&
322
+ route?.status === "healthy" &&
323
+ normalizeString(route?.managed_endpoint) === endpoint
324
+ ) {
325
+ const publicHealth = await fetch(`${endpoint.replace(/\/+$/, "")}/health`, {
326
+ headers: { Accept: "application/json" },
327
+ signal: AbortSignal.timeout(Math.min(10_000, Math.max(1, deadline - Date.now()))),
328
+ });
329
+ if (publicHealth.ok) return;
330
+ lastReason = `Managed Endpoint health returned HTTP ${publicHealth.status}`;
331
+ } else {
332
+ lastReason = `Managed route status is ${route?.status || "unknown"}`;
333
+ }
334
+ } else {
335
+ lastReason = `Managed route verification returned HTTP ${response.status}`;
336
+ }
337
+ } catch (error) {
338
+ lastReason = error?.message || String(error);
339
+ }
340
+ await wait(pollMs);
341
+ }
342
+ throw new Error(`${lastReason}; onboarding is resumable with the same install command`);
343
+ }
344
+
345
+ function runSetup({ deferServiceActivation = false, setupInputFile } = {}) {
346
+ run(process.execPath, [
347
+ setupScript,
348
+ "--release-setup",
349
+ ...(setupInputFile ? ["--setup-input-file",setupInputFile] : []),
350
+ ...(deferServiceActivation ? ["--defer-service-activation"] : []),
351
+ ], { setupInputDescriptor: process.env.OYSTERUN_SETUP_INPUT_FD !== undefined ? Number(process.env.OYSTERUN_SETUP_INPUT_NEXT_FD) : undefined });
352
+ }
353
+
354
+ async function main() {
355
+ const options = parseArgs(process.argv.slice(2));
356
+ const configPath = getConfigPath();
357
+ let raw = existsSync(configPath) ? readRawConfigSource() : {};
358
+ let identity = readCloudIdentity();
359
+ const hadReadyConfig = isConfigReady(raw, identity);
360
+ if (!hadReadyConfig || options.setupInputFile || process.env.OYSTERUN_SETUP_INPUT_FD !== undefined) readSetupInputFile({file: options.setupInputFile, configDir: dirname(configPath), unattended: !process.stdin.isTTY});
361
+ if (process.env.OYSTERUN_SETUP_INPUT_FD !== undefined && !/^\d+$/.test(process.env.OYSTERUN_SETUP_INPUT_NEXT_FD || "")) throw new Error("Missing internal setup input handoff descriptor");
362
+ const initialService = readServiceStatus();
363
+ const setupActivationDeferred =
364
+ !hadReadyConfig && initialService.healthy && initialService.running;
365
+
366
+ if (!hadReadyConfig) {
367
+ console.log("Completing Oysterun Host configuration...");
368
+ runSetup({ deferServiceActivation: setupActivationDeferred, setupInputFile: options.setupInputFile });
369
+ raw = readRawConfigSource();
370
+ identity = readCloudIdentity();
371
+ if (!isConfigReady(raw, identity)) {
372
+ throw new Error("Host setup returned without a complete configuration and identity");
373
+ }
374
+ } else {
375
+ console.log(`Existing Oysterun Host found at ${configPath}; preserving it.`);
376
+ }
377
+
378
+ let before = readServiceStatus();
379
+ let retainedLifecycle =
380
+ before.healthy && before.running
381
+ ? readLifecycleStatus()
382
+ : null;
383
+ if (
384
+ options.updateRequired &&
385
+ retainedLifecycle?.blocking === true &&
386
+ retainedLifecycle?.next_safe_action === "restore_and_restart"
387
+ ) {
388
+ console.log(
389
+ "Restoring the retained last working Host with the verified staged lifecycle driver..."
390
+ );
391
+ recoverRetainedLifecycleWithStagedDriver();
392
+ await waitForLocalHealth();
393
+ before = readServiceStatus();
394
+ retainedLifecycle =
395
+ before.healthy && before.running ? readLifecycleStatus() : null;
396
+ }
397
+ if (
398
+ before.healthy &&
399
+ before.running &&
400
+ !options.updateRequired &&
401
+ !options.repairRequired &&
402
+ !options.packageChanged &&
403
+ !setupActivationDeferred &&
404
+ retainedLifecycle?.blocking === true
405
+ ) {
406
+ throw new Error(
407
+ "An Oysterun lifecycle transaction is still blocking onboarding. Run oysterun service:lifecycle-status and follow that operation's reported safe next action, or wait for its completion before rerunning this install command."
408
+ );
409
+ }
410
+ if (
411
+ (hadReadyConfig || setupActivationDeferred) &&
412
+ options.updateRequired &&
413
+ before.healthy &&
414
+ before.running
415
+ ) {
416
+ if (!new Set(["latest", "beta"]).has(options.channel)) {
417
+ throw new Error("a running Host update requires a latest or beta channel");
418
+ }
419
+ console.log(`Updating through the ${options.channel} restore-aware Host lifecycle...`);
420
+ await runLifecycleMutation("update", ["--channel", options.channel]);
421
+ } else if (
422
+ (hadReadyConfig || setupActivationDeferred) &&
423
+ options.repairRequired &&
424
+ before.healthy &&
425
+ before.running
426
+ ) {
427
+ console.log("Repairing through the restore-aware Host lifecycle...");
428
+ await runLifecycleMutation("repair");
429
+ } else if (
430
+ (hadReadyConfig || setupActivationDeferred) &&
431
+ options.packageChanged &&
432
+ before.healthy &&
433
+ before.running
434
+ ) {
435
+ console.log("Applying the installed package through the restore-aware Host lifecycle...");
436
+ await restartThroughLifecycle();
437
+ } else if (setupActivationDeferred && before.healthy && before.running) {
438
+ console.log("Applying completed Host configuration through the restore-aware lifecycle...");
439
+ await restartThroughLifecycle();
440
+ } else if (!before.healthy || !before.running) {
441
+ console.log("Starting the Oysterun Host service...");
442
+ const runtimeIdentityExists = existsSync(
443
+ join(dirname(configPath), "node-npm-runtime-identity.json")
444
+ );
445
+ service(
446
+ options.packageChanged && !before.running
447
+ ? "install"
448
+ : before.raw.loaded === "yes" || runtimeIdentityExists
449
+ ? "start"
450
+ : "install"
451
+ );
452
+ }
453
+
454
+ await waitForLocalHealth();
455
+ const config = readConfig();
456
+ identity = readCloudIdentity();
457
+ await verifyManagedRoute(config, identity);
458
+ writeConfig({ oysterun_update_channel: options.channel });
459
+
460
+ console.log("\nOysterun is ready.");
461
+ console.log(`Config: ${configPath}`);
462
+ console.log(
463
+ `${config.connection_mode === "direct" ? "Direct Host" : "Managed Host"}: ${
464
+ config.connection_mode === "direct"
465
+ ? config.direct_host_url || config.public_base_url
466
+ : config.managed_endpoint
467
+ }`
468
+ );
469
+ console.log(
470
+ hadReadyConfig
471
+ ? "Existing Cloud identity and settings were preserved."
472
+ : "Host configuration, Cloud identity, service health, and endpoint were verified."
473
+ );
474
+ }
475
+
476
+ main().catch((error) => {
477
+ console.error(`Oysterun onboarding failed: ${error.message}`);
478
+ process.exitCode = 1;
479
+ });