@deepseek-harness-tui/dsh-tui 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -256
- package/bin/dsh-tui.js +172 -5
- package/cordis.patch.yml +16 -4
- package/dsh-ecosystem-spec/protocols/profile-definitions.d.ts +13 -13
- package/dsh-ecosystem-spec/protocols/profile-definitions.js +59 -59
- package/dsh-ecosystem-spec/protocols/tui-channel-http.d.ts +15 -15
- package/dsh-ecosystem-spec/protocols/tui-channel-http.js +32 -32
- package/dsh-ecosystem-spec/protocols/tui-channel.d.ts +59 -59
- package/dsh-ecosystem-spec/protocols/tui-channel.js +166 -166
- package/dsh-ecosystem-spec/protocols/tui-contributions.d.ts +42 -42
- package/dsh-ecosystem-spec/protocols/tui-contributions.js +143 -143
- package/dsh-ecosystem-spec/registry/README.md +14 -14
- package/dsh-ecosystem-spec/registry/host-descriptor.tui.example.json +56 -56
- package/dsh-ecosystem-spec/registry/permissions-0.1.json +13 -13
- package/dsh-ecosystem-spec/registry/registry-0.15.json +149 -149
- package/lib/types/cc/hyperlink.d.ts +7 -1
- package/lib/types/cc/hyperlink.d.ts.map +1 -1
- package/lib/types/cc/hyperlink.js +3 -2
- package/lib/types/cc/markdown.d.ts.map +1 -1
- package/lib/types/cc/markdown.js +33 -3
- package/lib/types/cc/sessionColors.d.ts +27 -0
- package/lib/types/cc/sessionColors.d.ts.map +1 -0
- package/lib/types/cc/sessionColors.js +39 -0
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +6 -0
- package/lib/types/components/AutoRecapRow.d.ts +23 -0
- package/lib/types/components/AutoRecapRow.d.ts.map +1 -0
- package/lib/types/components/AutoRecapRow.js +23 -0
- package/lib/types/components/ColorPicker.d.ts +17 -0
- package/lib/types/components/ColorPicker.d.ts.map +1 -0
- package/lib/types/components/ColorPicker.js +20 -0
- package/lib/types/components/CommandSuggestions.d.ts +3 -1
- package/lib/types/components/CommandSuggestions.d.ts.map +1 -1
- package/lib/types/components/ContextBarView.d.ts +33 -0
- package/lib/types/components/ContextBarView.d.ts.map +1 -0
- package/lib/types/components/ContextBarView.js +46 -0
- package/lib/types/components/EffortInputBorder.d.ts +12 -1
- package/lib/types/components/EffortInputBorder.d.ts.map +1 -1
- package/lib/types/components/EffortInputBorder.js +22 -5
- package/lib/types/components/FileActionsPanel.d.ts +22 -0
- package/lib/types/components/FileActionsPanel.d.ts.map +1 -0
- package/lib/types/components/FileActionsPanel.js +28 -0
- package/lib/types/components/FileSuggestions.d.ts +3 -1
- package/lib/types/components/FileSuggestions.d.ts.map +1 -1
- package/lib/types/components/MessageList.d.ts +3 -1
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +13 -6
- package/lib/types/components/ModelPicker.d.ts +26 -4
- package/lib/types/components/ModelPicker.d.ts.map +1 -1
- package/lib/types/components/ModelPicker.js +26 -6
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +365 -57
- package/lib/types/components/RecapPanel.d.ts +25 -0
- package/lib/types/components/RecapPanel.d.ts.map +1 -0
- package/lib/types/components/RecapPanel.js +55 -0
- package/lib/types/components/ScrollbarGutter.d.ts.map +1 -1
- package/lib/types/components/ScrollbarGutter.js +55 -4
- package/lib/types/components/SubagentCard.d.ts.map +1 -1
- package/lib/types/components/SubagentCard.js +6 -4
- package/lib/types/components/SuggestionCard.d.ts +4 -2
- package/lib/types/components/SuggestionCard.d.ts.map +1 -1
- package/lib/types/components/TimelineRail.d.ts.map +1 -1
- package/lib/types/components/TimelineRail.js +9 -2
- package/lib/types/components/design-system/ThemeProvider.d.ts +7 -0
- package/lib/types/components/design-system/ThemeProvider.d.ts.map +1 -1
- package/lib/types/components/design-system/ThemeProvider.js +2 -1
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts +8 -1
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantToolUseMessage.js +36 -8
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts +7 -3
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.js +41 -9
- package/lib/types/dsh-adapter/channel.d.ts +101 -2
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +969 -93
- package/lib/types/dsh-adapter/compat/index.d.ts +8 -6
- package/lib/types/dsh-adapter/compat/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/index.js +8 -6
- package/lib/types/dsh-adapter/compat/sessionLog.d.ts +150 -0
- package/lib/types/dsh-adapter/compat/sessionLog.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/sessionLog.js +518 -41
- package/lib/types/dsh-adapter/contract.d.ts +1 -1
- package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
- package/lib/types/dsh-adapter/contract.js +1 -0
- package/lib/types/dsh-adapter/extension-events.d.ts +1 -1
- package/lib/types/dsh-adapter/extension-events.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.d.ts +10 -0
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +5 -0
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +317 -8
- package/lib/types/dsh-adapter/presets.d.ts +18 -0
- package/lib/types/dsh-adapter/presets.d.ts.map +1 -1
- package/lib/types/dsh-adapter/presets.js +31 -0
- package/lib/types/dsh-adapter/providerWizard.d.ts +29 -1
- package/lib/types/dsh-adapter/providerWizard.d.ts.map +1 -1
- package/lib/types/dsh-adapter/providerWizard.js +89 -5
- package/lib/types/dsh-adapter/questions.d.ts +19 -2
- package/lib/types/dsh-adapter/questions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/questions.js +53 -10
- package/lib/types/dsh-adapter/recap.d.ts +49 -0
- package/lib/types/dsh-adapter/recap.d.ts.map +1 -0
- package/lib/types/dsh-adapter/recap.js +121 -0
- package/lib/types/dsh-adapter/sessionTree.d.ts +314 -0
- package/lib/types/dsh-adapter/sessionTree.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sessionTree.js +954 -0
- package/lib/types/dsh-adapter/shortcuts.d.ts +15 -44
- package/lib/types/dsh-adapter/shortcuts.d.ts.map +1 -1
- package/lib/types/dsh-adapter/shortcuts.js +28 -165
- package/lib/types/dsh-adapter/types.d.ts +1 -1
- package/lib/types/dsh-adapter/types.d.ts.map +1 -1
- package/lib/types/herdr.js +1 -1
- package/lib/types/i18n.d.ts +604 -176
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +206 -72
- package/lib/types/ink/log-update.d.ts.map +1 -1
- package/lib/types/ink/log-update.js +21 -0
- package/lib/types/migrationPrefs.d.ts +60 -0
- package/lib/types/migrationPrefs.d.ts.map +1 -0
- package/lib/types/migrationPrefs.js +97 -0
- package/lib/types/modelGroups.d.ts +72 -0
- package/lib/types/modelGroups.d.ts.map +1 -0
- package/lib/types/modelGroups.js +97 -0
- package/lib/types/modelRecents.d.ts +39 -0
- package/lib/types/modelRecents.d.ts.map +1 -0
- package/lib/types/modelRecents.js +97 -0
- package/lib/types/native-ts/yoga-layout/index.d.ts +11 -0
- package/lib/types/native-ts/yoga-layout/index.d.ts.map +1 -1
- package/lib/types/native-ts/yoga-layout/index.js +31 -1
- package/lib/types/oauth.d.ts +15 -0
- package/lib/types/oauth.d.ts.map +1 -0
- package/lib/types/oauth.js +14 -0
- package/lib/types/plugin-spec/registry.d.ts +1 -1
- package/lib/types/plugin-spec/registry.js +1 -1
- package/lib/types/reload.d.ts +109 -0
- package/lib/types/reload.d.ts.map +1 -0
- package/lib/types/reload.js +118 -0
- package/lib/types/screens/Chat.d.ts +3 -1
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +588 -64
- package/lib/types/screens/SessionTree.d.ts +27 -0
- package/lib/types/screens/SessionTree.d.ts.map +1 -0
- package/lib/types/screens/SessionTree.js +555 -0
- package/lib/types/screens/StatusLine.d.ts +0 -9
- package/lib/types/screens/StatusLine.d.ts.map +1 -1
- package/lib/types/screens/StatusLine.js +215 -52
- package/lib/types/screens/StatusMetrics.d.ts +35 -3
- package/lib/types/screens/StatusMetrics.d.ts.map +1 -1
- package/lib/types/screens/StatusMetrics.js +63 -8
- package/lib/types/screens/chatOverlay.d.ts +17 -1
- package/lib/types/screens/chatOverlay.d.ts.map +1 -1
- package/lib/types/screens/chatOverlay.js +3 -1
- package/lib/types/tips.d.ts.map +1 -1
- package/lib/types/tips.js +22 -4
- package/lib/types/update.d.ts +159 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +496 -30
- package/lib/types/utils/debug.d.ts +1 -1
- package/lib/types/utils/debug.js +3 -3
- package/lib/types/utils/fileTarget.d.ts +46 -0
- package/lib/types/utils/fileTarget.d.ts.map +1 -0
- package/lib/types/utils/fileTarget.js +178 -0
- package/lib/types/utils/keymap.d.ts +134 -0
- package/lib/types/utils/keymap.d.ts.map +1 -0
- package/lib/types/utils/keymap.js +311 -0
- package/lib/types/utils/mentions.d.ts +25 -2
- package/lib/types/utils/mentions.d.ts.map +1 -1
- package/lib/types/utils/mentions.js +69 -10
- package/lib/types/utils/openExternal.d.ts +50 -0
- package/lib/types/utils/openExternal.d.ts.map +1 -0
- package/lib/types/utils/openExternal.js +170 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/LICENSE +21 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/README.md +140 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/cordis.patch.yml +11 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/dsh-plugin.json +44 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/command.d.ts +14 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/command.js +67 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/credentials.d.ts +64 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/credentials.js +170 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/index.d.ts +79 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/index.js +196 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/interaction.d.ts +68 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/interaction.js +193 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/opener.d.ts +32 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/opener.js +101 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/profiles.d.ts +31 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/profiles.js +68 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/service.d.ts +69 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/service.js +129 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/package.json +70 -0
- package/node_modules/@dsh-std/command/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/command/LICENSE +21 -21
- package/node_modules/@dsh-std/command/README.md +26 -26
- package/node_modules/@dsh-std/command/README.zh.md +28 -28
- package/node_modules/@dsh-std/command/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/command/package.json +22 -22
- package/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/connection/LICENSE +21 -21
- package/node_modules/@dsh-std/connection/README.md +42 -42
- package/node_modules/@dsh-std/connection/README.zh.md +42 -42
- package/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
- package/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
- package/node_modules/@dsh-std/connection/package.json +20 -20
- package/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/core/LICENSE +21 -21
- package/node_modules/@dsh-std/core/README.md +13 -13
- package/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/core/package.json +21 -21
- package/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -9
- package/node_modules/@dsh-std/manifest/LICENSE +21 -21
- package/node_modules/@dsh-std/manifest/README.md +5 -5
- package/node_modules/@dsh-std/manifest/README.zh.md +5 -5
- package/node_modules/@dsh-std/manifest/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/manifest/package.json +20 -20
- package/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -144
- package/node_modules/@dsh-std/messages/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/messages/LICENSE +21 -21
- package/node_modules/@dsh-std/messages/README.md +5 -5
- package/node_modules/@dsh-std/messages/README.zh.md +7 -7
- package/node_modules/@dsh-std/messages/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/messages/package.json +20 -20
- package/node_modules/@dsh-std/messages/schema/message-observer-envelope-0.15.schema.json +54 -54
- package/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -9
- package/node_modules/@dsh-std/presentation/LICENSE +21 -21
- package/node_modules/@dsh-std/presentation/README.md +27 -27
- package/node_modules/@dsh-std/presentation/README.zh.md +29 -29
- package/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -1
- package/node_modules/@dsh-std/presentation/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/presentation/package.json +25 -25
- package/node_modules/@dsh-std/storage/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/storage/LICENSE +21 -21
- package/node_modules/@dsh-std/storage/README.md +5 -5
- package/node_modules/@dsh-std/storage/README.zh.md +7 -7
- package/node_modules/@dsh-std/storage/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/storage/package.json +20 -20
- package/node_modules/@dsh-std/storage/schema/local-storage-0.15.schema.json +56 -56
- package/package.json +17 -5
- package/presets/liangshen/.dsh-tui-managed.json +1 -1
- package/presets/liangshen/instruction-hint.mjs +8 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.md +42 -42
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.zh.md +42 -42
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/package.json +20 -20
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -9
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.md +5 -5
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.zh.md +5 -5
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/package.json +20 -20
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -144
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -9
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.md +27 -27
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.zh.md +29 -29
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/package.json +25 -25
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/LICENSE +21 -21
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.md +42 -42
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.zh.md +42 -42
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/package.json +20 -20
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/package.json +21 -21
package/lib/types/update.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { appendFileSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { gte, gt, lt, valid } from 'semver';
|
|
7
7
|
import { shellQuote } from './utils/shellQuote.js';
|
|
8
|
+
import { DATA_DIR } from './utils/paths.js';
|
|
8
9
|
// Re-exported for scripts/verify-update.mjs and the bin launcher, which reads
|
|
9
10
|
// the compiled copy at lib/types/utils/shellQuote.js.
|
|
10
11
|
export { shellQuote };
|
|
@@ -13,6 +14,72 @@ const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
|
|
|
13
14
|
const UPDATE_CHECK_TIMEOUT_MS = 4000;
|
|
14
15
|
/** env marker set on the /update restart; the new process verifies it at boot. */
|
|
15
16
|
const UPDATED_FROM_ENV = 'DSH_TUI_UPDATED_FROM';
|
|
17
|
+
/**
|
|
18
|
+
* env marker set on the /restart replacement: its boot logs to restart.log
|
|
19
|
+
* (ordinary launches stay silent, so the file is restart-only evidence).
|
|
20
|
+
*/
|
|
21
|
+
const RESTART_CHILD_ENV = 'DSH_TUI_RESTART_CHILD';
|
|
22
|
+
/**
|
|
23
|
+
* Field-diagnosis log for the /restart terminal handoff, appended by BOTH
|
|
24
|
+
* processes: the exiting TUI logs the funnel path, the spawned replacement
|
|
25
|
+
* logs its boot progress, and every line carries pid + timestamp — so a
|
|
26
|
+
* handoff that dies at any stage (loader entry, TTY gate, session resume,
|
|
27
|
+
* even after the parent's survival window) leaves breadcrumbs on disk even
|
|
28
|
+
* when nothing reaches the already-confused terminal. Diagnosis only: every
|
|
29
|
+
* write is best effort and must never break the handoff itself.
|
|
30
|
+
*/
|
|
31
|
+
const RESTART_LOG = join(DATA_DIR, 'restart.log');
|
|
32
|
+
/** Cap so a long debugging streak cannot grow the log unbounded. */
|
|
33
|
+
const RESTART_LOG_MAX_BYTES = 256 * 1024;
|
|
34
|
+
function writeRestartLine(line) {
|
|
35
|
+
try {
|
|
36
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
37
|
+
appendFileSync(RESTART_LOG, `${new Date().toISOString()} pid=${process.pid} ${line}\n`);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Diagnosis only.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Append a /restart handoff event to ~/.dsh-tui/restart.log.
|
|
45
|
+
* @param event - Short stable event name.
|
|
46
|
+
* @param data - Small JSON-safe detail (never credentials or session text).
|
|
47
|
+
*/
|
|
48
|
+
export function logRestartEvent(event, data) {
|
|
49
|
+
writeRestartLine(`${event}${data === undefined ? '' : ` ${JSON.stringify(data)}`}`);
|
|
50
|
+
}
|
|
51
|
+
/** Start a fresh, clearly delimited /restart attempt block in the log. */
|
|
52
|
+
export function beginRestartAttempt(sessionId) {
|
|
53
|
+
try {
|
|
54
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
55
|
+
let size = 0;
|
|
56
|
+
try {
|
|
57
|
+
size = statSync(RESTART_LOG).size;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
size = 0;
|
|
61
|
+
}
|
|
62
|
+
if (size > RESTART_LOG_MAX_BYTES)
|
|
63
|
+
writeFileSync(RESTART_LOG, '');
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Diagnosis only.
|
|
67
|
+
}
|
|
68
|
+
writeRestartLine(`--- /restart attempt session=${sessionId} ---`);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Write a handoff notice to stderr synchronously: process.exit() right after
|
|
72
|
+
* an async stream write skips the flush, and a vanished diagnosis is
|
|
73
|
+
* indistinguishable from a silent failure.
|
|
74
|
+
*/
|
|
75
|
+
export function writeHandoffNotice(text) {
|
|
76
|
+
try {
|
|
77
|
+
writeFileSync(2, text);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
process.stderr.write(text);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
16
83
|
/** Read the version from either the compiled package or the source checkout. */
|
|
17
84
|
export function installedTuiVersion() {
|
|
18
85
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -212,6 +279,111 @@ export function tuiUpdatePluginArgs(profile, targetVersion) {
|
|
|
212
279
|
? ['plugin', '--profile', profile, 'update', '--latest', PACKAGE_NAME]
|
|
213
280
|
: ['plugin', '--profile', profile, 'update', `${PACKAGE_NAME}@${targetVersion}`];
|
|
214
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* Postinstall-only transitive dependencies of the dsh-tui chain
|
|
284
|
+
* (dsh-auth → @earendil-works/pi-ai → @google/genai + protobufjs, plus the
|
|
285
|
+
* esbuild/koffi peers of that chain): pnpm ≥11 refuses to run their build
|
|
286
|
+
* scripts unless the workspace allowlists them, and a profile that never
|
|
287
|
+
* opted in fails the WHOLE install with ERR_PNPM_IGNORED_BUILDS. None of
|
|
288
|
+
* these scripts is needed at runtime (the repo root allowBuilds them all to
|
|
289
|
+
* `false`), so the update flow pre-seeds the profile's pnpm-workspace.yaml
|
|
290
|
+
* with explicit `false` entries — an explicit decision pnpm honors silently.
|
|
291
|
+
*/
|
|
292
|
+
const PROFILE_ALLOW_BUILDS = {
|
|
293
|
+
'@google/genai': false,
|
|
294
|
+
esbuild: false,
|
|
295
|
+
koffi: false,
|
|
296
|
+
protobufjs: false,
|
|
297
|
+
};
|
|
298
|
+
/** The profile's pnpm-workspace.yaml (`$DSH_HOME ?? ~/.dsh` layout). */
|
|
299
|
+
export function profileWorkspaceYamlPath(profile) {
|
|
300
|
+
const dshHome = process.env.DSH_HOME ?? join(homedir(), '.dsh');
|
|
301
|
+
return join(dshHome, 'profiles', profile, 'pnpm-workspace.yaml');
|
|
302
|
+
}
|
|
303
|
+
/** YAML key spelling for an allowBuilds entry (scoped names need quotes). */
|
|
304
|
+
function allowBuildsKeyLine(key) {
|
|
305
|
+
const needsQuotes = key.includes('@') || key.includes('/');
|
|
306
|
+
return ` ${needsQuotes ? `'${key}'` : key}: false`;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Make sure the profile's pnpm-workspace.yaml carries explicit `false`
|
|
310
|
+
* allowBuilds entries for the dsh-tui chain's postinstall-only deps, so a
|
|
311
|
+
* pnpm ≥11 update is not killed by ERR_PNPM_IGNORED_BUILDS. Best effort and
|
|
312
|
+
* idempotent: existing entries are never overwritten (an explicit user
|
|
313
|
+
* decision wins), a missing `allowBuilds:` block is appended, and a missing
|
|
314
|
+
* file is created. Returns undefined when the profile directory is absent or
|
|
315
|
+
* the file could not be updated — the caller still runs pnpm, whose own
|
|
316
|
+
* ERR_PNPM_IGNORED_BUILDS diagnostic stays the visible fallback.
|
|
317
|
+
*/
|
|
318
|
+
export function ensureProfileAllowBuilds(profile) {
|
|
319
|
+
const yamlPath = profileWorkspaceYamlPath(profile);
|
|
320
|
+
try {
|
|
321
|
+
if (!existsSafe(dirname(yamlPath)))
|
|
322
|
+
return undefined;
|
|
323
|
+
let text = '';
|
|
324
|
+
try {
|
|
325
|
+
text = readFileSync(yamlPath, 'utf8');
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// Missing file — start from an empty document; writeFileSync creates it.
|
|
329
|
+
}
|
|
330
|
+
const lines = text.split(/\r?\n/u);
|
|
331
|
+
let blockStart = -1;
|
|
332
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
333
|
+
const line = lines[i];
|
|
334
|
+
if (line !== '' && line === line.trimStart() && /^allowBuilds:/u.test(line)) {
|
|
335
|
+
blockStart = i;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const present = new Set();
|
|
340
|
+
if (blockStart !== -1) {
|
|
341
|
+
for (let i = blockStart + 1; i < lines.length; i += 1) {
|
|
342
|
+
const line = lines[i];
|
|
343
|
+
if (line === '' || line === line.trimStart())
|
|
344
|
+
break; // dedent = block ends
|
|
345
|
+
const keyMatch = /^\s+('?)(.+?)\1:\s/u.exec(line);
|
|
346
|
+
if (keyMatch !== null)
|
|
347
|
+
present.add(keyMatch[2]);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const added = Object.keys(PROFILE_ALLOW_BUILDS).filter(key => !present.has(key));
|
|
351
|
+
if (added.length === 0)
|
|
352
|
+
return { existing: [...present], added };
|
|
353
|
+
const insert = added.map(allowBuildsKeyLine);
|
|
354
|
+
if (blockStart !== -1) {
|
|
355
|
+
// Append after the block's last existing entry (before the next
|
|
356
|
+
// top-level key), so existing entries keep their position.
|
|
357
|
+
let blockEnd = blockStart + 1;
|
|
358
|
+
for (let i = blockStart + 1; i < lines.length; i += 1) {
|
|
359
|
+
const line = lines[i];
|
|
360
|
+
if (line === '' || line === line.trimStart())
|
|
361
|
+
break;
|
|
362
|
+
blockEnd = i + 1;
|
|
363
|
+
}
|
|
364
|
+
lines.splice(blockEnd, 0, ...insert);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
if (lines.length > 0 && lines[lines.length - 1] !== '')
|
|
368
|
+
lines.push('');
|
|
369
|
+
lines.push('allowBuilds:', ...insert);
|
|
370
|
+
}
|
|
371
|
+
writeFileSync(yamlPath, `${lines.join('\n')}\n`);
|
|
372
|
+
return { existing: [...present], added };
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function existsSafe(path) {
|
|
379
|
+
try {
|
|
380
|
+
statSync(path);
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
215
387
|
/**
|
|
216
388
|
* The pnpm Windows tmp-rename race signature (issue #225): pnpm swaps a
|
|
217
389
|
* package directory via a `<name>_tmp_<pid>` staging dir, and a file lock or
|
|
@@ -225,6 +397,79 @@ export function tuiUpdatePluginArgs(profile, targetVersion) {
|
|
|
225
397
|
export function isTransientUpdateFailure(stderr) {
|
|
226
398
|
return /ENOENT|EPERM|EBUSY/i.test(stderr) && /_tmp_\d+/i.test(stderr);
|
|
227
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* The pnpm Linux flavor of the tmp-rename race (issue #479): the very same
|
|
402
|
+
* `importPackage` staging path, but overlayfs surfaces the second swap of an
|
|
403
|
+
* already-replaced package dir as EEXIST — deterministically, on every run,
|
|
404
|
+
* never transiently. A plain retry of the identical command ALWAYS fails the
|
|
405
|
+
* same way (the leftover `_tmp_<pid>_<threadId>` staging dir keeps colliding),
|
|
406
|
+
* so this signature must route to the recovery path
|
|
407
|
+
* (removeStalePackageInstall + rerun), not to the plain retry.
|
|
408
|
+
*/
|
|
409
|
+
export function isEexistTmpRenameFailure(stderr) {
|
|
410
|
+
return /EEXIST/i.test(stderr) && /_tmp_\d+/i.test(stderr);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* The dsh profile layout places plugin packages under
|
|
414
|
+
* `$DSH_HOME ?? ~/.dsh` — the same root dsh itself resolves
|
|
415
|
+
* (`dshHomePath is $DSH_HOME ?? ~/.dsh`, and packaged presets resolve
|
|
416
|
+
* identically). /update's recovery path needs this location to clear a
|
|
417
|
+
* half-updated install pnpm can no longer swap in place (issue #479).
|
|
418
|
+
*/
|
|
419
|
+
export function profilePackageDir(profile) {
|
|
420
|
+
const dshHome = process.env.DSH_HOME ?? join(homedir(), '.dsh');
|
|
421
|
+
return join(dshHome, 'profiles', profile, 'node_modules', PACKAGE_NAME);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Recovery for the pnpm tmp-rename races (issues #225/#479): remove the
|
|
425
|
+
* profile's installed copy of this package plus the sibling `_tmp_`
|
|
426
|
+
* staging dirs a crashed swap leaves behind, so the next installer run
|
|
427
|
+
* takes the fresh-install path instead of renaming over the collision.
|
|
428
|
+
* Field-verified workaround (issue #479): `rm -rf <pkg dir>` + rerun
|
|
429
|
+
* succeeds where ten plain retries fail.
|
|
430
|
+
*
|
|
431
|
+
* A junction/symlink at the package dir is UNLINKED only — the link is
|
|
432
|
+
* removed, the target tree (a dev checkout it may point at) is never
|
|
433
|
+
* traversed or deleted. Best effort throughout: this runs inside a
|
|
434
|
+
* failure-recovery branch, so an unusable result simply falls through to
|
|
435
|
+
* the rerun and the manual repair hint.
|
|
436
|
+
*/
|
|
437
|
+
export function removeStalePackageInstall(profile) {
|
|
438
|
+
const pkgDir = profilePackageDir(profile);
|
|
439
|
+
const scopeDir = dirname(pkgDir);
|
|
440
|
+
const removal = { packageDir: 'absent', tmpDirs: [] };
|
|
441
|
+
try {
|
|
442
|
+
const stats = lstatSync(pkgDir);
|
|
443
|
+
// lstat reports junctions AND symlinks as isSymbolicLink — remove the
|
|
444
|
+
// link itself, never what it points at.
|
|
445
|
+
if (stats.isSymbolicLink() || stats.isDirectory()) {
|
|
446
|
+
rmSync(pkgDir, { recursive: true, force: true });
|
|
447
|
+
removal.packageDir = 'removed';
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
removal.packageDir = error.code === 'ENOENT' ? 'absent' : 'failed';
|
|
452
|
+
}
|
|
453
|
+
try {
|
|
454
|
+
for (const name of readdirSync(scopeDir)) {
|
|
455
|
+
if (!/^dsh-tui_tmp_\d+_\d+$/.test(name))
|
|
456
|
+
continue;
|
|
457
|
+
const staging = join(scopeDir, name);
|
|
458
|
+
try {
|
|
459
|
+
rmSync(staging, { recursive: true, force: true });
|
|
460
|
+
removal.tmpDirs.push(name);
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
// Staging leftovers are best effort; the package dir removal is
|
|
464
|
+
// the load-bearing half of the recovery.
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
// Unreadable scope dir — nothing more to clear.
|
|
470
|
+
}
|
|
471
|
+
return removal;
|
|
472
|
+
}
|
|
228
473
|
/**
|
|
229
474
|
* Best-effort migrate the GLOBAL launcher to the delegating shim (0.8.7):
|
|
230
475
|
* after a successful profile update, copy this package's `bin/dsh-tui.js`
|
|
@@ -290,24 +535,17 @@ export function migrateGlobalLauncher() {
|
|
|
290
535
|
return false;
|
|
291
536
|
}
|
|
292
537
|
/**
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
538
|
+
* Install-only half of `/update`: run `dsh plugin add`, refuse boot-deadlock
|
|
539
|
+
* targets, verify the profile actually advanced, and migrate the global
|
|
540
|
+
* launcher. No restart — `updateTuiAndRestart` layers the session-preserving
|
|
541
|
+
* restart on top, and the `dsh-tui update` CLI stops here.
|
|
296
542
|
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
* the manifest range and can restart unchanged across minor releases.
|
|
302
|
-
*
|
|
303
|
-
* @param sessionId - Session to resume in the replacement process.
|
|
304
|
-
* @param profile - The dsh profile this TUI was launched with; updating any
|
|
305
|
-
* other profile would leave the running install untouched.
|
|
306
|
-
* @param targetVersion - Exact version returned by the preflight registry
|
|
307
|
-
* check, or undefined when that check failed and pnpm should resolve latest.
|
|
308
|
-
* @returns Exit codes for the update run and the replacement process.
|
|
543
|
+
* @param profile - The dsh profile to update.
|
|
544
|
+
* @param targetVersion - Exact version from the preflight registry check, or
|
|
545
|
+
* undefined when that check failed and pnpm should resolve latest.
|
|
546
|
+
* @returns The update exit code plus the before/after versions.
|
|
309
547
|
*/
|
|
310
|
-
export async function
|
|
548
|
+
export async function updateTui(profile, targetVersion) {
|
|
311
549
|
// Stamp the pre-update version BEFORE pnpm runs: it reads this package's
|
|
312
550
|
// manifest from disk, which the update replaces on the fly — a
|
|
313
551
|
// post-update read already sees the NEW version, and the restarted
|
|
@@ -316,6 +554,15 @@ export async function updateTuiAndRestart(sessionId, profile, targetVersion) {
|
|
|
316
554
|
const updatedFrom = installedTuiVersion() ?? '';
|
|
317
555
|
const dsh = process.platform === 'win32' ? 'dsh.cmd' : 'dsh';
|
|
318
556
|
const updateArgs = tuiUpdatePluginArgs(profile, targetVersion);
|
|
557
|
+
// pnpm ≥11 hard-fails installs whose dependency tree carries un-allowlisted
|
|
558
|
+
// build scripts (ERR_PNPM_IGNORED_BUILDS). The dsh-auth chain pulls in
|
|
559
|
+
// postinstall-only deps (@google/genai/protobufjs via pi-ai), so pre-seed
|
|
560
|
+
// the profile workspace with explicit `false` entries before pnpm runs.
|
|
561
|
+
const allowBuilds = ensureProfileAllowBuilds(profile);
|
|
562
|
+
if (allowBuilds !== undefined && allowBuilds.added.length > 0) {
|
|
563
|
+
process.stderr.write(`dsh-tui: pre-seeded profile pnpm allowBuilds (${allowBuilds.added.join(', ')}) — ` +
|
|
564
|
+
'postinstall-only deps are explicitly ignored\n');
|
|
565
|
+
}
|
|
319
566
|
let updateStderr = '';
|
|
320
567
|
const capture = (chunk) => { updateStderr += chunk; };
|
|
321
568
|
let updateCode = await runProcess(dsh, updateArgs, { shell: true, onStderr: capture });
|
|
@@ -327,8 +574,23 @@ export async function updateTuiAndRestart(sessionId, profile, targetVersion) {
|
|
|
327
574
|
updateStderr = '';
|
|
328
575
|
updateCode = await runProcess(dsh, updateArgs, { shell: true, onStderr: capture });
|
|
329
576
|
}
|
|
577
|
+
// Deterministic Linux tmp-rename race (issue #479): EEXIST never clears on
|
|
578
|
+
// a plain retry (the staging-dir collision reproduces every run), and a
|
|
579
|
+
// transient retry that failed again means the same leftover state. Both
|
|
580
|
+
// recover by removing the stale install + staging dirs, then rerunning —
|
|
581
|
+
// the field-verified workaround that takes pnpm down its fresh-install
|
|
582
|
+
// path instead of renaming over the collision.
|
|
583
|
+
if (updateCode !== 0 && (isEexistTmpRenameFailure(updateStderr) || isTransientUpdateFailure(updateStderr))) {
|
|
584
|
+
const flavor = isEexistTmpRenameFailure(updateStderr) ? 'EEXIST, issue #479' : 'retry failed, issue #225';
|
|
585
|
+
const removal = removeStalePackageInstall(profile);
|
|
586
|
+
const stagingNote = removal.tmpDirs.length > 0 ? ` + ${removal.tmpDirs.length} staging dir(s)` : '';
|
|
587
|
+
process.stderr.write(`dsh-tui: pnpm tmp-rename race (${flavor}) — cleared the stale install ` +
|
|
588
|
+
`(package dir: ${removal.packageDir}${stagingNote}) and rerunning the update…\n`);
|
|
589
|
+
updateStderr = '';
|
|
590
|
+
updateCode = await runProcess(dsh, updateArgs, { shell: true, onStderr: capture });
|
|
591
|
+
}
|
|
330
592
|
if (updateCode !== 0)
|
|
331
|
-
return { updateCode,
|
|
593
|
+
return { code: updateCode, updatedFrom };
|
|
332
594
|
// A --latest fallback (preflight failed) on a stale mirror can still land
|
|
333
595
|
// on the 0.7.0–0.7.1 hard-inject range — restarting into it under an older
|
|
334
596
|
// global-launcher patch is the permanent boot deadlock of issues
|
|
@@ -341,7 +603,7 @@ export async function updateTuiAndRestart(sessionId, profile, targetVersion) {
|
|
|
341
603
|
`(#183/#307) — NOT restarting into it. Repair with:\n` +
|
|
342
604
|
` dsh plugin --profile ${profile} add ${PACKAGE_NAME}@latest\n` +
|
|
343
605
|
`(if the mirror has not synced the latest release yet, retry later)\n`);
|
|
344
|
-
return {
|
|
606
|
+
return { code: 1, updatedFrom, installed: installedNow };
|
|
345
607
|
}
|
|
346
608
|
// Post-update verification (issue #225): pnpm can report success yet leave
|
|
347
609
|
// the profile half-updated (manifest old / lockfile new). Verify against
|
|
@@ -358,7 +620,7 @@ export async function updateTuiAndRestart(sessionId, profile, targetVersion) {
|
|
|
358
620
|
process.stderr.write(`dsh-tui: update completed but the profile still runs ${installed ?? 'an unreadable version'} ` +
|
|
359
621
|
`(expected ${targetVersion}) — the profile is half-updated. Repair manually with:\n` +
|
|
360
622
|
` dsh plugin --profile ${profile} add ${PACKAGE_NAME}@${targetVersion}\n`);
|
|
361
|
-
return {
|
|
623
|
+
return { code: 1, updatedFrom, installed };
|
|
362
624
|
}
|
|
363
625
|
}
|
|
364
626
|
// Launcher migration (0.8.7): the freshly installed profile carries the
|
|
@@ -368,15 +630,219 @@ export async function updateTuiAndRestart(sessionId, profile, targetVersion) {
|
|
|
368
630
|
if (migrateGlobalLauncher()) {
|
|
369
631
|
process.stderr.write('dsh-tui: global launcher aligned to the delegating shim (no manual npm i -g needed anymore).\n');
|
|
370
632
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
633
|
+
return { code: 0, updatedFrom, installed: installedTuiVersion() };
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Update the installed dsh-tui package and restart the same launcher while
|
|
637
|
+
* preserving the active session. The TUI must already be unmounted before
|
|
638
|
+
* this is called so pnpm output cannot corrupt the rendered terminal frame.
|
|
639
|
+
*
|
|
640
|
+
* When the preflight registry check resolved an exact target, pass that
|
|
641
|
+
* version to pnpm instead of resolving `latest` a second time. This avoids a
|
|
642
|
+
* stale mirror/dist-tag response between the check and install. If preflight
|
|
643
|
+
* failed, retain the `--latest` fallback: a plain `pnpm update` stays inside
|
|
644
|
+
* the manifest range and can restart unchanged across minor releases.
|
|
645
|
+
*
|
|
646
|
+
* @param sessionId - Session to resume in the replacement process.
|
|
647
|
+
* @param profile - The dsh profile this TUI was launched with; updating any
|
|
648
|
+
* other profile would leave the running install untouched.
|
|
649
|
+
* @param targetVersion - Exact version returned by the preflight registry
|
|
650
|
+
* check, or undefined when that check failed and pnpm should resolve latest.
|
|
651
|
+
* @returns Exit codes for the update run and the replacement process.
|
|
652
|
+
*/
|
|
653
|
+
export async function updateTuiAndRestart(sessionId, profile, targetVersion) {
|
|
654
|
+
const outcome = await updateTui(profile, targetVersion);
|
|
655
|
+
const { updatedFrom } = outcome;
|
|
656
|
+
if (outcome.code !== 0)
|
|
657
|
+
return { updateCode: outcome.code, restartCode: outcome.code };
|
|
658
|
+
// Restart through the same hardened handoff as /restart (issues
|
|
659
|
+
// #284/#307/#483): wait for the replacement's natural exit (the outer
|
|
660
|
+
// interpreter must not reclaim the console under it), re-assert the
|
|
661
|
+
// stdin detach from a watchdog so a revived pump cannot swallow the
|
|
662
|
+
// child's keypresses, and capture a fast-death stderr report. The old
|
|
663
|
+
// plain inherit spawn raced the interpreter for input on Windows
|
|
664
|
+
// Terminal (issue #483: "cannot type anything after /update").
|
|
665
|
+
const restartCode = await restartTui(sessionId, {
|
|
666
|
+
env: { [UPDATED_FROM_ENV]: updatedFrom },
|
|
667
|
+
kind: 'update',
|
|
668
|
+
});
|
|
669
|
+
return { updateCode: 0, restartCode };
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Headless `dsh-tui update`: the `/update` decision flow (preflight, deadlock
|
|
673
|
+
* refusal, mirror-lag note, `--latest` fallback) followed by the install-only
|
|
674
|
+
* half — no TUI, no restart. The bin launcher dynamic-imports this from the
|
|
675
|
+
* profile copy's compiled lib.
|
|
676
|
+
*
|
|
677
|
+
* @param profile - The dsh profile to update.
|
|
678
|
+
* @returns Process exit code: 0 on success or already-latest, 1 otherwise.
|
|
679
|
+
*/
|
|
680
|
+
export async function cliUpdate(profile) {
|
|
681
|
+
const target = await resolveTuiUpdateTarget();
|
|
682
|
+
if (target.kind === 'latest') {
|
|
683
|
+
process.stdout.write(`dsh-tui: already the latest version (${target.current}).\n`);
|
|
684
|
+
return 0;
|
|
685
|
+
}
|
|
686
|
+
let targetVersion;
|
|
687
|
+
if (target.kind === 'update') {
|
|
688
|
+
// Mirror the /update flow exactly: refuse the 0.7.0–0.7.1 hard-inject
|
|
689
|
+
// range (permanent boot deadlock under older launcher patches, #183/#307)
|
|
690
|
+
// instead of installing it.
|
|
691
|
+
if (isBootDeadlockTarget(target.latest)) {
|
|
692
|
+
process.stderr.write(`dsh-tui: refusing to update onto ${target.latest} — that range can permanently deadlock boot ` +
|
|
693
|
+
`(#183/#307). Latest on the official registry: ${target.authoritative ?? target.latest}. ` +
|
|
694
|
+
`If you use a mirror, retry after it syncs.\n`);
|
|
695
|
+
return 1;
|
|
696
|
+
}
|
|
697
|
+
if (target.authoritative !== undefined) {
|
|
698
|
+
process.stdout.write(`dsh-tui: note — your registry serves ${target.latest} while npmjs.org has ${target.authoritative} (mirror lag).\n`);
|
|
699
|
+
}
|
|
700
|
+
targetVersion = target.latest;
|
|
701
|
+
process.stdout.write(`dsh-tui: updating ${target.current} → ${target.latest}…\n`);
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
process.stdout.write('dsh-tui: version check failed (offline or unreadable install) — falling back to `--latest`.\n');
|
|
705
|
+
}
|
|
706
|
+
const outcome = await updateTui(profile, targetVersion);
|
|
707
|
+
if (outcome.code === 0) {
|
|
708
|
+
// A preflight-less run (`--latest` fallback) can "succeed" as a pnpm
|
|
709
|
+
// no-op: same manifest before and after. Say so instead of printing a
|
|
710
|
+
// vacuous `updated X → X` — the /update path surfaces the same state via
|
|
711
|
+
// the DSH_TUI_UPDATED_FROM stamp on restart.
|
|
712
|
+
if (targetVersion === undefined && outcome.installed !== undefined && outcome.installed === outcome.updatedFrom) {
|
|
713
|
+
process.stdout.write(`dsh-tui: version did not advance (still ${outcome.installed}) — already the latest, or the registry has no newer release yet.\n`);
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
process.stdout.write(`dsh-tui: updated ${outcome.updatedFrom || '(unknown)'} → ${outcome.installed ?? '(unreadable)'}.\n`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return outcome.code;
|
|
720
|
+
}
|
|
721
|
+
export async function restartTui(sessionId, options = {}) {
|
|
722
|
+
const kind = options.kind ?? 'restart';
|
|
723
|
+
const tag = kind === 'update' ? 'update-restart' : 'restart';
|
|
724
|
+
const argv = [...process.execArgv, ...process.argv.slice(1)];
|
|
725
|
+
logRestartEvent(`${tag}: spawning replacement`, {
|
|
726
|
+
node: process.execPath,
|
|
727
|
+
argv,
|
|
728
|
+
cwd: process.cwd(),
|
|
729
|
+
stdinTty: process.stdin.isTTY === true,
|
|
730
|
+
stdoutTty: process.stdout.isTTY === true,
|
|
731
|
+
stderrTty: process.stderr.isTTY === true,
|
|
732
|
+
nodeOptions: process.env.NODE_OPTIONS ?? null,
|
|
733
|
+
dshHome: process.env.DSH_HOME ?? null,
|
|
734
|
+
});
|
|
735
|
+
const startedAt = Date.now();
|
|
736
|
+
return new Promise(resolve => {
|
|
737
|
+
const child = spawn(process.execPath, argv, {
|
|
738
|
+
env: {
|
|
739
|
+
...process.env,
|
|
740
|
+
// Dual-write the resume contract (issue #120): the cordis layer of a
|
|
741
|
+
// still-old TUI build reads only DSH_CC_RESUME_SESSION.
|
|
742
|
+
DSH_TUI_RESUME_SESSION: sessionId,
|
|
743
|
+
DSH_CC_RESUME_SESSION: sessionId,
|
|
744
|
+
// Marks the replacement so its own boot logs to restart.log without
|
|
745
|
+
// noisy logging on every ordinary launch (/restart only).
|
|
746
|
+
...(kind === 'restart' ? { [RESTART_CHILD_ENV]: '1' } : {}),
|
|
747
|
+
...options.env,
|
|
748
|
+
},
|
|
749
|
+
// stdin/stdout stay inherited so the replacement owns the console the
|
|
750
|
+
// moment it boots; stderr is captured so a boot failure is reportable
|
|
751
|
+
// through THIS process (the terminal may already be mid-handoff when
|
|
752
|
+
// the child dies, and a raw inherit write can vanish).
|
|
753
|
+
stdio: ['inherit', 'inherit', 'pipe'],
|
|
754
|
+
});
|
|
755
|
+
logRestartEvent(`${tag}: replacement spawned`, { childPid: child.pid });
|
|
756
|
+
// Handoff watchdog (field evidence 2026-08-24: restarted TUI mounts but
|
|
757
|
+
// takes no input). Two jobs, both diagnosis-grade:
|
|
758
|
+
// 1. SAMPLE this process's stdin state every second — if anything
|
|
759
|
+
// re-attaches a reader after the funnel's detachStdinForHandoff, the
|
|
760
|
+
// sample (taken before the re-assert below) shows it in the log.
|
|
761
|
+
// 2. RE-ASSERT the detach and finally destroy the stream: this process
|
|
762
|
+
// must never read the shared console again — every keypress belongs
|
|
763
|
+
// to the replacement, and a resumed pump here is exactly the
|
|
764
|
+
// "restarted TUI sees dropped or swallowed input" failure (#284/#307).
|
|
765
|
+
let watchdogTicks = 0;
|
|
766
|
+
const watchdog = setInterval(() => {
|
|
767
|
+
watchdogTicks += 1;
|
|
768
|
+
const stdin = process.stdin;
|
|
769
|
+
logRestartEvent('parent: stdin watchdog', {
|
|
770
|
+
tick: watchdogTicks,
|
|
771
|
+
readableListeners: stdin.listenerCount('readable'),
|
|
772
|
+
dataListeners: stdin.listenerCount('data'),
|
|
773
|
+
paused: stdin.isPaused,
|
|
774
|
+
raw: stdin.isRaw === true,
|
|
775
|
+
buffered: stdin.readableLength,
|
|
776
|
+
});
|
|
777
|
+
try {
|
|
778
|
+
stdin.removeAllListeners('readable');
|
|
779
|
+
stdin.removeAllListeners('data');
|
|
780
|
+
stdin.pause();
|
|
781
|
+
}
|
|
782
|
+
catch {
|
|
783
|
+
// Diagnosis/mitigation only.
|
|
784
|
+
}
|
|
785
|
+
if (watchdogTicks === 15) {
|
|
786
|
+
clearInterval(watchdog);
|
|
787
|
+
try {
|
|
788
|
+
// Terminal safeguard: a destroyed stream can never be resumed by
|
|
789
|
+
// any late re-attachment. The child holds its own inherited
|
|
790
|
+
// handle, so closing ours does not affect it.
|
|
791
|
+
process.stdin.destroy();
|
|
792
|
+
logRestartEvent('parent: stdin destroyed after watchdog');
|
|
793
|
+
}
|
|
794
|
+
catch {
|
|
795
|
+
// Best effort.
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}, 1000);
|
|
799
|
+
watchdog.unref();
|
|
800
|
+
let childStderr = '';
|
|
801
|
+
let loggedStderrBytes = 0;
|
|
802
|
+
child.stderr?.on('data', (chunk) => {
|
|
803
|
+
childStderr += chunk.toString('utf8');
|
|
804
|
+
// Mirror chunks to the log capped: the full text still reaches the
|
|
805
|
+
// terminal report below, and unbounded output must not grow the file.
|
|
806
|
+
if (loggedStderrBytes < 4096) {
|
|
807
|
+
loggedStderrBytes += chunk.length;
|
|
808
|
+
logRestartEvent(`${tag}: replacement stderr`, { text: chunk.toString('utf8').slice(0, 2048) });
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
// Diagnosis marker only — never resolves: after the window the child owns
|
|
812
|
+
// the terminal, and this process must keep the launch chain open until
|
|
813
|
+
// the replacement exits (see the header comment).
|
|
814
|
+
const timer = setTimeout(() => {
|
|
815
|
+
logRestartEvent(`${tag}: survival window passed, following the replacement until it exits`);
|
|
816
|
+
}, 4000);
|
|
817
|
+
timer.unref();
|
|
818
|
+
child.once('error', error => {
|
|
819
|
+
clearTimeout(timer);
|
|
820
|
+
logRestartEvent(`${tag}: spawn error`, { message: error.message });
|
|
821
|
+
writeHandoffNotice(`dsh-tui: failed to spawn the restart: ${error.message}\n`);
|
|
822
|
+
resolve(127);
|
|
823
|
+
});
|
|
824
|
+
child.once('close', (code, signal) => {
|
|
825
|
+
clearTimeout(timer);
|
|
826
|
+
const elapsedMs = Date.now() - startedAt;
|
|
827
|
+
logRestartEvent(`${tag}: replacement exited`, {
|
|
828
|
+
code: code ?? null,
|
|
829
|
+
signal: signal ?? null,
|
|
830
|
+
elapsedMs,
|
|
831
|
+
});
|
|
832
|
+
if (elapsedMs < 4000) {
|
|
833
|
+
// Fast death: the TUI never came up. Synchronous stderr write —
|
|
834
|
+
// process.exit() right after an async stream write skips the flush,
|
|
835
|
+
// and a vanished diagnosis is indistinguishable from silent failure.
|
|
836
|
+
const suffix = childStderr.trim() === '' ? '' : `\n${childStderr.trimEnd()}`;
|
|
837
|
+
writeHandoffNotice(`dsh-tui: restart child exited during the handoff (code ${code ?? 'null'}` +
|
|
838
|
+
`${signal === undefined || signal === null ? '' : `, signal ${signal}`}) — the TUI did not come up.` +
|
|
839
|
+
`${suffix}\nYour session is preserved; resume with the launcher or retry the command.\n`);
|
|
840
|
+
}
|
|
841
|
+
else if (code !== 0 && code !== null) {
|
|
842
|
+
// Late nonzero exit: the session ended abnormally, say so briefly.
|
|
843
|
+
writeHandoffNotice(`\ndsh-tui: the restarted session exited with code ${code}.\n`);
|
|
844
|
+
}
|
|
845
|
+
resolve(code ?? 1);
|
|
846
|
+
});
|
|
380
847
|
});
|
|
381
|
-
return { updateCode, restartCode };
|
|
382
848
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export declare function logForDebugging(message: string, fields?: Record<string, unknown>): void;
|
|
8
8
|
/**
|
|
9
|
-
* Mouse-chain diagnostics: appends to `~/.dsh-
|
|
9
|
+
* Mouse-chain diagnostics: appends to `~/.dsh-tui/mouse-debug.log` when
|
|
10
10
|
* `DSH_TUI_DEBUG_MOUSE` is set. Unlike logForDebugging (stderr), a file
|
|
11
11
|
* keeps the fullscreen alt screen unpolluted and survives the session for
|
|
12
12
|
* post-mortem reading. Every append is try/catch-guarded — diagnostics
|
package/lib/types/utils/debug.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { appendFileSync } from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import { DATA_DIR } from './paths.js';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
/**
|
|
5
5
|
* Debug logger for the ported Ink core. Writes to stderr only when
|
|
@@ -14,7 +14,7 @@ export function logForDebugging(message, fields) {
|
|
|
14
14
|
process.stderr.write(`[dsh-tui] ${message}${suffix}\n`);
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* Mouse-chain diagnostics: appends to `~/.dsh-
|
|
17
|
+
* Mouse-chain diagnostics: appends to `~/.dsh-tui/mouse-debug.log` when
|
|
18
18
|
* `DSH_TUI_DEBUG_MOUSE` is set. Unlike logForDebugging (stderr), a file
|
|
19
19
|
* keeps the fullscreen alt screen unpolluted and survives the session for
|
|
20
20
|
* post-mortem reading. Every append is try/catch-guarded — diagnostics
|
|
@@ -28,7 +28,7 @@ export function logMouseDebug(message, fields) {
|
|
|
28
28
|
const suffix = fields === undefined ? '' : ` ${JSON.stringify(fields)}`;
|
|
29
29
|
try {
|
|
30
30
|
const line = `${new Date().toISOString()} ${message}${suffix}\n`;
|
|
31
|
-
appendFileSync(join(
|
|
31
|
+
appendFileSync(join(DATA_DIR, 'mouse-debug.log'), line);
|
|
32
32
|
}
|
|
33
33
|
catch {
|
|
34
34
|
// ignore — see doc comment
|