jishushell 0.6.5 → 0.7.3
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/apps/anythingllm-container.yaml +16 -170
- package/apps/browserless-chromium-container.yaml +16 -10
- package/apps/filebrowser-container.yaml +15 -9
- package/apps/hermes-container.yaml +20 -5
- package/apps/immich-container-lite.yaml +337 -0
- package/apps/immich-container.yaml +371 -0
- package/apps/jishu-kb-container.yaml +50 -177
- package/apps/ollama-binary.yaml +33 -28
- package/apps/ollama-cpu-container.yaml +6 -0
- package/apps/ollama-with-hollama-binary.yaml +35 -28
- package/apps/openclaw-binary.yaml +35 -15
- package/apps/openclaw-container.yaml +29 -11
- package/apps/openclaw-with-ollama-container.yaml +9 -2
- package/apps/openclaw-with-searxng-container.yaml +38 -6
- package/apps/searxng-container.yaml +31 -6
- package/apps/weknora-container.yaml +26 -21
- package/dependencies/jishushell-panel-0.7.3.tgz +0 -0
- package/dist/cli/app.js +244 -213
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/backup.js +15 -12
- package/dist/cli/backup.js.map +1 -1
- package/dist/cli/core.d.ts +4 -3
- package/dist/cli/core.js +392 -227
- package/dist/cli/core.js.map +1 -1
- package/dist/cli/doctor.d.ts +1 -1
- package/dist/cli/doctor.js +113 -10
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/job.js +62 -14
- package/dist/cli/job.js.map +1 -1
- package/dist/cli/llm.js +80 -11
- package/dist/cli/llm.js.map +1 -1
- package/dist/cli/managed-list.d.ts +1 -3
- package/dist/cli/managed-list.js +18 -16
- package/dist/cli/managed-list.js.map +1 -1
- package/dist/cli/migrate.d.ts +2 -0
- package/dist/cli/migrate.js +160 -0
- package/dist/cli/migrate.js.map +1 -0
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +32 -20
- package/dist/config.js +132 -51
- package/dist/config.js.map +1 -1
- package/dist/control.d.ts +6 -6
- package/dist/control.js +31 -23
- package/dist/control.js.map +1 -1
- package/dist/core.d.ts +5 -5
- package/dist/core.js +5 -5
- package/dist/core.js.map +1 -1
- package/dist/install.d.ts +2 -2
- package/dist/install.js +78 -37
- package/dist/install.js.map +1 -1
- package/dist/routes/admin.d.ts +2 -0
- package/dist/routes/admin.js +72 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/apps.d.ts +1 -1
- package/dist/routes/apps.js +101 -193
- package/dist/routes/apps.js.map +1 -1
- package/dist/routes/auth.js +1 -1
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/backup.js +1 -1
- package/dist/routes/backup.js.map +1 -1
- package/dist/routes/docker.d.ts +2 -0
- package/dist/routes/docker.js +58 -0
- package/dist/routes/docker.js.map +1 -0
- package/dist/routes/external-mounts.d.ts +1 -1
- package/dist/routes/external-mounts.js +1 -1
- package/dist/routes/external-mounts.js.map +1 -1
- package/dist/routes/file-mounts.d.ts +4 -3
- package/dist/routes/file-mounts.js +49 -31
- package/dist/routes/file-mounts.js.map +1 -1
- package/dist/routes/files-organize.d.ts +2 -2
- package/dist/routes/files-organize.js +5 -5
- package/dist/routes/files-organize.js.map +1 -1
- package/dist/routes/files.d.ts +1 -1
- package/dist/routes/files.js +1 -1
- package/dist/routes/files.js.map +1 -1
- package/dist/routes/instances.d.ts +0 -8
- package/dist/routes/instances.js +202 -1560
- package/dist/routes/instances.js.map +1 -1
- package/dist/routes/integration-apps.d.ts +14 -0
- package/dist/routes/integration-apps.js +81 -0
- package/dist/routes/integration-apps.js.map +1 -0
- package/dist/routes/integrations.d.ts +9 -0
- package/dist/routes/integrations.js +12 -0
- package/dist/routes/integrations.js.map +1 -0
- package/dist/routes/llm-proxy.js +26 -3
- package/dist/routes/llm-proxy.js.map +1 -1
- package/dist/routes/setup.js +53 -38
- package/dist/routes/setup.js.map +1 -1
- package/dist/routes/system.js +108 -68
- package/dist/routes/system.js.map +1 -1
- package/dist/routes/webdav.d.ts +1 -1
- package/dist/routes/webdav.js +2 -2
- package/dist/routes/webdav.js.map +1 -1
- package/dist/server.d.ts +6 -0
- package/dist/server.js +368 -233
- package/dist/server.js.map +1 -1
- package/dist/services/app-common/app-compiler.js +186 -0
- package/dist/services/app-common/app-compiler.js.map +1 -0
- package/dist/services/app-common/app-shared.d.ts +15 -0
- package/dist/services/app-common/app-shared.js +64 -0
- package/dist/services/app-common/app-shared.js.map +1 -0
- package/dist/services/app-common/capability-service.d.ts +45 -0
- package/dist/services/app-common/capability-service.js +331 -0
- package/dist/services/app-common/capability-service.js.map +1 -0
- package/dist/services/app-common/catalog-service.d.ts +59 -0
- package/dist/services/app-common/catalog-service.js +318 -0
- package/dist/services/app-common/catalog-service.js.map +1 -0
- package/dist/services/app-common/create-pipeline.d.ts +26 -0
- package/dist/services/app-common/create-pipeline.js +298 -0
- package/dist/services/app-common/create-pipeline.js.map +1 -0
- package/dist/services/app-common/delete-service.d.ts +5 -0
- package/dist/services/app-common/delete-service.js +109 -0
- package/dist/services/app-common/delete-service.js.map +1 -0
- package/dist/services/app-common/execution-owner.d.ts +23 -0
- package/dist/services/app-common/execution-owner.js +124 -0
- package/dist/services/app-common/execution-owner.js.map +1 -0
- package/dist/services/app-common/execution-service.d.ts +23 -0
- package/dist/services/app-common/execution-service.js +105 -0
- package/dist/services/app-common/execution-service.js.map +1 -0
- package/dist/services/app-common/id-normalizer.d.ts +31 -0
- package/dist/services/app-common/id-normalizer.js +83 -0
- package/dist/services/app-common/id-normalizer.js.map +1 -0
- package/dist/services/app-common/install-store.d.ts +34 -0
- package/dist/services/app-common/install-store.js +261 -0
- package/dist/services/app-common/install-store.js.map +1 -0
- package/dist/services/app-common/instance-store.d.ts +78 -0
- package/dist/services/app-common/instance-store.js +498 -0
- package/dist/services/app-common/instance-store.js.map +1 -0
- package/dist/services/app-common/integration-refs.d.ts +17 -0
- package/dist/services/app-common/integration-refs.js +47 -0
- package/dist/services/app-common/integration-refs.js.map +1 -0
- package/dist/services/app-common/lifecycle-pipeline.d.ts +62 -0
- package/dist/services/app-common/lifecycle-pipeline.js +317 -0
- package/dist/services/app-common/lifecycle-pipeline.js.map +1 -0
- package/dist/services/app-common/lifecycle-scripts.d.ts +38 -0
- package/dist/services/app-common/lifecycle-scripts.js +935 -0
- package/dist/services/app-common/lifecycle-scripts.js.map +1 -0
- package/dist/services/app-common/lifecycle-service.d.ts +68 -0
- package/dist/services/app-common/lifecycle-service.js +475 -0
- package/dist/services/app-common/lifecycle-service.js.map +1 -0
- package/dist/services/app-common/ownership.d.ts +3 -0
- package/dist/services/app-common/ownership.js +11 -0
- package/dist/services/app-common/ownership.js.map +1 -0
- package/dist/services/app-common/paths.d.ts +29 -0
- package/dist/services/app-common/paths.js +34 -0
- package/dist/services/app-common/paths.js.map +1 -0
- package/dist/services/app-common/platform-transform.d.ts +32 -0
- package/dist/services/app-common/platform-transform.js +65 -0
- package/dist/services/app-common/platform-transform.js.map +1 -0
- package/dist/services/app-common/provide-resolver.d.ts +29 -0
- package/dist/services/app-common/provide-resolver.js +129 -0
- package/dist/services/app-common/provide-resolver.js.map +1 -0
- package/dist/services/app-common/remote-spec.d.ts +14 -0
- package/dist/services/app-common/remote-spec.js +58 -0
- package/dist/services/app-common/remote-spec.js.map +1 -0
- package/dist/services/app-common/runtime-builder.d.ts +1 -0
- package/dist/services/app-common/runtime-builder.js +2 -0
- package/dist/services/app-common/runtime-builder.js.map +1 -0
- package/dist/services/app-common/runtime-facts.d.ts +19 -0
- package/dist/services/app-common/runtime-facts.js +128 -0
- package/dist/services/app-common/runtime-facts.js.map +1 -0
- package/dist/services/app-common/service.d.ts +9 -0
- package/dist/services/app-common/service.js +10 -0
- package/dist/services/app-common/service.js.map +1 -0
- package/dist/services/app-common/spec-materializer.d.ts +8 -0
- package/dist/services/app-common/spec-materializer.js +295 -0
- package/dist/services/app-common/spec-materializer.js.map +1 -0
- package/dist/services/app-common/status-refresh.d.ts +33 -0
- package/dist/services/app-common/status-refresh.js +771 -0
- package/dist/services/app-common/status-refresh.js.map +1 -0
- package/dist/services/app-common/task-service.d.ts +29 -0
- package/dist/services/app-common/task-service.js +93 -0
- package/dist/services/app-common/task-service.js.map +1 -0
- package/dist/services/app-common/terminal-session-manager.js +157 -0
- package/dist/services/app-common/terminal-session-manager.js.map +1 -0
- package/dist/services/app-modules/browserless/routes.d.ts +9 -0
- package/dist/services/app-modules/browserless/routes.js +519 -0
- package/dist/services/app-modules/browserless/routes.js.map +1 -0
- package/dist/services/app-modules/routes.d.ts +2 -0
- package/dist/services/app-modules/routes.js +5 -0
- package/dist/services/app-modules/routes.js.map +1 -0
- package/dist/services/backup/backup-admin.d.ts +95 -0
- package/dist/services/backup/backup-admin.js +246 -0
- package/dist/services/backup/backup-admin.js.map +1 -0
- package/dist/services/backup/backup-manager.d.ts +264 -0
- package/dist/services/backup/backup-manager.js +2318 -0
- package/dist/services/backup/backup-manager.js.map +1 -0
- package/dist/services/backup/backup-verify.js +240 -0
- package/dist/services/backup/backup-verify.js.map +1 -0
- package/dist/services/capabilities/browser-policy.d.ts +14 -0
- package/dist/services/capabilities/browser-policy.js +141 -0
- package/dist/services/capabilities/browser-policy.js.map +1 -0
- package/dist/services/capabilities/contract.d.ts +49 -0
- package/dist/services/capabilities/contract.js +119 -0
- package/dist/services/capabilities/contract.js.map +1 -0
- package/dist/services/capabilities/endpoint-validator.d.ts +42 -0
- package/dist/services/capabilities/endpoint-validator.js +113 -0
- package/dist/services/capabilities/endpoint-validator.js.map +1 -0
- package/dist/services/capabilities/health.d.ts +16 -0
- package/dist/services/capabilities/health.js +121 -0
- package/dist/services/capabilities/health.js.map +1 -0
- package/dist/services/capabilities/registry.d.ts +56 -0
- package/dist/services/capabilities/registry.js +222 -0
- package/dist/services/capabilities/registry.js.map +1 -0
- package/dist/services/capabilities/sync.d.ts +7 -0
- package/dist/services/capabilities/sync.js +223 -0
- package/dist/services/capabilities/sync.js.map +1 -0
- package/dist/services/capability-proxy/html-rewriters/browserless.d.ts +1 -0
- package/dist/services/capability-proxy/html-rewriters/browserless.js +83 -0
- package/dist/services/capability-proxy/html-rewriters/browserless.js.map +1 -0
- package/dist/services/capability-proxy/html-rewriters/index.d.ts +12 -0
- package/dist/services/capability-proxy/html-rewriters/index.js +25 -0
- package/dist/services/capability-proxy/html-rewriters/index.js.map +1 -0
- package/dist/services/capability-proxy/html-rewriters/jishukb.d.ts +1 -0
- package/dist/services/capability-proxy/html-rewriters/jishukb.js +161 -0
- package/dist/services/capability-proxy/html-rewriters/jishukb.js.map +1 -0
- package/dist/services/capability-proxy/http.d.ts +7 -0
- package/dist/services/capability-proxy/http.js +555 -0
- package/dist/services/capability-proxy/http.js.map +1 -0
- package/dist/services/capability-proxy/terminal.d.ts +4 -0
- package/dist/services/capability-proxy/terminal.js +179 -0
- package/dist/services/capability-proxy/terminal.js.map +1 -0
- package/dist/services/connections/admin.d.ts +80 -0
- package/dist/services/connections/admin.js +337 -0
- package/dist/services/connections/admin.js.map +1 -0
- package/dist/services/connections/apply.d.ts +104 -0
- package/dist/services/connections/apply.js +415 -0
- package/dist/services/connections/apply.js.map +1 -0
- package/dist/services/connections/resolver.d.ts +82 -0
- package/dist/services/connections/resolver.js +289 -0
- package/dist/services/connections/resolver.js.map +1 -0
- package/dist/services/connections/transactor.d.ts +39 -0
- package/dist/services/connections/transactor.js +307 -0
- package/dist/services/connections/transactor.js.map +1 -0
- package/dist/services/files/bootstrap.d.ts +7 -0
- package/dist/services/files/bootstrap.js +16 -0
- package/dist/services/files/bootstrap.js.map +1 -0
- package/dist/services/files/external-mounts.js +187 -0
- package/dist/services/files/external-mounts.js.map +1 -0
- package/dist/services/files/files-manager.d.ts +265 -0
- package/dist/services/files/files-manager.js +1189 -0
- package/dist/services/files/files-manager.js.map +1 -0
- package/dist/services/files/files-mounts.d.ts +42 -0
- package/dist/services/files/files-mounts.js +207 -0
- package/dist/services/files/files-mounts.js.map +1 -0
- package/dist/services/files/organize/applier.js +218 -0
- package/dist/services/files/organize/applier.js.map +1 -0
- package/dist/services/files/organize/rules.js +286 -0
- package/dist/services/files/organize/rules.js.map +1 -0
- package/dist/services/files/organize/scanner.js +366 -0
- package/dist/services/files/organize/scanner.js.map +1 -0
- package/dist/services/files/organize/store.js +82 -0
- package/dist/services/files/organize/store.js.map +1 -0
- package/dist/services/files/photos/upload-page.d.ts +2 -0
- package/dist/services/files/photos/upload-page.js +248 -0
- package/dist/services/files/photos/upload-page.js.map +1 -0
- package/dist/services/files/photos/upload-store.d.ts +74 -0
- package/dist/services/files/photos/upload-store.js +432 -0
- package/dist/services/files/photos/upload-store.js.map +1 -0
- package/dist/services/files/webdav/server.d.ts +47 -0
- package/dist/services/files/webdav/server.js +329 -0
- package/dist/services/files/webdav/server.js.map +1 -0
- package/dist/services/files/webdav/xml-builder.js.map +1 -0
- package/dist/services/http/proxy-utils.d.ts +7 -0
- package/dist/services/http/proxy-utils.js +29 -0
- package/dist/services/http/proxy-utils.js.map +1 -0
- package/dist/services/http/request-utils.d.ts +3 -0
- package/dist/services/http/request-utils.js +23 -0
- package/dist/services/http/request-utils.js.map +1 -0
- package/dist/services/instances/admin.d.ts +23 -0
- package/dist/services/instances/admin.js +218 -0
- package/dist/services/instances/admin.js.map +1 -0
- package/dist/services/instances/clone.d.ts +26 -0
- package/dist/services/instances/clone.js +78 -0
- package/dist/services/instances/clone.js.map +1 -0
- package/dist/services/instances/config-admin.d.ts +17 -0
- package/dist/services/instances/config-admin.js +181 -0
- package/dist/services/instances/config-admin.js.map +1 -0
- package/dist/services/instances/manager.d.ts +232 -0
- package/dist/services/instances/manager.js +1342 -0
- package/dist/services/instances/manager.js.map +1 -0
- package/dist/services/instances/pairing.d.ts +17 -0
- package/dist/services/instances/pairing.js +53 -0
- package/dist/services/instances/pairing.js.map +1 -0
- package/dist/services/instances/passwords.js +173 -0
- package/dist/services/instances/passwords.js.map +1 -0
- package/dist/services/instances/status.d.ts +2 -0
- package/dist/services/instances/status.js +11 -0
- package/dist/services/instances/status.js.map +1 -0
- package/dist/services/instances/types.d.ts +21 -0
- package/dist/services/instances/types.js +2 -0
- package/dist/services/instances/types.js.map +1 -0
- package/dist/services/integrations/anythingllm/integration.d.ts +25 -0
- package/dist/services/integrations/anythingllm/integration.js +251 -0
- package/dist/services/integrations/anythingllm/integration.js.map +1 -0
- package/dist/services/integrations/catalog.d.ts +3 -0
- package/dist/services/integrations/catalog.js +73 -0
- package/dist/services/integrations/catalog.js.map +1 -0
- package/dist/services/integrations/custom/integration.d.ts +28 -0
- package/dist/services/integrations/custom/integration.js +179 -0
- package/dist/services/integrations/custom/integration.js.map +1 -0
- package/dist/services/integrations/hermes/integration.d.ts +194 -0
- package/dist/services/integrations/hermes/integration.js +1668 -0
- package/dist/services/integrations/hermes/integration.js.map +1 -0
- package/dist/services/integrations/immich/client.d.ts +93 -0
- package/dist/services/integrations/immich/client.js +458 -0
- package/dist/services/integrations/immich/client.js.map +1 -0
- package/dist/services/integrations/immich/config.d.ts +15 -0
- package/dist/services/integrations/immich/config.js +178 -0
- package/dist/services/integrations/immich/config.js.map +1 -0
- package/dist/services/integrations/immich/discovery.d.ts +9 -0
- package/dist/services/integrations/immich/discovery.js +101 -0
- package/dist/services/integrations/immich/discovery.js.map +1 -0
- package/dist/services/integrations/immich/gallery-renderer.d.ts +5 -0
- package/dist/services/integrations/immich/gallery-renderer.js +150 -0
- package/dist/services/integrations/immich/gallery-renderer.js.map +1 -0
- package/dist/services/integrations/immich/immich-shim.d.ts +11 -0
- package/dist/services/integrations/immich/immich-shim.js +439 -0
- package/dist/services/integrations/immich/immich-shim.js.map +1 -0
- package/dist/services/integrations/immich/integration.d.ts +18 -0
- package/dist/services/integrations/immich/integration.js +64 -0
- package/dist/services/integrations/immich/integration.js.map +1 -0
- package/dist/services/integrations/immich/photo-library.d.ts +4 -0
- package/dist/services/integrations/immich/photo-library.js +63 -0
- package/dist/services/integrations/immich/photo-library.js.map +1 -0
- package/dist/services/integrations/immich/review-executor.d.ts +3 -0
- package/dist/services/integrations/immich/review-executor.js +41 -0
- package/dist/services/integrations/immich/review-executor.js.map +1 -0
- package/dist/services/integrations/immich/review-session-service.d.ts +27 -0
- package/dist/services/integrations/immich/review-session-service.js +206 -0
- package/dist/services/integrations/immich/review-session-service.js.map +1 -0
- package/dist/services/integrations/immich/review-store.d.ts +47 -0
- package/dist/services/integrations/immich/review-store.js +347 -0
- package/dist/services/integrations/immich/review-store.js.map +1 -0
- package/dist/services/integrations/immich/routes.d.ts +7 -0
- package/dist/services/integrations/immich/routes.js +363 -0
- package/dist/services/integrations/immich/routes.js.map +1 -0
- package/dist/services/integrations/immich/types.d.ts +186 -0
- package/dist/services/integrations/immich/types.js +2 -0
- package/dist/services/integrations/immich/types.js.map +1 -0
- package/dist/services/integrations/index.d.ts +41 -0
- package/dist/services/integrations/index.js +60 -0
- package/dist/services/integrations/index.js.map +1 -0
- package/dist/services/integrations/installable/catalog.d.ts +33 -0
- package/dist/services/integrations/installable/catalog.js +88 -0
- package/dist/services/integrations/installable/catalog.js.map +1 -0
- package/dist/services/integrations/installable/index.d.ts +35 -0
- package/dist/services/integrations/installable/index.js +170 -0
- package/dist/services/integrations/installable/index.js.map +1 -0
- package/dist/services/integrations/installable/installers/integration-probes.d.ts +50 -0
- package/dist/services/integrations/installable/installers/integration-probes.js +231 -0
- package/dist/services/integrations/installable/installers/integration-probes.js.map +1 -0
- package/dist/services/integrations/installable/installers/integration.d.ts +30 -0
- package/dist/services/integrations/installable/installers/integration.js +283 -0
- package/dist/services/integrations/installable/installers/integration.js.map +1 -0
- package/dist/services/integrations/installable/installers/registry-probe.js.map +1 -0
- package/dist/services/integrations/installable/installers/shell-script.d.ts +46 -0
- package/dist/services/integrations/installable/installers/shell-script.js +487 -0
- package/dist/services/integrations/installable/installers/shell-script.js.map +1 -0
- package/dist/services/integrations/installable/types.d.ts +130 -0
- package/dist/services/integrations/installable/types.js +19 -0
- package/dist/services/integrations/installable/types.js.map +1 -0
- package/dist/services/integrations/jishukb/integration.d.ts +24 -0
- package/dist/services/integrations/jishukb/integration.js +300 -0
- package/dist/services/integrations/jishukb/integration.js.map +1 -0
- package/dist/services/integrations/openclaw/anythingllm-shim.d.ts +46 -0
- package/dist/services/integrations/openclaw/anythingllm-shim.js +281 -0
- package/dist/services/integrations/openclaw/anythingllm-shim.js.map +1 -0
- package/dist/services/integrations/openclaw/drive-shim.js +490 -0
- package/dist/services/integrations/openclaw/drive-shim.js.map +1 -0
- package/dist/services/integrations/openclaw/integration.d.ts +438 -0
- package/dist/services/integrations/openclaw/integration.js +4629 -0
- package/dist/services/integrations/openclaw/integration.js.map +1 -0
- package/dist/services/integrations/openclaw/jishukb-native-mcp.d.ts +58 -0
- package/dist/services/integrations/openclaw/jishukb-native-mcp.js +373 -0
- package/dist/services/integrations/openclaw/jishukb-native-mcp.js.map +1 -0
- package/dist/services/integrations/openclaw/jishukb-shim.d.ts +52 -0
- package/dist/services/integrations/openclaw/jishukb-shim.js +1357 -0
- package/dist/services/integrations/openclaw/jishukb-shim.js.map +1 -0
- package/dist/services/integrations/openclaw/mcporter-lite.js +276 -0
- package/dist/services/integrations/openclaw/mcporter-lite.js.map +1 -0
- package/dist/services/integrations/openclaw/mcporter.d.ts +59 -0
- package/dist/services/integrations/openclaw/mcporter.js +143 -0
- package/dist/services/integrations/openclaw/mcporter.js.map +1 -0
- package/dist/services/integrations/openclaw/native-mcp.d.ts +48 -0
- package/dist/services/integrations/openclaw/native-mcp.js +125 -0
- package/dist/services/integrations/openclaw/native-mcp.js.map +1 -0
- package/dist/services/integrations/openclaw/routes.d.ts +21 -0
- package/dist/services/integrations/openclaw/routes.js +1194 -0
- package/dist/services/integrations/openclaw/routes.js.map +1 -0
- package/dist/services/integrations/registry.d.ts +17 -0
- package/dist/services/integrations/registry.js +36 -0
- package/dist/services/integrations/registry.js.map +1 -0
- package/dist/services/integrations/routes.d.ts +2 -0
- package/dist/services/integrations/routes.js +9 -0
- package/dist/services/integrations/routes.js.map +1 -0
- package/dist/services/integrations/types.d.ts +457 -0
- package/dist/services/integrations/types.js +2 -0
- package/dist/services/integrations/types.js.map +1 -0
- package/dist/services/legacy-migrator/classifier.d.ts +44 -0
- package/dist/services/legacy-migrator/classifier.js +309 -0
- package/dist/services/legacy-migrator/classifier.js.map +1 -0
- package/dist/services/legacy-migrator/executor.d.ts +42 -0
- package/dist/services/legacy-migrator/executor.js +637 -0
- package/dist/services/legacy-migrator/executor.js.map +1 -0
- package/dist/services/legacy-migrator/index.d.ts +31 -0
- package/dist/services/legacy-migrator/index.js +34 -0
- package/dist/services/legacy-migrator/index.js.map +1 -0
- package/dist/services/legacy-migrator/planner.d.ts +8 -0
- package/dist/services/legacy-migrator/planner.js +154 -0
- package/dist/services/legacy-migrator/planner.js.map +1 -0
- package/dist/services/legacy-migrator/provider-settings.d.ts +6 -0
- package/dist/services/legacy-migrator/provider-settings.js +72 -0
- package/dist/services/legacy-migrator/provider-settings.js.map +1 -0
- package/dist/services/legacy-migrator/report.d.ts +9 -0
- package/dist/services/legacy-migrator/report.js +99 -0
- package/dist/services/legacy-migrator/report.js.map +1 -0
- package/dist/services/legacy-migrator/scanner.d.ts +13 -0
- package/dist/services/legacy-migrator/scanner.js +157 -0
- package/dist/services/legacy-migrator/scanner.js.map +1 -0
- package/dist/services/legacy-migrator/types.d.ts +97 -0
- package/dist/services/legacy-migrator/types.js +23 -0
- package/dist/services/legacy-migrator/types.js.map +1 -0
- package/dist/services/llm-proxy/instance-proxy.d.ts +17 -1
- package/dist/services/llm-proxy/instance-proxy.js +171 -44
- package/dist/services/llm-proxy/instance-proxy.js.map +1 -1
- package/dist/services/llm-proxy/probe.js +5 -14
- package/dist/services/llm-proxy/probe.js.map +1 -1
- package/dist/services/llm-proxy/providers.js +23 -31
- package/dist/services/llm-proxy/providers.js.map +1 -1
- package/dist/services/llm-proxy/ssrf.d.ts +11 -4
- package/dist/services/llm-proxy/ssrf.js +45 -7
- package/dist/services/llm-proxy/ssrf.js.map +1 -1
- package/dist/services/llm-proxy/validate-key.js +16 -37
- package/dist/services/llm-proxy/validate-key.js.map +1 -1
- package/dist/services/repair/runtime-repair.d.ts +22 -0
- package/dist/services/repair/runtime-repair.js +374 -0
- package/dist/services/repair/runtime-repair.js.map +1 -0
- package/dist/services/runtime/docker-network.d.ts +8 -0
- package/dist/services/runtime/docker-network.js +123 -0
- package/dist/services/runtime/docker-network.js.map +1 -0
- package/dist/services/runtime/driver-registry.d.ts +25 -0
- package/dist/services/runtime/driver-registry.js +22 -0
- package/dist/services/runtime/driver-registry.js.map +1 -0
- package/dist/services/runtime/drivers/nomad.d.ts +261 -0
- package/dist/services/runtime/drivers/nomad.js +3122 -0
- package/dist/services/runtime/drivers/nomad.js.map +1 -0
- package/dist/services/runtime/errors.d.ts +3 -3
- package/dist/services/runtime/errors.js +3 -3
- package/dist/services/runtime/instance.d.ts +14 -16
- package/dist/services/runtime/instance.js +93 -123
- package/dist/services/runtime/instance.js.map +1 -1
- package/dist/services/runtime/job-id.d.ts +1 -1
- package/dist/services/runtime/job-id.js +1 -1
- package/dist/services/runtime/mcp-shims/firewall.d.ts +2 -2
- package/dist/services/runtime/mcp-shims/firewall.js +2 -2
- package/dist/services/runtime/mcp-shims/searxng-shim.d.ts +3 -5
- package/dist/services/runtime/mcp-shims/searxng-shim.js +3 -5
- package/dist/services/runtime/mcp-shims/searxng-shim.js.map +1 -1
- package/dist/services/runtime/mcp-shims/write-mcp-entry.d.ts +20 -20
- package/dist/services/runtime/mcp-shims/write-mcp-entry.js +16 -16
- package/dist/services/runtime/mcp-shims/write-mcp-entry.js.map +1 -1
- package/dist/services/runtime/ownership-marker.d.ts +83 -0
- package/dist/services/runtime/ownership-marker.js +109 -0
- package/dist/services/runtime/ownership-marker.js.map +1 -0
- package/dist/services/runtime/service-manager.d.ts +2 -0
- package/dist/services/runtime/service-manager.js +18 -0
- package/dist/services/runtime/service-manager.js.map +1 -0
- package/dist/services/runtime/types.d.ts +23 -501
- package/dist/services/runtime/types.js +0 -12
- package/dist/services/runtime/types.js.map +1 -1
- package/dist/services/runtime/workload-compiler.d.ts +17 -0
- package/dist/services/runtime/workload-compiler.js +550 -0
- package/dist/services/runtime/workload-compiler.js.map +1 -0
- package/dist/services/runtime/workload-types.d.ts +11 -0
- package/dist/services/runtime/workload-types.js +2 -0
- package/dist/services/runtime/workload-types.js.map +1 -0
- package/dist/services/setup/core-manager.d.ts +50 -0
- package/dist/services/setup/core-manager.js +456 -0
- package/dist/services/setup/core-manager.js.map +1 -0
- package/dist/services/setup/plugin-installer.js +136 -0
- package/dist/services/setup/plugin-installer.js.map +1 -0
- package/dist/services/setup/setup-manager.d.ts +158 -0
- package/dist/services/setup/setup-manager.js +2724 -0
- package/dist/services/setup/setup-manager.js.map +1 -0
- package/dist/services/system/cli-command.d.ts +5 -0
- package/dist/services/system/cli-command.js +18 -0
- package/dist/services/system/cli-command.js.map +1 -0
- package/dist/services/system/macos-launchd.js +312 -0
- package/dist/services/system/macos-launchd.js.map +1 -0
- package/dist/services/system/repair-orchestrator.d.ts +71 -0
- package/dist/services/system/repair-orchestrator.js +412 -0
- package/dist/services/system/repair-orchestrator.js.map +1 -0
- package/dist/services/system/runtime-ownership.d.ts +36 -0
- package/dist/services/system/runtime-ownership.js +250 -0
- package/dist/services/system/runtime-ownership.js.map +1 -0
- package/dist/services/system/system-monitor.js +96 -0
- package/dist/services/system/system-monitor.js.map +1 -0
- package/dist/services/system/system-ollama-provider.d.ts +14 -0
- package/dist/services/system/system-ollama-provider.js +129 -0
- package/dist/services/system/system-ollama-provider.js.map +1 -0
- package/dist/services/system/system-reconciler.d.ts +59 -0
- package/dist/services/system/system-reconciler.js +763 -0
- package/dist/services/system/system-reconciler.js.map +1 -0
- package/dist/services/system/update-manager.d.ts +43 -0
- package/dist/services/system/update-manager.js +315 -0
- package/dist/services/system/update-manager.js.map +1 -0
- package/dist/services/system/upgrade-finalize.d.ts +80 -0
- package/dist/services/system/upgrade-finalize.js +507 -0
- package/dist/services/system/upgrade-finalize.js.map +1 -0
- package/dist/services/tasks/registry.d.ts +44 -0
- package/dist/services/tasks/registry.js +90 -0
- package/dist/services/tasks/registry.js.map +1 -0
- package/dist/services/telemetry/activation.d.ts +6 -2
- package/dist/services/telemetry/activation.js +6 -2
- package/dist/services/telemetry/activation.js.map +1 -1
- package/dist/services/telemetry/heartbeat.d.ts +6 -2
- package/dist/services/telemetry/heartbeat.js +6 -2
- package/dist/services/telemetry/heartbeat.js.map +1 -1
- package/dist/services/workspaces/builder.d.ts +29 -0
- package/dist/services/workspaces/builder.js +186 -0
- package/dist/services/workspaces/builder.js.map +1 -0
- package/dist/types.d.ts +350 -48
- package/dist/utils/instance-lock.d.ts +2 -2
- package/dist/utils/instance-lock.js +2 -2
- package/dist/utils/path-safety.js +1 -1
- package/dist/utils/service-user.d.ts +13 -0
- package/dist/utils/service-user.js +129 -0
- package/dist/utils/service-user.js.map +1 -0
- package/install/jishu-install.sh +107 -27
- package/install/jishu-uninstall.sh +8 -0
- package/install/post-install.sh +162 -185
- package/install/post-uninstall.sh +6 -0
- package/node_modules/@fastify/static/.github/workflows/ci.yml +1 -1
- package/node_modules/@fastify/static/.github/workflows/lock-threads.yml +19 -0
- package/node_modules/@fastify/static/LICENSE +1 -3
- package/node_modules/@fastify/static/example/server-benchmark.js +39 -0
- package/node_modules/@fastify/static/index.js +169 -23
- package/node_modules/@fastify/static/lib/dirList.js +20 -6
- package/node_modules/@fastify/static/package.json +10 -8
- package/node_modules/@fastify/static/test/dir-list.test.js +82 -0
- package/node_modules/@fastify/static/test/static.test.js +326 -4
- package/node_modules/@fastify/static/types/index.d.ts +0 -4
- package/node_modules/@fastify/static/types/index.test-d.ts +1 -1
- package/node_modules/brace-expansion/dist/commonjs/index.js +24 -14
- package/node_modules/brace-expansion/dist/commonjs/index.js.map +1 -1
- package/node_modules/brace-expansion/dist/esm/index.js +24 -14
- package/node_modules/brace-expansion/dist/esm/index.js.map +1 -1
- package/node_modules/brace-expansion/package.json +2 -2
- package/node_modules/content-disposition/README.md +21 -22
- package/node_modules/content-disposition/index.js +122 -44
- package/node_modules/content-disposition/package.json +16 -20
- package/node_modules/fast-uri/index.js +1 -1
- package/node_modules/fast-uri/package.json +1 -1
- package/node_modules/fast-uri/test/security.test.js +28 -0
- package/node_modules/fastify/SECURITY.md +1 -1
- package/node_modules/fastify/SPONSORS.md +6 -4
- package/node_modules/fastify/docs/Guides/Database.md +0 -28
- package/node_modules/fastify/docs/Guides/Ecosystem.md +13 -2
- package/node_modules/fastify/docs/Guides/Serverless.md +2 -2
- package/node_modules/fastify/docs/Guides/Write-Plugin.md +1 -1
- package/node_modules/fastify/docs/Reference/Encapsulation.md +27 -26
- package/node_modules/fastify/docs/Reference/Errors.md +10 -4
- package/node_modules/fastify/docs/Reference/HTTP2.md +10 -10
- package/node_modules/fastify/docs/Reference/Hooks.md +4 -4
- package/node_modules/fastify/docs/Reference/Index.md +14 -16
- package/node_modules/fastify/docs/Reference/LTS.md +12 -13
- package/node_modules/fastify/docs/Reference/Lifecycle.md +9 -8
- package/node_modules/fastify/docs/Reference/Logging.md +44 -39
- package/node_modules/fastify/docs/Reference/Middleware.md +21 -25
- package/node_modules/fastify/docs/Reference/Principles.md +2 -2
- package/node_modules/fastify/docs/Reference/Reply.md +6 -1
- package/node_modules/fastify/docs/Reference/Request.md +27 -16
- package/node_modules/fastify/docs/Reference/Routes.md +5 -2
- package/node_modules/fastify/docs/Reference/Server.md +31 -3
- package/node_modules/fastify/docs/Reference/Type-Providers.md +29 -5
- package/node_modules/fastify/docs/Reference/Validation-and-Serialization.md +15 -2
- package/node_modules/fastify/docs/Reference/Warnings.md +7 -6
- package/node_modules/fastify/eslint.config.js +7 -2
- package/node_modules/fastify/fastify.d.ts +8 -3
- package/node_modules/fastify/fastify.js +43 -14
- package/node_modules/fastify/lib/content-type-parser.js +13 -1
- package/node_modules/fastify/lib/decorate.js +11 -3
- package/node_modules/fastify/lib/error-handler.js +4 -3
- package/node_modules/fastify/lib/error-serializer.js +59 -59
- package/node_modules/fastify/lib/errors.js +16 -1
- package/node_modules/fastify/lib/four-oh-four.js +14 -9
- package/node_modules/fastify/lib/handle-request.js +11 -5
- package/node_modules/fastify/lib/plugin-override.js +2 -1
- package/node_modules/fastify/lib/plugin-utils.js +5 -5
- package/node_modules/fastify/lib/reply.js +63 -8
- package/node_modules/fastify/lib/request.js +14 -4
- package/node_modules/fastify/lib/route.js +20 -6
- package/node_modules/fastify/lib/schema-controller.js +1 -1
- package/node_modules/fastify/lib/schemas.js +37 -30
- package/node_modules/fastify/lib/symbols.js +3 -1
- package/node_modules/fastify/lib/validation.js +1 -13
- package/node_modules/fastify/lib/warnings.js +3 -3
- package/node_modules/fastify/package.json +13 -15
- package/node_modules/fastify/scripts/validate-ecosystem-links.js +1 -0
- package/node_modules/fastify/test/bundler/esbuild/package.json +1 -1
- package/node_modules/fastify/test/close-pipelining.test.js +1 -2
- package/node_modules/fastify/test/custom-http-server.test.js +38 -0
- package/node_modules/fastify/test/decorator-instance-properties.test.js +63 -0
- package/node_modules/fastify/test/diagnostics-channel/async-error-handler.test.js +74 -0
- package/node_modules/fastify/test/hooks.test.js +23 -0
- package/node_modules/fastify/test/http-methods/get.test.js +1 -1
- package/node_modules/fastify/test/http2/plain.test.js +135 -0
- package/node_modules/fastify/test/http2/secure-with-fallback.test.js +1 -1
- package/node_modules/fastify/test/https/https.test.js +1 -2
- package/node_modules/fastify/test/internals/errors.test.js +31 -1
- package/node_modules/fastify/test/internals/plugin.test.js +3 -1
- package/node_modules/fastify/test/internals/request.test.js +27 -3
- package/node_modules/fastify/test/internals/schema-controller-perf.test.js +33 -0
- package/node_modules/fastify/test/logger/logging.test.js +18 -1
- package/node_modules/fastify/test/logger/options.test.js +38 -1
- package/node_modules/fastify/test/reply-error.test.js +1 -1
- package/node_modules/fastify/test/reply-trailers.test.js +70 -0
- package/node_modules/fastify/test/request-media-type.test.js +105 -0
- package/node_modules/fastify/test/route-prefix.test.js +34 -0
- package/node_modules/fastify/test/router-options.test.js +222 -11
- package/node_modules/fastify/test/schema-serialization.test.js +108 -0
- package/node_modules/fastify/test/schema-validation.test.js +24 -0
- package/node_modules/fastify/test/scripts/validate-ecosystem-links.test.js +40 -57
- package/node_modules/fastify/test/throw.test.js +14 -0
- package/node_modules/fastify/test/trust-proxy.test.js +21 -0
- package/node_modules/fastify/test/types/content-type-parser.tst.ts +70 -0
- package/node_modules/fastify/test/types/decorate-request-reply.tst.ts +18 -0
- package/node_modules/fastify/test/types/dummy-plugin.mts +9 -0
- package/node_modules/fastify/test/types/errors.tst.ts +91 -0
- package/node_modules/fastify/test/types/fastify.tst.ts +351 -0
- package/node_modules/fastify/test/types/hooks.tst.ts +578 -0
- package/node_modules/fastify/test/types/instance.tst.ts +597 -0
- package/node_modules/fastify/test/types/logger.tst.ts +276 -0
- package/node_modules/fastify/test/types/plugin.tst.ts +96 -0
- package/node_modules/fastify/test/types/register.tst.ts +245 -0
- package/node_modules/fastify/test/types/reply.tst.ts +297 -0
- package/node_modules/fastify/test/types/request.tst.ts +199 -0
- package/node_modules/fastify/test/types/route.tst.ts +576 -0
- package/node_modules/fastify/test/types/schema.tst.ts +135 -0
- package/node_modules/fastify/test/types/serverFactory.tst.ts +37 -0
- package/node_modules/fastify/test/types/tsconfig.json +9 -0
- package/node_modules/fastify/test/types/type-provider.tst.ts +1219 -0
- package/node_modules/fastify/test/types/using.tst.ts +14 -0
- package/node_modules/fastify/types/errors.d.ts +3 -0
- package/node_modules/fastify/types/request.d.ts +23 -2
- package/node_modules/glob/README.md +39 -130
- package/node_modules/glob/dist/commonjs/glob.d.ts +8 -0
- package/node_modules/glob/dist/commonjs/glob.d.ts.map +1 -1
- package/node_modules/glob/dist/commonjs/glob.js +2 -1
- package/node_modules/glob/dist/commonjs/glob.js.map +1 -1
- package/node_modules/glob/dist/commonjs/index.min.js +4 -0
- package/node_modules/glob/dist/commonjs/index.min.js.map +7 -0
- package/node_modules/glob/dist/commonjs/pattern.d.ts +3 -0
- package/node_modules/glob/dist/commonjs/pattern.d.ts.map +1 -1
- package/node_modules/glob/dist/commonjs/pattern.js +4 -0
- package/node_modules/glob/dist/commonjs/pattern.js.map +1 -1
- package/node_modules/glob/dist/esm/glob.d.ts +8 -0
- package/node_modules/glob/dist/esm/glob.d.ts.map +1 -1
- package/node_modules/glob/dist/esm/glob.js +2 -1
- package/node_modules/glob/dist/esm/glob.js.map +1 -1
- package/node_modules/glob/dist/esm/index.min.js +4 -0
- package/node_modules/glob/dist/esm/index.min.js.map +7 -0
- package/node_modules/glob/dist/esm/pattern.d.ts +3 -0
- package/node_modules/glob/dist/esm/pattern.d.ts.map +1 -1
- package/node_modules/glob/dist/esm/pattern.js +4 -0
- package/node_modules/glob/dist/esm/pattern.js.map +1 -1
- package/node_modules/glob/package.json +38 -37
- package/node_modules/jishushell-panel/README.md +4 -4
- package/node_modules/jishushell-panel/output/dist/server.js +17 -6
- package/node_modules/jishushell-panel/output/dist/server.js.map +1 -1
- package/node_modules/jishushell-panel/output/public/assets/ApiKeyField-Ce5d1xna.js +1 -0
- package/node_modules/jishushell-panel/output/public/assets/Dashboard-BXame3yg.js +1 -0
- package/node_modules/jishushell-panel/output/public/assets/HermesChatPanel-BHZtPCJd.js +1 -0
- package/node_modules/jishushell-panel/output/public/assets/HermesConfigForm-CB3GbNX9.js +4 -0
- package/node_modules/jishushell-panel/output/public/assets/InitPassword-Boab9F6g.js +1 -0
- package/node_modules/jishushell-panel/output/public/assets/InstanceDetail-DrIWCqo-.js +14 -0
- package/node_modules/jishushell-panel/output/public/assets/Login-CzpOkNau.js +1 -0
- package/node_modules/jishushell-panel/output/public/assets/NewInstance-CANXyCcL.js +1 -0
- package/node_modules/jishushell-panel/output/public/assets/ProviderRecommendations-BABo9VOC.js +1 -0
- package/node_modules/jishushell-panel/output/public/assets/Settings-CKp5XxFh.js +1 -0
- package/node_modules/jishushell-panel/output/public/assets/Setup-C7xVDPow.js +1 -0
- package/node_modules/jishushell-panel/output/public/assets/WeixinLoginPanel-B765Xz4C.js +1 -0
- package/node_modules/jishushell-panel/output/public/assets/api-C70Gt678.js +4 -0
- package/node_modules/jishushell-panel/output/public/assets/index-Bs6DSbiR.js +23 -0
- package/node_modules/jishushell-panel/output/public/assets/index-DnnqTf7s.css +1 -0
- package/node_modules/jishushell-panel/output/public/assets/registry-sWIZsIEF.js +2 -0
- package/node_modules/jishushell-panel/output/public/assets/rolldown-runtime-QTnfLwEv.js +1 -0
- package/node_modules/jishushell-panel/output/public/assets/setup-task-q21GnI0E.js +1 -0
- package/node_modules/jishushell-panel/output/public/assets/usePolling-D4IDOQd_.js +1 -0
- package/node_modules/jishushell-panel/output/public/assets/vendor-i18n-Df8aUdv8.js +1 -0
- package/node_modules/jishushell-panel/output/public/assets/vendor-react-0L0rjmYG.js +8 -0
- package/node_modules/jishushell-panel/output/public/index.html +6 -4
- package/node_modules/jishushell-panel/package.json +2 -2
- package/node_modules/semver/classes/range.js +17 -4
- package/node_modules/semver/package.json +2 -2
- package/package.json +12 -64
- package/scripts/check-app-path-boundaries.mjs +121 -0
- package/scripts/check-app-spec.mjs +123 -29
- package/scripts/check-architecture-boundaries.mjs +178 -0
- package/scripts/check-colima-launchd.mjs +10 -8
- package/scripts/check-integration-isolation.ts +541 -0
- package/scripts/check-new-file-tests.mjs +11 -3
- package/scripts/check-open-core-boundaries.mjs +60 -10
- package/scripts/check-test-layering.sh +1 -1
- package/scripts/fixtures/instances/hermes-sample/instance.json +3 -2
- package/scripts/fixtures/instances/legacy-openclaw-sample/instance.json +1 -1
- package/scripts/local-web-upgrade-test.README +4 -3
- package/scripts/local-web-upgrade-test.example.env +2 -2
- package/scripts/local-web-upgrade-test.sh +14 -1
- package/scripts/pack-gui-and-send-pi.sh +43 -0
- package/scripts/perf/instances.js +1 -1
- package/scripts/prune-open-core-dist.mjs +89 -2
- package/scripts/smoke/hermes-bootstrap.sh +5 -5
- package/templates/hermes-entrypoint.sh +19 -29
- package/apps/openwebui-container.yaml +0 -97
- package/apps/playwright-container.yaml +0 -126
- package/dependencies/jishushell-panel-0.6.5.tgz +0 -0
- package/dist/crypto-shim.d.ts +0 -1
- package/dist/crypto-shim.js +0 -2
- package/dist/crypto-shim.js.map +0 -1
- package/dist/routes/agent-apps.d.ts +0 -14
- package/dist/routes/agent-apps.js +0 -77
- package/dist/routes/agent-apps.js.map +0 -1
- package/dist/routes/internal.d.ts +0 -2
- package/dist/routes/internal.js +0 -55
- package/dist/routes/internal.js.map +0 -1
- package/dist/routes/openclaw-routes.d.ts +0 -22
- package/dist/routes/openclaw-routes.js +0 -1020
- package/dist/routes/openclaw-routes.js.map +0 -1
- package/dist/routes/runtime.d.ts +0 -15
- package/dist/routes/runtime.js +0 -76
- package/dist/routes/runtime.js.map +0 -1
- package/dist/services/agent-apps/catalog.d.ts +0 -33
- package/dist/services/agent-apps/catalog.js +0 -88
- package/dist/services/agent-apps/catalog.js.map +0 -1
- package/dist/services/agent-apps/index.d.ts +0 -36
- package/dist/services/agent-apps/index.js +0 -171
- package/dist/services/agent-apps/index.js.map +0 -1
- package/dist/services/agent-apps/installers/adapter-probes.d.ts +0 -49
- package/dist/services/agent-apps/installers/adapter-probes.js +0 -230
- package/dist/services/agent-apps/installers/adapter-probes.js.map +0 -1
- package/dist/services/agent-apps/installers/adapter.d.ts +0 -30
- package/dist/services/agent-apps/installers/adapter.js +0 -171
- package/dist/services/agent-apps/installers/adapter.js.map +0 -1
- package/dist/services/agent-apps/installers/registry-probe.js.map +0 -1
- package/dist/services/agent-apps/installers/shell-script.d.ts +0 -47
- package/dist/services/agent-apps/installers/shell-script.js +0 -488
- package/dist/services/agent-apps/installers/shell-script.js.map +0 -1
- package/dist/services/agent-apps/types.d.ts +0 -128
- package/dist/services/agent-apps/types.js +0 -17
- package/dist/services/agent-apps/types.js.map +0 -1
- package/dist/services/app/app-compiler.js +0 -185
- package/dist/services/app/app-compiler.js.map +0 -1
- package/dist/services/app/app-manager.d.ts +0 -184
- package/dist/services/app/app-manager.js +0 -2933
- package/dist/services/app/app-manager.js.map +0 -1
- package/dist/services/app/custom-manager.d.ts +0 -27
- package/dist/services/app/custom-manager.js +0 -382
- package/dist/services/app/custom-manager.js.map +0 -1
- package/dist/services/app/hermes-agent-manager.d.ts +0 -20
- package/dist/services/app/hermes-agent-manager.js +0 -299
- package/dist/services/app/hermes-agent-manager.js.map +0 -1
- package/dist/services/app/id-normalizer.d.ts +0 -27
- package/dist/services/app/id-normalizer.js +0 -77
- package/dist/services/app/id-normalizer.js.map +0 -1
- package/dist/services/app/ollama-manager.d.ts +0 -18
- package/dist/services/app/ollama-manager.js +0 -224
- package/dist/services/app/ollama-manager.js.map +0 -1
- package/dist/services/app/openclaw-manager.d.ts +0 -63
- package/dist/services/app/openclaw-manager.js +0 -1215
- package/dist/services/app/openclaw-manager.js.map +0 -1
- package/dist/services/app/paths.d.ts +0 -27
- package/dist/services/app/paths.js +0 -40
- package/dist/services/app/paths.js.map +0 -1
- package/dist/services/app/platform-transform.d.ts +0 -32
- package/dist/services/app/platform-transform.js +0 -65
- package/dist/services/app/platform-transform.js.map +0 -1
- package/dist/services/app/provide-resolver.d.ts +0 -29
- package/dist/services/app/provide-resolver.js +0 -135
- package/dist/services/app/provide-resolver.js.map +0 -1
- package/dist/services/app/registry.d.ts +0 -17
- package/dist/services/app/registry.js +0 -31
- package/dist/services/app/registry.js.map +0 -1
- package/dist/services/app/remote-spec.d.ts +0 -14
- package/dist/services/app/remote-spec.js +0 -58
- package/dist/services/app/remote-spec.js.map +0 -1
- package/dist/services/app/terminal-session-manager.js +0 -157
- package/dist/services/app/terminal-session-manager.js.map +0 -1
- package/dist/services/app/types.d.ts +0 -74
- package/dist/services/app/types.js +0 -16
- package/dist/services/app/types.js.map +0 -1
- package/dist/services/app-config-admin.d.ts +0 -17
- package/dist/services/app-config-admin.js +0 -177
- package/dist/services/app-config-admin.js.map +0 -1
- package/dist/services/app-create-from-installed.d.ts +0 -23
- package/dist/services/app-create-from-installed.js +0 -75
- package/dist/services/app-create-from-installed.js.map +0 -1
- package/dist/services/app-passwords.js +0 -173
- package/dist/services/app-passwords.js.map +0 -1
- package/dist/services/backup-admin.d.ts +0 -101
- package/dist/services/backup-admin.js +0 -259
- package/dist/services/backup-admin.js.map +0 -1
- package/dist/services/backup-manager.d.ts +0 -264
- package/dist/services/backup-manager.js +0 -2263
- package/dist/services/backup-manager.js.map +0 -1
- package/dist/services/backup-verify.js +0 -240
- package/dist/services/backup-verify.js.map +0 -1
- package/dist/services/capability-endpoint-validator.d.ts +0 -41
- package/dist/services/capability-endpoint-validator.js +0 -114
- package/dist/services/capability-endpoint-validator.js.map +0 -1
- package/dist/services/capability-health.d.ts +0 -16
- package/dist/services/capability-health.js +0 -121
- package/dist/services/capability-health.js.map +0 -1
- package/dist/services/capability-registry.d.ts +0 -29
- package/dist/services/capability-registry.js +0 -176
- package/dist/services/capability-registry.js.map +0 -1
- package/dist/services/capability-sync.d.ts +0 -4
- package/dist/services/capability-sync.js +0 -220
- package/dist/services/capability-sync.js.map +0 -1
- package/dist/services/connection-admin.d.ts +0 -74
- package/dist/services/connection-admin.js +0 -287
- package/dist/services/connection-admin.js.map +0 -1
- package/dist/services/connection-apply.d.ts +0 -91
- package/dist/services/connection-apply.js +0 -471
- package/dist/services/connection-apply.js.map +0 -1
- package/dist/services/connection-resolver.d.ts +0 -65
- package/dist/services/connection-resolver.js +0 -281
- package/dist/services/connection-resolver.js.map +0 -1
- package/dist/services/connection-transactor.d.ts +0 -39
- package/dist/services/connection-transactor.js +0 -354
- package/dist/services/connection-transactor.js.map +0 -1
- package/dist/services/core-manager.d.ts +0 -50
- package/dist/services/core-manager.js +0 -411
- package/dist/services/core-manager.js.map +0 -1
- package/dist/services/external-mounts.js +0 -187
- package/dist/services/external-mounts.js.map +0 -1
- package/dist/services/files-manager.d.ts +0 -252
- package/dist/services/files-manager.js +0 -1156
- package/dist/services/files-manager.js.map +0 -1
- package/dist/services/files-mounts.d.ts +0 -42
- package/dist/services/files-mounts.js +0 -207
- package/dist/services/files-mounts.js.map +0 -1
- package/dist/services/instance-admin.d.ts +0 -26
- package/dist/services/instance-admin.js +0 -218
- package/dist/services/instance-admin.js.map +0 -1
- package/dist/services/instance-manager.d.ts +0 -192
- package/dist/services/instance-manager.js +0 -1289
- package/dist/services/instance-manager.js.map +0 -1
- package/dist/services/macos-launchd.js +0 -312
- package/dist/services/macos-launchd.js.map +0 -1
- package/dist/services/nomad-manager.d.ts +0 -307
- package/dist/services/nomad-manager.js +0 -3958
- package/dist/services/nomad-manager.js.map +0 -1
- package/dist/services/organize/applier.js +0 -218
- package/dist/services/organize/applier.js.map +0 -1
- package/dist/services/organize/rules.js +0 -286
- package/dist/services/organize/rules.js.map +0 -1
- package/dist/services/organize/scanner.js +0 -366
- package/dist/services/organize/scanner.js.map +0 -1
- package/dist/services/organize/store.js +0 -82
- package/dist/services/organize/store.js.map +0 -1
- package/dist/services/plugin-installer.js +0 -128
- package/dist/services/plugin-installer.js.map +0 -1
- package/dist/services/process-manager.d.ts +0 -25
- package/dist/services/process-manager.js +0 -568
- package/dist/services/process-manager.js.map +0 -1
- package/dist/services/runtime/adapters/custom.d.ts +0 -20
- package/dist/services/runtime/adapters/custom.js +0 -188
- package/dist/services/runtime/adapters/custom.js.map +0 -1
- package/dist/services/runtime/adapters/hermes.d.ts +0 -204
- package/dist/services/runtime/adapters/hermes.js +0 -1684
- package/dist/services/runtime/adapters/hermes.js.map +0 -1
- package/dist/services/runtime/adapters/openclaw-mcporter.d.ts +0 -45
- package/dist/services/runtime/adapters/openclaw-mcporter.js +0 -108
- package/dist/services/runtime/adapters/openclaw-mcporter.js.map +0 -1
- package/dist/services/runtime/adapters/openclaw.d.ts +0 -426
- package/dist/services/runtime/adapters/openclaw.js +0 -3975
- package/dist/services/runtime/adapters/openclaw.js.map +0 -1
- package/dist/services/runtime/index.d.ts +0 -34
- package/dist/services/runtime/index.js +0 -51
- package/dist/services/runtime/index.js.map +0 -1
- package/dist/services/runtime/mcp-shims/anythingllm-shim.d.ts +0 -46
- package/dist/services/runtime/mcp-shims/anythingllm-shim.js +0 -281
- package/dist/services/runtime/mcp-shims/anythingllm-shim.js.map +0 -1
- package/dist/services/runtime/mcp-shims/drive-shim.js +0 -490
- package/dist/services/runtime/mcp-shims/drive-shim.js.map +0 -1
- package/dist/services/runtime/mcp-shims/jishukb-shim.d.ts +0 -48
- package/dist/services/runtime/mcp-shims/jishukb-shim.js +0 -723
- package/dist/services/runtime/mcp-shims/jishukb-shim.js.map +0 -1
- package/dist/services/runtime/mcp-shims/mcporter-lite.js +0 -276
- package/dist/services/runtime/mcp-shims/mcporter-lite.js.map +0 -1
- package/dist/services/runtime/migrations.d.ts +0 -23
- package/dist/services/runtime/migrations.js +0 -125
- package/dist/services/runtime/migrations.js.map +0 -1
- package/dist/services/runtime/registry.d.ts +0 -13
- package/dist/services/runtime/registry.js +0 -32
- package/dist/services/runtime/registry.js.map +0 -1
- package/dist/services/runtime-identity.d.ts +0 -13
- package/dist/services/runtime-identity.js +0 -166
- package/dist/services/runtime-identity.js.map +0 -1
- package/dist/services/runtime-repair.d.ts +0 -52
- package/dist/services/runtime-repair.js +0 -352
- package/dist/services/runtime-repair.js.map +0 -1
- package/dist/services/setup-manager.d.ts +0 -158
- package/dist/services/setup-manager.js +0 -2740
- package/dist/services/setup-manager.js.map +0 -1
- package/dist/services/suggestions.d.ts +0 -27
- package/dist/services/suggestions.js +0 -133
- package/dist/services/suggestions.js.map +0 -1
- package/dist/services/system-monitor.js +0 -79
- package/dist/services/system-monitor.js.map +0 -1
- package/dist/services/system-ollama-provider.d.ts +0 -14
- package/dist/services/system-ollama-provider.js +0 -125
- package/dist/services/system-ollama-provider.js.map +0 -1
- package/dist/services/system-reconciler.d.ts +0 -72
- package/dist/services/system-reconciler.js +0 -600
- package/dist/services/system-reconciler.js.map +0 -1
- package/dist/services/task-registry.d.ts +0 -44
- package/dist/services/task-registry.js +0 -76
- package/dist/services/task-registry.js.map +0 -1
- package/dist/services/types-shim.d.ts +0 -16
- package/dist/services/types-shim.js +0 -2
- package/dist/services/types-shim.js.map +0 -1
- package/dist/services/update-manager.d.ts +0 -47
- package/dist/services/update-manager.js +0 -351
- package/dist/services/update-manager.js.map +0 -1
- package/dist/services/webdav/server.d.ts +0 -24
- package/dist/services/webdav/server.js +0 -420
- package/dist/services/webdav/server.js.map +0 -1
- package/dist/services/webdav/xml-builder.js.map +0 -1
- package/dist/services/workspace-builder.d.ts +0 -29
- package/dist/services/workspace-builder.js +0 -188
- package/dist/services/workspace-builder.js.map +0 -1
- package/node_modules/@fastify/static/.github/stale.yml +0 -21
- package/node_modules/@isaacs/cliui/LICENSE.md +0 -63
- package/node_modules/@isaacs/cliui/README.md +0 -151
- package/node_modules/@isaacs/cliui/dist/commonjs/ansi-regex/index.d.ts +0 -4
- package/node_modules/@isaacs/cliui/dist/commonjs/ansi-regex/index.d.ts.map +0 -1
- package/node_modules/@isaacs/cliui/dist/commonjs/ansi-regex/index.js +0 -16
- package/node_modules/@isaacs/cliui/dist/commonjs/ansi-regex/index.js.map +0 -1
- package/node_modules/@isaacs/cliui/dist/commonjs/ansi-styles/index.d.ts +0 -34
- package/node_modules/@isaacs/cliui/dist/commonjs/ansi-styles/index.d.ts.map +0 -1
- package/node_modules/@isaacs/cliui/dist/commonjs/ansi-styles/index.js +0 -170
- package/node_modules/@isaacs/cliui/dist/commonjs/ansi-styles/index.js.map +0 -1
- package/node_modules/@isaacs/cliui/dist/commonjs/eastasianwidth/index.d.ts +0 -6
- package/node_modules/@isaacs/cliui/dist/commonjs/eastasianwidth/index.d.ts.map +0 -1
- package/node_modules/@isaacs/cliui/dist/commonjs/eastasianwidth/index.js +0 -307
- package/node_modules/@isaacs/cliui/dist/commonjs/eastasianwidth/index.js.map +0 -1
- package/node_modules/@isaacs/cliui/dist/commonjs/emoji-regex/index.d.ts +0 -2
- package/node_modules/@isaacs/cliui/dist/commonjs/emoji-regex/index.d.ts.map +0 -1
- package/node_modules/@isaacs/cliui/dist/commonjs/emoji-regex/index.js +0 -7
- package/node_modules/@isaacs/cliui/dist/commonjs/emoji-regex/index.js.map +0 -1
- package/node_modules/@isaacs/cliui/dist/commonjs/index.d.ts +0 -41
- package/node_modules/@isaacs/cliui/dist/commonjs/index.d.ts.map +0 -1
- package/node_modules/@isaacs/cliui/dist/commonjs/index.js +0 -322
- package/node_modules/@isaacs/cliui/dist/commonjs/index.js.map +0 -1
- package/node_modules/@isaacs/cliui/dist/commonjs/index.min.js +0 -12
- package/node_modules/@isaacs/cliui/dist/commonjs/index.min.js.map +0 -7
- package/node_modules/@isaacs/cliui/dist/commonjs/package.json +0 -3
- package/node_modules/@isaacs/cliui/dist/commonjs/string-width/index.d.ts +0 -5
- package/node_modules/@isaacs/cliui/dist/commonjs/string-width/index.d.ts.map +0 -1
- package/node_modules/@isaacs/cliui/dist/commonjs/string-width/index.js +0 -49
- package/node_modules/@isaacs/cliui/dist/commonjs/string-width/index.js.map +0 -1
- package/node_modules/@isaacs/cliui/dist/commonjs/strip-ansi/index.d.ts +0 -2
- package/node_modules/@isaacs/cliui/dist/commonjs/strip-ansi/index.d.ts.map +0 -1
- package/node_modules/@isaacs/cliui/dist/commonjs/strip-ansi/index.js +0 -8
- package/node_modules/@isaacs/cliui/dist/commonjs/strip-ansi/index.js.map +0 -1
- package/node_modules/@isaacs/cliui/dist/commonjs/wrap-ansi/index.d.ts +0 -7
- package/node_modules/@isaacs/cliui/dist/commonjs/wrap-ansi/index.d.ts.map +0 -1
- package/node_modules/@isaacs/cliui/dist/commonjs/wrap-ansi/index.js +0 -176
- package/node_modules/@isaacs/cliui/dist/commonjs/wrap-ansi/index.js.map +0 -1
- package/node_modules/@isaacs/cliui/dist/esm/ansi-regex/index.d.ts +0 -4
- package/node_modules/@isaacs/cliui/dist/esm/ansi-regex/index.d.ts.map +0 -1
- package/node_modules/@isaacs/cliui/dist/esm/ansi-regex/index.js +0 -12
- package/node_modules/@isaacs/cliui/dist/esm/ansi-regex/index.js.map +0 -1
- package/node_modules/@isaacs/cliui/dist/esm/ansi-styles/index.d.ts +0 -34
- package/node_modules/@isaacs/cliui/dist/esm/ansi-styles/index.d.ts.map +0 -1
- package/node_modules/@isaacs/cliui/dist/esm/ansi-styles/index.js +0 -167
- package/node_modules/@isaacs/cliui/dist/esm/ansi-styles/index.js.map +0 -1
- package/node_modules/@isaacs/cliui/dist/esm/eastasianwidth/index.d.ts +0 -6
- package/node_modules/@isaacs/cliui/dist/esm/eastasianwidth/index.d.ts.map +0 -1
- package/node_modules/@isaacs/cliui/dist/esm/eastasianwidth/index.js +0 -299
- package/node_modules/@isaacs/cliui/dist/esm/eastasianwidth/index.js.map +0 -1
- package/node_modules/@isaacs/cliui/dist/esm/emoji-regex/index.d.ts +0 -2
- package/node_modules/@isaacs/cliui/dist/esm/emoji-regex/index.d.ts.map +0 -1
- package/node_modules/@isaacs/cliui/dist/esm/emoji-regex/index.js +0 -3
- package/node_modules/@isaacs/cliui/dist/esm/emoji-regex/index.js.map +0 -1
- package/node_modules/@isaacs/cliui/dist/esm/index.d.ts +0 -41
- package/node_modules/@isaacs/cliui/dist/esm/index.d.ts.map +0 -1
- package/node_modules/@isaacs/cliui/dist/esm/index.js +0 -317
- package/node_modules/@isaacs/cliui/dist/esm/index.js.map +0 -1
- package/node_modules/@isaacs/cliui/dist/esm/index.min.js +0 -12
- package/node_modules/@isaacs/cliui/dist/esm/index.min.js.map +0 -7
- package/node_modules/@isaacs/cliui/dist/esm/package.json +0 -3
- package/node_modules/@isaacs/cliui/dist/esm/string-width/index.d.ts +0 -5
- package/node_modules/@isaacs/cliui/dist/esm/string-width/index.d.ts.map +0 -1
- package/node_modules/@isaacs/cliui/dist/esm/string-width/index.js +0 -46
- package/node_modules/@isaacs/cliui/dist/esm/string-width/index.js.map +0 -1
- package/node_modules/@isaacs/cliui/dist/esm/strip-ansi/index.d.ts +0 -2
- package/node_modules/@isaacs/cliui/dist/esm/strip-ansi/index.d.ts.map +0 -1
- package/node_modules/@isaacs/cliui/dist/esm/strip-ansi/index.js +0 -4
- package/node_modules/@isaacs/cliui/dist/esm/strip-ansi/index.js.map +0 -1
- package/node_modules/@isaacs/cliui/dist/esm/wrap-ansi/index.d.ts +0 -7
- package/node_modules/@isaacs/cliui/dist/esm/wrap-ansi/index.d.ts.map +0 -1
- package/node_modules/@isaacs/cliui/dist/esm/wrap-ansi/index.js +0 -172
- package/node_modules/@isaacs/cliui/dist/esm/wrap-ansi/index.js.map +0 -1
- package/node_modules/@isaacs/cliui/package.json +0 -163
- package/node_modules/content-disposition/HISTORY.md +0 -60
- package/node_modules/cross-spawn/LICENSE +0 -21
- package/node_modules/cross-spawn/README.md +0 -89
- package/node_modules/cross-spawn/index.js +0 -39
- package/node_modules/cross-spawn/lib/enoent.js +0 -59
- package/node_modules/cross-spawn/lib/parse.js +0 -91
- package/node_modules/cross-spawn/lib/util/escape.js +0 -47
- package/node_modules/cross-spawn/lib/util/readShebang.js +0 -23
- package/node_modules/cross-spawn/lib/util/resolveCommand.js +0 -52
- package/node_modules/cross-spawn/package.json +0 -73
- package/node_modules/fastify/test/types/content-type-parser.test-d.ts +0 -72
- package/node_modules/fastify/test/types/decorate-request-reply.test-d.ts +0 -18
- package/node_modules/fastify/test/types/dummy-plugin.ts +0 -9
- package/node_modules/fastify/test/types/errors.test-d.ts +0 -90
- package/node_modules/fastify/test/types/fastify.test-d.ts +0 -352
- package/node_modules/fastify/test/types/hooks.test-d.ts +0 -550
- package/node_modules/fastify/test/types/import.ts +0 -2
- package/node_modules/fastify/test/types/instance.test-d.ts +0 -588
- package/node_modules/fastify/test/types/logger.test-d.ts +0 -277
- package/node_modules/fastify/test/types/plugin.test-d.ts +0 -97
- package/node_modules/fastify/test/types/register.test-d.ts +0 -237
- package/node_modules/fastify/test/types/reply.test-d.ts +0 -254
- package/node_modules/fastify/test/types/request.test-d.ts +0 -188
- package/node_modules/fastify/test/types/route.test-d.ts +0 -553
- package/node_modules/fastify/test/types/schema.test-d.ts +0 -135
- package/node_modules/fastify/test/types/serverFactory.test-d.ts +0 -37
- package/node_modules/fastify/test/types/type-provider.test-d.ts +0 -1213
- package/node_modules/fastify/test/types/using.test-d.ts +0 -17
- package/node_modules/foreground-child/LICENSE +0 -15
- package/node_modules/foreground-child/README.md +0 -128
- package/node_modules/foreground-child/dist/commonjs/all-signals.d.ts +0 -2
- package/node_modules/foreground-child/dist/commonjs/all-signals.d.ts.map +0 -1
- package/node_modules/foreground-child/dist/commonjs/all-signals.js +0 -58
- package/node_modules/foreground-child/dist/commonjs/all-signals.js.map +0 -1
- package/node_modules/foreground-child/dist/commonjs/index.d.ts +0 -58
- package/node_modules/foreground-child/dist/commonjs/index.d.ts.map +0 -1
- package/node_modules/foreground-child/dist/commonjs/index.js +0 -123
- package/node_modules/foreground-child/dist/commonjs/index.js.map +0 -1
- package/node_modules/foreground-child/dist/commonjs/package.json +0 -3
- package/node_modules/foreground-child/dist/commonjs/proxy-signals.d.ts +0 -6
- package/node_modules/foreground-child/dist/commonjs/proxy-signals.d.ts.map +0 -1
- package/node_modules/foreground-child/dist/commonjs/proxy-signals.js +0 -38
- package/node_modules/foreground-child/dist/commonjs/proxy-signals.js.map +0 -1
- package/node_modules/foreground-child/dist/commonjs/watchdog.d.ts +0 -10
- package/node_modules/foreground-child/dist/commonjs/watchdog.d.ts.map +0 -1
- package/node_modules/foreground-child/dist/commonjs/watchdog.js +0 -50
- package/node_modules/foreground-child/dist/commonjs/watchdog.js.map +0 -1
- package/node_modules/foreground-child/dist/esm/all-signals.d.ts +0 -2
- package/node_modules/foreground-child/dist/esm/all-signals.d.ts.map +0 -1
- package/node_modules/foreground-child/dist/esm/all-signals.js +0 -52
- package/node_modules/foreground-child/dist/esm/all-signals.js.map +0 -1
- package/node_modules/foreground-child/dist/esm/index.d.ts +0 -58
- package/node_modules/foreground-child/dist/esm/index.d.ts.map +0 -1
- package/node_modules/foreground-child/dist/esm/index.js +0 -115
- package/node_modules/foreground-child/dist/esm/index.js.map +0 -1
- package/node_modules/foreground-child/dist/esm/package.json +0 -3
- package/node_modules/foreground-child/dist/esm/proxy-signals.d.ts +0 -6
- package/node_modules/foreground-child/dist/esm/proxy-signals.d.ts.map +0 -1
- package/node_modules/foreground-child/dist/esm/proxy-signals.js +0 -34
- package/node_modules/foreground-child/dist/esm/proxy-signals.js.map +0 -1
- package/node_modules/foreground-child/dist/esm/watchdog.d.ts +0 -10
- package/node_modules/foreground-child/dist/esm/watchdog.d.ts.map +0 -1
- package/node_modules/foreground-child/dist/esm/watchdog.js +0 -46
- package/node_modules/foreground-child/dist/esm/watchdog.js.map +0 -1
- package/node_modules/foreground-child/package.json +0 -106
- package/node_modules/glob/dist/esm/bin.d.mts +0 -3
- package/node_modules/glob/dist/esm/bin.d.mts.map +0 -1
- package/node_modules/glob/dist/esm/bin.mjs +0 -346
- package/node_modules/glob/dist/esm/bin.mjs.map +0 -1
- package/node_modules/isexe/.npmignore +0 -2
- package/node_modules/isexe/LICENSE +0 -15
- package/node_modules/isexe/README.md +0 -51
- package/node_modules/isexe/index.js +0 -57
- package/node_modules/isexe/mode.js +0 -41
- package/node_modules/isexe/package.json +0 -31
- package/node_modules/isexe/test/basic.js +0 -221
- package/node_modules/isexe/windows.js +0 -42
- package/node_modules/jackspeak/LICENSE.md +0 -55
- package/node_modules/jackspeak/README.md +0 -394
- package/node_modules/jackspeak/dist/commonjs/index.d.ts +0 -323
- package/node_modules/jackspeak/dist/commonjs/index.d.ts.map +0 -1
- package/node_modules/jackspeak/dist/commonjs/index.js +0 -944
- package/node_modules/jackspeak/dist/commonjs/index.js.map +0 -1
- package/node_modules/jackspeak/dist/commonjs/index.min.js +0 -33
- package/node_modules/jackspeak/dist/commonjs/index.min.js.map +0 -7
- package/node_modules/jackspeak/dist/commonjs/package.json +0 -3
- package/node_modules/jackspeak/dist/esm/index.d.ts +0 -323
- package/node_modules/jackspeak/dist/esm/index.d.ts.map +0 -1
- package/node_modules/jackspeak/dist/esm/index.js +0 -936
- package/node_modules/jackspeak/dist/esm/index.js.map +0 -1
- package/node_modules/jackspeak/dist/esm/index.min.js +0 -33
- package/node_modules/jackspeak/dist/esm/index.min.js.map +0 -7
- package/node_modules/jackspeak/dist/esm/package.json +0 -3
- package/node_modules/jackspeak/package.json +0 -115
- package/node_modules/jishushell-panel/output/public/assets/ApiKeyField-D1i7zWXR.js +0 -1
- package/node_modules/jishushell-panel/output/public/assets/Dashboard-sWIvL43F.js +0 -1
- package/node_modules/jishushell-panel/output/public/assets/HermesChatPanel-DQ8RyvQY.js +0 -1
- package/node_modules/jishushell-panel/output/public/assets/HermesConfigForm-tIbPP1sB.js +0 -4
- package/node_modules/jishushell-panel/output/public/assets/InitPassword-C3Slq3Dd.js +0 -1
- package/node_modules/jishushell-panel/output/public/assets/InstanceDetail-7JqY9tq4.js +0 -92
- package/node_modules/jishushell-panel/output/public/assets/Login-BXLDJlQN.js +0 -1
- package/node_modules/jishushell-panel/output/public/assets/NewInstance-dLc5Xrpx.js +0 -1
- package/node_modules/jishushell-panel/output/public/assets/ProviderRecommendations-DIAXxesl.js +0 -1
- package/node_modules/jishushell-panel/output/public/assets/Settings-Bd5utbBh.js +0 -1
- package/node_modules/jishushell-panel/output/public/assets/Setup-Yn9_20FL.js +0 -1
- package/node_modules/jishushell-panel/output/public/assets/WeixinLoginPanel-C21doQTJ.js +0 -9
- package/node_modules/jishushell-panel/output/public/assets/index-CCkaIEjn.js +0 -20
- package/node_modules/jishushell-panel/output/public/assets/index-D7qxy-Vh.css +0 -1
- package/node_modules/jishushell-panel/output/public/assets/registry-B2ZQZXWL.js +0 -2
- package/node_modules/jishushell-panel/output/public/assets/usePolling-BFZm4do_.js +0 -1
- package/node_modules/jishushell-panel/output/public/assets/vendor-i18n-DqPtOicc.js +0 -9
- package/node_modules/jishushell-panel/output/public/assets/vendor-react-DW5juQin.js +0 -59
- package/node_modules/package-json-from-dist/LICENSE.md +0 -63
- package/node_modules/package-json-from-dist/README.md +0 -110
- package/node_modules/package-json-from-dist/dist/commonjs/index.d.ts +0 -89
- package/node_modules/package-json-from-dist/dist/commonjs/index.d.ts.map +0 -1
- package/node_modules/package-json-from-dist/dist/commonjs/index.js +0 -134
- package/node_modules/package-json-from-dist/dist/commonjs/index.js.map +0 -1
- package/node_modules/package-json-from-dist/dist/commonjs/package.json +0 -3
- package/node_modules/package-json-from-dist/dist/esm/index.d.ts +0 -89
- package/node_modules/package-json-from-dist/dist/esm/index.d.ts.map +0 -1
- package/node_modules/package-json-from-dist/dist/esm/index.js +0 -129
- package/node_modules/package-json-from-dist/dist/esm/index.js.map +0 -1
- package/node_modules/package-json-from-dist/dist/esm/package.json +0 -3
- package/node_modules/package-json-from-dist/package.json +0 -68
- package/node_modules/path-key/index.d.ts +0 -40
- package/node_modules/path-key/index.js +0 -16
- package/node_modules/path-key/license +0 -9
- package/node_modules/path-key/package.json +0 -39
- package/node_modules/path-key/readme.md +0 -61
- package/node_modules/safe-buffer/LICENSE +0 -21
- package/node_modules/safe-buffer/README.md +0 -584
- package/node_modules/safe-buffer/index.d.ts +0 -187
- package/node_modules/safe-buffer/index.js +0 -65
- package/node_modules/safe-buffer/package.json +0 -51
- package/node_modules/shebang-command/index.js +0 -19
- package/node_modules/shebang-command/license +0 -9
- package/node_modules/shebang-command/package.json +0 -34
- package/node_modules/shebang-command/readme.md +0 -34
- package/node_modules/shebang-regex/index.d.ts +0 -22
- package/node_modules/shebang-regex/index.js +0 -2
- package/node_modules/shebang-regex/license +0 -9
- package/node_modules/shebang-regex/package.json +0 -35
- package/node_modules/shebang-regex/readme.md +0 -33
- package/node_modules/signal-exit/LICENSE.txt +0 -16
- package/node_modules/signal-exit/README.md +0 -74
- package/node_modules/signal-exit/dist/cjs/browser.d.ts +0 -12
- package/node_modules/signal-exit/dist/cjs/browser.d.ts.map +0 -1
- package/node_modules/signal-exit/dist/cjs/browser.js +0 -10
- package/node_modules/signal-exit/dist/cjs/browser.js.map +0 -1
- package/node_modules/signal-exit/dist/cjs/index.d.ts +0 -48
- package/node_modules/signal-exit/dist/cjs/index.d.ts.map +0 -1
- package/node_modules/signal-exit/dist/cjs/index.js +0 -279
- package/node_modules/signal-exit/dist/cjs/index.js.map +0 -1
- package/node_modules/signal-exit/dist/cjs/package.json +0 -3
- package/node_modules/signal-exit/dist/cjs/signals.d.ts +0 -29
- package/node_modules/signal-exit/dist/cjs/signals.d.ts.map +0 -1
- package/node_modules/signal-exit/dist/cjs/signals.js +0 -42
- package/node_modules/signal-exit/dist/cjs/signals.js.map +0 -1
- package/node_modules/signal-exit/dist/mjs/browser.d.ts +0 -12
- package/node_modules/signal-exit/dist/mjs/browser.d.ts.map +0 -1
- package/node_modules/signal-exit/dist/mjs/browser.js +0 -4
- package/node_modules/signal-exit/dist/mjs/browser.js.map +0 -1
- package/node_modules/signal-exit/dist/mjs/index.d.ts +0 -48
- package/node_modules/signal-exit/dist/mjs/index.d.ts.map +0 -1
- package/node_modules/signal-exit/dist/mjs/index.js +0 -275
- package/node_modules/signal-exit/dist/mjs/index.js.map +0 -1
- package/node_modules/signal-exit/dist/mjs/package.json +0 -3
- package/node_modules/signal-exit/dist/mjs/signals.d.ts +0 -29
- package/node_modules/signal-exit/dist/mjs/signals.d.ts.map +0 -1
- package/node_modules/signal-exit/dist/mjs/signals.js +0 -39
- package/node_modules/signal-exit/dist/mjs/signals.js.map +0 -1
- package/node_modules/signal-exit/package.json +0 -106
- package/node_modules/which/CHANGELOG.md +0 -166
- package/node_modules/which/LICENSE +0 -15
- package/node_modules/which/README.md +0 -54
- package/node_modules/which/bin/node-which +0 -52
- package/node_modules/which/package.json +0 -43
- package/node_modules/which/which.js +0 -125
- package/scripts/check-adapter-isolation.ts +0 -293
- /package/dist/services/{app → app-common}/app-compiler.d.ts +0 -0
- /package/dist/services/{app → app-common}/terminal-session-manager.d.ts +0 -0
- /package/dist/services/{backup-verify.d.ts → backup/backup-verify.d.ts} +0 -0
- /package/dist/services/{external-mounts.d.ts → files/external-mounts.d.ts} +0 -0
- /package/dist/services/{organize → files/organize}/applier.d.ts +0 -0
- /package/dist/services/{organize → files/organize}/rules.d.ts +0 -0
- /package/dist/services/{organize → files/organize}/scanner.d.ts +0 -0
- /package/dist/services/{organize → files/organize}/store.d.ts +0 -0
- /package/dist/services/{webdav → files/webdav}/xml-builder.d.ts +0 -0
- /package/dist/services/{webdav → files/webdav}/xml-builder.js +0 -0
- /package/dist/services/{app-passwords.d.ts → instances/passwords.d.ts} +0 -0
- /package/dist/services/{agent-apps → integrations/installable}/installers/registry-probe.d.ts +0 -0
- /package/dist/services/{agent-apps → integrations/installable}/installers/registry-probe.js +0 -0
- /package/dist/services/{runtime/mcp-shims → integrations/openclaw}/drive-shim.d.ts +0 -0
- /package/dist/services/{runtime/mcp-shims → integrations/openclaw}/mcporter-lite.d.ts +0 -0
- /package/dist/services/{plugin-installer.d.ts → setup/plugin-installer.d.ts} +0 -0
- /package/dist/services/{macos-launchd.d.ts → system/macos-launchd.d.ts} +0 -0
- /package/dist/services/{system-monitor.d.ts → system/system-monitor.d.ts} +0 -0
|
@@ -0,0 +1,2318 @@
|
|
|
1
|
+
import { execFileSync, spawn as spawnChild } from "child_process";
|
|
2
|
+
import { createHash, randomBytes, randomUUID } from "crypto";
|
|
3
|
+
import { copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "fs";
|
|
4
|
+
import { dirname, join, relative } from "path";
|
|
5
|
+
import { posix as pathPosix } from "path";
|
|
6
|
+
import { BACKUPS_DIR, TMP_DIR } from "../../config.js";
|
|
7
|
+
/**
|
|
8
|
+
* Encode an absolute filesystem path for use inside backup archives.
|
|
9
|
+
* Ported from OpenClaw official: encodeAbsolutePathForBackupArchive.
|
|
10
|
+
*/
|
|
11
|
+
export function encodeAbsolutePathForArchive(sourcePath) {
|
|
12
|
+
const normalized = sourcePath.replaceAll("\\", "/");
|
|
13
|
+
const windowsMatch = normalized.match(/^([A-Za-z]):\/(.*)$/);
|
|
14
|
+
if (windowsMatch) {
|
|
15
|
+
const drive = (windowsMatch[1] ?? "UNKNOWN").toUpperCase();
|
|
16
|
+
const rest = windowsMatch[2] ?? "";
|
|
17
|
+
return pathPosix.join("windows", drive, rest);
|
|
18
|
+
}
|
|
19
|
+
if (normalized.startsWith("/"))
|
|
20
|
+
return pathPosix.join("posix", normalized.slice(1));
|
|
21
|
+
return pathPosix.join("relative", normalized);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Decode a path produced by {@link encodeAbsolutePathForArchive} back to
|
|
25
|
+
* the original absolute filesystem path.
|
|
26
|
+
*
|
|
27
|
+
* - `"posix/Users/arm/foo"` → `"/Users/arm/foo"`
|
|
28
|
+
* - `"windows/C/Users/foo"` → `"C:\\Users\\foo"`
|
|
29
|
+
* - `"relative/some/path"` → `"some/path"`
|
|
30
|
+
*
|
|
31
|
+
* Returns the input unchanged when the prefix is unrecognised.
|
|
32
|
+
*/
|
|
33
|
+
export function decodeArchivePath(encodedPath) {
|
|
34
|
+
const normalized = encodedPath.replaceAll("\\", "/");
|
|
35
|
+
if (normalized.startsWith("posix/")) {
|
|
36
|
+
return "/" + normalized.slice("posix/".length);
|
|
37
|
+
}
|
|
38
|
+
if (normalized.startsWith("windows/")) {
|
|
39
|
+
const rest = normalized.slice("windows/".length);
|
|
40
|
+
const slashIdx = rest.indexOf("/");
|
|
41
|
+
const drive = slashIdx >= 0 ? rest.slice(0, slashIdx) : rest;
|
|
42
|
+
// Drive letter must be a single alphabetic character
|
|
43
|
+
if (!/^[A-Za-z]$/.test(drive))
|
|
44
|
+
return encodedPath;
|
|
45
|
+
const tail = slashIdx >= 0 ? rest.slice(slashIdx + 1) : "";
|
|
46
|
+
return `${drive}:\\${tail.replaceAll("/", "\\")}`;
|
|
47
|
+
}
|
|
48
|
+
if (normalized.startsWith("relative/")) {
|
|
49
|
+
return normalized.slice("relative/".length);
|
|
50
|
+
}
|
|
51
|
+
return encodedPath;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Validate that a decoded `oldHome` path is safe to use with `replaceAll`.
|
|
55
|
+
*
|
|
56
|
+
* Guards against catastrophic replacements when the inferred path is empty,
|
|
57
|
+
* a filesystem root, or relative (which would match too broadly).
|
|
58
|
+
*/
|
|
59
|
+
function isValidOldHome(path) {
|
|
60
|
+
if (!path || path === "." || path === "..")
|
|
61
|
+
return false;
|
|
62
|
+
// POSIX: must start with / and not be root itself
|
|
63
|
+
if (path.startsWith("/"))
|
|
64
|
+
return path.length > 1;
|
|
65
|
+
// Windows: must be drive:\something (at least one segment after drive)
|
|
66
|
+
const winMatch = path.match(/^[A-Za-z]:[/\\](.+)$/);
|
|
67
|
+
if (winMatch)
|
|
68
|
+
return true;
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const MAX_REWRITE_FILE_SIZE = 100 * 1024 * 1024; // 100 MB
|
|
72
|
+
function integrationHomeFromMetadata(meta, instanceId) {
|
|
73
|
+
const integrationHome = meta?.paths?.integrationHome;
|
|
74
|
+
if (typeof integrationHome === "string" && integrationHome.trim()) {
|
|
75
|
+
return integrationHome;
|
|
76
|
+
}
|
|
77
|
+
throw Object.assign(new Error(`Instance '${instanceId}' has no canonical paths.integrationHome; run upgrade repair before backup restore`), { statusCode: 409, code: "needs-migration" });
|
|
78
|
+
}
|
|
79
|
+
/** Core facts/derived layouts must never be restored from another instance. */
|
|
80
|
+
function isNonPortableHomeEntry(rel) {
|
|
81
|
+
return rel === ".jishushell-owned"
|
|
82
|
+
|| rel === "workspace"
|
|
83
|
+
|| rel.startsWith("workspace/")
|
|
84
|
+
|| rel === "openclaw.json";
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Rewrite absolute paths in session data and symlinks after copying from a
|
|
88
|
+
* backup archive. Best-effort per file: individual failures produce warnings
|
|
89
|
+
* but do not block the import or affect other files.
|
|
90
|
+
*
|
|
91
|
+
* Walks all `.json` and `.jsonl` files under {@link targetDir} recursively
|
|
92
|
+
* and replaces occurrences of {@link oldHome} with {@link newHome}. Also
|
|
93
|
+
* rewrites symlink targets that contain {@link oldHome}.
|
|
94
|
+
*/
|
|
95
|
+
function rewriteInstancePaths(targetDir, oldHome, newHome, warnings) {
|
|
96
|
+
if (oldHome === newHome)
|
|
97
|
+
return;
|
|
98
|
+
if (!isValidOldHome(oldHome)) {
|
|
99
|
+
warnings.push(`Path rewrite skipped: inferred source path "${oldHome}" is invalid or too broad`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const walkDir = (dir) => {
|
|
103
|
+
if (!existsSync(dir))
|
|
104
|
+
return;
|
|
105
|
+
let entries;
|
|
106
|
+
try {
|
|
107
|
+
entries = readdirSync(dir);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
const fullPath = join(dir, entry);
|
|
114
|
+
let s;
|
|
115
|
+
try {
|
|
116
|
+
s = lstatSync(fullPath);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// Rewrite symlink targets
|
|
122
|
+
if (s.isSymbolicLink()) {
|
|
123
|
+
try {
|
|
124
|
+
const target = readlinkSync(fullPath);
|
|
125
|
+
if (target.includes(oldHome)) {
|
|
126
|
+
unlinkSync(fullPath);
|
|
127
|
+
symlinkSync(target.replaceAll(oldHome, newHome), fullPath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
warnings.push(`Symlink rewrite failed for ${entry}: ${e.message}`);
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (s.isDirectory()) {
|
|
136
|
+
walkDir(fullPath);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
// Only process .json and .jsonl regular files
|
|
140
|
+
if (!s.isFile())
|
|
141
|
+
continue;
|
|
142
|
+
if (!entry.endsWith(".json") && !entry.endsWith(".jsonl"))
|
|
143
|
+
continue;
|
|
144
|
+
if (s.size > MAX_REWRITE_FILE_SIZE) {
|
|
145
|
+
warnings.push(`Skipped path rewrite for large file: ${entry} (${s.size} bytes)`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
150
|
+
if (content.includes(oldHome)) {
|
|
151
|
+
writeFileSync(fullPath, content.replaceAll(oldHome, newHome));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
warnings.push(`Path rewrite failed for ${entry}: ${e.message}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
walkDir(targetDir);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Detect the original `openclawHome` path that was used when the backup
|
|
163
|
+
* archive was created.
|
|
164
|
+
*
|
|
165
|
+
* Priority:
|
|
166
|
+
* 1. `manifest.source_home` (written by newer selfPackOfficialFormat)
|
|
167
|
+
* 2. Fallback: decode the encoded path from the payload/ directory structure
|
|
168
|
+
*/
|
|
169
|
+
function detectOldHome(manifest, payloadDir, extractedStateDir) {
|
|
170
|
+
// Priority 1: manifest metadata
|
|
171
|
+
if (manifest?.source_home && typeof manifest.source_home === "string") {
|
|
172
|
+
return manifest.source_home;
|
|
173
|
+
}
|
|
174
|
+
// Priority 2: decode from payload/ path structure
|
|
175
|
+
try {
|
|
176
|
+
const extractedHomeDir = dirname(extractedStateDir);
|
|
177
|
+
const relativeToPayload = relative(payloadDir, extractedHomeDir);
|
|
178
|
+
// Reject if relative() produced a `..` traversal (unexpected layout)
|
|
179
|
+
if (relativeToPayload.startsWith(".."))
|
|
180
|
+
return null;
|
|
181
|
+
return decodeArchivePath(relativeToPayload);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async function installOpenclawRestoreTarget(instanceId, name, description = "") {
|
|
188
|
+
const { getInstance, listBuiltinAppSpecs } = await import("../app-common/service.js");
|
|
189
|
+
const { getIntegration } = await import("../integrations/index.js");
|
|
190
|
+
const { installInstance } = await import("../app-common/lifecycle-service.js");
|
|
191
|
+
const template = listBuiltinAppSpecs().find((entry) => entry.fileName === "openclaw-container.yaml" || entry.id === "openclaw-container");
|
|
192
|
+
if (!template) {
|
|
193
|
+
throw new Error("Builtin app spec 'openclaw-container.yaml' not found");
|
|
194
|
+
}
|
|
195
|
+
await installInstance(template.yaml, instanceId, {
|
|
196
|
+
bootstrap: {
|
|
197
|
+
name,
|
|
198
|
+
description,
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
const meta = getInstance(instanceId);
|
|
202
|
+
if (!meta) {
|
|
203
|
+
throw new Error(`Imported instance '${instanceId}' did not produce canonical instance metadata`);
|
|
204
|
+
}
|
|
205
|
+
const integrationHome = meta.paths?.integrationHome;
|
|
206
|
+
if (typeof integrationHome !== "string" || !integrationHome.trim()) {
|
|
207
|
+
throw new Error(`Imported instance '${instanceId}' has no canonical paths.integrationHome`);
|
|
208
|
+
}
|
|
209
|
+
const integration = getIntegration("openclaw");
|
|
210
|
+
if (typeof integration.resolveAgentHome !== "function") {
|
|
211
|
+
throw new Error("OpenClaw integration does not expose its canonical home path");
|
|
212
|
+
}
|
|
213
|
+
const openclawHome = integration.resolveAgentHome(instanceId);
|
|
214
|
+
if (openclawHome !== integrationHome) {
|
|
215
|
+
throw new Error(`Imported instance '${instanceId}' integration home is inconsistent`);
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
openclawHome,
|
|
219
|
+
stateDir: join(openclawHome, ".openclaw"),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
async function rollbackOpenclawRestoreTarget(instanceId) {
|
|
223
|
+
const { uninstallInstance } = await import("../app-common/lifecycle-service.js");
|
|
224
|
+
await uninstallInstance(instanceId);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Try to create a backup by calling the official `openclaw backup create` CLI.
|
|
228
|
+
* Returns ok:false if the binary is missing or the command fails.
|
|
229
|
+
*/
|
|
230
|
+
export async function callOpenclawBackup(instanceId, outputDir, opts) {
|
|
231
|
+
// Resolve executable details through the integration so backup-manager
|
|
232
|
+
// does not reach into OpenClaw's private implementation. The backup CLI
|
|
233
|
+
// itself is OpenClaw-specific today, but bin/env discovery belongs to the
|
|
234
|
+
// integration that owns that CLI contract.
|
|
235
|
+
const { getIntegration } = await import("../integrations/index.js");
|
|
236
|
+
const { resolveExecutionOwner } = await import("../app-common/execution-owner.js");
|
|
237
|
+
const owner = resolveExecutionOwner(instanceId);
|
|
238
|
+
const kind = owner?.type === "integration" ? owner.integrationKind : undefined;
|
|
239
|
+
if (!kind) {
|
|
240
|
+
return { ok: false, error: `Instance '${instanceId}' does not expose a runtime integration backup CLI` };
|
|
241
|
+
}
|
|
242
|
+
const integration = getIntegration(kind);
|
|
243
|
+
if (typeof integration.resolveBin !== "function" || typeof integration.buildCliEnv !== "function") {
|
|
244
|
+
return { ok: false, error: `Runtime "${kind}" does not support backup CLI` };
|
|
245
|
+
}
|
|
246
|
+
const openclawBin = integration.resolveBin();
|
|
247
|
+
const cliEnv = integration.buildCliEnv(instanceId);
|
|
248
|
+
if (!existsSync(openclawBin)) {
|
|
249
|
+
return { ok: false, error: `agent binary not found: ${openclawBin}` };
|
|
250
|
+
}
|
|
251
|
+
if (!existsSync(outputDir))
|
|
252
|
+
mkdirSync(outputDir, { recursive: true });
|
|
253
|
+
const preExisting = new Set(readdirSync(outputDir).filter(f => f.endsWith(".tar.gz")));
|
|
254
|
+
const args = ["backup", "create", "--output", outputDir];
|
|
255
|
+
if (opts.onlyConfig)
|
|
256
|
+
args.push("--only-config");
|
|
257
|
+
if (opts.noWorkspace)
|
|
258
|
+
args.push("--no-include-workspace");
|
|
259
|
+
try {
|
|
260
|
+
await new Promise((resolve, reject) => {
|
|
261
|
+
const child = spawnChild(openclawBin, args, {
|
|
262
|
+
env: { ...process.env, ...cliEnv },
|
|
263
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
264
|
+
timeout: 120_000,
|
|
265
|
+
});
|
|
266
|
+
let stderr = "";
|
|
267
|
+
child.stderr?.on("data", (chunk) => { stderr += chunk.toString(); });
|
|
268
|
+
child.on("close", (code) => {
|
|
269
|
+
if (code !== 0)
|
|
270
|
+
reject(new Error(`openclaw backup create exited ${code}: ${stderr.slice(0, 500)}`));
|
|
271
|
+
else
|
|
272
|
+
resolve();
|
|
273
|
+
});
|
|
274
|
+
child.on("error", (err) => reject(err));
|
|
275
|
+
});
|
|
276
|
+
// Pick up only entries created by THIS CLI run. If the CLI happens to
|
|
277
|
+
// produce more than one archive (it currently produces exactly one),
|
|
278
|
+
// take the newest by mtime.
|
|
279
|
+
const newFiles = readdirSync(outputDir)
|
|
280
|
+
.filter(f => f.endsWith(".tar.gz") && !preExisting.has(f))
|
|
281
|
+
.map(f => ({ name: f, mtime: statSync(join(outputDir, f)).mtimeMs }))
|
|
282
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
283
|
+
if (newFiles.length === 0) {
|
|
284
|
+
return { ok: false, error: "openclaw backup create produced no new .tar.gz file" };
|
|
285
|
+
}
|
|
286
|
+
return { ok: true, archivePath: join(outputDir, newFiles[0].name) };
|
|
287
|
+
}
|
|
288
|
+
catch (e) {
|
|
289
|
+
return { ok: false, error: e.message };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// ── Directory initialization ──
|
|
293
|
+
/** Ensure backup and tmp directories exist */
|
|
294
|
+
export function ensureBackupDirs() {
|
|
295
|
+
for (const dir of [BACKUPS_DIR, TMP_DIR]) {
|
|
296
|
+
if (!existsSync(dir))
|
|
297
|
+
mkdirSync(dir, { recursive: true, mode: 0o755 });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// ── Tmp file management ──
|
|
301
|
+
const TMP_MAX_AGE_MS = 30 * 60 * 1000; // 30 minutes
|
|
302
|
+
const EXPORTS_SUBDIR = "exports";
|
|
303
|
+
/**
|
|
304
|
+
* Get the per-instance export output directory (TMP_DIR/exports/<id>/).
|
|
305
|
+
* Exports live in an isolated subdirectory so the download route can enforce
|
|
306
|
+
* `:id` → file ownership and one instance's URL can't reach another's archive.
|
|
307
|
+
*/
|
|
308
|
+
export function getInstanceExportDir(instanceId) {
|
|
309
|
+
const dir = join(TMP_DIR, EXPORTS_SUBDIR, instanceId);
|
|
310
|
+
if (!existsSync(dir))
|
|
311
|
+
mkdirSync(dir, { recursive: true, mode: 0o755 });
|
|
312
|
+
return dir;
|
|
313
|
+
}
|
|
314
|
+
/** Clean up stale tmp files older than 30 minutes. Call on startup and periodically. */
|
|
315
|
+
export function cleanupStaleTmpFiles() {
|
|
316
|
+
if (!existsSync(TMP_DIR))
|
|
317
|
+
return 0;
|
|
318
|
+
let cleaned = 0;
|
|
319
|
+
const now = Date.now();
|
|
320
|
+
for (const entry of readdirSync(TMP_DIR)) {
|
|
321
|
+
const fullPath = join(TMP_DIR, entry);
|
|
322
|
+
// Exports live in per-instance subdirs; sweep them one file at a time so
|
|
323
|
+
// we don't wipe an active export collection because of a stale sibling.
|
|
324
|
+
if (entry === EXPORTS_SUBDIR) {
|
|
325
|
+
try {
|
|
326
|
+
for (const instanceEntry of readdirSync(fullPath)) {
|
|
327
|
+
const instanceDir = join(fullPath, instanceEntry);
|
|
328
|
+
try {
|
|
329
|
+
if (!statSync(instanceDir).isDirectory())
|
|
330
|
+
continue;
|
|
331
|
+
for (const fileEntry of readdirSync(instanceDir)) {
|
|
332
|
+
const filePath = join(instanceDir, fileEntry);
|
|
333
|
+
try {
|
|
334
|
+
const stat = statSync(filePath);
|
|
335
|
+
if (now - stat.mtimeMs > TMP_MAX_AGE_MS) {
|
|
336
|
+
rmSync(filePath, { recursive: true, force: true });
|
|
337
|
+
cleaned++;
|
|
338
|
+
console.log(`[backup] Cleaned stale export: ${instanceEntry}/${fileEntry}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch { /* skip */ }
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch { /* skip */ }
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch { /* skip */ }
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
const stat = statSync(fullPath);
|
|
352
|
+
if (now - stat.mtimeMs > TMP_MAX_AGE_MS) {
|
|
353
|
+
rmSync(fullPath, { recursive: true, force: true });
|
|
354
|
+
cleaned++;
|
|
355
|
+
console.log(`[backup] Cleaned stale tmp: ${entry}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
// Skip entries that disappeared during iteration
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return cleaned;
|
|
363
|
+
}
|
|
364
|
+
// ── Tmp cleanup scheduler ──
|
|
365
|
+
let cleanupTimer = null;
|
|
366
|
+
const CLEANUP_INTERVAL_MS = 10 * 60 * 1000; // Check every 10 minutes
|
|
367
|
+
/** Start periodic tmp cleanup. Call once at server startup. */
|
|
368
|
+
export function startTmpCleanupScheduler() {
|
|
369
|
+
// Run immediately on startup
|
|
370
|
+
ensureBackupDirs();
|
|
371
|
+
cleanupStaleTmpFiles();
|
|
372
|
+
// Schedule periodic cleanup
|
|
373
|
+
if (cleanupTimer)
|
|
374
|
+
clearInterval(cleanupTimer);
|
|
375
|
+
cleanupTimer = setInterval(() => {
|
|
376
|
+
cleanupStaleTmpFiles();
|
|
377
|
+
}, CLEANUP_INTERVAL_MS);
|
|
378
|
+
// Don't prevent process exit
|
|
379
|
+
if (cleanupTimer.unref)
|
|
380
|
+
cleanupTimer.unref();
|
|
381
|
+
}
|
|
382
|
+
/** Stop the cleanup scheduler (for graceful shutdown). */
|
|
383
|
+
export function stopTmpCleanupScheduler() {
|
|
384
|
+
if (cleanupTimer) {
|
|
385
|
+
clearInterval(cleanupTimer);
|
|
386
|
+
cleanupTimer = null;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// ── Backup directory helpers ──
|
|
390
|
+
/** Get the backup directory for an instance (creates if needed) */
|
|
391
|
+
export function getInstanceBackupDir(instanceId) {
|
|
392
|
+
const dir = join(BACKUPS_DIR, instanceId);
|
|
393
|
+
if (!existsSync(dir))
|
|
394
|
+
mkdirSync(dir, { recursive: true, mode: 0o755 });
|
|
395
|
+
return dir;
|
|
396
|
+
}
|
|
397
|
+
/** List all backup files for an instance, sorted by mtime descending (newest first) */
|
|
398
|
+
export function listInstanceBackups(instanceId) {
|
|
399
|
+
const dir = join(BACKUPS_DIR, instanceId);
|
|
400
|
+
if (!existsSync(dir))
|
|
401
|
+
return [];
|
|
402
|
+
return readdirSync(dir)
|
|
403
|
+
.filter(f => f.endsWith(".tar.gz"))
|
|
404
|
+
.map(filename => {
|
|
405
|
+
const stat = statSync(join(dir, filename));
|
|
406
|
+
let type = "manual-backup";
|
|
407
|
+
if (filename.startsWith("auto-backup"))
|
|
408
|
+
type = "auto-backup";
|
|
409
|
+
else if (filename.startsWith("pre-restore"))
|
|
410
|
+
type = "pre-restore";
|
|
411
|
+
return {
|
|
412
|
+
filename,
|
|
413
|
+
size: stat.size,
|
|
414
|
+
created_at: stat.mtime.toISOString(),
|
|
415
|
+
type,
|
|
416
|
+
};
|
|
417
|
+
})
|
|
418
|
+
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
419
|
+
}
|
|
420
|
+
/** List all instance IDs that have backups (including orphans) */
|
|
421
|
+
export function listAllBackupInstanceIds() {
|
|
422
|
+
if (!existsSync(BACKUPS_DIR))
|
|
423
|
+
return [];
|
|
424
|
+
return readdirSync(BACKUPS_DIR).filter(entry => {
|
|
425
|
+
const dir = join(BACKUPS_DIR, entry);
|
|
426
|
+
try {
|
|
427
|
+
return statSync(dir).isDirectory();
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
const instanceLocks = new Map();
|
|
435
|
+
const HEARTBEAT_STALE_MS = 2 * 60 * 1000; // 2 minutes without heartbeat = stale
|
|
436
|
+
/** Acquire an exclusive lock for an instance. Returns false if already locked. */
|
|
437
|
+
export function acquireInstanceLock(instanceId, operation) {
|
|
438
|
+
const existing = instanceLocks.get(instanceId);
|
|
439
|
+
if (existing) {
|
|
440
|
+
if (Date.now() - existing.lastHeartbeat > HEARTBEAT_STALE_MS) {
|
|
441
|
+
console.warn(`[backup] Force-releasing stale lock for ${instanceId} (op: ${existing.operation}, age: ${Math.round((Date.now() - existing.since) / 1000)}s)`);
|
|
442
|
+
instanceLocks.delete(instanceId);
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
instanceLocks.set(instanceId, { operation, since: Date.now(), lastHeartbeat: Date.now() });
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
/** Update heartbeat for a held lock. Call periodically during long operations. */
|
|
452
|
+
export function touchInstanceLock(instanceId) {
|
|
453
|
+
const lock = instanceLocks.get(instanceId);
|
|
454
|
+
if (lock)
|
|
455
|
+
lock.lastHeartbeat = Date.now();
|
|
456
|
+
}
|
|
457
|
+
/** Release the lock for an instance. */
|
|
458
|
+
export function releaseInstanceLock(instanceId) {
|
|
459
|
+
instanceLocks.delete(instanceId);
|
|
460
|
+
}
|
|
461
|
+
/** Get current lock status for an instance (for status API). */
|
|
462
|
+
export function getInstanceLockStatus(instanceId) {
|
|
463
|
+
const lock = instanceLocks.get(instanceId);
|
|
464
|
+
if (!lock)
|
|
465
|
+
return null;
|
|
466
|
+
if (Date.now() - lock.lastHeartbeat > HEARTBEAT_STALE_MS) {
|
|
467
|
+
instanceLocks.delete(instanceId);
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
return { locked: true, operation: lock.operation };
|
|
471
|
+
}
|
|
472
|
+
/** Start a heartbeat interval for long operations. Returns a cleanup function. */
|
|
473
|
+
export function startLockHeartbeat(instanceId, intervalMs = 15_000) {
|
|
474
|
+
const timer = setInterval(() => touchInstanceLock(instanceId), intervalMs);
|
|
475
|
+
return () => clearInterval(timer);
|
|
476
|
+
}
|
|
477
|
+
/** Check if instance is locked. Throws with 409 status if locked. */
|
|
478
|
+
export function assertNotLocked(instanceId) {
|
|
479
|
+
const lock = getInstanceLockStatus(instanceId);
|
|
480
|
+
if (lock?.locked) {
|
|
481
|
+
const err = new Error(`Instance ${instanceId} is locked: ${lock.operation}`);
|
|
482
|
+
err.statusCode = 409;
|
|
483
|
+
throw err;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// ── Tar variant detection ──
|
|
487
|
+
let _tarVariant = null;
|
|
488
|
+
/** Detect whether system tar is GNU or BSD. Cached after first call. */
|
|
489
|
+
export function detectTarVariant() {
|
|
490
|
+
if (_tarVariant)
|
|
491
|
+
return _tarVariant;
|
|
492
|
+
try {
|
|
493
|
+
const output = execFileSync("tar", ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
494
|
+
_tarVariant = output.includes("GNU tar") ? "gnu" : "bsd";
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
// BSD tar may not support --version, or tar may not exist
|
|
498
|
+
try {
|
|
499
|
+
execFileSync("tar", ["--help"], { encoding: "utf-8", timeout: 5000 });
|
|
500
|
+
_tarVariant = "bsd";
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
throw new Error("tar command not found. Please install tar.");
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
console.log(`[backup] Detected tar variant: ${_tarVariant}`);
|
|
507
|
+
return _tarVariant;
|
|
508
|
+
}
|
|
509
|
+
// ── Secure extraction ──
|
|
510
|
+
const MAX_UNCOMPRESSED_SIZE = 2 * 1024 * 1024 * 1024; // 2GB
|
|
511
|
+
/** Parse tar -tvzf output into structured entries */
|
|
512
|
+
function parseTarVerboseOutput(output) {
|
|
513
|
+
const entries = [];
|
|
514
|
+
for (const line of output.split("\n")) {
|
|
515
|
+
if (!line.trim())
|
|
516
|
+
continue;
|
|
517
|
+
// Format: -rw-r--r-- user/group 12345 2026-04-08 10:00 path/to/file
|
|
518
|
+
// Or: lrwxrwxrwx user/group 0 2026-04-08 10:00 link -> target
|
|
519
|
+
// Or: drwxr-xr-x user/group 0 2026-04-08 10:00 dir/
|
|
520
|
+
const perms = line.charAt(0);
|
|
521
|
+
let type = "-";
|
|
522
|
+
if (perms === "l")
|
|
523
|
+
type = "l"; // symlink
|
|
524
|
+
else if (perms === "h")
|
|
525
|
+
type = "h"; // hardlink
|
|
526
|
+
else if (perms === "d")
|
|
527
|
+
type = "d"; // directory
|
|
528
|
+
// Parse size (field after user/group)
|
|
529
|
+
const parts = line.split(/\s+/);
|
|
530
|
+
// parts: [perms, user/group, size, date, time, path...]
|
|
531
|
+
const size = parseInt(parts[2], 10) || 0;
|
|
532
|
+
// Path is everything after the time field
|
|
533
|
+
// Find the path by looking for the portion after date+time
|
|
534
|
+
const pathMatch = line.match(/\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\s+(.+)/);
|
|
535
|
+
const path = pathMatch ? pathMatch[1].replace(/ -> .+$/, "").trim() : parts.slice(5).join(" ");
|
|
536
|
+
// Also detect symlinks from " -> " in the path
|
|
537
|
+
if (line.includes(" -> ") && type !== "l")
|
|
538
|
+
type = "l";
|
|
539
|
+
entries.push({ type, size, path });
|
|
540
|
+
}
|
|
541
|
+
return entries;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Pre-scan a tar.gz archive for security threats.
|
|
545
|
+
* Throws on: path traversal, symlinks, hardlinks, absolute paths, oversized.
|
|
546
|
+
*/
|
|
547
|
+
export function preScanArchive(archivePath) {
|
|
548
|
+
detectTarVariant(); // Ensure tar exists
|
|
549
|
+
const output = execFileSync("tar", ["-tvzf", archivePath], {
|
|
550
|
+
encoding: "utf-8",
|
|
551
|
+
timeout: 60_000, // 60s timeout for large archives
|
|
552
|
+
maxBuffer: 50 * 1024 * 1024, // 50MB buffer for listing
|
|
553
|
+
});
|
|
554
|
+
const entries = parseTarVerboseOutput(output);
|
|
555
|
+
let totalSize = 0;
|
|
556
|
+
for (const entry of entries) {
|
|
557
|
+
// Symlinks are allowed — npm packages routinely contain
|
|
558
|
+
// node_modules/.bin/ symlinks pointing at sibling paths within the
|
|
559
|
+
// archive. The post-extract walk (verifyNoEscapes) will reject any
|
|
560
|
+
// symlink whose real target resolves outside the extraction directory.
|
|
561
|
+
// We only hard-ban hardlinks here because they can reference files
|
|
562
|
+
// by inode in ways that bypass the extraction root without a
|
|
563
|
+
// resolvable path.
|
|
564
|
+
// Reject hardlinks
|
|
565
|
+
if (entry.type === "h") {
|
|
566
|
+
throw new Error(`Archive contains hardlink: ${entry.path}`);
|
|
567
|
+
}
|
|
568
|
+
// Reject path traversal
|
|
569
|
+
if (entry.path.includes("../../") || entry.path.includes("..\\")) {
|
|
570
|
+
throw new Error(`Archive contains path traversal: ${entry.path}`);
|
|
571
|
+
}
|
|
572
|
+
// Reject absolute paths
|
|
573
|
+
if (entry.path.startsWith("/")) {
|
|
574
|
+
throw new Error(`Archive contains absolute path: ${entry.path}`);
|
|
575
|
+
}
|
|
576
|
+
// Accumulate size
|
|
577
|
+
totalSize += entry.size;
|
|
578
|
+
if (totalSize > MAX_UNCOMPRESSED_SIZE) {
|
|
579
|
+
throw new Error(`Archive uncompressed size exceeds 2GB limit (${Math.round(totalSize / 1024 / 1024)}MB)`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return { entries, totalSize };
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Safely extract a tar.gz archive to destDir.
|
|
586
|
+
* Pre-scans for security threats, then extracts with safety flags.
|
|
587
|
+
*/
|
|
588
|
+
export async function safeExtract(archivePath, destDir) {
|
|
589
|
+
// Step 1: Pre-scan
|
|
590
|
+
const scanResult = preScanArchive(archivePath);
|
|
591
|
+
// Step 2: Ensure dest directory exists
|
|
592
|
+
if (!existsSync(destDir))
|
|
593
|
+
mkdirSync(destDir, { recursive: true });
|
|
594
|
+
// Step 3: Build extract args based on tar variant
|
|
595
|
+
const variant = detectTarVariant();
|
|
596
|
+
const args = ["-xzf", archivePath, "-C", destDir, "--no-same-owner"];
|
|
597
|
+
if (variant === "gnu") {
|
|
598
|
+
args.push("--no-same-permissions");
|
|
599
|
+
}
|
|
600
|
+
// Step 4: Extract via spawn (non-blocking)
|
|
601
|
+
await new Promise((resolve, reject) => {
|
|
602
|
+
const child = spawnChild("tar", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
603
|
+
let stderr = "";
|
|
604
|
+
child.stderr?.on("data", (chunk) => { stderr += chunk.toString(); });
|
|
605
|
+
child.on("close", (code) => {
|
|
606
|
+
if (code !== 0) {
|
|
607
|
+
reject(new Error(`tar extract failed (exit ${code}): ${stderr.slice(0, 500)}`));
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
resolve();
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
child.on("error", reject);
|
|
614
|
+
});
|
|
615
|
+
// Step 5: Post-extract verification — ensure no entry's real path
|
|
616
|
+
// escapes the extraction directory.
|
|
617
|
+
//
|
|
618
|
+
// Symlinks get special-cased: we treat them as opaque blobs that will
|
|
619
|
+
// never be followed by downstream cpSync calls (those pass
|
|
620
|
+
// verbatimSymlinks: true). That eliminates the "symlink-to-/etc/passwd
|
|
621
|
+
// then write through it" attack surface, which means a symlink's
|
|
622
|
+
// target doesn't need to resolve at all for the archive to be safe.
|
|
623
|
+
// This matters because real archives routinely contain:
|
|
624
|
+
// - npm node_modules/.bin/* pointing at siblings by relative path
|
|
625
|
+
// - docker-originated absolute paths like `/app/...` that are
|
|
626
|
+
// intentionally dangling on the host but valid inside a container
|
|
627
|
+
// Escape detection still runs for non-symlink entries via realpathSync.
|
|
628
|
+
const destDirReal = realpathSync(destDir);
|
|
629
|
+
function verifyNoEscapes(dir) {
|
|
630
|
+
for (const entry of readdirSync(dir)) {
|
|
631
|
+
const fullPath = join(dir, entry);
|
|
632
|
+
const stat = lstatSync(fullPath);
|
|
633
|
+
if (stat.isSymbolicLink()) {
|
|
634
|
+
// Opaque blob — skip real-path check and do not recurse.
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
// Regular files/dirs: realpath is defined. If anything resolves
|
|
638
|
+
// outside destDir, it's an escape attempt that slipped past the
|
|
639
|
+
// pre-scan (e.g. via hardlink creativity).
|
|
640
|
+
const real = realpathSync(fullPath);
|
|
641
|
+
if (!real.startsWith(destDirReal + "/") && real !== destDirReal) {
|
|
642
|
+
throw new Error(`Extracted entry escaped target directory: ${fullPath} -> ${real}`);
|
|
643
|
+
}
|
|
644
|
+
if (stat.isDirectory()) {
|
|
645
|
+
verifyNoEscapes(fullPath);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
verifyNoEscapes(destDir);
|
|
650
|
+
return scanResult;
|
|
651
|
+
}
|
|
652
|
+
// ── Pack / backup helpers ──
|
|
653
|
+
/** Calculate SHA-256 checksum of files in a directory, sorted by relative path */
|
|
654
|
+
function calculateContentChecksum(baseDir, files) {
|
|
655
|
+
const hash = createHash("sha256");
|
|
656
|
+
const sorted = [...files].sort();
|
|
657
|
+
for (const f of sorted) {
|
|
658
|
+
const fullPath = join(baseDir, f);
|
|
659
|
+
// Use lstat so we never follow a symlink during hashing; `files` is
|
|
660
|
+
// built by collectFiles which already excludes symlinks, but be
|
|
661
|
+
// defensive in case another caller passes a symlinked entry.
|
|
662
|
+
let stat;
|
|
663
|
+
try {
|
|
664
|
+
stat = lstatSync(fullPath);
|
|
665
|
+
}
|
|
666
|
+
catch {
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
if (stat.isFile()) {
|
|
670
|
+
hash.update(readFileSync(fullPath));
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return hash.digest("hex");
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Best-effort libc detection. Returns "glibc", "musl", "unknown", or "n/a" for non-Linux.
|
|
677
|
+
*/
|
|
678
|
+
function detectLibc() {
|
|
679
|
+
if (process.platform !== "linux")
|
|
680
|
+
return "n/a";
|
|
681
|
+
try {
|
|
682
|
+
const output = execFileSync("ldd", ["--version"], { encoding: "utf-8", timeout: 2000 }).toString();
|
|
683
|
+
if (/musl/i.test(output))
|
|
684
|
+
return "musl";
|
|
685
|
+
if (/glibc|GNU libc/i.test(output))
|
|
686
|
+
return "glibc";
|
|
687
|
+
return "unknown";
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
return "unknown";
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Copy a directory tree preserving symlinks as-is, without ever following
|
|
695
|
+
* them. Uses lstat exclusively so dangling / absolute-path symlinks (e.g.
|
|
696
|
+
* `app/openclaw.mjs -> /app/...` from a docker-cp'd container tree) do not
|
|
697
|
+
* crash the copy with ENOENT.
|
|
698
|
+
*
|
|
699
|
+
* `filter(rel)` returns false to skip the entry; for directories, returning
|
|
700
|
+
* false also skips the entire subtree. `rel` is the source-relative path
|
|
701
|
+
* using forward slashes.
|
|
702
|
+
*
|
|
703
|
+
* Used instead of `fs.cpSync` because Node's cpSync internally calls
|
|
704
|
+
* `stat` (not `lstat`) during bookkeeping, which follows symlinks and
|
|
705
|
+
* fails on broken targets even with `verbatimSymlinks: true`.
|
|
706
|
+
*/
|
|
707
|
+
function copyTreeLstat(src, dst, filter, baseRel = "") {
|
|
708
|
+
// Create the destination directory itself (no-op if it exists).
|
|
709
|
+
if (!existsSync(dst))
|
|
710
|
+
mkdirSync(dst, { recursive: true });
|
|
711
|
+
for (const entry of readdirSync(src)) {
|
|
712
|
+
const srcPath = join(src, entry);
|
|
713
|
+
const rel = baseRel ? `${baseRel}/${entry}` : entry;
|
|
714
|
+
if (!filter(rel, entry))
|
|
715
|
+
continue;
|
|
716
|
+
let stat;
|
|
717
|
+
try {
|
|
718
|
+
stat = lstatSync(srcPath);
|
|
719
|
+
}
|
|
720
|
+
catch {
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
const dstPath = join(dst, entry);
|
|
724
|
+
if (stat.isSymbolicLink()) {
|
|
725
|
+
// Preserve the link target verbatim — never read through it.
|
|
726
|
+
try {
|
|
727
|
+
const target = readlinkSync(srcPath);
|
|
728
|
+
try {
|
|
729
|
+
rmSync(dstPath, { force: true });
|
|
730
|
+
}
|
|
731
|
+
catch { /* ignore */ }
|
|
732
|
+
symlinkSync(target, dstPath);
|
|
733
|
+
}
|
|
734
|
+
catch { /* best effort — skip unreadable symlinks */ }
|
|
735
|
+
}
|
|
736
|
+
else if (stat.isDirectory()) {
|
|
737
|
+
copyTreeLstat(srcPath, dstPath, filter, rel);
|
|
738
|
+
}
|
|
739
|
+
else if (stat.isFile()) {
|
|
740
|
+
try {
|
|
741
|
+
copyFileSync(srcPath, dstPath);
|
|
742
|
+
}
|
|
743
|
+
catch { /* best effort */ }
|
|
744
|
+
}
|
|
745
|
+
// Special files (sockets, devices, fifos) are silently skipped — they
|
|
746
|
+
// don't belong in a backup archive.
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Recursively collect all regular files under a directory.
|
|
751
|
+
*
|
|
752
|
+
* Uses `lstat` so symlinks are identified as symlinks (not followed) — we
|
|
753
|
+
* intentionally skip them so dangling/absolute symlinks (like
|
|
754
|
+
* `app/openclaw.mjs -> /app/...` from a docker-cp'd tree) don't blow up
|
|
755
|
+
* checksum calculation with ENOENT. Symlinks are preserved in the tar
|
|
756
|
+
* archive by the packing step; they simply don't contribute file bytes
|
|
757
|
+
* to the content checksum.
|
|
758
|
+
*/
|
|
759
|
+
function collectFiles(dir, baseDir, excludes) {
|
|
760
|
+
const results = [];
|
|
761
|
+
if (!existsSync(dir))
|
|
762
|
+
return results;
|
|
763
|
+
for (const entry of readdirSync(dir)) {
|
|
764
|
+
const fullPath = join(dir, entry);
|
|
765
|
+
const relPath = fullPath.slice(baseDir.length + 1); // relative to baseDir
|
|
766
|
+
// Check exclusions
|
|
767
|
+
if (excludes.some(ex => relPath.startsWith(ex) || relPath.endsWith(ex)))
|
|
768
|
+
continue;
|
|
769
|
+
let stat;
|
|
770
|
+
try {
|
|
771
|
+
stat = lstatSync(fullPath);
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
if (stat.isSymbolicLink())
|
|
777
|
+
continue; // skip; don't follow
|
|
778
|
+
if (stat.isDirectory()) {
|
|
779
|
+
results.push(...collectFiles(fullPath, baseDir, excludes));
|
|
780
|
+
}
|
|
781
|
+
else if (stat.isFile()) {
|
|
782
|
+
results.push(relPath);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return results;
|
|
786
|
+
}
|
|
787
|
+
/** Create a manual or auto backup of an instance */
|
|
788
|
+
export async function backupInstance(instanceId, opts = {}) {
|
|
789
|
+
const backupDir = getInstanceBackupDir(instanceId);
|
|
790
|
+
const type = opts.type || "manual-backup";
|
|
791
|
+
// Auto-backup is always state-scope (small, frequent).
|
|
792
|
+
// Manual backup defaults to home-scope (full disaster recovery).
|
|
793
|
+
const scope = type === "auto-backup" ? "state" : (opts.scope ?? "home");
|
|
794
|
+
// Workspace is included by default; callers can pass `includeWorkspace: false`
|
|
795
|
+
// to drop it. `onlyConfig` implies no workspace (it's a config-only subset).
|
|
796
|
+
const includeWorkspace = opts.onlyConfig ? false : (opts.includeWorkspace ?? true);
|
|
797
|
+
// Only state scope can use the official CLI; home scope must self-pack.
|
|
798
|
+
if (scope === "state") {
|
|
799
|
+
const cliResult = await callOpenclawBackup(instanceId, backupDir, {
|
|
800
|
+
onlyConfig: opts.onlyConfig,
|
|
801
|
+
noWorkspace: !includeWorkspace,
|
|
802
|
+
});
|
|
803
|
+
if (cliResult.ok && cliResult.archivePath) {
|
|
804
|
+
// Rename the CLI's output to JishuShell's canonical
|
|
805
|
+
// "<type>-<timestamp>.tar.gz" pattern. Both listInstanceBackups()
|
|
806
|
+
// (filename-prefix classification) and cleanOldAutoBackups() (prefix-
|
|
807
|
+
// based rolling deletion) depend on this convention. Without it, an
|
|
808
|
+
// auto-backup created by the official CLI — whose naming is not under
|
|
809
|
+
// our control — would be misclassified as "manual-backup" AND would
|
|
810
|
+
// never be rolled out of the backups directory, causing unbounded growth.
|
|
811
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "").replace("T", "-").slice(0, 19);
|
|
812
|
+
const canonicalName = `${type}-${timestamp}.tar.gz`;
|
|
813
|
+
const canonicalPath = join(backupDir, canonicalName);
|
|
814
|
+
let finalArchivePath = cliResult.archivePath;
|
|
815
|
+
if (canonicalPath !== cliResult.archivePath) {
|
|
816
|
+
try {
|
|
817
|
+
// rename may fail if the canonical path already exists (extremely
|
|
818
|
+
// unlikely at second-precision timestamp, but guard anyway).
|
|
819
|
+
if (existsSync(canonicalPath))
|
|
820
|
+
rmSync(canonicalPath, { force: true });
|
|
821
|
+
renameSync(cliResult.archivePath, canonicalPath);
|
|
822
|
+
finalArchivePath = canonicalPath;
|
|
823
|
+
}
|
|
824
|
+
catch (e) {
|
|
825
|
+
console.warn(`[backup] Failed to rename CLI output ${cliResult.archivePath} -> ${canonicalPath}: ${e.message}. ` +
|
|
826
|
+
`File will be kept under its original name; it will be listed as "manual-backup" and will NOT be auto-rotated.`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
// Official CLI produced the archive — read its manifest for return value
|
|
830
|
+
const tmpDir = join(TMP_DIR, `manifest-read-${Date.now()}`);
|
|
831
|
+
try {
|
|
832
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
833
|
+
// Full extract (state-scope archives are small) so resolveArchiveRoot
|
|
834
|
+
// can locate manifest.json regardless of whether the CLI wrapped it
|
|
835
|
+
// inside a <basename>/ top-level directory.
|
|
836
|
+
try {
|
|
837
|
+
execFileSync("tar", ["-xzf", finalArchivePath, "-C", tmpDir], { timeout: 30_000 });
|
|
838
|
+
}
|
|
839
|
+
catch { /* extraction may fail, continue with defaults */ }
|
|
840
|
+
const archiveRoot = resolveArchiveRoot(tmpDir);
|
|
841
|
+
const manifestPath = join(archiveRoot, "manifest.json");
|
|
842
|
+
const manifest = existsSync(manifestPath)
|
|
843
|
+
? JSON.parse(readFileSync(manifestPath, "utf-8"))
|
|
844
|
+
: { schemaVersion: 1, type };
|
|
845
|
+
// Ensure scope + type fields match what JishuShell asked for (official
|
|
846
|
+
// CLI doesn't know about our scope enum and may not write type).
|
|
847
|
+
if (!manifest.scope)
|
|
848
|
+
manifest.scope = "state";
|
|
849
|
+
if (!manifest.type)
|
|
850
|
+
manifest.type = type;
|
|
851
|
+
const size = statSync(finalArchivePath).size;
|
|
852
|
+
return {
|
|
853
|
+
filename: finalArchivePath.split("/").pop(),
|
|
854
|
+
filepath: finalArchivePath,
|
|
855
|
+
size,
|
|
856
|
+
manifest,
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
finally {
|
|
860
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
console.log(`[backup] Official CLI unavailable (${cliResult.error}), using self-pack fallback`);
|
|
864
|
+
}
|
|
865
|
+
// Self-pack path: used for home scope always, or as fallback for state scope
|
|
866
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "").replace("T", "-").slice(0, 19);
|
|
867
|
+
const filename = `${type}-${timestamp}.tar.gz`;
|
|
868
|
+
const outputPath = opts.outputPath || join(backupDir, filename);
|
|
869
|
+
return selfPackOfficialFormat(instanceId, outputPath, {
|
|
870
|
+
type,
|
|
871
|
+
scope,
|
|
872
|
+
includeSessions: opts.includeSessions,
|
|
873
|
+
includeWorkspace,
|
|
874
|
+
onlyConfig: opts.onlyConfig,
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Pack an instance in the official OpenClaw backup archive format.
|
|
879
|
+
* Layout: manifest.json at root + payload/<encoded-path>/.openclaw/...
|
|
880
|
+
* This is the fallback when `openclaw backup create` CLI is unavailable.
|
|
881
|
+
*/
|
|
882
|
+
export async function selfPackOfficialFormat(instanceId, outputPath, opts) {
|
|
883
|
+
const { getInstance } = await import("../app-common/service.js");
|
|
884
|
+
const meta = getInstance(instanceId);
|
|
885
|
+
if (!meta)
|
|
886
|
+
throw new Error(`Instance ${instanceId} not found`);
|
|
887
|
+
const openclawHome = integrationHomeFromMetadata(meta, instanceId);
|
|
888
|
+
const stateDir = join(openclawHome, ".openclaw");
|
|
889
|
+
if (!existsSync(stateDir))
|
|
890
|
+
throw new Error(`State directory not found: ${stateDir}`);
|
|
891
|
+
const scope = opts.scope ?? "state";
|
|
892
|
+
const encodedPath = encodeAbsolutePathForArchive(openclawHome);
|
|
893
|
+
const stagingDir = join(TMP_DIR, `selfpack-${instanceId}-${Date.now()}`);
|
|
894
|
+
const stagingHomeDir = join(stagingDir, "payload", encodedPath);
|
|
895
|
+
const payloadStateDir = join(stagingHomeDir, ".openclaw");
|
|
896
|
+
try {
|
|
897
|
+
if (scope === "home") {
|
|
898
|
+
// Home scope: pack entire openclaw-home/ tree minus cache dirs
|
|
899
|
+
mkdirSync(stagingHomeDir, { recursive: true });
|
|
900
|
+
const homeTopLevelExcludes = [
|
|
901
|
+
".jishushell-owned",
|
|
902
|
+
"workspace",
|
|
903
|
+
"openclaw.json",
|
|
904
|
+
".npm/_cacache",
|
|
905
|
+
".npm/_logs",
|
|
906
|
+
".npm/_npx",
|
|
907
|
+
".npm/_update-notifier-last-checked",
|
|
908
|
+
".cache",
|
|
909
|
+
".node_compile_cache",
|
|
910
|
+
];
|
|
911
|
+
const stateSubExcludes = [
|
|
912
|
+
".npm",
|
|
913
|
+
".cache",
|
|
914
|
+
".node_compile_cache",
|
|
915
|
+
"workspace/.npm-global",
|
|
916
|
+
];
|
|
917
|
+
const sessionExcludes = opts.includeSessions ? [] : ["agents/main/sessions"];
|
|
918
|
+
const workspaceExcludes = opts.includeWorkspace === false ? ["workspace"] : [];
|
|
919
|
+
if (opts.onlyConfig) {
|
|
920
|
+
// Only include openclaw.json inside .openclaw/
|
|
921
|
+
mkdirSync(payloadStateDir, { recursive: true });
|
|
922
|
+
const configSrc = join(stateDir, "openclaw.json");
|
|
923
|
+
if (existsSync(configSrc)) {
|
|
924
|
+
copyFileSync(configSrc, join(payloadStateDir, "openclaw.json"));
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
else {
|
|
928
|
+
// Use copyTreeLstat so we never follow symlinks (even dangling
|
|
929
|
+
// ones like `app/openclaw.mjs -> /app/...` from a docker-cp'd
|
|
930
|
+
// container tree). fs.cpSync cannot be used here: even with
|
|
931
|
+
// verbatimSymlinks: true, its internal bookkeeping calls `stat`
|
|
932
|
+
// on dest entries which follows broken links and crashes with
|
|
933
|
+
// ENOENT on the first dangling symlink.
|
|
934
|
+
copyTreeLstat(openclawHome, stagingHomeDir, (rel) => {
|
|
935
|
+
if (!rel)
|
|
936
|
+
return true;
|
|
937
|
+
if (homeTopLevelExcludes.some(ex => rel === ex || rel.startsWith(ex + "/")))
|
|
938
|
+
return false;
|
|
939
|
+
if (rel.startsWith(".npm-global/") && rel.includes("/.cache/"))
|
|
940
|
+
return false;
|
|
941
|
+
if (rel === ".openclaw" || rel.startsWith(".openclaw/")) {
|
|
942
|
+
const stateRel = rel === ".openclaw" ? "" : rel.slice(".openclaw/".length);
|
|
943
|
+
if (!stateRel)
|
|
944
|
+
return true;
|
|
945
|
+
if (stateSubExcludes.some(ex => stateRel === ex || stateRel.startsWith(ex + "/")))
|
|
946
|
+
return false;
|
|
947
|
+
if (sessionExcludes.some(ex => stateRel === ex || stateRel.startsWith(ex + "/")))
|
|
948
|
+
return false;
|
|
949
|
+
if (workspaceExcludes.some(ex => stateRel === ex || stateRel.startsWith(ex + "/")))
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
return true;
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
else {
|
|
957
|
+
// State scope (default): only .openclaw/ contents
|
|
958
|
+
mkdirSync(payloadStateDir, { recursive: true });
|
|
959
|
+
// Build exclusion list
|
|
960
|
+
const excludes = [
|
|
961
|
+
".npm/",
|
|
962
|
+
".cache/",
|
|
963
|
+
".node_compile_cache/",
|
|
964
|
+
"workspace/.npm-global/",
|
|
965
|
+
];
|
|
966
|
+
if (!opts.includeSessions)
|
|
967
|
+
excludes.push("agents/main/sessions/");
|
|
968
|
+
if (opts.includeWorkspace === false)
|
|
969
|
+
excludes.push("workspace/");
|
|
970
|
+
if (opts.onlyConfig) {
|
|
971
|
+
// Only copy openclaw.json
|
|
972
|
+
const configSrc = join(stateDir, "openclaw.json");
|
|
973
|
+
if (existsSync(configSrc)) {
|
|
974
|
+
copyFileSync(configSrc, join(payloadStateDir, "openclaw.json"));
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
else {
|
|
978
|
+
// Copy .openclaw/ contents (respecting excludes). Manual lstat
|
|
979
|
+
// walker — same rationale as the home-scope branch above.
|
|
980
|
+
copyTreeLstat(stateDir, payloadStateDir, (rel) => {
|
|
981
|
+
if (!rel)
|
|
982
|
+
return true; // root dir
|
|
983
|
+
return !excludes.some(ex => rel.startsWith(ex));
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
// Collect all files for checksum (excluding manifest which doesn't exist yet)
|
|
988
|
+
const allFiles = collectFiles(stagingDir, stagingDir, []);
|
|
989
|
+
// Calculate checksum from staged files
|
|
990
|
+
const checksum = calculateContentChecksum(stagingDir, allFiles);
|
|
991
|
+
const hasSessions = opts.includeSessions ?? false;
|
|
992
|
+
// Generate manifest (minimum required fields matching official format)
|
|
993
|
+
const manifest = {
|
|
994
|
+
schemaVersion: 1,
|
|
995
|
+
type: opts.type,
|
|
996
|
+
scope,
|
|
997
|
+
createdAt: new Date().toISOString(),
|
|
998
|
+
name: meta.name || instanceId,
|
|
999
|
+
description: meta.description || "",
|
|
1000
|
+
platform: process.platform,
|
|
1001
|
+
arch: process.arch,
|
|
1002
|
+
libc: detectLibc(),
|
|
1003
|
+
nodeVersion: process.version,
|
|
1004
|
+
paths: {
|
|
1005
|
+
stateDir: `payload/${encodedPath}/.openclaw`,
|
|
1006
|
+
},
|
|
1007
|
+
source_home: openclawHome,
|
|
1008
|
+
has_sessions: hasSessions,
|
|
1009
|
+
checksum: `sha256:${checksum}`,
|
|
1010
|
+
checksum_scope: "content-excluding-manifest",
|
|
1011
|
+
};
|
|
1012
|
+
// Write manifest to staging root
|
|
1013
|
+
writeFileSync(join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
1014
|
+
// Single-pass tar via spawn with heartbeat
|
|
1015
|
+
const stopHeartbeat = startLockHeartbeat(instanceId);
|
|
1016
|
+
try {
|
|
1017
|
+
await new Promise((resolve, reject) => {
|
|
1018
|
+
const child = spawnChild("tar", ["-czf", outputPath, "."], {
|
|
1019
|
+
cwd: stagingDir,
|
|
1020
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1021
|
+
});
|
|
1022
|
+
let stderr = "";
|
|
1023
|
+
child.stderr?.on("data", (chunk) => { stderr += chunk.toString(); });
|
|
1024
|
+
child.on("close", (code) => {
|
|
1025
|
+
if (code !== 0)
|
|
1026
|
+
reject(new Error(`tar pack failed (exit ${code}): ${stderr.slice(0, 500)}`));
|
|
1027
|
+
else
|
|
1028
|
+
resolve();
|
|
1029
|
+
});
|
|
1030
|
+
child.on("error", reject);
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
finally {
|
|
1034
|
+
stopHeartbeat();
|
|
1035
|
+
}
|
|
1036
|
+
const finalSize = statSync(outputPath).size;
|
|
1037
|
+
return { filename: outputPath.split("/").pop(), filepath: outputPath, size: finalSize, manifest };
|
|
1038
|
+
}
|
|
1039
|
+
finally {
|
|
1040
|
+
// Cleanup staging
|
|
1041
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
/** Step 13: only nomad service manager is supported on the runtime path. */
|
|
1045
|
+
async function getSvc() {
|
|
1046
|
+
const { getServiceManagerType } = await import("../../config.js");
|
|
1047
|
+
const type = getServiceManagerType();
|
|
1048
|
+
if (type !== "nomad") {
|
|
1049
|
+
throw new Error(`service_manager='${type}' is no longer supported. ` +
|
|
1050
|
+
"Only 'nomad' runtimes are accepted on the runtime path. " +
|
|
1051
|
+
"Run `jishushell migrate legacy` to convert legacy instances.");
|
|
1052
|
+
}
|
|
1053
|
+
return import("../runtime/drivers/nomad.js");
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Restore an instance from a backup file.
|
|
1057
|
+
* Full flow: lock -> extract -> validate -> pre-restore backup -> stop -> restore -> rebuild -> start -> healthcheck -> rollback on failure
|
|
1058
|
+
*/
|
|
1059
|
+
export async function restoreInstance(instanceId, backupFilePath) {
|
|
1060
|
+
const { getInstance } = await import("../app-common/service.js");
|
|
1061
|
+
const meta = getInstance(instanceId);
|
|
1062
|
+
if (!meta)
|
|
1063
|
+
throw new Error(`Instance ${instanceId} not found`);
|
|
1064
|
+
const shouldRestart = meta.desiredState === "running";
|
|
1065
|
+
const openclawHome = integrationHomeFromMetadata(meta, instanceId);
|
|
1066
|
+
const warnings = [];
|
|
1067
|
+
// Default to "preserved"; promoted to "lost" below if we detect the archive
|
|
1068
|
+
// was built on a different machine (platform/arch mismatch is a proxy signal).
|
|
1069
|
+
let apiKeyStatus = "preserved";
|
|
1070
|
+
// Step 1: Acquire lock
|
|
1071
|
+
if (!acquireInstanceLock(instanceId, "restoring")) {
|
|
1072
|
+
const lock = getInstanceLockStatus(instanceId);
|
|
1073
|
+
throw Object.assign(new Error(`Instance ${instanceId} is locked: ${lock?.operation}`), { statusCode: 409 });
|
|
1074
|
+
}
|
|
1075
|
+
const stopHeartbeat = startLockHeartbeat(instanceId);
|
|
1076
|
+
const tmpDir = join(TMP_DIR, `restore-${instanceId}-${Date.now()}`);
|
|
1077
|
+
let currentPreRestorePath = null;
|
|
1078
|
+
try {
|
|
1079
|
+
// Step 2: Safe extract to temp dir
|
|
1080
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
1081
|
+
await safeExtract(backupFilePath, tmpDir);
|
|
1082
|
+
touchInstanceLock(instanceId);
|
|
1083
|
+
// Step 3: Resolve the archive root (handles both self-pack layout and
|
|
1084
|
+
// official CLI's top-level wrapper directory) and validate manifest
|
|
1085
|
+
const archiveRoot = resolveArchiveRoot(tmpDir);
|
|
1086
|
+
const manifestPath = join(archiveRoot, "manifest.json");
|
|
1087
|
+
if (!existsSync(manifestPath))
|
|
1088
|
+
throw new Error("Archive missing manifest.json");
|
|
1089
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
1090
|
+
if (manifest.schemaVersion && manifest.schemaVersion > 1) {
|
|
1091
|
+
warnings.push(`Archive schemaVersion ${manifest.schemaVersion} is newer than supported (1)`);
|
|
1092
|
+
}
|
|
1093
|
+
// Step 4: Locate .openclaw/ via payload/ directory
|
|
1094
|
+
const payloadDir = join(archiveRoot, "payload");
|
|
1095
|
+
if (!existsSync(payloadDir))
|
|
1096
|
+
throw new Error("Archive missing payload/ directory");
|
|
1097
|
+
const extractedStateDir = findStateDir(payloadDir);
|
|
1098
|
+
if (!extractedStateDir)
|
|
1099
|
+
throw new Error("Could not locate .openclaw directory in archive");
|
|
1100
|
+
// extractedHomeDir is the parent of .openclaw — source for home-scope restore
|
|
1101
|
+
const extractedHomeDir = dirname(extractedStateDir);
|
|
1102
|
+
// Verify openclaw.json exists in located dir
|
|
1103
|
+
const extractedConfigPath = join(extractedStateDir, "openclaw.json");
|
|
1104
|
+
if (!existsSync(extractedConfigPath))
|
|
1105
|
+
throw new Error("Archive missing openclaw.json");
|
|
1106
|
+
JSON.parse(readFileSync(extractedConfigPath, "utf-8")); // verify parseable
|
|
1107
|
+
// Determine effective scope and check arch compatibility
|
|
1108
|
+
const archiveScope = manifest.scope === "home" ? "home" : "state";
|
|
1109
|
+
let effectiveScope = archiveScope;
|
|
1110
|
+
const platformMismatch = manifest.platform && manifest.platform !== process.platform;
|
|
1111
|
+
const archMismatch = manifest.arch && manifest.arch !== process.arch;
|
|
1112
|
+
if (platformMismatch || archMismatch) {
|
|
1113
|
+
// Archive was built elsewhere — openclaw.json may reference API key env
|
|
1114
|
+
// vars that this machine's provider.env doesn't have set.
|
|
1115
|
+
apiKeyStatus = "lost";
|
|
1116
|
+
}
|
|
1117
|
+
if (archiveScope === "home" && (platformMismatch || archMismatch)) {
|
|
1118
|
+
warnings.push(`Architecture mismatch: archive built for ${manifest.platform}/${manifest.arch}, ` +
|
|
1119
|
+
`current host is ${process.platform}/${process.arch}. ` +
|
|
1120
|
+
`Downgrading to state-only restore. You may need to re-run "openclaw update" after restore.`);
|
|
1121
|
+
effectiveScope = "state";
|
|
1122
|
+
}
|
|
1123
|
+
touchInstanceLock(instanceId);
|
|
1124
|
+
// Step 5: Create pre-restore backup at the SAME scope we're about to
|
|
1125
|
+
// overwrite, so rollback can actually put everything back. This snapshot
|
|
1126
|
+
// is transaction-local: it lives under this restore's tmp directory and
|
|
1127
|
+
// is removed in finally whether restore succeeds, fails, or rolls back.
|
|
1128
|
+
// If snapshot creation fails, restoration must stop before touching the
|
|
1129
|
+
// current instance.
|
|
1130
|
+
try {
|
|
1131
|
+
const preRestorePath = join(tmpDir, "pre-restore-current.tar.gz");
|
|
1132
|
+
await selfPackOfficialFormat(instanceId, preRestorePath, {
|
|
1133
|
+
type: "pre-restore",
|
|
1134
|
+
scope: effectiveScope,
|
|
1135
|
+
includeSessions: true,
|
|
1136
|
+
includeWorkspace: true,
|
|
1137
|
+
});
|
|
1138
|
+
currentPreRestorePath = preRestorePath;
|
|
1139
|
+
}
|
|
1140
|
+
catch (e) {
|
|
1141
|
+
throw Object.assign(new Error(`Restore cancelled because the safety snapshot could not be created: ${e?.message ?? e}`), { statusCode: 500, code: "restore-snapshot-failed" });
|
|
1142
|
+
}
|
|
1143
|
+
touchInstanceLock(instanceId);
|
|
1144
|
+
// Step 6: Stop instance
|
|
1145
|
+
try {
|
|
1146
|
+
const { stopInstance } = await import("../app-common/lifecycle-service.js");
|
|
1147
|
+
await stopInstance(instanceId);
|
|
1148
|
+
}
|
|
1149
|
+
catch {
|
|
1150
|
+
// Instance may not be running -- ignore stop failures
|
|
1151
|
+
}
|
|
1152
|
+
// Clean proxy state
|
|
1153
|
+
try {
|
|
1154
|
+
const { cleanupInstance } = await import("../llm-proxy/instance-proxy.js");
|
|
1155
|
+
cleanupInstance(instanceId);
|
|
1156
|
+
}
|
|
1157
|
+
catch { /* ignore */ }
|
|
1158
|
+
touchInstanceLock(instanceId);
|
|
1159
|
+
// Step 7: Clear current target directory based on effective scope.
|
|
1160
|
+
// Canonical integration paths preserve custom-home installs without a
|
|
1161
|
+
// second OpenClaw-specific path field.
|
|
1162
|
+
const stateDir = join(openclawHome, ".openclaw");
|
|
1163
|
+
const cacheKeepDirs = new Set([".npm", ".cache", ".node_compile_cache"]);
|
|
1164
|
+
const preservedHomeEntries = new Set([
|
|
1165
|
+
".npm",
|
|
1166
|
+
".cache",
|
|
1167
|
+
".node_compile_cache",
|
|
1168
|
+
".jishushell-owned",
|
|
1169
|
+
"workspace",
|
|
1170
|
+
]);
|
|
1171
|
+
if (effectiveScope === "home") {
|
|
1172
|
+
// Home scope: clear openclaw-home except cache dirs
|
|
1173
|
+
if (existsSync(openclawHome)) {
|
|
1174
|
+
for (const entry of readdirSync(openclawHome)) {
|
|
1175
|
+
if (preservedHomeEntries.has(entry))
|
|
1176
|
+
continue;
|
|
1177
|
+
rmSync(join(openclawHome, entry), { recursive: true, force: true });
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
else {
|
|
1181
|
+
mkdirSync(openclawHome, { recursive: true });
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
else {
|
|
1185
|
+
// State scope: clear only .openclaw/, keep cache subdirs inside
|
|
1186
|
+
const legacyConfig = join(openclawHome, "openclaw.json");
|
|
1187
|
+
if (existsSync(legacyConfig))
|
|
1188
|
+
rmSync(legacyConfig, { force: true });
|
|
1189
|
+
if (existsSync(stateDir)) {
|
|
1190
|
+
for (const entry of readdirSync(stateDir)) {
|
|
1191
|
+
if (cacheKeepDirs.has(entry))
|
|
1192
|
+
continue;
|
|
1193
|
+
rmSync(join(stateDir, entry), { recursive: true, force: true });
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
else {
|
|
1197
|
+
mkdirSync(stateDir, { recursive: true });
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
touchInstanceLock(instanceId);
|
|
1201
|
+
// Step 8: Copy restored content based on effective scope. Uses the
|
|
1202
|
+
// manual lstat walker (not fs.cpSync) so dangling/absolute symlinks
|
|
1203
|
+
// survive the round-trip without ENOENT.
|
|
1204
|
+
if (effectiveScope === "home") {
|
|
1205
|
+
// Home scope: copy entire extracted home dir into openclawHome
|
|
1206
|
+
copyTreeLstat(extractedHomeDir, openclawHome, (rel) => !isNonPortableHomeEntry(rel));
|
|
1207
|
+
}
|
|
1208
|
+
else {
|
|
1209
|
+
// State scope: copy only .openclaw/
|
|
1210
|
+
if (existsSync(extractedStateDir)) {
|
|
1211
|
+
copyTreeLstat(extractedStateDir, stateDir, () => true);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
touchInstanceLock(instanceId);
|
|
1215
|
+
// Rewrite absolute paths in session data and symlinks when the backup
|
|
1216
|
+
// originates from a different machine or instance (cross-machine restore).
|
|
1217
|
+
const oldHome = detectOldHome(manifest, payloadDir, extractedStateDir);
|
|
1218
|
+
if (oldHome) {
|
|
1219
|
+
rewriteInstancePaths(effectiveScope === "home" ? openclawHome : stateDir, oldHome, openclawHome, warnings);
|
|
1220
|
+
}
|
|
1221
|
+
// Step 10: Rebuild runtime state
|
|
1222
|
+
try {
|
|
1223
|
+
const { bootstrapInstanceProxy } = await import("../llm-proxy/instance-proxy.js");
|
|
1224
|
+
await bootstrapInstanceProxy(instanceId);
|
|
1225
|
+
}
|
|
1226
|
+
catch (e) {
|
|
1227
|
+
warnings.push(`Proxy bootstrap warning: ${e.message}`);
|
|
1228
|
+
}
|
|
1229
|
+
touchInstanceLock(instanceId);
|
|
1230
|
+
// Step 11: Restore the user's long-term desired state. A restore must not
|
|
1231
|
+
// turn a deliberately stopped instance into a running one.
|
|
1232
|
+
let started = false;
|
|
1233
|
+
if (shouldRestart) {
|
|
1234
|
+
try {
|
|
1235
|
+
const { startInstance } = await import("../app-common/lifecycle-service.js");
|
|
1236
|
+
const result = await startInstance(instanceId);
|
|
1237
|
+
started = result.ok !== false;
|
|
1238
|
+
}
|
|
1239
|
+
catch { /* ignore start failures */ }
|
|
1240
|
+
}
|
|
1241
|
+
touchInstanceLock(instanceId);
|
|
1242
|
+
// Step 12: Use normal observed readiness. Allocation running alone is not
|
|
1243
|
+
// enough; health and Core proxy reachability must also be ready.
|
|
1244
|
+
if (shouldRestart) {
|
|
1245
|
+
let ready = false;
|
|
1246
|
+
if (started) {
|
|
1247
|
+
try {
|
|
1248
|
+
const { waitForObservedStatus } = await import("../app-common/status-refresh.js");
|
|
1249
|
+
const snapshot = await waitForObservedStatus(instanceId, "ready", {
|
|
1250
|
+
reason: "manual",
|
|
1251
|
+
timeoutMs: 30_000,
|
|
1252
|
+
});
|
|
1253
|
+
ready = snapshot?.instance.status.phase === "ready";
|
|
1254
|
+
}
|
|
1255
|
+
catch { /* handled by rollback below */ }
|
|
1256
|
+
}
|
|
1257
|
+
if (!ready) {
|
|
1258
|
+
warnings.push("Health check failed after restore. Attempting auto-rollback...");
|
|
1259
|
+
if (!currentPreRestorePath) {
|
|
1260
|
+
warnings.push("No snapshot from this restore operation is available; automatic rollback was skipped.");
|
|
1261
|
+
return { ok: false, warnings, api_key_status: apiKeyStatus, rolled_back: false };
|
|
1262
|
+
}
|
|
1263
|
+
try {
|
|
1264
|
+
await _rollbackFromPreRestore(instanceId, meta, currentPreRestorePath);
|
|
1265
|
+
warnings.push("Auto-rollback completed. Instance restored to pre-restore state.");
|
|
1266
|
+
return { ok: false, warnings, api_key_status: apiKeyStatus, rolled_back: true };
|
|
1267
|
+
}
|
|
1268
|
+
catch (e) {
|
|
1269
|
+
warnings.push(`Auto-rollback also failed: ${e.message}. Manual recovery needed.`);
|
|
1270
|
+
return { ok: false, warnings, api_key_status: apiKeyStatus, rolled_back: false };
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
return { ok: true, warnings, api_key_status: apiKeyStatus };
|
|
1275
|
+
}
|
|
1276
|
+
finally {
|
|
1277
|
+
stopHeartbeat();
|
|
1278
|
+
if (existsSync(tmpDir))
|
|
1279
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
1280
|
+
releaseInstanceLock(instanceId);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Create a new instance from a backup file.
|
|
1285
|
+
* Does NOT copy model.env (new proxy token), provider.env (can't decrypt), or IM credentials.
|
|
1286
|
+
* Hard rule: provider.env is always ignored — no decrypt attempt, no migration.
|
|
1287
|
+
*/
|
|
1288
|
+
export async function createFromBackup(backupFilePath, opts) {
|
|
1289
|
+
const warnings = [];
|
|
1290
|
+
const tmpDir = join(TMP_DIR, `create-from-backup-${opts.newId}-${Date.now()}`);
|
|
1291
|
+
let targetInstalled = false;
|
|
1292
|
+
try {
|
|
1293
|
+
// Extract and validate
|
|
1294
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
1295
|
+
await safeExtract(backupFilePath, tmpDir);
|
|
1296
|
+
// Resolve root — handles both self-pack and official CLI wrapper layouts
|
|
1297
|
+
const archiveRoot = resolveArchiveRoot(tmpDir);
|
|
1298
|
+
// Detect archive scope — for home-scope archives we restore the full
|
|
1299
|
+
// openclaw-home tree, not just .openclaw (see importInstance for the
|
|
1300
|
+
// rationale).
|
|
1301
|
+
let archiveScope = "state";
|
|
1302
|
+
let manifest = null;
|
|
1303
|
+
const manifestPath = join(archiveRoot, "manifest.json");
|
|
1304
|
+
if (existsSync(manifestPath)) {
|
|
1305
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
1306
|
+
if (manifest.has_api_keys === false || !manifest.has_api_keys) {
|
|
1307
|
+
warnings.push("No API Key in this backup. You will need to configure one.");
|
|
1308
|
+
}
|
|
1309
|
+
if (manifest?.scope === "home")
|
|
1310
|
+
archiveScope = "home";
|
|
1311
|
+
}
|
|
1312
|
+
// Locate .openclaw/ via payload/ directory (official format)
|
|
1313
|
+
const payloadDir = join(archiveRoot, "payload");
|
|
1314
|
+
if (!existsSync(payloadDir))
|
|
1315
|
+
throw new Error("Archive missing payload/ directory");
|
|
1316
|
+
const extractedStateDir = findStateDir(payloadDir);
|
|
1317
|
+
if (!extractedStateDir)
|
|
1318
|
+
throw new Error("Could not locate .openclaw directory in archive");
|
|
1319
|
+
const { openclawHome: newOpenclawHome, stateDir: newStateDir } = await installOpenclawRestoreTarget(opts.newId, opts.newName, opts.newDescription || "");
|
|
1320
|
+
targetInstalled = true;
|
|
1321
|
+
// Copy content to the new instance's openclaw-home. Manual lstat
|
|
1322
|
+
// walker preserves symlinks as opaque blobs (see pack-side notes).
|
|
1323
|
+
if (archiveScope === "home") {
|
|
1324
|
+
// Home scope: copy the entire extracted home tree.
|
|
1325
|
+
const extractedHomeDir = dirname(extractedStateDir);
|
|
1326
|
+
copyTreeLstat(extractedHomeDir, newOpenclawHome, (rel) => {
|
|
1327
|
+
if (!rel)
|
|
1328
|
+
return true;
|
|
1329
|
+
if (isNonPortableHomeEntry(rel))
|
|
1330
|
+
return false;
|
|
1331
|
+
if (rel === ".openclaw/openclaw-weixin")
|
|
1332
|
+
return false;
|
|
1333
|
+
if (rel.startsWith(".openclaw/openclaw-weixin/"))
|
|
1334
|
+
return false;
|
|
1335
|
+
return true;
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
else {
|
|
1339
|
+
// State scope: copy only .openclaw/.
|
|
1340
|
+
copyTreeLstat(extractedStateDir, newStateDir, (rel) => {
|
|
1341
|
+
if (!rel)
|
|
1342
|
+
return true;
|
|
1343
|
+
if (rel.startsWith("openclaw-weixin"))
|
|
1344
|
+
return false;
|
|
1345
|
+
return true;
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
// Rewrite absolute paths in session data and symlinks from the source
|
|
1349
|
+
// instance to the new instance (fixes ENOENT crash on Docker import
|
|
1350
|
+
// and silent cross-instance data leakage on local import).
|
|
1351
|
+
const oldHome = detectOldHome(manifest, payloadDir, extractedStateDir);
|
|
1352
|
+
if (oldHome) {
|
|
1353
|
+
rewriteInstancePaths(archiveScope === "home" ? newOpenclawHome : newStateDir, oldHome, newOpenclawHome, warnings);
|
|
1354
|
+
}
|
|
1355
|
+
// Scrub channel credentials from copied openclaw.json (new instance should not inherit IM bindings)
|
|
1356
|
+
await scrubNewInstanceConfig(join(newStateDir, "openclaw.json"));
|
|
1357
|
+
// Regenerate gateway auth token (scrub cleared it to prevent credential leakage,
|
|
1358
|
+
// but the gateway requires a valid token to accept requests)
|
|
1359
|
+
regenerateGatewayAuthToken(join(newStateDir, "openclaw.json"));
|
|
1360
|
+
// Bootstrap proxy for new instance (generates new token)
|
|
1361
|
+
try {
|
|
1362
|
+
const { bootstrapInstanceProxy } = await import("../llm-proxy/instance-proxy.js");
|
|
1363
|
+
await bootstrapInstanceProxy(opts.newId);
|
|
1364
|
+
}
|
|
1365
|
+
catch (e) {
|
|
1366
|
+
warnings.push(`Proxy bootstrap warning: ${e.message}`);
|
|
1367
|
+
}
|
|
1368
|
+
warnings.push("Instance created. Configure API Key before starting.");
|
|
1369
|
+
return { ok: true, instance_id: opts.newId, warnings };
|
|
1370
|
+
}
|
|
1371
|
+
catch (error) {
|
|
1372
|
+
if (targetInstalled) {
|
|
1373
|
+
try {
|
|
1374
|
+
await rollbackOpenclawRestoreTarget(opts.newId);
|
|
1375
|
+
}
|
|
1376
|
+
catch (rollbackError) {
|
|
1377
|
+
console.error(`[backup] failed to rollback partial restore target ${opts.newId}: ${rollbackError?.message ?? rollbackError}`);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
throw error;
|
|
1381
|
+
}
|
|
1382
|
+
finally {
|
|
1383
|
+
if (existsSync(tmpDir))
|
|
1384
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Internal rollback helper -- restores only from the snapshot created by the
|
|
1389
|
+
* current restore operation.
|
|
1390
|
+
* Does NOT re-acquire lock (caller already holds it).
|
|
1391
|
+
* Does NOT create another pre-restore backup.
|
|
1392
|
+
*/
|
|
1393
|
+
async function _rollbackFromPreRestore(instanceId, meta, preRestorePath) {
|
|
1394
|
+
if (!existsSync(preRestorePath)) {
|
|
1395
|
+
throw new Error("Current restore snapshot is missing");
|
|
1396
|
+
}
|
|
1397
|
+
const tmpDir = join(TMP_DIR, `rollback-${instanceId}-${Date.now()}`);
|
|
1398
|
+
try {
|
|
1399
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
1400
|
+
await safeExtract(preRestorePath, tmpDir);
|
|
1401
|
+
const openclawHome = integrationHomeFromMetadata(meta, instanceId);
|
|
1402
|
+
const stateDir = join(openclawHome, ".openclaw");
|
|
1403
|
+
// Resolve archive root (pre-restore is always self-pack so this is
|
|
1404
|
+
// usually a no-op, but handle either layout for safety).
|
|
1405
|
+
const archiveRoot = resolveArchiveRoot(tmpDir);
|
|
1406
|
+
// Locate .openclaw/ and its parent (source for home-scope rollback)
|
|
1407
|
+
const payloadDir = join(archiveRoot, "payload");
|
|
1408
|
+
const extractedStateDir = existsSync(payloadDir) ? findStateDir(payloadDir) : null;
|
|
1409
|
+
if (!extractedStateDir)
|
|
1410
|
+
throw new Error("pre-restore archive missing .openclaw directory");
|
|
1411
|
+
const extractedHomeDir = dirname(extractedStateDir);
|
|
1412
|
+
// Read the pre-restore's own manifest to decide rollback scope.
|
|
1413
|
+
// If the pre-restore was packed as home scope, we must rollback the full
|
|
1414
|
+
// portable home tree (.npm-global/, .codex/, etc.) — not just
|
|
1415
|
+
// .openclaw/ — otherwise we leave the instance in a half-deleted state.
|
|
1416
|
+
let rollbackScope = "state";
|
|
1417
|
+
const manifestPath = join(archiveRoot, "manifest.json");
|
|
1418
|
+
if (existsSync(manifestPath)) {
|
|
1419
|
+
try {
|
|
1420
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
1421
|
+
if (manifest?.scope === "home")
|
|
1422
|
+
rollbackScope = "home";
|
|
1423
|
+
}
|
|
1424
|
+
catch { /* fall back to state */ }
|
|
1425
|
+
}
|
|
1426
|
+
const cacheKeepDirs = new Set([".npm", ".cache", ".node_compile_cache"]);
|
|
1427
|
+
const preservedHomeEntries = new Set([
|
|
1428
|
+
".npm",
|
|
1429
|
+
".cache",
|
|
1430
|
+
".node_compile_cache",
|
|
1431
|
+
".jishushell-owned",
|
|
1432
|
+
"workspace",
|
|
1433
|
+
]);
|
|
1434
|
+
if (rollbackScope === "home") {
|
|
1435
|
+
// Clear openclaw-home/ except cache dirs, then restore the entire tree.
|
|
1436
|
+
if (existsSync(openclawHome)) {
|
|
1437
|
+
for (const entry of readdirSync(openclawHome)) {
|
|
1438
|
+
if (preservedHomeEntries.has(entry))
|
|
1439
|
+
continue;
|
|
1440
|
+
rmSync(join(openclawHome, entry), { recursive: true, force: true });
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
else {
|
|
1444
|
+
mkdirSync(openclawHome, { recursive: true });
|
|
1445
|
+
}
|
|
1446
|
+
copyTreeLstat(extractedHomeDir, openclawHome, (rel) => !isNonPortableHomeEntry(rel));
|
|
1447
|
+
}
|
|
1448
|
+
else {
|
|
1449
|
+
// State-scope rollback: clear and restore only .openclaw/.
|
|
1450
|
+
if (existsSync(stateDir)) {
|
|
1451
|
+
for (const entry of readdirSync(stateDir)) {
|
|
1452
|
+
if (cacheKeepDirs.has(entry))
|
|
1453
|
+
continue;
|
|
1454
|
+
rmSync(join(stateDir, entry), { recursive: true, force: true });
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
else {
|
|
1458
|
+
mkdirSync(stateDir, { recursive: true });
|
|
1459
|
+
}
|
|
1460
|
+
copyTreeLstat(extractedStateDir, stateDir, () => true);
|
|
1461
|
+
}
|
|
1462
|
+
// Rebuild proxy state
|
|
1463
|
+
try {
|
|
1464
|
+
const { bootstrapInstanceProxy } = await import("../llm-proxy/instance-proxy.js");
|
|
1465
|
+
await bootstrapInstanceProxy(instanceId);
|
|
1466
|
+
}
|
|
1467
|
+
catch { /* best effort */ }
|
|
1468
|
+
// Try to restart
|
|
1469
|
+
try {
|
|
1470
|
+
const { startInstance } = await import("../app-common/lifecycle-service.js");
|
|
1471
|
+
await startInstance(instanceId);
|
|
1472
|
+
}
|
|
1473
|
+
catch { /* best effort */ }
|
|
1474
|
+
}
|
|
1475
|
+
finally {
|
|
1476
|
+
if (existsSync(tmpDir))
|
|
1477
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
// ── Export ──
|
|
1481
|
+
/**
|
|
1482
|
+
* Export an instance for sharing. Uses WHITELIST strategy — only safe directories included.
|
|
1483
|
+
* API keys and credentials are stripped. Sensitive fields in openclaw.json are scrubbed.
|
|
1484
|
+
*/
|
|
1485
|
+
export async function exportInstance(instanceId, opts = {}) {
|
|
1486
|
+
const { getInstance } = await import("../app-common/service.js");
|
|
1487
|
+
const meta = getInstance(instanceId);
|
|
1488
|
+
if (!meta)
|
|
1489
|
+
throw new Error(`Instance ${instanceId} not found`);
|
|
1490
|
+
const openclawHome = integrationHomeFromMetadata(meta, instanceId);
|
|
1491
|
+
const stateDir = join(openclawHome, ".openclaw");
|
|
1492
|
+
if (!existsSync(stateDir))
|
|
1493
|
+
throw new Error(`State directory not found: ${stateDir}`);
|
|
1494
|
+
if (!acquireInstanceLock(instanceId, "exporting")) {
|
|
1495
|
+
throw Object.assign(new Error(`Instance ${instanceId} is locked`), { statusCode: 409 });
|
|
1496
|
+
}
|
|
1497
|
+
const stopHeartbeat = startLockHeartbeat(instanceId);
|
|
1498
|
+
// Export downloads are served per-instance from TMP_DIR/exports/<instanceId>/.
|
|
1499
|
+
// The download route enforces this isolation so one instance's URL can't
|
|
1500
|
+
// reach another's file.
|
|
1501
|
+
const instanceExportDir = getInstanceExportDir(instanceId);
|
|
1502
|
+
const outputPath = opts.outputPath || join(instanceExportDir, `export-${instanceId}-${Date.now()}.tar.gz`);
|
|
1503
|
+
const stagingDir = join(TMP_DIR, `export-staging-${instanceId}-${Date.now()}`);
|
|
1504
|
+
const tmpBackupPath = join(TMP_DIR, `export-raw-${instanceId}-${Date.now()}.tar.gz`);
|
|
1505
|
+
try {
|
|
1506
|
+
// Step 1: Self-pack in official format (always self-pack for export — we need to filter)
|
|
1507
|
+
await selfPackOfficialFormat(instanceId, tmpBackupPath, {
|
|
1508
|
+
type: "export",
|
|
1509
|
+
includeSessions: opts.includeSessions,
|
|
1510
|
+
includeWorkspace: true,
|
|
1511
|
+
});
|
|
1512
|
+
// Step 2: Extract to staging
|
|
1513
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
1514
|
+
await safeExtract(tmpBackupPath, stagingDir);
|
|
1515
|
+
if (existsSync(tmpBackupPath))
|
|
1516
|
+
rmSync(tmpBackupPath, { force: true });
|
|
1517
|
+
// Step 3: Resolve archive root + locate .openclaw/ via payload/.
|
|
1518
|
+
// self-pack always writes at root so resolveArchiveRoot is a no-op here,
|
|
1519
|
+
// but using it keeps all consumers on one canonical path.
|
|
1520
|
+
const stagingRoot = resolveArchiveRoot(stagingDir);
|
|
1521
|
+
const payloadDir = join(stagingRoot, "payload");
|
|
1522
|
+
if (!existsSync(payloadDir))
|
|
1523
|
+
throw new Error("Self-pack missing payload/ directory");
|
|
1524
|
+
const foundDir = findStateDir(payloadDir);
|
|
1525
|
+
if (!foundDir)
|
|
1526
|
+
throw new Error("Could not locate .openclaw in self-pack");
|
|
1527
|
+
// Step 4: Whitelist filter — remove everything outside allowed dirs
|
|
1528
|
+
const allowedRoots = ["openclaw.json", "extensions", "workspace"];
|
|
1529
|
+
if (opts.includeSessions)
|
|
1530
|
+
allowedRoots.push("agents");
|
|
1531
|
+
for (const entry of readdirSync(foundDir)) {
|
|
1532
|
+
if (!allowedRoots.includes(entry)) {
|
|
1533
|
+
rmSync(join(foundDir, entry), { recursive: true, force: true });
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
// Step 5: Scrub openclaw.json
|
|
1537
|
+
const configPath = join(foundDir, "openclaw.json");
|
|
1538
|
+
const scrubWarnings = [];
|
|
1539
|
+
if (existsSync(configPath)) {
|
|
1540
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
1541
|
+
// Scrub provider API keys
|
|
1542
|
+
const providers = config?.models?.providers;
|
|
1543
|
+
if (providers) {
|
|
1544
|
+
for (const [pid, prov] of Object.entries(providers)) {
|
|
1545
|
+
if (prov?.apiKey)
|
|
1546
|
+
prov.apiKey = "";
|
|
1547
|
+
// Remove proxy providers
|
|
1548
|
+
if (typeof prov?.baseUrl === "string" && prov.baseUrl.includes("/proxy/")) {
|
|
1549
|
+
delete providers[pid];
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
// Scrub channel credentials
|
|
1554
|
+
const channels = config?.channels;
|
|
1555
|
+
if (channels) {
|
|
1556
|
+
for (const [, ch] of Object.entries(channels)) {
|
|
1557
|
+
if (ch?.appSecret)
|
|
1558
|
+
ch.appSecret = "";
|
|
1559
|
+
if (ch?.appId)
|
|
1560
|
+
ch.appId = "";
|
|
1561
|
+
if (ch?.token)
|
|
1562
|
+
ch.token = "";
|
|
1563
|
+
if (ch?.secret)
|
|
1564
|
+
ch.secret = "";
|
|
1565
|
+
if (ch?.credentials)
|
|
1566
|
+
ch.credentials = {};
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
// Scrub gateway control-UI token — this is the token the panel uses
|
|
1570
|
+
// to drive OpenClaw's gateway control API; leaking it would let anyone
|
|
1571
|
+
// with the export package call the recipient's gateway once imported.
|
|
1572
|
+
if (config?.gateway?.auth?.token)
|
|
1573
|
+
config.gateway.auth.token = "";
|
|
1574
|
+
// Remove proxy model reference
|
|
1575
|
+
const defaultModel = config?.agents?.defaults?.model;
|
|
1576
|
+
if (typeof defaultModel === "string" && (defaultModel.startsWith("jsproxy/") || defaultModel.startsWith("js-"))) {
|
|
1577
|
+
delete config.agents.defaults.model;
|
|
1578
|
+
}
|
|
1579
|
+
// Residual scan — walk the object and flag any string-valued field
|
|
1580
|
+
// whose key name looks sensitive. The warning only records the JSON
|
|
1581
|
+
// path and field length, NEVER the value itself, so a residual field
|
|
1582
|
+
// can't leak through manifest.warnings.
|
|
1583
|
+
const SENSITIVE_KEY_RE = /^(api_?key|token|secret|credential|password|auth_?key|app_?secret|app_?id)$/i;
|
|
1584
|
+
const walk = (node, path) => {
|
|
1585
|
+
if (!node || typeof node !== "object")
|
|
1586
|
+
return;
|
|
1587
|
+
for (const [key, val] of Object.entries(node)) {
|
|
1588
|
+
const here = path ? `${path}.${key}` : key;
|
|
1589
|
+
if (typeof val === "string" && val.length >= 4 && SENSITIVE_KEY_RE.test(key)) {
|
|
1590
|
+
scrubWarnings.push(`Possible residual sensitive field at ${here} (${val.length} chars)`);
|
|
1591
|
+
}
|
|
1592
|
+
else if (val && typeof val === "object") {
|
|
1593
|
+
walk(val, here);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
walk(config, "");
|
|
1598
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1599
|
+
}
|
|
1600
|
+
// Step 6: Update manifest
|
|
1601
|
+
const manifestPath = join(stagingDir, "manifest.json");
|
|
1602
|
+
const manifest = existsSync(manifestPath)
|
|
1603
|
+
? JSON.parse(readFileSync(manifestPath, "utf-8"))
|
|
1604
|
+
: { schemaVersion: 1 };
|
|
1605
|
+
manifest.type = "export";
|
|
1606
|
+
manifest.has_api_keys = false;
|
|
1607
|
+
manifest.has_sessions = opts.includeSessions ?? false;
|
|
1608
|
+
if (scrubWarnings.length > 0)
|
|
1609
|
+
manifest.warnings = scrubWarnings;
|
|
1610
|
+
// Recalculate checksum
|
|
1611
|
+
const allFiles = collectFiles(stagingDir, stagingDir, []);
|
|
1612
|
+
const nonManifest = allFiles.filter(f => f !== "manifest.json");
|
|
1613
|
+
const checksum = calculateContentChecksum(stagingDir, nonManifest);
|
|
1614
|
+
manifest.checksum = `sha256:${checksum}`;
|
|
1615
|
+
manifest.checksum_scope = "content-excluding-manifest";
|
|
1616
|
+
manifest.stats = { total_files: allFiles.length };
|
|
1617
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
1618
|
+
// Step 7: Repack
|
|
1619
|
+
touchInstanceLock(instanceId);
|
|
1620
|
+
await new Promise((resolve, reject) => {
|
|
1621
|
+
const child = spawnChild("tar", ["-czf", outputPath, "."], {
|
|
1622
|
+
cwd: stagingDir,
|
|
1623
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1624
|
+
});
|
|
1625
|
+
let stderr = "";
|
|
1626
|
+
child.stderr?.on("data", (chunk) => { stderr += chunk.toString(); });
|
|
1627
|
+
child.on("close", (code) => {
|
|
1628
|
+
if (code !== 0)
|
|
1629
|
+
reject(new Error(`tar pack failed (exit ${code}): ${stderr.slice(0, 500)}`));
|
|
1630
|
+
else
|
|
1631
|
+
resolve();
|
|
1632
|
+
});
|
|
1633
|
+
child.on("error", reject);
|
|
1634
|
+
});
|
|
1635
|
+
const finalSize = statSync(outputPath).size;
|
|
1636
|
+
return { filename: outputPath.split("/").pop(), filepath: outputPath, size: finalSize, manifest };
|
|
1637
|
+
}
|
|
1638
|
+
finally {
|
|
1639
|
+
stopHeartbeat();
|
|
1640
|
+
if (existsSync(stagingDir))
|
|
1641
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
1642
|
+
if (existsSync(tmpBackupPath))
|
|
1643
|
+
rmSync(tmpBackupPath, { force: true });
|
|
1644
|
+
releaseInstanceLock(instanceId);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
// ── Import (three-step) ──
|
|
1648
|
+
/** Step 1: Store uploaded file to tmp. Returns temp_id for subsequent calls. */
|
|
1649
|
+
export async function storeUpload(filePath) {
|
|
1650
|
+
const tempId = randomUUID();
|
|
1651
|
+
const destPath = join(TMP_DIR, `import-${tempId}.tar.gz`);
|
|
1652
|
+
ensureBackupDirs();
|
|
1653
|
+
copyFileSync(filePath, destPath);
|
|
1654
|
+
return { temp_id: tempId };
|
|
1655
|
+
}
|
|
1656
|
+
/** Step 2: Preview an uploaded archive without creating an instance. */
|
|
1657
|
+
export async function previewImport(tempId) {
|
|
1658
|
+
const archivePath = join(TMP_DIR, `import-${tempId}.tar.gz`);
|
|
1659
|
+
if (!existsSync(archivePath))
|
|
1660
|
+
throw new Error("Upload not found or expired");
|
|
1661
|
+
const tmpDir = join(TMP_DIR, `preview-${tempId}`);
|
|
1662
|
+
const warnings = [];
|
|
1663
|
+
try {
|
|
1664
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
1665
|
+
const scanResult = await safeExtract(archivePath, tmpDir);
|
|
1666
|
+
// Resolve archive root — handles both self-pack and CLI wrapper layouts
|
|
1667
|
+
const archiveRoot = resolveArchiveRoot(tmpDir);
|
|
1668
|
+
// Read manifest
|
|
1669
|
+
const manifestPath = join(archiveRoot, "manifest.json");
|
|
1670
|
+
let manifest = null;
|
|
1671
|
+
if (existsSync(manifestPath)) {
|
|
1672
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
1673
|
+
}
|
|
1674
|
+
// Detect format — all archives use the official payload/ layout
|
|
1675
|
+
let format = "unknown";
|
|
1676
|
+
if (existsSync(join(archiveRoot, "payload"))) {
|
|
1677
|
+
format = "official";
|
|
1678
|
+
}
|
|
1679
|
+
// Check for missing API keys
|
|
1680
|
+
if (manifest?.has_api_keys === false) {
|
|
1681
|
+
warnings.push("This package does not contain API keys. You will need to configure them.");
|
|
1682
|
+
}
|
|
1683
|
+
// Check sessions
|
|
1684
|
+
if (manifest?.has_sessions === false) {
|
|
1685
|
+
warnings.push("This package does not contain conversation history.");
|
|
1686
|
+
}
|
|
1687
|
+
// Version compatibility
|
|
1688
|
+
if (manifest?.runtimeVersion) {
|
|
1689
|
+
warnings.push(`Created with OpenClaw ${manifest.runtimeVersion}`);
|
|
1690
|
+
}
|
|
1691
|
+
return {
|
|
1692
|
+
manifest,
|
|
1693
|
+
warnings,
|
|
1694
|
+
estimated_size: scanResult.totalSize,
|
|
1695
|
+
format,
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
finally {
|
|
1699
|
+
if (existsSync(tmpDir))
|
|
1700
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
/** Step 3: Create a new instance from a previously uploaded archive. */
|
|
1704
|
+
export async function importInstance(tempId, opts) {
|
|
1705
|
+
const archivePath = join(TMP_DIR, `import-${tempId}.tar.gz`);
|
|
1706
|
+
if (!existsSync(archivePath))
|
|
1707
|
+
throw new Error("Upload not found or expired");
|
|
1708
|
+
const warnings = [];
|
|
1709
|
+
const tmpDir = join(TMP_DIR, `import-create-${tempId}`);
|
|
1710
|
+
let targetInstalled = false;
|
|
1711
|
+
try {
|
|
1712
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
1713
|
+
await safeExtract(archivePath, tmpDir);
|
|
1714
|
+
// Resolve archive root — handles both self-pack and CLI wrapper layouts
|
|
1715
|
+
const archiveRoot = resolveArchiveRoot(tmpDir);
|
|
1716
|
+
// Locate .openclaw/ via payload/ directory (official format)
|
|
1717
|
+
const payloadDir = join(archiveRoot, "payload");
|
|
1718
|
+
if (!existsSync(payloadDir))
|
|
1719
|
+
throw new Error("Archive missing payload/ directory");
|
|
1720
|
+
const extractedStateDir = findStateDir(payloadDir);
|
|
1721
|
+
if (!extractedStateDir)
|
|
1722
|
+
throw new Error("Could not locate .openclaw directory in archive");
|
|
1723
|
+
// Detect archive scope — for home-scope archives we restore the full
|
|
1724
|
+
// openclaw-home tree (app/, .npm-global/, .codex/, etc.), not just
|
|
1725
|
+
// the .openclaw state dir. Without this, importing a home-scope
|
|
1726
|
+
// backup silently drops the runtime/app tree that motivated the
|
|
1727
|
+
// home-scope in the first place.
|
|
1728
|
+
let archiveScope = "state";
|
|
1729
|
+
let manifest = null;
|
|
1730
|
+
const manifestPath = join(archiveRoot, "manifest.json");
|
|
1731
|
+
if (existsSync(manifestPath)) {
|
|
1732
|
+
try {
|
|
1733
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
1734
|
+
if (manifest?.scope === "home")
|
|
1735
|
+
archiveScope = "home";
|
|
1736
|
+
}
|
|
1737
|
+
catch { /* fall back to state */ }
|
|
1738
|
+
}
|
|
1739
|
+
const { openclawHome: newOpenclawHome, stateDir: newStateDir } = await installOpenclawRestoreTarget(opts.id, opts.name, opts.description || "");
|
|
1740
|
+
targetInstalled = true;
|
|
1741
|
+
// Manual lstat walker preserves symlinks as opaque blobs and never
|
|
1742
|
+
// follows dangling targets.
|
|
1743
|
+
if (archiveScope === "home") {
|
|
1744
|
+
// Home scope: copy the portable extracted home tree so app/,
|
|
1745
|
+
// .npm-global/ (upgraded runtime), .codex/, and user files survive.
|
|
1746
|
+
// Still exclude IM credentials — same channel can't bind multiple
|
|
1747
|
+
// instances.
|
|
1748
|
+
const extractedHomeDir = dirname(extractedStateDir);
|
|
1749
|
+
copyTreeLstat(extractedHomeDir, newOpenclawHome, (rel) => {
|
|
1750
|
+
if (!rel)
|
|
1751
|
+
return true;
|
|
1752
|
+
if (isNonPortableHomeEntry(rel))
|
|
1753
|
+
return false;
|
|
1754
|
+
if (rel === ".openclaw/openclaw-weixin")
|
|
1755
|
+
return false;
|
|
1756
|
+
if (rel.startsWith(".openclaw/openclaw-weixin/"))
|
|
1757
|
+
return false;
|
|
1758
|
+
return true;
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
else {
|
|
1762
|
+
// State scope: copy only .openclaw/.
|
|
1763
|
+
copyTreeLstat(extractedStateDir, newStateDir, (rel) => {
|
|
1764
|
+
if (!rel)
|
|
1765
|
+
return true;
|
|
1766
|
+
if (rel.startsWith("openclaw-weixin"))
|
|
1767
|
+
return false;
|
|
1768
|
+
return true;
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
// Rewrite absolute paths in session data and symlinks from the source
|
|
1772
|
+
// instance to the new instance (fixes ENOENT crash on Docker import
|
|
1773
|
+
// and silent cross-instance data leakage on local import).
|
|
1774
|
+
const oldHome = detectOldHome(manifest, payloadDir, extractedStateDir);
|
|
1775
|
+
if (oldHome) {
|
|
1776
|
+
rewriteInstancePaths(archiveScope === "home" ? newOpenclawHome : newStateDir, oldHome, newOpenclawHome, warnings);
|
|
1777
|
+
}
|
|
1778
|
+
// Scrub channel credentials from copied openclaw.json (new instance should not inherit IM bindings)
|
|
1779
|
+
await scrubNewInstanceConfig(join(newStateDir, "openclaw.json"));
|
|
1780
|
+
// Regenerate gateway auth token (scrub cleared it to prevent credential leakage,
|
|
1781
|
+
// but the gateway requires a valid token to accept requests)
|
|
1782
|
+
regenerateGatewayAuthToken(join(newStateDir, "openclaw.json"));
|
|
1783
|
+
// DO NOT copy model.env, provider.env, instance.json
|
|
1784
|
+
// Bootstrap proxy
|
|
1785
|
+
try {
|
|
1786
|
+
const { bootstrapInstanceProxy } = await import("../llm-proxy/instance-proxy.js");
|
|
1787
|
+
await bootstrapInstanceProxy(opts.id);
|
|
1788
|
+
}
|
|
1789
|
+
catch (e) {
|
|
1790
|
+
warnings.push(`Proxy bootstrap: ${e.message}`);
|
|
1791
|
+
}
|
|
1792
|
+
// Check if a default provider can be injected
|
|
1793
|
+
try {
|
|
1794
|
+
const { getDefaultLlmProvider } = await import("../llm-proxy/providers.js");
|
|
1795
|
+
if (getDefaultLlmProvider()?.apiKey) {
|
|
1796
|
+
warnings.push("Default provider detected. You may configure API Key from the config page.");
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
catch { /* ignore */ }
|
|
1800
|
+
warnings.push("Instance created. Configure API Key before starting.");
|
|
1801
|
+
// Clean up the upload file
|
|
1802
|
+
rmSync(archivePath, { force: true });
|
|
1803
|
+
return { ok: true, instance_id: opts.id, warnings };
|
|
1804
|
+
}
|
|
1805
|
+
catch (error) {
|
|
1806
|
+
if (targetInstalled) {
|
|
1807
|
+
try {
|
|
1808
|
+
await rollbackOpenclawRestoreTarget(opts.id);
|
|
1809
|
+
}
|
|
1810
|
+
catch (rollbackError) {
|
|
1811
|
+
console.error(`[backup] failed to rollback partial import target ${opts.id}: ${rollbackError?.message ?? rollbackError}`);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
throw error;
|
|
1815
|
+
}
|
|
1816
|
+
finally {
|
|
1817
|
+
if (existsSync(tmpDir))
|
|
1818
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Scrub proxy identity and IM bindings from a copied openclaw.json before it
|
|
1823
|
+
* is handed to a brand-new instance (importInstance / createFromBackup).
|
|
1824
|
+
*
|
|
1825
|
+
* Matches the domain-clone behavior in `createInstance` so both paths produce
|
|
1826
|
+
* identical "clean slate" configs: no inherited proxy provider, no inherited
|
|
1827
|
+
* channel bindings, no dangling IM plugin entries. This avoids the hazard
|
|
1828
|
+
* where imported configs kept `channels.*.enabled = true` with scrubbed
|
|
1829
|
+
* credentials, leaving the plugin loader to boot a half-configured binding.
|
|
1830
|
+
*
|
|
1831
|
+
* Only the canonical `.openclaw/openclaw.json` is written. Old archive
|
|
1832
|
+
* layouts may contain an outer-home mirror, but import filters it out rather
|
|
1833
|
+
* than creating a second config source in the new instance.
|
|
1834
|
+
*/
|
|
1835
|
+
async function scrubNewInstanceConfig(configPath) {
|
|
1836
|
+
// Integration dispatch keeps framework code independent of OpenClaw's
|
|
1837
|
+
// private channel/plugin config structure.
|
|
1838
|
+
const { stripImBindings } = await import("../integrations/index.js");
|
|
1839
|
+
const scrubOne = (path) => {
|
|
1840
|
+
if (!existsSync(path))
|
|
1841
|
+
return;
|
|
1842
|
+
try {
|
|
1843
|
+
const config = JSON.parse(readFileSync(path, "utf-8"));
|
|
1844
|
+
// Remove proxy providers (will be regenerated by bootstrapInstanceProxy)
|
|
1845
|
+
const providers = config?.models?.providers;
|
|
1846
|
+
if (providers) {
|
|
1847
|
+
for (const [pid, prov] of Object.entries(providers)) {
|
|
1848
|
+
if (typeof prov?.baseUrl === "string" && prov.baseUrl.includes("/proxy/")) {
|
|
1849
|
+
delete providers[pid];
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
// Remove proxy model reference from agent defaults
|
|
1854
|
+
const defaultModel = config?.agents?.defaults?.model;
|
|
1855
|
+
if (typeof defaultModel === "string" && (defaultModel.startsWith("jsproxy/") || defaultModel.startsWith("js-"))) {
|
|
1856
|
+
delete config.agents.defaults.model;
|
|
1857
|
+
}
|
|
1858
|
+
// Unified IM scrub: delete channels block + matching plugin entries
|
|
1859
|
+
stripImBindings(config);
|
|
1860
|
+
writeFileSync(path, JSON.stringify(config, null, 2));
|
|
1861
|
+
}
|
|
1862
|
+
catch { /* best effort */ }
|
|
1863
|
+
};
|
|
1864
|
+
scrubOne(configPath);
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1867
|
+
* Regenerate the `gateway.auth.token` in an openclaw.json that was scrubbed
|
|
1868
|
+
* by {@link scrubNewInstanceConfig}. The scrub intentionally clears the old
|
|
1869
|
+
* token (to avoid leaking the source instance's credential), but for newly
|
|
1870
|
+
* created instances the gateway needs a valid token — otherwise it rejects
|
|
1871
|
+
* all requests with "unauthorized: gateway token missing".
|
|
1872
|
+
*
|
|
1873
|
+
* Writes only the canonical config path.
|
|
1874
|
+
*/
|
|
1875
|
+
function regenerateGatewayAuthToken(configPath) {
|
|
1876
|
+
const regenOne = (path) => {
|
|
1877
|
+
if (!existsSync(path))
|
|
1878
|
+
return;
|
|
1879
|
+
try {
|
|
1880
|
+
const config = JSON.parse(readFileSync(path, "utf-8"));
|
|
1881
|
+
if (!config.gateway)
|
|
1882
|
+
config.gateway = {};
|
|
1883
|
+
if (!config.gateway.auth)
|
|
1884
|
+
config.gateway.auth = { mode: "token" };
|
|
1885
|
+
config.gateway.auth.token = randomBytes(24).toString("hex");
|
|
1886
|
+
writeFileSync(path, JSON.stringify(config, null, 2));
|
|
1887
|
+
}
|
|
1888
|
+
catch { /* best effort — instance will still start, user can set token from UI */ }
|
|
1889
|
+
};
|
|
1890
|
+
regenOne(configPath);
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Resolve the effective "archive root" inside an extracted tarball.
|
|
1894
|
+
*
|
|
1895
|
+
* Returns the directory where `manifest.json` and `payload/` live. Two
|
|
1896
|
+
* archive layouts are supported:
|
|
1897
|
+
*
|
|
1898
|
+
* 1. **JishuShell self-pack** (`selfPackOfficialFormat`): writes
|
|
1899
|
+
* `manifest.json` and `payload/` directly at the archive root, so the
|
|
1900
|
+
* effective root is the extraction dir itself.
|
|
1901
|
+
*
|
|
1902
|
+
* 2. **Official OpenClaw `backup create`**: wraps everything inside a
|
|
1903
|
+
* single top-level directory named after the archive basename, e.g.
|
|
1904
|
+
* `2026-04-10T03-06-23.257Z-openclaw-backup/`. The wrapper name is
|
|
1905
|
+
* also advertised via `manifest.archiveRoot`.
|
|
1906
|
+
*
|
|
1907
|
+
* Detection strategy (cheapest first):
|
|
1908
|
+
* a. If `<tmpDir>/manifest.json` exists → self-pack → return tmpDir.
|
|
1909
|
+
* b. Else scan top-level entries; if exactly one is a directory containing
|
|
1910
|
+
* `manifest.json`, that's the official CLI wrapper → return it.
|
|
1911
|
+
* c. Fall back to tmpDir; the caller will throw its own missing-manifest
|
|
1912
|
+
* error with a meaningful path.
|
|
1913
|
+
*
|
|
1914
|
+
* Without this helper, `restoreInstance` / `importInstance` / `verifyArchive`
|
|
1915
|
+
* would throw "Archive missing manifest.json" on every state-scope archive
|
|
1916
|
+
* produced by the official CLI.
|
|
1917
|
+
*/
|
|
1918
|
+
export function resolveArchiveRoot(tmpDir) {
|
|
1919
|
+
if (existsSync(join(tmpDir, "manifest.json")))
|
|
1920
|
+
return tmpDir;
|
|
1921
|
+
try {
|
|
1922
|
+
for (const entry of readdirSync(tmpDir)) {
|
|
1923
|
+
const candidate = join(tmpDir, entry);
|
|
1924
|
+
try {
|
|
1925
|
+
if (statSync(candidate).isDirectory() && existsSync(join(candidate, "manifest.json"))) {
|
|
1926
|
+
return candidate;
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
catch { /* skip inaccessible */ }
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
catch { /* skip */ }
|
|
1933
|
+
return tmpDir;
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Locate the OpenClaw state directory inside an extracted archive.
|
|
1937
|
+
*
|
|
1938
|
+
* Finds a directory **literally named `.openclaw`** that contains an
|
|
1939
|
+
* `openclaw.json` file. Uses BFS so the shallowest match wins, which guards
|
|
1940
|
+
* against nested copies inside extensions/ or workspace/.
|
|
1941
|
+
*
|
|
1942
|
+
* This is intentionally stricter than "any directory containing
|
|
1943
|
+
* openclaw.json". Historical archives may contain an outer-home config
|
|
1944
|
+
* mirror, so a naive search can select the wrong level for home-scope
|
|
1945
|
+
* archives.
|
|
1946
|
+
*/
|
|
1947
|
+
export function findStateDir(rootDir) {
|
|
1948
|
+
const queue = [rootDir];
|
|
1949
|
+
while (queue.length > 0) {
|
|
1950
|
+
const current = queue.shift();
|
|
1951
|
+
try {
|
|
1952
|
+
for (const entry of readdirSync(current)) {
|
|
1953
|
+
const fullPath = join(current, entry);
|
|
1954
|
+
try {
|
|
1955
|
+
if (!statSync(fullPath).isDirectory())
|
|
1956
|
+
continue;
|
|
1957
|
+
if (entry === ".openclaw" && existsSync(join(fullPath, "openclaw.json"))) {
|
|
1958
|
+
return fullPath;
|
|
1959
|
+
}
|
|
1960
|
+
queue.push(fullPath);
|
|
1961
|
+
}
|
|
1962
|
+
catch { /* skip inaccessible entries */ }
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
catch { /* skip inaccessible dirs */ }
|
|
1966
|
+
}
|
|
1967
|
+
return null;
|
|
1968
|
+
}
|
|
1969
|
+
// ── Auto-backup scheduler ──
|
|
1970
|
+
const autoBackupTimers = new Map();
|
|
1971
|
+
let autoBackupShuttingDown = false;
|
|
1972
|
+
/** Get auto-backup config from instance.json */
|
|
1973
|
+
export async function getAutoBackupConfig(instanceId) {
|
|
1974
|
+
try {
|
|
1975
|
+
const { getInstance } = await import("../app-common/service.js");
|
|
1976
|
+
const meta = getInstance(instanceId);
|
|
1977
|
+
return meta?.auto_backup || null;
|
|
1978
|
+
}
|
|
1979
|
+
catch {
|
|
1980
|
+
return null;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
/** Update auto-backup status in instance.json */
|
|
1984
|
+
async function updateAutoBackupStatus(instanceId, patch) {
|
|
1985
|
+
const { updateInstanceMeta, getInstance } = await import("../app-common/service.js");
|
|
1986
|
+
const meta = getInstance(instanceId);
|
|
1987
|
+
const current = meta?.auto_backup || {};
|
|
1988
|
+
updateInstanceMeta(instanceId, { auto_backup: { ...current, ...patch } });
|
|
1989
|
+
}
|
|
1990
|
+
/** Check if instance has changed since last backup (mtime-based) */
|
|
1991
|
+
async function hasChangedSince(instanceId, sinceMs) {
|
|
1992
|
+
try {
|
|
1993
|
+
const { getInstance } = await import("../app-common/service.js");
|
|
1994
|
+
const meta = getInstance(instanceId);
|
|
1995
|
+
const openclawHome = integrationHomeFromMetadata(meta, instanceId);
|
|
1996
|
+
const stateDir = join(openclawHome, ".openclaw");
|
|
1997
|
+
if (!existsSync(stateDir))
|
|
1998
|
+
return false;
|
|
1999
|
+
return getMaxMtime(stateDir, 0) > sinceMs;
|
|
2000
|
+
}
|
|
2001
|
+
catch {
|
|
2002
|
+
return true; // On error, assume changed (safe default)
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
/** Recursively find max mtime in a directory, skipping cache dirs */
|
|
2006
|
+
function getMaxMtime(dir, currentMax) {
|
|
2007
|
+
const skipDirs = new Set([".npm", ".cache", ".node_compile_cache", ".npm-global"]);
|
|
2008
|
+
try {
|
|
2009
|
+
for (const entry of readdirSync(dir)) {
|
|
2010
|
+
if (skipDirs.has(entry))
|
|
2011
|
+
continue;
|
|
2012
|
+
const fullPath = join(dir, entry);
|
|
2013
|
+
try {
|
|
2014
|
+
const stat = statSync(fullPath);
|
|
2015
|
+
if (stat.mtimeMs > currentMax)
|
|
2016
|
+
currentMax = stat.mtimeMs;
|
|
2017
|
+
if (stat.isDirectory()) {
|
|
2018
|
+
currentMax = getMaxMtime(fullPath, currentMax);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
catch { /* skip inaccessible */ }
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
catch { /* skip */ }
|
|
2025
|
+
return currentMax;
|
|
2026
|
+
}
|
|
2027
|
+
/** Clean old auto-backup files, keeping only the newest `keepCount` */
|
|
2028
|
+
export async function cleanOldAutoBackups(instanceId, keepCount) {
|
|
2029
|
+
const backupDir = join(BACKUPS_DIR, instanceId);
|
|
2030
|
+
if (!existsSync(backupDir))
|
|
2031
|
+
return 0;
|
|
2032
|
+
const autoBackups = readdirSync(backupDir)
|
|
2033
|
+
.filter(f => f.startsWith("auto-backup-") && f.endsWith(".tar.gz"))
|
|
2034
|
+
.map(f => ({ name: f, mtime: statSync(join(backupDir, f)).mtimeMs }))
|
|
2035
|
+
.sort((a, b) => b.mtime - a.mtime); // newest first
|
|
2036
|
+
let cleaned = 0;
|
|
2037
|
+
for (let i = keepCount; i < autoBackups.length; i++) {
|
|
2038
|
+
rmSync(join(backupDir, autoBackups[i].name), { force: true });
|
|
2039
|
+
cleaned++;
|
|
2040
|
+
}
|
|
2041
|
+
return cleaned;
|
|
2042
|
+
}
|
|
2043
|
+
/** Check available disk space (returns bytes) */
|
|
2044
|
+
function getAvailableDiskSpace() {
|
|
2045
|
+
try {
|
|
2046
|
+
const output = execFileSync("df", ["-B1", "--output=avail", BACKUPS_DIR], {
|
|
2047
|
+
encoding: "utf-8",
|
|
2048
|
+
timeout: 5000,
|
|
2049
|
+
});
|
|
2050
|
+
const lines = output.trim().split("\n");
|
|
2051
|
+
return parseInt(lines[lines.length - 1].trim(), 10) || 0;
|
|
2052
|
+
}
|
|
2053
|
+
catch {
|
|
2054
|
+
try {
|
|
2055
|
+
// macOS fallback
|
|
2056
|
+
const output = execFileSync("df", ["-k", BACKUPS_DIR], { encoding: "utf-8", timeout: 5000 });
|
|
2057
|
+
const lines = output.trim().split("\n");
|
|
2058
|
+
const parts = lines[lines.length - 1].split(/\s+/);
|
|
2059
|
+
return (parseInt(parts[3], 10) || 0) * 1024; // Convert KB to bytes
|
|
2060
|
+
}
|
|
2061
|
+
catch {
|
|
2062
|
+
return Infinity; // Can't check, don't block
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
const MIN_DISK_BYTES = 2 * 1024 * 1024 * 1024; // 2GB
|
|
2067
|
+
const jobQueue = [];
|
|
2068
|
+
const jobHistory = [];
|
|
2069
|
+
const MAX_HISTORY = 20;
|
|
2070
|
+
let currentJob = null;
|
|
2071
|
+
let queueProcessing = false;
|
|
2072
|
+
// Map of job ID to resolve/reject for callers that want to await completion
|
|
2073
|
+
const jobWaiters = new Map();
|
|
2074
|
+
// Store execute functions for queued jobs
|
|
2075
|
+
const jobExecutors = new Map();
|
|
2076
|
+
/** Enqueue a backup operation. Returns the job immediately (non-blocking). */
|
|
2077
|
+
export function enqueueJob(instanceId, operation, executeFn) {
|
|
2078
|
+
const job = {
|
|
2079
|
+
id: randomUUID(),
|
|
2080
|
+
instanceId,
|
|
2081
|
+
operation,
|
|
2082
|
+
status: "queued",
|
|
2083
|
+
createdAt: Date.now(),
|
|
2084
|
+
};
|
|
2085
|
+
jobQueue.push(job);
|
|
2086
|
+
// Store the execute function for when this job's turn comes
|
|
2087
|
+
jobExecutors.set(job.id, executeFn);
|
|
2088
|
+
// Start processing if not already running
|
|
2089
|
+
if (!queueProcessing) {
|
|
2090
|
+
processQueue();
|
|
2091
|
+
}
|
|
2092
|
+
return job;
|
|
2093
|
+
}
|
|
2094
|
+
/** Enqueue and wait for completion. Returns the completed job. */
|
|
2095
|
+
export function enqueueJobAndWait(instanceId, operation, executeFn) {
|
|
2096
|
+
const job = enqueueJob(instanceId, operation, executeFn);
|
|
2097
|
+
return new Promise((resolve, reject) => {
|
|
2098
|
+
jobWaiters.set(job.id, { resolve, reject });
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
async function processQueue() {
|
|
2102
|
+
if (queueProcessing)
|
|
2103
|
+
return;
|
|
2104
|
+
queueProcessing = true;
|
|
2105
|
+
while (jobQueue.length > 0) {
|
|
2106
|
+
const job = jobQueue.shift();
|
|
2107
|
+
currentJob = job;
|
|
2108
|
+
job.status = "running";
|
|
2109
|
+
job.startedAt = Date.now();
|
|
2110
|
+
// Get the executor function
|
|
2111
|
+
const executeFn = jobExecutors.get(job.id);
|
|
2112
|
+
jobExecutors.delete(job.id);
|
|
2113
|
+
if (!executeFn) {
|
|
2114
|
+
job.status = "failed";
|
|
2115
|
+
job.error = "No executor function found";
|
|
2116
|
+
job.completedAt = Date.now();
|
|
2117
|
+
moveToHistory(job);
|
|
2118
|
+
continue;
|
|
2119
|
+
}
|
|
2120
|
+
try {
|
|
2121
|
+
const result = await executeFn(job);
|
|
2122
|
+
job.status = "completed";
|
|
2123
|
+
job.result = result;
|
|
2124
|
+
}
|
|
2125
|
+
catch (e) {
|
|
2126
|
+
job.status = "failed";
|
|
2127
|
+
job.error = e.message || "Unknown error";
|
|
2128
|
+
}
|
|
2129
|
+
finally {
|
|
2130
|
+
job.completedAt = Date.now();
|
|
2131
|
+
currentJob = null;
|
|
2132
|
+
moveToHistory(job);
|
|
2133
|
+
// Notify waiter if any
|
|
2134
|
+
const waiter = jobWaiters.get(job.id);
|
|
2135
|
+
if (waiter) {
|
|
2136
|
+
jobWaiters.delete(job.id);
|
|
2137
|
+
if (job.status === "completed") {
|
|
2138
|
+
waiter.resolve(job);
|
|
2139
|
+
}
|
|
2140
|
+
else {
|
|
2141
|
+
waiter.reject(new Error(job.error || "Job failed"));
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
queueProcessing = false;
|
|
2147
|
+
}
|
|
2148
|
+
function moveToHistory(job) {
|
|
2149
|
+
jobHistory.unshift(job);
|
|
2150
|
+
if (jobHistory.length > MAX_HISTORY)
|
|
2151
|
+
jobHistory.pop();
|
|
2152
|
+
}
|
|
2153
|
+
/** Update progress message for the currently running job */
|
|
2154
|
+
export function updateJobProgress(jobId, progress) {
|
|
2155
|
+
if (currentJob?.id === jobId) {
|
|
2156
|
+
currentJob.progress = progress;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
/** Get full queue status */
|
|
2160
|
+
export function getQueueStatus() {
|
|
2161
|
+
return {
|
|
2162
|
+
current: currentJob,
|
|
2163
|
+
queued: [...jobQueue],
|
|
2164
|
+
recent: jobHistory.slice(0, 10),
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
/** Get a specific job by ID (checks current, queue, and history) */
|
|
2168
|
+
export function getJob(jobId) {
|
|
2169
|
+
if (currentJob?.id === jobId)
|
|
2170
|
+
return currentJob;
|
|
2171
|
+
const queued = jobQueue.find(j => j.id === jobId);
|
|
2172
|
+
if (queued)
|
|
2173
|
+
return queued;
|
|
2174
|
+
const historic = jobHistory.find(j => j.id === jobId);
|
|
2175
|
+
return historic || null;
|
|
2176
|
+
}
|
|
2177
|
+
/** Cancel a queued job (cannot cancel running jobs) */
|
|
2178
|
+
export function cancelJob(jobId) {
|
|
2179
|
+
const idx = jobQueue.findIndex(j => j.id === jobId);
|
|
2180
|
+
if (idx === -1)
|
|
2181
|
+
return false;
|
|
2182
|
+
const job = jobQueue.splice(idx, 1)[0];
|
|
2183
|
+
job.status = "failed";
|
|
2184
|
+
job.error = "Cancelled";
|
|
2185
|
+
job.completedAt = Date.now();
|
|
2186
|
+
moveToHistory(job);
|
|
2187
|
+
jobExecutors.delete(jobId);
|
|
2188
|
+
const waiter = jobWaiters.get(jobId);
|
|
2189
|
+
if (waiter) {
|
|
2190
|
+
jobWaiters.delete(jobId);
|
|
2191
|
+
waiter.reject(new Error("Job cancelled"));
|
|
2192
|
+
}
|
|
2193
|
+
return true;
|
|
2194
|
+
}
|
|
2195
|
+
/** Schedule auto-backup for an instance */
|
|
2196
|
+
export function scheduleAutoBackup(instanceId, config) {
|
|
2197
|
+
cancelAutoBackup(instanceId);
|
|
2198
|
+
if (autoBackupShuttingDown || !config.enabled)
|
|
2199
|
+
return;
|
|
2200
|
+
const lastAt = config.last_backup_at ? new Date(config.last_backup_at).getTime() : 0;
|
|
2201
|
+
const intervalMs = config.interval_hours * 3600_000;
|
|
2202
|
+
const delay = Math.max(0, intervalMs - (Date.now() - lastAt));
|
|
2203
|
+
const timer = setTimeout(async () => {
|
|
2204
|
+
if (autoBackupShuttingDown)
|
|
2205
|
+
return;
|
|
2206
|
+
// Always work against the freshest persisted config so we don't race
|
|
2207
|
+
// against user edits or updateAutoBackupStatus writes. The `config` arg
|
|
2208
|
+
// is only used for scheduling delay above.
|
|
2209
|
+
const liveConfig = (await getAutoBackupConfig(instanceId)) || config;
|
|
2210
|
+
const liveLastAt = liveConfig.last_backup_at
|
|
2211
|
+
? new Date(liveConfig.last_backup_at).getTime()
|
|
2212
|
+
: 0;
|
|
2213
|
+
const keepCount = liveConfig.keep_count || 7;
|
|
2214
|
+
try {
|
|
2215
|
+
// Check disk space
|
|
2216
|
+
const available = getAvailableDiskSpace();
|
|
2217
|
+
if (available < MIN_DISK_BYTES) {
|
|
2218
|
+
await updateAutoBackupStatus(instanceId, {
|
|
2219
|
+
last_backup_ok: false,
|
|
2220
|
+
consecutive_failures: (liveConfig.consecutive_failures || 0) + 1,
|
|
2221
|
+
warnings: ["Auto-backup paused: disk space below 2GB"],
|
|
2222
|
+
});
|
|
2223
|
+
console.warn(`[auto-backup] ${instanceId}: paused, disk below 2GB`);
|
|
2224
|
+
// Still reschedule to check again later
|
|
2225
|
+
const fresh = await getAutoBackupConfig(instanceId);
|
|
2226
|
+
if (!autoBackupShuttingDown && fresh?.enabled)
|
|
2227
|
+
scheduleAutoBackup(instanceId, fresh);
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
// Check if changed since last backup
|
|
2231
|
+
if (await hasChangedSince(instanceId, liveLastAt)) {
|
|
2232
|
+
// Enqueue to avoid parallel I/O contention on RPi SD card
|
|
2233
|
+
await enqueueJobAndWait(instanceId, "auto-backup", async (job) => {
|
|
2234
|
+
updateJobProgress(job.id, "Backing up...");
|
|
2235
|
+
const result = await backupInstance(instanceId, { type: "auto-backup" });
|
|
2236
|
+
updateJobProgress(job.id, "Cleaning old backups...");
|
|
2237
|
+
await cleanOldAutoBackups(instanceId, keepCount);
|
|
2238
|
+
return result;
|
|
2239
|
+
});
|
|
2240
|
+
await updateAutoBackupStatus(instanceId, {
|
|
2241
|
+
last_backup_at: new Date().toISOString(),
|
|
2242
|
+
last_backup_ok: true,
|
|
2243
|
+
consecutive_failures: 0,
|
|
2244
|
+
warnings: [],
|
|
2245
|
+
});
|
|
2246
|
+
console.log(`[auto-backup] ${instanceId}: completed`);
|
|
2247
|
+
}
|
|
2248
|
+
else {
|
|
2249
|
+
console.log(`[auto-backup] ${instanceId}: skipped (no changes)`);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
catch (e) {
|
|
2253
|
+
const failures = (liveConfig.consecutive_failures || 0) + 1;
|
|
2254
|
+
const warnings = failures >= 3
|
|
2255
|
+
? [`Auto-backup failed ${failures} times: ${e.message}`]
|
|
2256
|
+
: [];
|
|
2257
|
+
await updateAutoBackupStatus(instanceId, {
|
|
2258
|
+
last_backup_ok: false,
|
|
2259
|
+
consecutive_failures: failures,
|
|
2260
|
+
warnings,
|
|
2261
|
+
});
|
|
2262
|
+
console.error(`[auto-backup] ${instanceId} failed (${failures}x):`, e.message);
|
|
2263
|
+
}
|
|
2264
|
+
// Reschedule (re-read config in case user changed it)
|
|
2265
|
+
const freshConfig = await getAutoBackupConfig(instanceId);
|
|
2266
|
+
if (!autoBackupShuttingDown && freshConfig?.enabled) {
|
|
2267
|
+
scheduleAutoBackup(instanceId, freshConfig);
|
|
2268
|
+
}
|
|
2269
|
+
}, delay);
|
|
2270
|
+
timer.unref(); // Don't prevent process exit
|
|
2271
|
+
autoBackupTimers.set(instanceId, timer);
|
|
2272
|
+
}
|
|
2273
|
+
/** Cancel auto-backup for an instance */
|
|
2274
|
+
export function cancelAutoBackup(instanceId) {
|
|
2275
|
+
const t = autoBackupTimers.get(instanceId);
|
|
2276
|
+
if (t) {
|
|
2277
|
+
clearTimeout(t);
|
|
2278
|
+
autoBackupTimers.delete(instanceId);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
/** Stop every per-instance auto-backup timer during Core shutdown. */
|
|
2282
|
+
export function shutdownAutoBackup() {
|
|
2283
|
+
autoBackupShuttingDown = true;
|
|
2284
|
+
for (const timer of autoBackupTimers.values()) {
|
|
2285
|
+
clearTimeout(timer);
|
|
2286
|
+
}
|
|
2287
|
+
autoBackupTimers.clear();
|
|
2288
|
+
}
|
|
2289
|
+
/** Initialize auto-backup for all instances (call on server startup) */
|
|
2290
|
+
export async function initAutoBackup() {
|
|
2291
|
+
autoBackupShuttingDown = false;
|
|
2292
|
+
try {
|
|
2293
|
+
const { listInstances } = await import("../app-common/service.js");
|
|
2294
|
+
const instances = listInstances();
|
|
2295
|
+
let jitterIndex = 0;
|
|
2296
|
+
for (const inst of instances) {
|
|
2297
|
+
const config = inst.auto_backup;
|
|
2298
|
+
if (config?.enabled) {
|
|
2299
|
+
// Stagger startup: if multiple instances are due at the same time,
|
|
2300
|
+
// add incremental jitter (2 min per instance) to avoid I/O contention
|
|
2301
|
+
if (!config.last_backup_at) {
|
|
2302
|
+
const jitterMs = jitterIndex * 2 * 60_000; // 2 min apart
|
|
2303
|
+
const jitteredConfig = { ...config, last_backup_at: new Date(Date.now() - (config.interval_hours * 3600_000) + jitterMs).toISOString() };
|
|
2304
|
+
scheduleAutoBackup(inst.id, jitteredConfig);
|
|
2305
|
+
}
|
|
2306
|
+
else {
|
|
2307
|
+
scheduleAutoBackup(inst.id, config);
|
|
2308
|
+
}
|
|
2309
|
+
jitterIndex++;
|
|
2310
|
+
console.log(`[auto-backup] ${inst.id}: scheduled (interval: ${config.interval_hours}h, keep: ${config.keep_count})`);
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
catch (e) {
|
|
2315
|
+
console.error("[auto-backup] Init failed:", e.message);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
//# sourceMappingURL=backup-manager.js.map
|