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,54 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
const { resolveRequestedPathForIpc } = require('./hardening.cjs')
|
|
6
|
+
|
|
7
|
+
function findGitRoot(start, fsImpl = fs) {
|
|
8
|
+
let dir = start
|
|
9
|
+
|
|
10
|
+
for (let i = 0; i < 50; i += 1) {
|
|
11
|
+
try {
|
|
12
|
+
if (fsImpl.existsSync(path.join(dir, '.git'))) {
|
|
13
|
+
return dir
|
|
14
|
+
}
|
|
15
|
+
} catch {
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const parent = path.dirname(dir)
|
|
20
|
+
|
|
21
|
+
if (parent === dir) {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
dir = parent
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function gitRootForIpc(startPath, options = {}) {
|
|
32
|
+
const fsImpl = options.fs || fs
|
|
33
|
+
let resolved
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
resolved = resolveRequestedPathForIpc(startPath, { purpose: 'Git root' })
|
|
37
|
+
} catch {
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const stat = await fsImpl.promises.stat(resolved)
|
|
43
|
+
const start = stat.isDirectory() ? resolved : path.dirname(resolved)
|
|
44
|
+
|
|
45
|
+
return findGitRoot(start, fsImpl)
|
|
46
|
+
} catch {
|
|
47
|
+
return findGitRoot(resolved, fsImpl)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
findGitRoot,
|
|
53
|
+
gitRootForIpc
|
|
54
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict')
|
|
4
|
+
const fs = require('node:fs')
|
|
5
|
+
const os = require('node:os')
|
|
6
|
+
const path = require('node:path')
|
|
7
|
+
const test = require('node:test')
|
|
8
|
+
const { pathToFileURL } = require('node:url')
|
|
9
|
+
|
|
10
|
+
const { gitRootForIpc } = require('./git-root.cjs')
|
|
11
|
+
|
|
12
|
+
function mkTmpDir() {
|
|
13
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'nastech-git-root-'))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test('gitRootForIpc returns null for invalid and device paths', async () => {
|
|
17
|
+
assert.equal(await gitRootForIpc(''), null)
|
|
18
|
+
assert.equal(await gitRootForIpc(' '), null)
|
|
19
|
+
assert.equal(await gitRootForIpc(null), null)
|
|
20
|
+
assert.equal(await gitRootForIpc('\\\\?\\C:\\secret'), null)
|
|
21
|
+
assert.equal(await gitRootForIpc('file:///%E0%A4%A'), null)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('gitRootForIpc resolves directories files missing descendants and file URLs', async t => {
|
|
25
|
+
const root = mkTmpDir()
|
|
26
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }))
|
|
27
|
+
|
|
28
|
+
const gitDir = path.join(root, '.git')
|
|
29
|
+
const srcDir = path.join(root, 'src')
|
|
30
|
+
const filePath = path.join(srcDir, 'index.ts')
|
|
31
|
+
fs.mkdirSync(gitDir)
|
|
32
|
+
fs.mkdirSync(srcDir)
|
|
33
|
+
fs.writeFileSync(filePath, 'export {}\n', 'utf8')
|
|
34
|
+
|
|
35
|
+
assert.equal(await gitRootForIpc(root), root)
|
|
36
|
+
assert.equal(await gitRootForIpc(srcDir), root)
|
|
37
|
+
assert.equal(await gitRootForIpc(filePath), root)
|
|
38
|
+
assert.equal(await gitRootForIpc(pathToFileURL(filePath).toString()), root)
|
|
39
|
+
assert.equal(await gitRootForIpc(path.join(srcDir, 'missing.ts')), root)
|
|
40
|
+
})
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Resolve git-worktree relationships for a set of session cwds, reading git's
|
|
4
|
+
// on-disk metadata directly (no `git` spawn per path):
|
|
5
|
+
//
|
|
6
|
+
// - A normal checkout has a `.git` DIRECTORY at its root → it's the main
|
|
7
|
+
// worktree; its repo root IS that directory's parent.
|
|
8
|
+
// - A linked worktree has a `.git` FILE: `gitdir: <repo>/.git/worktrees/<name>`.
|
|
9
|
+
// That admin dir's `commondir` points back at the shared `<repo>/.git`, whose
|
|
10
|
+
// parent is the main repo root.
|
|
11
|
+
//
|
|
12
|
+
// Grouping by repoRoot therefore clusters a repo's main checkout with all of its
|
|
13
|
+
// linked worktrees, regardless of how the worktree directories are named. The
|
|
14
|
+
// branch (read from the worktree's own HEAD) gives each worktree a meaningful
|
|
15
|
+
// label.
|
|
16
|
+
|
|
17
|
+
const fs = require('node:fs')
|
|
18
|
+
const path = require('node:path')
|
|
19
|
+
const { resolveRequestedPathForIpc } = require('./hardening.cjs')
|
|
20
|
+
|
|
21
|
+
// Walk up from `start` to the nearest ancestor that carries a `.git` entry
|
|
22
|
+
// (file for a linked worktree, dir for the main checkout). Capped so a stray
|
|
23
|
+
// path can't loop forever.
|
|
24
|
+
function findGitHost(start, fsImpl) {
|
|
25
|
+
let dir = start
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < 64; i += 1) {
|
|
28
|
+
const dotgit = path.join(dir, '.git')
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
if (fsImpl.existsSync(dotgit)) {
|
|
32
|
+
return dir
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const parent = path.dirname(dir)
|
|
39
|
+
|
|
40
|
+
if (parent === dir) {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
dir = parent
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readBranch(gitDir, fsImpl) {
|
|
51
|
+
try {
|
|
52
|
+
const head = fsImpl.readFileSync(path.join(gitDir, 'HEAD'), 'utf8').trim()
|
|
53
|
+
const ref = head.match(/^ref:\s*refs\/heads\/(.+)$/)
|
|
54
|
+
|
|
55
|
+
if (ref) {
|
|
56
|
+
return ref[1]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Detached HEAD: surface a short sha so the worktree still gets a label.
|
|
60
|
+
return /^[0-9a-f]{7,40}$/i.test(head) ? head.slice(0, 8) : null
|
|
61
|
+
} catch {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Given the directory that owns the `.git` entry, resolve its worktree identity.
|
|
67
|
+
function resolveFromHost(host, fsImpl) {
|
|
68
|
+
const dotgit = path.join(host, '.git')
|
|
69
|
+
let stat
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
stat = fsImpl.statSync(dotgit)
|
|
73
|
+
} catch {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (stat.isDirectory()) {
|
|
78
|
+
return {
|
|
79
|
+
repoRoot: host,
|
|
80
|
+
worktreeRoot: host,
|
|
81
|
+
isMainWorktree: true,
|
|
82
|
+
branch: readBranch(dotgit, fsImpl)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Linked worktree: `.git` is a file pointing at the admin dir.
|
|
87
|
+
let contents
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
contents = fsImpl.readFileSync(dotgit, 'utf8').trim()
|
|
91
|
+
} catch {
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const match = contents.match(/^gitdir:\s*(.+)$/m)
|
|
96
|
+
|
|
97
|
+
if (!match) {
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const adminDir = path.resolve(host, match[1].trim())
|
|
102
|
+
|
|
103
|
+
// `commondir` resolves to the shared `<repo>/.git`; fall back to walking two
|
|
104
|
+
// levels up from `<repo>/.git/worktrees/<name>` if it's missing.
|
|
105
|
+
let commonDir
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const rel = fsImpl.readFileSync(path.join(adminDir, 'commondir'), 'utf8').trim()
|
|
109
|
+
commonDir = path.resolve(adminDir, rel)
|
|
110
|
+
} catch {
|
|
111
|
+
commonDir = path.dirname(path.dirname(adminDir))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
repoRoot: path.dirname(commonDir),
|
|
116
|
+
worktreeRoot: host,
|
|
117
|
+
isMainWorktree: false,
|
|
118
|
+
branch: readBranch(adminDir, fsImpl)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolveWorktree(startPath, fsImpl = fs) {
|
|
123
|
+
let resolved
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
resolved = resolveRequestedPathForIpc(startPath, { purpose: 'Worktree lookup' })
|
|
127
|
+
} catch {
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let start = resolved
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const stat = fsImpl.statSync(resolved)
|
|
135
|
+
|
|
136
|
+
if (!stat.isDirectory()) {
|
|
137
|
+
start = path.dirname(resolved)
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const host = findGitHost(start, fsImpl)
|
|
144
|
+
|
|
145
|
+
if (!host) {
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return resolveFromHost(host, fsImpl)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Batch entry point for the renderer: maps each requested cwd to its worktree
|
|
153
|
+
// info (or null when it isn't inside a git checkout / can't be read). Dedupes so
|
|
154
|
+
// many sessions sharing a cwd cost one lookup.
|
|
155
|
+
async function worktreesForIpc(cwds, options = {}) {
|
|
156
|
+
const fsImpl = options.fs || fs
|
|
157
|
+
const list = Array.isArray(cwds) ? cwds : []
|
|
158
|
+
const out = {}
|
|
159
|
+
|
|
160
|
+
for (const cwd of list) {
|
|
161
|
+
if (typeof cwd !== 'string' || !cwd.trim() || cwd in out) {
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
out[cwd] = resolveWorktree(cwd, fsImpl)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return out
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
resolveWorktree,
|
|
173
|
+
worktreesForIpc
|
|
174
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
const fs = require('node:fs')
|
|
2
|
+
const path = require('node:path')
|
|
3
|
+
const { fileURLToPath } = require('node:url')
|
|
4
|
+
|
|
5
|
+
const DEFAULT_FETCH_TIMEOUT_MS = 15_000
|
|
6
|
+
const DATA_URL_READ_MAX_BYTES = 16 * 1024 * 1024
|
|
7
|
+
const TEXT_PREVIEW_SOURCE_MAX_BYTES = 64 * 1024 * 1024
|
|
8
|
+
|
|
9
|
+
const SAFE_ENV_SUFFIXES = new Set(['dist', 'example', 'sample', 'template'])
|
|
10
|
+
const SENSITIVE_EXTENSIONS = new Set(['.kdbx', '.p12', '.pem', '.pfx'])
|
|
11
|
+
|
|
12
|
+
function resolveTimeoutMs(timeoutMs, fallbackMs = DEFAULT_FETCH_TIMEOUT_MS) {
|
|
13
|
+
const fallback =
|
|
14
|
+
Number.isFinite(fallbackMs) && Number(fallbackMs) > 0 ? Math.round(Number(fallbackMs)) : DEFAULT_FETCH_TIMEOUT_MS
|
|
15
|
+
const parsed = Number(timeoutMs)
|
|
16
|
+
|
|
17
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
18
|
+
return Math.round(parsed)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return fallback
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function encryptDesktopSecret(value, safeStorageApi) {
|
|
25
|
+
const raw = String(value || '')
|
|
26
|
+
|
|
27
|
+
if (!raw) {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let encryptionAvailable = false
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
encryptionAvailable = Boolean(safeStorageApi?.isEncryptionAvailable?.())
|
|
35
|
+
} catch {
|
|
36
|
+
encryptionAvailable = false
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!encryptionAvailable) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
'Secure token storage is unavailable, so NasTech Desktop cannot save remote gateway tokens. ' +
|
|
42
|
+
'Set NASTECH_DESKTOP_REMOTE_URL and NASTECH_DESKTOP_REMOTE_TOKEN in your environment, or enable OS keychain access and try again.'
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
return {
|
|
48
|
+
encoding: 'safeStorage',
|
|
49
|
+
value: safeStorageApi.encryptString(raw).toString('base64')
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const detail = error instanceof Error && error.message ? ` (${error.message})` : ''
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Failed to encrypt the remote gateway token for secure storage${detail}. ` +
|
|
55
|
+
'Set NASTECH_DESKTOP_REMOTE_URL and NASTECH_DESKTOP_REMOTE_TOKEN in your environment as a fallback.'
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sensitiveFileBlockReason(filePath) {
|
|
61
|
+
const normalized = String(filePath || '')
|
|
62
|
+
.replace(/\\/g, '/')
|
|
63
|
+
.toLowerCase()
|
|
64
|
+
const basename = path.basename(normalized)
|
|
65
|
+
const ext = path.extname(basename)
|
|
66
|
+
|
|
67
|
+
if (!basename) {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (normalized.includes('/.ssh/')) {
|
|
72
|
+
return 'SSH key/config files are blocked.'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (normalized.includes('/.gnupg/')) {
|
|
76
|
+
return 'GPG key material is blocked.'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (normalized.endsWith('/.aws/credentials')) {
|
|
80
|
+
return 'AWS credential files are blocked.'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (basename === '.env') {
|
|
84
|
+
return '.env files are blocked because they commonly contain secrets.'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (basename.startsWith('.env.')) {
|
|
88
|
+
const suffix = basename.slice('.env.'.length)
|
|
89
|
+
if (!SAFE_ENV_SUFFIXES.has(suffix)) {
|
|
90
|
+
return `${basename} is blocked because it appears to contain environment secrets.`
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (/^id_(rsa|dsa|ecdsa|ed25519)(?:\..+)?$/.test(basename) && !basename.endsWith('.pub')) {
|
|
95
|
+
return 'SSH private key files are blocked.'
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (SENSITIVE_EXTENSIONS.has(ext)) {
|
|
99
|
+
return `${ext} key/certificate files are blocked.`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (basename === '.npmrc' || basename === '.netrc' || basename === '.pypirc') {
|
|
103
|
+
return `${basename} is blocked because it may include auth credentials.`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveRequestedFilePath(filePath, baseDir = process.cwd(), purpose = 'File read') {
|
|
110
|
+
const raw = String(filePath || '').trim()
|
|
111
|
+
|
|
112
|
+
if (!raw) {
|
|
113
|
+
throw new Error(`${purpose} failed: file path is required.`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (raw.includes('\0')) {
|
|
117
|
+
throw new Error(`${purpose} failed: file path is invalid.`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (/^file:/i.test(raw)) {
|
|
121
|
+
try {
|
|
122
|
+
return fileURLToPath(raw)
|
|
123
|
+
} catch {
|
|
124
|
+
throw new Error(`${purpose} failed: file URL is invalid.`)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const resolvedBase = path.resolve(String(baseDir || process.cwd()))
|
|
129
|
+
return path.resolve(resolvedBase, raw)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function resolveReadableFileForIpc(filePath, options = {}) {
|
|
133
|
+
const purpose = String(options.purpose || 'File read')
|
|
134
|
+
const resolvedPath = resolveRequestedFilePath(filePath, options.baseDir, purpose)
|
|
135
|
+
|
|
136
|
+
if (options.blockSensitive !== false) {
|
|
137
|
+
const blockReason = sensitiveFileBlockReason(resolvedPath)
|
|
138
|
+
if (blockReason) {
|
|
139
|
+
throw new Error(`${purpose} blocked for sensitive file: ${blockReason}`)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let stat
|
|
144
|
+
try {
|
|
145
|
+
stat = await fs.promises.stat(resolvedPath)
|
|
146
|
+
} catch (error) {
|
|
147
|
+
const code = error && typeof error === 'object' ? error.code : ''
|
|
148
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') {
|
|
149
|
+
throw new Error(`${purpose} failed: file does not exist.`)
|
|
150
|
+
}
|
|
151
|
+
throw new Error(`${purpose} failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (stat.isDirectory()) {
|
|
155
|
+
throw new Error(`${purpose} failed: path points to a directory.`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!stat.isFile()) {
|
|
159
|
+
throw new Error(`${purpose} failed: only regular files can be read.`)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const maxBytes = Number.isFinite(options.maxBytes) && Number(options.maxBytes) > 0 ? Number(options.maxBytes) : null
|
|
163
|
+
if (maxBytes && stat.size > maxBytes) {
|
|
164
|
+
throw new Error(`${purpose} failed: file is too large (${stat.size} bytes; limit ${maxBytes} bytes).`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
await fs.promises.access(resolvedPath, fs.constants.R_OK)
|
|
169
|
+
} catch {
|
|
170
|
+
throw new Error(`${purpose} failed: file is not readable.`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { resolvedPath, stat }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
DATA_URL_READ_MAX_BYTES,
|
|
178
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
179
|
+
TEXT_PREVIEW_SOURCE_MAX_BYTES,
|
|
180
|
+
encryptDesktopSecret,
|
|
181
|
+
resolveReadableFileForIpc,
|
|
182
|
+
resolveTimeoutMs,
|
|
183
|
+
sensitiveFileBlockReason
|
|
184
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
const fs = require('node:fs')
|
|
3
|
+
const os = require('node:os')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
const test = require('node:test')
|
|
6
|
+
const { pathToFileURL } = require('node:url')
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
10
|
+
encryptDesktopSecret,
|
|
11
|
+
resolveReadableFileForIpc,
|
|
12
|
+
resolveTimeoutMs,
|
|
13
|
+
sensitiveFileBlockReason
|
|
14
|
+
} = require('./hardening.cjs')
|
|
15
|
+
|
|
16
|
+
test('resolveTimeoutMs falls back to defaults and accepts overrides', () => {
|
|
17
|
+
assert.equal(resolveTimeoutMs(undefined), DEFAULT_FETCH_TIMEOUT_MS)
|
|
18
|
+
assert.equal(resolveTimeoutMs(0), DEFAULT_FETCH_TIMEOUT_MS)
|
|
19
|
+
assert.equal(resolveTimeoutMs(-25), DEFAULT_FETCH_TIMEOUT_MS)
|
|
20
|
+
assert.equal(resolveTimeoutMs('2750'), 2750)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('encryptDesktopSecret requires available secure storage', () => {
|
|
24
|
+
assert.equal(
|
|
25
|
+
encryptDesktopSecret('', { isEncryptionAvailable: () => true, encryptString: () => Buffer.alloc(0) }),
|
|
26
|
+
null
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
assert.throws(
|
|
30
|
+
() => encryptDesktopSecret('token', { isEncryptionAvailable: () => false, encryptString: () => Buffer.alloc(0) }),
|
|
31
|
+
/Secure token storage is unavailable/
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('encryptDesktopSecret stores safeStorage base64 payload', () => {
|
|
36
|
+
const secret = encryptDesktopSecret('token-123', {
|
|
37
|
+
isEncryptionAvailable: () => true,
|
|
38
|
+
encryptString: value => Buffer.from(`enc:${value}`, 'utf8')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
assert.deepEqual(secret, {
|
|
42
|
+
encoding: 'safeStorage',
|
|
43
|
+
value: Buffer.from('enc:token-123', 'utf8').toString('base64')
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('sensitiveFileBlockReason blocks obvious secret file patterns', () => {
|
|
48
|
+
assert.match(String(sensitiveFileBlockReason('/tmp/.env')), /\.env/)
|
|
49
|
+
assert.equal(sensitiveFileBlockReason('/tmp/.env.example'), null)
|
|
50
|
+
assert.match(String(sensitiveFileBlockReason('/Users/me/.ssh/id_ed25519')), /SSH/)
|
|
51
|
+
assert.match(String(sensitiveFileBlockReason('/tmp/server-cert.pem')), /\.pem/)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('resolveReadableFileForIpc validates existence type size and sensitivity', async t => {
|
|
55
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nastech-desktop-hardening-'))
|
|
56
|
+
t.after(() => fs.rmSync(tempDir, { recursive: true, force: true }))
|
|
57
|
+
|
|
58
|
+
const textPath = path.join(tempDir, 'notes.txt')
|
|
59
|
+
fs.writeFileSync(textPath, 'hello world', 'utf8')
|
|
60
|
+
|
|
61
|
+
const fromRelative = await resolveReadableFileForIpc('notes.txt', {
|
|
62
|
+
baseDir: tempDir,
|
|
63
|
+
maxBytes: 256,
|
|
64
|
+
purpose: 'File preview'
|
|
65
|
+
})
|
|
66
|
+
assert.equal(fromRelative.resolvedPath, textPath)
|
|
67
|
+
assert.equal(fromRelative.stat.size, 11)
|
|
68
|
+
|
|
69
|
+
const fromFileUrl = await resolveReadableFileForIpc(pathToFileURL(textPath).toString(), {
|
|
70
|
+
purpose: 'File preview'
|
|
71
|
+
})
|
|
72
|
+
assert.equal(fromFileUrl.resolvedPath, textPath)
|
|
73
|
+
|
|
74
|
+
await assert.rejects(
|
|
75
|
+
resolveReadableFileForIpc('missing.txt', {
|
|
76
|
+
baseDir: tempDir,
|
|
77
|
+
purpose: 'Text preview'
|
|
78
|
+
}),
|
|
79
|
+
/file does not exist/
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const nestedDir = path.join(tempDir, 'directory')
|
|
83
|
+
fs.mkdirSync(nestedDir)
|
|
84
|
+
await assert.rejects(
|
|
85
|
+
resolveReadableFileForIpc(nestedDir, {
|
|
86
|
+
purpose: 'Text preview'
|
|
87
|
+
}),
|
|
88
|
+
/path points to a directory/
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
const largePath = path.join(tempDir, 'large.txt')
|
|
92
|
+
fs.writeFileSync(largePath, 'x'.repeat(40), 'utf8')
|
|
93
|
+
await assert.rejects(
|
|
94
|
+
resolveReadableFileForIpc(largePath, {
|
|
95
|
+
maxBytes: 8,
|
|
96
|
+
purpose: 'File preview'
|
|
97
|
+
}),
|
|
98
|
+
/file is too large/
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const envPath = path.join(tempDir, '.env')
|
|
102
|
+
fs.writeFileSync(envPath, 'SECRET_TOKEN=123', 'utf8')
|
|
103
|
+
await assert.rejects(
|
|
104
|
+
resolveReadableFileForIpc(envPath, {
|
|
105
|
+
purpose: 'File preview'
|
|
106
|
+
}),
|
|
107
|
+
/blocked for sensitive file/
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const envTemplatePath = path.join(tempDir, '.env.example')
|
|
111
|
+
fs.writeFileSync(envTemplatePath, 'EXAMPLE_TOKEN=value', 'utf8')
|
|
112
|
+
const envTemplate = await resolveReadableFileForIpc(envTemplatePath, {
|
|
113
|
+
purpose: 'File preview'
|
|
114
|
+
})
|
|
115
|
+
assert.equal(envTemplate.resolvedPath, envTemplatePath)
|
|
116
|
+
})
|