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.
- package/bin/oysterun.mjs +64 -17
- package/dev/client/connectivity/completed-message-neighbor-click.mjs +78 -0
- package/dev/client/connectivity/completed-message-neighbor-contract.mjs +205 -0
- package/dev/client/connectivity/completed-message-visibility.d.mts +4 -4
- package/dev/client/connectivity/completed-message-visibility.mjs +14 -11
- package/dev/client/connectivity/desktop-chat-sidebar-preference.mjs +57 -0
- package/dev/client/connectivity/host-connection-feedback.mjs +12 -1
- package/dev/client/connectivity/managed-host-browser.mjs +10 -0
- package/dev/client/connectivity/session-navigation-intent.mjs +189 -0
- package/dev/client/connectivity/session-transition-state.mjs +18 -6
- package/dev/client/connectivity/test-completed-message-neighbor-click.mjs +180 -0
- package/dev/client/web/index.html +5367 -609
- package/dev/client/web-chat/dist/assets/{ReactPrism-BwJhidYf.js → ReactPrism-C6YiTZTC.js} +1 -1
- package/dev/client/web-chat/dist/assets/{ReactPrism-BwJhidYf.js.map → ReactPrism-C6YiTZTC.js.map} +1 -1
- package/dev/client/web-chat/dist/assets/SessionsSetupApp-CKJOSPFP.js +6442 -0
- package/dev/client/web-chat/dist/assets/SessionsSetupApp-CKJOSPFP.js.map +1 -0
- package/dev/client/web-chat/dist/assets/SessionsSetupApp-Dx09Uz-6.css +1 -0
- package/dev/client/web-chat/dist/assets/index-Ae5I4LHp.js +9 -0
- package/dev/client/web-chat/dist/assets/index-Ae5I4LHp.js.map +1 -0
- package/dev/client/web-chat/dist/assets/index-CN_FoTB1.js +2 -0
- package/dev/client/web-chat/dist/assets/index-CN_FoTB1.js.map +1 -0
- package/dev/client/web-chat/dist/assets/{index--1xmQ99x.js → index-DCQFwgGf.js} +2 -2
- package/dev/client/web-chat/dist/assets/{index--1xmQ99x.js.map → index-DCQFwgGf.js.map} +1 -1
- package/dev/client/web-chat/dist/assets/matrix-entry-BV7G38kH.css +10 -0
- package/dev/client/web-chat/dist/assets/matrix-entry-abAy3aOo.js +594 -0
- package/dev/client/web-chat/dist/assets/matrix-entry-abAy3aOo.js.map +1 -0
- package/dev/client/web-chat/dist/assets/{pdf-ZjtalfgI.js → pdf-DN7oVvM6.js} +5 -5
- package/dev/client/web-chat/dist/assets/pdf-DN7oVvM6.js.map +1 -0
- package/dev/client/web-chat/dist/assets/session-notice-DlyODy1z.css +1 -0
- package/dev/client/web-chat/dist/assets/web-BlYuDlOV.js +1 -0
- package/dev/client/web-chat/dist/assets/{web-c70YaskK.js.map → web-BlYuDlOV.js.map} +1 -1
- package/dev/client/web-chat/dist/assets/web-Dabw6cJi.js +1 -0
- package/dev/client/web-chat/dist/assets/{web-BzHcZK2M.js.map → web-Dabw6cJi.js.map} +1 -1
- package/dev/client/web-chat/dist/index.html +1 -2
- package/docs/routec/dev/cloud_deplyments.md +5 -0
- package/docs/routec/dev/npm_native_dependency_install.md +6 -2
- package/docs/routec/dev/npmjs_publish.md +55 -19
- package/docs/routec/dev/p262_setup_input_file.md +117 -0
- package/docs/routec/host2_turn_started_idempotency_diagnostics.md +8 -7
- package/docs/routec/host_setup.md +39 -10
- package/docs/routec/oysterun_operation_interface_spec.md +78 -2
- package/docs/routec/provider_contract.md +35 -5
- package/host-service/README.md +36 -0
- package/host-service/adapters/claude-code-adapter.mjs +48 -5
- package/host-service/adapters/codex-app-server-adapter.mjs +320 -18
- package/host-service/adapters/debug-fixture-provider-adapter.mjs +102 -1
- package/host-service/adapters/debug-large-tool-spillover-provider-adapter.mjs +2 -0
- package/host-service/adapters/debug-p135-codex-replay-provider-adapter.mjs +2 -0
- package/host-service/adapters/debug-routec-structural-replay-provider-adapter.mjs +2 -0
- package/host-service/agent-config.mjs +16 -5
- package/host-service/agent-folder-availability.mjs +41 -0
- package/host-service/apns-complete-message-dispatcher.mjs +1 -0
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-find-context/SKILL.md +24 -0
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-session-chat/SKILL.md +94 -5
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-sessions/SKILL.md +103 -0
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-website/SKILL.md +7 -0
- package/host-service/assets/product-skills/oysterun-find-context/SKILL.md +24 -0
- package/host-service/assets/product-skills/oysterun-session-chat/SKILL.md +94 -5
- package/host-service/assets/product-skills/oysterun-sessions/SKILL.md +103 -0
- package/host-service/assets/product-skills/oysterun-website/SKILL.md +7 -0
- package/host-service/callback-durable-store.mjs +354 -47
- package/host-service/callback-foundation.mjs +21 -1
- package/host-service/callback-host-integration.mjs +345 -30
- package/host-service/claude-acp-model-discovery.mjs +23 -18
- package/host-service/cli/assert-installer-reinstall-target.mjs +9 -0
- package/host-service/cli/installer-recovery-guidance.mjs +20 -0
- package/host-service/cli/prepare-restart-restore.mjs +2 -0
- package/host-service/cli/product-cli.mjs +34 -33
- package/host-service/cli/reconcile-initial-preflight.mjs +19 -0
- package/host-service/cli/recover-installer-lifecycle.mjs +24 -0
- package/host-service/cli/run-host-lifecycle.mjs +2 -0
- package/host-service/cli/run-staged-lifecycle-recovery.mjs +161 -0
- package/host-service/codex-model-discovery.mjs +23 -18
- package/host-service/complete-cli-uninstall.mjs +176 -0
- package/host-service/complete-message-notification-predicate.mjs +8 -6
- package/host-service/complete-message-notification-title.mjs +4 -0
- package/host-service/completed-session-unread.mjs +425 -42
- package/host-service/completed-turn-navigation.mjs +444 -144
- package/host-service/completed-turn-notification-observer.mjs +116 -0
- package/host-service/completed-turn-service.mjs +445 -0
- package/host-service/config.mjs +115 -9
- package/host-service/desired-open-session-state.mjs +331 -0
- package/host-service/fixtures/p218-completed-turn-notification.json +11 -0
- package/host-service/frp-agent.mjs +9 -9
- package/host-service/grok-acp-model-discovery.mjs +31 -33
- package/host-service/host-lifecycle-recovery.mjs +37 -0
- package/host-service/host-port-resolution.mjs +98 -0
- package/host-service/host-runtime-diagnostics.mjs +18 -1
- package/host-service/initial-preflight-recovery.mjs +71 -0
- package/host-service/installer-recovery-guidance.mjs +85 -0
- package/host-service/installer-reinstall-bindings.mjs +49 -0
- package/host-service/installer-reinstall-target.mjs +49 -0
- package/host-service/legacy-continuity-observation.mjs +33 -0
- package/host-service/lifecycle-compat-cli.mjs +13 -0
- package/host-service/lifecycle-compat-preload.mjs +89 -0
- package/host-service/lifecycle-compatibility.mjs +164 -0
- package/host-service/lifecycle-continuity-assets.mjs +222 -0
- package/host-service/lifecycle-continuity-probe.mjs +97 -0
- package/host-service/lifecycle-continuity-projection.mjs +29 -0
- package/host-service/lifecycle-continuity-runner.mjs +192 -0
- package/host-service/lifecycle-continuity.mjs +493 -0
- package/host-service/lifecycle-service-render-bundle.mjs +120 -0
- package/host-service/lifecycle-user-path.mjs +34 -0
- package/host-service/link-annotations.mjs +10 -0
- package/host-service/matrix-credential-issuer.mjs +32 -1
- package/host-service/matrix-event-correlator.mjs +166 -27
- package/host-service/matrix-facade.mjs +96 -19
- package/host-service/matrix-room-binding.mjs +24 -0
- package/host-service/matrix-transcript-read-adapter.mjs +17 -3
- package/host-service/model-discovery-guardian.mjs +40 -0
- package/host-service/model-discovery-process.mjs +141 -0
- package/host-service/onboard-lifecycle-status.mjs +28 -0
- package/host-service/onboard.mjs +479 -0
- package/host-service/package.json +2 -1
- package/host-service/portable-scheduler-definitions.mjs +28 -0
- package/host-service/product-runtime-host-origin.mjs +22 -0
- package/host-service/provider-model-params.mjs +14 -17
- package/host-service/provider-model-refresh-runner.mjs +124 -169
- package/host-service/provider-model-refresh-schedule.mjs +11 -0
- package/host-service/provider-resume-contract.mjs +9 -3
- package/host-service/restart-restore-state.mjs +25 -0
- package/host-service/retained-host-state-holder.mjs +57 -0
- package/host-service/routec-matrix-sqlite-store.mjs +155 -10
- package/host-service/routec-matrix-storage-adapter.mjs +208 -25
- package/host-service/routec-matrix-storage-paths.mjs +12 -0
- package/host-service/routec-request-delay-window.mjs +55 -0
- package/host-service/scheduler-acp-provider-runner.mjs +2 -1
- package/host-service/scheduler-direct-provider-runner.mjs +71 -9
- package/host-service/scheduler-run-timeout-policy.mjs +85 -0
- package/host-service/scheduler-service.mjs +96 -9
- package/host-service/scheduler-store.mjs +9 -2
- package/host-service/server.mjs +2202 -552
- package/host-service/service-definition-migration.mjs +10 -3
- package/host-service/session-collection-service.mjs +320 -0
- package/host-service/session-history.mjs +99 -15
- package/host-service/session-list-preview-service.mjs +87 -12
- package/host-service/session-list-projection-service.mjs +182 -0
- package/host-service/session-loop-definitions.mjs +702 -75
- package/host-service/session-manager.mjs +473 -38
- package/host-service/session-switcher-projection.mjs +11 -3
- package/host-service/session-tags.mjs +451 -0
- package/host-service/session-transcript-target.mjs +103 -0
- package/host-service/setup-input-file.mjs +91 -0
- package/host-service/setup-provider-model-defaults.mjs +53 -0
- package/host-service/setup.mjs +133 -101
- package/host-service/stopped-user-host-package.mjs +60 -0
- package/host-service/test-fixtures/p236-navigation-original.mjs +500 -0
- package/host-service/update-reminder-state.mjs +7 -0
- package/host-service/voice-input-receipt-store.mjs +60 -0
- package/package.json +1 -1
- package/static/oysterun-social-preview.png +0 -0
- package/static/site/assets/oysterun-social-preview.png +0 -0
- package/static/site/blog/en/index.html +17 -0
- package/static/site/blog/generated_manifest.json +9 -9
- package/static/site/blog/index.html +18 -0
- package/static/site/blog/voice-input-model-experiment/en/index.html +17 -0
- package/static/site/blog/voice-input-model-experiment/zh/index.html +17 -0
- package/static/site/blog/zh/index.html +17 -0
- package/static/site/docs/choose-and-recover-a-provider/en/index.html +17 -0
- package/static/site/docs/choose-and-recover-a-provider/zh/index.html +17 -0
- package/static/site/docs/configure-session-files-and-settings/en/index.html +17 -0
- package/static/site/docs/configure-session-files-and-settings/zh/index.html +17 -0
- package/static/site/docs/en/index.html +17 -0
- package/static/site/docs/generated_manifest.json +60 -60
- package/static/site/docs/host-authentication-and-connection/en/index.html +17 -0
- package/static/site/docs/host-authentication-and-connection/zh/index.html +17 -0
- package/static/site/docs/index.html +18 -0
- package/static/site/docs/install-and-use-a-skill/en/index.html +17 -0
- package/static/site/docs/install-and-use-a-skill/zh/index.html +17 -0
- package/static/site/docs/read-large-tool-results/en/index.html +17 -0
- package/static/site/docs/read-large-tool-results/zh/index.html +17 -0
- package/static/site/docs/recover-from-a-host-runtime-error/en/index.html +17 -0
- package/static/site/docs/recover-from-a-host-runtime-error/zh/index.html +17 -0
- package/static/site/docs/restore-work-after-a-host-restart/en/index.html +17 -0
- package/static/site/docs/restore-work-after-a-host-restart/zh/index.html +17 -0
- package/static/site/docs/send-images-and-media/en/index.html +17 -0
- package/static/site/docs/send-images-and-media/zh/index.html +17 -0
- package/static/site/docs/set-up-and-understand-a-host/en/index.html +17 -0
- package/static/site/docs/set-up-and-understand-a-host/zh/index.html +17 -0
- package/static/site/docs/understand-agent-activity/en/index.html +19 -2
- package/static/site/docs/understand-agent-activity/en/index.md +2 -2
- package/static/site/docs/understand-agent-activity/zh/index.html +19 -2
- package/static/site/docs/understand-agent-activity/zh/index.md +2 -2
- package/static/site/docs/use-the-oysterun-cli-safely/en/index.html +17 -0
- package/static/site/docs/use-the-oysterun-cli-safely/zh/index.html +17 -0
- package/static/site/docs/zh/index.html +17 -0
- package/static/site/en/index.html +18 -1
- package/static/site/en/index.md +1 -1
- package/static/site/faq/en/index.html +17 -0
- package/static/site/faq/index.html +18 -0
- package/static/site/faq/zh/index.html +17 -0
- package/static/site/getting-started/en/index.html +23 -1
- package/static/site/getting-started/en/index.md +7 -1
- package/static/site/getting-started/index.html +18 -0
- package/static/site/getting-started/zh/index.html +23 -1
- package/static/site/getting-started/zh/index.md +7 -1
- package/static/site/index.html +18 -0
- package/static/site/install.sh +676 -127
- package/static/site/overview/en/index.html +18 -1
- package/static/site/overview/en/index.md +2 -2
- package/static/site/overview/index.html +18 -0
- package/static/site/overview/zh/index.html +18 -1
- package/static/site/overview/zh/index.md +2 -2
- package/static/site/privacy/en/index.html +17 -0
- package/static/site/privacy/index.html +18 -0
- package/static/site/privacy/zh/index.html +17 -0
- package/static/site/zh/index.html +18 -1
- package/static/site/zh/index.md +1 -1
- package/static/site_source/content/tutorials/published/understand-agent-activity/en/article.html +2 -2
- package/static/site_source/content/tutorials/published/understand-agent-activity/zh/article.html +2 -2
- package/static/site_source/downloads/install.sh +676 -127
- package/static/site_source/manifests/host_install.json +2 -10
- package/static/site_source/pages/getting-started/en.html +6 -1
- package/static/site_source/pages/getting-started/zh.html +6 -1
- package/static/site_source/pages/home/en.html +1 -1
- package/static/site_source/pages/home/zh.html +1 -1
- package/static/site_source/pages/home_main.html +3 -2
- package/static/site_source/pages/overview/en.html +1 -1
- package/static/site_source/pages/overview/zh.html +1 -1
- package/tool_scripts/install_oysterun_update.mjs +78 -13
- package/tool_scripts/oysterun_continuity_transaction.mjs +438 -0
- package/tool_scripts/oysterun_npm_package_manifest.mjs +32 -0
- package/tool_scripts/oysterun_release_service.sh +5 -0
- package/tool_scripts/oysterun_service_common.sh +59 -24
- package/tool_scripts/oysterun_setup_input.sh +97 -0
- package/tool_scripts/oysterun_update_restart_runner.mjs +19 -3
- package/tool_scripts/p233_legacy_bootstrap.mjs +254 -0
- package/tool_scripts/resolve_user_host_port.mjs +15 -0
- package/tool_scripts/restart_oysterun.sh +8 -2
- package/tool_scripts/stack_dashboard_credentials.mjs +2 -1
- package/tool_scripts/status_oysterun_launch_agents.sh +13 -1
- package/tool_scripts/sync_host_stack_config.mjs +9 -6
- package/tool_scripts/test_environment_remote_cloud_deprovision.mjs +419 -0
- package/tool_scripts/uninstall_oysterun_launch_agents.sh +3 -5
- package/dev/client/web-chat/dist/assets/index-Ce9NZBZf.js +0 -129
- package/dev/client/web-chat/dist/assets/index-Ce9NZBZf.js.map +0 -1
- package/dev/client/web-chat/dist/assets/index-DzIClZ3T.js +0 -2
- package/dev/client/web-chat/dist/assets/index-DzIClZ3T.js.map +0 -1
- package/dev/client/web-chat/dist/assets/index-NYnaSGBb.css +0 -1
- package/dev/client/web-chat/dist/assets/pdf-ZjtalfgI.js.map +0 -1
- package/dev/client/web-chat/dist/assets/web-BzHcZK2M.js +0 -1
- package/dev/client/web-chat/dist/assets/web-c70YaskK.js +0 -1
package/static/site/install.sh
CHANGED
|
@@ -6,53 +6,138 @@ platform=""
|
|
|
6
6
|
architecture=""
|
|
7
7
|
target_user=""
|
|
8
8
|
target_home=""
|
|
9
|
+
target_uid=""
|
|
10
|
+
target_gid=""
|
|
11
|
+
canonical_home=""
|
|
12
|
+
config_dir=""
|
|
13
|
+
setup_input_file=""
|
|
14
|
+
setup_input_root_memory=false
|
|
9
15
|
profile_file=""
|
|
10
|
-
runtime_path_prefix=""
|
|
11
16
|
temporary_root=""
|
|
17
|
+
install_lock=""
|
|
18
|
+
lock_owned=false
|
|
12
19
|
dry_run=false
|
|
13
20
|
install_only=false
|
|
14
|
-
|
|
21
|
+
reinstall=false
|
|
22
|
+
modify_path=true
|
|
23
|
+
package_install_spec=""
|
|
24
|
+
requested_channel_override=""
|
|
25
|
+
version_compare_script='const p=v=>{const m=String(v).match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);return m?[+m[1],+m[2],+m[3],m[4]||""]:null},a=p(process.argv[1]),b=p(process.argv[2]);if(!a||!b)process.exit(2);let result=null;for(let i=0;i<3;i++){if(a[i]!==b[i]){result=a[i]>b[i]?1:-1;break}}if(result===null){if(a[3]===b[3])result=0;else if(!a[3])result=1;else if(!b[3])result=-1;else result=a[3].localeCompare(b[3],undefined,{numeric:true})>0?1:-1}console.log(result)'
|
|
15
26
|
|
|
16
27
|
cleanup() {
|
|
17
28
|
local exit_status=$?
|
|
29
|
+
unset setup_input_snapshot
|
|
18
30
|
[[ -z "$temporary_root" ]] || rm -rf "$temporary_root"
|
|
19
|
-
if [[ "$
|
|
20
|
-
|
|
31
|
+
if [[ "$lock_owned" == true && -n "$install_lock" ]]; then
|
|
32
|
+
rmdir "$install_lock" >/dev/null 2>&1 || true
|
|
21
33
|
fi
|
|
34
|
+
return "$exit_status"
|
|
35
|
+
}
|
|
36
|
+
print_recovery_inspection() {
|
|
37
|
+
# Signal handlers only print this command. They must not inspect or mutate
|
|
38
|
+
# a Host while an interrupted child may still own a lifecycle transaction.
|
|
39
|
+
if [[ -n "${node_bin:-}" && -n "${driver_prefix:-}" && -n "${config_dir:-}" && -n "${selected_channel:-}" && \
|
|
40
|
+
-f "$driver_prefix/lib/node_modules/oysterun/host-service/cli/installer-recovery-guidance.mjs" ]]; then
|
|
41
|
+
printf 'Next (read-only recovery inspection):' >&2
|
|
42
|
+
# Mirror capture_user's supported account routing without executing it.
|
|
43
|
+
if [[ "$EUID" -eq 0 ]]; then
|
|
44
|
+
if [[ "$platform" == "Darwin" ]]; then
|
|
45
|
+
printf ' %q' sudo -H -u "$target_user" env "HOME=$target_home" "USER=$target_user" "LOGNAME=$target_user" >&2
|
|
46
|
+
else
|
|
47
|
+
printf ' %q' runuser -u "$target_user" -- env "HOME=$target_home" "USER=$target_user" "LOGNAME=$target_user" >&2
|
|
48
|
+
fi
|
|
49
|
+
else
|
|
50
|
+
printf ' %q' env "HOME=$target_home" "USER=$target_user" "LOGNAME=$target_user" >&2
|
|
51
|
+
fi
|
|
52
|
+
printf ' %q' "$node_bin" "$driver_prefix/lib/node_modules/oysterun/host-service/cli/installer-recovery-guidance.mjs" \
|
|
53
|
+
"$config_dir" "${active_package_root:-}" "" "$selected_channel" \
|
|
54
|
+
"$canonical_home" "$target_uid" "$reinstall" >&2
|
|
55
|
+
printf '\n' >&2
|
|
56
|
+
elif [[ -n "${config_dir:-}" ]]; then
|
|
57
|
+
printf 'Next (inspect target ownership): ls -ld -- %q\n' "$config_dir" >&2
|
|
58
|
+
else
|
|
59
|
+
printf 'Next (inspect platform): uname -sm\n' >&2
|
|
60
|
+
fi
|
|
61
|
+
}
|
|
62
|
+
fail() {
|
|
63
|
+
trap - ERR
|
|
64
|
+
printf 'Oysterun installer failed during %s: %s\n' "$stage" "$1" >&2
|
|
65
|
+
if [[ -n "${node_bin:-}" && -n "${driver_prefix:-}" && -n "${active_package_root:-}" && \
|
|
66
|
+
-f "$driver_prefix/lib/node_modules/oysterun/host-service/cli/installer-recovery-guidance.mjs" ]]; then
|
|
67
|
+
capture_user "$node_bin" "$driver_prefix/lib/node_modules/oysterun/host-service/cli/installer-recovery-guidance.mjs" \
|
|
68
|
+
"$config_dir" "$active_package_root" "" "$selected_channel" \
|
|
69
|
+
"$canonical_home" "$target_uid" "$reinstall" >&2 || true
|
|
70
|
+
fi
|
|
71
|
+
print_recovery_inspection
|
|
72
|
+
exit 1
|
|
73
|
+
}
|
|
74
|
+
interrupted() {
|
|
75
|
+
trap - ERR INT TERM
|
|
76
|
+
printf 'Oysterun installer interrupted by %s during %s; completion and running Host readiness are unproved. Preserve the Host and lifecycle receipts.\n' "$1" "$stage" >&2
|
|
77
|
+
print_recovery_inspection
|
|
78
|
+
exit "$2"
|
|
22
79
|
}
|
|
23
|
-
fail() { trap - ERR; printf 'Oysterun installer failed during %s: %s\n' "$stage" "$1" >&2; exit 1; }
|
|
24
80
|
trap 'fail "command exited with status $?"' ERR
|
|
25
81
|
trap cleanup EXIT
|
|
82
|
+
trap 'interrupted INT 130' INT
|
|
83
|
+
trap 'interrupted TERM 143' TERM
|
|
26
84
|
|
|
27
85
|
while [[ $# -gt 0 ]]; do
|
|
28
86
|
case "$1" in
|
|
29
87
|
--dry-run) dry_run=true ;;
|
|
30
88
|
--install-only) install_only=true ;;
|
|
31
|
-
|
|
89
|
+
--reinstall) reinstall=true ;;
|
|
90
|
+
--no-modify-path) modify_path=false ;;
|
|
91
|
+
--channel)
|
|
92
|
+
[[ $# -ge 2 ]] || fail "--channel requires latest or beta"
|
|
93
|
+
requested_channel_override="$2"
|
|
94
|
+
shift
|
|
95
|
+
;;
|
|
96
|
+
--channel=*) requested_channel_override="${1#--channel=}" ;;
|
|
97
|
+
--setup-input-file)
|
|
98
|
+
[[ $# -ge 2 ]] || fail "--setup-input-file requires an absolute path"
|
|
99
|
+
setup_input_file="$2"; shift
|
|
100
|
+
;;
|
|
101
|
+
--setup-input-file=*) setup_input_file="${1#--setup-input-file=}" ;;
|
|
102
|
+
--config-dir)
|
|
103
|
+
[[ $# -ge 2 ]] || fail "--config-dir requires an absolute path"
|
|
104
|
+
config_dir="$2"
|
|
105
|
+
shift
|
|
106
|
+
;;
|
|
107
|
+
--config-dir=*) config_dir="${1#--config-dir=}" ;;
|
|
108
|
+
*) fail "supported options: --channel latest|beta, --reinstall (preserve config; stops sessions), --dry-run, --install-only, --no-modify-path, --config-dir PATH, --setup-input-file PATH" ;;
|
|
32
109
|
esac
|
|
33
110
|
shift
|
|
34
111
|
done
|
|
35
112
|
|
|
113
|
+
if $reinstall && $install_only; then
|
|
114
|
+
fail "--reinstall includes service restoration and cannot be combined with --install-only"
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
case "$requested_channel_override" in
|
|
118
|
+
""|latest|beta) ;;
|
|
119
|
+
*) fail "--channel must be latest or beta" ;;
|
|
120
|
+
esac
|
|
121
|
+
|
|
36
122
|
platform="$(uname -s)"
|
|
37
123
|
architecture="$(uname -m)"
|
|
38
124
|
case "$platform:$architecture" in
|
|
39
125
|
Darwin:x86_64|Darwin:arm64|Linux:x86_64|Linux:aarch64|Linux:arm64) ;;
|
|
40
126
|
Darwin:*|Linux:*) fail "unsupported CPU architecture: $architecture" ;;
|
|
41
|
-
*) fail "supported operating systems: macOS and
|
|
127
|
+
*) fail "supported operating systems: macOS and Ubuntu Linux" ;;
|
|
42
128
|
esac
|
|
43
129
|
|
|
44
130
|
if [[ "$platform" == "Linux" ]]; then
|
|
45
131
|
os_release="/etc/os-release"
|
|
46
|
-
if $dry_run && [[ -n "${P230_TEST_OS_RELEASE:-}" ]]; then
|
|
132
|
+
if $dry_run && [[ -n "${P230_TEST_OS_RELEASE:-}" ]]; then
|
|
133
|
+
os_release="$P230_TEST_OS_RELEASE"
|
|
134
|
+
fi
|
|
47
135
|
[[ -f "$os_release" ]] || fail "Linux distribution metadata is missing"
|
|
48
136
|
distro_id="$(. "$os_release"; printf '%s' "${ID:-}")"
|
|
49
137
|
distro_version="$(. "$os_release"; printf '%s' "${VERSION_ID:-}")"
|
|
50
138
|
distro_major="${distro_version%%.*}"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
debian) [[ "$distro_major" =~ ^[0-9]+$ && "$distro_major" -ge 12 ]] || fail "Debian 12 or newer is required" ;;
|
|
54
|
-
*) fail "supported Linux distributions: Ubuntu 22.04+ and Debian 12+" ;;
|
|
55
|
-
esac
|
|
139
|
+
[[ "$distro_id" == "ubuntu" ]] || fail "supported Linux distribution: Ubuntu 22.04 or newer"
|
|
140
|
+
[[ "$distro_major" =~ ^[0-9]+$ && "$distro_major" -ge 22 ]] || fail "Ubuntu 22.04 or newer is required"
|
|
56
141
|
fi
|
|
57
142
|
|
|
58
143
|
lookup_home() {
|
|
@@ -75,182 +160,646 @@ else
|
|
|
75
160
|
fi
|
|
76
161
|
|
|
77
162
|
stage="target account"
|
|
163
|
+
# BEGIN generated setup input validation
|
|
164
|
+
# Canonical read-only setup-file/pre-input validation; also embedded in install.sh.
|
|
165
|
+
setup_input_error() { printf '%s\n' "$1" >&2; return 1; }
|
|
166
|
+
setup_config_has() {
|
|
167
|
+
local file="$1" key="$2" kind="$3" value
|
|
168
|
+
[[ -f "$file" ]] || return 1
|
|
169
|
+
if [[ "$(uname -s)" == Darwin ]]; then
|
|
170
|
+
local expected="$kind"; [[ "$kind" != direct ]] || expected=string
|
|
171
|
+
value=$(/usr/bin/plutil -extract "$key" raw -expect "$expected" -o - "$file" 2>/dev/null) || return 1
|
|
172
|
+
case "$kind" in bool) return 0;; direct) [[ "$value" == direct ]];; integer) [[ "$value" =~ ^[0-9]+$ ]] && (( value > 0 && value <= 65535 ));; *) [[ "$value" =~ [^[:space:]] ]];; esac
|
|
173
|
+
else
|
|
174
|
+
command -v python3 >/dev/null || { setup_input_error 'Read-only Ubuntu configuration preflight requires python3; no changes made'; return 2; }
|
|
175
|
+
python3 - "$file" "$key" "$kind" <<'PY'
|
|
176
|
+
import json,sys
|
|
177
|
+
try:
|
|
178
|
+
c=json.load(open(sys.argv[1]));v=c.get(sys.argv[2]);kind=sys.argv[3];ok=isinstance(v,bool) if kind=='bool' else (type(v)==int and 0<v<=65535) if kind=='integer' else v=='direct' if kind=='direct' else isinstance(v,str) and bool(v.strip())
|
|
179
|
+
except Exception: ok=False
|
|
180
|
+
sys.exit(0 if ok else 1)
|
|
181
|
+
PY
|
|
182
|
+
fi
|
|
183
|
+
}
|
|
184
|
+
setup_config_ready() {
|
|
185
|
+
local dir="$1" cfg="$1/config.json"
|
|
186
|
+
setup_config_has "$cfg" port integer && setup_config_has "$cfg" dashboard_user string && setup_config_has "$cfg" dashboard_password_hash string && setup_config_has "$cfg" default_browse_path string || return 1
|
|
187
|
+
if setup_config_has "$cfg" connection_mode direct; then
|
|
188
|
+
setup_config_has "$cfg" direct_host_url string || setup_config_has "$cfg" public_base_url string
|
|
189
|
+
else
|
|
190
|
+
setup_config_has "$cfg" managed_endpoint string &&
|
|
191
|
+
{ setup_config_has "$dir/cloud_identity.json" device_id string || setup_config_has "$dir/cloud_identity.json" host_id string || setup_config_has "$cfg" device_id string || setup_config_has "$cfg" host_id string; } &&
|
|
192
|
+
{ setup_config_has "$dir/cloud_identity.json" device_token string || setup_config_has "$dir/cloud_identity.json" host_credential string || setup_config_has "$cfg" device_token string || setup_config_has "$cfg" host_credential string; }
|
|
193
|
+
fi
|
|
194
|
+
}
|
|
195
|
+
setup_input_validate() {
|
|
196
|
+
set +x
|
|
197
|
+
set +a
|
|
198
|
+
unset setup_input_snapshot
|
|
199
|
+
setup_input_snapshot=''
|
|
200
|
+
local source="$1" uid="$2" config="$3" line key value mode owner missing='' seen='|' name='' root='' password='' consent=''
|
|
201
|
+
export -n password line value
|
|
202
|
+
if [[ -f "$config/config.json" && "$(uname -s)" == Linux ]] && ! command -v python3 >/dev/null; then setup_input_error "Read-only Ubuntu configuration preflight requires python3; no changes made"; return 1; fi
|
|
203
|
+
if [[ -f "$config/config.json" ]]; then
|
|
204
|
+
if [[ "$(uname -s)" == Darwin ]]; then
|
|
205
|
+
/usr/bin/osascript -l JavaScript -e 'function run(argv) { ObjC.import("Foundation"); const text = $.NSString.stringWithContentsOfFileEncodingError(argv[0], $.NSUTF8StringEncoding, null).js; const value = JSON.parse(text); if (!value || Array.isArray(value) || typeof value !== "object") throw Error("invalid configuration"); return "valid"; }' "$config/config.json" >/dev/null 2>&1 || { setup_input_error 'Existing configuration is invalid JSON; preserve it and restore the configuration before retrying'; return 1; }
|
|
206
|
+
else
|
|
207
|
+
python3 -c 'import json,sys; value=json.load(open(sys.argv[1])); assert isinstance(value,dict)' "$config/config.json" >/dev/null 2>&1 || { setup_input_error 'Existing configuration is invalid JSON; preserve it and restore the configuration before retrying'; return 1; }
|
|
208
|
+
fi
|
|
209
|
+
fi
|
|
210
|
+
if [[ -n "$source" ]]; then
|
|
211
|
+
[[ "$source" == /* && -f "$source" && ! -L "$source" ]] || { setup_input_error 'Setup input must be an absolute regular file, not a symlink'; return 1; }
|
|
212
|
+
local parent parent_path="${source%/*}"; [[ -n "$parent_path" ]] || parent_path=/
|
|
213
|
+
parent=$(cd -P "$parent_path" && pwd) || return 1
|
|
214
|
+
[[ "$parent" == "$parent_path" ]] || { setup_input_error "Setup input path must not traverse symlinks"; return 1; }
|
|
215
|
+
exec 9< "$source" || { setup_input_error 'Setup input is not readable'; return 1; }
|
|
216
|
+
local opened=/dev/fd/9; [[ "$(uname -s)" != Linux ]] || opened=/proc/self/fd/9
|
|
217
|
+
if [[ "$(uname -s)" == Darwin ]]; then
|
|
218
|
+
owner=$(/usr/bin/stat -Lf %u "$opened"); mode=$(/usr/bin/stat -Lf %Lp "$opened")
|
|
219
|
+
else
|
|
220
|
+
owner=$(/usr/bin/stat -Lc %u "$opened"); mode=$(/usr/bin/stat -Lc %a "$opened")
|
|
221
|
+
fi
|
|
222
|
+
if ! [[ -f "$opened" && -n "$uid" && "$owner" == "$uid" && ( "$mode" == 600 || "$mode" == 400 ) ]]; then
|
|
223
|
+
exec 9<&-; setup_input_error 'Setup input must belong to the selected source owner and have private permissions (0600 or 0400)'; return 1
|
|
224
|
+
fi
|
|
225
|
+
if IFS= read -r -d '' setup_input_snapshot <&9; then
|
|
226
|
+
exec 9<&-; unset setup_input_snapshot; setup_input_error 'Setup input must be UTF-8 text without NULs'; return 1
|
|
227
|
+
fi
|
|
228
|
+
exec 9<&-
|
|
229
|
+
builtin printf '%s' "$setup_input_snapshot" | /usr/bin/iconv -f UTF-8 -t UTF-8 >/dev/null 2>&1 || { unset setup_input_snapshot; setup_input_error 'Setup input must be UTF-8 text without NULs'; return 1; }
|
|
230
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
231
|
+
[[ -z "$line" || "$line" == \#* ]] && continue
|
|
232
|
+
[[ "$line" == *=* ]] || { setup_input_error 'Invalid setup input format'; return 1; }
|
|
233
|
+
key=${line%%=*}; value=${line#*=}
|
|
234
|
+
case "$key" in display_name|default_browse_root|dashboard_password|daily_telemetry_enabled) ;; *) setup_input_error 'Unknown setup input field'; return 1;; esac
|
|
235
|
+
[[ "$seen" != *"|$key|"* ]] || { setup_input_error 'Duplicate setup input field'; return 1; }; seen="$seen$key|"
|
|
236
|
+
case "$key" in
|
|
237
|
+
display_name) name="$value" ;;
|
|
238
|
+
default_browse_root) [[ -z "$value" || "$value" == /* ]] || { setup_input_error 'Setup Browse Root must be absolute'; return 1; }; root="$value" ;;
|
|
239
|
+
dashboard_password) password="$value" ;;
|
|
240
|
+
daily_telemetry_enabled) [[ "$value" == true || "$value" == false ]] || { setup_input_error 'Setup telemetry consent must be true or false'; return 1; }; consent="$value" ;;
|
|
241
|
+
esac
|
|
242
|
+
done < <(builtin printf '%s' "$setup_input_snapshot")
|
|
243
|
+
[[ -z "$root" || -d "$root" ]] || { setup_input_error "Setup Browse Root must be an existing directory"; return 1; }
|
|
244
|
+
fi
|
|
245
|
+
setup_config_ready "$config" && return 0
|
|
246
|
+
setup_config_has "$config/config.json" display_name string || [[ "$name" =~ [^[:space:]] ]] || missing="$missing display_name"
|
|
247
|
+
setup_config_has "$config/config.json" default_browse_path string || [[ -n "$root" ]] || missing="$missing default_browse_root"
|
|
248
|
+
setup_config_has "$config/config.json" dashboard_password_hash string || [[ "$password" =~ [^[:space:]] ]] || missing="$missing dashboard_password"
|
|
249
|
+
if ! { setup_config_has "$config/config.json" daily_telemetry_enabled bool && setup_config_has "$config/config.json" daily_telemetry_consent_recorded_at string; } && [[ -z "$consent" ]]; then missing="$missing daily_telemetry_enabled"; fi
|
|
250
|
+
[[ -z "$missing" ]] || { setup_input_error "Missing onboarding inputs:$missing. Resume the same command from a terminal or with --setup-input-file /absolute/private-file"; return 1; }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# Only the invoking-root account-creation branch may emit this memory snapshot.
|
|
254
|
+
# Anonymous pipes retain root ownership and read-only access in the target child.
|
|
255
|
+
setup_input_emit_for_uid() {
|
|
256
|
+
set +x
|
|
257
|
+
builtin printf 'oysterun-root-setup-v1:%s\n' "$1"
|
|
258
|
+
builtin printf '%s' "$setup_input_snapshot"
|
|
259
|
+
}
|
|
260
|
+
# END generated setup input validation
|
|
261
|
+
require_fresh_onboarding_terminal() {
|
|
262
|
+
local prospective_config="$1" prospective_uid="${2:-${target_uid:-}}"
|
|
263
|
+
if ! $dry_run && ! $install_only && { [[ -n "$setup_input_file" ]] || { [[ ! -t 0 ]] && ! ( : </dev/tty ) 2>/dev/null; }; }; then
|
|
264
|
+
setup_input_validate "$setup_input_file" "$prospective_uid" "$prospective_config" || fail "onboarding inputs are incomplete or unsafe; no changes made"
|
|
265
|
+
fi
|
|
266
|
+
}
|
|
78
267
|
if [[ "$platform" == "Linux" && "$(id -u)" -eq 0 && "$target_user" == "oysterun" ]] && ! id "$target_user" >/dev/null 2>&1; then
|
|
268
|
+
if ! $dry_run && ! $install_only && [[ -n "$setup_input_file" ]]; then
|
|
269
|
+
setup_input_validate "$setup_input_file" 0 "${config_dir:-${OYSTERUN_CONFIG_DIR:-/home/oysterun/.oysterun}}" || fail "onboarding inputs are incomplete or unsafe; no changes made"
|
|
270
|
+
setup_input_root_memory=true
|
|
271
|
+
else
|
|
272
|
+
require_fresh_onboarding_terminal "${config_dir:-${OYSTERUN_CONFIG_DIR:-/home/oysterun/.oysterun}}"
|
|
273
|
+
fi
|
|
79
274
|
if $dry_run; then
|
|
80
|
-
printf '+ root: useradd --create-home --home-dir /home/oysterun --shell /bin/bash
|
|
81
|
-
direct_root_account_created=true
|
|
275
|
+
printf '+ root: useradd --create-home --home-dir /home/oysterun --shell /bin/bash oysterun\n'
|
|
82
276
|
else
|
|
83
|
-
useradd --create-home --home-dir /home/oysterun --shell /bin/bash
|
|
84
|
-
direct_root_account_created=true
|
|
277
|
+
useradd --create-home --home-dir /home/oysterun --shell /bin/bash oysterun
|
|
85
278
|
fi
|
|
86
279
|
fi
|
|
87
280
|
|
|
88
281
|
stage="target identity validation"
|
|
89
|
-
if $dry_run && $
|
|
90
|
-
target_uid="<created-user-uid>"
|
|
282
|
+
if $dry_run && [[ "$target_user" == "oysterun" ]] && ! id "$target_user" >/dev/null 2>&1; then
|
|
91
283
|
target_home="/home/oysterun"
|
|
284
|
+
target_uid="1000"
|
|
285
|
+
target_gid="1000"
|
|
92
286
|
canonical_home="$target_home"
|
|
93
287
|
else
|
|
94
288
|
target_home="$(lookup_home "$target_user")" || fail "target account has no home directory"
|
|
95
289
|
[[ "$target_user" != "root" && "$target_home" == /* && "$target_home" != "/" && "$target_home" != "/root" ]] || fail "unsafe target user or home"
|
|
96
|
-
if [[ "$(id -u)" -ne 0 && "${HOME:?HOME is required}" != "$target_home" ]]; then
|
|
290
|
+
if [[ "$(id -u)" -ne 0 && "${HOME:?HOME is required}" != "$target_home" ]]; then
|
|
291
|
+
fail "HOME does not match the invoking account database home"
|
|
292
|
+
fi
|
|
97
293
|
target_uid="$(id -u "$target_user")" || fail "target user does not exist"
|
|
294
|
+
target_gid="$(id -g "$target_user")" || fail "target user group does not exist"
|
|
98
295
|
[[ "$target_uid" =~ ^[0-9]+$ && "$target_uid" -ne 0 ]] || fail "target user must have a non-root numeric UID"
|
|
99
296
|
[[ -d "$target_home" && ! -L "$target_home" ]] || fail "target home must be an existing non-symlink directory"
|
|
100
297
|
canonical_home="$(cd -P "$target_home" && pwd -P)"
|
|
101
298
|
[[ "$canonical_home" == "$target_home" ]] || fail "target home must already be canonical"
|
|
102
|
-
if [[ "$platform" == "Darwin" ]]; then
|
|
299
|
+
if [[ "$platform" == "Darwin" ]]; then
|
|
300
|
+
home_uid="$(stat -f %u "$target_home")"
|
|
301
|
+
else
|
|
302
|
+
home_uid="$(stat -c %u "$target_home")"
|
|
303
|
+
fi
|
|
103
304
|
[[ "$home_uid" == "$target_uid" ]] || fail "target home must be owned by the target user"
|
|
104
305
|
fi
|
|
105
306
|
|
|
106
|
-
stage="existing installation check"
|
|
107
|
-
default_config_dir="$canonical_home/.oysterun"
|
|
108
|
-
if [[ -n "${OYSTERUN_CONFIG_DIR:-}" && "$OYSTERUN_CONFIG_DIR" != "$default_config_dir" ]]; then
|
|
109
|
-
fail "custom OYSTERUN_CONFIG_DIR is ambiguous; use the existing installation's update command"
|
|
110
|
-
fi
|
|
111
|
-
existing_config_found=false
|
|
112
|
-
[[ -f "$default_config_dir/config.json" ]] && existing_config_found=true
|
|
113
|
-
|
|
114
|
-
if [[ "$platform" == "Linux" ]]; then
|
|
115
|
-
command -v apt-get >/dev/null 2>&1 || fail "this release supports Debian and Ubuntu Linux (apt-get)"
|
|
116
|
-
profile_file="$target_home/.profile"
|
|
117
|
-
else
|
|
118
|
-
profile_file="$target_home/.zprofile"
|
|
119
|
-
fi
|
|
120
|
-
|
|
121
307
|
run_root() {
|
|
122
|
-
if $dry_run; then
|
|
123
|
-
|
|
124
|
-
|
|
308
|
+
if $dry_run; then
|
|
309
|
+
printf '+ root:'
|
|
310
|
+
printf ' %q' "$@"
|
|
311
|
+
printf '\n'
|
|
312
|
+
elif [[ "$(id -u)" -eq 0 ]]; then
|
|
313
|
+
"$@"
|
|
314
|
+
else
|
|
315
|
+
sudo "$@"
|
|
125
316
|
fi
|
|
126
317
|
}
|
|
127
318
|
|
|
128
|
-
|
|
129
|
-
local command="$1"
|
|
130
|
-
command="set -Eeuo pipefail; unset NPM_CONFIG_PREFIX npm_config_prefix; $command"
|
|
131
|
-
if [[ -n "$runtime_path_prefix" ]]; then command="export PATH='$runtime_path_prefix':\$PATH; $command"; fi
|
|
319
|
+
run_user() {
|
|
132
320
|
if $dry_run; then
|
|
133
|
-
printf '+ user %s (HOME=%s):
|
|
321
|
+
printf '+ user %s (HOME=%s):' "$target_user" "$target_home"
|
|
322
|
+
printf ' %q' "$@"
|
|
323
|
+
printf '\n'
|
|
134
324
|
elif [[ "$(id -u)" -eq 0 ]]; then
|
|
135
325
|
if [[ "$platform" == "Darwin" ]]; then
|
|
136
|
-
sudo -H -u "$target_user" env HOME="$target_home" USER="$target_user" LOGNAME="$target_user"
|
|
326
|
+
sudo -H -u "$target_user" env HOME="$target_home" USER="$target_user" LOGNAME="$target_user" "$@"
|
|
137
327
|
else
|
|
138
|
-
runuser -u "$target_user" -- env HOME="$target_home" USER="$target_user" LOGNAME="$target_user"
|
|
328
|
+
runuser -u "$target_user" -- env HOME="$target_home" USER="$target_user" LOGNAME="$target_user" "$@"
|
|
139
329
|
fi
|
|
140
330
|
else
|
|
141
|
-
HOME="$target_home" USER="$target_user" LOGNAME="$target_user"
|
|
331
|
+
HOME="$target_home" USER="$target_user" LOGNAME="$target_user" "$@"
|
|
142
332
|
fi
|
|
143
333
|
}
|
|
144
334
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
command="set -Eeuo pipefail; unset NPM_CONFIG_PREFIX npm_config_prefix; $command"
|
|
148
|
-
if [[ -n "$runtime_path_prefix" ]]; then command="export PATH='$runtime_path_prefix':\$PATH; $command"; fi
|
|
149
|
-
if $dry_run; then
|
|
150
|
-
HOME="$target_home" USER="$target_user" LOGNAME="$target_user" /bin/bash -c "$command"
|
|
151
|
-
elif [[ "$(id -u)" -eq 0 ]]; then
|
|
335
|
+
capture_user() {
|
|
336
|
+
if [[ "$(id -u)" -eq 0 ]]; then
|
|
152
337
|
if [[ "$platform" == "Darwin" ]]; then
|
|
153
|
-
sudo -H -u "$target_user" env HOME="$target_home" USER="$target_user" LOGNAME="$target_user"
|
|
338
|
+
sudo -H -u "$target_user" env HOME="$target_home" USER="$target_user" LOGNAME="$target_user" "$@"
|
|
154
339
|
else
|
|
155
|
-
runuser -u "$target_user" -- env HOME="$target_home" USER="$target_user" LOGNAME="$target_user"
|
|
340
|
+
runuser -u "$target_user" -- env HOME="$target_home" USER="$target_user" LOGNAME="$target_user" "$@"
|
|
156
341
|
fi
|
|
157
342
|
else
|
|
158
|
-
HOME="$target_home" USER="$target_user" LOGNAME="$target_user"
|
|
343
|
+
HOME="$target_home" USER="$target_user" LOGNAME="$target_user" "$@"
|
|
159
344
|
fi
|
|
160
345
|
}
|
|
161
346
|
|
|
162
|
-
if $
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
347
|
+
if [[ -z "$config_dir" ]]; then
|
|
348
|
+
config_dir="${OYSTERUN_CONFIG_DIR:-$canonical_home/.oysterun}"
|
|
349
|
+
fi
|
|
350
|
+
[[ "$config_dir" == /* ]] || fail "--config-dir must be absolute"
|
|
351
|
+
if [[ -e "$config_dir" ]]; then
|
|
352
|
+
[[ -d "$config_dir" && ! -L "$config_dir" ]] || fail "config path must be a non-symlink directory"
|
|
353
|
+
if [[ "$platform" == "Darwin" ]]; then
|
|
354
|
+
config_uid="$(stat -f %u "$config_dir")"
|
|
355
|
+
else
|
|
356
|
+
config_uid="$(stat -c %u "$config_dir")"
|
|
357
|
+
fi
|
|
358
|
+
[[ "$config_uid" == "$target_uid" ]] || fail "config directory is not owned by the target user"
|
|
359
|
+
fi
|
|
360
|
+
export OYSTERUN_CONFIG_DIR="$config_dir"
|
|
361
|
+
# Invoking-root FILE authority survives account creation, failures and repeats.
|
|
362
|
+
# Target-owned files retain their ordinary target-user path below.
|
|
363
|
+
if ! $dry_run && ! $install_only && [[ -n "$setup_input_file" && "$setup_input_root_memory" != true && "$platform" == Linux && "$(id -u)" -eq 0 ]] && [[ "$(stat -c %u "$setup_input_file" 2>/dev/null)" == 0 ]]; then
|
|
364
|
+
setup_input_validate "$setup_input_file" 0 "$config_dir" || fail "onboarding inputs are incomplete or unsafe; no changes made"
|
|
365
|
+
setup_input_root_memory=true
|
|
366
|
+
fi
|
|
367
|
+
if ! $dry_run && ! $install_only && [[ -n "$setup_input_file" && "$setup_input_root_memory" != true ]]; then
|
|
368
|
+
run_user test -r "$setup_input_file" || fail "setup input is not readable through the target user route; no changes made"
|
|
369
|
+
fi
|
|
370
|
+
if [[ "$setup_input_root_memory" != true ]]; then require_fresh_onboarding_terminal "$config_dir"; fi
|
|
371
|
+
if $reinstall && [[ "$config_dir" != "$canonical_home/.oysterun" ]]; then
|
|
372
|
+
fail "--reinstall targets the default user Host only; it cannot remove another stack's service"
|
|
373
|
+
fi
|
|
374
|
+
|
|
375
|
+
stage="installer lock"
|
|
376
|
+
state_root="$canonical_home/.local/state/oysterun"
|
|
377
|
+
install_lock="$state_root/install.lock"
|
|
378
|
+
run_user mkdir -p "$state_root"
|
|
379
|
+
if $dry_run; then
|
|
380
|
+
printf '+ acquire exclusive installer lock %q\n' "$install_lock"
|
|
381
|
+
else
|
|
382
|
+
if ! run_user mkdir "$install_lock" 2>/dev/null; then
|
|
383
|
+
fail "another Oysterun install, update, or repair is already running"
|
|
166
384
|
fi
|
|
167
|
-
|
|
385
|
+
lock_owned=true
|
|
168
386
|
fi
|
|
169
387
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
388
|
+
if [[ "$platform" == "Darwin" ]]; then
|
|
389
|
+
profile_file="$canonical_home/.zprofile"
|
|
390
|
+
else
|
|
391
|
+
profile_file="$canonical_home/.profile"
|
|
392
|
+
fi
|
|
393
|
+
|
|
394
|
+
stage="Oysterun-managed Node.js 22"
|
|
395
|
+
runtime_root="$canonical_home/.local/share/oysterun/runtime"
|
|
396
|
+
case "$platform:$architecture" in
|
|
397
|
+
Darwin:arm64) node_suffix="darwin-arm64.tar.gz" ;;
|
|
398
|
+
Darwin:x86_64) node_suffix="darwin-x64.tar.gz" ;;
|
|
399
|
+
Linux:arm64|Linux:aarch64) node_suffix="linux-arm64.tar.gz" ;;
|
|
400
|
+
Linux:x86_64) node_suffix="linux-x64.tar.gz" ;;
|
|
401
|
+
esac
|
|
402
|
+
|
|
403
|
+
if $dry_run; then
|
|
404
|
+
node_version="v22.latest"
|
|
405
|
+
node_release_dir="$runtime_root/$node_version"
|
|
406
|
+
node_bin="$node_release_dir/bin/node"
|
|
407
|
+
npm_cli="$node_release_dir/lib/node_modules/npm/bin/npm-cli.js"
|
|
408
|
+
printf '+ download and verify official Node.js 22 %s artifact with SHASUMS256.txt\n' "$node_suffix"
|
|
409
|
+
printf '+ install versioned user-owned runtime at %q\n' "$node_release_dir"
|
|
410
|
+
else
|
|
411
|
+
temporary_root="$(mktemp -d)"
|
|
412
|
+
chmod 700 "$temporary_root"
|
|
413
|
+
curl --proto '=https' --tlsv1.2 -fsSLo "$temporary_root/SHASUMS256.txt" https://nodejs.org/dist/latest-v22.x/SHASUMS256.txt
|
|
414
|
+
node_archive="$(awk -v suffix="$node_suffix" 'index($2, suffix) == length($2) - length(suffix) + 1 { print $2; exit }' "$temporary_root/SHASUMS256.txt")"
|
|
415
|
+
[[ -n "$node_archive" ]] || fail "official Node.js 22 checksum manifest has no artifact for this platform"
|
|
416
|
+
node_version="${node_archive%%-$node_suffix}"
|
|
417
|
+
node_release_dir="$runtime_root/$node_version"
|
|
418
|
+
node_bin="$node_release_dir/bin/node"
|
|
419
|
+
npm_cli="$node_release_dir/lib/node_modules/npm/bin/npm-cli.js"
|
|
420
|
+
run_user mkdir -p "$runtime_root"
|
|
421
|
+
if [[ ! -x "$node_bin" ]] || ! "$node_bin" -e 'if (process.versions.node.split(".")[0] !== "22") process.exit(1)'; then
|
|
422
|
+
curl --proto '=https' --tlsv1.2 -fsSLo "$temporary_root/$node_archive" "https://nodejs.org/dist/latest-v22.x/$node_archive"
|
|
423
|
+
expected_sha="$(awk -v file="$node_archive" '$2 == file { print $1; exit }' "$temporary_root/SHASUMS256.txt")"
|
|
424
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
425
|
+
actual_sha="$(shasum -a 256 "$temporary_root/$node_archive" | awk '{print $1}')"
|
|
426
|
+
elif command -v sha256sum >/dev/null 2>&1; then
|
|
427
|
+
actual_sha="$(sha256sum "$temporary_root/$node_archive" | awk '{print $1}')"
|
|
188
428
|
else
|
|
189
|
-
|
|
190
|
-
if [[ -z "$brew_command" ]]; then brew_command="$(user_output 'command -v brew || true')"; fi
|
|
429
|
+
fail "SHA-256 verification tool is required (shasum or sha256sum)"
|
|
191
430
|
fi
|
|
192
|
-
[[ -n "$
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
431
|
+
[[ -n "$expected_sha" && "$actual_sha" == "$expected_sha" ]] || fail "official Node.js 22 artifact checksum mismatch"
|
|
432
|
+
tar -xzf "$temporary_root/$node_archive" -C "$temporary_root"
|
|
433
|
+
extracted="$temporary_root/${node_archive%.tar.gz}"
|
|
434
|
+
[[ -x "$extracted/bin/node" && -f "$extracted/lib/node_modules/npm/bin/npm-cli.js" ]] || fail "official Node.js archive is incomplete"
|
|
435
|
+
if [[ "$(id -u)" -eq 0 ]]; then
|
|
436
|
+
rm -rf "$node_release_dir.pending"
|
|
437
|
+
mv "$extracted" "$node_release_dir.pending"
|
|
438
|
+
chown -R "$target_uid:$target_gid" "$node_release_dir.pending"
|
|
439
|
+
mv "$node_release_dir.pending" "$node_release_dir"
|
|
196
440
|
else
|
|
197
|
-
|
|
198
|
-
runtime_path_prefix="$(user_output "'$brew_command' --prefix node@22")/bin"
|
|
199
|
-
export PATH="$runtime_path_prefix:$PATH"
|
|
441
|
+
mv "$extracted" "$node_release_dir"
|
|
200
442
|
fi
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
443
|
+
fi
|
|
444
|
+
"$node_bin" -e 'if (process.versions.node.split(".")[0] !== "22") process.exit(1)'
|
|
445
|
+
[[ -f "$npm_cli" ]] || fail "managed Node.js runtime does not include npm"
|
|
446
|
+
run_user ln -sfn "$node_release_dir" "$runtime_root/current"
|
|
447
|
+
fi
|
|
448
|
+
printf ' ✓ Oysterun-managed Node.js 22 verified: %s\n' "$node_version"
|
|
449
|
+
managed_runtime_path="$node_release_dir/bin:${PATH:-/usr/local/bin:/usr/bin:/bin}"
|
|
450
|
+
|
|
451
|
+
stage="release resolution"
|
|
452
|
+
npm_prefix="$canonical_home/.local/npm"
|
|
453
|
+
active_package_root="$npm_prefix/lib/node_modules/oysterun"
|
|
454
|
+
active_cli_module="$active_package_root/bin/oysterun.mjs"
|
|
455
|
+
installed_version=""
|
|
456
|
+
active_cli_usable=false
|
|
457
|
+
if ! $dry_run && [[ -f "$active_package_root/package.json" ]]; then
|
|
458
|
+
installed_version="$(capture_user "$node_bin" -p "require(process.argv[1]).version" "$active_package_root/package.json" 2>/dev/null || true)"
|
|
459
|
+
if [[ -f "$active_cli_module" ]] && capture_user "$node_bin" "$active_cli_module" --version >/dev/null 2>&1; then
|
|
460
|
+
active_cli_usable=true
|
|
461
|
+
fi
|
|
462
|
+
fi
|
|
463
|
+
|
|
464
|
+
recorded_channel=""
|
|
465
|
+
if ! $dry_run && [[ -f "$config_dir/config.json" ]]; then
|
|
466
|
+
recorded_channel="$(capture_user "$node_bin" -e 'const fs=require("fs");try{const c=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));process.stdout.write(typeof c.oysterun_update_channel==="string"?c.oysterun_update_channel:"")}catch{}' "$config_dir/config.json")"
|
|
467
|
+
fi
|
|
468
|
+
if [[ -n "$requested_channel_override" ]]; then
|
|
469
|
+
requested_spec="$requested_channel_override"
|
|
470
|
+
selected_channel="$requested_channel_override"
|
|
471
|
+
else
|
|
472
|
+
case "$recorded_channel" in
|
|
473
|
+
beta) requested_spec="beta"; selected_channel="beta" ;;
|
|
474
|
+
exact:*) requested_spec="${recorded_channel#exact:}"; selected_channel="$recorded_channel" ;;
|
|
475
|
+
latest|"") requested_spec="latest"; selected_channel="latest" ;;
|
|
476
|
+
*) fail "existing update channel metadata is invalid: $recorded_channel" ;;
|
|
477
|
+
esac
|
|
478
|
+
fi
|
|
479
|
+
|
|
480
|
+
if [[ -n "${OYSTERUN_INSTALL_TEST_PACKAGE:-}" ]]; then
|
|
481
|
+
[[ -n "${OYSTERUN_INSTALL_TEST_ROOT:-}" && -n "${OYSTERUN_INSTALL_TEST_VERSION:-}" ]] || fail "isolated test package requires TEST_ROOT and TEST_VERSION"
|
|
482
|
+
[[ "$OYSTERUN_INSTALL_TEST_ROOT" == /* && "$OYSTERUN_INSTALL_TEST_PACKAGE" == /* ]] || fail "isolated test paths must be absolute"
|
|
483
|
+
[[ -d "$OYSTERUN_INSTALL_TEST_ROOT" && ! -L "$OYSTERUN_INSTALL_TEST_ROOT" ]] || fail "isolated test root must be a non-symlink directory"
|
|
484
|
+
[[ -f "$OYSTERUN_INSTALL_TEST_PACKAGE" && ! -L "$OYSTERUN_INSTALL_TEST_PACKAGE" ]] || fail "isolated test package must be a regular non-symlink file"
|
|
485
|
+
test_root="$(cd -P "$OYSTERUN_INSTALL_TEST_ROOT" && pwd -P)"
|
|
486
|
+
test_package_dir="$(cd -P "$(dirname "$OYSTERUN_INSTALL_TEST_PACKAGE")" && pwd -P)"
|
|
487
|
+
test_package_path="$test_package_dir/$(basename "$OYSTERUN_INSTALL_TEST_PACKAGE")"
|
|
488
|
+
[[ "$test_package_path" == "$test_root/"* ]] || fail "isolated test package must be contained by TEST_ROOT"
|
|
489
|
+
[[ "$OYSTERUN_INSTALL_TEST_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]] || fail "isolated test version is invalid"
|
|
490
|
+
selected_version="$OYSTERUN_INSTALL_TEST_VERSION"
|
|
491
|
+
selected_channel="beta"
|
|
492
|
+
package_install_spec="$test_package_path"
|
|
493
|
+
printf '+ isolated verification package accepted inside %q\n' "$test_root"
|
|
494
|
+
elif $dry_run; then
|
|
495
|
+
selected_version="<resolved-$requested_spec-version>"
|
|
496
|
+
package_install_spec="oysterun@$selected_version"
|
|
497
|
+
printf '+ resolve one exact oysterun@%s package version\n' "$requested_spec"
|
|
498
|
+
else
|
|
499
|
+
selected_version="$(capture_user env PATH="$managed_runtime_path" NPM_CONFIG_PREFIX="$npm_prefix" "$node_bin" "$npm_cli" view "oysterun@$requested_spec" version --json | "$node_bin" -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const v=JSON.parse(s);process.stdout.write(Array.isArray(v)?v[v.length-1]:v)})')"
|
|
500
|
+
[[ "$selected_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]] || fail "npm returned an invalid Oysterun version"
|
|
501
|
+
if [[ -n "$installed_version" && -z "$recorded_channel" ]]; then
|
|
502
|
+
comparison="$("$node_bin" -e "$version_compare_script" "$selected_version" "$installed_version")"
|
|
503
|
+
if [[ "$comparison" == "-1" ]]; then
|
|
504
|
+
selected_channel="exact:$installed_version"
|
|
505
|
+
selected_version="$installed_version"
|
|
506
|
+
if ( : </dev/tty ) 2>/dev/null; then
|
|
507
|
+
printf 'This Host runs Oysterun %s.\n' "$installed_version"
|
|
508
|
+
printf 'Public latest is older and would downgrade this persistent Host.\n\n'
|
|
509
|
+
printf ' 1) Keep %s and repair this exact version\n' "$installed_version"
|
|
510
|
+
printf ' 2) Continue on the Beta channel\n'
|
|
511
|
+
printf ' 3) Stop without changes\n\n'
|
|
512
|
+
printf 'Update choice [1]: '
|
|
513
|
+
IFS= read -r downgrade_choice </dev/tty || exit 130
|
|
514
|
+
case "${downgrade_choice:-1}" in
|
|
515
|
+
1) ;;
|
|
516
|
+
2)
|
|
517
|
+
selected_channel="beta"
|
|
518
|
+
selected_version="$(capture_user env PATH="$managed_runtime_path" NPM_CONFIG_PREFIX="$npm_prefix" "$node_bin" "$npm_cli" view oysterun@beta version --json | "$node_bin" -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.stdout.write(JSON.parse(s)))')"
|
|
519
|
+
;;
|
|
520
|
+
3) fail "stopped before version or service changes" ;;
|
|
521
|
+
*) fail "update choice must be 1, 2, or 3" ;;
|
|
522
|
+
esac
|
|
523
|
+
fi
|
|
214
524
|
fi
|
|
215
|
-
run_root apt-get install -y nodejs
|
|
216
525
|
fi
|
|
526
|
+
package_install_spec="oysterun@$selected_version"
|
|
217
527
|
fi
|
|
528
|
+
printf ' ✓ Oysterun release selected: %s (%s)\n' "$selected_version" "$selected_channel"
|
|
218
529
|
|
|
219
|
-
stage="
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
530
|
+
stage="verified package staging"
|
|
531
|
+
driver_prefix="$canonical_home/.local/share/oysterun/bootstrap/$selected_version"
|
|
532
|
+
driver_cli_module="$driver_prefix/lib/node_modules/oysterun/bin/oysterun.mjs"
|
|
533
|
+
run_user mkdir -p "$npm_prefix" "$canonical_home/.local/bin" "$driver_prefix"
|
|
534
|
+
if $dry_run; then
|
|
535
|
+
printf '+ stage and verify oysterun@%s at %q\n' "$selected_version" "$driver_prefix"
|
|
536
|
+
else
|
|
537
|
+
capture_user env PATH="$managed_runtime_path" NPM_CONFIG_PREFIX="$driver_prefix" "$node_bin" "$npm_cli" install -g "$package_install_spec" --prefer-online --no-audit --no-fund
|
|
538
|
+
[[ -f "$driver_cli_module" ]] || fail "staged Oysterun CLI is missing"
|
|
539
|
+
staged_version="$(capture_user "$node_bin" -p "require(process.argv[1]).version" "$driver_prefix/lib/node_modules/oysterun/package.json")"
|
|
540
|
+
[[ "$staged_version" == "$selected_version" ]] || fail "staged Oysterun version does not match the selected release"
|
|
541
|
+
capture_user "$node_bin" --input-type=module - "$driver_prefix/lib/node_modules/oysterun" <<'PORT_CAPABILITY'
|
|
542
|
+
import { pathToFileURL } from 'node:url';
|
|
543
|
+
import path from 'node:path';
|
|
544
|
+
const root = process.argv[2];
|
|
545
|
+
let capability;
|
|
546
|
+
try { capability = await import(pathToFileURL(path.join(root, 'host-service/host-port-resolution.mjs')).href); }
|
|
547
|
+
catch { throw new Error('Selected package cannot preserve configured Host ports. Select a published release with this capability on your chosen channel; no service or active package was changed.'); }
|
|
548
|
+
if (capability.USER_HOST_PORT_CAPABILITY !== 1) throw new Error('Selected package lacks the required persisted-port capability.');
|
|
549
|
+
PORT_CAPABILITY
|
|
550
|
+
if ! $install_only && [[ -n "$setup_input_file" ]]; then
|
|
551
|
+
[[ -f "$driver_prefix/lib/node_modules/oysterun/host-service/setup-input-file.mjs" && -f "$driver_prefix/lib/node_modules/oysterun/tool_scripts/oysterun_setup_input.sh" ]] || fail "Selected package cannot accept --setup-input-file; select a capable release on your chosen channel. No service or active package was changed"
|
|
552
|
+
fi
|
|
553
|
+
capture_user "$node_bin" "$driver_cli_module" --version
|
|
554
|
+
capture_user "$node_bin" -e 'require(process.argv[1]);const Database=require(process.argv[2]);const db=new Database(":memory:");db.prepare("select 1").get();db.close()' "$driver_prefix/lib/node_modules/oysterun/node_modules/node-datachannel" "$driver_prefix/lib/node_modules/oysterun/node_modules/better-sqlite3"
|
|
225
555
|
fi
|
|
556
|
+
printf ' ✓ Package prepared: %s; running Host qualification follows onboarding.\n' "$selected_version"
|
|
226
557
|
|
|
227
|
-
|
|
228
|
-
|
|
558
|
+
service_running=false
|
|
559
|
+
service_healthy=false
|
|
560
|
+
if ! $dry_run && [[ -f "$config_dir/config.json" ]]; then
|
|
561
|
+
stage="existing Host service binding"
|
|
562
|
+
# The parent supplies the contextual failure. Bash 3.2 otherwise inherits
|
|
563
|
+
# ERR inside this guarded command substitution and prints recovery twice.
|
|
564
|
+
staged_status="$(trap - ERR; capture_user env OYSTERUN_CONFIG_DIR="$config_dir" OYSTERUN_NODE_BIN="$node_bin" "$node_bin" "$driver_cli_module" service:status)" || fail "cannot bind the existing Host service; preserve its config/runtime and resolve the reported target mismatch before retrying"
|
|
565
|
+
printf '%s\n' "$staged_status" | grep -q '^health=yes$' && service_healthy=true
|
|
566
|
+
if printf '%s\n' "$staged_status" | grep -q '^loaded=yes$' && printf '%s\n' "$staged_status" | grep -Eq '^pid=[0-9]+$'; then
|
|
567
|
+
service_running=true
|
|
568
|
+
fi
|
|
569
|
+
fi
|
|
229
570
|
|
|
230
|
-
|
|
231
|
-
|
|
571
|
+
# A stopped Host may still own an interrupted update. Preserve its active
|
|
572
|
+
# package and receipts, and use the existing exact staged recovery path before
|
|
573
|
+
# considering an ordinary package install. Explicit reinstall has its own
|
|
574
|
+
# narrower initial-preflight eligibility checks below.
|
|
575
|
+
if ! $dry_run && ! $reinstall && [[ "$service_running" != true && -f "$config_dir/config.json" ]]; then
|
|
576
|
+
retained_blocking="$(capture_user "$node_bin" --input-type=module - "$driver_prefix/lib/node_modules/oysterun" "$config_dir" <<'RETAINED_LIFECYCLE'
|
|
577
|
+
import path from 'node:path';
|
|
578
|
+
import { pathToFileURL } from 'node:url';
|
|
579
|
+
const [driver, configDir] = process.argv.slice(2);
|
|
580
|
+
const { readUpdateOperationState, isUpdateOperationBlocking } = await import(pathToFileURL(path.join(driver, 'host-service/update-reminder-state.mjs')).href);
|
|
581
|
+
process.stdout.write(isUpdateOperationBlocking(readUpdateOperationState({ configDir })) ? 'yes' : 'no');
|
|
582
|
+
RETAINED_LIFECYCLE
|
|
583
|
+
)"
|
|
584
|
+
if [[ "$retained_blocking" == yes ]]; then
|
|
585
|
+
stage="recover retained lifecycle before package replacement"
|
|
586
|
+
capture_user env OYSTERUN_CONFIG_DIR="$config_dir" OYSTERUN_RELEASE_STACK=production \
|
|
587
|
+
"$node_bin" "$driver_prefix/lib/node_modules/oysterun/host-service/cli/recover-installer-lifecycle.mjs" "$config_dir" "$active_package_root"
|
|
588
|
+
staged_status="$(capture_user env OYSTERUN_CONFIG_DIR="$config_dir" OYSTERUN_NODE_BIN="$node_bin" "$node_bin" "$driver_cli_module" service:status)"
|
|
589
|
+
printf '%s\n' "$staged_status" | grep -q '^health=yes$' && service_healthy=true
|
|
590
|
+
if printf '%s\n' "$staged_status" | grep -q '^loaded=yes$' && printf '%s\n' "$staged_status" | grep -Eq '^pid=[0-9]+$'; then
|
|
591
|
+
service_running=true
|
|
592
|
+
fi
|
|
593
|
+
[[ "$service_running" == true && "$service_healthy" == true ]] || fail "retained lifecycle recovery did not restore a healthy bound Host; its package and receipts were preserved"
|
|
594
|
+
installed_version="$(capture_user "$node_bin" -p "require(process.argv[1]).version" "$active_package_root/package.json")"
|
|
595
|
+
active_cli_usable=false
|
|
596
|
+
capture_user "$node_bin" "$active_cli_module" --version >/dev/null && active_cli_usable=true
|
|
597
|
+
fi
|
|
232
598
|
fi
|
|
233
599
|
|
|
234
|
-
|
|
235
|
-
|
|
600
|
+
if $reinstall; then
|
|
601
|
+
stage="preserve-config reinstall preflight"
|
|
602
|
+
printf 'Reinstall requested: running sessions will stop; saved configuration, Cloud identity, history and Agent folders are preserved.\n'
|
|
603
|
+
if $dry_run; then
|
|
604
|
+
printf '+ verify exact installed Host and reject in-progress or post-mutation recovery before service removal\n'
|
|
605
|
+
else
|
|
606
|
+
[[ "$active_cli_usable" == true && -f "$config_dir/config.json" ]] || fail "--reinstall requires the existing usable CLI and Host configuration"
|
|
607
|
+
reinstall_operation="$(capture_user "$node_bin" --input-type=module - "$driver_prefix/lib/node_modules/oysterun" "$active_package_root" "$config_dir" "$installed_version" "$selected_channel" "$canonical_home" "$target_uid" <<'REINSTALL_PREFLIGHT'
|
|
608
|
+
import path from 'node:path';
|
|
609
|
+
import { pathToFileURL } from 'node:url';
|
|
610
|
+
const [driver, active, configDir, version, channel, userHome, uid] = process.argv.slice(2);
|
|
611
|
+
const { inspectInstallerReinstall } = await import(pathToFileURL(path.join(driver, 'host-service/installer-recovery-guidance.mjs')).href);
|
|
612
|
+
const { operationId } = inspectInstallerReinstall({ configDir, activePackage: active, installedVersion: version, selectedChannel: channel, userHome, uid });
|
|
613
|
+
process.stdout.write(operationId);
|
|
614
|
+
REINSTALL_PREFLIGHT
|
|
615
|
+
)"
|
|
616
|
+
fi
|
|
617
|
+
stage="remove existing service while preserving configuration"
|
|
618
|
+
run_user env OYSTERUN_CONFIG_DIR="$config_dir" OYSTERUN_RELEASE_STACK=production OYSTERUN_NODE_BIN="$node_bin" \
|
|
619
|
+
"$node_bin" "$active_cli_module" uninstall --stack production --keep-config
|
|
620
|
+
if ! $dry_run && [[ -n "$reinstall_operation" ]]; then
|
|
621
|
+
stage="reconcile stopped initial-preflight failure"
|
|
622
|
+
run_user env OYSTERUN_CONFIG_DIR="$config_dir" "$node_bin" \
|
|
623
|
+
"$driver_prefix/lib/node_modules/oysterun/host-service/cli/reconcile-initial-preflight.mjs" \
|
|
624
|
+
--config-dir "$config_dir" --operation-id "$reinstall_operation"
|
|
625
|
+
fi
|
|
626
|
+
if ! $dry_run; then
|
|
627
|
+
stage="verify stopped Host before package replacement"
|
|
628
|
+
capture_user "$node_bin" --input-type=module - "$driver_prefix/lib/node_modules/oysterun" "$active_package_root" "$config_dir" <<'REINSTALL_STOPPED'
|
|
629
|
+
import fs from 'node:fs';
|
|
630
|
+
import path from 'node:path';
|
|
631
|
+
import { pathToFileURL } from 'node:url';
|
|
632
|
+
const [driver, active, configDir] = process.argv.slice(2);
|
|
633
|
+
const load = relative => import(pathToFileURL(path.join(driver, 'host-service', relative)).href);
|
|
634
|
+
const { readHostRuntimeMetadata } = await load('host-runtime-diagnostics.mjs');
|
|
635
|
+
const { processDefinitelyAbsent } = await load('initial-preflight-recovery.mjs');
|
|
636
|
+
const { readUpdateOperationState, isUpdateOperationBlocking } = await load('update-reminder-state.mjs');
|
|
637
|
+
const runtime = readHostRuntimeMetadata(configDir);
|
|
638
|
+
if (!runtime || runtime.config_dir !== configDir || runtime.stack_name !== 'production' ||
|
|
639
|
+
fs.realpathSync(runtime.repo_root) !== fs.realpathSync(active) || !processDefinitelyAbsent(runtime.pid)) {
|
|
640
|
+
throw new Error('The original Host has not been proven stopped; its active package was not replaced.');
|
|
641
|
+
}
|
|
642
|
+
if (isUpdateOperationBlocking(readUpdateOperationState({ configDir }))) {
|
|
643
|
+
throw new Error('A lifecycle operation changed during service removal; its evidence and package are preserved.');
|
|
644
|
+
}
|
|
645
|
+
REINSTALL_STOPPED
|
|
646
|
+
fi
|
|
647
|
+
service_running=false
|
|
648
|
+
service_healthy=false
|
|
649
|
+
fi
|
|
236
650
|
|
|
237
|
-
|
|
651
|
+
package_changed=false
|
|
652
|
+
onboard_mode=()
|
|
653
|
+
[[ "$installed_version" != "$selected_version" ]] && package_changed=true
|
|
654
|
+
if [[ "$service_running" == true && "$service_healthy" != true ]]; then
|
|
655
|
+
fail "the existing Host is still running but unhealthy; its package was preserved. Inspect its service logs and use its supported lifecycle recovery before retrying"
|
|
656
|
+
fi
|
|
657
|
+
if [[ "$service_running" == true && "$package_changed" == true && "$selected_channel" == exact:* ]]; then
|
|
658
|
+
fail "the restored running version differs from the selected exact package; its working package was preserved. Choose a compatible update channel explicitly before retrying"
|
|
659
|
+
fi
|
|
660
|
+
if [[ "$service_running" == true && "$service_healthy" == true ]]; then
|
|
661
|
+
if [[ "$package_changed" == true && "$selected_channel" != exact:* ]]; then
|
|
662
|
+
onboard_mode+=(--update-required)
|
|
663
|
+
elif [[ "$active_cli_usable" != true ]]; then
|
|
664
|
+
onboard_mode+=(--repair-required)
|
|
665
|
+
fi
|
|
666
|
+
else
|
|
667
|
+
stage="active package installation"
|
|
668
|
+
if $dry_run; then
|
|
669
|
+
printf '+ install verified oysterun@%s into active user prefix %q\n' "$selected_version" "$npm_prefix"
|
|
670
|
+
else
|
|
671
|
+
capture_user "$node_bin" "$driver_prefix/lib/node_modules/oysterun/host-service/stopped-user-host-package.mjs" "$config_dir"
|
|
672
|
+
capture_user env PATH="$managed_runtime_path" NPM_CONFIG_PREFIX="$npm_prefix" "$node_bin" "$npm_cli" config set prefix "$npm_prefix" --location=user
|
|
673
|
+
capture_user env PATH="$managed_runtime_path" NPM_CONFIG_PREFIX="$npm_prefix" "$node_bin" "$npm_cli" install -g "$package_install_spec" --prefer-online --no-audit --no-fund
|
|
674
|
+
[[ -f "$active_cli_module" ]] || fail "active Oysterun CLI is missing"
|
|
675
|
+
active_version="$(capture_user "$node_bin" -p "require(process.argv[1]).version" "$active_package_root/package.json")"
|
|
676
|
+
[[ "$active_version" == "$selected_version" ]] || fail "active Oysterun version does not match the selected release"
|
|
677
|
+
capture_user "$node_bin" "$active_cli_module" --version
|
|
678
|
+
fi
|
|
679
|
+
if ! $reinstall && [[ "$package_changed" == true ]]; then
|
|
680
|
+
onboard_mode+=(--package-changed)
|
|
681
|
+
fi
|
|
682
|
+
fi
|
|
683
|
+
|
|
684
|
+
if $reinstall; then
|
|
685
|
+
stage="restore installed Host service"
|
|
686
|
+
# Install from the active package, never from the temporary bootstrap prefix.
|
|
687
|
+
# Explicit install recreates launchd and persisted Node/npm identity; start
|
|
688
|
+
# alone would leave macOS in pid-file mode after service uninstall.
|
|
689
|
+
run_user env OYSTERUN_CONFIG_DIR="$config_dir" OYSTERUN_RELEASE_STACK=production OYSTERUN_NODE_BIN="$node_bin" \
|
|
690
|
+
"$node_bin" "$active_cli_module" service:install --stack production
|
|
691
|
+
fi
|
|
692
|
+
|
|
693
|
+
stage="command launcher"
|
|
694
|
+
launcher_dir="$canonical_home/.local/bin"
|
|
695
|
+
launcher_path="$launcher_dir/oysterun"
|
|
696
|
+
launcher_text="#!/bin/sh
|
|
697
|
+
# Managed by Oysterun installer. Do not edit.
|
|
698
|
+
exec \"$node_bin\" \"$active_cli_module\" \"\$@\"
|
|
699
|
+
"
|
|
238
700
|
if $dry_run; then
|
|
239
|
-
printf '+
|
|
701
|
+
printf '+ write ownership-checked managed launcher %q\n' "$launcher_path"
|
|
702
|
+
else
|
|
703
|
+
if [[ -e "$launcher_path" || -L "$launcher_path" ]]; then
|
|
704
|
+
if [[ -f "$launcher_path" ]] && grep -q 'Managed by Oysterun installer' "$launcher_path"; then
|
|
705
|
+
:
|
|
706
|
+
elif [[ -L "$launcher_path" ]] && [[ "$(readlink "$launcher_path")" == "$npm_prefix/bin/oysterun" || "$(readlink "$launcher_path")" == "$active_cli_module" ]]; then
|
|
707
|
+
rm "$launcher_path"
|
|
708
|
+
else
|
|
709
|
+
fail "refused to replace a foreign command at $launcher_path"
|
|
710
|
+
fi
|
|
711
|
+
fi
|
|
712
|
+
if [[ "$(id -u)" -eq 0 ]]; then
|
|
713
|
+
temporary_launcher="$temporary_root/oysterun-user-launcher"
|
|
714
|
+
printf '%s' "$launcher_text" > "$temporary_launcher"
|
|
715
|
+
run_root install -o "$target_uid" -g "$target_gid" -m 755 "$temporary_launcher" "$launcher_path"
|
|
716
|
+
else
|
|
717
|
+
printf '%s' "$launcher_text" > "$launcher_path"
|
|
718
|
+
chmod 755 "$launcher_path"
|
|
719
|
+
fi
|
|
720
|
+
fi
|
|
721
|
+
|
|
722
|
+
path_has_launcher=false
|
|
723
|
+
case ":${PATH:-}:" in *":$launcher_dir:"*) path_has_launcher=true ;; esac
|
|
724
|
+
if [[ "$path_has_launcher" != true ]]; then
|
|
725
|
+
IFS=':' read -r -a inherited_path_parts <<< "${PATH:-}"
|
|
726
|
+
# Bash 3.2 treats an empty array as unset under nounset. Preserve zero
|
|
727
|
+
# arguments (not one empty argument), without disabling nounset globally.
|
|
728
|
+
for candidate_dir in ${inherited_path_parts[@]+"${inherited_path_parts[@]}"}; do
|
|
729
|
+
[[ "$candidate_dir" == "/usr/local/bin" || "$candidate_dir" == "/opt/homebrew/bin" ]] || continue
|
|
730
|
+
candidate="$candidate_dir/oysterun"
|
|
731
|
+
if [[ -e "$candidate" || -L "$candidate" ]]; then
|
|
732
|
+
if [[ -f "$candidate" ]] &&
|
|
733
|
+
grep -q 'Managed by Oysterun installer' "$candidate" &&
|
|
734
|
+
grep -Fq -- "$node_bin" "$candidate" &&
|
|
735
|
+
grep -Fq -- "$active_cli_module" "$candidate"; then
|
|
736
|
+
:
|
|
737
|
+
else
|
|
738
|
+
continue
|
|
739
|
+
fi
|
|
740
|
+
fi
|
|
741
|
+
if $dry_run; then
|
|
742
|
+
printf '+ install narrow system launcher %q using sudo only if required\n' "$candidate"
|
|
743
|
+
else
|
|
744
|
+
temporary_launcher="$temporary_root/oysterun-launcher"
|
|
745
|
+
printf '%s' "$launcher_text" > "$temporary_launcher"
|
|
746
|
+
chmod 755 "$temporary_launcher"
|
|
747
|
+
run_root install -m 755 "$temporary_launcher" "$candidate"
|
|
748
|
+
fi
|
|
749
|
+
launcher_path="$candidate"
|
|
750
|
+
path_has_launcher=true
|
|
751
|
+
break
|
|
752
|
+
done
|
|
753
|
+
fi
|
|
754
|
+
|
|
755
|
+
if [[ "$modify_path" == true ]]; then
|
|
756
|
+
run_user touch "$profile_file"
|
|
757
|
+
managed_begin="# >>> Oysterun managed PATH >>>"
|
|
758
|
+
managed_end="# <<< Oysterun managed PATH <<<"
|
|
759
|
+
if ! $dry_run; then
|
|
760
|
+
capture_user "$node_bin" -e 'const fs=require("fs"),p=process.argv[1],b=process.argv[2],e=process.argv[3],d=process.argv[4],esc=v=>v.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");let s=fs.readFileSync(p,"utf8");s=s.replace(new RegExp(`\\n?${esc(b)}[\\s\\S]*?${esc(e)}\\n?`,"g"),"\n").replace(/\\s*$/,"");fs.writeFileSync(p,`${s}\n\n${b}\nexport PATH="${d}:$PATH"\n${e}\n`)' "$profile_file" "$managed_begin" "$managed_end" "$launcher_dir"
|
|
761
|
+
fi
|
|
762
|
+
printf ' ✓ PATH configured for future terminals: %s\n' "$profile_file"
|
|
240
763
|
else
|
|
241
|
-
|
|
242
|
-
installed_cli="$(user_output 'export PATH="$HOME/.local/npm/bin:$PATH"; command -v oysterun')"
|
|
243
|
-
[[ "$installed_prefix" == "$canonical_home/.local/npm" && "$installed_cli" == "$canonical_home/.local/npm/bin/oysterun" ]] || fail "npm prefix or Oysterun executable is outside the target home"
|
|
244
|
-
if [[ "$platform" == "Darwin" ]]; then cli_uid="$(stat -f %u "$installed_cli")"; else cli_uid="$(stat -c %u "$installed_cli")"; fi
|
|
245
|
-
[[ "$cli_uid" == "$target_uid" ]] || fail "Oysterun executable is not owned by the target user"
|
|
764
|
+
printf ' ○ Shell profile unchanged (--no-modify-path)\n'
|
|
246
765
|
fi
|
|
247
766
|
|
|
248
|
-
stage="
|
|
767
|
+
stage="Oysterun onboarding"
|
|
768
|
+
onboard_cli_module="$driver_cli_module"
|
|
769
|
+
# A staged driver is needed to update an older live Host. Once the selected
|
|
770
|
+
# package is active, setup and service rendering must use that durable package.
|
|
771
|
+
if [[ "$service_running" != true || ( "$package_changed" != true && "$active_cli_usable" == true ) ]]; then
|
|
772
|
+
onboard_cli_module="$active_cli_module"
|
|
773
|
+
fi
|
|
249
774
|
if $install_only; then
|
|
250
|
-
printf 'Oysterun
|
|
775
|
+
printf 'Oysterun files are installed for %s; Host onboarding was intentionally not run (--install-only).\n' "$target_user"
|
|
251
776
|
elif $dry_run; then
|
|
252
|
-
printf '+ user %s
|
|
777
|
+
printf '+ user %s: %q onboard --channel %q%s\n' "$target_user" "$onboard_cli_module" "$selected_channel" "${onboard_mode[*]:+ ${onboard_mode[*]}}"
|
|
778
|
+
else
|
|
779
|
+
# Reinstall and already-current Hosts intentionally have no mode flags.
|
|
780
|
+
# The guarded expansion also works with macOS system Bash 3.2 and set -u.
|
|
781
|
+
[[ -z "$setup_input_file" ]] || onboard_mode+=(--setup-input-file "$setup_input_file")
|
|
782
|
+
onboard_command=(env OYSTERUN_CONFIG_DIR="$config_dir" "$node_bin" "$onboard_cli_module" onboard --channel "$selected_channel" ${onboard_mode[@]+"${onboard_mode[@]}"})
|
|
783
|
+
if [[ "$setup_input_root_memory" == true ]]; then
|
|
784
|
+
# target_uid is the verified existing or canonically created account UID.
|
|
785
|
+
# Two root-owned anonymous read pipes let onboard and setup independently
|
|
786
|
+
# validate the same snapshot, with no disk copy or secret argv/environment.
|
|
787
|
+
run_user env OYSTERUN_SETUP_INPUT_FD=3 OYSTERUN_SETUP_INPUT_NEXT_FD=4 "${onboard_command[@]}" \
|
|
788
|
+
3< <(setup_input_emit_for_uid "$target_uid") 4< <(setup_input_emit_for_uid "$target_uid")
|
|
789
|
+
elif ( : </dev/tty ) 2>/dev/null; then
|
|
790
|
+
run_user "${onboard_command[@]}" </dev/tty
|
|
791
|
+
else
|
|
792
|
+
run_user "${onboard_command[@]}"
|
|
793
|
+
fi
|
|
794
|
+
fi
|
|
795
|
+
|
|
796
|
+
stage="final command verification"
|
|
797
|
+
if ! $dry_run; then
|
|
798
|
+
capture_user "$launcher_path" --version
|
|
799
|
+
fi
|
|
800
|
+
printf ' ✓ Oysterun command verified: %s\n' "$launcher_path"
|
|
801
|
+
if [[ "$path_has_launcher" == true ]]; then
|
|
802
|
+
printf 'Current terminal command: oysterun\n'
|
|
253
803
|
else
|
|
254
|
-
printf '
|
|
255
|
-
run_user_shell 'export PATH="$HOME/.local/npm/bin:$PATH"; exec oysterun'
|
|
804
|
+
printf 'Current terminal: export PATH="%s:$PATH"\n' "$launcher_dir"
|
|
256
805
|
fi
|