windows-development-sandbox 1.5.0 → 1.7.0
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/README.md +1 -0
- package/config.example.json +17 -1
- package/dist/cli/args.d.ts +13 -2
- package/dist/cli/args.js +95 -24
- package/dist/cli/commands/accounts.js +45 -17
- package/dist/cli/commands/context.d.ts +2 -0
- package/dist/cli/commands/context.js +9 -2
- package/dist/cli/commands/data/clean.js +32 -9
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/launch/add-root.d.ts +3 -2
- package/dist/cli/commands/launch/add-root.js +11 -10
- package/dist/cli/commands/launch/launch.js +57 -4
- package/dist/cli/commands/launch/prepare.js +3 -1
- package/dist/cli/commands/maintenance/audit.js +1 -1
- package/dist/cli/commands/maintenance/clean.d.ts +1 -1
- package/dist/cli/commands/maintenance/clean.js +17 -3
- package/dist/cli/commands/maintenance/config.d.ts +1 -1
- package/dist/cli/commands/maintenance/config.js +112 -10
- package/dist/cli/commands/maintenance/doctor.d.ts +1 -1
- package/dist/cli/commands/maintenance/doctor.js +2 -4
- package/dist/cli/commands/maintenance/migrate.d.ts +2 -1
- package/dist/cli/commands/maintenance/migrate.js +19 -4
- package/dist/cli/commands/maintenance/sync.js +2 -0
- package/dist/cli/commands/maintenance/update.d.ts +2 -0
- package/dist/cli/commands/maintenance/update.js +27 -4
- package/dist/cli/commands/projects/here.js +12 -7
- package/dist/cli/commands/projects/projects.js +26 -17
- package/dist/cli/commands/registry.js +7 -0
- package/dist/cli/commands/repair/checks/agents.d.ts +2 -0
- package/dist/cli/commands/repair/checks/agents.js +70 -0
- package/dist/cli/commands/repair/checks/common.d.ts +8 -0
- package/dist/cli/commands/repair/checks/common.js +22 -0
- package/dist/cli/commands/repair/checks/data.d.ts +2 -0
- package/dist/cli/commands/repair/checks/data.js +241 -0
- package/dist/cli/commands/repair/checks/index.d.ts +2 -0
- package/dist/cli/commands/repair/checks/index.js +20 -0
- package/dist/cli/commands/repair/checks/install.d.ts +5 -0
- package/dist/cli/commands/repair/checks/install.js +318 -0
- package/dist/cli/commands/repair/checks/machine.d.ts +2 -0
- package/dist/cli/commands/repair/checks/machine.js +232 -0
- package/dist/cli/commands/repair/checks/network.d.ts +6 -0
- package/dist/cli/commands/repair/checks/network.js +60 -0
- package/dist/cli/commands/repair/checks/pc.d.ts +4 -0
- package/dist/cli/commands/repair/checks/pc.js +55 -0
- package/dist/cli/commands/repair/checks/sandbox.d.ts +2 -0
- package/dist/cli/commands/repair/checks/sandbox.js +267 -0
- package/dist/cli/commands/repair/checks/settings.d.ts +2 -0
- package/dist/cli/commands/repair/checks/settings.js +169 -0
- package/dist/cli/commands/repair/checks/vpn.d.ts +2 -0
- package/dist/cli/commands/repair/checks/vpn.js +53 -0
- package/dist/cli/commands/repair/engine.d.ts +32 -0
- package/dist/cli/commands/repair/engine.js +127 -0
- package/dist/cli/commands/repair/env.d.ts +7 -0
- package/dist/cli/commands/repair/env.js +49 -0
- package/dist/cli/commands/repair/index.d.ts +1 -9
- package/dist/cli/commands/repair/index.js +2 -54
- package/dist/cli/commands/repair/log.d.ts +17 -0
- package/dist/cli/commands/repair/log.js +82 -0
- package/dist/cli/commands/repair/printer.d.ts +32 -0
- package/dist/cli/commands/repair/printer.js +132 -0
- package/dist/cli/commands/repair/run.d.ts +33 -0
- package/dist/cli/commands/repair/run.js +133 -0
- package/dist/cli/commands/repair/types.d.ts +97 -9
- package/dist/cli/commands/repair/types.js +11 -7
- package/dist/cli/commands/resume.js +2 -2
- package/dist/cli/commands/sessions/list.js +5 -2
- package/dist/cli/commands/sessions/status.d.ts +2 -0
- package/dist/cli/commands/sessions/status.js +27 -6
- package/dist/cli/commands/setup/index.d.ts +5 -0
- package/dist/cli/commands/setup/index.js +25 -1
- package/dist/cli/commands/storage/index.d.ts +2 -0
- package/dist/cli/commands/storage/index.js +2 -0
- package/dist/cli/commands/storage/offer.d.ts +3 -0
- package/dist/cli/commands/storage/offer.js +85 -0
- package/dist/cli/commands/storage/storage.d.ts +8 -0
- package/dist/cli/commands/storage/storage.js +171 -0
- package/dist/cli/commands/user-config.js +1 -1
- package/dist/cli/commands/vpn.d.ts +4 -0
- package/dist/cli/commands/vpn.js +75 -0
- package/dist/cli/commands/web.d.ts +4 -0
- package/dist/cli/commands/web.js +95 -0
- package/dist/cli/history.js +1 -1
- package/dist/cli/index.js +46 -3
- package/dist/cli/manifest.d.ts +303 -29
- package/dist/cli/manifest.js +173 -43
- package/dist/cli/menu/add-agent.d.ts +47 -0
- package/dist/cli/menu/add-agent.js +168 -0
- package/dist/cli/menu/apps.d.ts +31 -2
- package/dist/cli/menu/apps.js +44 -5
- package/dist/cli/menu/command-fields.d.ts +18 -0
- package/dist/cli/menu/command-fields.js +61 -0
- package/dist/cli/menu/command-form.d.ts +22 -0
- package/dist/cli/menu/command-form.js +169 -46
- package/dist/cli/menu/data-folder.d.ts +18 -0
- package/dist/cli/menu/data-folder.js +99 -0
- package/dist/cli/menu/menu.d.ts +1 -1
- package/dist/cli/menu/menu.js +11 -8
- package/dist/cli/menu/pages/accounts-page.js +55 -24
- package/dist/cli/menu/pages/agents-page.d.ts +66 -0
- package/dist/cli/menu/pages/agents-page.js +338 -0
- package/dist/cli/menu/pages/apps-page.js +1 -2
- package/dist/cli/menu/pages/list-page.d.ts +16 -3
- package/dist/cli/menu/pages/list-page.js +40 -8
- package/dist/cli/menu/pages/maintenance-page.d.ts +9 -0
- package/dist/cli/menu/pages/maintenance-page.js +161 -72
- package/dist/cli/menu/pages/sandbox-page.d.ts +3 -0
- package/dist/cli/menu/pages/sandbox-page.js +132 -43
- package/dist/cli/menu/pages/sessions-page.d.ts +4 -0
- package/dist/cli/menu/pages/sessions-page.js +41 -25
- package/dist/cli/menu/pages/settings-page.d.ts +8 -4
- package/dist/cli/menu/pages/settings-page.js +36 -13
- package/dist/cli/menu/settings/categories.d.ts +2 -0
- package/dist/cli/menu/settings/categories.js +19 -44
- package/dist/cli/menu/settings/collections/collection-editor.d.ts +7 -1
- package/dist/cli/menu/settings/collections/collection-editor.js +12 -8
- package/dist/cli/menu/settings/collections/map-editor.js +5 -1
- package/dist/cli/menu/settings/collections/object-list-editor.js +2 -2
- package/dist/cli/menu/settings/collections/object-map-editor.js +2 -2
- package/dist/cli/menu/settings/collections/string-list-editor.d.ts +11 -1
- package/dist/cli/menu/settings/collections/string-list-editor.js +23 -0
- package/dist/cli/menu/settings/index.d.ts +1 -1
- package/dist/cli/menu/settings/index.js +1 -1
- package/dist/cli/menu/settings/object-editor.d.ts +5 -1
- package/dist/cli/menu/settings/object-editor.js +15 -4
- package/dist/cli/menu/settings/path-kind.d.ts +20 -0
- package/dist/cli/menu/settings/path-kind.js +44 -0
- package/dist/cli/menu/settings/schema-node.d.ts +9 -0
- package/dist/cli/menu/settings/schema-node.js +26 -0
- package/dist/cli/menu/settings/value-editor.d.ts +5 -0
- package/dist/cli/menu/settings/value-editor.js +51 -18
- package/dist/cli/menu/several.js +7 -5
- package/dist/cli/menu/shell.d.ts +37 -10
- package/dist/cli/menu/shell.js +90 -10
- package/dist/cli/menu/vpn-login.d.ts +16 -0
- package/dist/cli/menu/vpn-login.js +66 -0
- package/dist/cli/output/print.d.ts +2 -0
- package/dist/cli/output/print.js +8 -1
- package/dist/cli/services/accounts-service.d.ts +2 -0
- package/dist/cli/services/accounts-service.js +2 -2
- package/dist/cli/services/health-service.d.ts +0 -6
- package/dist/cli/services/health-service.js +5 -42
- package/dist/cli/services/index.d.ts +3 -1
- package/dist/cli/services/index.js +3 -1
- package/dist/cli/services/migrate-service.js +1 -1
- package/dist/cli/services/update-service.d.ts +2 -0
- package/dist/cli/services/update-service.js +13 -6
- package/dist/cli/services/vpn-service.d.ts +28 -0
- package/dist/cli/services/vpn-service.js +46 -0
- package/dist/cli/services/web-service.d.ts +22 -0
- package/dist/cli/services/web-service.js +111 -0
- package/dist/cli/tui/app.d.ts +6 -3
- package/dist/cli/tui/app.js +12 -5
- package/dist/cli/tui/dialogs/path-listing.d.ts +43 -0
- package/dist/cli/tui/dialogs/path-listing.js +127 -0
- package/dist/cli/tui/dialogs/path-picker.d.ts +67 -0
- package/dist/cli/tui/dialogs/path-picker.js +423 -0
- package/dist/cli/tui/dialogs/text-screen.d.ts +6 -0
- package/dist/cli/tui/dialogs/text-screen.js +10 -3
- package/dist/cli/tui/frame.d.ts +7 -10
- package/dist/cli/tui/frame.js +19 -10
- package/dist/cli/tui/index.d.ts +4 -3
- package/dist/cli/tui/index.js +2 -1
- package/dist/cli/tui/list/list-screen.d.ts +14 -2
- package/dist/cli/tui/list/list-screen.js +68 -32
- package/dist/cli/tui/list/list-types.d.ts +8 -4
- package/dist/cli/tui/nav.d.ts +29 -0
- package/dist/cli/tui/nav.js +67 -0
- package/dist/cli/validate.d.ts +1 -1
- package/dist/cli/validate.js +3 -3
- package/dist/cli/version.d.ts +2 -0
- package/dist/cli/version.js +11 -0
- package/dist/core/config/defaults.js +5 -1
- package/dist/core/config/loader.js +12 -2
- package/dist/core/local-config/overrides.js +1 -0
- package/dist/core/prompt.d.ts +19 -0
- package/dist/core/prompt.js +43 -3
- package/dist/core/schema/common.d.ts +6 -0
- package/dist/core/schema/common.js +8 -0
- package/dist/core/schema/config.d.ts +23 -0
- package/dist/core/schema/config.js +10 -1
- package/dist/core/schema/harness.d.ts +1 -0
- package/dist/core/schema/harness.js +8 -2
- package/dist/core/schema/index.d.ts +3 -1
- package/dist/core/schema/index.js +3 -1
- package/dist/core/schema/profile.d.ts +1 -0
- package/dist/core/schema/profile.js +3 -0
- package/dist/core/schema/redact.d.ts +13 -0
- package/dist/core/schema/redact.js +75 -0
- package/dist/core/schema/sandbox.js +1 -0
- package/dist/core/schema/vpn.d.ts +17 -0
- package/dist/core/schema/vpn.js +22 -0
- package/dist/core/schema/web.d.ts +6 -0
- package/dist/core/schema/web.js +14 -0
- package/dist/core/types.d.ts +26 -0
- package/dist/guest/agents/agent-files.d.ts +49 -0
- package/dist/guest/agents/agent-files.js +63 -0
- package/dist/guest/agents/agent-installer.js +2 -9
- package/dist/guest/agents/local-state.d.ts +12 -0
- package/dist/guest/agents/local-state.js +198 -0
- package/dist/guest/agents/sbx-agent.d.ts +2 -0
- package/dist/guest/agents/sbx-agent.js +35 -0
- package/dist/guest/boot/boot.d.ts +4 -0
- package/dist/guest/boot/boot.js +50 -11
- package/dist/guest/daemon/daemon.js +4 -1
- package/dist/guest/daemon/exec.js +2 -1
- package/dist/guest/daemon/idle-shutdown.js +2 -0
- package/dist/guest/daemon/sessions.js +4 -1
- package/dist/guest/entries/agent.d.ts +1 -0
- package/dist/guest/entries/agent.js +10 -0
- package/dist/guest/entries/daemon.js +2 -0
- package/dist/guest/runtime/csc.d.ts +1 -0
- package/dist/guest/runtime/csc.js +1 -0
- package/dist/guest/runtime/secrets.d.ts +12 -0
- package/dist/guest/runtime/secrets.js +63 -0
- package/dist/guest/setup/claude-profile.d.ts +2 -1
- package/dist/guest/setup/claude-profile.js +12 -1
- package/dist/guest/setup/environment.d.ts +6 -0
- package/dist/guest/setup/environment.js +56 -6
- package/dist/guest/setup/path-plan.d.ts +32 -0
- package/dist/guest/setup/path-plan.js +67 -0
- package/dist/guest/setup/shell-path.d.ts +1 -0
- package/dist/guest/setup/shell-path.js +22 -0
- package/dist/guest/toolchains/languages/python.js +1 -1
- package/dist/guest/toolchains/runtimes.js +2 -2
- package/dist/guest/vpn/index.d.ts +1 -0
- package/dist/guest/vpn/index.js +1 -0
- package/dist/guest/vpn/openvpn.d.ts +9 -0
- package/dist/guest/vpn/openvpn.js +47 -0
- package/dist/guest/vpn/tunnel.d.ts +17 -0
- package/dist/guest/vpn/tunnel.js +78 -0
- package/dist/guest/vpn/vpn.d.ts +6 -0
- package/dist/guest/vpn/vpn.js +126 -0
- package/dist/host/channel/attach.d.ts +3 -1
- package/dist/host/channel/attach.js +1 -1
- package/dist/host/channel/channel.d.ts +7 -0
- package/dist/host/channel/channel.js +26 -2
- package/dist/host/health/acl-read.d.ts +17 -0
- package/dist/host/health/acl-read.js +45 -0
- package/dist/host/health/config-backup.d.ts +11 -0
- package/dist/host/health/config-backup.js +73 -0
- package/dist/host/health/index.d.ts +6 -0
- package/dist/host/health/index.js +7 -0
- package/dist/host/health/install-state.d.ts +33 -0
- package/dist/host/health/install-state.js +110 -0
- package/dist/host/health/path-copies.d.ts +32 -0
- package/dist/host/health/path-copies.js +82 -0
- package/dist/host/health/stale.d.ts +45 -0
- package/dist/host/health/stale.js +120 -0
- package/dist/host/health/system.d.ts +37 -0
- package/dist/host/health/system.js +87 -0
- package/dist/host/install/copies.d.ts +13 -0
- package/dist/host/install/copies.js +63 -0
- package/dist/host/install/index.d.ts +1 -0
- package/dist/host/install/index.js +1 -0
- package/dist/host/launch/generate.js +25 -1
- package/dist/host/launch/session-file.js +4 -1
- package/dist/host/launch/stage.js +1 -1
- package/dist/host/migrate/backup.js +3 -0
- package/dist/host/migrate/migrations/config-file.d.ts +2 -0
- package/dist/host/migrate/migrations/config-file.js +7 -0
- package/dist/host/migrate/migrations/follow-machine-storage.js +9 -1
- package/dist/host/migrate/migrations/pin-storage-root.d.ts +4 -0
- package/dist/host/migrate/migrations/pin-storage-root.js +46 -0
- package/dist/host/migrate/migrations/unpin-config-defaults.js +4 -1
- package/dist/host/migrate/registry.js +2 -1
- package/dist/host/profile/accounts.d.ts +25 -1
- package/dist/host/profile/accounts.js +96 -13
- package/dist/host/profile/agent-accounts.d.ts +5 -1
- package/dist/host/profile/agent-accounts.js +35 -5
- package/dist/host/profile/index.d.ts +2 -1
- package/dist/host/profile/index.js +1 -0
- package/dist/host/profile/plugin-share.d.ts +26 -0
- package/dist/host/profile/plugin-share.js +146 -0
- package/dist/host/projects/join.d.ts +7 -1
- package/dist/host/projects/join.js +21 -4
- package/dist/host/projects/registry.d.ts +16 -10
- package/dist/host/projects/registry.js +74 -6
- package/dist/host/sandbox/platform.d.ts +2 -0
- package/dist/host/sandbox/platform.js +21 -2
- package/dist/host/secrets/index.d.ts +2 -0
- package/dist/host/secrets/index.js +2 -0
- package/dist/host/secrets/launch-secrets.d.ts +17 -0
- package/dist/host/secrets/launch-secrets.js +93 -0
- package/dist/host/secrets/secret-store.d.ts +25 -0
- package/dist/host/secrets/secret-store.js +89 -0
- package/dist/host/sharing/acl.d.ts +16 -2
- package/dist/host/sharing/acl.js +35 -10
- package/dist/host/sharing/current.js +11 -2
- package/dist/host/sharing/doctor.d.ts +2 -0
- package/dist/host/sharing/doctor.js +44 -0
- package/dist/host/sharing/group-access.d.ts +6 -0
- package/dist/host/sharing/group-access.js +24 -0
- package/dist/host/sharing/index.d.ts +2 -1
- package/dist/host/sharing/index.js +2 -1
- package/dist/host/sharing/lifecycle.d.ts +8 -2
- package/dist/host/sharing/lifecycle.js +16 -7
- package/dist/host/storage/cleanup.d.ts +2 -0
- package/dist/host/storage/cleanup.js +2 -1
- package/dist/host/storage/history.d.ts +21 -0
- package/dist/host/storage/history.js +49 -0
- package/dist/host/storage/index.d.ts +4 -0
- package/dist/host/storage/index.js +4 -0
- package/dist/host/storage/move.d.ts +95 -0
- package/dist/host/storage/move.js +384 -0
- package/dist/host/storage/offer.d.ts +11 -0
- package/dist/host/storage/offer.js +36 -0
- package/dist/host/storage/root.d.ts +18 -0
- package/dist/host/storage/root.js +77 -0
- package/dist/host/tools/windows-terminal.d.ts +2 -0
- package/dist/host/tools/windows-terminal.js +23 -5
- package/dist/host/update/index.d.ts +1 -1
- package/dist/host/update/index.js +1 -1
- package/dist/host/update/notice.d.ts +2 -0
- package/dist/host/update/notice.js +13 -1
- package/dist/host/update/update-state.d.ts +6 -0
- package/dist/host/update/update-state.js +2 -0
- package/dist/host/update/updater.d.ts +19 -0
- package/dist/host/update/updater.js +88 -12
- package/dist/host/vpn/index.d.ts +4 -0
- package/dist/host/vpn/index.js +4 -0
- package/dist/host/vpn/login.d.ts +24 -0
- package/dist/host/vpn/login.js +79 -0
- package/dist/host/vpn/nordvpn.d.ts +21 -0
- package/dist/host/vpn/nordvpn.js +77 -0
- package/dist/host/vpn/prepare.d.ts +17 -0
- package/dist/host/vpn/prepare.js +77 -0
- package/dist/host/vpn/status.d.ts +12 -0
- package/dist/host/vpn/status.js +53 -0
- package/dist/host/web/auth.d.ts +38 -0
- package/dist/host/web/auth.js +96 -0
- package/dist/host/web/backend.d.ts +14 -0
- package/dist/host/web/backend.js +1 -0
- package/dist/host/web/bridge.d.ts +27 -0
- package/dist/host/web/bridge.js +144 -0
- package/dist/host/web/browser.d.ts +1 -0
- package/dist/host/web/browser.js +10 -0
- package/dist/host/web/control.d.ts +17 -0
- package/dist/host/web/control.js +67 -0
- package/dist/host/web/index.d.ts +9 -0
- package/dist/host/web/index.js +8 -0
- package/dist/host/web/runner.d.ts +14 -0
- package/dist/host/web/runner.js +45 -0
- package/dist/host/web/server.d.ts +40 -0
- package/dist/host/web/server.js +218 -0
- package/dist/host/web/state.d.ts +20 -0
- package/dist/host/web/state.js +41 -0
- package/dist/host/web/static-files.d.ts +13 -0
- package/dist/host/web/static-files.js +57 -0
- package/dist/runtime/agent.js +27614 -0
- package/dist/runtime/daemon.js +1012 -618
- package/dist/runtime/db.js +236 -106
- package/dist/runtime/main.js +1093 -250
- package/dist/runtime/netproxy.js +102 -3
- package/dist/runtime/view.js +102 -3
- package/dist/selftest/accounts.test.js +40 -0
- package/dist/selftest/agent-paths.test.d.ts +2 -0
- package/dist/selftest/agent-paths.test.js +115 -0
- package/dist/selftest/channel.test.js +5 -0
- package/dist/selftest/cli.test.js +34 -4
- package/dist/selftest/credentials.test.d.ts +2 -0
- package/dist/selftest/credentials.test.js +198 -0
- package/dist/selftest/generate.test.js +1 -1
- package/dist/selftest/health.test.d.ts +2 -0
- package/dist/selftest/health.test.js +470 -0
- package/dist/selftest/host-sim.test.js +2 -0
- package/dist/selftest/index.js +19 -0
- package/dist/selftest/layout.test.js +2 -0
- package/dist/selftest/menu-screens.d.ts +7 -0
- package/dist/selftest/menu-screens.js +65 -16
- package/dist/selftest/menu.test.js +574 -21
- package/dist/selftest/migrate.test.js +1 -1
- package/dist/selftest/personalization.test.js +3 -1
- package/dist/selftest/projects.test.js +39 -1
- package/dist/selftest/shared-use.test.js +64 -2
- package/dist/selftest/sharing.test.js +50 -1
- package/dist/selftest/storage.test.d.ts +2 -0
- package/dist/selftest/storage.test.js +363 -0
- package/dist/selftest/support/fixtures.d.ts +14 -0
- package/dist/selftest/support/releases.js +1 -1
- package/dist/selftest/toolchains.test.js +21 -0
- package/dist/selftest/update-service.test.js +87 -3
- package/dist/selftest/vpn.test.d.ts +2 -0
- package/dist/selftest/vpn.test.js +279 -0
- package/dist/selftest/web.test.d.ts +2 -0
- package/dist/selftest/web.test.js +304 -0
- package/dist/shared/apps/catalogue.js +31 -0
- package/dist/shared/channel/index.d.ts +1 -0
- package/dist/shared/channel/index.js +1 -0
- package/dist/shared/channel/run-dir.d.ts +6 -1
- package/dist/shared/channel/run-dir.js +12 -1
- package/dist/shared/channel/schema.d.ts +19 -0
- package/dist/shared/channel/schema.js +11 -0
- package/dist/shared/channel/secrets.d.ts +28 -0
- package/dist/shared/channel/secrets.js +44 -0
- package/dist/shared/vpn/download.d.ts +13 -0
- package/dist/shared/vpn/download.js +14 -0
- package/dist/shared/vpn/kill-switch.d.ts +18 -0
- package/dist/shared/vpn/kill-switch.js +81 -0
- package/dist/shared/vpn/ovpn.d.ts +23 -0
- package/dist/shared/vpn/ovpn.js +244 -0
- package/dist/shared/web/index.d.ts +1 -0
- package/dist/shared/web/index.js +1 -0
- package/dist/shared/web/protocol.d.ts +61 -0
- package/dist/shared/web/protocol.js +35 -0
- package/dist/sync/sync-cycle.js +5 -1
- package/dist/sync/sync-state.d.ts +1 -0
- package/dist/sync/sync-state.js +1 -0
- package/dist/sync/watcher.js +10 -0
- package/dist/web/index.html +13 -0
- package/dist/web/main-CXESL347.js +45 -0
- package/dist/web/main-VLR7DVQ4.css +1 -0
- package/native/csharp/FolderSecurity.cs +29 -0
- package/native/scripts/protect-data.ps1 +29 -0
- package/native/scripts/read-folder-sddl.ps1 +10 -0
- package/native/scripts/set-folder-access.ps1 +49 -7
- package/node_modules/ws/LICENSE +20 -0
- package/node_modules/ws/README.md +548 -0
- package/node_modules/ws/browser.js +8 -0
- package/node_modules/ws/index.js +22 -0
- package/node_modules/ws/lib/buffer-util.js +131 -0
- package/node_modules/ws/lib/constants.js +19 -0
- package/node_modules/ws/lib/event-target.js +292 -0
- package/node_modules/ws/lib/extension.js +203 -0
- package/node_modules/ws/lib/limiter.js +55 -0
- package/node_modules/ws/lib/permessage-deflate.js +530 -0
- package/node_modules/ws/lib/receiver.js +743 -0
- package/node_modules/ws/lib/sender.js +607 -0
- package/node_modules/ws/lib/stream.js +161 -0
- package/node_modules/ws/lib/subprotocol.js +62 -0
- package/node_modules/ws/lib/validation.js +152 -0
- package/node_modules/ws/lib/websocket-server.js +562 -0
- package/node_modules/ws/lib/websocket.js +1421 -0
- package/node_modules/ws/package.json +74 -0
- package/node_modules/ws/wrapper.mjs +21 -0
- package/package.json +15 -5
- package/sandbox/csharp/EnvBroadcast.cs +33 -0
- package/sandbox/csharp/PathSync.cs +90 -0
- package/sandbox/scripts/install-shell-path.ps1 +73 -0
- package/sandbox/scripts/set-machine-environment.ps1 +37 -4
- package/schemas/config.schema.json +79 -4
- package/schemas/sandbox-config.schema.json +14 -1
- package/dist/cli/commands/repair/checks.d.ts +0 -2
- package/dist/cli/commands/repair/checks.js +0 -246
- package/dist/cli/menu/pages/start-page.d.ts +0 -43
- package/dist/cli/menu/pages/start-page.js +0 -192
- package/dist/cli/tui/tab-bar.d.ts +0 -9
- package/dist/cli/tui/tab-bar.js +0 -33
|
@@ -7,24 +7,28 @@ import { z } from 'zod';
|
|
|
7
7
|
import { RunDir } from '../../shared/channel/index.js';
|
|
8
8
|
import { isWithin } from '../../shared/paths/index.js';
|
|
9
9
|
import { JsonFile } from '../../shared/io/index.js';
|
|
10
|
-
import { InstallLayout, InstallRecordFile } from '../install/index.js';
|
|
10
|
+
import { InstallLayout, InstallRecordFile, machineInstall } from '../install/index.js';
|
|
11
11
|
import { storageOf } from '../storage/index.js';
|
|
12
12
|
import { sharingAclPlan, applyAcl } from './acl.js';
|
|
13
13
|
import { ActiveRecords } from './active-record.js';
|
|
14
|
+
import { groupHasAccess } from './group-access.js';
|
|
14
15
|
import { activeDir, usersDir } from './machine-paths.js';
|
|
15
16
|
/**
|
|
16
|
-
* The machinery of
|
|
17
|
-
* storage folder).
|
|
17
|
+
* The machinery of the all-users install on this PC, or null when there is none (nothing is shared then: one user, one
|
|
18
|
+
* storage folder). It is found from any copy of the program: an npm copy or a checkout that starts a sandbox shares it
|
|
19
|
+
* the same way the installed one does, with the group and the settings the install recorded.
|
|
18
20
|
*/
|
|
19
21
|
export function sharingDeps(packageRoot) {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
+
const detected = InstallLayout.detect(packageRoot);
|
|
23
|
+
const layout = detected?.scope === 'machine' ? detected : machineInstall();
|
|
24
|
+
if (!layout)
|
|
22
25
|
return null;
|
|
23
26
|
return {
|
|
24
27
|
records: new ActiveRecords(activeDir()),
|
|
25
28
|
apply: applyAcl,
|
|
26
29
|
groupSid: new InstallRecordFile(layout.root).read()?.groupSid ?? null,
|
|
27
30
|
usersDir: usersDir(),
|
|
31
|
+
groupHasAccess,
|
|
28
32
|
};
|
|
29
33
|
}
|
|
30
34
|
/** Folder permissions change only with a group, and only inside the shared tree (never your own storage folder). */
|
|
@@ -41,12 +45,17 @@ export function beginSharing(deps, start, now = new Date()) {
|
|
|
41
45
|
const run = new RunDir(start.dataPath);
|
|
42
46
|
for (const dir of [run.root, run.requestsDir, run.responsesDir, run.sessionsDir, run.projectsDir, run.auditDir])
|
|
43
47
|
fs.mkdirSync(dir, { recursive: true });
|
|
44
|
-
|
|
48
|
+
const plan = sharingAclPlan({
|
|
45
49
|
storageUserDir: storageOf(start.dataPath),
|
|
46
50
|
dataPath: start.dataPath,
|
|
47
51
|
groupSid: deps.groupSid,
|
|
48
52
|
on: start.shared,
|
|
49
|
-
})
|
|
53
|
+
});
|
|
54
|
+
// Not shared and nothing of the group left to take away: nothing to write, so a slow folder cannot block the launch.
|
|
55
|
+
// When the entries cannot be read, or one is there, the removal runs and a failure of it refuses the launch.
|
|
56
|
+
if (!start.shared && deps.groupHasAccess && !deps.groupHasAccess(plan.map((c) => c.path)))
|
|
57
|
+
return;
|
|
58
|
+
deps.apply(plan);
|
|
50
59
|
}
|
|
51
60
|
/** The sandbox of this data folder ended: its record goes and the group loses access again. Never throws. */
|
|
52
61
|
export function endSharing(deps, dataPath) {
|
|
@@ -31,7 +31,8 @@ export function pruneStorage(storageRoot, options = {}) {
|
|
|
31
31
|
const result = { launches: 0, logs: 0, leftovers: 0, bytes: 0 };
|
|
32
32
|
const remove = (p, counter) => {
|
|
33
33
|
result.bytes += sizeOf(p);
|
|
34
|
-
|
|
34
|
+
if (!o.dryRun)
|
|
35
|
+
safeRemove(p, storageRoot);
|
|
35
36
|
result[counter]++;
|
|
36
37
|
};
|
|
37
38
|
for (const launch of children(path.join(storageRoot, 'launches'))) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** "Not now" is not asked again for this long. */
|
|
3
|
+
export declare const NOT_NOW_DAYS = 14;
|
|
4
|
+
declare const HistorySchema: z.ZodObject<{
|
|
5
|
+
previous: z.ZodArray<z.ZodString>;
|
|
6
|
+
dontAsk: z.ZodBoolean;
|
|
7
|
+
notNowAt: z.ZodNullable<z.ZodString>;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
export type StorageHistoryData = z.infer<typeof HistorySchema>;
|
|
10
|
+
export declare class StorageHistory {
|
|
11
|
+
private readonly store;
|
|
12
|
+
constructor(userDir: string);
|
|
13
|
+
read(): StorageHistoryData;
|
|
14
|
+
/** Remembers a folder that held (or holds) the data. */
|
|
15
|
+
remember(root: string): void;
|
|
16
|
+
setDontAsk(): void;
|
|
17
|
+
setNotNow(now?: Date): void;
|
|
18
|
+
/** The offer may be shown: not switched off and not postponed recently. */
|
|
19
|
+
mayAsk(now?: Date): boolean;
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// What this PC remembers about where the data was and whether to offer moving it: <userDir>\storage-history.json, kept
|
|
2
|
+
// outside the storage root (it has to be found when the root is empty or missing).
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { JsonFile } from '../../shared/io/index.js';
|
|
6
|
+
const MAX_PREVIOUS = 10;
|
|
7
|
+
/** "Not now" is not asked again for this long. */
|
|
8
|
+
export const NOT_NOW_DAYS = 14;
|
|
9
|
+
const HistorySchema = z.object({
|
|
10
|
+
/** Storage roots that held data before, newest first. */
|
|
11
|
+
previous: z.array(z.string().min(1).max(1024)).max(MAX_PREVIOUS),
|
|
12
|
+
/** "Don't ask again" for the offer to move the data to the default folder. */
|
|
13
|
+
dontAsk: z.boolean(),
|
|
14
|
+
/** ISO time of the last "Not now". */
|
|
15
|
+
notNowAt: z.string().nullable(),
|
|
16
|
+
});
|
|
17
|
+
const EMPTY = { previous: [], dontAsk: false, notNowAt: null };
|
|
18
|
+
export class StorageHistory {
|
|
19
|
+
store;
|
|
20
|
+
constructor(userDir) {
|
|
21
|
+
this.store = new JsonFile(HistorySchema, path.join(userDir, 'storage-history.json'));
|
|
22
|
+
}
|
|
23
|
+
read() {
|
|
24
|
+
return this.store.read() ?? EMPTY;
|
|
25
|
+
}
|
|
26
|
+
/** Remembers a folder that held (or holds) the data. */
|
|
27
|
+
remember(root) {
|
|
28
|
+
const key = path.resolve(root).toLowerCase();
|
|
29
|
+
const data = this.read();
|
|
30
|
+
const others = data.previous.filter((p) => path.resolve(p).toLowerCase() !== key);
|
|
31
|
+
this.store.write({ ...data, previous: [path.resolve(root), ...others].slice(0, MAX_PREVIOUS) });
|
|
32
|
+
}
|
|
33
|
+
setDontAsk() {
|
|
34
|
+
this.store.write({ ...this.read(), dontAsk: true });
|
|
35
|
+
}
|
|
36
|
+
setNotNow(now = new Date()) {
|
|
37
|
+
this.store.write({ ...this.read(), notNowAt: now.toISOString() });
|
|
38
|
+
}
|
|
39
|
+
/** The offer may be shown: not switched off and not postponed recently. */
|
|
40
|
+
mayAsk(now = new Date()) {
|
|
41
|
+
const data = this.read();
|
|
42
|
+
if (data.dontAsk)
|
|
43
|
+
return false;
|
|
44
|
+
if (!data.notNowAt)
|
|
45
|
+
return true;
|
|
46
|
+
const at = Date.parse(data.notNowAt);
|
|
47
|
+
return Number.isNaN(at) || now.getTime() - at > NOT_NOW_DAYS * 24 * 3600 * 1000;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** A copy is estimated at this speed, plus a cost per file (small files are slower than big ones). */
|
|
2
|
+
export declare const COPY_BYTES_PER_SECOND: number;
|
|
3
|
+
export declare const COPY_SECONDS_PER_FILE = 0.002;
|
|
4
|
+
/** Above these a move is called big: the dialog and the command warn. */
|
|
5
|
+
export declare const BIG_BYTES: number;
|
|
6
|
+
export declare const BIG_FILES = 200000;
|
|
7
|
+
/** Measuring stops after this long or this many entries and reports "at least". */
|
|
8
|
+
export declare const MEASURE_MS = 20000;
|
|
9
|
+
export declare const MEASURE_ENTRIES = 3000000;
|
|
10
|
+
export interface Measured {
|
|
11
|
+
readonly bytes: number;
|
|
12
|
+
readonly files: number;
|
|
13
|
+
/** The walk stopped at its limit: the real size is at least this. */
|
|
14
|
+
readonly partial: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface MoveEstimate {
|
|
17
|
+
/** The same drive: folders are renamed, nothing is copied. */
|
|
18
|
+
readonly sameVolume: boolean;
|
|
19
|
+
readonly seconds: number;
|
|
20
|
+
readonly freeBytes: number | null;
|
|
21
|
+
/** Room on the target drive (always true for a rename). */
|
|
22
|
+
readonly fits: boolean;
|
|
23
|
+
readonly big: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface MoveProgress {
|
|
26
|
+
readonly bytes: number;
|
|
27
|
+
readonly files: number;
|
|
28
|
+
}
|
|
29
|
+
export interface MoveOptions {
|
|
30
|
+
/** Copy even on the same drive (tests, and --keep-old which needs the old folder to stay). */
|
|
31
|
+
readonly forceCopy?: boolean;
|
|
32
|
+
readonly keepOld?: boolean;
|
|
33
|
+
readonly onProgress?: (progress: MoveProgress) => void;
|
|
34
|
+
}
|
|
35
|
+
export interface MoveResult {
|
|
36
|
+
readonly from: string;
|
|
37
|
+
readonly to: string;
|
|
38
|
+
readonly copied: boolean;
|
|
39
|
+
readonly files: number;
|
|
40
|
+
readonly bytes: number;
|
|
41
|
+
/** What was stored as storage.root. */
|
|
42
|
+
readonly pinned: string;
|
|
43
|
+
/** The old folder is gone (false with keepOld, when links were left in it, or when removing failed). */
|
|
44
|
+
readonly removedOld: boolean;
|
|
45
|
+
readonly notes: readonly string[];
|
|
46
|
+
}
|
|
47
|
+
/** Size and file count, never following links or junctions, with a time and entry limit. */
|
|
48
|
+
export declare function measureFolder(root: string, maxMs?: number, maxEntries?: number): Measured;
|
|
49
|
+
/** Do both folders (the target may not exist yet) sit on the same volume? */
|
|
50
|
+
export declare function sameVolume(from: string, to: string): boolean;
|
|
51
|
+
/** The pure part of the estimate: time, room and whether it is big. `freeBytes` null = unknown (assumed enough). */
|
|
52
|
+
export declare function estimateMove(measured: Measured, same: boolean, freeBytes: number | null): MoveEstimate;
|
|
53
|
+
export declare function estimate(from: string, to: string, measured: Measured, copyOnly?: boolean): MoveEstimate;
|
|
54
|
+
/** "about 3 minutes", "a few seconds". */
|
|
55
|
+
export declare function describeDuration(seconds: number): string;
|
|
56
|
+
/** "12.3 GB, 412,000 files", with "at least" when the measuring stopped early. */
|
|
57
|
+
export declare function describeSize(measured: Measured): string;
|
|
58
|
+
/** A warning line for a big or too-big move, or null. */
|
|
59
|
+
export declare function moveWarning(measured: Measured, est: MoveEstimate): string | null;
|
|
60
|
+
/** What to store in config.json for this folder: the %VARIABLE% form when it is a default, else the full path. */
|
|
61
|
+
export declare function pinValue(folder: string, env?: NodeJS.ProcessEnv): string;
|
|
62
|
+
/** Why `from` -> `to` is not allowed, or null. `allowNonEmpty`: the target may hold data already (switching to it). */
|
|
63
|
+
export declare function moveProblem(from: string, to: string, allowNonEmpty?: boolean): {
|
|
64
|
+
message: string;
|
|
65
|
+
fix: string;
|
|
66
|
+
} | null;
|
|
67
|
+
export declare class StorageMove {
|
|
68
|
+
private readonly configFile;
|
|
69
|
+
private readonly busy;
|
|
70
|
+
/** Whether a sandbox runs on this PC (given by the caller: host/sandbox imports this folder). */
|
|
71
|
+
private readonly sandboxRunning;
|
|
72
|
+
private readonly protect?;
|
|
73
|
+
private readonly env;
|
|
74
|
+
/**
|
|
75
|
+
* @param configFile the user's config.json (created if needed): storage.root is pinned in it
|
|
76
|
+
* @param busy why a move cannot happen now (a background job on the old folder), or null
|
|
77
|
+
*/
|
|
78
|
+
constructor(configFile: string, busy: () => string | null,
|
|
79
|
+
/** Whether a sandbox runs on this PC (given by the caller: host/sandbox imports this folder). */
|
|
80
|
+
sandboxRunning: () => boolean, protect?: ((dir: string) => void) | undefined, env?: NodeJS.ProcessEnv);
|
|
81
|
+
/** Throws a UserError when this move is not possible right now. */
|
|
82
|
+
validate(from: string, to: string): void;
|
|
83
|
+
move(from: string, to: string, options?: MoveOptions): Promise<MoveResult>;
|
|
84
|
+
/** Writes storage.root without moving anything (sandbox storage use). */
|
|
85
|
+
pin(target: string): void;
|
|
86
|
+
/** Creates the target, private when it is under the users folder, before anything is written in it. */
|
|
87
|
+
private prepare;
|
|
88
|
+
private rename;
|
|
89
|
+
private undoRename;
|
|
90
|
+
private copy;
|
|
91
|
+
/** Failure cleanup: removes what a failed copy left, never the old folder. */
|
|
92
|
+
private discard;
|
|
93
|
+
private removeOld;
|
|
94
|
+
private removeEmpty;
|
|
95
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
// Moving the storage root (everything kept between sandboxes) to another folder: measuring, estimating, checking, and
|
|
2
|
+
// the move itself (rename on the same drive, a verified copy otherwise). The folder is pinned as storage.root in the
|
|
3
|
+
// user's config at the end, so an older version still finds it. Returns data and calls back; never prints.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { pipeline } from 'node:stream/promises';
|
|
7
|
+
import { DEFAULTS } from '../../core/config/index.js';
|
|
8
|
+
import { freeBytesAt, GB, humanBytes } from '../../core/disk.js';
|
|
9
|
+
import { errMsg, expandPath, fail, isInsideOrEqual, isLink, isRootPath } from '../../core/util/index.js';
|
|
10
|
+
import { safeRemove } from '../../shared/io/index.js';
|
|
11
|
+
import { setValue } from '../migrate/migrations/config-file.js';
|
|
12
|
+
import { ensureStorageRoot, OLD_DEFAULT_ROOT, oldDefaultRoot } from './root.js';
|
|
13
|
+
/** A copy is estimated at this speed, plus a cost per file (small files are slower than big ones). */
|
|
14
|
+
export const COPY_BYTES_PER_SECOND = 80 * 1024 * 1024;
|
|
15
|
+
export const COPY_SECONDS_PER_FILE = 0.002;
|
|
16
|
+
/** Above these a move is called big: the dialog and the command warn. */
|
|
17
|
+
export const BIG_BYTES = 5 * GB;
|
|
18
|
+
export const BIG_FILES = 200_000;
|
|
19
|
+
/** Measuring stops after this long or this many entries and reports "at least". */
|
|
20
|
+
export const MEASURE_MS = 20_000;
|
|
21
|
+
export const MEASURE_ENTRIES = 3_000_000;
|
|
22
|
+
/** Files from this size on are copied as a stream, so progress moves while one big file copies. */
|
|
23
|
+
const STREAM_FROM_BYTES = 64 * 1024 * 1024;
|
|
24
|
+
/** Used when no way to make a folder private was given: a shared-tree target is refused rather than left open. */
|
|
25
|
+
const refuseUnprotected = (dir) => fail('Cannot make ' + dir + ' private here.', 'Move the data with sandbox storage move on your PC.');
|
|
26
|
+
/** Size and file count, never following links or junctions, with a time and entry limit. */
|
|
27
|
+
export function measureFolder(root, maxMs = MEASURE_MS, maxEntries = MEASURE_ENTRIES) {
|
|
28
|
+
const deadline = Date.now() + maxMs;
|
|
29
|
+
let bytes = 0;
|
|
30
|
+
let files = 0;
|
|
31
|
+
let entries = 0;
|
|
32
|
+
const stack = [root];
|
|
33
|
+
while (stack.length) {
|
|
34
|
+
const dir = stack.pop();
|
|
35
|
+
let list;
|
|
36
|
+
try {
|
|
37
|
+
list = fs.readdirSync(dir, { withFileTypes: true });
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
continue; // gone or unreadable: counts as nothing
|
|
41
|
+
}
|
|
42
|
+
for (const entry of list) {
|
|
43
|
+
if (++entries > maxEntries || (entries % 512 === 0 && Date.now() > deadline))
|
|
44
|
+
return { bytes, files, partial: true };
|
|
45
|
+
if (entry.isSymbolicLink())
|
|
46
|
+
continue;
|
|
47
|
+
const full = path.join(dir, entry.name);
|
|
48
|
+
if (entry.isDirectory()) {
|
|
49
|
+
stack.push(full);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
bytes += fs.lstatSync(full).size;
|
|
54
|
+
files++;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// vanished while measuring
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { bytes, files, partial: false };
|
|
62
|
+
}
|
|
63
|
+
/** Do both folders (the target may not exist yet) sit on the same volume? */
|
|
64
|
+
export function sameVolume(from, to) {
|
|
65
|
+
const near = (p) => {
|
|
66
|
+
let at = path.resolve(p);
|
|
67
|
+
while (!fs.existsSync(at) && path.dirname(at) !== at)
|
|
68
|
+
at = path.dirname(at);
|
|
69
|
+
return at;
|
|
70
|
+
};
|
|
71
|
+
const a = near(from);
|
|
72
|
+
const b = near(to);
|
|
73
|
+
if (path.parse(a).root.toLowerCase() !== path.parse(b).root.toLowerCase())
|
|
74
|
+
return false;
|
|
75
|
+
try {
|
|
76
|
+
return fs.statSync(a).dev === fs.statSync(b).dev;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** The pure part of the estimate: time, room and whether it is big. `freeBytes` null = unknown (assumed enough). */
|
|
83
|
+
export function estimateMove(measured, same, freeBytes) {
|
|
84
|
+
const seconds = same
|
|
85
|
+
? Math.max(1, Math.ceil(measured.files / 50_000))
|
|
86
|
+
: Math.ceil(measured.bytes / COPY_BYTES_PER_SECOND + measured.files * COPY_SECONDS_PER_FILE);
|
|
87
|
+
const fits = same || freeBytes === null || freeBytes > measured.bytes * 1.02 + 64 * 1024 * 1024;
|
|
88
|
+
return {
|
|
89
|
+
sameVolume: same,
|
|
90
|
+
seconds,
|
|
91
|
+
freeBytes,
|
|
92
|
+
fits,
|
|
93
|
+
big: measured.bytes > BIG_BYTES || measured.files > BIG_FILES,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export function estimate(from, to, measured, copyOnly = false) {
|
|
97
|
+
const same = !copyOnly && sameVolume(from, to);
|
|
98
|
+
return estimateMove(measured, same, same ? null : freeBytesAt(to));
|
|
99
|
+
}
|
|
100
|
+
/** "about 3 minutes", "a few seconds". */
|
|
101
|
+
export function describeDuration(seconds) {
|
|
102
|
+
if (seconds < 10)
|
|
103
|
+
return 'a few seconds';
|
|
104
|
+
if (seconds < 90)
|
|
105
|
+
return 'about ' + Math.round(seconds / 10) * 10 + ' seconds';
|
|
106
|
+
const minutes = Math.round(seconds / 60);
|
|
107
|
+
if (minutes < 90)
|
|
108
|
+
return 'about ' + minutes + ' minutes';
|
|
109
|
+
const hours = Math.round(seconds / 360) / 10;
|
|
110
|
+
return 'about ' + hours + ' hours';
|
|
111
|
+
}
|
|
112
|
+
/** "12.3 GB, 412,000 files", with "at least" when the measuring stopped early. */
|
|
113
|
+
export function describeSize(measured) {
|
|
114
|
+
return ((measured.partial ? 'at least ' : '') +
|
|
115
|
+
humanBytes(measured.bytes) +
|
|
116
|
+
', ' +
|
|
117
|
+
measured.files.toLocaleString('en-US') +
|
|
118
|
+
' files');
|
|
119
|
+
}
|
|
120
|
+
/** A warning line for a big or too-big move, or null. */
|
|
121
|
+
export function moveWarning(measured, est) {
|
|
122
|
+
if (!est.fits)
|
|
123
|
+
return ('The target drive has ' +
|
|
124
|
+
(est.freeBytes === null ? 'too little' : humanBytes(est.freeBytes)) +
|
|
125
|
+
' free; this needs ' +
|
|
126
|
+
humanBytes(measured.bytes) +
|
|
127
|
+
' and some room to spare.');
|
|
128
|
+
if (est.big)
|
|
129
|
+
return ('This is a lot of data (' + describeSize(measured) + '): the move takes ' + describeDuration(est.seconds) + '.');
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
/** What to store in config.json for this folder: the %VARIABLE% form when it is a default, else the full path. */
|
|
133
|
+
export function pinValue(folder, env = process.env) {
|
|
134
|
+
const same = (a, b) => path.resolve(a).toLowerCase() === path.resolve(b).toLowerCase();
|
|
135
|
+
const shared = expandPath(DEFAULTS.storage.root, process.cwd());
|
|
136
|
+
if (same(folder, shared))
|
|
137
|
+
return DEFAULTS.storage.root;
|
|
138
|
+
const old = oldDefaultRoot(env);
|
|
139
|
+
if (old && same(folder, old))
|
|
140
|
+
return OLD_DEFAULT_ROOT;
|
|
141
|
+
return path.resolve(folder);
|
|
142
|
+
}
|
|
143
|
+
/** Why `from` -> `to` is not allowed, or null. `allowNonEmpty`: the target may hold data already (switching to it). */
|
|
144
|
+
export function moveProblem(from, to, allowNonEmpty = false) {
|
|
145
|
+
const a = path.resolve(from);
|
|
146
|
+
const b = path.resolve(to);
|
|
147
|
+
if (!path.isAbsolute(to))
|
|
148
|
+
return { message: to + ' is not a full path.', fix: 'Give a path like D:\\Sandbox.' };
|
|
149
|
+
if (isRootPath(b))
|
|
150
|
+
return { message: b + ' is a whole drive.', fix: 'Pick a folder on it, e.g. ' + b + 'Sandbox.' };
|
|
151
|
+
if (a.toLowerCase() === b.toLowerCase())
|
|
152
|
+
return { message: 'Your data is in that folder already.', fix: '' };
|
|
153
|
+
if (isInsideOrEqual(a, b))
|
|
154
|
+
return { message: 'The new folder is inside the old one.', fix: 'Pick a folder outside ' + a + '.' };
|
|
155
|
+
if (isInsideOrEqual(b, a))
|
|
156
|
+
return { message: 'The old folder is inside the new one.', fix: 'Pick a folder that does not contain ' + a + '.' };
|
|
157
|
+
if (isLink(b) || isLink(path.dirname(b)))
|
|
158
|
+
return { message: b + ' (or the folder above it) is a link.', fix: 'Pick a plain folder.' };
|
|
159
|
+
if (fs.existsSync(b)) {
|
|
160
|
+
let entries = [];
|
|
161
|
+
try {
|
|
162
|
+
if (!fs.statSync(b).isDirectory())
|
|
163
|
+
return { message: b + ' is a file.', fix: 'Pick a folder.' };
|
|
164
|
+
entries = fs.readdirSync(b);
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
return { message: 'Cannot look into ' + b + ': ' + errMsg(e), fix: 'Pick another folder.' };
|
|
168
|
+
}
|
|
169
|
+
if (entries.length && !allowNonEmpty)
|
|
170
|
+
return { message: b + ' is not empty.', fix: 'Pick an empty or new folder.' };
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
export class StorageMove {
|
|
175
|
+
configFile;
|
|
176
|
+
busy;
|
|
177
|
+
sandboxRunning;
|
|
178
|
+
protect;
|
|
179
|
+
env;
|
|
180
|
+
/**
|
|
181
|
+
* @param configFile the user's config.json (created if needed): storage.root is pinned in it
|
|
182
|
+
* @param busy why a move cannot happen now (a background job on the old folder), or null
|
|
183
|
+
*/
|
|
184
|
+
constructor(configFile, busy,
|
|
185
|
+
/** Whether a sandbox runs on this PC (given by the caller: host/sandbox imports this folder). */
|
|
186
|
+
sandboxRunning, protect, env = process.env) {
|
|
187
|
+
this.configFile = configFile;
|
|
188
|
+
this.busy = busy;
|
|
189
|
+
this.sandboxRunning = sandboxRunning;
|
|
190
|
+
this.protect = protect;
|
|
191
|
+
this.env = env;
|
|
192
|
+
}
|
|
193
|
+
/** Throws a UserError when this move is not possible right now. */
|
|
194
|
+
validate(from, to) {
|
|
195
|
+
if (!fs.existsSync(from) || !fs.statSync(from).isDirectory())
|
|
196
|
+
fail('There is no folder to move: ' + from + ' does not exist.');
|
|
197
|
+
if (isLink(from))
|
|
198
|
+
fail(from + ' is a link.', 'Move the real folder, not a link.');
|
|
199
|
+
const problem = moveProblem(from, to);
|
|
200
|
+
if (problem)
|
|
201
|
+
fail(problem.message, problem.fix || undefined);
|
|
202
|
+
if (this.sandboxRunning())
|
|
203
|
+
fail('A Windows Sandbox is running.', 'Close it (sandbox stop), then move your data.');
|
|
204
|
+
const why = this.busy();
|
|
205
|
+
if (why)
|
|
206
|
+
fail(why, 'Wait a minute, then try again.');
|
|
207
|
+
}
|
|
208
|
+
async move(from, to, options = {}) {
|
|
209
|
+
this.validate(from, to);
|
|
210
|
+
const target = path.resolve(to);
|
|
211
|
+
const source = path.resolve(from);
|
|
212
|
+
const measured = measureFolder(source);
|
|
213
|
+
const est = estimate(source, target, measured, Boolean(options.forceCopy || options.keepOld));
|
|
214
|
+
if (!est.fits)
|
|
215
|
+
fail(moveWarning(measured, est) ?? 'Not enough room.', 'Free some space or pick another drive.');
|
|
216
|
+
const existedBefore = fs.existsSync(target);
|
|
217
|
+
const notes = [];
|
|
218
|
+
let copiedFiles = measured.files;
|
|
219
|
+
let copiedBytes = measured.bytes;
|
|
220
|
+
let skippedLinks = 0;
|
|
221
|
+
let renamed = false;
|
|
222
|
+
try {
|
|
223
|
+
this.prepare(target);
|
|
224
|
+
if (est.sameVolume) {
|
|
225
|
+
this.rename(source, target);
|
|
226
|
+
renamed = true;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
const done = await this.copy(source, target, options.onProgress);
|
|
230
|
+
copiedFiles = done.files;
|
|
231
|
+
copiedBytes = done.bytes;
|
|
232
|
+
skippedLinks = done.links;
|
|
233
|
+
}
|
|
234
|
+
this.pin(target);
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
// Back to how it was: the old folder is whole again, the config is untouched, what was made in the target goes.
|
|
238
|
+
if (renamed)
|
|
239
|
+
this.undoRename(source, target);
|
|
240
|
+
this.discard(target, existedBefore);
|
|
241
|
+
throw e;
|
|
242
|
+
}
|
|
243
|
+
let removedOld = false;
|
|
244
|
+
if (est.sameVolume)
|
|
245
|
+
removedOld = this.removeEmpty(source, notes);
|
|
246
|
+
else if (options.keepOld)
|
|
247
|
+
notes.push('The old folder is kept: ' + source);
|
|
248
|
+
else if (skippedLinks)
|
|
249
|
+
notes.push('The old folder is kept because it holds ' + skippedLinks + ' link(s): ' + source);
|
|
250
|
+
else
|
|
251
|
+
removedOld = this.removeOld(source, notes);
|
|
252
|
+
return {
|
|
253
|
+
from: source,
|
|
254
|
+
to: target,
|
|
255
|
+
copied: !est.sameVolume,
|
|
256
|
+
files: copiedFiles,
|
|
257
|
+
bytes: copiedBytes,
|
|
258
|
+
pinned: pinValue(target, this.env),
|
|
259
|
+
removedOld,
|
|
260
|
+
notes,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/** Writes storage.root without moving anything (sandbox storage use). */
|
|
264
|
+
pin(target) {
|
|
265
|
+
setValue(this.configFile, ['storage', 'root'], pinValue(target, this.env));
|
|
266
|
+
}
|
|
267
|
+
/** Creates the target, private when it is under the users folder, before anything is written in it. */
|
|
268
|
+
prepare(target) {
|
|
269
|
+
ensureStorageRoot(target, this.protect ?? refuseUnprotected, this.env);
|
|
270
|
+
}
|
|
271
|
+
rename(source, target) {
|
|
272
|
+
const moved = [];
|
|
273
|
+
try {
|
|
274
|
+
for (const name of fs.readdirSync(source)) {
|
|
275
|
+
fs.renameSync(path.join(source, name), path.join(target, name));
|
|
276
|
+
moved.push(name);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch (e) {
|
|
280
|
+
for (const name of moved.reverse()) {
|
|
281
|
+
try {
|
|
282
|
+
fs.renameSync(path.join(target, name), path.join(source, name));
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// best effort: the message below says where the rest is
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
fail('Moving stopped: ' + errMsg(e), 'Nothing was changed. Close programs that use ' + source + ' and try again.');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
undoRename(source, target) {
|
|
292
|
+
for (const name of fs.readdirSync(target)) {
|
|
293
|
+
try {
|
|
294
|
+
fs.renameSync(path.join(target, name), path.join(source, name));
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// best effort
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async copy(source, target, onProgress) {
|
|
302
|
+
const done = { bytes: 0, files: 0, links: 0 };
|
|
303
|
+
let lastReport = 0;
|
|
304
|
+
const report = (force = false) => {
|
|
305
|
+
if (!onProgress || (!force && Date.now() - lastReport < 200))
|
|
306
|
+
return;
|
|
307
|
+
lastReport = Date.now();
|
|
308
|
+
onProgress({ bytes: done.bytes, files: done.files });
|
|
309
|
+
};
|
|
310
|
+
const walk = async (from, to) => {
|
|
311
|
+
await fs.promises.mkdir(to, { recursive: true });
|
|
312
|
+
for (const entry of await fs.promises.readdir(from, { withFileTypes: true })) {
|
|
313
|
+
const a = path.join(from, entry.name);
|
|
314
|
+
const b = path.join(to, entry.name);
|
|
315
|
+
if (entry.isSymbolicLink()) {
|
|
316
|
+
done.links++;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (entry.isDirectory()) {
|
|
320
|
+
await walk(a, b);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const stat = await fs.promises.stat(a);
|
|
324
|
+
if (stat.size >= STREAM_FROM_BYTES) {
|
|
325
|
+
const before = done.bytes;
|
|
326
|
+
const input = fs.createReadStream(a);
|
|
327
|
+
input.on('data', () => {
|
|
328
|
+
done.bytes = before + input.bytesRead;
|
|
329
|
+
report();
|
|
330
|
+
});
|
|
331
|
+
await pipeline(input, fs.createWriteStream(b));
|
|
332
|
+
done.bytes = before;
|
|
333
|
+
}
|
|
334
|
+
else
|
|
335
|
+
await fs.promises.copyFile(a, b);
|
|
336
|
+
const copied = await fs.promises.stat(b);
|
|
337
|
+
if (copied.size !== stat.size)
|
|
338
|
+
fail('The copy of ' + a + ' has another size than the original.');
|
|
339
|
+
await fs.promises.utimes(b, stat.atime, stat.mtime);
|
|
340
|
+
done.bytes += stat.size;
|
|
341
|
+
done.files++;
|
|
342
|
+
report();
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
await walk(source, target);
|
|
346
|
+
report(true);
|
|
347
|
+
return done;
|
|
348
|
+
}
|
|
349
|
+
/** Failure cleanup: removes what a failed copy left, never the old folder. */
|
|
350
|
+
discard(target, existedBefore) {
|
|
351
|
+
try {
|
|
352
|
+
if (!existedBefore)
|
|
353
|
+
safeRemove(target, path.dirname(target));
|
|
354
|
+
else
|
|
355
|
+
for (const name of fs.readdirSync(target))
|
|
356
|
+
safeRemove(path.join(target, name), target);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
// the original error is the one to show; a leftover partial folder is only wasted space
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
removeOld(source, notes) {
|
|
363
|
+
try {
|
|
364
|
+
safeRemove(source, path.dirname(source));
|
|
365
|
+
if (!fs.existsSync(source))
|
|
366
|
+
return true;
|
|
367
|
+
notes.push('Some of the old folder could not be removed: ' + source);
|
|
368
|
+
}
|
|
369
|
+
catch (e) {
|
|
370
|
+
notes.push('The old folder could not be removed (' + errMsg(e) + '): ' + source);
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
removeEmpty(source, notes) {
|
|
375
|
+
try {
|
|
376
|
+
fs.rmdirSync(source);
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
catch (e) {
|
|
380
|
+
notes.push('The old folder is still there (' + errMsg(e) + '): ' + source);
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { StorageHistory } from './history.js';
|
|
2
|
+
export interface MoveOffer {
|
|
3
|
+
readonly from: string;
|
|
4
|
+
readonly to: string;
|
|
5
|
+
/** `to` is the shared default folder (the wording says "the default folder"). */
|
|
6
|
+
readonly toIsDefault: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Folders that may hold saved data, other than `current`: the old default, the shared default, the remembered ones. */
|
|
9
|
+
export declare function otherDataFolders(current: string, history: StorageHistory, env?: NodeJS.ProcessEnv): string[];
|
|
10
|
+
/** What to offer, or null (nothing to move, the user said no, or the move would not be allowed anyway). */
|
|
11
|
+
export declare function planOffer(current: string, history: StorageHistory, env?: NodeJS.ProcessEnv): MoveOffer | null;
|