nastechai-desktop 18.1.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/.prettierrc +11 -0
- package/DESIGN.md +167 -0
- package/README.md +141 -0
- package/assets/icon.icns +0 -0
- package/assets/icon.ico +0 -0
- package/assets/icon.png +0 -0
- package/components.json +21 -0
- package/electron/backend-env.cjs +112 -0
- package/electron/backend-env.test.cjs +111 -0
- package/electron/backend-probes.cjs +106 -0
- package/electron/backend-probes.test.cjs +82 -0
- package/electron/backend-ready.cjs +66 -0
- package/electron/bootstrap-platform.cjs +91 -0
- package/electron/bootstrap-platform.test.cjs +111 -0
- package/electron/bootstrap-runner.cjs +720 -0
- package/electron/bootstrap-runner.test.cjs +138 -0
- package/electron/connection-config.cjs +254 -0
- package/electron/connection-config.test.cjs +329 -0
- package/electron/dashboard-token.cjs +99 -0
- package/electron/dashboard-token.test.cjs +142 -0
- package/electron/desktop-uninstall.cjs +232 -0
- package/electron/desktop-uninstall.test.cjs +246 -0
- package/electron/entitlements.mac.inherit.plist +14 -0
- package/electron/entitlements.mac.plist +14 -0
- package/electron/fs-read-dir.cjs +109 -0
- package/electron/fs-read-dir.test.cjs +364 -0
- package/electron/gateway-ws-probe.cjs +188 -0
- package/electron/gateway-ws-probe.test.cjs +122 -0
- package/electron/git-root.cjs +54 -0
- package/electron/git-root.test.cjs +40 -0
- package/electron/git-worktrees.cjs +174 -0
- package/electron/hardening.cjs +184 -0
- package/electron/hardening.test.cjs +116 -0
- package/electron/main.cjs +5762 -0
- package/electron/oauth-net-request.cjs +20 -0
- package/electron/oauth-net-request.test.cjs +34 -0
- package/electron/preload.cjs +135 -0
- package/electron/session-windows.cjs +99 -0
- package/electron/session-windows.test.cjs +177 -0
- package/electron/update-remote.cjs +56 -0
- package/electron/update-remote.test.cjs +78 -0
- package/electron/vscode-marketplace.cjs +331 -0
- package/electron/vscode-marketplace.test.cjs +113 -0
- package/electron/windows-child-process.test.cjs +57 -0
- package/electron/windows-user-env.cjs +76 -0
- package/electron/windows-user-env.test.cjs +90 -0
- package/electron/workspace-cwd.cjs +38 -0
- package/electron/workspace-cwd.test.cjs +45 -0
- package/eslint.config.mjs +122 -0
- package/index.html +17 -0
- package/package.json +254 -0
- package/pr-assets/session-source-folders.png +0 -0
- package/preview-demo.html +65 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/ds-assets/filler-bg0.jpg +0 -0
- package/public/nastech-frames/nastech-frame-0.png +0 -0
- package/public/nastech-frames/nastech-frame-1.png +0 -0
- package/public/nastech-frames/nastech-frame-2.png +0 -0
- package/public/nastech-frames/nastech-frame-3.png +0 -0
- package/public/nastech-frames/nastech-frame-4.png +0 -0
- package/public/nastech-frames/nastech-frame-5.png +0 -0
- package/public/nastech-frames/nastech-frame-6.png +0 -0
- package/public/nastech-frames/nastech-frame-7.png +0 -0
- package/public/nastech-girl.jpg +0 -0
- package/public/nastech-sprite.png +0 -0
- package/public/nastech.png +0 -0
- package/scripts/after-pack.cjs +41 -0
- package/scripts/assert-dist-built.cjs +70 -0
- package/scripts/assert-dist-built.test.cjs +84 -0
- package/scripts/assert-root-install.cjs +13 -0
- package/scripts/before-build.cjs +11 -0
- package/scripts/before-pack.cjs +78 -0
- package/scripts/before-pack.test.cjs +53 -0
- package/scripts/click-session.mjs +51 -0
- package/scripts/dev-no-hmr.mjs +22 -0
- package/scripts/diag-jump.mjs +115 -0
- package/scripts/diag-scroll-reset.mjs +229 -0
- package/scripts/eval.mjs +21 -0
- package/scripts/leak-typing.mjs +222 -0
- package/scripts/measure-jump.mjs +108 -0
- package/scripts/measure-latency.mjs +184 -0
- package/scripts/measure-real-stream.mjs +252 -0
- package/scripts/measure-submit.mjs +179 -0
- package/scripts/measure-synthetic-stream.mjs +322 -0
- package/scripts/notarize-artifact.cjs +77 -0
- package/scripts/notarize.cjs +100 -0
- package/scripts/patch-electron-builder-mac-binary.cjs +59 -0
- package/scripts/probe-renderer.mjs +38 -0
- package/scripts/probe-thread.mjs +40 -0
- package/scripts/profile-long-stream.mjs +191 -0
- package/scripts/profile-real-stream.mjs +137 -0
- package/scripts/profile-synth-stream.mjs +103 -0
- package/scripts/profile-typing-lag.md +381 -0
- package/scripts/profile-typing.mjs +260 -0
- package/scripts/reload-renderer.mjs +25 -0
- package/scripts/reload.mjs +36 -0
- package/scripts/set-exe-identity.cjs +94 -0
- package/scripts/stage-native-deps.cjs +159 -0
- package/scripts/test-desktop.mjs +425 -0
- package/scripts/write-build-stamp.cjs +126 -0
- package/src/app/agents/index.tsx +398 -0
- package/src/app/artifacts/index.test.ts +62 -0
- package/src/app/artifacts/index.tsx +906 -0
- package/src/app/chat/chat-drop-overlay.tsx +48 -0
- package/src/app/chat/chat-swap-overlay.tsx +47 -0
- package/src/app/chat/composer/attachments.tsx +114 -0
- package/src/app/chat/composer/completion-drawer.tsx +63 -0
- package/src/app/chat/composer/context-menu.tsx +172 -0
- package/src/app/chat/composer/controls.tsx +289 -0
- package/src/app/chat/composer/drop-affordance.ts +2 -0
- package/src/app/chat/composer/enter-submit-dom-race.test.tsx +218 -0
- package/src/app/chat/composer/focus.ts +134 -0
- package/src/app/chat/composer/help-hint.tsx +59 -0
- package/src/app/chat/composer/hooks/use-at-completions.ts +141 -0
- package/src/app/chat/composer/hooks/use-live-completion-adapter.ts +119 -0
- package/src/app/chat/composer/hooks/use-mic-recorder.ts +291 -0
- package/src/app/chat/composer/hooks/use-slash-completions.ts +114 -0
- package/src/app/chat/composer/hooks/use-voice-conversation.ts +390 -0
- package/src/app/chat/composer/hooks/use-voice-recorder.ts +116 -0
- package/src/app/chat/composer/ime-composition-dom-repro.test.tsx +108 -0
- package/src/app/chat/composer/index.tsx +1611 -0
- package/src/app/chat/composer/inline-refs.ts +138 -0
- package/src/app/chat/composer/model-pill.tsx +86 -0
- package/src/app/chat/composer/queue-panel.tsx +130 -0
- package/src/app/chat/composer/rich-editor.test.ts +18 -0
- package/src/app/chat/composer/rich-editor.ts +165 -0
- package/src/app/chat/composer/skin-slash-popover.tsx +61 -0
- package/src/app/chat/composer/slash-nav-dom-repro.test.tsx +186 -0
- package/src/app/chat/composer/status-stack/index.tsx +202 -0
- package/src/app/chat/composer/status-stack/status-row.tsx +155 -0
- package/src/app/chat/composer/text-utils.test.ts +77 -0
- package/src/app/chat/composer/text-utils.ts +107 -0
- package/src/app/chat/composer/trigger-popover.test.tsx +42 -0
- package/src/app/chat/composer/trigger-popover.tsx +116 -0
- package/src/app/chat/composer/types.ts +64 -0
- package/src/app/chat/composer/url-dialog.tsx +82 -0
- package/src/app/chat/composer/voice-activity.tsx +252 -0
- package/src/app/chat/hooks/use-composer-actions.test.ts +57 -0
- package/src/app/chat/hooks/use-composer-actions.ts +525 -0
- package/src/app/chat/hooks/use-file-drop-zone.ts +118 -0
- package/src/app/chat/index.tsx +390 -0
- package/src/app/chat/perf-probe.tsx +269 -0
- package/src/app/chat/right-rail/index.ts +1 -0
- package/src/app/chat/right-rail/preview-console-state.ts +82 -0
- package/src/app/chat/right-rail/preview-console.tsx +290 -0
- package/src/app/chat/right-rail/preview-file.tsx +559 -0
- package/src/app/chat/right-rail/preview-pane.test.tsx +43 -0
- package/src/app/chat/right-rail/preview-pane.tsx +657 -0
- package/src/app/chat/right-rail/preview.tsx +171 -0
- package/src/app/chat/scroll-to-bottom-button.test.tsx +67 -0
- package/src/app/chat/scroll-to-bottom-button.tsx +74 -0
- package/src/app/chat/sidebar/cron-jobs-section.tsx +325 -0
- package/src/app/chat/sidebar/index.tsx +1219 -0
- package/src/app/chat/sidebar/load-more-row.tsx +30 -0
- package/src/app/chat/sidebar/order.test.ts +21 -0
- package/src/app/chat/sidebar/order.ts +17 -0
- package/src/app/chat/sidebar/profile-switcher.tsx +516 -0
- package/src/app/chat/sidebar/session-actions-menu.tsx +264 -0
- package/src/app/chat/sidebar/session-row.tsx +257 -0
- package/src/app/chat/sidebar/virtual-session-list.tsx +154 -0
- package/src/app/chat/sidebar/workspace-groups.test.ts +149 -0
- package/src/app/chat/sidebar/workspace-groups.ts +326 -0
- package/src/app/chat/thread-loading.test.ts +34 -0
- package/src/app/chat/thread-loading.ts +26 -0
- package/src/app/command-center/index.tsx +654 -0
- package/src/app/command-palette/index.tsx +513 -0
- package/src/app/command-palette/marketplace-theme-page.tsx +157 -0
- package/src/app/cron/index.tsx +942 -0
- package/src/app/cron/job-state.ts +29 -0
- package/src/app/desktop-controller.tsx +938 -0
- package/src/app/floating-hud.ts +22 -0
- package/src/app/gateway/hooks/use-gateway-boot.test.tsx +265 -0
- package/src/app/gateway/hooks/use-gateway-boot.ts +387 -0
- package/src/app/gateway/hooks/use-gateway-request.ts +138 -0
- package/src/app/hooks/use-keybinds.ts +186 -0
- package/src/app/hooks/use-refresh-hotkey.ts +45 -0
- package/src/app/hooks/use-route-enum-param.ts +38 -0
- package/src/app/index.tsx +1 -0
- package/src/app/layout-constants.ts +13 -0
- package/src/app/messaging/index.tsx +648 -0
- package/src/app/messaging/platform-icon.tsx +93 -0
- package/src/app/model-picker-overlay.tsx +42 -0
- package/src/app/model-visibility-overlay.tsx +31 -0
- package/src/app/overlays/overlay-chrome.tsx +66 -0
- package/src/app/overlays/overlay-search-input.tsx +33 -0
- package/src/app/overlays/overlay-split-layout.tsx +130 -0
- package/src/app/overlays/overlay-view.tsx +91 -0
- package/src/app/page-search-shell.tsx +75 -0
- package/src/app/profiles/create-profile-dialog.tsx +154 -0
- package/src/app/profiles/delete-profile-dialog.tsx +65 -0
- package/src/app/profiles/index.tsx +671 -0
- package/src/app/profiles/rename-profile-dialog.tsx +125 -0
- package/src/app/right-sidebar/files/dnd-manager.ts +27 -0
- package/src/app/right-sidebar/files/ipc.test.ts +100 -0
- package/src/app/right-sidebar/files/ipc.ts +161 -0
- package/src/app/right-sidebar/files/remote-picker.tsx +177 -0
- package/src/app/right-sidebar/files/tree.tsx +224 -0
- package/src/app/right-sidebar/files/use-project-tree.test.ts +190 -0
- package/src/app/right-sidebar/files/use-project-tree.ts +268 -0
- package/src/app/right-sidebar/index.test.tsx +75 -0
- package/src/app/right-sidebar/index.tsx +395 -0
- package/src/app/right-sidebar/store.ts +15 -0
- package/src/app/right-sidebar/terminal/buffer.ts +65 -0
- package/src/app/right-sidebar/terminal/index.tsx +98 -0
- package/src/app/right-sidebar/terminal/persistent.tsx +122 -0
- package/src/app/right-sidebar/terminal/selection.ts +75 -0
- package/src/app/right-sidebar/terminal/use-terminal-session.ts +504 -0
- package/src/app/routes.ts +88 -0
- package/src/app/session/hooks/use-context-suggestions.ts +58 -0
- package/src/app/session/hooks/use-cwd-actions.ts +109 -0
- package/src/app/session/hooks/use-message-stream.ts +957 -0
- package/src/app/session/hooks/use-model-controls.test.tsx +198 -0
- package/src/app/session/hooks/use-model-controls.ts +106 -0
- package/src/app/session/hooks/use-nastech-config.ts +74 -0
- package/src/app/session/hooks/use-preview-routing.test.tsx +168 -0
- package/src/app/session/hooks/use-preview-routing.ts +223 -0
- package/src/app/session/hooks/use-prompt-actions.test.tsx +316 -0
- package/src/app/session/hooks/use-prompt-actions.ts +1030 -0
- package/src/app/session/hooks/use-route-resume.test.tsx +136 -0
- package/src/app/session/hooks/use-route-resume.ts +115 -0
- package/src/app/session/hooks/use-session-actions.test.tsx +119 -0
- package/src/app/session/hooks/use-session-actions.ts +885 -0
- package/src/app/session/hooks/use-session-state-cache.test.tsx +118 -0
- package/src/app/session/hooks/use-session-state-cache.ts +191 -0
- package/src/app/session-picker-overlay.tsx +32 -0
- package/src/app/session-switcher.tsx +107 -0
- package/src/app/settings/about-settings.tsx +173 -0
- package/src/app/settings/appearance-settings.tsx +162 -0
- package/src/app/settings/config-settings.tsx +384 -0
- package/src/app/settings/constants.ts +545 -0
- package/src/app/settings/credential-key-ui.tsx +373 -0
- package/src/app/settings/env-credentials.tsx +198 -0
- package/src/app/settings/env-var-actions-menu.tsx +136 -0
- package/src/app/settings/field-copy.ts +56 -0
- package/src/app/settings/gateway-settings.tsx +620 -0
- package/src/app/settings/helpers.test.ts +138 -0
- package/src/app/settings/helpers.ts +151 -0
- package/src/app/settings/index.tsx +237 -0
- package/src/app/settings/keys-settings.tsx +96 -0
- package/src/app/settings/mcp-settings.tsx +271 -0
- package/src/app/settings/model-settings.test.tsx +157 -0
- package/src/app/settings/model-settings.tsx +559 -0
- package/src/app/settings/notifications-settings.tsx +150 -0
- package/src/app/settings/primitives.tsx +115 -0
- package/src/app/settings/providers-settings.test.tsx +100 -0
- package/src/app/settings/providers-settings.tsx +258 -0
- package/src/app/settings/sessions-settings.tsx +276 -0
- package/src/app/settings/toolset-config-panel.test.tsx +289 -0
- package/src/app/settings/toolset-config-panel.tsx +449 -0
- package/src/app/settings/types.ts +42 -0
- package/src/app/settings/uninstall-section.tsx +185 -0
- package/src/app/settings/use-deep-link-highlight.ts +60 -0
- package/src/app/shell/app-shell.tsx +167 -0
- package/src/app/shell/gateway-menu-panel.tsx +150 -0
- package/src/app/shell/hooks/use-overlay-routing.ts +71 -0
- package/src/app/shell/hooks/use-status-snapshot.ts +57 -0
- package/src/app/shell/hooks/use-statusbar-items.tsx +403 -0
- package/src/app/shell/keybind-panel.tsx +220 -0
- package/src/app/shell/model-edit-submenu.test.tsx +84 -0
- package/src/app/shell/model-edit-submenu.tsx +245 -0
- package/src/app/shell/model-menu-panel.tsx +295 -0
- package/src/app/shell/sidebar-label.tsx +22 -0
- package/src/app/shell/statusbar-controls.tsx +185 -0
- package/src/app/shell/titlebar-controls.tsx +244 -0
- package/src/app/shell/titlebar.test.ts +26 -0
- package/src/app/shell/titlebar.ts +45 -0
- package/src/app/shell/use-group-registry.ts +39 -0
- package/src/app/skills/index.test.tsx +103 -0
- package/src/app/skills/index.tsx +371 -0
- package/src/app/types.ts +99 -0
- package/src/app/updates-overlay.tsx +369 -0
- package/src/components/Backdrop.tsx +114 -0
- package/src/components/assistant-ui/ansi-text.tsx +34 -0
- package/src/components/assistant-ui/clarify-tool.tsx +281 -0
- package/src/components/assistant-ui/directive-text.test.ts +39 -0
- package/src/components/assistant-ui/directive-text.tsx +389 -0
- package/src/components/assistant-ui/markdown-text.test.ts +204 -0
- package/src/components/assistant-ui/markdown-text.tsx +497 -0
- package/src/components/assistant-ui/message-render-boundary.test.tsx +80 -0
- package/src/components/assistant-ui/message-render-boundary.tsx +48 -0
- package/src/components/assistant-ui/streaming.test.tsx +739 -0
- package/src/components/assistant-ui/thread-list.tsx +307 -0
- package/src/components/assistant-ui/thread-virtualizer.tsx +512 -0
- package/src/components/assistant-ui/thread.tsx +1474 -0
- package/src/components/assistant-ui/todo-tool.tsx +109 -0
- package/src/components/assistant-ui/tool-approval-group.test.tsx +158 -0
- package/src/components/assistant-ui/tool-approval.test.tsx +81 -0
- package/src/components/assistant-ui/tool-approval.tsx +209 -0
- package/src/components/assistant-ui/tool-fallback-model.test.ts +66 -0
- package/src/components/assistant-ui/tool-fallback-model.ts +1368 -0
- package/src/components/assistant-ui/tool-fallback.tsx +466 -0
- package/src/components/assistant-ui/tooltip-icon-button.tsx +33 -0
- package/src/components/assistant-ui/user-message-edit.test.tsx +141 -0
- package/src/components/assistant-ui/user-message-text.tsx +150 -0
- package/src/components/boot-failure-overlay.tsx +246 -0
- package/src/components/boot-failure-reauth.test.ts +100 -0
- package/src/components/boot-failure-reauth.ts +81 -0
- package/src/components/brand-mark.tsx +19 -0
- package/src/components/chat/activity-timer-text.tsx +24 -0
- package/src/components/chat/activity-timer.test.tsx +43 -0
- package/src/components/chat/activity-timer.ts +64 -0
- package/src/components/chat/code-card.tsx +78 -0
- package/src/components/chat/compact-markdown.tsx +113 -0
- package/src/components/chat/composer-dock.ts +31 -0
- package/src/components/chat/diff-lines.tsx +54 -0
- package/src/components/chat/disclosure-row.tsx +63 -0
- package/src/components/chat/generated-image-context.tsx +19 -0
- package/src/components/chat/generated-image-result.tsx +174 -0
- package/src/components/chat/image-generation-placeholder.tsx +279 -0
- package/src/components/chat/intro-copy.jsonl +75 -0
- package/src/components/chat/intro.tsx +182 -0
- package/src/components/chat/preview-attachment.tsx +125 -0
- package/src/components/chat/shiki-highlighter.tsx +107 -0
- package/src/components/chat/status-row.tsx +70 -0
- package/src/components/chat/status-section.tsx +42 -0
- package/src/components/chat/terminal-output.tsx +50 -0
- package/src/components/chat/zoomable-image.tsx +177 -0
- package/src/components/desktop-install-overlay.tsx +595 -0
- package/src/components/desktop-onboarding-overlay.test.tsx +100 -0
- package/src/components/desktop-onboarding-overlay.tsx +1286 -0
- package/src/components/error-boundary.tsx +77 -0
- package/src/components/gateway-connecting-overlay.test.tsx +143 -0
- package/src/components/gateway-connecting-overlay.tsx +183 -0
- package/src/components/haptics-provider.tsx +19 -0
- package/src/components/language-switcher.test.tsx +53 -0
- package/src/components/language-switcher.tsx +175 -0
- package/src/components/model-picker.tsx +340 -0
- package/src/components/model-visibility-dialog.tsx +155 -0
- package/src/components/notifications.tsx +196 -0
- package/src/components/page-loader.tsx +34 -0
- package/src/components/pane-shell/context.ts +14 -0
- package/src/components/pane-shell/index.ts +4 -0
- package/src/components/pane-shell/pane-shell.test.tsx +333 -0
- package/src/components/pane-shell/pane-shell.tsx +330 -0
- package/src/components/prompt-overlays.tsx +234 -0
- package/src/components/session-picker.tsx +108 -0
- package/src/components/status-dot.tsx +26 -0
- package/src/components/ui/action-status.tsx +25 -0
- package/src/components/ui/alert.tsx +53 -0
- package/src/components/ui/badge.tsx +35 -0
- package/src/components/ui/braille-spinner.tsx +61 -0
- package/src/components/ui/button.tsx +81 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/codicon.tsx +20 -0
- package/src/components/ui/command.tsx +111 -0
- package/src/components/ui/confirm-dialog.tsx +109 -0
- package/src/components/ui/context-menu.tsx +141 -0
- package/src/components/ui/control.ts +25 -0
- package/src/components/ui/copy-button.test.tsx +36 -0
- package/src/components/ui/copy-button.tsx +229 -0
- package/src/components/ui/dialog.tsx +152 -0
- package/src/components/ui/disclosure-caret.tsx +20 -0
- package/src/components/ui/dropdown-menu.tsx +291 -0
- package/src/components/ui/error-state.tsx +50 -0
- package/src/components/ui/fade-text.tsx +110 -0
- package/src/components/ui/glyph-spinner.tsx +63 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/kbd.tsx +37 -0
- package/src/components/ui/loader.tsx +558 -0
- package/src/components/ui/log-view.tsx +17 -0
- package/src/components/ui/pagination.tsx +114 -0
- package/src/components/ui/popover.tsx +44 -0
- package/src/components/ui/scroll-area.tsx +43 -0
- package/src/components/ui/search-field.tsx +80 -0
- package/src/components/ui/segmented-control.tsx +51 -0
- package/src/components/ui/select.tsx +92 -0
- package/src/components/ui/separator.tsx +26 -0
- package/src/components/ui/sheet.tsx +116 -0
- package/src/components/ui/sidebar.tsx +674 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/switch.tsx +49 -0
- package/src/components/ui/tabs.tsx +36 -0
- package/src/components/ui/text-tab.tsx +43 -0
- package/src/components/ui/textarea.tsx +11 -0
- package/src/components/ui/tool-icon.tsx +65 -0
- package/src/components/ui/tooltip.tsx +69 -0
- package/src/fonts/JetBrainsMono-Bold.woff2 +0 -0
- package/src/fonts/JetBrainsMono-Italic.woff2 +0 -0
- package/src/fonts/JetBrainsMono-Regular.woff2 +0 -0
- package/src/global.d.ts +457 -0
- package/src/hooks/use-image-download.ts +85 -0
- package/src/hooks/use-media-query.ts +24 -0
- package/src/hooks/use-mobile.ts +3 -0
- package/src/hooks/use-resize-observer.ts +38 -0
- package/src/hooks/use-worktree-info.ts +68 -0
- package/src/i18n/catalog.ts +12 -0
- package/src/i18n/context.test.tsx +232 -0
- package/src/i18n/context.tsx +183 -0
- package/src/i18n/define-locale.ts +41 -0
- package/src/i18n/en.ts +1779 -0
- package/src/i18n/index.ts +20 -0
- package/src/i18n/ja.ts +1890 -0
- package/src/i18n/languages.test.ts +43 -0
- package/src/i18n/languages.ts +86 -0
- package/src/i18n/runtime.test.ts +75 -0
- package/src/i18n/runtime.ts +53 -0
- package/src/i18n/types.ts +1452 -0
- package/src/i18n/zh-hant.ts +1849 -0
- package/src/i18n/zh.ts +1923 -0
- package/src/lib/ansi.test.ts +123 -0
- package/src/lib/ansi.ts +175 -0
- package/src/lib/chat-messages.test.ts +708 -0
- package/src/lib/chat-messages.ts +885 -0
- package/src/lib/chat-runtime.test.ts +18 -0
- package/src/lib/chat-runtime.ts +335 -0
- package/src/lib/clipboard.ts +28 -0
- package/src/lib/commit-changelog.test.ts +114 -0
- package/src/lib/commit-changelog.ts +177 -0
- package/src/lib/completion-sound.ts +519 -0
- package/src/lib/desktop-fs.test.ts +116 -0
- package/src/lib/desktop-fs.ts +113 -0
- package/src/lib/desktop-slash-commands.test.ts +126 -0
- package/src/lib/desktop-slash-commands.ts +286 -0
- package/src/lib/embedded-images.test.ts +35 -0
- package/src/lib/embedded-images.ts +60 -0
- package/src/lib/external-link.test.tsx +168 -0
- package/src/lib/external-link.tsx +303 -0
- package/src/lib/gateway-events.test.ts +27 -0
- package/src/lib/gateway-events.ts +49 -0
- package/src/lib/gateway-ws-url.test.ts +78 -0
- package/src/lib/gateway-ws-url.ts +91 -0
- package/src/lib/generated-images.test.ts +97 -0
- package/src/lib/generated-images.ts +116 -0
- package/src/lib/haptics.ts +129 -0
- package/src/lib/icons.ts +203 -0
- package/src/lib/incremental-external-store-runtime.ts +188 -0
- package/src/lib/katex-memo.ts +260 -0
- package/src/lib/keybinds/actions.ts +125 -0
- package/src/lib/keybinds/combo.test.ts +86 -0
- package/src/lib/keybinds/combo.ts +169 -0
- package/src/lib/local-preview.ts +126 -0
- package/src/lib/markdown-code.test.ts +23 -0
- package/src/lib/markdown-code.ts +195 -0
- package/src/lib/markdown-preprocess.ts +386 -0
- package/src/lib/media.remote.test.ts +58 -0
- package/src/lib/media.ts +111 -0
- package/src/lib/model-status-label.test.ts +31 -0
- package/src/lib/model-status-label.ts +103 -0
- package/src/lib/mutable-ref.ts +6 -0
- package/src/lib/preview-targets.test.ts +27 -0
- package/src/lib/preview-targets.ts +63 -0
- package/src/lib/profile-color.ts +58 -0
- package/src/lib/provider-setup-errors.test.ts +26 -0
- package/src/lib/provider-setup-errors.ts +12 -0
- package/src/lib/query-client.ts +13 -0
- package/src/lib/remend-tail.test.ts +105 -0
- package/src/lib/remend-tail.ts +108 -0
- package/src/lib/runtime-readiness.test.ts +65 -0
- package/src/lib/runtime-readiness.ts +147 -0
- package/src/lib/session-export.ts +57 -0
- package/src/lib/session-search.test.ts +58 -0
- package/src/lib/session-search.ts +19 -0
- package/src/lib/session-source.ts +62 -0
- package/src/lib/speech-text.ts +35 -0
- package/src/lib/statusbar.ts +91 -0
- package/src/lib/storage.test.ts +25 -0
- package/src/lib/storage.ts +107 -0
- package/src/lib/todos.test.ts +35 -0
- package/src/lib/todos.ts +51 -0
- package/src/lib/tool-result-summary.test.ts +106 -0
- package/src/lib/tool-result-summary.ts +467 -0
- package/src/lib/update-copy.test.ts +38 -0
- package/src/lib/update-copy.ts +44 -0
- package/src/lib/use-enter-animation.ts +100 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/voice-playback.ts +128 -0
- package/src/lib/yolo-session.ts +26 -0
- package/src/main.tsx +43 -0
- package/src/nastech.test.ts +49 -0
- package/src/nastech.ts +718 -0
- package/src/store/activity.ts +100 -0
- package/src/store/boot.ts +91 -0
- package/src/store/clarify.test.ts +81 -0
- package/src/store/clarify.ts +69 -0
- package/src/store/command-palette.ts +20 -0
- package/src/store/compaction.test.ts +53 -0
- package/src/store/compaction.ts +38 -0
- package/src/store/completion-sound.ts +32 -0
- package/src/store/composer-input-history.test.ts +147 -0
- package/src/store/composer-input-history.ts +158 -0
- package/src/store/composer-queue.test.ts +148 -0
- package/src/store/composer-queue.ts +239 -0
- package/src/store/composer-status.test.ts +99 -0
- package/src/store/composer-status.ts +277 -0
- package/src/store/composer.test.ts +106 -0
- package/src/store/composer.ts +184 -0
- package/src/store/cron.ts +19 -0
- package/src/store/gateway.ts +290 -0
- package/src/store/haptics.ts +17 -0
- package/src/store/keybinds.ts +139 -0
- package/src/store/layout.ts +176 -0
- package/src/store/model-presets.test.ts +51 -0
- package/src/store/model-presets.ts +86 -0
- package/src/store/model-visibility.test.ts +37 -0
- package/src/store/model-visibility.ts +108 -0
- package/src/store/native-notifications.test.ts +192 -0
- package/src/store/native-notifications.ts +203 -0
- package/src/store/notifications.ts +165 -0
- package/src/store/onboarding.test.ts +372 -0
- package/src/store/onboarding.ts +866 -0
- package/src/store/panes.test.ts +146 -0
- package/src/store/panes.ts +145 -0
- package/src/store/preview.test.ts +135 -0
- package/src/store/preview.ts +466 -0
- package/src/store/profile.test.ts +89 -0
- package/src/store/profile.ts +365 -0
- package/src/store/prompts.test.ts +121 -0
- package/src/store/prompts.ts +115 -0
- package/src/store/session-switcher.test.ts +115 -0
- package/src/store/session-switcher.ts +128 -0
- package/src/store/session-sync.ts +25 -0
- package/src/store/session.test.ts +131 -0
- package/src/store/session.ts +255 -0
- package/src/store/subagents.test.ts +111 -0
- package/src/store/subagents.ts +260 -0
- package/src/store/thread-scroll.ts +46 -0
- package/src/store/todos.test.ts +47 -0
- package/src/store/todos.ts +64 -0
- package/src/store/tool-diffs.ts +23 -0
- package/src/store/tool-dismiss.ts +45 -0
- package/src/store/tool-view.ts +91 -0
- package/src/store/translucency.ts +38 -0
- package/src/store/updates.test.ts +77 -0
- package/src/store/updates.ts +315 -0
- package/src/store/voice-playback.ts +24 -0
- package/src/store/windows.test.ts +143 -0
- package/src/store/windows.ts +77 -0
- package/src/styles.css +1235 -0
- package/src/themes/color.ts +142 -0
- package/src/themes/context.tsx +339 -0
- package/src/themes/index.ts +3 -0
- package/src/themes/install.test.ts +119 -0
- package/src/themes/install.ts +95 -0
- package/src/themes/presets.test.ts +33 -0
- package/src/themes/presets.ts +293 -0
- package/src/themes/profile-theme.test.ts +41 -0
- package/src/themes/types.ts +66 -0
- package/src/themes/use-skin-command.ts +60 -0
- package/src/themes/user-themes.test.ts +63 -0
- package/src/themes/user-themes.ts +122 -0
- package/src/themes/vscode.test.ts +171 -0
- package/src/themes/vscode.ts +343 -0
- package/src/types/nastech.ts +646 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +25 -0
- package/vite.config.ts +56 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* desktop-uninstall.cjs
|
|
3
|
+
*
|
|
4
|
+
* Pure, electron-free helpers for the desktop Chat GUI uninstaller. These map
|
|
5
|
+
* the three user-facing uninstall modes to the `nastech uninstall` CLI flags,
|
|
6
|
+
* resolve the running app bundle/exe so a detached cleanup script can remove
|
|
7
|
+
* it after the app quits, and build that cleanup script for each OS.
|
|
8
|
+
*
|
|
9
|
+
* Kept standalone (no `require('electron')`) so it can be unit-tested with
|
|
10
|
+
* `node --test` — same pattern as connection-config.cjs / backend-probes.cjs.
|
|
11
|
+
* main.cjs requires these and wires them into the electron-coupled IPC layer.
|
|
12
|
+
*
|
|
13
|
+
* The three modes mirror the CLI's options exactly:
|
|
14
|
+
* - 'gui' → remove ONLY the Chat GUI, keep the agent + all user data.
|
|
15
|
+
* `nastech uninstall --gui --yes`
|
|
16
|
+
* - 'lite' → remove the GUI + agent code, KEEP user data (config / sessions
|
|
17
|
+
* / .env) for a future reinstall. `nastech uninstall --yes`
|
|
18
|
+
* - 'full' → remove everything: GUI + agent + all user data.
|
|
19
|
+
* `nastech uninstall --full --yes`
|
|
20
|
+
*
|
|
21
|
+
* Why a detached cleanup script: 'lite'/'full' delete the very venv the
|
|
22
|
+
* `nastech` command runs from, and every mode may need to delete the running
|
|
23
|
+
* app bundle (locked on macOS/Windows while the process is alive). So we hand
|
|
24
|
+
* the work to a detached child that waits for this app's PID to exit, runs the
|
|
25
|
+
* Python uninstall, then removes the app bundle — then the app quits. Same
|
|
26
|
+
* shape as the self-update swap-and-relaunch flow already in main.cjs.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const path = require('node:path')
|
|
30
|
+
|
|
31
|
+
const UNINSTALL_MODES = ['gui', 'lite', 'full']
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Map an uninstall mode to the `python -m nastech_cli.uninstall` argv (after the
|
|
35
|
+
* python executable). Uses the dedicated lightweight module entrypoint (not
|
|
36
|
+
* `nastech_cli.main`) so it can run under a system Python OUTSIDE the venv that
|
|
37
|
+
* lite/full delete — see the Finding-3 note in buildWindowsCleanupScript.
|
|
38
|
+
* Throws on an unknown mode so a typo can't silently become a full wipe.
|
|
39
|
+
*/
|
|
40
|
+
function uninstallArgsForMode(mode) {
|
|
41
|
+
if (!UNINSTALL_MODES.includes(mode)) {
|
|
42
|
+
throw new Error(`Unknown uninstall mode: ${mode}`)
|
|
43
|
+
}
|
|
44
|
+
return ['-m', 'nastech_cli.uninstall', '--mode', mode]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** True when `mode` removes the agent (lite/full), false for gui-only. */
|
|
48
|
+
function modeRemovesAgent(mode) {
|
|
49
|
+
return mode === 'lite' || mode === 'full'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** True when `mode` removes user data (full only). */
|
|
53
|
+
function modeRemovesUserData(mode) {
|
|
54
|
+
return mode === 'full'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the on-disk app bundle/dir to remove for the running desktop app,
|
|
59
|
+
* given the path to the running executable (`process.execPath`) and platform.
|
|
60
|
+
*
|
|
61
|
+
* macOS: …/NasTech.app/Contents/MacOS/NasTech → …/NasTech.app
|
|
62
|
+
* Windows: …\NasTech\NasTech.exe → …\NasTech (install dir)
|
|
63
|
+
* Linux: AppImage → the APPIMAGE env path; unpacked → the *-unpacked dir
|
|
64
|
+
*
|
|
65
|
+
* Returns null when we can't confidently identify a removable bundle (e.g.
|
|
66
|
+
* running from a dev checkout, or a system-package install we must not rmtree).
|
|
67
|
+
*/
|
|
68
|
+
function resolveRemovableAppPath(execPath, platform, env = {}) {
|
|
69
|
+
const exe = String(execPath || '')
|
|
70
|
+
if (!exe) return null
|
|
71
|
+
|
|
72
|
+
// Use the path flavor that matches the TARGET platform, not the host running
|
|
73
|
+
// this code — so the Windows branch parses backslash paths correctly even
|
|
74
|
+
// when these pure helpers are unit-tested on Linux/macOS CI.
|
|
75
|
+
const p = platform === 'win32' ? path.win32 : path.posix
|
|
76
|
+
|
|
77
|
+
if (platform === 'darwin') {
|
|
78
|
+
// …/NasTech.app/Contents/MacOS/NasTech → strip 3 segments to the .app
|
|
79
|
+
const macOsDir = p.dirname(exe) // …/Contents/MacOS
|
|
80
|
+
const contents = p.dirname(macOsDir) // …/Contents
|
|
81
|
+
const appBundle = p.dirname(contents) // …/NasTech.app
|
|
82
|
+
if (appBundle.endsWith('.app')) return appBundle
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (platform === 'win32') {
|
|
87
|
+
// NSIS per-user installs NasTech.exe directly in the install dir.
|
|
88
|
+
const dir = p.dirname(exe)
|
|
89
|
+
if (/[\\/]NasTech$/i.test(dir) || /[\\/]nastech-desktop$/i.test(dir)) return dir
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Linux: an AppImage exposes its own path via the APPIMAGE env var.
|
|
94
|
+
if (env.APPIMAGE) return env.APPIMAGE
|
|
95
|
+
// Unpacked electron-builder tree: …/linux-unpacked/nastech
|
|
96
|
+
const dir = p.dirname(exe)
|
|
97
|
+
if (/-unpacked$/.test(dir)) return dir
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Should we even try to remove the running app bundle from a cleanup script?
|
|
103
|
+
* Only when packaged AND we resolved a concrete removable path. Dev runs
|
|
104
|
+
* (electron from node_modules) and system-package installs return null above
|
|
105
|
+
* and are left to the OS package manager.
|
|
106
|
+
*/
|
|
107
|
+
function shouldRemoveAppBundle(isPackaged, appPath) {
|
|
108
|
+
return Boolean(isPackaged) && Boolean(appPath)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Build a POSIX cleanup shell script (macOS / Linux). It:
|
|
113
|
+
* 1. waits (bounded ~30s) for the desktop PID to exit (venv/bundle unlock),
|
|
114
|
+
* 2. runs the Python uninstall module with the mode,
|
|
115
|
+
* 3. removes the app bundle if one was resolved.
|
|
116
|
+
*
|
|
117
|
+
* `pythonExe` should be a Python OUTSIDE the venv for lite/full (the venv is
|
|
118
|
+
* being deleted); `pythonPath` is prepended to PYTHONPATH so `import nastech_cli`
|
|
119
|
+
* resolves from the agent source. `q()` single-quote-escapes for the shell
|
|
120
|
+
* (closes-escapes-reopens any embedded apostrophe), defending against spaces.
|
|
121
|
+
*/
|
|
122
|
+
function buildPosixCleanupScript({ desktopPid, pythonExe, pythonPath, agentRoot, uninstallArgs, appPath, nastechHome }) {
|
|
123
|
+
const q = s => `'${String(s).replace(/'/g, `'\\''`)}'`
|
|
124
|
+
const lines = [
|
|
125
|
+
'#!/bin/bash',
|
|
126
|
+
'set -u',
|
|
127
|
+
'# Wait (up to ~30s) for the desktop process to exit so the venv python',
|
|
128
|
+
'# and the app bundle are no longer in use.',
|
|
129
|
+
`pid=${Number(desktopPid) || 0}`,
|
|
130
|
+
'if [ "$pid" -gt 0 ]; then',
|
|
131
|
+
' for _ in $(seq 1 60); do',
|
|
132
|
+
' kill -0 "$pid" 2>/dev/null || break',
|
|
133
|
+
' sleep 0.5',
|
|
134
|
+
' done',
|
|
135
|
+
'fi',
|
|
136
|
+
`export NASTECH_HOME=${q(nastechHome)}`
|
|
137
|
+
]
|
|
138
|
+
if (pythonPath) {
|
|
139
|
+
lines.push(`export PYTHONPATH=${q(pythonPath)}\${PYTHONPATH:+:$PYTHONPATH}`)
|
|
140
|
+
}
|
|
141
|
+
lines.push(
|
|
142
|
+
`cd ${q(agentRoot)} 2>/dev/null || true`,
|
|
143
|
+
`${q(pythonExe)} ${uninstallArgs.map(q).join(' ')} || true`
|
|
144
|
+
)
|
|
145
|
+
if (appPath) {
|
|
146
|
+
lines.push(`rm -rf ${q(appPath)} || true`)
|
|
147
|
+
}
|
|
148
|
+
// Self-delete the script.
|
|
149
|
+
lines.push('rm -f "$0" 2>/dev/null || true')
|
|
150
|
+
lines.push('')
|
|
151
|
+
return lines.join('\n')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Build a Windows cleanup batch script. Same three steps, cmd.exe flavored.
|
|
156
|
+
*
|
|
157
|
+
* Finding 3 (venv self-deletion): for lite/full the agent uninstall rmtree's
|
|
158
|
+
* the venv that contains `python.exe`. A running .exe is mandatory-locked on
|
|
159
|
+
* Windows, so running the uninstall from the venv's OWN python half-fails. The
|
|
160
|
+
* desktop passes a system Python (findSystemPython) as `pythonExe` for those
|
|
161
|
+
* modes + `pythonPath`=agentRoot so `import nastech_cli` resolves from source
|
|
162
|
+
* while the venv is torn down. gui-only doesn't touch the venv, so it can use
|
|
163
|
+
* either interpreter.
|
|
164
|
+
*
|
|
165
|
+
* Wait-loop: bounded (matches POSIX's ~30s cap) so a never-exiting / mismatched
|
|
166
|
+
* PID can't wedge the cleanup forever. The `/FI "PID eq"` filter is an EXACT
|
|
167
|
+
* match, so no redundant `| find` (which would substring-match 99→990).
|
|
168
|
+
*
|
|
169
|
+
* Removal: even after the desktop PID is gone, Windows releases directory
|
|
170
|
+
* handles lazily, so a single `rmdir /s /q` can half-fail — retry up to 10x.
|
|
171
|
+
*/
|
|
172
|
+
function buildWindowsCleanupScript({ desktopPid, pythonExe, pythonPath, agentRoot, uninstallArgs, appPath, nastechHome }) {
|
|
173
|
+
const pid = Number(desktopPid) || 0
|
|
174
|
+
// cmd.exe has no string escaping inside quotes; strip embedded quotes (paths
|
|
175
|
+
// under %LOCALAPPDATA% never contain them). `&`/`^` in a path would still be
|
|
176
|
+
// a problem, but NasTech install paths don't use them.
|
|
177
|
+
const q = s => `"${String(s).replace(/"/g, '')}"`
|
|
178
|
+
const lines = [
|
|
179
|
+
'@echo off',
|
|
180
|
+
'setlocal enableextensions',
|
|
181
|
+
`set "NASTECH_HOME=${String(nastechHome).replace(/"/g, '')}"`,
|
|
182
|
+
`set "PID=${pid}"`
|
|
183
|
+
]
|
|
184
|
+
if (pythonPath) {
|
|
185
|
+
lines.push(`set "PYTHONPATH=${String(pythonPath).replace(/"/g, '')};%PYTHONPATH%"`)
|
|
186
|
+
}
|
|
187
|
+
lines.push(
|
|
188
|
+
'set /a waited=0',
|
|
189
|
+
':waitloop',
|
|
190
|
+
'rem /FI "PID eq %PID%" is an EXACT filter — tasklist outputs the one task',
|
|
191
|
+
'rem row for that PID, or "INFO: No tasks..." otherwise. /NH drops the',
|
|
192
|
+
'rem header; findstr matches the PID as a whole space-delimited token so',
|
|
193
|
+
'rem PID 99 cannot match 990 (the substring trap of a bare `find`).',
|
|
194
|
+
'tasklist /NH /FI "PID eq %PID%" 2>nul | findstr /r /c:" %PID% " >nul',
|
|
195
|
+
'if %ERRORLEVEL% neq 0 goto waited_done',
|
|
196
|
+
'set /a waited+=1',
|
|
197
|
+
'if %waited% geq 60 goto waited_done',
|
|
198
|
+
'timeout /t 1 /nobreak >nul',
|
|
199
|
+
'goto waitloop',
|
|
200
|
+
':waited_done',
|
|
201
|
+
`cd /d ${q(agentRoot)}`,
|
|
202
|
+
`${q(pythonExe)} ${uninstallArgs.map(q).join(' ')}`
|
|
203
|
+
)
|
|
204
|
+
if (appPath) {
|
|
205
|
+
lines.push(
|
|
206
|
+
'set /a tries=0',
|
|
207
|
+
':rmloop',
|
|
208
|
+
`if not exist ${q(appPath)} goto rmdone`,
|
|
209
|
+
`rmdir /s /q ${q(appPath)} >nul 2>&1`,
|
|
210
|
+
`if not exist ${q(appPath)} goto rmdone`,
|
|
211
|
+
'set /a tries+=1',
|
|
212
|
+
'if %tries% geq 10 goto rmdone',
|
|
213
|
+
'timeout /t 1 /nobreak >nul',
|
|
214
|
+
'goto rmloop',
|
|
215
|
+
':rmdone'
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
lines.push('del "%~f0"')
|
|
219
|
+
lines.push('')
|
|
220
|
+
return lines.join('\r\n')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = {
|
|
224
|
+
UNINSTALL_MODES,
|
|
225
|
+
buildPosixCleanupScript,
|
|
226
|
+
buildWindowsCleanupScript,
|
|
227
|
+
modeRemovesAgent,
|
|
228
|
+
modeRemovesUserData,
|
|
229
|
+
resolveRemovableAppPath,
|
|
230
|
+
shouldRemoveAppBundle,
|
|
231
|
+
uninstallArgsForMode
|
|
232
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for electron/desktop-uninstall.cjs.
|
|
3
|
+
*
|
|
4
|
+
* Run with: node --test electron/desktop-uninstall.test.cjs
|
|
5
|
+
* (Wired into npm test:desktop:platforms in package.json.)
|
|
6
|
+
*
|
|
7
|
+
* These are the pure helpers behind the desktop Chat GUI uninstaller: the
|
|
8
|
+
* mode → CLI-flag mapping, the running-app-bundle resolution per OS, and the
|
|
9
|
+
* cleanup-script builders (POSIX + Windows).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const test = require('node:test')
|
|
13
|
+
const assert = require('node:assert/strict')
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
UNINSTALL_MODES,
|
|
17
|
+
buildPosixCleanupScript,
|
|
18
|
+
buildWindowsCleanupScript,
|
|
19
|
+
modeRemovesAgent,
|
|
20
|
+
modeRemovesUserData,
|
|
21
|
+
resolveRemovableAppPath,
|
|
22
|
+
shouldRemoveAppBundle,
|
|
23
|
+
uninstallArgsForMode
|
|
24
|
+
} = require('./desktop-uninstall.cjs')
|
|
25
|
+
|
|
26
|
+
// --- uninstallArgsForMode ---
|
|
27
|
+
|
|
28
|
+
test('uninstallArgsForMode maps each mode to the module-runner argv', () => {
|
|
29
|
+
assert.deepEqual(uninstallArgsForMode('gui'), ['-m', 'nastech_cli.uninstall', '--mode', 'gui'])
|
|
30
|
+
assert.deepEqual(uninstallArgsForMode('lite'), ['-m', 'nastech_cli.uninstall', '--mode', 'lite'])
|
|
31
|
+
assert.deepEqual(uninstallArgsForMode('full'), ['-m', 'nastech_cli.uninstall', '--mode', 'full'])
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('uninstallArgsForMode throws on an unknown mode (no silent full wipe)', () => {
|
|
35
|
+
assert.throws(() => uninstallArgsForMode('nuke'), /Unknown uninstall mode/)
|
|
36
|
+
assert.throws(() => uninstallArgsForMode(''), /Unknown uninstall mode/)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('UNINSTALL_MODES lists exactly the three supported modes', () => {
|
|
40
|
+
assert.deepEqual([...UNINSTALL_MODES].sort(), ['full', 'gui', 'lite'])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// --- modeRemovesAgent / modeRemovesUserData ---
|
|
44
|
+
|
|
45
|
+
test('mode predicates classify what each mode removes', () => {
|
|
46
|
+
assert.equal(modeRemovesAgent('gui'), false)
|
|
47
|
+
assert.equal(modeRemovesAgent('lite'), true)
|
|
48
|
+
assert.equal(modeRemovesAgent('full'), true)
|
|
49
|
+
|
|
50
|
+
assert.equal(modeRemovesUserData('gui'), false)
|
|
51
|
+
assert.equal(modeRemovesUserData('lite'), false)
|
|
52
|
+
assert.equal(modeRemovesUserData('full'), true)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// --- resolveRemovableAppPath ---
|
|
56
|
+
|
|
57
|
+
test('resolveRemovableAppPath finds the .app bundle on macOS', () => {
|
|
58
|
+
assert.equal(
|
|
59
|
+
resolveRemovableAppPath('/Applications/NasTech.app/Contents/MacOS/NasTech', 'darwin'),
|
|
60
|
+
'/Applications/NasTech.app'
|
|
61
|
+
)
|
|
62
|
+
assert.equal(
|
|
63
|
+
resolveRemovableAppPath('/Users/x/Applications/NasTech.app/Contents/MacOS/NasTech', 'darwin'),
|
|
64
|
+
'/Users/x/Applications/NasTech.app'
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('resolveRemovableAppPath: dev-run .app resolves (safety is shouldRemoveAppBundle, not null)', () => {
|
|
69
|
+
// A dev run from node_modules' Electron DOES resolve to a .app — the real
|
|
70
|
+
// dev-run safety gate is shouldRemoveAppBundle(isPackaged=false,...), not a
|
|
71
|
+
// null return here. This test documents that contract.
|
|
72
|
+
assert.equal(
|
|
73
|
+
resolveRemovableAppPath('/repo/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron', 'darwin'),
|
|
74
|
+
'/repo/node_modules/electron/dist/Electron.app'
|
|
75
|
+
)
|
|
76
|
+
assert.equal(shouldRemoveAppBundle(false, '/repo/node_modules/electron/dist/Electron.app'), false)
|
|
77
|
+
// A bare path with no .app ancestor → null.
|
|
78
|
+
assert.equal(resolveRemovableAppPath('/usr/bin/electron', 'darwin'), null)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('resolveRemovableAppPath finds the install dir on Windows', () => {
|
|
82
|
+
assert.equal(
|
|
83
|
+
resolveRemovableAppPath('C:\\Users\\x\\AppData\\Local\\Programs\\NasTech\\NasTech.exe', 'win32'),
|
|
84
|
+
'C:\\Users\\x\\AppData\\Local\\Programs\\NasTech'
|
|
85
|
+
)
|
|
86
|
+
assert.equal(
|
|
87
|
+
resolveRemovableAppPath('C:\\Users\\x\\AppData\\Local\\nastech-desktop\\NasTech.exe', 'win32'),
|
|
88
|
+
'C:\\Users\\x\\AppData\\Local\\nastech-desktop'
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('resolveRemovableAppPath returns null for an unrecognized Windows dir', () => {
|
|
93
|
+
assert.equal(resolveRemovableAppPath('C:\\Temp\\foo\\NasTech.exe', 'win32'), null)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('resolveRemovableAppPath uses APPIMAGE on Linux when set', () => {
|
|
97
|
+
assert.equal(
|
|
98
|
+
resolveRemovableAppPath('/tmp/.mount_NasTechXXXX/nastech', 'linux', { APPIMAGE: '/home/x/Apps/NasTech.AppImage' }),
|
|
99
|
+
'/home/x/Apps/NasTech.AppImage'
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('resolveRemovableAppPath finds the unpacked dir on Linux', () => {
|
|
104
|
+
assert.equal(
|
|
105
|
+
resolveRemovableAppPath('/opt/nastech/linux-unpacked/nastech', 'linux', {}),
|
|
106
|
+
'/opt/nastech/linux-unpacked'
|
|
107
|
+
)
|
|
108
|
+
// A system-package install (/usr/bin) → null, left to apt/dnf.
|
|
109
|
+
assert.equal(resolveRemovableAppPath('/usr/bin/nastech', 'linux', {}), null)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('resolveRemovableAppPath returns null for an empty exe path', () => {
|
|
113
|
+
assert.equal(resolveRemovableAppPath('', 'darwin'), null)
|
|
114
|
+
assert.equal(resolveRemovableAppPath(null, 'win32'), null)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// --- shouldRemoveAppBundle ---
|
|
118
|
+
|
|
119
|
+
test('shouldRemoveAppBundle requires packaged AND a resolved path', () => {
|
|
120
|
+
assert.equal(shouldRemoveAppBundle(true, '/Applications/NasTech.app'), true)
|
|
121
|
+
assert.equal(shouldRemoveAppBundle(false, '/Applications/NasTech.app'), false)
|
|
122
|
+
assert.equal(shouldRemoveAppBundle(true, null), false)
|
|
123
|
+
assert.equal(shouldRemoveAppBundle(false, null), false)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// --- buildPosixCleanupScript ---
|
|
127
|
+
|
|
128
|
+
test('buildPosixCleanupScript waits for the PID, runs the uninstall module, removes bundle', () => {
|
|
129
|
+
const script = buildPosixCleanupScript({
|
|
130
|
+
desktopPid: 4321,
|
|
131
|
+
pythonExe: '/home/x/.nastech/nastech-agent/venv/bin/python',
|
|
132
|
+
pythonPath: null,
|
|
133
|
+
agentRoot: '/home/x/.nastech/nastech-agent',
|
|
134
|
+
uninstallArgs: ['-m', 'nastech_cli.uninstall', '--mode', 'gui'],
|
|
135
|
+
appPath: '/opt/nastech/linux-unpacked',
|
|
136
|
+
nastechHome: '/home/x/.nastech'
|
|
137
|
+
})
|
|
138
|
+
assert.match(script, /^#!\/bin\/bash/)
|
|
139
|
+
assert.match(script, /pid=4321/)
|
|
140
|
+
assert.match(script, /kill -0 "\$pid"/)
|
|
141
|
+
// bounded wait (~30s), not unbounded
|
|
142
|
+
assert.match(script, /seq 1 60/)
|
|
143
|
+
assert.match(script, /'-m' 'nastech_cli\.uninstall' '--mode' 'gui'/)
|
|
144
|
+
assert.match(script, /rm -rf '\/opt\/nastech\/linux-unpacked'/)
|
|
145
|
+
assert.match(script, /export NASTECH_HOME='\/home\/x\/\.nastech'/)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('buildPosixCleanupScript exports PYTHONPATH when pythonPath is set (lite/full)', () => {
|
|
149
|
+
const script = buildPosixCleanupScript({
|
|
150
|
+
desktopPid: 1,
|
|
151
|
+
pythonExe: '/usr/bin/python3',
|
|
152
|
+
pythonPath: '/home/x/.nastech/nastech-agent',
|
|
153
|
+
agentRoot: '/home/x/.nastech/nastech-agent',
|
|
154
|
+
uninstallArgs: ['-m', 'nastech_cli.uninstall', '--mode', 'full'],
|
|
155
|
+
appPath: null,
|
|
156
|
+
nastechHome: '/home/x/.nastech'
|
|
157
|
+
})
|
|
158
|
+
// System python + source on PYTHONPATH so import nastech_cli works while the
|
|
159
|
+
// venv is torn down.
|
|
160
|
+
assert.match(script, /export PYTHONPATH='\/home\/x\/\.nastech\/nastech-agent'/)
|
|
161
|
+
assert.match(script, /'\/usr\/bin\/python3' '-m' 'nastech_cli\.uninstall' '--mode' 'full'/)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('buildPosixCleanupScript omits PYTHONPATH when pythonPath is null (gui)', () => {
|
|
165
|
+
const script = buildPosixCleanupScript({
|
|
166
|
+
desktopPid: 1,
|
|
167
|
+
pythonExe: '/p/python',
|
|
168
|
+
pythonPath: null,
|
|
169
|
+
agentRoot: '/a',
|
|
170
|
+
uninstallArgs: ['-m', 'nastech_cli.uninstall', '--mode', 'gui'],
|
|
171
|
+
appPath: null,
|
|
172
|
+
nastechHome: '/h'
|
|
173
|
+
})
|
|
174
|
+
assert.doesNotMatch(script, /export PYTHONPATH/)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('buildPosixCleanupScript omits the bundle rm when appPath is null', () => {
|
|
178
|
+
const script = buildPosixCleanupScript({
|
|
179
|
+
desktopPid: 1,
|
|
180
|
+
pythonExe: '/p/python',
|
|
181
|
+
pythonPath: null,
|
|
182
|
+
agentRoot: '/a',
|
|
183
|
+
uninstallArgs: ['-m', 'nastech_cli.uninstall', '--mode', 'lite'],
|
|
184
|
+
appPath: null,
|
|
185
|
+
nastechHome: '/h'
|
|
186
|
+
})
|
|
187
|
+
assert.doesNotMatch(script, /rm -rf '\//)
|
|
188
|
+
// Still runs the uninstall.
|
|
189
|
+
assert.match(script, /'-m' 'nastech_cli\.uninstall' '--mode' 'lite'/)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('buildPosixCleanupScript single-quote-escapes paths with apostrophes', () => {
|
|
193
|
+
const script = buildPosixCleanupScript({
|
|
194
|
+
desktopPid: 1,
|
|
195
|
+
pythonExe: "/home/o'brien/python",
|
|
196
|
+
pythonPath: null,
|
|
197
|
+
agentRoot: '/a',
|
|
198
|
+
uninstallArgs: ['-m', 'nastech_cli.uninstall', '--mode', 'gui'],
|
|
199
|
+
appPath: null,
|
|
200
|
+
nastechHome: '/h'
|
|
201
|
+
})
|
|
202
|
+
// The apostrophe is closed-escaped-reopened so the shell sees the literal.
|
|
203
|
+
assert.match(script, /'\/home\/o'\\''brien\/python'/)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// --- buildWindowsCleanupScript ---
|
|
207
|
+
|
|
208
|
+
test('buildWindowsCleanupScript waits (bounded) for PID, runs uninstall, rmdir bundle', () => {
|
|
209
|
+
const script = buildWindowsCleanupScript({
|
|
210
|
+
desktopPid: 9988,
|
|
211
|
+
pythonExe: 'C:\\Python313\\python.exe',
|
|
212
|
+
pythonPath: 'C:\\nastech',
|
|
213
|
+
agentRoot: 'C:\\nastech',
|
|
214
|
+
uninstallArgs: ['-m', 'nastech_cli.uninstall', '--mode', 'full'],
|
|
215
|
+
appPath: 'C:\\Users\\x\\AppData\\Local\\Programs\\NasTech',
|
|
216
|
+
nastechHome: 'C:\\Users\\x\\AppData\\Local\\nastech'
|
|
217
|
+
})
|
|
218
|
+
assert.match(script, /@echo off/)
|
|
219
|
+
assert.match(script, /set "PID=9988"/)
|
|
220
|
+
// PYTHONPATH set so a system python can import nastech_cli from source.
|
|
221
|
+
assert.match(script, /set "PYTHONPATH=C:\\nastech;%PYTHONPATH%"/)
|
|
222
|
+
assert.match(script, /"C:\\Python313\\python.exe" "-m" "nastech_cli\.uninstall" "--mode" "full"/)
|
|
223
|
+
// Bounded wait-loop (no infinite loop), whole-token PID match (no substring).
|
|
224
|
+
assert.match(script, /if %waited% geq 60 goto waited_done/)
|
|
225
|
+
assert.match(script, /findstr \/r \/c:" %PID% "/)
|
|
226
|
+
assert.doesNotMatch(script, /find "%PID%"/) // the old substring-prone form is gone
|
|
227
|
+
// Removal is a retry loop (Windows releases dir handles lazily).
|
|
228
|
+
assert.match(script, /:rmloop/)
|
|
229
|
+
assert.match(script, /rmdir \/s \/q "C:\\Users\\x\\AppData\\Local\\Programs\\NasTech" >nul 2>&1/)
|
|
230
|
+
assert.match(script, /if %tries% geq 10 goto rmdone/)
|
|
231
|
+
assert.match(script, /del "%~f0"/)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test('buildWindowsCleanupScript omits PYTHONPATH + rmdir when not needed (gui, no bundle)', () => {
|
|
235
|
+
const script = buildWindowsCleanupScript({
|
|
236
|
+
desktopPid: 2,
|
|
237
|
+
pythonExe: 'C:\\h\\venv\\Scripts\\python.exe',
|
|
238
|
+
pythonPath: null,
|
|
239
|
+
agentRoot: 'C:\\h',
|
|
240
|
+
uninstallArgs: ['-m', 'nastech_cli.uninstall', '--mode', 'gui'],
|
|
241
|
+
appPath: null,
|
|
242
|
+
nastechHome: 'C:\\h'
|
|
243
|
+
})
|
|
244
|
+
assert.doesNotMatch(script, /rmdir/)
|
|
245
|
+
assert.doesNotMatch(script, /set "PYTHONPATH=/)
|
|
246
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>com.apple.security.cs.allow-jit</key>
|
|
6
|
+
<true/>
|
|
7
|
+
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
|
8
|
+
<true/>
|
|
9
|
+
<key>com.apple.security.cs.disable-library-validation</key>
|
|
10
|
+
<true/>
|
|
11
|
+
<key>com.apple.security.device.audio-input</key>
|
|
12
|
+
<true/>
|
|
13
|
+
</dict>
|
|
14
|
+
</plist>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>com.apple.security.cs.allow-jit</key>
|
|
6
|
+
<true/>
|
|
7
|
+
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
|
8
|
+
<true/>
|
|
9
|
+
<key>com.apple.security.cs.disable-library-validation</key>
|
|
10
|
+
<true/>
|
|
11
|
+
<key>com.apple.security.device.audio-input</key>
|
|
12
|
+
<true/>
|
|
13
|
+
</dict>
|
|
14
|
+
</plist>
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
const { resolveDirectoryForIpc } = require('./hardening.cjs')
|
|
6
|
+
|
|
7
|
+
const FS_READDIR_STAT_CONCURRENCY = 16
|
|
8
|
+
|
|
9
|
+
// Always-hidden noise (covers non-git projects too; gitignore catches many of
|
|
10
|
+
// these, but the project tree should keep the same hygiene without one).
|
|
11
|
+
const FS_READDIR_HIDDEN = new Set([
|
|
12
|
+
'.git',
|
|
13
|
+
'.hg',
|
|
14
|
+
'.svn',
|
|
15
|
+
'.cache',
|
|
16
|
+
'.next',
|
|
17
|
+
'.turbo',
|
|
18
|
+
'.venv',
|
|
19
|
+
'__pycache__',
|
|
20
|
+
'build',
|
|
21
|
+
'dist',
|
|
22
|
+
'node_modules',
|
|
23
|
+
'target',
|
|
24
|
+
'venv'
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
function direntIsDirectory(dirent) {
|
|
28
|
+
return typeof dirent.isDirectory === 'function' && dirent.isDirectory()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function direntIsFile(dirent) {
|
|
32
|
+
return typeof dirent.isFile === 'function' && dirent.isFile()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function direntIsSymbolicLink(dirent) {
|
|
36
|
+
return typeof dirent.isSymbolicLink === 'function' && dirent.isSymbolicLink()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function shouldStatDirent(dirent) {
|
|
40
|
+
if (direntIsDirectory(dirent)) return false
|
|
41
|
+
|
|
42
|
+
return direntIsSymbolicLink(dirent) || !direntIsFile(dirent)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function entryForDirent(dirent, resolved, fsImpl) {
|
|
46
|
+
const fullPath = path.join(resolved, dirent.name)
|
|
47
|
+
let isDirectory = direntIsDirectory(dirent)
|
|
48
|
+
|
|
49
|
+
if (!isDirectory && shouldStatDirent(dirent)) {
|
|
50
|
+
try {
|
|
51
|
+
isDirectory = (await fsImpl.promises.stat(fullPath)).isDirectory()
|
|
52
|
+
} catch {
|
|
53
|
+
isDirectory = false
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { name: dirent.name, path: fullPath, isDirectory }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function mapWithStatConcurrency(items, mapper) {
|
|
61
|
+
const results = new Array(items.length)
|
|
62
|
+
let nextIndex = 0
|
|
63
|
+
|
|
64
|
+
async function runWorker() {
|
|
65
|
+
while (nextIndex < items.length) {
|
|
66
|
+
const index = nextIndex
|
|
67
|
+
nextIndex += 1
|
|
68
|
+
results[index] = await mapper(items[index])
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const workerCount = Math.min(FS_READDIR_STAT_CONCURRENCY, items.length)
|
|
73
|
+
const workers = Array.from({ length: workerCount }, () => runWorker())
|
|
74
|
+
await Promise.all(workers)
|
|
75
|
+
|
|
76
|
+
return results
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function readDirForIpc(dirPath, options = {}) {
|
|
80
|
+
const fsImpl = options.fs || fs
|
|
81
|
+
let resolved
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
;({ resolvedPath: resolved } = await resolveDirectoryForIpc(dirPath, {
|
|
85
|
+
fs: fsImpl,
|
|
86
|
+
purpose: 'Directory read'
|
|
87
|
+
}))
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return { entries: [], error: error?.code || 'read-error' }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const dirents = await fsImpl.promises.readdir(resolved, { withFileTypes: true })
|
|
94
|
+
const visibleDirents = dirents.filter(dirent => !FS_READDIR_HIDDEN.has(dirent.name))
|
|
95
|
+
const entries = await mapWithStatConcurrency(visibleDirents, dirent =>
|
|
96
|
+
entryForDirent(dirent, resolved, fsImpl)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
entries.sort((a, b) => Number(b.isDirectory) - Number(a.isDirectory) || a.name.localeCompare(b.name))
|
|
100
|
+
|
|
101
|
+
return { entries }
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return { entries: [], error: error?.code || 'read-error' }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
readDirForIpc
|
|
109
|
+
}
|