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
|
@@ -1,32 +1,40 @@
|
|
|
1
|
-
// The menu's pure parts: the
|
|
2
|
-
// the
|
|
1
|
+
// The menu's pure parts: the navigation on the left and the key bar at every width, the shell's keys, the dialog
|
|
2
|
+
// box, the Draft of unsaved changes, and the sections' lists. With MENU_SCREENS_OUT set it also writes every screen as
|
|
3
|
+
// text there, for review.
|
|
3
4
|
import fs from 'node:fs';
|
|
4
5
|
import { ConfigFile } from '../cli/menu/settings/config-file.js';
|
|
5
6
|
import { Draft } from '../cli/menu/settings/draft.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { AddAgent, agentNameProblem, commandFromPackage, makeDefaultAgent, newAgent, removeAgent, } from '../cli/menu/add-agent.js';
|
|
9
|
+
import { APP_GROUPS, AppsModel } from '../cli/menu/apps.js';
|
|
10
|
+
import { argPathKind, optionLabel, optionPathKind, plainDescription } from '../cli/menu/command-fields.js';
|
|
11
|
+
import { CommandForm } from '../cli/menu/command-form.js';
|
|
12
|
+
import { COMMANDS, commandSpec } from '../cli/manifest.js';
|
|
13
|
+
import { accountsOptions } from '../cli/menu/pages/accounts-page.js';
|
|
14
|
+
import { addedAgents, agentActions, agentCategories, agentsOptions, hostAgentsToShow, startArgv, } from '../cli/menu/pages/agents-page.js';
|
|
15
|
+
import { ListPage } from '../cli/menu/pages/list-page.js';
|
|
16
|
+
import { sandboxOptions } from '../cli/menu/pages/sandbox-page.js';
|
|
17
|
+
import { sessionsOptions } from '../cli/menu/pages/sessions-page.js';
|
|
18
|
+
import { pathKindOf } from '../cli/menu/settings/path-kind.js';
|
|
19
|
+
import { SchemaNode } from '../cli/menu/settings/schema-node.js';
|
|
20
|
+
import { numberProblem, ValueEditor } from '../cli/menu/settings/value-editor.js';
|
|
21
|
+
import { APP_CATEGORIES } from '../shared/apps/index.js';
|
|
22
|
+
import { settingsOf, settingsOptions } from '../cli/menu/pages/settings-page.js';
|
|
23
|
+
import { CATEGORIES, SETTINGS_ELSEWHERE } from '../cli/menu/settings/index.js';
|
|
24
|
+
import { SECTION_LABELS, Shell } from '../cli/menu/shell.js';
|
|
8
25
|
import { DEFAULTS } from '../core/config/index.js';
|
|
9
26
|
import { HarnessSchema } from '../core/schema/index.js';
|
|
10
27
|
import { profileLinkPairs } from '../shared/paths/index.js';
|
|
11
28
|
import { AccountsService, parseDatabaseList } from '../cli/services/index.js';
|
|
12
|
-
import { dialogBox, KeyBar, overlayBox,
|
|
13
|
-
import { renderScreens } from './menu-screens.js';
|
|
29
|
+
import { App, dialogBox, Frame, KeyBar, ListScreen, overlayBox, SideNav, Text, } from '../cli/tui/index.js';
|
|
30
|
+
import { renderScreens, SIZES } from './menu-screens.js';
|
|
14
31
|
const WIDTHS = [120, 80, 40];
|
|
15
32
|
export const menuSuite = {
|
|
16
33
|
name: 'menu',
|
|
17
34
|
title: 'menu tabs, key bar, dialogs and unsaved changes',
|
|
18
35
|
run: async (t) => {
|
|
19
|
-
t.section('
|
|
20
|
-
const
|
|
21
|
-
const tooWide = [];
|
|
22
|
-
for (const width of WIDTHS)
|
|
23
|
-
for (const active of [0, 3, 6]) {
|
|
24
|
-
const line = TabBar.render(tabs, active, width);
|
|
25
|
-
if (Text.width(line) > width || !Text.strip(line).includes(TAB_LABELS[active]))
|
|
26
|
-
tooWide.push(width + '/' + active);
|
|
27
|
-
}
|
|
28
|
-
t.equal(tooWide, [], 'the tab bar fits at every width and names the open tab');
|
|
29
|
-
const keys = [
|
|
36
|
+
t.section('key bar');
|
|
37
|
+
const hints = [
|
|
30
38
|
['ctrl+s', 'save (2)', 0],
|
|
31
39
|
['↵', 'edit', 1],
|
|
32
40
|
['r', 'reset', 2],
|
|
@@ -35,11 +43,548 @@ export const menuSuite = {
|
|
|
35
43
|
['/', 'search', 6],
|
|
36
44
|
['esc', 'quit', 0],
|
|
37
45
|
];
|
|
38
|
-
const bar = (width) => Text.strip(KeyBar.render(
|
|
46
|
+
const bar = (width) => Text.strip(KeyBar.render(hints, width));
|
|
39
47
|
t.check(WIDTHS.every((w) => Text.width(bar(w)) <= w && bar(w).includes('quit') && bar(w).includes('save (2)')) &&
|
|
40
48
|
!bar(60).includes('search') &&
|
|
41
49
|
bar(60).includes('edit') &&
|
|
42
50
|
bar(200).includes('search'), 'the key bar fits, keeps save and the way out, and drops the least important keys first');
|
|
51
|
+
t.section('navigation on the left');
|
|
52
|
+
const navState = (active, focused, dimmed = false) => ({
|
|
53
|
+
sections: SECTION_LABELS.map((label, i) => ({
|
|
54
|
+
label,
|
|
55
|
+
marked: label === 'Settings',
|
|
56
|
+
categories: i === active
|
|
57
|
+
? ['General', 'Languages & tools', 'Network'].map((l, c) => ({ label: l, current: c === 1 }))
|
|
58
|
+
: [],
|
|
59
|
+
})),
|
|
60
|
+
active,
|
|
61
|
+
focused,
|
|
62
|
+
dimmed,
|
|
63
|
+
});
|
|
64
|
+
const navOdd = [];
|
|
65
|
+
for (const width of [40, 79, 80, 120, 200])
|
|
66
|
+
for (const height of [8, 18, 54])
|
|
67
|
+
for (const focused of [true, false]) {
|
|
68
|
+
const lines = SideNav.render(navState(4, focused), width, height);
|
|
69
|
+
if (lines.length !== height || lines.some((l) => Text.width(l) !== SideNav.width(width)))
|
|
70
|
+
navOdd.push(width + 'x' + height);
|
|
71
|
+
if (!lines.some((l) => Text.strip(l).includes(width < 80 ? '●' : 'Languages')))
|
|
72
|
+
navOdd.push('hidden ' + width + 'x' + height);
|
|
73
|
+
}
|
|
74
|
+
t.equal(navOdd, [], 'the navigation has the width and height asked for, and always shows the open category');
|
|
75
|
+
const wide = SideNav.render(navState(4, true), 100, 20).map((l) => Text.strip(l).trimEnd());
|
|
76
|
+
const slim = SideNav.render(navState(4, true), 60, 20).map((l) => Text.strip(l).trimEnd());
|
|
77
|
+
t.check(wide[0].includes('1 Agents') &&
|
|
78
|
+
wide[4].startsWith('▾5 Settings') &&
|
|
79
|
+
wide[4].endsWith('✎') &&
|
|
80
|
+
wide[5].includes('├ General') &&
|
|
81
|
+
wide[6].includes('› Languages & tools') &&
|
|
82
|
+
wide[7].includes('└ Network') &&
|
|
83
|
+
wide[8].includes('6 Accounts'), 'wide: the sections are numbered, the open one is a tree, the open category has a marker, unsaved sections a mark', wide.slice(0, 9).join('|'));
|
|
84
|
+
t.check(!slim.join('').includes('Agents') && slim[0].includes('1') && slim[4].startsWith('▾5') && slim[6].includes('●'), 'narrow (under 80 columns): numbers only; the open section keeps dots for its categories', slim.slice(0, 9).join('|'));
|
|
85
|
+
const bodyOdd = [];
|
|
86
|
+
for (const size of [
|
|
87
|
+
{ width: 80, height: 24 },
|
|
88
|
+
{ width: 200, height: 60 },
|
|
89
|
+
]) {
|
|
90
|
+
const nav = navState(4, true);
|
|
91
|
+
const row = 'x'.repeat(Frame.bodyWidth(size.width, nav) - 1);
|
|
92
|
+
const body = Array.from({ length: Frame.bodyHeight(size.height) }, () => row);
|
|
93
|
+
const rows = Frame.render({ path: ['Settings'], body, keys: [['esc', 'quit', 0]], nav }, size.width, size.height);
|
|
94
|
+
if (rows.length !== size.height || rows.some((r) => Text.width(r) > size.width - 1))
|
|
95
|
+
bodyOdd.push(size.width + 'x' + size.height);
|
|
96
|
+
}
|
|
97
|
+
t.equal(bodyOdd, [], 'the frame fits the navigation, its divider and the body in the terminal');
|
|
98
|
+
t.section('navigation keys (Shell)');
|
|
99
|
+
const press = (name, text = '') => ({ name, text, ctrl: false, meta: false, shift: false });
|
|
100
|
+
class FakePage {
|
|
101
|
+
label;
|
|
102
|
+
ids;
|
|
103
|
+
stages = false;
|
|
104
|
+
capturing = false;
|
|
105
|
+
category;
|
|
106
|
+
focusList = true;
|
|
107
|
+
constructor(label, ids) {
|
|
108
|
+
this.label = label;
|
|
109
|
+
this.ids = ids;
|
|
110
|
+
this.category = ids[0];
|
|
111
|
+
}
|
|
112
|
+
categories() {
|
|
113
|
+
return this.ids.map((id) => ({ id, label: id }));
|
|
114
|
+
}
|
|
115
|
+
selectCategory(id) {
|
|
116
|
+
this.category = id;
|
|
117
|
+
return Promise.resolve();
|
|
118
|
+
}
|
|
119
|
+
setFocus(list) {
|
|
120
|
+
this.focusList = list;
|
|
121
|
+
}
|
|
122
|
+
open() {
|
|
123
|
+
return Promise.resolve();
|
|
124
|
+
}
|
|
125
|
+
render() {
|
|
126
|
+
return { path: [], body: [], keys: [['↵', 'open']] };
|
|
127
|
+
}
|
|
128
|
+
handle(k) {
|
|
129
|
+
return k.name === 'escape' || k.name === 'left' ? { done: { kind: 'quit' } } : undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const pages = [new FakePage('One', ['a', 'b']), new FakePage('Two', []), new FakePage('Three', ['c', 'd'])];
|
|
133
|
+
const shell = new Shell(new App(), pages, new Draft());
|
|
134
|
+
await shell.handle(press('escape'));
|
|
135
|
+
const toNav = shell.focused;
|
|
136
|
+
await shell.handle(press('down'));
|
|
137
|
+
const inOne = pages[0].category;
|
|
138
|
+
await shell.handle(press('down'));
|
|
139
|
+
const onTwo = [shell.page.label, shell.nav().sections[1].categories.length];
|
|
140
|
+
await shell.handle(press('down'));
|
|
141
|
+
const onThree = [shell.page.label, pages[2].category];
|
|
142
|
+
await shell.handle(press('up'));
|
|
143
|
+
await shell.handle(press('up'));
|
|
144
|
+
const backInOne = [shell.page.label, pages[0].category];
|
|
145
|
+
await shell.handle(press('right'));
|
|
146
|
+
const toList = [shell.focused, pages[0].focusList];
|
|
147
|
+
await shell.handle(press('left'));
|
|
148
|
+
await shell.handle(press('3', '3'));
|
|
149
|
+
pages[2].capturing = true;
|
|
150
|
+
await shell.handle(press('1', '1'));
|
|
151
|
+
t.equal([toNav, inOne, onTwo, onThree, backInOne, toList, shell.page.label, shell.focused], ['nav', 'b', ['Two', 0], ['Three', 'c'], ['One', 'b'], ['list', true], 'Three', 'nav'], 'Esc/Left in the list go to the navigation; Up/Down walk categories then sections; Right returns; digits jump');
|
|
152
|
+
const keys = shell.render({ width: 100, height: 20 }).keys.map((k) => k[0] + ' ' + k[1]);
|
|
153
|
+
t.check(keys.includes('→ open') && keys.includes('1-7 jump'), 'the key bar shows the navigation keys while it has the keyboard', keys.join(', '));
|
|
154
|
+
const leave = await shell.handle(press('escape'));
|
|
155
|
+
t.check(leave !== undefined && leave.done.kind === 'quit', 'Esc in the navigation quits');
|
|
156
|
+
await shell.restore({ section: 2, category: 'd' });
|
|
157
|
+
t.equal([shell.page.label, pages[2].category, shell.view()], ['Three', 'd', { section: 2, category: 'd' }], 'the menu can be put back on the section and category it was left on');
|
|
158
|
+
t.section('list categories driven from outside');
|
|
159
|
+
const catItems = (id, n) => Array.from({ length: n }, (_, i) => ({ value: id + i, label: id + ' item ' + i, category: id }));
|
|
160
|
+
const catList = {
|
|
161
|
+
path: [],
|
|
162
|
+
categories: [
|
|
163
|
+
{ id: 'x', label: 'Ex' },
|
|
164
|
+
{ id: 'y', label: 'Why' },
|
|
165
|
+
],
|
|
166
|
+
items: [...catItems('x', 2), ...catItems('y', 3)],
|
|
167
|
+
};
|
|
168
|
+
const external = new ListScreen({ ...catList, categoryNav: 'external' });
|
|
169
|
+
const own = new ListScreen(catList);
|
|
170
|
+
const drawn = (l) => Text.strip(l.render({ width: 100, height: 20 }).body.join('\n'));
|
|
171
|
+
external.setCategory('y');
|
|
172
|
+
const left = external.handle(press('left'));
|
|
173
|
+
t.check(external.currentCategory === 'y' &&
|
|
174
|
+
drawn(external).includes('y item 2') &&
|
|
175
|
+
!drawn(external).includes('x item 0') &&
|
|
176
|
+
!drawn(external).includes('Why') &&
|
|
177
|
+
drawn(own).includes('Why') &&
|
|
178
|
+
left !== undefined &&
|
|
179
|
+
left.done.kind === 'back', 'with external navigation the list shows no sidebar, switches category on request and hands Left back');
|
|
180
|
+
t.section('settings categories');
|
|
181
|
+
const settingsFile = ConfigFile.user(t.at('settings', 'config.json'));
|
|
182
|
+
const projectFile = ConfigFile.project(t.dir('settings', 'project'));
|
|
183
|
+
const listed = CATEGORIES.flatMap((c) => c.keys);
|
|
184
|
+
const topLevel = [...settingsFile.schema.children(), ...projectFile.schema.children()].map(([name]) => name);
|
|
185
|
+
t.check(CATEGORIES.length <= 8 &&
|
|
186
|
+
topLevel.every((name) => listed.includes(name) || SETTINGS_ELSEWHERE.includes(name)) &&
|
|
187
|
+
listed.length === new Set(listed).size, 'settings are in at most 8 categories; each top-level setting is in exactly one, or covered by the Apps section');
|
|
188
|
+
const placed = settingsOf(settingsFile);
|
|
189
|
+
t.check(CATEGORIES.every((c) => placed.some((s) => s.category === c.id)) &&
|
|
190
|
+
settingsFile.schema
|
|
191
|
+
.leaves()
|
|
192
|
+
.every((l) => SETTINGS_ELSEWHERE.includes(l.key[0]) || placed.some((s) => s.key[0] === l.key[0])), 'every category has settings, and every setting that is not an app choice appears in a list');
|
|
193
|
+
const row = { value: '@data-folder', label: 'Data folder', category: 'general' };
|
|
194
|
+
const files = { user: settingsFile, project: projectFile };
|
|
195
|
+
t.check(settingsOptions(files, 'user', new Draft(), [row]).items.some((i) => i.value === row.value) &&
|
|
196
|
+
!settingsOptions(files, 'project', new Draft(), [row]).items.some((i) => i.value === row.value), "the Data folder row is in your own settings, not in a project's");
|
|
197
|
+
t.section('agents');
|
|
198
|
+
const agentsModel = {
|
|
199
|
+
folder: 'C:\\code\\api',
|
|
200
|
+
defaultAgent: 'codex',
|
|
201
|
+
agents: [
|
|
202
|
+
{ name: 'claude', ready: true },
|
|
203
|
+
{ name: 'codex', ready: false },
|
|
204
|
+
],
|
|
205
|
+
last: ['launch'],
|
|
206
|
+
attach: false,
|
|
207
|
+
window: 'config',
|
|
208
|
+
hostAgents: [],
|
|
209
|
+
};
|
|
210
|
+
const agentList = agentsOptions(agentsModel, 'C:\\code\\api');
|
|
211
|
+
t.equal([
|
|
212
|
+
agentList.items.filter((i) => i.category === 'start').map((i) => i.value),
|
|
213
|
+
agentList.categories?.map((c) => c.id),
|
|
214
|
+
], [
|
|
215
|
+
['repeat', 'agent:codex', 'agent:claude', 'add'],
|
|
216
|
+
['start', 'options'],
|
|
217
|
+
], 'Agents: Repeat last, the default agent first, the others, then Add an agent; no "On this PC" while it is empty');
|
|
218
|
+
t.check(agentList.items
|
|
219
|
+
.filter((i) => i.category === 'options')
|
|
220
|
+
.map((i) => i.value)
|
|
221
|
+
.join() === 'opt:attach,opt:window,opt:folder,more,several,noagent' &&
|
|
222
|
+
agentsOptions({ ...agentsModel, hostAgents: ['claude'] }).items.some((i) => i.value === 'host:claude') &&
|
|
223
|
+
agentCategories({ ...agentsModel, hostAgents: ['claude'] }).some((c) => c.id === 'host'), 'Agents > Options holds the start options and the other ways to start; "On this PC" appears with entries');
|
|
224
|
+
t.equal([
|
|
225
|
+
agentNameProblem('my-agent', ['claude']),
|
|
226
|
+
agentNameProblem('Claude', ['claude']),
|
|
227
|
+
agentNameProblem('bad name', []),
|
|
228
|
+
agentNameProblem(' ', []),
|
|
229
|
+
commandFromPackage('@scope/my-cli@latest'),
|
|
230
|
+
commandFromPackage('aider-chat'),
|
|
231
|
+
], [
|
|
232
|
+
true,
|
|
233
|
+
'Claude is an agent already.',
|
|
234
|
+
'Use letters, numbers, - and _ only.',
|
|
235
|
+
'Type a name.',
|
|
236
|
+
'my-cli',
|
|
237
|
+
'aider-chat',
|
|
238
|
+
], 'a new agent name is checked (letters, no duplicates); the command is guessed from the package');
|
|
239
|
+
const agentFile = ConfigFile.user(t.at('agents', 'config.json'));
|
|
240
|
+
t.write(agentFile.file, '{\n // mine\n "defaultHarness": "claude"\n}\n');
|
|
241
|
+
const typed = (...answers) => ({
|
|
242
|
+
choose: () => Promise.resolve(answers.shift() === 'host' ? 'host' : 'npm'),
|
|
243
|
+
text: () => Promise.resolve(answers.shift()),
|
|
244
|
+
pickPath: () => Promise.resolve(answers.shift()),
|
|
245
|
+
});
|
|
246
|
+
const added = await new AddAgent(typed('npm', 'my-agent', '@me/my-agent@latest', 'my-agent'), agentFile, new Draft(), ['claude', 'codex']).run();
|
|
247
|
+
const saved = agentFile.userConfig().harnesses['my-agent'];
|
|
248
|
+
t.check(added?.name === 'my-agent' &&
|
|
249
|
+
added.saved &&
|
|
250
|
+
saved?.npmPackage === '@me/my-agent@latest' &&
|
|
251
|
+
saved.npmBin === 'my-agent' &&
|
|
252
|
+
agentFile.text().includes('// mine') &&
|
|
253
|
+
Boolean(agentFile.userConfig().harnesses.claude), 'Add an agent writes it to your config file (comments and the built-in agents stay)', agentFile.text());
|
|
254
|
+
const stagedAgents = new Draft();
|
|
255
|
+
stagedAgents.stage(agentFile, ['harnesses'], {
|
|
256
|
+
...agentFile.valueInFile(['harnesses']),
|
|
257
|
+
other: newAgent('uv', 'tool', 'tool'),
|
|
258
|
+
});
|
|
259
|
+
const joined = await new AddAgent(typed('host', 'third', 'third.exe'), agentFile, stagedAgents, ['claude']).run();
|
|
260
|
+
t.check(joined?.saved === false &&
|
|
261
|
+
Object.keys(stagedAgents.value(agentFile, ['harnesses'])).join() === 'my-agent,other,third', 'with unsaved changes to the agents, the new agent joins them instead of being written');
|
|
262
|
+
t.section('list keys follow the highlighted entry');
|
|
263
|
+
const keyList = new ListScreen({
|
|
264
|
+
path: [],
|
|
265
|
+
items: [
|
|
266
|
+
{ value: 'a', label: 'A' },
|
|
267
|
+
{ value: 'b', label: 'B' },
|
|
268
|
+
],
|
|
269
|
+
actions: [{ key: 'x', label: 'remove', applies: (v) => v === 'b' }],
|
|
270
|
+
enterLabel: (_c, v) => (v === 'a' ? '' : 'go'),
|
|
271
|
+
});
|
|
272
|
+
const barOf = (l) => l.render({ width: 100, height: 20 }).keys.map((k) => k[1]);
|
|
273
|
+
const onA = barOf(keyList);
|
|
274
|
+
const pressedOnA = keyList.handle(press('x', 'x'));
|
|
275
|
+
keyList.handle(press('down'));
|
|
276
|
+
const onB = barOf(keyList);
|
|
277
|
+
const pressedOnB = keyList.handle(press('x', 'x'));
|
|
278
|
+
t.check(!onA.includes('remove') &&
|
|
279
|
+
!onA.includes('go') &&
|
|
280
|
+
pressedOnA === undefined &&
|
|
281
|
+
onB.includes('remove') &&
|
|
282
|
+
onB.includes('go') &&
|
|
283
|
+
pressedOnB !== undefined &&
|
|
284
|
+
pressedOnB.done.kind === 'action', 'a key (and Enter) shows in the key bar, and works, only while an entry it applies to is highlighted');
|
|
285
|
+
const long = new ListScreen({
|
|
286
|
+
path: [],
|
|
287
|
+
items: Array.from({ length: 40 }, (_, i) => ({ value: 'v' + i, label: 'item ' + i })),
|
|
288
|
+
});
|
|
289
|
+
const longBody = Text.strip(long.render({ width: 100, height: 14 }).body.join('\n'));
|
|
290
|
+
t.check(longBody.includes('more') && longBody.includes('/ search'), 'a long list says how to search');
|
|
291
|
+
class OpenOn extends ListPage {
|
|
292
|
+
label = 'Open';
|
|
293
|
+
load() {
|
|
294
|
+
return Promise.resolve();
|
|
295
|
+
}
|
|
296
|
+
options() {
|
|
297
|
+
return {
|
|
298
|
+
path: [],
|
|
299
|
+
items: [
|
|
300
|
+
{ value: 'a', label: 'Alpha' },
|
|
301
|
+
{ value: 'b', label: 'Beta' },
|
|
302
|
+
],
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
act() {
|
|
306
|
+
return Promise.resolve({});
|
|
307
|
+
}
|
|
308
|
+
initialFocus() {
|
|
309
|
+
return 'b';
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const openOn = new OpenOn(new App());
|
|
313
|
+
await openOn.open();
|
|
314
|
+
const marked = openOn
|
|
315
|
+
.render({ width: 100, height: 12 })
|
|
316
|
+
.body.map((l) => Text.strip(l))
|
|
317
|
+
.find((l) => l.includes('›'));
|
|
318
|
+
t.check(marked?.includes('Beta') === true, 'a section opens with the cursor on the entry it asks for');
|
|
319
|
+
t.section('proper inputs');
|
|
320
|
+
const user2 = ConfigFile.user(t.at('inputs', 'config.json'));
|
|
321
|
+
const leaf = (...key) => {
|
|
322
|
+
let node = user2.schema;
|
|
323
|
+
for (const k of key)
|
|
324
|
+
node = node?.child(k);
|
|
325
|
+
if (!node)
|
|
326
|
+
throw new Error('no setting ' + key.join('.'));
|
|
327
|
+
return node;
|
|
328
|
+
};
|
|
329
|
+
const port = new SchemaNode(z.number().int().min(1024).max(65535));
|
|
330
|
+
t.equal([
|
|
331
|
+
port.rangeText(),
|
|
332
|
+
numberProblem(port, '5000'),
|
|
333
|
+
numberProblem(port, '80'),
|
|
334
|
+
numberProblem(port, 'abc'),
|
|
335
|
+
numberProblem(port, '1.5'),
|
|
336
|
+
new SchemaNode(z.number().int().positive()).rangeText(),
|
|
337
|
+
new SchemaNode(z.number().int().min(2048)).rangeText(),
|
|
338
|
+
], [
|
|
339
|
+
'1024 to 65535',
|
|
340
|
+
true,
|
|
341
|
+
'Use 1024 to 65535.',
|
|
342
|
+
'Enter a number.',
|
|
343
|
+
'Enter a whole number.',
|
|
344
|
+
'more than 0',
|
|
345
|
+
'at least 2048',
|
|
346
|
+
], 'a number is checked against the schema limits, with the range in the message');
|
|
347
|
+
const fake = (script) => {
|
|
348
|
+
const seen = {
|
|
349
|
+
text: [],
|
|
350
|
+
path: [],
|
|
351
|
+
choose: [],
|
|
352
|
+
list: [],
|
|
353
|
+
};
|
|
354
|
+
const app = {
|
|
355
|
+
text: (o) => {
|
|
356
|
+
seen.text.push(o);
|
|
357
|
+
return Promise.resolve(script.text?.shift());
|
|
358
|
+
},
|
|
359
|
+
pickPath: (o) => {
|
|
360
|
+
seen.path.push(o);
|
|
361
|
+
return Promise.resolve(script.path?.shift());
|
|
362
|
+
},
|
|
363
|
+
choose: (_p, _i, choices, current) => {
|
|
364
|
+
seen.choose.push({ current, values: choices.map((c) => c.value) });
|
|
365
|
+
return Promise.resolve(script.choose?.shift());
|
|
366
|
+
},
|
|
367
|
+
list: (o) => {
|
|
368
|
+
seen.list.push(o);
|
|
369
|
+
return Promise.resolve(script.list?.shift() ?? { kind: 'back' });
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
return { app, ...seen };
|
|
373
|
+
};
|
|
374
|
+
const flipped = await new ValueEditor(fake({}).app).edit(['S'], ['sandbox', 'headless'], leaf('sandbox', 'headless'), false);
|
|
375
|
+
const choiceNode = user2.schema.leaves().find((l) => l.node.editor === 'choice');
|
|
376
|
+
const choiceFake = fake({ choose: [choiceNode?.node.choiceValues[0]] });
|
|
377
|
+
const chosenValue = choiceNode
|
|
378
|
+
? await new ValueEditor(choiceFake.app).edit(['S'], choiceNode.key, choiceNode.node, choiceNode.node.choiceValues[1])
|
|
379
|
+
: undefined;
|
|
380
|
+
t.check(flipped?.value === true &&
|
|
381
|
+
choiceNode !== undefined &&
|
|
382
|
+
chosenValue?.value === choiceNode.node.choiceValues[0] &&
|
|
383
|
+
choiceFake.choose[0].current === choiceNode.node.choiceValues[1] &&
|
|
384
|
+
choiceFake.choose[0].values.length === choiceNode.node.choiceValues.length, 'an on/off setting flips; a choice setting offers its values with the current one marked');
|
|
385
|
+
const days = leaf('audit', 'days');
|
|
386
|
+
const numberFake = fake({ text: ['30'] });
|
|
387
|
+
const typedDays = await new ValueEditor(numberFake.app).edit(['S'], ['audit', 'days'], days, 7);
|
|
388
|
+
const numberOptions = numberFake.text[0];
|
|
389
|
+
t.check(typedDays?.value === 30 &&
|
|
390
|
+
numberOptions.accept?.test('7') === true &&
|
|
391
|
+
numberOptions.accept?.test('a') === false &&
|
|
392
|
+
numberOptions.hint?.includes('1 to 365') === true &&
|
|
393
|
+
numberOptions.validate?.('400') === 'Use 1 to 365.' &&
|
|
394
|
+
numberOptions.validate?.('30') === true &&
|
|
395
|
+
numberOptions.initial === '7', 'a number setting takes digits only, shows its range, and refuses a value outside it as you type');
|
|
396
|
+
t.equal([
|
|
397
|
+
pathKindOf(['storage', 'root']),
|
|
398
|
+
pathKindOf(['projects', 'roots']),
|
|
399
|
+
pathKindOf(['mappings', 'host']),
|
|
400
|
+
pathKindOf(['claudeProfile', 'hostDir']),
|
|
401
|
+
pathKindOf(['claudeProfile', 'globalStateFile']),
|
|
402
|
+
pathKindOf(['sandbox', 'headless']),
|
|
403
|
+
pathKindOf(['defaultHarness']),
|
|
404
|
+
], ['dir', 'dir', 'dir', 'dir', 'file', undefined, undefined], 'folder and file settings are told apart from the rest');
|
|
405
|
+
const folderFake = fake({ path: ['C:\\Users\\me\\.claude'] });
|
|
406
|
+
const folderValue = await new ValueEditor(folderFake.app).edit(['S'], ['claudeProfile', 'hostDir'], leaf('claudeProfile', 'hostDir'), 'auto');
|
|
407
|
+
const fileFake = fake({ path: ['C:\\Users\\me\\.claude.json'] });
|
|
408
|
+
await new ValueEditor(fileFake.app).edit(['S'], ['claudeProfile', 'globalStateFile'], leaf('claudeProfile', 'globalStateFile'), 'auto');
|
|
409
|
+
t.check(folderValue?.value === 'C:\\Users\\me\\.claude' &&
|
|
410
|
+
folderFake.path[0].files === false &&
|
|
411
|
+
folderFake.path[0].literals?.includes('auto') === true &&
|
|
412
|
+
folderFake.text.length === 0 &&
|
|
413
|
+
fileFake.path[0].files === true, 'a folder setting opens the path picker (a file setting a file picker); "auto" can still be typed');
|
|
414
|
+
const roots = leaf('projects', 'roots');
|
|
415
|
+
const listFake = fake({
|
|
416
|
+
path: ['C:\\Code', 'D:\\More'],
|
|
417
|
+
list: [
|
|
418
|
+
{ kind: 'pick', value: '@add' },
|
|
419
|
+
{ kind: 'pick', value: '@add' },
|
|
420
|
+
{ kind: 'action', key: 'd', value: '0' },
|
|
421
|
+
{ kind: 'back' },
|
|
422
|
+
],
|
|
423
|
+
});
|
|
424
|
+
const rootsValue = await new ValueEditor(listFake.app).edit(['S'], ['projects', 'roots'], roots, []);
|
|
425
|
+
const keysOf = (o, id) => (typeof o.actions === 'function' ? o.actions(undefined) : (o.actions ?? []))
|
|
426
|
+
.filter((a) => !a.applies || a.applies(id))
|
|
427
|
+
.map((a) => a.key);
|
|
428
|
+
t.check(JSON.stringify(rootsValue?.value) === '["D:\\\\More"]' &&
|
|
429
|
+
listFake.path.length === 2 &&
|
|
430
|
+
listFake.path.every((p) => p.files === false) &&
|
|
431
|
+
keysOf(listFake.list[0], '@add').length === 0 &&
|
|
432
|
+
keysOf(listFake.list[2], '0').join() === 'd,u,j', 'a folder list adds with the path picker and removes entries; reorder keys only appear on an entry');
|
|
433
|
+
const plainList = user2.schema.leaves().find((l) => l.node.editor === 'string-list' && !pathKindOf(l.key));
|
|
434
|
+
const moved = plainList
|
|
435
|
+
? await new ValueEditor(fake({
|
|
436
|
+
list: [
|
|
437
|
+
{ kind: 'action', key: 'j', value: '0' },
|
|
438
|
+
{ kind: 'action', key: 'd', value: '2' },
|
|
439
|
+
{ kind: 'back' },
|
|
440
|
+
],
|
|
441
|
+
}).app).edit(['S'], plainList.key, plainList.node, ['a', 'b', 'c'])
|
|
442
|
+
: undefined;
|
|
443
|
+
t.equal(moved?.value, ['b', 'a'], 'list items move down and are removed (a, b, c: a down, then c removed)');
|
|
444
|
+
const envNode = user2.schema.leaves().find((l) => l.node.editor === 'string-map');
|
|
445
|
+
const envValue = envNode
|
|
446
|
+
? await new ValueEditor(fake({ text: ['FOO', 'bar'], list: [{ kind: 'pick', value: '@add' }, { kind: 'back' }] }).app).edit(['S'], envNode.key, envNode.node, {})
|
|
447
|
+
: undefined;
|
|
448
|
+
t.equal(envValue?.value, { FOO: 'bar' }, 'a name-to-value setting is edited as names and values');
|
|
449
|
+
const mappingFake = fake({
|
|
450
|
+
path: ['C:\\data'],
|
|
451
|
+
list: [{ kind: 'pick', value: '@add' }, { kind: 'pick', value: 'host' }, { kind: 'back' }, { kind: 'back' }],
|
|
452
|
+
});
|
|
453
|
+
const mapped = await new ValueEditor(mappingFake.app).edit(['S'], ['mappings'], leaf('mappings'), []);
|
|
454
|
+
t.check(JSON.stringify(mapped?.value) === '[{"host":"C:\\\\data","readOnly":false}]' &&
|
|
455
|
+
mappingFake.path.length === 1 &&
|
|
456
|
+
mappingFake.path[0].files === false, 'a field of a form inside a list (a mapped folder) is picked in the path picker too', JSON.stringify(mapped));
|
|
457
|
+
t.section('command forms');
|
|
458
|
+
const formCtx = {
|
|
459
|
+
storageRoot: t.dir('forms', 'storage'),
|
|
460
|
+
dataPath: t.dir('forms', 'data'),
|
|
461
|
+
loaded: { config: structuredClone(DEFAULTS), configPath: null, base: t.temp },
|
|
462
|
+
};
|
|
463
|
+
const account = commandSpec('account');
|
|
464
|
+
const renameFake = fake({ list: [{ kind: 'pick', value: 'arg:action' }, { kind: 'back' }], choose: ['rename'] });
|
|
465
|
+
await new CommandForm(renameFake.app, formCtx, account).run();
|
|
466
|
+
const renameItems = renameFake.list[1].items;
|
|
467
|
+
t.check(renameItems.some((i) => i.label === 'New name' && i.description === 'The new name') &&
|
|
468
|
+
!renameItems.some((i) => i.label === 'From host') &&
|
|
469
|
+
renameItems.every((i) => !i.label.startsWith('-') && !(i.description ?? '').startsWith('(')), 'a form filtered by action drops the "(rename)" prefix and the other actions\' options; no flag is a label');
|
|
470
|
+
t.check(COMMANDS.every((c) => c.options.every((o) => !optionLabel(o).startsWith('-') && optionLabel(o).length > 2)) &&
|
|
471
|
+
plainDescription('(rename) the new name', ['rename', 'save']) === 'The new name' &&
|
|
472
|
+
plainDescription('(claude) use your profile', ['rename']) === 'For claude: use your profile' &&
|
|
473
|
+
plainDescription('stop here', []) === 'Stop here', 'every option has a plain label, and action prefixes read as plain descriptions');
|
|
474
|
+
const launch = commandSpec('launch');
|
|
475
|
+
const workspaceFake = fake({
|
|
476
|
+
list: [{ kind: 'pick', value: 'opt:--workspace' }, { kind: 'back' }],
|
|
477
|
+
path: ['C:\\work'],
|
|
478
|
+
});
|
|
479
|
+
await new CommandForm(workspaceFake.app, formCtx, launch).run();
|
|
480
|
+
t.check(workspaceFake.path.length === 1 &&
|
|
481
|
+
workspaceFake.path[0].files === false &&
|
|
482
|
+
workspaceFake.text.length === 0 &&
|
|
483
|
+
(workspaceFake.list[1].items.find((i) => i.value === 'opt:--workspace')?.hint ?? '').includes('C:\\work'), 'a folder option (--workspace) is picked in the path picker');
|
|
484
|
+
const timeoutFake = fake({ list: [{ kind: 'pick', value: 'opt:--timeout' }, { kind: 'back' }], text: ['30'] });
|
|
485
|
+
await new CommandForm(timeoutFake.app, formCtx, commandSpec('exec')).run();
|
|
486
|
+
const timeoutText = timeoutFake.text[0];
|
|
487
|
+
t.check(timeoutText.accept?.test('x') === false &&
|
|
488
|
+
timeoutText.accept?.test('3') === true &&
|
|
489
|
+
timeoutText.hint?.includes('Default: 600') === true &&
|
|
490
|
+
typeof timeoutText.validate?.('0') === 'string', 'a number option takes digits only and shows its default');
|
|
491
|
+
const storageFake = fake({
|
|
492
|
+
list: [{ kind: 'pick', value: 'arg:action' }, { kind: 'pick', value: 'arg:folder' }, { kind: 'back' }],
|
|
493
|
+
choose: ['move'],
|
|
494
|
+
path: ['D:\\Data'],
|
|
495
|
+
});
|
|
496
|
+
await new CommandForm(storageFake.app, formCtx, commandSpec('storage')).run();
|
|
497
|
+
t.check(storageFake.path.length === 1 &&
|
|
498
|
+
(storageFake.list[2].items.find((i) => i.value === 'arg:folder')?.hint ?? '').includes('D:\\Data'), 'storage move takes its folder from the path picker');
|
|
499
|
+
const projectsFake = fake({
|
|
500
|
+
list: [{ kind: 'pick', value: 'arg:action' }, { kind: 'pick', value: 'arg:value' }, { kind: 'back' }],
|
|
501
|
+
choose: ['all', 'on'],
|
|
502
|
+
});
|
|
503
|
+
await new CommandForm(projectsFake.app, formCtx, commandSpec('projects')).run();
|
|
504
|
+
t.check(JSON.stringify(projectsFake.choose[1].values.slice(0, 2)) === '["on","off"]' &&
|
|
505
|
+
argPathKind({ name: 'folder', description: '', required: false }, 'storage', 'move') === 'dir' &&
|
|
506
|
+
optionPathKind({ flag: '--workspace', type: 'string', value: 'dir', description: '' }) === 'dir' &&
|
|
507
|
+
optionPathKind({ flag: '--script', type: 'list', value: 'file', description: '' }) === 'file' &&
|
|
508
|
+
optionPathKind({ flag: '--harness', type: 'string', value: 'name', description: '' }) === undefined, 'choices and paths are told apart: on/off for projects all, folders and files for the rest');
|
|
509
|
+
t.section('agents: make default and remove');
|
|
510
|
+
const actionModel = {
|
|
511
|
+
folder: 'C:\\code\\api',
|
|
512
|
+
defaultAgent: 'claude',
|
|
513
|
+
agents: [
|
|
514
|
+
{ name: 'claude', ready: true },
|
|
515
|
+
{ name: 'codex', ready: false },
|
|
516
|
+
{ name: 'mine', ready: false },
|
|
517
|
+
],
|
|
518
|
+
last: undefined,
|
|
519
|
+
attach: false,
|
|
520
|
+
window: 'config',
|
|
521
|
+
hostAgents: [],
|
|
522
|
+
removable: addedAgents(['claude', 'codex', 'mine']),
|
|
523
|
+
};
|
|
524
|
+
const startActions = agentActions(actionModel);
|
|
525
|
+
const keyFor = (key, id) => startActions.find((a) => a.key === key)?.applies?.(id) ?? false;
|
|
526
|
+
const actionList = agentsOptions(actionModel, 'C:\\code\\api');
|
|
527
|
+
t.check(JSON.stringify(actionModel.removable) === '["mine"]' &&
|
|
528
|
+
keyFor('d', 'agent:codex') &&
|
|
529
|
+
!keyFor('d', 'agent:claude') &&
|
|
530
|
+
!keyFor('d', 'add') &&
|
|
531
|
+
keyFor('x', 'agent:mine') &&
|
|
532
|
+
!keyFor('x', 'agent:codex') &&
|
|
533
|
+
typeof actionList.actions === 'function' &&
|
|
534
|
+
actionList.actions('start').length === 2 &&
|
|
535
|
+
actionList.actions('options').length === 0, 'Make default shows on any other agent, Remove only on one you added; neither on built-in agents or other rows');
|
|
536
|
+
const agentsFile = ConfigFile.user(t.at('agents-actions', 'config.json'));
|
|
537
|
+
t.write(agentsFile.file, '{\n // mine\n "defaultHarness": "claude"\n}\n');
|
|
538
|
+
const agentDraft = new Draft();
|
|
539
|
+
await new AddAgent({
|
|
540
|
+
choose: () => Promise.resolve('npm'),
|
|
541
|
+
text: (() => {
|
|
542
|
+
const answers = ['mine', '@me/mine@latest', 'mine'];
|
|
543
|
+
return () => Promise.resolve(answers.shift());
|
|
544
|
+
})(),
|
|
545
|
+
}, agentsFile, agentDraft, ['claude']).run();
|
|
546
|
+
makeDefaultAgent(agentsFile, agentDraft, 'mine');
|
|
547
|
+
const becameDefault = agentsFile.userConfig().defaultHarness;
|
|
548
|
+
await t.throws(() => removeAgent(agentsFile, agentDraft, 'mine'), 'default agent', 'the default agent is not removed');
|
|
549
|
+
makeDefaultAgent(agentsFile, agentDraft, 'codex');
|
|
550
|
+
removeAgent(agentsFile, agentDraft, 'mine');
|
|
551
|
+
await t.throws(() => removeAgent(agentsFile, agentDraft, 'claude'), 'built in', 'a built-in agent is not removed');
|
|
552
|
+
t.check(becameDefault === 'mine' &&
|
|
553
|
+
agentsFile.userConfig().defaultHarness === 'codex' &&
|
|
554
|
+
!agentsFile.text().includes('mine"') &&
|
|
555
|
+
agentsFile.text().includes('// mine') &&
|
|
556
|
+
Boolean(agentsFile.userConfig().harnesses.claude), 'Make default and Remove change your config file (comments stay; the built-in agents stay)', agentsFile.text());
|
|
557
|
+
t.section('empty states offer the next step');
|
|
558
|
+
const sessionsEmpty = sessionsOptions({ sandboxRunning: true, sessions: [], defaultAgent: 'codex' });
|
|
559
|
+
const enter = sessionsEmpty.enterLabel;
|
|
560
|
+
t.check(sessionsEmpty.items.length === 1 &&
|
|
561
|
+
sessionsEmpty.items[0].label === 'Start codex' &&
|
|
562
|
+
typeof enter === 'function' &&
|
|
563
|
+
enter(undefined, sessionsEmpty.items[0].value) === 'start', 'with no session, Sessions offers to start the default agent (Enter)');
|
|
564
|
+
const stoppedSandbox = {
|
|
565
|
+
report: { sandbox: { running: false }, repoGuard: { guarded: 0, findings: [] }, projects: [], otherSessions: [] },
|
|
566
|
+
projects: { all: false, roots: [], exclude: [], projects: [] },
|
|
567
|
+
databases: null,
|
|
568
|
+
defaultAgent: 'claude',
|
|
569
|
+
};
|
|
570
|
+
const stoppedItems = sandboxOptions(stoppedSandbox).items;
|
|
571
|
+
t.check(stoppedItems.some((i) => i.value === 'act:start' && i.category === 'status') &&
|
|
572
|
+
stoppedItems.some((i) => i.value === 'act:start' && i.category === 'databases') &&
|
|
573
|
+
stoppedItems.some((i) => i.value === '@add-root') &&
|
|
574
|
+
!stoppedItems.some((i) => i.value === 'act:stop'), 'with no sandbox running, Sandbox offers to start one (Status and Databases) instead of dead rows');
|
|
575
|
+
const noLogins = accountsOptions({ agents: [{ name: 'claude', kept: true, canSavePc: true }], rows: {} });
|
|
576
|
+
t.equal(noLogins.items.map((i) => i.value), ['@save-sandbox', '@save-pc'], 'with no saved login, Accounts offers to save the sandbox login or this PC login (Enter)');
|
|
577
|
+
t.section('apps in a few groups');
|
|
578
|
+
const appsUser = new AppsModel(user2, t.dir('apps-groups'));
|
|
579
|
+
const groupIds = appsUser.categories().map((c) => c.id);
|
|
580
|
+
const appItems = appsUser.items(new Draft());
|
|
581
|
+
t.check(groupIds.length <= 6 &&
|
|
582
|
+
new AppsModel(ConfigFile.project(t.dir('apps-groups', 'project')), t.dir('apps-groups')).categories().length <=
|
|
583
|
+
5 &&
|
|
584
|
+
APP_GROUPS.every((g) => appItems.some((i) => i.category === g.id)) &&
|
|
585
|
+
appItems.every((i) => groupIds.includes(i.category ?? '')) &&
|
|
586
|
+
appItems.length === appsUser.entries().length &&
|
|
587
|
+
APP_CATEGORIES.every((c) => APP_GROUPS.some((g) => g.members.includes(c.id))), 'the apps are in at most 6 groups; every catalogue category belongs to one and every app is listed', groupIds.join());
|
|
43
588
|
t.section('dialog box');
|
|
44
589
|
const box = dialogBox({ title: 'Unsaved changes', lines: ['You have 2 unsaved changes.'], buttons: 'Save' }, 61);
|
|
45
590
|
const base = Array.from({ length: 12 }, () => 'x'.repeat(60));
|
|
@@ -71,7 +616,7 @@ export const menuSuite = {
|
|
|
71
616
|
draft.stage(file, ['defaultHarness'], 42);
|
|
72
617
|
t.check(draft.apply().problems.length > 0 && draft.count() === 1, 'an invalid change is refused and stays staged');
|
|
73
618
|
draft.discard();
|
|
74
|
-
t.section('
|
|
619
|
+
t.section('section pages');
|
|
75
620
|
t.equal(startArgv({
|
|
76
621
|
folder: 'C:\\code\\api',
|
|
77
622
|
defaultAgent: 'claude',
|
|
@@ -80,7 +625,7 @@ export const menuSuite = {
|
|
|
80
625
|
attach: true,
|
|
81
626
|
window: 'hide',
|
|
82
627
|
hostAgents: [],
|
|
83
|
-
}, 'codex', 'C:\\code\\other'), ['launch', '--harness', 'codex', '--terminal', '--headless', '--workspace', 'C:\\code\\api'], 'the
|
|
628
|
+
}, 'codex', 'C:\\code\\other'), ['launch', '--harness', 'codex', '--terminal', '--headless', '--workspace', 'C:\\code\\api'], 'the Agents section builds the same command line the form would');
|
|
84
629
|
t.equal([
|
|
85
630
|
hostAgentsToShow(false, [{ harness: 'claude' }]),
|
|
86
631
|
hostAgentsToShow(true, [{ harness: 'claude' }, { harness: 'codex' }]),
|
|
@@ -139,7 +684,15 @@ export const menuSuite = {
|
|
|
139
684
|
await t.throws(() => service.save('bad name', false, 'codex'), 'Invalid account name', 'bad names are refused');
|
|
140
685
|
t.section('every screen');
|
|
141
686
|
const screens = renderScreens(t.dir('screens'));
|
|
142
|
-
|
|
687
|
+
const blocks = screens.split('\n=== ').slice(1);
|
|
688
|
+
const overflow = blocks.filter((block) => {
|
|
689
|
+
const [head, ...rows] = block.trimEnd().split('\n');
|
|
690
|
+
const size = /\((\d+)x(\d+)\)/.exec(head);
|
|
691
|
+
return !size || rows.length !== Number(size[2]) || rows.some((l) => Text.width(l) > Number(size[1]) - 1);
|
|
692
|
+
});
|
|
693
|
+
t.equal(overflow.map((b) => b.slice(0, b.indexOf('\n'))), [], 'every screen is exactly as tall as the terminal and never wider, at 200x60, 100x30, 80x24 and 60x20');
|
|
694
|
+
t.check(SIZES.length === 4 &&
|
|
695
|
+
screens.includes('=== 7 Maintenance · health (80x24) ===') &&
|
|
143
696
|
screens.includes('unsaved: 2') &&
|
|
144
697
|
screens.includes('Save'), 'all screens render at both sizes; the settings screen shows its unsaved count and the leave dialog offers Save');
|
|
145
698
|
const out = process.env.MENU_SCREENS_OUT;
|