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
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
getConfigPath,
|
|
6
6
|
readRawConfigSource,
|
|
7
7
|
} from "../config.mjs";
|
|
8
|
+
import { parseSessionTagsCliValue } from "../session-tags.mjs";
|
|
9
|
+
import { parseSchedulerRunTimeoutDuration } from "../scheduler-run-timeout-policy.mjs";
|
|
8
10
|
|
|
9
11
|
export const P86_CLI_CONTRACT = "p86_product_cli_module_action_v1";
|
|
10
12
|
export const P86_CLI_PROFILE_CONTRACT = "p86_cli_profile_0600_token_fallback_v1";
|
|
@@ -610,10 +612,10 @@ async function readCurrentSessionMessagesAround({ options, env, fetchFn }) {
|
|
|
610
612
|
session_id: resolvedSessionId,
|
|
611
613
|
session_ref: body?.session_ref || sessionTarget.session_ref || null,
|
|
612
614
|
agent_id: normalizeString(body?.agent_id) || agentId,
|
|
613
|
-
cross_session_search: crossSessionContext,
|
|
614
|
-
context_surface: crossSessionContext
|
|
615
|
+
cross_session_search: body?.cross_session_search ?? crossSessionContext,
|
|
616
|
+
context_surface: body?.context_surface || (crossSessionContext
|
|
615
617
|
? "session_messages_matrix_target_session"
|
|
616
|
-
: "session_messages_matrix_current_session",
|
|
618
|
+
: "session_messages_matrix_current_session"),
|
|
617
619
|
};
|
|
618
620
|
}
|
|
619
621
|
|
|
@@ -671,6 +673,14 @@ function boolPayload(options, sourceKey, targetKey, payload) {
|
|
|
671
673
|
}
|
|
672
674
|
}
|
|
673
675
|
|
|
676
|
+
function applySessionTagsOptions(options, body) {
|
|
677
|
+
const hasTags = Object.prototype.hasOwnProperty.call(options, "tags");
|
|
678
|
+
const clearTags = parseBooleanOption(options, "clearTags", false);
|
|
679
|
+
if (hasTags && clearTags) fail("--tags and --clear-tags are mutually exclusive");
|
|
680
|
+
if (hasTags) body.tags = parseSessionTagsCliValue(options.tags);
|
|
681
|
+
if (clearTags) body.tags = [];
|
|
682
|
+
}
|
|
683
|
+
|
|
674
684
|
function sessionStartPayload(options, env = process.env) {
|
|
675
685
|
const body = {
|
|
676
686
|
agent_id: requireAgentId(options, env),
|
|
@@ -706,6 +716,7 @@ function sessionStartPayload(options, env = process.env) {
|
|
|
706
716
|
body.telegram_allowed_users =
|
|
707
717
|
allowedUsers === "." ? ["."] : allowedUsers.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
708
718
|
}
|
|
719
|
+
applySessionTagsOptions(options, body);
|
|
709
720
|
return body;
|
|
710
721
|
}
|
|
711
722
|
|
|
@@ -720,6 +731,7 @@ function commaListOption(options, ...keys) {
|
|
|
720
731
|
|
|
721
732
|
function sessionProfileUpdatePayload(options, env = process.env) {
|
|
722
733
|
const body = {};
|
|
734
|
+
applySessionTagsOptions(options, body);
|
|
723
735
|
boolPayload(options, "notificationsEnabled", "notifications_enabled", body);
|
|
724
736
|
boolPayload(options, "webEnabled", "web_enabled", body);
|
|
725
737
|
const webAccess = optionalOption(options, "webAccess", "access");
|
|
@@ -868,40 +880,15 @@ function resolveSessionUrlIdFromStatus(status, sessionTarget) {
|
|
|
868
880
|
return resolvedSessionId;
|
|
869
881
|
}
|
|
870
882
|
|
|
871
|
-
async function resolveSessionMessagesTarget({ options, env
|
|
883
|
+
async function resolveSessionMessagesTarget({ options, env }) {
|
|
872
884
|
const sessionTarget = requireSessionTarget(options, env);
|
|
885
|
+
// Host resolves durable target ownership. Explicit agent is only an assertion.
|
|
873
886
|
const explicitAgentId = resolveAgentId(options, {});
|
|
874
|
-
const
|
|
875
|
-
const envAgentId = normalizeString(env.OYSTERUN_AGENT_ID);
|
|
876
|
-
if (
|
|
877
|
-
sessionTarget.session_id &&
|
|
878
|
-
(explicitAgentId || (envAgentId && sessionTarget.session_id === envSessionId))
|
|
879
|
-
) {
|
|
880
|
-
return {
|
|
881
|
-
sessionTarget,
|
|
882
|
-
sessionId: sessionTarget.session_id,
|
|
883
|
-
agentId: explicitAgentId || envAgentId,
|
|
884
|
-
};
|
|
885
|
-
}
|
|
886
|
-
const status = await resolveLiveSessionTargetStatus({
|
|
887
|
-
options,
|
|
888
|
-
env,
|
|
889
|
-
fetchFn,
|
|
890
|
-
sessionTarget,
|
|
891
|
-
});
|
|
892
|
-
const statusAgentId = normalizeString(status?.agent_id);
|
|
893
|
-
if (explicitAgentId && statusAgentId && explicitAgentId !== statusAgentId) {
|
|
894
|
-
fail("target session agent id does not match explicit --agent-id");
|
|
895
|
-
}
|
|
896
|
-
const agentId =
|
|
897
|
-
explicitAgentId ||
|
|
898
|
-
statusAgentId ||
|
|
899
|
-
resolveAgentId(options, env);
|
|
900
|
-
if (!agentId) fail("target session agent id is required");
|
|
887
|
+
const sameCurrent = sessionTarget.session_id === normalizeString(env.OYSTERUN_SESSION_ID);
|
|
901
888
|
return {
|
|
902
889
|
sessionTarget,
|
|
903
|
-
sessionId:
|
|
904
|
-
agentId,
|
|
890
|
+
sessionId: sessionTarget.session_id || null,
|
|
891
|
+
agentId: explicitAgentId || (sameCurrent ? normalizeString(env.OYSTERUN_AGENT_ID) : ""),
|
|
905
892
|
};
|
|
906
893
|
}
|
|
907
894
|
|
|
@@ -1052,6 +1039,7 @@ async function runSessions({ action, rest, options, env, fetchFn }) {
|
|
|
1052
1039
|
session_id: status.session_id || sessionTarget.session_id || null,
|
|
1053
1040
|
session_ref: sessionTarget.session_ref || null,
|
|
1054
1041
|
session_name: status.session_name || null,
|
|
1042
|
+
tags: Array.isArray(status.tags) ? status.tags : [],
|
|
1055
1043
|
agent_id: status.agent_id || null,
|
|
1056
1044
|
profile_config: status.profile_config || {},
|
|
1057
1045
|
notification_settings: status.notification_settings || null,
|
|
@@ -1103,6 +1091,9 @@ async function runSessions({ action, rest, options, env, fetchFn }) {
|
|
|
1103
1091
|
})).data;
|
|
1104
1092
|
result.profile_config_update = profileResult;
|
|
1105
1093
|
result.session_id = profileResult.session_id || result.session_id;
|
|
1094
|
+
result.tags = Array.isArray(profileResult.tags)
|
|
1095
|
+
? profileResult.tags
|
|
1096
|
+
: [];
|
|
1106
1097
|
}
|
|
1107
1098
|
if (!sessionName && !hasSessionProfileUpdateBody(profileBody)) {
|
|
1108
1099
|
fail("profile update requires at least one typed field");
|
|
@@ -1362,6 +1353,7 @@ async function runChat({ action, rest, options, env, fetchFn }) {
|
|
|
1362
1353
|
agent_id: targetContext.agentId,
|
|
1363
1354
|
limit: optionalOption(options, "limit"),
|
|
1364
1355
|
after_seq: optionalOption(options, "afterSeq"),
|
|
1356
|
+
next_batch: optionalOption(options, "nextBatch"),
|
|
1365
1357
|
})}`,
|
|
1366
1358
|
options,
|
|
1367
1359
|
env,
|
|
@@ -1471,6 +1463,15 @@ function schedulerPayload(options) {
|
|
|
1471
1463
|
const weekdays = commaListOption(options, "weekdays", "selectedWeekdays");
|
|
1472
1464
|
if (weekdays) body.weekdays = weekdays;
|
|
1473
1465
|
boolPayload(options, "enabled", "enabled", body);
|
|
1466
|
+
if (Object.prototype.hasOwnProperty.call(options, "runTimeout")) {
|
|
1467
|
+
try {
|
|
1468
|
+
body.run_timeout_seconds = parseSchedulerRunTimeoutDuration(
|
|
1469
|
+
options.runTimeout
|
|
1470
|
+
);
|
|
1471
|
+
} catch (error) {
|
|
1472
|
+
fail(error.message);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1474
1475
|
return body;
|
|
1475
1476
|
}
|
|
1476
1477
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Offline compatibility entry for Hosts predating initial-preflight terminalization.
|
|
3
|
+
// The service must be stopped; this never owns a restore, install, or snapshot.
|
|
4
|
+
import { readHostRuntimeMetadata } from "../host-runtime-diagnostics.mjs";
|
|
5
|
+
import { processDefinitelyAbsent, reconcileFailedInitialPreflight } from "../initial-preflight-recovery.mjs";
|
|
6
|
+
import { isUpdateOperationBlocking } from "../update-reminder-state.mjs";
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
if (args.length !== 4 || args[0] !== "--config-dir" || args[2] !== "--operation-id") {
|
|
9
|
+
throw new Error("Required: --config-dir <exact stopped Host config> --operation-id <exact failed attempt>");
|
|
10
|
+
}
|
|
11
|
+
const configDir = args[1];
|
|
12
|
+
const runtime = readHostRuntimeMetadata(configDir);
|
|
13
|
+
if (!runtime || !processDefinitelyAbsent(runtime.pid)) {
|
|
14
|
+
throw new Error("Offline reconciliation requires affirmative stopped Host process evidence.");
|
|
15
|
+
}
|
|
16
|
+
const operation = reconcileFailedInitialPreflight({ configDir, currentVersion: runtime.package_version, expectedOperationId: args[3] });
|
|
17
|
+
if (isUpdateOperationBlocking(operation) || operation.status !== "update_check_failed" ||
|
|
18
|
+
operation.reconciliation_outcome !== "abandoned_initial_preflight_failed") throw new Error("Retained operation is not a proven abandoned initial-preflight failure.");
|
|
19
|
+
console.log(JSON.stringify({ operation_id: operation.operation_id, status: operation.status, reconciliation_outcome: operation.reconciliation_outcome }));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { readUserHostPort } from "../host-port-resolution.mjs";
|
|
4
|
+
import { readUpdateOperationState, isUpdateOperationBlocking } from "../update-reminder-state.mjs";
|
|
5
|
+
import { continuityReceiptPath } from "../lifecycle-continuity.mjs";
|
|
6
|
+
import { runStagedLifecycleRecovery } from "./run-staged-lifecycle-recovery.mjs";
|
|
7
|
+
try {
|
|
8
|
+
const [configDir, activePackage] = process.argv.slice(2);
|
|
9
|
+
const port = readUserHostPort({ configPath: join(configDir, "config.json"), runtimePath: join(configDir, "run", "host-runtime.json"), configDir });
|
|
10
|
+
const operation = readUpdateOperationState({ configDir });
|
|
11
|
+
if (operation.continuity_path !== continuityReceiptPath(configDir, operation.operation_id)) throw new Error("Retained lifecycle has no exact continuity receipt; preserve it and use its reported recovery path.");
|
|
12
|
+
const receipt = JSON.parse(readFileSync(operation.continuity_path, "utf8"));
|
|
13
|
+
if (receipt.baseline?.host?.port !== port ||
|
|
14
|
+
resolve(receipt.baseline?.host?.config_dir || "") !== resolve(configDir) ||
|
|
15
|
+
resolve(receipt.baseline?.host?.package_root || "") !== resolve(activePackage) ||
|
|
16
|
+
resolve(receipt.resources?.active_package_root || "") !== resolve(activePackage)) {
|
|
17
|
+
throw new Error("Retained recovery differs from the selected Host port/config/package; its package was preserved.");
|
|
18
|
+
}
|
|
19
|
+
await runStagedLifecycleRecovery({ configDir, stack: "production", port: String(port) });
|
|
20
|
+
if (isUpdateOperationBlocking(readUpdateOperationState({ configDir }))) throw new Error("Retained lifecycle cleanup is still blocking; preserve its package and receipts.");
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error(`Installer lifecycle recovery failed: ${error.message}`);
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
}
|
|
@@ -61,6 +61,8 @@ function printBoundedStatus(payload) {
|
|
|
61
61
|
const operation = payload.update_operation || payload.operation || {};
|
|
62
62
|
const fields = {
|
|
63
63
|
status: payload.status || operation.status || "unknown",
|
|
64
|
+
operation_status:
|
|
65
|
+
operation.status || payload.operation_status || null,
|
|
64
66
|
operation_id: operation.operation_id || payload.operation_id || null,
|
|
65
67
|
transaction_kind:
|
|
66
68
|
operation.transaction_kind || payload.transaction_kind || null,
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
import { buildHostLifecycleRecoveryPlan } from "../host-lifecycle-recovery.mjs";
|
|
8
|
+
import { readContinuity, continuityReceiptPath } from "../lifecycle-continuity.mjs";
|
|
9
|
+
import {
|
|
10
|
+
verifyPersistedNodeNpmRuntimeIdentityAuthoritatively,
|
|
11
|
+
} from "../node-npm-runtime-identity.mjs";
|
|
12
|
+
import {
|
|
13
|
+
readUpdateOperationState,
|
|
14
|
+
} from "../update-reminder-state.mjs";
|
|
15
|
+
import { runHostLifecycleTransaction } from "../../tool_scripts/oysterun_update_restart_runner.mjs";
|
|
16
|
+
|
|
17
|
+
function normalizeString(value) {
|
|
18
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isProcessRunning(pid) {
|
|
22
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
23
|
+
try {
|
|
24
|
+
process.kill(pid, 0);
|
|
25
|
+
return true;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error?.code === "ESRCH") return false;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseJsonObject(path, label) {
|
|
33
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
34
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
35
|
+
throw new Error(`${label} must contain a JSON object`);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function runStagedLifecycleRecovery({
|
|
41
|
+
configDir = process.env.OYSTERUN_CONFIG_DIR,
|
|
42
|
+
stack = process.env.OYSTERUN_RELEASE_STACK || "production",
|
|
43
|
+
port = process.env.OYSTERUN_PORT || null,
|
|
44
|
+
readOperation = readUpdateOperationState,
|
|
45
|
+
readReceipt = readContinuity,
|
|
46
|
+
buildRecoveryPlan = buildHostLifecycleRecoveryPlan,
|
|
47
|
+
verifyIdentity = verifyPersistedNodeNpmRuntimeIdentityAuthoritatively,
|
|
48
|
+
readPreparation = parseJsonObject,
|
|
49
|
+
runnerIsAlive = isProcessRunning,
|
|
50
|
+
runTransaction = runHostLifecycleTransaction,
|
|
51
|
+
} = {}) {
|
|
52
|
+
const exactConfigDir = normalizeString(configDir);
|
|
53
|
+
if (!exactConfigDir || resolve(exactConfigDir) !== exactConfigDir) {
|
|
54
|
+
throw new Error("OYSTERUN_CONFIG_DIR must be an absolute path");
|
|
55
|
+
}
|
|
56
|
+
const operation = readOperation({ configDir: exactConfigDir });
|
|
57
|
+
const expectedContinuityPath = continuityReceiptPath(
|
|
58
|
+
exactConfigDir,
|
|
59
|
+
operation.operation_id
|
|
60
|
+
);
|
|
61
|
+
if (
|
|
62
|
+
!operation.operation_id ||
|
|
63
|
+
operation.continuity_path !== expectedContinuityPath
|
|
64
|
+
) {
|
|
65
|
+
throw new Error("retained lifecycle operation has no exact continuity binding");
|
|
66
|
+
}
|
|
67
|
+
const receipt = readReceipt(
|
|
68
|
+
operation.continuity_path,
|
|
69
|
+
operation.operation_id
|
|
70
|
+
);
|
|
71
|
+
const runnerAlive = runnerIsAlive(operation.runner_pid);
|
|
72
|
+
const recoveryPlan = buildRecoveryPlan({
|
|
73
|
+
operation,
|
|
74
|
+
continuityReceipt: receipt,
|
|
75
|
+
runnerAlive,
|
|
76
|
+
});
|
|
77
|
+
if (
|
|
78
|
+
recoveryPlan?.blocking !== true ||
|
|
79
|
+
recoveryPlan?.user_action?.kind !== "restore_and_restart" ||
|
|
80
|
+
operation.running === true ||
|
|
81
|
+
runnerAlive
|
|
82
|
+
) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
"retained lifecycle operation is not eligible for staged restore"
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const baseline = receipt.baseline?.host || {};
|
|
88
|
+
const resources = receipt.resources || {};
|
|
89
|
+
const exactPort = Number(port || baseline.port);
|
|
90
|
+
if (
|
|
91
|
+
receipt.transaction_kind === "restart_only" ||
|
|
92
|
+
resolve(baseline.config_dir || "") !== exactConfigDir ||
|
|
93
|
+
baseline.stack !== stack ||
|
|
94
|
+
!Number.isInteger(exactPort) ||
|
|
95
|
+
exactPort !== baseline.port ||
|
|
96
|
+
resolve(resources.active_package_root || "") !==
|
|
97
|
+
resolve(baseline.package_root || "") ||
|
|
98
|
+
operation.result_path !== resources.result_path
|
|
99
|
+
) {
|
|
100
|
+
throw new Error("staged recovery geometry differs from the retained Host");
|
|
101
|
+
}
|
|
102
|
+
const preparation = readPreparation(
|
|
103
|
+
operation.result_path,
|
|
104
|
+
"retained update preparation receipt"
|
|
105
|
+
);
|
|
106
|
+
if (
|
|
107
|
+
preparation.operation_id !== operation.operation_id ||
|
|
108
|
+
resolve(preparation.active_package_root || "") !==
|
|
109
|
+
resolve(resources.active_package_root)
|
|
110
|
+
) {
|
|
111
|
+
throw new Error("retained preparation receipt differs from the operation");
|
|
112
|
+
}
|
|
113
|
+
const identity = verifyIdentity({
|
|
114
|
+
configDir: exactConfigDir,
|
|
115
|
+
});
|
|
116
|
+
if (
|
|
117
|
+
identity.toolchain_identity_digest !== receipt.source?.runtime_identity_digest
|
|
118
|
+
) {
|
|
119
|
+
throw new Error("retained recovery Node/npm identity changed");
|
|
120
|
+
}
|
|
121
|
+
const result = await runTransaction({
|
|
122
|
+
transaction_kind: receipt.transaction_kind,
|
|
123
|
+
operation_id: operation.operation_id,
|
|
124
|
+
active_package_root: resources.active_package_root,
|
|
125
|
+
config_dir: exactConfigDir,
|
|
126
|
+
node_bin: identity.node_path,
|
|
127
|
+
npm_cli: identity.npm_cli_path,
|
|
128
|
+
identity_digest: identity.toolchain_identity_digest,
|
|
129
|
+
stack,
|
|
130
|
+
health_url: `http://127.0.0.1:${exactPort}/health`,
|
|
131
|
+
result_path: operation.result_path,
|
|
132
|
+
restart_id: receipt.restart_id,
|
|
133
|
+
login_gate_id: operation.login_gate_id || null,
|
|
134
|
+
continuity_path: operation.continuity_path,
|
|
135
|
+
restore_before_restart: "1",
|
|
136
|
+
});
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function main() {
|
|
141
|
+
const result = await runStagedLifecycleRecovery();
|
|
142
|
+
process.stdout.write(
|
|
143
|
+
`${JSON.stringify({
|
|
144
|
+
status: result?.status || "recovered",
|
|
145
|
+
operation_id: result?.operation_id || null,
|
|
146
|
+
operation_cleanup_completed:
|
|
147
|
+
result?.operation_cleanup_completed === true,
|
|
148
|
+
secret_fields_redacted: true,
|
|
149
|
+
}, null, 2)}\n`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (
|
|
154
|
+
process.argv[1] &&
|
|
155
|
+
resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))
|
|
156
|
+
) {
|
|
157
|
+
main().catch((error) => {
|
|
158
|
+
process.stderr.write(`[oysterun-recovery] ${error.message}\n`);
|
|
159
|
+
process.exitCode = 1;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ownDiscoveryProcess, spawnDiscoveryProcess, collectDiscoveryCleanup } from "./model-discovery-process.mjs";
|
|
2
2
|
import { execFileSync } from "child_process";
|
|
3
3
|
import { EventEmitter } from "events";
|
|
4
4
|
import { existsSync } from "fs";
|
|
@@ -89,6 +89,7 @@ class JsonRpcLineClient extends EventEmitter {
|
|
|
89
89
|
this.stdoutBuffer = "";
|
|
90
90
|
this.nextId = 1;
|
|
91
91
|
this.pending = new Map();
|
|
92
|
+
this.terminalError = null;
|
|
92
93
|
proc.stdout.on("data", (chunk) => this.consumeStdout(String(chunk)));
|
|
93
94
|
proc.stderr.on("data", (chunk) => {
|
|
94
95
|
this.emit("stderr", String(chunk));
|
|
@@ -98,20 +99,16 @@ class JsonRpcLineClient extends EventEmitter {
|
|
|
98
99
|
"codex_app_server_exit",
|
|
99
100
|
`Codex app-server exited during discovery (code=${code}, signal=${signal || "none"})`
|
|
100
101
|
);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
this.pending.clear();
|
|
102
|
+
this.terminalError ||= error;
|
|
103
|
+
this.failPending(this.terminalError);
|
|
105
104
|
});
|
|
106
105
|
proc.on("error", (err) => {
|
|
107
106
|
const error = new CodexModelDiscoveryError(
|
|
108
107
|
"codex_app_server_error",
|
|
109
108
|
err.message
|
|
110
109
|
);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
this.pending.clear();
|
|
110
|
+
this.terminalError ||= error;
|
|
111
|
+
this.failPending(this.terminalError);
|
|
115
112
|
});
|
|
116
113
|
}
|
|
117
114
|
|
|
@@ -169,9 +166,11 @@ class JsonRpcLineClient extends EventEmitter {
|
|
|
169
166
|
|
|
170
167
|
request(method, params = {}) {
|
|
171
168
|
const id = String(this.nextId++);
|
|
172
|
-
this.writeJson({ jsonrpc: "2.0", id, method, params });
|
|
173
169
|
return new Promise((resolve, reject) => {
|
|
170
|
+
if (this.terminalError) { reject(this.terminalError); return; }
|
|
174
171
|
this.pending.set(id, { resolve, reject });
|
|
172
|
+
try { this.writeJson({ jsonrpc: "2.0", id, method, params }); }
|
|
173
|
+
catch (error) { this.pending.delete(id); reject(error); }
|
|
175
174
|
});
|
|
176
175
|
}
|
|
177
176
|
|
|
@@ -180,6 +179,7 @@ class JsonRpcLineClient extends EventEmitter {
|
|
|
180
179
|
}
|
|
181
180
|
|
|
182
181
|
writeJson(payload) {
|
|
182
|
+
if (this.terminalError) throw this.terminalError;
|
|
183
183
|
if (!this.proc.stdin?.writable) {
|
|
184
184
|
throw new CodexModelDiscoveryError(
|
|
185
185
|
"codex_app_server_stdin_unavailable",
|
|
@@ -189,11 +189,6 @@ class JsonRpcLineClient extends EventEmitter {
|
|
|
189
189
|
this.proc.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
close() {
|
|
193
|
-
if (typeof this.proc.kill === "function") {
|
|
194
|
-
this.proc.kill("SIGTERM");
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
192
|
}
|
|
198
193
|
|
|
199
194
|
function extractModelEntries(result) {
|
|
@@ -249,7 +244,7 @@ export async function discoverCodexProviderModels(options = {}) {
|
|
|
249
244
|
? options.timeoutMs
|
|
250
245
|
: 5000;
|
|
251
246
|
const maxPages = Number.isInteger(options.maxPages) ? options.maxPages : 20;
|
|
252
|
-
const spawnFn = options.spawnFn ||
|
|
247
|
+
const spawnFn = options.spawnFn || spawnDiscoveryProcess;
|
|
253
248
|
const commandResolution = resolveCodexDiscoveryCommand(options);
|
|
254
249
|
const proc = spawnFn(commandResolution.resolved_command, [
|
|
255
250
|
"app-server",
|
|
@@ -258,10 +253,15 @@ export async function discoverCodexProviderModels(options = {}) {
|
|
|
258
253
|
], {
|
|
259
254
|
cwd: options.cwd || process.cwd(),
|
|
260
255
|
stdio: ["pipe", "pipe", "pipe"],
|
|
256
|
+
detached: true,
|
|
257
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
261
258
|
env: { ...process.env, ...(options.env || {}) },
|
|
262
259
|
});
|
|
263
|
-
const
|
|
260
|
+
const ownedProcess = ownDiscoveryProcess(proc, options.processCleanup);
|
|
261
|
+
const processCleanup = {};
|
|
262
|
+
let discoveryError;
|
|
264
263
|
try {
|
|
264
|
+
const client = new JsonRpcLineClient(proc);
|
|
265
265
|
await withTimeout(
|
|
266
266
|
client.request("initialize", {
|
|
267
267
|
clientInfo: {
|
|
@@ -300,6 +300,7 @@ export async function discoverCodexProviderModels(options = {}) {
|
|
|
300
300
|
rawModels.map(normalizeCodexDiscoveryModel)
|
|
301
301
|
);
|
|
302
302
|
return {
|
|
303
|
+
process_cleanup: processCleanup,
|
|
303
304
|
provider: "codex",
|
|
304
305
|
command_available: true,
|
|
305
306
|
resolved_command_redacted: redactCommand(
|
|
@@ -315,7 +316,11 @@ export async function discoverCodexProviderModels(options = {}) {
|
|
|
315
316
|
"codex_model_list_pagination_limit",
|
|
316
317
|
`Codex model/list exceeded ${maxPages} pages`
|
|
317
318
|
);
|
|
319
|
+
} catch (err) {
|
|
320
|
+
discoveryError = err;
|
|
321
|
+
throw err;
|
|
318
322
|
} finally {
|
|
319
|
-
|
|
323
|
+
Object.assign(processCleanup, await collectDiscoveryCleanup(ownedProcess));
|
|
324
|
+
if (discoveryError) discoveryError.details = { ...discoveryError.details, process_cleanup: processCleanup };
|
|
320
325
|
}
|
|
321
326
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { constants, accessSync, existsSync, lstatSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
|
+
import { userInfo } from "node:os";
|
|
6
|
+
import { resolveNodeNpmRuntimeIdentity, buildNodeNpmChildEnvironment, assertNodeNpmRuntimeIdentityMatches } from "./node-npm-runtime-identity.mjs";
|
|
7
|
+
|
|
8
|
+
function inside(parent, child) {
|
|
9
|
+
const r = relative(parent, child);
|
|
10
|
+
return !r || (!r.startsWith("..") && !isAbsolute(r));
|
|
11
|
+
}
|
|
12
|
+
function canonical(path) {
|
|
13
|
+
if (!isAbsolute(path) || /[\r\n\t\0]/u.test(path)) throw new Error("Uninstall requires exact absolute paths without control characters.");
|
|
14
|
+
if (existsSync(path)) return realpathSync(path);
|
|
15
|
+
return join(canonical(dirname(path)), path.slice(dirname(path).length + 1));
|
|
16
|
+
}
|
|
17
|
+
function checkedRun(spawn, command, args, options) {
|
|
18
|
+
const r = spawn(command, args, { encoding: "utf8", ...options });
|
|
19
|
+
if (r.error || r.status !== 0) throw new Error(`${command}: ${r.error?.message || String(r.stderr || r.stdout || `exit ${r.status}`).trim()}`);
|
|
20
|
+
return String(r.stdout || "").trim();
|
|
21
|
+
}
|
|
22
|
+
function writableRemoval(path, uid) {
|
|
23
|
+
if (!existsSync(path)) return;
|
|
24
|
+
if (statSync(path).uid !== uid) throw new Error(`Selected removal target is not owned by the running OS user: ${path}`);
|
|
25
|
+
accessSync(dirname(path), constants.W_OK | constants.X_OK);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// npm enables Node's compile cache even for --version. Keep this policy local
|
|
29
|
+
// to identity/resolver probes; lifecycle and the actual npm uninstall retain
|
|
30
|
+
// the caller's normal child environment.
|
|
31
|
+
export function buildUninstallProbeEnvironment(env) {
|
|
32
|
+
const probeEnv = { ...env, NODE_DISABLE_COMPILE_CACHE: "1" };
|
|
33
|
+
delete probeEnv.NODE_COMPILE_CACHE;
|
|
34
|
+
return probeEnv;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolveLaunchingCli({ cliPath, env = process.env, spawn = spawnSync } = {}) {
|
|
38
|
+
const cli = realpathSync(cliPath);
|
|
39
|
+
const root = dirname(dirname(cli));
|
|
40
|
+
const prefix = dirname(dirname(dirname(root)));
|
|
41
|
+
if (root !== join(prefix, "lib", "node_modules", "oysterun") || cli !== join(root, "bin", "oysterun.mjs")) {
|
|
42
|
+
throw new Error("Complete uninstall requires the installed global Oysterun CLI, not a checkout or npm link. Run the exact installed prefix/bin/oysterun.");
|
|
43
|
+
}
|
|
44
|
+
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
45
|
+
const bin = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.oysterun;
|
|
46
|
+
if (manifest.name !== "oysterun" || bin !== "bin/oysterun.mjs" || canonical(join(prefix, "bin", "oysterun")) !== cli) {
|
|
47
|
+
throw new Error("Launching Oysterun package/bin identity disagrees; repair this exact global installation before uninstalling.");
|
|
48
|
+
}
|
|
49
|
+
const identity = resolveNodeNpmRuntimeIdentity({ nodeBin: process.execPath, env: buildUninstallProbeEnvironment(env), spawnSyncFn: spawn });
|
|
50
|
+
const childEnv = buildNodeNpmChildEnvironment(identity, env);
|
|
51
|
+
const probeEnv = buildUninstallProbeEnvironment(childEnv);
|
|
52
|
+
// The removal arguments remain unchanged. Read-only npm root must also avoid
|
|
53
|
+
// cache directory creation, log cleanup, timing output and update checks.
|
|
54
|
+
// /dev/null is a non-directory on the supported Unix global-install layout.
|
|
55
|
+
const npmArgs = ["--global", "--prefix", prefix, "--ignore-scripts", "--logs-max=0", "--no-audit", "--no-fund"];
|
|
56
|
+
const modules = checkedRun(spawn, identity.node_path, [identity.npm_cli_path, "root", ...npmArgs, "--cache=/dev/null", "--logs-dir=/dev/null", "--update-notifier=false", "--timing=false"], { env: probeEnv });
|
|
57
|
+
if (canonical(modules) !== canonical(join(prefix, "lib", "node_modules")) || canonical(join(modules, "oysterun")) !== root) {
|
|
58
|
+
throw new Error("Matching npm does not address the launching Oysterun prefix; no service or config was changed.");
|
|
59
|
+
}
|
|
60
|
+
const osUser = userInfo();
|
|
61
|
+
if (process.getuid() !== osUser.uid || process.geteuid() !== osUser.uid) throw new Error("Running OS user identity is ambiguous; run uninstall as the installation owner without escalation.");
|
|
62
|
+
writableRemoval(root, osUser.uid);
|
|
63
|
+
accessSync(join(prefix, "bin"), constants.W_OK | constants.X_OK);
|
|
64
|
+
return { cli, root, prefix, manifestSha256: createHash("sha256").update(readFileSync(join(root, "package.json"))).digest("hex"), bin: join(prefix, "bin", "oysterun"), identity, childEnv, probeEnv, user: { uid: osUser.uid, name: osUser.username }, npmArgs };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// This only reads the canonical service resolver; it never executes lifecycle.
|
|
68
|
+
export function resolveUninstallServices({ repoRoot, stack, env, spawn = spawnSync }) {
|
|
69
|
+
const stacks = stack === "all" ? ["production", "staging"] : [stack];
|
|
70
|
+
const source = `source "$1"; shift
|
|
71
|
+
for target in "$@"; do
|
|
72
|
+
STACK_NAME="$target"; configure_stack_runtime
|
|
73
|
+
printf '%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\n' "$STACK_NAME" "$CONFIG_DIR" "$HOST_LABEL" "$HOST_PORT" "$HOST_PID_FILE" "$HOST_ORIGIN_FILE" "$HOME/Library/LaunchAgents/$HOST_LABEL.plist"
|
|
74
|
+
done`;
|
|
75
|
+
const raw = checkedRun(spawn, "/bin/bash", ["--noprofile", "--norc", "-eu", "-c", source, "--", join(repoRoot, "tool_scripts", "oysterun_service_common.sh"), ...stacks], { env, cwd: repoRoot });
|
|
76
|
+
const targets = raw.split("\n").map(line => {
|
|
77
|
+
const [name, config, label, port, pidFile, originFile, plist] = line.split("\t");
|
|
78
|
+
if (!name || !config || !label || !pidFile || !originFile || !plist) throw new Error("Canonical service resolver returned an incomplete uninstall target.");
|
|
79
|
+
return { name, config: canonical(config), label, port, pidFile, originFile, plist,
|
|
80
|
+
pid: existsSync(pidFile) ? Number(readFileSync(pidFile, "utf8").trim()) : null };
|
|
81
|
+
});
|
|
82
|
+
if (targets.length !== stacks.length || targets.some((t, i) => t.name !== stacks[i])) throw new Error("Canonical uninstall stack selection drifted.");
|
|
83
|
+
return targets;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function preflightCompleteUninstall({ cliPath, configDir, stack, env = process.env, spawn = spawnSync, resolveServices = resolveUninstallServices }) {
|
|
87
|
+
const installation = resolveLaunchingCli({ cliPath, env, spawn });
|
|
88
|
+
const services = resolveServices({ repoRoot: installation.root, stack, env: { ...installation.probeEnv, OYSTERUN_NODE_BIN: installation.identity.node_path }, spawn });
|
|
89
|
+
const config = canonical(resolve(configDir));
|
|
90
|
+
if (!services.some(t => t.config === config)) throw new Error("Selected config does not match the selected service stack(s); set OYSTERUN_CONFIG_DIR and --stack/OYSTERUN_RELEASE_STACK to the same installation. No changes made.");
|
|
91
|
+
writableRemoval(config, installation.user.uid);
|
|
92
|
+
const protectedPaths = [installation.prefix, installation.root, installation.identity.node_realpath, installation.identity.npm_cli_realpath, canonical(userInfo().homedir)];
|
|
93
|
+
for (const filename of ["config.json", "session-history.json", "loops.json"]) {
|
|
94
|
+
const path = join(config, filename);
|
|
95
|
+
if (!existsSync(path)) continue;
|
|
96
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
97
|
+
const collect = value => {
|
|
98
|
+
if (!value || typeof value !== "object") return;
|
|
99
|
+
for (const [key, item] of Object.entries(value)) {
|
|
100
|
+
if (["default_browse_path", "agent_folder", "start_folder", "cwd"].includes(key) && typeof item === "string" && item) protectedPaths.push(canonical(resolve(item)));
|
|
101
|
+
else if (item && typeof item === "object") collect(item);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
collect(raw);
|
|
105
|
+
}
|
|
106
|
+
if (protectedPaths.some(p => inside(config, p)) || protectedPaths.slice(5).some(p => inside(installation.root, p))) throw new Error("Config removal overlaps the CLI/toolchain, user home, Default Browse Root or Agent/project data. Move the protected data outside this config before complete uninstall; no changes made.");
|
|
107
|
+
for (const service of services) {
|
|
108
|
+
// all preserves both service selections; only the selected config must
|
|
109
|
+
// belong to the launching package, since only that config is deleted.
|
|
110
|
+
if (service.config === config) {
|
|
111
|
+
const runtimePath = join(dirname(service.pidFile), "host-runtime.json");
|
|
112
|
+
if (existsSync(runtimePath)) {
|
|
113
|
+
const runtime = JSON.parse(readFileSync(runtimePath, "utf8"));
|
|
114
|
+
if (canonical(runtime.config_dir) !== config || canonical(runtime.repo_root) !== installation.root || (runtime.stack_name ?? "production") !== service.name) throw new Error("Selected Host runtime belongs to another package/config/stack; no changes made.");
|
|
115
|
+
}
|
|
116
|
+
if (process.platform === "darwin" && existsSync(service.plist)) {
|
|
117
|
+
const plist = JSON.parse(checkedRun(spawn, "/usr/bin/plutil", ["-convert", "json", "-o", "-", service.plist], { env: installation.probeEnv }));
|
|
118
|
+
const args = plist.ProgramArguments || []; const pe = plist.EnvironmentVariables || {};
|
|
119
|
+
if (plist.Label !== service.label || canonical(args[0] || "") !== join(installation.root, "tool_scripts", "run_oysterun_host.sh") || args[args.indexOf("--stack") + 1] !== service.name ||
|
|
120
|
+
![installation.root, join(installation.root, "host-service")].includes(canonical(plist.WorkingDirectory || "")) ||
|
|
121
|
+
(pe.OYSTERUN_CONFIG_DIR && canonical(pe.OYSTERUN_CONFIG_DIR) !== config) || (pe.HOME && canonical(pe.HOME) !== canonical(userInfo().homedir))) throw new Error("Selected service definition belongs to another package/config/user; no changes made.");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (process.platform === "darwin") writableRemoval(service.plist, installation.user.uid);
|
|
125
|
+
}
|
|
126
|
+
return { ...installation, config, services, stack };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function verifyUninstallServices(plan, { spawn = spawnSync } = {}) {
|
|
130
|
+
for (const t of plan.services) {
|
|
131
|
+
if (existsSync(t.pidFile) || existsSync(t.originFile) || (process.platform === "darwin" && existsSync(t.plist))) throw new Error(`Service state remains for ${t.name}.`);
|
|
132
|
+
if (Number.isInteger(t.pid) && t.pid > 0) {
|
|
133
|
+
try { process.kill(t.pid, 0); throw new Error(`Service PID ${t.pid} remains alive.`); }
|
|
134
|
+
catch (error) { if (error.code !== "ESRCH") throw error; }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const source = `source "$1"; shift
|
|
138
|
+
for target in "$@"; do
|
|
139
|
+
STACK_NAME="$target"; configure_stack_runtime
|
|
140
|
+
if is_macos && launch_service_is_loaded "$HOST_LABEL"; then echo "Service remains loaded: $HOST_LABEL" >&2; exit 1; fi
|
|
141
|
+
if [[ -n "$HOST_PORT" ]]; then owners="$(listener_pids_for_port "$HOST_PORT")"; [[ -z "$owners" ]] || { echo "Service port remains occupied: $HOST_PORT" >&2; exit 1; }; fi
|
|
142
|
+
done`;
|
|
143
|
+
checkedRun(spawn, "/bin/bash", ["--noprofile", "--norc", "-eu", "-c", source, "--", join(plan.root, "tool_scripts", "oysterun_service_common.sh"), ...plan.services.map(t => t.name)], { env: buildUninstallProbeEnvironment(plan.childEnv), cwd: plan.root });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function executeCompleteUninstall(plan, { spawn = spawnSync, remove = rmSync, verifyServices = verifyUninstallServices, log = console.log } = {}) {
|
|
147
|
+
const steps = { service: "incomplete", services: Object.fromEntries(plan.services.map(t => [t.name, "incomplete"])), config: "incomplete", cli: "incomplete", userData: "preserved" };
|
|
148
|
+
let current = "service";
|
|
149
|
+
try {
|
|
150
|
+
for (const target of plan.services) {
|
|
151
|
+
checkedRun(spawn, "/bin/bash", [join(plan.root, "tool_scripts", "oysterun_release_service.sh"), "uninstall", "--stack", target.name], { env: plan.childEnv, cwd: plan.root, stdio: "inherit" });
|
|
152
|
+
verifyServices({ ...plan, services: [target] }, { spawn });
|
|
153
|
+
steps.services[target.name] = "removed/absent";
|
|
154
|
+
}
|
|
155
|
+
steps.service = "removed/absent";
|
|
156
|
+
current = "config";
|
|
157
|
+
remove(plan.config, { recursive: true, force: true });
|
|
158
|
+
if (existsSync(plan.config)) throw new Error("Selected config directory remains.");
|
|
159
|
+
steps.config = "removed/absent";
|
|
160
|
+
current = "cli";
|
|
161
|
+
// Recheck the exact installation after earlier work; never switch prefixes.
|
|
162
|
+
if (realpathSync(plan.cli) !== plan.cli || canonical(plan.bin) !== plan.cli || realpathSync(plan.root) !== plan.root) throw new Error("Launching CLI identity changed during cleanup.");
|
|
163
|
+
if (createHash("sha256").update(readFileSync(join(plan.root, "package.json"))).digest("hex") !== plan.manifestSha256) throw new Error("Launching package changed during cleanup.");
|
|
164
|
+
assertNodeNpmRuntimeIdentityMatches(plan.identity, resolveNodeNpmRuntimeIdentity({ nodeBin: plan.identity.node_path, npmCli: plan.identity.npm_cli_path, env: buildUninstallProbeEnvironment(plan.childEnv), spawnSyncFn: spawn }));
|
|
165
|
+
checkedRun(spawn, plan.identity.node_path, [plan.identity.npm_cli_path, "uninstall", ...plan.npmArgs, "oysterun"], { env: plan.childEnv, cwd: plan.prefix, stdio: "inherit" });
|
|
166
|
+
if (existsSync(plan.root) || existsSync(plan.bin) || (() => { try { lstatSync(plan.bin); return true; } catch (e) { if (e.code === "ENOENT") return false; throw e; } })()) throw new Error("npm completed but the exact Oysterun package/bin remains.");
|
|
167
|
+
steps.cli = "removed/absent";
|
|
168
|
+
log("Complete uninstall succeeded: service removed, configuration removed, CLI removed; user data preserved.");
|
|
169
|
+
return { exitCode: 0, steps };
|
|
170
|
+
} catch (error) {
|
|
171
|
+
log(`Complete uninstall failed at ${current}: ${error.message}`);
|
|
172
|
+
log(`Services: ${Object.entries(steps.services).map(([name, status]) => `${name}=${status}`).join("; ")}.`);
|
|
173
|
+
log(`Completed/incomplete steps: service=${steps.service}; config=${steps.config}; CLI=${steps.cli}; user data=${steps.userData}.`);
|
|
174
|
+
return { exitCode: 1, steps, error: error.message };
|
|
175
|
+
}
|
|
176
|
+
}
|