@orderful/droid 0.14.0 → 0.16.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.
Files changed (204) hide show
  1. package/.claude/CLAUDE.md +31 -9
  2. package/CHANGELOG.md +35 -0
  3. package/dist/bin/droid.js +8 -8
  4. package/dist/bin/droid.js.map +1 -1
  5. package/dist/commands/config.js +1 -1
  6. package/dist/commands/config.js.map +1 -1
  7. package/dist/commands/install.js +3 -3
  8. package/dist/commands/install.js.map +1 -1
  9. package/dist/commands/setup.d.ts +1 -1
  10. package/dist/commands/setup.d.ts.map +1 -1
  11. package/dist/commands/setup.js +3 -3
  12. package/dist/commands/setup.js.map +1 -1
  13. package/dist/commands/skills.js +4 -4
  14. package/dist/commands/skills.js.map +1 -1
  15. package/dist/commands/tui/components/Badge.d.ts +13 -0
  16. package/dist/commands/tui/components/Badge.d.ts.map +1 -0
  17. package/dist/commands/tui/components/Badge.js +29 -0
  18. package/dist/commands/tui/components/Badge.js.map +1 -0
  19. package/dist/commands/tui/components/Markdown.d.ts +5 -0
  20. package/dist/commands/tui/components/Markdown.d.ts.map +1 -0
  21. package/dist/commands/tui/components/Markdown.js +42 -0
  22. package/dist/commands/tui/components/Markdown.js.map +1 -0
  23. package/dist/commands/tui/components/SettingsDetails.d.ts +5 -0
  24. package/dist/commands/tui/components/SettingsDetails.d.ts.map +1 -0
  25. package/dist/commands/tui/components/SettingsDetails.js +11 -0
  26. package/dist/commands/tui/components/SettingsDetails.js.map +1 -0
  27. package/dist/commands/tui/components/TabBar.d.ts +10 -0
  28. package/dist/commands/tui/components/TabBar.d.ts.map +1 -0
  29. package/dist/commands/tui/components/TabBar.js +7 -0
  30. package/dist/commands/tui/components/TabBar.js.map +1 -0
  31. package/dist/commands/tui/components/ToolDetails.d.ts +8 -0
  32. package/dist/commands/tui/components/ToolDetails.d.ts.map +1 -0
  33. package/dist/commands/tui/components/ToolDetails.js +35 -0
  34. package/dist/commands/tui/components/ToolDetails.js.map +1 -0
  35. package/dist/commands/tui/components/ToolItem.d.ts +9 -0
  36. package/dist/commands/tui/components/ToolItem.d.ts.map +1 -0
  37. package/dist/commands/tui/components/ToolItem.js +11 -0
  38. package/dist/commands/tui/components/ToolItem.js.map +1 -0
  39. package/dist/commands/tui/constants.d.ts +16 -0
  40. package/dist/commands/tui/constants.d.ts.map +1 -0
  41. package/dist/commands/tui/constants.js +17 -0
  42. package/dist/commands/tui/constants.js.map +1 -0
  43. package/dist/commands/tui/hooks/useAppUpdate.d.ts +13 -0
  44. package/dist/commands/tui/hooks/useAppUpdate.d.ts.map +1 -0
  45. package/dist/commands/tui/hooks/useAppUpdate.js +52 -0
  46. package/dist/commands/tui/hooks/useAppUpdate.js.map +1 -0
  47. package/dist/commands/tui/hooks/useToolUpdates.d.ts +22 -0
  48. package/dist/commands/tui/hooks/useToolUpdates.d.ts.map +1 -0
  49. package/dist/commands/tui/hooks/useToolUpdates.js +77 -0
  50. package/dist/commands/tui/hooks/useToolUpdates.js.map +1 -0
  51. package/dist/commands/tui/types.d.ts +5 -0
  52. package/dist/commands/tui/types.d.ts.map +1 -0
  53. package/dist/commands/tui/types.js +2 -0
  54. package/dist/commands/tui/types.js.map +1 -0
  55. package/dist/commands/tui/views/ReadmeViewer.d.ts +7 -0
  56. package/dist/commands/tui/views/ReadmeViewer.d.ts.map +1 -0
  57. package/dist/commands/tui/views/ReadmeViewer.js +56 -0
  58. package/dist/commands/tui/views/ReadmeViewer.js.map +1 -0
  59. package/dist/commands/tui/views/SetupScreen.d.ts +8 -0
  60. package/dist/commands/tui/views/SetupScreen.d.ts.map +1 -0
  61. package/dist/commands/tui/views/SetupScreen.js +114 -0
  62. package/dist/commands/tui/views/SetupScreen.js.map +1 -0
  63. package/dist/commands/tui/views/SkillConfigScreen.d.ts +8 -0
  64. package/dist/commands/tui/views/SkillConfigScreen.d.ts.map +1 -0
  65. package/dist/commands/tui/views/SkillConfigScreen.js +148 -0
  66. package/dist/commands/tui/views/SkillConfigScreen.js.map +1 -0
  67. package/dist/commands/tui/views/ToolExplorer.d.ts +8 -0
  68. package/dist/commands/tui/views/ToolExplorer.d.ts.map +1 -0
  69. package/dist/commands/tui/views/ToolExplorer.js +86 -0
  70. package/dist/commands/tui/views/ToolExplorer.js.map +1 -0
  71. package/dist/commands/tui/views/ToolUpdatePrompt.d.ts +10 -0
  72. package/dist/commands/tui/views/ToolUpdatePrompt.d.ts.map +1 -0
  73. package/dist/commands/tui/views/ToolUpdatePrompt.js +38 -0
  74. package/dist/commands/tui/views/ToolUpdatePrompt.js.map +1 -0
  75. package/dist/commands/tui/views/WelcomeScreen.d.ts +11 -0
  76. package/dist/commands/tui/views/WelcomeScreen.d.ts.map +1 -0
  77. package/dist/commands/tui/views/WelcomeScreen.js +46 -0
  78. package/dist/commands/tui/views/WelcomeScreen.js.map +1 -0
  79. package/dist/commands/tui.d.ts.map +1 -1
  80. package/dist/commands/tui.js +54 -756
  81. package/dist/commands/tui.js.map +1 -1
  82. package/dist/commands/uninstall.js +2 -2
  83. package/dist/commands/uninstall.js.map +1 -1
  84. package/dist/commands/update.js +1 -1
  85. package/dist/commands/update.js.map +1 -1
  86. package/dist/index.d.ts +4 -4
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +4 -4
  89. package/dist/index.js.map +1 -1
  90. package/dist/lib/agents.d.ts +7 -7
  91. package/dist/lib/agents.d.ts.map +1 -1
  92. package/dist/lib/agents.js +63 -41
  93. package/dist/lib/agents.js.map +1 -1
  94. package/dist/lib/config.d.ts +1 -1
  95. package/dist/lib/config.d.ts.map +1 -1
  96. package/dist/lib/config.js +1 -1
  97. package/dist/lib/config.js.map +1 -1
  98. package/dist/lib/platforms.d.ts +1 -1
  99. package/dist/lib/platforms.d.ts.map +1 -1
  100. package/dist/lib/platforms.js +1 -1
  101. package/dist/lib/platforms.js.map +1 -1
  102. package/dist/lib/skill-config.d.ts +1 -1
  103. package/dist/lib/skill-config.d.ts.map +1 -1
  104. package/dist/lib/skill-config.js +2 -2
  105. package/dist/lib/skill-config.js.map +1 -1
  106. package/dist/lib/skills.d.ts +2 -1
  107. package/dist/lib/skills.d.ts.map +1 -1
  108. package/dist/lib/skills.js +45 -12
  109. package/dist/lib/skills.js.map +1 -1
  110. package/dist/lib/tools.d.ts +1 -1
  111. package/dist/lib/tools.d.ts.map +1 -1
  112. package/dist/lib/tools.js +3 -3
  113. package/dist/lib/tools.js.map +1 -1
  114. package/dist/lib/types.d.ts +4 -0
  115. package/dist/lib/types.d.ts.map +1 -1
  116. package/dist/tools/README.md +79 -50
  117. package/dist/tools/brain/TOOL.yaml +1 -1
  118. package/dist/tools/brain/skills/brain/SKILL.md +1 -0
  119. package/dist/tools/brain/skills/brain-obsidian/SKILL.md +1 -0
  120. package/dist/tools/coach/TOOL.yaml +1 -1
  121. package/dist/tools/coach/skills/coach/SKILL.md +1 -0
  122. package/{src/tools/code-review/agents/edi-standards-reviewer/AGENT.md → dist/tools/code-review/agents/edi-standards-reviewer.md} +10 -0
  123. package/dist/tools/code-review/agents/{error-handling-reviewer/AGENT.md → error-handling-reviewer.md} +10 -0
  124. package/{src/tools/code-review/agents/test-coverage-analyzer/AGENT.md → dist/tools/code-review/agents/test-coverage-analyzer.md} +11 -0
  125. package/dist/tools/code-review/agents/{type-reviewer/AGENT.md → type-reviewer.md} +10 -0
  126. package/dist/tools/code-review/skills/code-review/SKILL.md +1 -0
  127. package/dist/tools/comments/TOOL.yaml +2 -2
  128. package/dist/tools/comments/skills/comments/SKILL.md +1 -0
  129. package/dist/tools/droid/skills/droid/SKILL.md +1 -0
  130. package/dist/tools/project/skills/project/SKILL.md +1 -0
  131. package/package.json +3 -3
  132. package/src/bin/droid.ts +8 -8
  133. package/src/commands/config.ts +1 -1
  134. package/src/commands/install.ts +3 -3
  135. package/src/commands/setup.test.ts +1 -1
  136. package/src/commands/setup.ts +3 -3
  137. package/src/commands/skills.ts +4 -4
  138. package/src/commands/tui/components/Badge.tsx +86 -0
  139. package/src/commands/tui/components/Markdown.tsx +48 -0
  140. package/src/commands/tui/components/SettingsDetails.tsx +70 -0
  141. package/src/commands/tui/components/TabBar.tsx +26 -0
  142. package/src/commands/tui/components/ToolDetails.tsx +117 -0
  143. package/src/commands/tui/components/ToolItem.tsx +39 -0
  144. package/src/commands/tui/constants.ts +17 -0
  145. package/src/commands/tui/hooks/useAppUpdate.ts +67 -0
  146. package/src/commands/tui/hooks/useToolUpdates.ts +110 -0
  147. package/src/commands/tui/types.ts +4 -0
  148. package/src/commands/tui/views/ReadmeViewer.tsx +93 -0
  149. package/src/commands/tui/views/SetupScreen.tsx +242 -0
  150. package/src/commands/tui/views/SkillConfigScreen.tsx +278 -0
  151. package/src/commands/tui/views/ToolExplorer.tsx +190 -0
  152. package/src/commands/tui/views/ToolUpdatePrompt.tsx +109 -0
  153. package/src/commands/tui/views/WelcomeScreen.tsx +149 -0
  154. package/src/commands/tui.tsx +65 -1588
  155. package/src/commands/uninstall.ts +2 -2
  156. package/src/commands/update.ts +1 -1
  157. package/src/index.ts +4 -4
  158. package/src/lib/agents.ts +68 -45
  159. package/src/lib/config.ts +1 -1
  160. package/src/lib/platforms.ts +1 -1
  161. package/src/lib/skill-config.ts +2 -2
  162. package/src/lib/skills.test.ts +28 -33
  163. package/src/lib/skills.ts +49 -12
  164. package/src/lib/tools.ts +3 -3
  165. package/src/lib/types.test.ts +1 -1
  166. package/src/lib/types.ts +5 -0
  167. package/src/lib/version.test.ts +1 -1
  168. package/src/tools/README.md +79 -50
  169. package/src/tools/brain/TOOL.yaml +1 -1
  170. package/src/tools/brain/skills/brain/SKILL.md +1 -0
  171. package/src/tools/brain/skills/brain-obsidian/SKILL.md +1 -0
  172. package/src/tools/coach/TOOL.yaml +1 -1
  173. package/src/tools/coach/skills/coach/SKILL.md +1 -0
  174. package/{dist/tools/code-review/agents/edi-standards-reviewer/AGENT.md → src/tools/code-review/agents/edi-standards-reviewer.md} +10 -0
  175. package/src/tools/code-review/agents/{error-handling-reviewer/AGENT.md → error-handling-reviewer.md} +10 -0
  176. package/{dist/tools/code-review/agents/test-coverage-analyzer/AGENT.md → src/tools/code-review/agents/test-coverage-analyzer.md} +11 -0
  177. package/src/tools/code-review/agents/{type-reviewer/AGENT.md → type-reviewer.md} +10 -0
  178. package/src/tools/code-review/skills/code-review/SKILL.md +1 -0
  179. package/src/tools/comments/TOOL.yaml +2 -2
  180. package/src/tools/comments/skills/comments/SKILL.md +1 -0
  181. package/src/tools/droid/skills/droid/SKILL.md +1 -0
  182. package/src/tools/project/skills/project/SKILL.md +1 -0
  183. package/dist/tools/brain/skills/brain/SKILL.yaml +0 -29
  184. package/dist/tools/brain/skills/brain-obsidian/SKILL.yaml +0 -42
  185. package/dist/tools/coach/skills/coach/SKILL.yaml +0 -25
  186. package/dist/tools/code-review/agents/edi-standards-reviewer/AGENT.yaml +0 -14
  187. package/dist/tools/code-review/agents/error-handling-reviewer/AGENT.yaml +0 -14
  188. package/dist/tools/code-review/agents/test-coverage-analyzer/AGENT.yaml +0 -14
  189. package/dist/tools/code-review/agents/type-reviewer/AGENT.yaml +0 -13
  190. package/dist/tools/code-review/skills/code-review/SKILL.yaml +0 -19
  191. package/dist/tools/comments/skills/comments/SKILL.yaml +0 -50
  192. package/dist/tools/droid/skills/droid/SKILL.yaml +0 -7
  193. package/dist/tools/project/skills/project/SKILL.yaml +0 -30
  194. package/src/tools/brain/skills/brain/SKILL.yaml +0 -29
  195. package/src/tools/brain/skills/brain-obsidian/SKILL.yaml +0 -42
  196. package/src/tools/coach/skills/coach/SKILL.yaml +0 -25
  197. package/src/tools/code-review/agents/edi-standards-reviewer/AGENT.yaml +0 -14
  198. package/src/tools/code-review/agents/error-handling-reviewer/AGENT.yaml +0 -14
  199. package/src/tools/code-review/agents/test-coverage-analyzer/AGENT.yaml +0 -14
  200. package/src/tools/code-review/agents/type-reviewer/AGENT.yaml +0 -13
  201. package/src/tools/code-review/skills/code-review/SKILL.yaml +0 -19
  202. package/src/tools/comments/skills/comments/SKILL.yaml +0 -50
  203. package/src/tools/droid/skills/droid/SKILL.yaml +0 -7
  204. package/src/tools/project/skills/project/SKILL.yaml +0 -30
@@ -1,1417 +1,32 @@
1
1
  import { render, Box, Text, useInput, useApp } from 'ink';
2
- import TextInput from 'ink-text-input';
3
- import { useState, useMemo } from 'react';
4
- import { execSync } from 'child_process';
5
- import { existsSync, readFileSync } from 'fs';
6
- import { join } from 'path';
2
+ import { useState } from 'react';
7
3
  import {
8
4
  getBundledSkills,
9
- isSkillInstalled,
10
5
  installSkill,
11
6
  uninstallSkill,
12
7
  updateSkill,
13
- getSkillUpdateStatus,
14
- } from '../lib/skills.js';
15
- import { getBundledTools, getBundledToolsDir, isToolInstalled, getToolUpdateStatus, getInstalledToolVersion, getToolsWithUpdates, type ToolUpdateInfo } from '../lib/tools.js';
16
- import { configExists, loadConfig, saveConfig, loadSkillOverrides, saveSkillOverrides, getAutoUpdateConfig, setAutoUpdateConfig } from '../lib/config.js';
17
- import { configurePlatformPermissions } from './setup.js';
18
- import { Platform, BuiltInOutput, ConfigOptionType, type DroidConfig, type OutputPreference, type SkillManifest, type ConfigOption, type SkillOverrides, type ToolManifest } from '../lib/types.js';
19
- import { getVersion, getUpdateInfo, runUpdate, type UpdateInfo } from '../lib/version.js';
20
- import { getRandomQuote } from '../lib/quotes.js';
8
+ } from '../lib/skills';
9
+ import { getBundledTools, isToolInstalled, getToolUpdateStatus } from '../lib/tools';
10
+ import { configExists, loadConfig } from '../lib/config';
11
+ import { Platform, type ConfigOption, type ToolManifest } from '../lib/types';
12
+ import { getVersion } from '../lib/version';
13
+ import { type Tab, type View } from './tui/types';
14
+ import { colors, MAX_VISIBLE_ITEMS } from './tui/constants';
15
+ import { TabBar } from './tui/components/TabBar';
16
+ import { ToolItem } from './tui/components/ToolItem';
17
+ import { ToolDetails } from './tui/components/ToolDetails';
18
+ import { SettingsDetails } from './tui/components/SettingsDetails';
19
+ import { WelcomeScreen } from './tui/views/WelcomeScreen';
20
+ import { ToolUpdatePrompt } from './tui/views/ToolUpdatePrompt';
21
+ import { SetupScreen } from './tui/views/SetupScreen';
22
+ import { ReadmeViewer } from './tui/views/ReadmeViewer';
23
+ import { ToolExplorer } from './tui/views/ToolExplorer';
24
+ import { SkillConfigScreen } from './tui/views/SkillConfigScreen';
25
+ import { useAppUpdate } from './tui/hooks/useAppUpdate';
26
+ import { useToolUpdates } from './tui/hooks/useToolUpdates';
21
27
 
22
- type Tab = 'tools' | 'settings';
23
- type View = 'welcome' | 'tool-updates' | 'setup' | 'menu' | 'detail' | 'configure' | 'readme' | 'explorer';
24
-
25
- // Module-level variable to store exit message (printed after leaving alternate screen)
26
- let exitMessage: string | null = null;
27
- type SetupStep = 'platform' | 'user_mention' | 'confirm';
28
- type ComponentType = 'skill' | 'command' | 'agent';
29
-
30
- const colors = {
31
- primary: '#6366f1',
32
- bgSelected: '#2d2d2d',
33
- border: '#3a3a3a',
34
- text: '#e8e8e8',
35
- textMuted: '#999999',
36
- textDim: '#6a6a6a',
37
- success: '#4ade80',
38
- error: '#f87171',
39
- // Component type badges
40
- skill: '#ec4899', // pink/magenta
41
- command: '#22d3ee', // cyan
42
- agent: '#fbbf24', // yellow/amber
43
- };
44
-
45
- function Badge({
46
- type,
47
- label,
48
- isSelected = false,
49
- dimmed = false,
50
- }: {
51
- type: ComponentType;
52
- label?: string;
53
- isSelected?: boolean;
54
- dimmed?: boolean;
55
- }) {
56
- const color = colors[type];
57
- const displayLabel = label || type.charAt(0).toUpperCase() + type.slice(1);
58
-
59
- if (dimmed) {
60
- return (
61
- <Text color={colors.textDim}>{displayLabel}</Text>
62
- );
63
- }
64
-
65
- return (
66
- <Text
67
- backgroundColor={isSelected ? color : undefined}
68
- color={isSelected ? '#000000' : color}
69
- bold={isSelected}
70
- >
71
- {isSelected ? ` ${displayLabel} ` : displayLabel}
72
- </Text>
73
- );
74
- }
75
-
76
- function ComponentBadges({ tool, compact = false }: { tool: ToolManifest; compact?: boolean }) {
77
- const hasSkills = tool.includes.skills.length > 0;
78
- const hasCommands = tool.includes.commands.length > 0;
79
- const hasAgents = tool.includes.agents.length > 0;
80
-
81
- if (compact) {
82
- // Show colored squares for list view (single char with spacing)
83
- const parts: string[] = [];
84
- if (hasSkills) parts.push('skill');
85
- if (hasCommands) parts.push('command');
86
- if (hasAgents) parts.push('agent');
87
-
88
- return (
89
- <Box flexDirection="row">
90
- {parts.map((type, i) => (
91
- <Text key={type}>
92
- <Text backgroundColor={colors[type as ComponentType]}> </Text>
93
- {i < parts.length - 1 && ' '}
94
- </Text>
95
- ))}
96
- </Box>
97
- );
98
- }
99
-
100
- return (
101
- <Box flexDirection="row">
102
- {hasSkills && (
103
- <Box marginRight={1}>
104
- <Text backgroundColor={colors.skill} color="#000000" bold>
105
- {` ${tool.includes.skills.length} skill${tool.includes.skills.length > 1 ? 's' : ''} `}
106
- </Text>
107
- </Box>
108
- )}
109
- {hasCommands && (
110
- <Box marginRight={1}>
111
- <Text backgroundColor={colors.command} color="#000000" bold>
112
- {` ${tool.includes.commands.length} cmd${tool.includes.commands.length > 1 ? 's' : ''} `}
113
- </Text>
114
- </Box>
115
- )}
116
- {hasAgents && (
117
- <Box marginRight={1}>
118
- <Text backgroundColor={colors.agent} color="#000000" bold>
119
- {` ${tool.includes.agents.length} agent${tool.includes.agents.length > 1 ? 's' : ''} `}
120
- </Text>
121
- </Box>
122
- )}
123
- </Box>
124
- );
125
- }
126
-
127
- function detectInstalledPlatforms(): Set<Platform> {
128
- const installed = new Set<Platform>();
129
- try {
130
- execSync('claude --version', { stdio: 'ignore' });
131
- installed.add(Platform.ClaudeCode);
132
- } catch {
133
- // Claude Code not found
134
- }
135
- try {
136
- execSync('opencode --version', { stdio: 'ignore' });
137
- installed.add(Platform.OpenCode);
138
- } catch {
139
- // OpenCode not found
140
- }
141
- return installed;
142
- }
143
-
144
- function getDefaultPlatform(): Platform {
145
- const installed = detectInstalledPlatforms();
146
- if (installed.has(Platform.ClaudeCode)) return Platform.ClaudeCode;
147
- if (installed.has(Platform.OpenCode)) return Platform.OpenCode;
148
- return Platform.ClaudeCode;
149
- }
150
-
151
- function getOutputOptions(): Array<{ label: string; value: OutputPreference }> {
152
- const options: Array<{ label: string; value: OutputPreference }> = [
153
- { label: 'Terminal (display in CLI)', value: BuiltInOutput.Terminal },
154
- { label: 'Editor ($EDITOR)', value: BuiltInOutput.Editor },
155
- ];
156
- const skills = getBundledSkills();
157
- for (const skill of skills) {
158
- if (skill.provides_output) {
159
- options.push({ label: `${skill.name} (${skill.description})`, value: skill.name });
160
- }
161
- }
162
- return options;
163
- }
164
-
165
- interface WelcomeScreenProps {
166
- onContinue: () => void;
167
- onUpdate: () => void;
168
- onAlways: () => void;
169
- onExit: () => void;
170
- updateInfo: UpdateInfo;
171
- isUpdating: boolean;
172
- }
173
-
174
- function WelcomeScreen({ onContinue, onUpdate, onAlways, onExit, updateInfo, isUpdating }: WelcomeScreenProps) {
175
- const [selectedButton, setSelectedButton] = useState(0);
176
- const welcomeQuote = useMemo(() => getRandomQuote(), []);
177
-
178
- useInput((input, key) => {
179
- if (isUpdating) return;
180
-
181
- if (updateInfo.hasUpdate) {
182
- if (key.leftArrow) {
183
- setSelectedButton((prev) => Math.max(0, prev - 1));
184
- }
185
- if (key.rightArrow) {
186
- setSelectedButton((prev) => Math.min(2, prev + 1));
187
- }
188
- if (key.return) {
189
- if (selectedButton === 0) {
190
- onUpdate();
191
- } else if (selectedButton === 1) {
192
- onAlways();
193
- } else {
194
- onContinue();
195
- }
196
- }
197
- if (input === 'q') {
198
- onExit();
199
- }
200
- } else {
201
- if (key.return) {
202
- onContinue();
203
- }
204
- if (input === 'q') {
205
- onExit();
206
- }
207
- }
208
- });
209
-
210
- const hasUpdate = updateInfo.hasUpdate && updateInfo.latestVersion;
211
-
212
- return (
213
- <Box flexDirection="column" alignItems="center" justifyContent="center" height={18}>
214
- <Box
215
- flexDirection="column"
216
- alignItems="center"
217
- borderStyle="single"
218
- borderColor={hasUpdate ? '#eab308' : colors.border}
219
- paddingX={4}
220
- paddingY={1}
221
- >
222
- <Box flexDirection="column">
223
- <Text>
224
- <Text color={colors.textDim}>╔═════╗ </Text>
225
- <Text color={colors.text}>droid</Text>
226
- <Text color={colors.textDim}> v{updateInfo.currentVersion}</Text>
227
- </Text>
228
- <Text>
229
- <Text color={colors.textDim}>║ </Text>
230
- <Text color={hasUpdate ? '#eab308' : colors.primary}>●</Text>
231
- <Text color={colors.textDim}> </Text>
232
- <Text color={hasUpdate ? '#eab308' : colors.primary}>●</Text>
233
- <Text color={colors.textDim}> ║ </Text>
234
- <Text color={colors.textMuted}>Droid, teaching your AI new tricks</Text>
235
- </Text>
236
- <Text>
237
- <Text color={colors.textDim}>╚═╦═╦═╝ </Text>
238
- <Text color={colors.textDim}>github.com/Orderful/droid</Text>
239
- </Text>
240
- </Box>
241
-
242
- {hasUpdate ? (
243
- <>
244
- <Box marginTop={2} marginBottom={1} flexDirection="column" alignItems="center">
245
- <Text color="#eab308" italic>
246
- "The odds of functioning optimally without this
247
- </Text>
248
- <Text color="#eab308" italic>
249
- update are approximately 3,720 to 1."
250
- </Text>
251
- </Box>
252
-
253
- <Box marginBottom={1}>
254
- <Text color={colors.textMuted}>
255
- v{updateInfo.currentVersion} → v{updateInfo.latestVersion}
256
- </Text>
257
- </Box>
258
-
259
- {isUpdating ? (
260
- <Text color="#eab308">Updating...</Text>
261
- ) : (
262
- <Box flexDirection="row">
263
- <Text
264
- backgroundColor={selectedButton === 0 ? '#eab308' : colors.bgSelected}
265
- color={selectedButton === 0 ? '#000000' : colors.textMuted}
266
- bold={selectedButton === 0}
267
- >
268
- {' '}Update{' '}
269
- </Text>
270
- <Text> </Text>
271
- <Text
272
- backgroundColor={selectedButton === 1 ? '#eab308' : colors.bgSelected}
273
- color={selectedButton === 1 ? '#000000' : colors.textMuted}
274
- bold={selectedButton === 1}
275
- >
276
- {' '}Always{' '}
277
- </Text>
278
- <Text> </Text>
279
- <Text
280
- backgroundColor={selectedButton === 2 ? colors.bgSelected : undefined}
281
- color={selectedButton === 2 ? colors.text : colors.textMuted}
282
- bold={selectedButton === 2}
283
- >
284
- {' '}Skip{' '}
285
- </Text>
286
- </Box>
287
- )}
288
-
289
- <Box marginTop={1}>
290
- <Text color={colors.textDim}>←→ select · enter · "Always" enables auto-update</Text>
291
- </Box>
292
- </>
293
- ) : (
294
- <>
295
- <Box marginTop={2} marginBottom={1}>
296
- <Text backgroundColor={colors.primary} color="#ffffff" bold>
297
- {` ${welcomeQuote} `}
298
- </Text>
299
- </Box>
300
-
301
- <Text color={colors.textDim}>press enter</Text>
302
- </>
303
- )}
304
- </Box>
305
- </Box>
306
- );
307
- }
308
-
309
- interface ToolUpdatePromptProps {
310
- toolUpdates: ToolUpdateInfo[];
311
- onUpdateAll: () => void;
312
- onAlways: () => void;
313
- onSkip: () => void;
314
- isUpdating: boolean;
315
- }
316
-
317
- function ToolUpdatePrompt({ toolUpdates, onUpdateAll, onAlways, onSkip, isUpdating }: ToolUpdatePromptProps) {
318
- const [selectedButton, setSelectedButton] = useState(0);
319
-
320
- useInput((input, key) => {
321
- if (isUpdating) return;
322
-
323
- if (key.leftArrow) {
324
- setSelectedButton((prev) => Math.max(0, prev - 1));
325
- }
326
- if (key.rightArrow) {
327
- setSelectedButton((prev) => Math.min(2, prev + 1));
328
- }
329
- if (key.return) {
330
- if (selectedButton === 0) {
331
- onUpdateAll();
332
- } else if (selectedButton === 1) {
333
- onAlways();
334
- } else {
335
- onSkip();
336
- }
337
- }
338
- if (input === 'q') {
339
- onSkip();
340
- }
341
- });
342
-
343
- const buttons = [
344
- { label: 'Update All', action: onUpdateAll },
345
- { label: 'Always', action: onAlways },
346
- { label: 'Skip', action: onSkip },
347
- ];
348
-
349
- return (
350
- <Box flexDirection="column" alignItems="center" justifyContent="center" height={18}>
351
- <Box
352
- flexDirection="column"
353
- alignItems="center"
354
- borderStyle="single"
355
- borderColor={colors.primary}
356
- paddingX={4}
357
- paddingY={1}
358
- >
359
- <Box flexDirection="column">
360
- <Text>
361
- <Text color={colors.textDim}>╔═════╗ </Text>
362
- <Text color={colors.text}>Tool Updates</Text>
363
- </Text>
364
- <Text>
365
- <Text color={colors.textDim}>║ </Text>
366
- <Text color={colors.primary}>●</Text>
367
- <Text color={colors.textDim}> </Text>
368
- <Text color={colors.primary}>●</Text>
369
- <Text color={colors.textDim}> ║ </Text>
370
- <Text color={colors.textMuted}>{toolUpdates.length} tool{toolUpdates.length > 1 ? 's have' : ' has'} updates available</Text>
371
- </Text>
372
- <Text>
373
- <Text color={colors.textDim}>╚═╦═╦═╝</Text>
374
- </Text>
375
- </Box>
376
-
377
- <Box marginTop={1} marginBottom={1} flexDirection="column">
378
- {toolUpdates.slice(0, 5).map((tool) => (
379
- <Text key={tool.name} color={colors.textMuted}>
380
- <Text color={colors.primary}>↑</Text> {tool.name}
381
- <Text color={colors.textDim}> {tool.installedVersion} → {tool.bundledVersion}</Text>
382
- </Text>
383
- ))}
384
- {toolUpdates.length > 5 && (
385
- <Text color={colors.textDim}>...and {toolUpdates.length - 5} more</Text>
386
- )}
387
- </Box>
388
-
389
- {isUpdating ? (
390
- <Text color={colors.primary}>Updating tools...</Text>
391
- ) : (
392
- <Box flexDirection="row">
393
- {buttons.map((button, index) => (
394
- <Text
395
- key={button.label}
396
- backgroundColor={selectedButton === index ? colors.primary : colors.bgSelected}
397
- color={selectedButton === index ? '#000000' : colors.textMuted}
398
- bold={selectedButton === index}
399
- >
400
- {' '}{button.label}{' '}
401
- </Text>
402
- ))}
403
- </Box>
404
- )}
405
-
406
- <Box marginTop={1}>
407
- <Text color={colors.textDim}>←→ select · enter · "Always" enables auto-update</Text>
408
- </Box>
409
- </Box>
410
- </Box>
411
- );
412
- }
413
-
414
- interface SetupScreenProps {
415
- onComplete: () => void;
416
- onSkip: () => void;
417
- initialConfig?: DroidConfig;
418
- }
419
-
420
- function SetupScreen({ onComplete, onSkip, initialConfig }: SetupScreenProps) {
421
- const [step, setStep] = useState<SetupStep>('platform');
422
- const [platform, setPlatform] = useState<Platform>(
423
- initialConfig?.platform || getDefaultPlatform()
424
- );
425
- const [userMention, setUserMention] = useState(initialConfig?.user_mention || '@user');
426
- const [selectedIndex, setSelectedIndex] = useState(0);
427
- const [error, setError] = useState<string | null>(null);
428
-
429
- // Cache detection results once on mount (avoids re-running execSync on every render)
430
- const installedPlatforms = useMemo(() => detectInstalledPlatforms(), []);
431
- const platformOptions = useMemo(() => [
432
- { label: 'Claude Code', value: Platform.ClaudeCode },
433
- { label: 'OpenCode', value: Platform.OpenCode },
434
- ], []);
435
-
436
- const steps: SetupStep[] = ['platform', 'user_mention', 'confirm'];
437
- const stepIndex = steps.indexOf(step);
438
- const totalSteps = steps.length - 1; // Don't count confirm as a step
439
-
440
- const handleUserMentionSubmit = () => {
441
- // Auto-add @ prefix if missing
442
- const mention = userMention.startsWith('@') ? userMention : `@${userMention}`;
443
- setUserMention(mention);
444
- setError(null);
445
- setStep('confirm');
446
- };
447
-
448
- // Handle escape during text input (only intercept escape, nothing else)
449
- useInput((input, key) => {
450
- if (key.escape) {
451
- setStep('platform');
452
- setSelectedIndex(0);
453
- }
454
- }, { isActive: step === 'user_mention' });
455
-
456
- // Handle all input for non-text-input steps
457
- useInput((input, key) => {
458
- if (key.escape) {
459
- if (step === 'platform') {
460
- onSkip();
461
- } else if (step === 'confirm') {
462
- setStep('user_mention');
463
- }
464
- return;
465
- }
466
-
467
- if (step === 'platform') {
468
- if (key.upArrow) setSelectedIndex((prev) => Math.max(0, prev - 1));
469
- if (key.downArrow) setSelectedIndex((prev) => Math.min(platformOptions.length - 1, prev + 1));
470
- if (key.return) {
471
- setPlatform(platformOptions[selectedIndex].value);
472
- setStep('user_mention');
473
- }
474
- } else if (step === 'confirm') {
475
- if (key.return) {
476
- const existingConfig = loadConfig();
477
- const config: DroidConfig = {
478
- ...existingConfig,
479
- platform: platform,
480
- user_mention: userMention,
481
- output_preference: BuiltInOutput.Terminal, // Default to terminal
482
- };
483
- saveConfig(config);
484
- configurePlatformPermissions(platform);
485
- onComplete();
486
- }
487
- }
488
- }, { isActive: step !== 'user_mention' });
489
-
490
- const renderHeader = () => (
491
- <Box flexDirection="column" marginBottom={1}>
492
- <Text>
493
- <Text color={colors.textDim}>[</Text>
494
- <Text color={colors.primary}>●</Text>
495
- <Text color={colors.textDim}> </Text>
496
- <Text color={colors.primary}>●</Text>
497
- <Text color={colors.textDim}>] </Text>
498
- <Text color={colors.text} bold>droid setup</Text>
499
- <Text color={colors.textDim}> · Step {Math.min(stepIndex + 1, totalSteps)} of {totalSteps}</Text>
500
- </Text>
501
- </Box>
502
- );
503
-
504
- if (step === 'platform') {
505
- return (
506
- <Box flexDirection="column" padding={1}>
507
- {renderHeader()}
508
- <Text color={colors.text}>Which platform are you using?</Text>
509
- <Box flexDirection="column" marginTop={1}>
510
- {platformOptions.map((option, index) => (
511
- <Text key={option.value}>
512
- <Text color={colors.textDim}>{index === selectedIndex ? '>' : ' '} </Text>
513
- <Text color={index === selectedIndex ? colors.text : colors.textMuted}>{option.label}</Text>
514
- {installedPlatforms.has(option.value) && <Text color={colors.success}> (detected)</Text>}
515
- </Text>
516
- ))}
517
- </Box>
518
- <Box marginTop={1}>
519
- <Text color={colors.textDim}>↑↓ select · enter next · esc skip</Text>
520
- </Box>
521
- </Box>
522
- );
523
- }
524
-
525
- if (step === 'user_mention') {
526
- return (
527
- <Box flexDirection="column" padding={1}>
528
- {renderHeader()}
529
- <Text color={colors.text}>What @mention should be used for you?</Text>
530
- <Box marginTop={1}>
531
- <Text color={colors.textDim}>{'> '}</Text>
532
- <TextInput
533
- value={userMention}
534
- onChange={setUserMention}
535
- onSubmit={handleUserMentionSubmit}
536
- placeholder="@user"
537
- />
538
- </Box>
539
- {error && (
540
- <Box marginTop={1}>
541
- <Text color={colors.error}>{error}</Text>
542
- </Box>
543
- )}
544
- <Box marginTop={1}>
545
- <Text color={colors.textDim}>enter next · esc back</Text>
546
- </Box>
547
- </Box>
548
- );
549
- }
550
-
551
- // Confirm step
552
- return (
553
- <Box flexDirection="column" padding={1}>
554
- {renderHeader()}
555
- <Text color={colors.text} bold>Review your settings</Text>
556
- <Box flexDirection="column" marginTop={1}>
557
- <Text>
558
- <Text color={colors.textDim}>Platform: </Text>
559
- <Text color={colors.text}>{platform === Platform.ClaudeCode ? 'Claude Code' : 'OpenCode'}</Text>
560
- </Text>
561
- <Text>
562
- <Text color={colors.textDim}>Your @mention: </Text>
563
- <Text color={colors.text}>{userMention}</Text>
564
- </Text>
565
- </Box>
566
- <Box marginTop={2}>
567
- <Text backgroundColor={colors.primary} color="#ffffff" bold>
568
- {' '}Save{' '}
569
- </Text>
570
- </Box>
571
- <Box marginTop={1}>
572
- <Text color={colors.textDim}>enter save · esc back</Text>
573
- </Box>
574
- </Box>
575
- );
576
- }
577
-
578
- function TabBar({ tabs, activeTab }: { tabs: { id: Tab; label: string }[]; activeTab: Tab }) {
579
- return (
580
- <Box flexDirection="row" flexWrap="wrap">
581
- {tabs.map((tab) => (
582
- <Text
583
- key={tab.id}
584
- backgroundColor={tab.id === activeTab ? colors.primary : undefined}
585
- color={tab.id === activeTab ? '#ffffff' : colors.textMuted}
586
- bold={tab.id === activeTab}
587
- wrap="truncate"
588
- >
589
- {' '}{tab.label}{' '}
590
- </Text>
591
- ))}
592
- </Box>
593
- );
594
- }
595
-
596
- function ToolItem({
597
- tool,
598
- isSelected,
599
- isActive,
600
- wasAutoUpdated,
601
- }: {
602
- tool: ToolManifest;
603
- isSelected: boolean;
604
- isActive: boolean;
605
- wasAutoUpdated?: boolean;
606
- }) {
607
- const installed = isToolInstalled(tool.name);
608
- const installedVersion = getInstalledToolVersion(tool.name);
609
- const updateStatus = getToolUpdateStatus(tool.name);
610
-
611
- return (
612
- <Box paddingX={1} backgroundColor={isActive ? colors.bgSelected : undefined}>
613
- <Text wrap="truncate">
614
- <Text color={colors.textDim}>{isSelected ? '>' : ' '} </Text>
615
- <Text color={isSelected || isActive ? colors.text : colors.textMuted}>{tool.name}</Text>
616
- {installed && installedVersion && <Text color={colors.textDim}> v{installedVersion}</Text>}
617
- {installed && <Text color={colors.success}> ✓</Text>}
618
- {wasAutoUpdated && <Text color={colors.success}> ↑</Text>}
619
- {updateStatus.hasUpdate && !wasAutoUpdated && <Text color={colors.primary}> ↑</Text>}
620
- <Text> </Text>
621
- {tool.includes.skills.length > 0 && <Text color={colors.skill}>● </Text>}
622
- {tool.includes.commands.length > 0 && <Text color={colors.command}>● </Text>}
623
- {tool.includes.agents.length > 0 && <Text color={colors.agent}>●</Text>}
624
- </Text>
625
- </Box>
626
- );
627
- }
628
-
629
- function ToolDetails({
630
- tool,
631
- isFocused,
632
- selectedAction,
633
- }: {
634
- tool: ToolManifest | null;
635
- isFocused: boolean;
636
- selectedAction: number;
637
- }) {
638
- if (!tool) {
639
- return (
640
- <Box paddingLeft={2} flexGrow={1}>
641
- <Text color={colors.textDim}>Select a tool</Text>
642
- </Box>
643
- );
644
- }
645
-
646
- const installed = isToolInstalled(tool.name);
647
- const installedVersion = getInstalledToolVersion(tool.name);
648
- const updateStatus = getToolUpdateStatus(tool.name);
649
- const isSystemTool = (tool as ToolManifest & { system?: boolean }).system === true;
650
-
651
- const actions = installed
652
- ? [
653
- { id: 'explore', label: 'Explore', variant: 'default' },
654
- ...(updateStatus.hasUpdate
655
- ? [{ id: 'update', label: `Update (${updateStatus.bundledVersion})`, variant: 'primary' }]
656
- : []),
657
- { id: 'configure', label: 'Configure', variant: 'default' },
658
- // System tools can't be uninstalled
659
- ...(!isSystemTool ? [{ id: 'uninstall', label: 'Uninstall', variant: 'danger' }] : []),
660
- ]
661
- : [
662
- { id: 'explore', label: 'Explore', variant: 'default' },
663
- // System tools auto-install, but show Install for manual trigger if needed
664
- { id: 'install', label: 'Install', variant: 'primary' },
665
- ];
666
-
667
- return (
668
- <Box flexDirection="column" paddingLeft={2} flexGrow={1}>
669
- <Text color={colors.text} bold>{tool.name}</Text>
670
-
671
- <Box marginTop={1}>
672
- <Text color={colors.textDim}>
673
- {tool.version}
674
- {tool.status && ` · ${tool.status}`}
675
- {installed && <Text color={colors.success}> · installed</Text>}
676
- {updateStatus.hasUpdate && (
677
- <Text color={colors.primary}> · update ({installedVersion} → {updateStatus.bundledVersion})</Text>
678
- )}
679
- </Text>
680
- </Box>
681
-
682
- <Box marginTop={1}>
683
- <Text color={colors.textMuted}>{tool.description}</Text>
684
- </Box>
685
-
686
- {/* Colored component badges */}
687
- <Box flexDirection="column" marginTop={1}>
688
- <Text color={colors.textDim}>Includes:</Text>
689
- <Box marginTop={1}>
690
- <ComponentBadges tool={tool} />
691
- </Box>
692
- {/* Show names below badges */}
693
- <Box flexDirection="column" marginTop={1} marginLeft={1}>
694
- {tool.includes.skills.length > 0 && (
695
- <Text>
696
- <Text color={colors.skill}>Skills: </Text>
697
- <Text color={colors.textMuted}>{tool.includes.skills.map(s => s.name).join(', ')}</Text>
698
- </Text>
699
- )}
700
- {tool.includes.commands.length > 0 && (
701
- <Text>
702
- <Text color={colors.command}>Commands: </Text>
703
- <Text color={colors.textMuted}>{tool.includes.commands.map(c => `/${c}`).join(', ')}</Text>
704
- </Text>
705
- )}
706
- {tool.includes.agents.length > 0 && (
707
- <Text>
708
- <Text color={colors.agent}>Agents: </Text>
709
- <Text color={colors.textMuted}>{tool.includes.agents.join(', ')}</Text>
710
- </Text>
711
- )}
712
- </Box>
713
- </Box>
714
-
715
- {isFocused && (
716
- <Box flexDirection="row" marginTop={1}>
717
- {actions.map((action, index) => (
718
- <Text
719
- key={action.id}
720
- backgroundColor={
721
- selectedAction === index
722
- ? action.variant === 'danger'
723
- ? colors.error
724
- : colors.primary
725
- : colors.bgSelected
726
- }
727
- color={selectedAction === index ? '#ffffff' : colors.textMuted}
728
- bold={selectedAction === index}
729
- >
730
- {' '}{action.label}{' '}
731
- </Text>
732
- ))}
733
- </Box>
734
- )}
735
- </Box>
736
- );
737
- }
738
-
739
- function MarkdownLine({ line, inCodeBlock }: { line: string; inCodeBlock: boolean }) {
740
- // Code block content
741
- if (inCodeBlock) {
742
- return <Text color="#a5d6ff">{line || ' '}</Text>;
743
- }
744
-
745
- // Code block delimiter
746
- if (line.startsWith('```')) {
747
- return <Text color={colors.textDim}>{line}</Text>;
748
- }
749
-
750
- // Headers
751
- if (line.startsWith('# ')) {
752
- return <Text color={colors.text} bold>{line.slice(2)}</Text>;
753
- }
754
- if (line.startsWith('## ')) {
755
- return <Text color={colors.text} bold>{line.slice(3)}</Text>;
756
- }
757
- if (line.startsWith('### ')) {
758
- return <Text color="#c9d1d9" bold>{line.slice(4)}</Text>;
759
- }
760
-
761
- // YAML frontmatter delimiter
762
- if (line === '---') {
763
- return <Text color={colors.textDim}>{line}</Text>;
764
- }
765
-
766
- // List items
767
- if (line.match(/^[\s]*[-*]\s/)) {
768
- return <Text color={colors.textMuted}>{line}</Text>;
769
- }
770
-
771
- // Blockquotes
772
- if (line.startsWith('>')) {
773
- return <Text color="#8b949e" italic>{line}</Text>;
774
- }
775
-
776
- // Table rows
777
- if (line.includes('|')) {
778
- return <Text color={colors.textMuted}>{line}</Text>;
779
- }
780
-
781
- // Default
782
- return <Text color={colors.textMuted}>{line || ' '}</Text>;
783
- }
784
-
785
- function ReadmeViewer({
786
- title,
787
- content,
788
- onClose,
789
- }: {
790
- title: string;
791
- content: string;
792
- onClose: () => void;
793
- }) {
794
- const [scrollOffset, setScrollOffset] = useState(0);
795
- const lines = useMemo(() => content.split('\n'), [content]);
796
- const maxVisible = 20;
797
-
798
- // Pre-compute code block state for each line
799
- const lineStates = useMemo(() => {
800
- const states: boolean[] = [];
801
- let inCode = false;
802
- for (const line of lines) {
803
- if (line.startsWith('```')) {
804
- states.push(false); // Delimiter itself is not "in" code block for styling
805
- inCode = !inCode;
806
- } else {
807
- states.push(inCode);
808
- }
809
- }
810
- return states;
811
- }, [lines]);
812
-
813
- // Max offset: when at end, we have top indicator + (maxVisible-1) content lines
814
- // So max offset is lines.length - (maxVisible - 1) = lines.length - maxVisible + 1
815
- const maxOffset = Math.max(0, lines.length - maxVisible + 1);
816
-
817
- useInput((input, key) => {
818
- if (key.escape) {
819
- onClose();
820
- return;
821
- }
822
- if (key.upArrow) {
823
- setScrollOffset((prev) => Math.max(0, prev - 1));
824
- }
825
- if (key.downArrow) {
826
- setScrollOffset((prev) => Math.min(maxOffset, prev + 1));
827
- }
828
- if (key.pageDown || input === ' ') {
829
- setScrollOffset((prev) => Math.min(maxOffset, prev + maxVisible));
830
- }
831
- if (key.pageUp) {
832
- setScrollOffset((prev) => Math.max(0, prev - maxVisible));
833
- }
834
- });
835
-
836
- // Adjust visible lines based on whether indicators are shown
837
- const showTopIndicator = scrollOffset > 0;
838
- // Reserve space for bottom indicator if not at end
839
- const contentLines = maxVisible - (showTopIndicator ? 1 : 0);
840
- const endIndex = Math.min(scrollOffset + contentLines, lines.length);
841
- const showBottomIndicator = endIndex < lines.length;
842
- const actualContentLines = contentLines - (showBottomIndicator ? 1 : 0);
843
- const visibleLines = lines.slice(scrollOffset, scrollOffset + actualContentLines);
844
-
845
- return (
846
- <Box flexDirection="column" padding={1}>
847
- <Box marginBottom={1}>
848
- <Text color={colors.text} bold>{title}</Text>
849
- <Text color={colors.textDim}> · {lines.length} lines</Text>
850
- </Box>
851
-
852
- <Box
853
- flexDirection="column"
854
- borderStyle="single"
855
- borderColor={colors.border}
856
- paddingX={1}
857
- >
858
- {showTopIndicator && (
859
- <Text color={colors.textDim}>↑ {scrollOffset} more lines</Text>
860
- )}
861
- {visibleLines.map((line, i) => (
862
- <MarkdownLine key={scrollOffset + i} line={line} inCodeBlock={lineStates[scrollOffset + i]} />
863
- ))}
864
- {showBottomIndicator && (
865
- <Text color={colors.textDim}>↓ {lines.length - scrollOffset - actualContentLines} more lines</Text>
866
- )}
867
- </Box>
868
-
869
- <Box marginTop={1}>
870
- <Text color={colors.textDim}>↑↓ scroll · space/pgdn page · esc back</Text>
871
- </Box>
872
- </Box>
873
- );
874
- }
875
-
876
- interface ExplorerItem {
877
- type: ComponentType;
878
- name: string;
879
- path: string;
880
- }
881
-
882
- interface ToolExplorerProps {
883
- tool: ToolManifest;
884
- onViewSource: (title: string, content: string) => void;
885
- onClose: () => void;
886
- }
887
-
888
- function ToolExplorer({ tool, onViewSource, onClose }: ToolExplorerProps) {
889
- const [selectedIndex, setSelectedIndex] = useState(0);
890
-
891
- // Build list of all explorable items
892
- const items: ExplorerItem[] = useMemo(() => {
893
- const result: ExplorerItem[] = [];
894
- const toolDir = getBundledToolsDir();
895
-
896
- // Add skills
897
- for (const skill of tool.includes.skills) {
898
- result.push({
899
- type: 'skill',
900
- name: skill.name,
901
- path: join(toolDir, tool.name, 'skills', skill.name, 'SKILL.md'),
902
- });
903
- }
904
-
905
- // Add commands
906
- for (const cmd of tool.includes.commands) {
907
- result.push({
908
- type: 'command',
909
- name: `/${cmd}`,
910
- path: join(toolDir, tool.name, 'commands', `${cmd}.md`),
911
- });
912
- }
913
-
914
- // Add agents
915
- for (const agent of tool.includes.agents) {
916
- result.push({
917
- type: 'agent',
918
- name: agent,
919
- path: join(toolDir, tool.name, 'agents', agent, 'AGENT.md'),
920
- });
921
- }
922
-
923
- return result;
924
- }, [tool]);
925
-
926
- useInput((input, key) => {
927
- if (key.escape) {
928
- onClose();
929
- return;
930
- }
931
-
932
- if (key.leftArrow || key.upArrow) {
933
- setSelectedIndex((prev) => Math.max(0, prev - 1));
934
- }
935
- if (key.rightArrow || key.downArrow) {
936
- setSelectedIndex((prev) => Math.min(items.length - 1, prev + 1));
937
- }
938
-
939
- if (key.return && items.length > 0) {
940
- const item = items[selectedIndex];
941
- if (existsSync(item.path)) {
942
- const content = readFileSync(item.path, 'utf-8');
943
- onViewSource(`${tool.name} / ${item.name}`, content);
944
- } else {
945
- // Try YAML fallback for skills/agents
946
- const yamlPath = item.path.replace('.md', '.yaml');
947
- if (existsSync(yamlPath)) {
948
- const content = readFileSync(yamlPath, 'utf-8');
949
- onViewSource(`${tool.name} / ${item.name}`, content);
950
- }
951
- }
952
- }
953
- });
954
-
955
- // Group items by type for display
956
- const skillItems = items.filter(i => i.type === 'skill');
957
- const commandItems = items.filter(i => i.type === 'command');
958
- const agentItems = items.filter(i => i.type === 'agent');
959
-
960
- const getItemIndex = (item: ExplorerItem) => items.indexOf(item);
961
-
962
- return (
963
- <Box flexDirection="column" padding={1}>
964
- <Box marginBottom={1}>
965
- <Text>
966
- <Text color={colors.textDim}>[</Text>
967
- <Text color={colors.primary}>●</Text>
968
- <Text color={colors.textDim}> </Text>
969
- <Text color={colors.primary}>●</Text>
970
- <Text color={colors.textDim}>] </Text>
971
- <Text color={colors.text} bold>{tool.name}</Text>
972
- </Text>
973
- </Box>
974
-
975
- <Box marginBottom={1}>
976
- <Text color={colors.textMuted}>{tool.description}</Text>
977
- </Box>
978
-
979
- {/* Skills section */}
980
- {skillItems.length > 0 && (
981
- <Box flexDirection="column" marginBottom={1}>
982
- <Text color={colors.skill} bold>Skills</Text>
983
- <Box flexDirection="row" flexWrap="wrap" marginTop={1}>
984
- {skillItems.map((item) => {
985
- const idx = getItemIndex(item);
986
- const isSelected = selectedIndex === idx;
987
- return (
988
- <Box key={item.name} marginRight={1} marginBottom={1}>
989
- <Text
990
- backgroundColor={isSelected ? colors.skill : colors.bgSelected}
991
- color={isSelected ? '#000000' : colors.skill}
992
- bold={isSelected}
993
- >
994
- {` ${item.name} `}
995
- </Text>
996
- </Box>
997
- );
998
- })}
999
- </Box>
1000
- </Box>
1001
- )}
1002
-
1003
- {/* Commands section */}
1004
- {commandItems.length > 0 && (
1005
- <Box flexDirection="column" marginBottom={1}>
1006
- <Text color={colors.command} bold>Commands</Text>
1007
- <Box flexDirection="row" flexWrap="wrap" marginTop={1}>
1008
- {commandItems.map((item) => {
1009
- const idx = getItemIndex(item);
1010
- const isSelected = selectedIndex === idx;
1011
- return (
1012
- <Box key={item.name} marginRight={1} marginBottom={1}>
1013
- <Text
1014
- backgroundColor={isSelected ? colors.command : colors.bgSelected}
1015
- color={isSelected ? '#000000' : colors.command}
1016
- bold={isSelected}
1017
- >
1018
- {` ${item.name} `}
1019
- </Text>
1020
- </Box>
1021
- );
1022
- })}
1023
- </Box>
1024
- </Box>
1025
- )}
1026
-
1027
- {/* Agents section */}
1028
- {agentItems.length > 0 && (
1029
- <Box flexDirection="column" marginBottom={1}>
1030
- <Text color={colors.agent} bold>Agents</Text>
1031
- <Box flexDirection="row" flexWrap="wrap" marginTop={1}>
1032
- {agentItems.map((item) => {
1033
- const idx = getItemIndex(item);
1034
- const isSelected = selectedIndex === idx;
1035
- return (
1036
- <Box key={item.name} marginRight={1} marginBottom={1}>
1037
- <Text
1038
- backgroundColor={isSelected ? colors.agent : colors.bgSelected}
1039
- color={isSelected ? '#000000' : colors.agent}
1040
- bold={isSelected}
1041
- >
1042
- {` ${item.name} `}
1043
- </Text>
1044
- </Box>
1045
- );
1046
- })}
1047
- </Box>
1048
- </Box>
1049
- )}
1050
-
1051
- <Box marginTop={1}>
1052
- <Text color={colors.textDim}>←→ navigate · enter view source · esc back</Text>
1053
- </Box>
1054
- </Box>
1055
- );
1056
- }
1057
-
1058
- function SettingsDetails({
1059
- onEditSettings,
1060
- isFocused,
1061
- onToggleAutoUpdate,
1062
- selectedSetting,
1063
- }: {
1064
- onEditSettings: () => void;
1065
- isFocused: boolean;
1066
- onToggleAutoUpdate: () => void;
1067
- selectedSetting: number;
1068
- }) {
1069
- const config = loadConfig();
1070
- const autoUpdateConfig = getAutoUpdateConfig();
1071
-
1072
- return (
1073
- <Box flexDirection="column" paddingLeft={2} flexGrow={1}>
1074
- <Text color={colors.text} bold>Settings</Text>
1075
-
1076
- <Box flexDirection="column" marginTop={1}>
1077
- <Text>
1078
- <Text color={colors.textDim}>Platform: </Text>
1079
- <Text color={colors.text}>
1080
- {config.platform === Platform.ClaudeCode ? 'Claude Code' : 'OpenCode'}
1081
- </Text>
1082
- </Text>
1083
- <Text>
1084
- <Text color={colors.textDim}>Your @mention: </Text>
1085
- <Text color={colors.text}>{config.user_mention}</Text>
1086
- </Text>
1087
- </Box>
1088
-
1089
- <Box flexDirection="column" marginTop={2}>
1090
- <Text color={colors.text} bold>Auto-Update</Text>
1091
- <Box marginTop={1}>
1092
- <Text>
1093
- <Text color={colors.textDim}>{isFocused && selectedSetting === 0 ? '> ' : ' '}</Text>
1094
- <Text color={isFocused && selectedSetting === 0 ? colors.text : colors.textMuted}>
1095
- [{autoUpdateConfig.tools ? 'x' : ' '}] Auto-update tools
1096
- </Text>
1097
- </Text>
1098
- </Box>
1099
- <Text color={colors.textDim}> Update tools automatically when droid starts</Text>
1100
- <Box marginTop={1}>
1101
- <Text>
1102
- <Text color={colors.textDim}>{isFocused && selectedSetting === 1 ? '> ' : ' '}</Text>
1103
- <Text color={isFocused && selectedSetting === 1 ? colors.text : colors.textMuted}>
1104
- [{autoUpdateConfig.app ? 'x' : ' '}] Auto-update app
1105
- </Text>
1106
- </Text>
1107
- </Box>
1108
- <Text color={colors.textDim}> Update droid automatically when a new version is available</Text>
1109
- </Box>
1110
-
1111
- <Box marginTop={2}>
1112
- <Text color={colors.textDim}>Config: ~/.droid/config.yaml</Text>
1113
- </Box>
1114
-
1115
- {isFocused && (
1116
- <Box marginTop={2}>
1117
- <Text
1118
- backgroundColor={selectedSetting === 2 ? colors.primary : colors.bgSelected}
1119
- color={selectedSetting === 2 ? '#ffffff' : colors.textMuted}
1120
- bold={selectedSetting === 2}
1121
- >
1122
- {' '}Edit Profile{' '}
1123
- </Text>
1124
- </Box>
1125
- )}
1126
-
1127
- {isFocused && (
1128
- <Box marginTop={1}>
1129
- <Text color={colors.textDim}>↑↓ select · enter toggle/edit · esc back</Text>
1130
- </Box>
1131
- )}
1132
-
1133
- {!isFocused && (
1134
- <Box marginTop={2}>
1135
- <Text color={colors.textDim}>press enter to configure</Text>
1136
- </Box>
1137
- )}
1138
- </Box>
1139
- );
1140
- }
1141
-
1142
- interface SkillConfigScreenProps {
1143
- skill: SkillManifest;
1144
- onComplete: () => void;
1145
- onCancel: () => void;
1146
- }
1147
-
1148
- const MAX_VISIBLE_CONFIG_ITEMS = 4; // Each config item takes ~3 lines
1149
-
1150
- function SkillConfigScreen({ skill, onComplete, onCancel }: SkillConfigScreenProps) {
1151
- const configSchema = skill.config_schema || {};
1152
- const configKeys = Object.keys(configSchema);
1153
-
1154
- const initialOverrides = useMemo(() => loadSkillOverrides(skill.name), [skill.name]);
1155
-
1156
- // Initialize values from saved overrides or defaults
1157
- const [values, setValues] = useState<SkillOverrides>(() => {
1158
- const initial: SkillOverrides = {};
1159
- for (const key of configKeys) {
1160
- const option = configSchema[key];
1161
- initial[key] = initialOverrides[key] ?? option.default ?? (option.type === ConfigOptionType.Boolean ? false : '');
1162
- }
1163
- return initial;
1164
- });
1165
-
1166
- const [selectedIndex, setSelectedIndex] = useState(0);
1167
- const [scrollOffset, setScrollOffset] = useState(0);
1168
- const [editingField, setEditingField] = useState<string | null>(null);
1169
- const [editValue, setEditValue] = useState('');
1170
- const [editingSelect, setEditingSelect] = useState<string | null>(null);
1171
- const [selectOptionIndex, setSelectOptionIndex] = useState(0);
1172
-
1173
- // Total items = config keys + Save button
1174
- const totalItems = configKeys.length + 1;
1175
-
1176
- const handleSave = () => {
1177
- saveSkillOverrides(skill.name, values);
1178
- onComplete();
1179
- };
1180
-
1181
- const handleSubmitEdit = () => {
1182
- if (editingField) {
1183
- setValues((prev) => ({ ...prev, [editingField]: editValue }));
1184
- setEditingField(null);
1185
- setEditValue('');
1186
- }
1187
- };
1188
-
1189
- // Handle text input for string fields
1190
- useInput((input, key) => {
1191
- if (key.escape) {
1192
- setEditingField(null);
1193
- setEditValue('');
1194
- }
1195
- }, { isActive: editingField !== null });
1196
-
1197
- // Handle select field editing
1198
- useInput((input, key) => {
1199
- if (!editingSelect) return;
1200
- const option = configSchema[editingSelect];
1201
- if (!option?.options) return;
1202
-
1203
- if (key.escape) {
1204
- setEditingSelect(null);
1205
- return;
1206
- }
1207
- if (key.leftArrow || key.upArrow) {
1208
- setSelectOptionIndex((prev) => Math.max(0, prev - 1));
1209
- }
1210
- if (key.rightArrow || key.downArrow) {
1211
- setSelectOptionIndex((prev) => Math.min(option.options!.length - 1, prev + 1));
1212
- }
1213
- if (key.return) {
1214
- setValues((prev) => ({ ...prev, [editingSelect]: option.options![selectOptionIndex] }));
1215
- setEditingSelect(null);
1216
- }
1217
- }, { isActive: editingSelect !== null });
1218
-
1219
- // Handle navigation and actions when not editing
1220
- useInput((input, key) => {
1221
- if (key.escape) {
1222
- onCancel();
1223
- return;
1224
- }
1225
-
1226
- if (key.upArrow) {
1227
- setSelectedIndex((prev) => {
1228
- const newIndex = Math.max(0, prev - 1);
1229
- // Scroll up if needed
1230
- if (newIndex < scrollOffset) {
1231
- setScrollOffset(newIndex);
1232
- }
1233
- return newIndex;
1234
- });
1235
- }
1236
- if (key.downArrow) {
1237
- // +1 for the Save button at the end
1238
- setSelectedIndex((prev) => {
1239
- const newIndex = Math.min(totalItems - 1, prev + 1);
1240
- // Scroll down if needed
1241
- if (newIndex >= scrollOffset + MAX_VISIBLE_CONFIG_ITEMS) {
1242
- setScrollOffset(newIndex - MAX_VISIBLE_CONFIG_ITEMS + 1);
1243
- }
1244
- return newIndex;
1245
- });
1246
- }
1247
-
1248
- if (key.return) {
1249
- // Save button is at index === configKeys.length
1250
- if (selectedIndex === configKeys.length) {
1251
- handleSave();
1252
- return;
1253
- }
1254
-
1255
- const key = configKeys[selectedIndex];
1256
- const option = configSchema[key];
1257
-
1258
- if (option.type === ConfigOptionType.Boolean) {
1259
- // Toggle boolean
1260
- setValues((prev) => ({ ...prev, [key]: !prev[key] }));
1261
- } else if (option.type === ConfigOptionType.Select && option.options) {
1262
- // Enter select edit mode
1263
- const currentValue = String(values[key] || option.options[0]);
1264
- const currentIndex = option.options.indexOf(currentValue);
1265
- setSelectOptionIndex(currentIndex >= 0 ? currentIndex : 0);
1266
- setEditingSelect(key);
1267
- } else if (option.type === ConfigOptionType.String) {
1268
- // Enter edit mode for string
1269
- setEditingField(key);
1270
- setEditValue(String(values[key] || ''));
1271
- }
1272
- }
1273
- }, { isActive: editingField === null && editingSelect === null });
1274
-
1275
- if (configKeys.length === 0) {
1276
- return (
1277
- <Box flexDirection="column" padding={1}>
1278
- <Box marginBottom={1}>
1279
- <Text>
1280
- <Text color={colors.textDim}>[</Text>
1281
- <Text color={colors.primary}>●</Text>
1282
- <Text color={colors.textDim}> </Text>
1283
- <Text color={colors.primary}>●</Text>
1284
- <Text color={colors.textDim}>] </Text>
1285
- <Text color={colors.text} bold>configure {skill.name}</Text>
1286
- </Text>
1287
- </Box>
1288
- <Text color={colors.textMuted}>This skill has no configuration options.</Text>
1289
- <Box marginTop={1}>
1290
- <Text color={colors.textDim}>esc to go back</Text>
1291
- </Box>
1292
- </Box>
1293
- );
1294
- }
1295
-
1296
- // Calculate visible range
1297
- const visibleEndIndex = Math.min(scrollOffset + MAX_VISIBLE_CONFIG_ITEMS, totalItems);
1298
- const visibleConfigKeys = configKeys.slice(scrollOffset, Math.min(scrollOffset + MAX_VISIBLE_CONFIG_ITEMS, configKeys.length));
1299
- const showSaveButton = visibleEndIndex > configKeys.length || scrollOffset + MAX_VISIBLE_CONFIG_ITEMS > configKeys.length;
1300
- const showTopIndicator = scrollOffset > 0;
1301
- const showBottomIndicator = scrollOffset + MAX_VISIBLE_CONFIG_ITEMS < totalItems;
1302
-
1303
- return (
1304
- <Box flexDirection="column" padding={1}>
1305
- <Box marginBottom={1}>
1306
- <Text>
1307
- <Text color={colors.textDim}>[</Text>
1308
- <Text color={colors.primary}>●</Text>
1309
- <Text color={colors.textDim}> </Text>
1310
- <Text color={colors.primary}>●</Text>
1311
- <Text color={colors.textDim}>] </Text>
1312
- <Text color={colors.text} bold>configure {skill.name}</Text>
1313
- </Text>
1314
- </Box>
1315
-
1316
- <Box flexDirection="column">
1317
- {/* Scroll up indicator */}
1318
- {showTopIndicator && (
1319
- <Box marginBottom={1}>
1320
- <Text color={colors.textDim}> ↑ {scrollOffset} more</Text>
1321
- </Box>
1322
- )}
1323
-
1324
- {visibleConfigKeys.map((key) => {
1325
- const actualIndex = configKeys.indexOf(key);
1326
- const option = configSchema[key];
1327
- const isSelected = selectedIndex === actualIndex;
1328
- const isEditing = editingField === key;
1329
-
1330
- return (
1331
- <Box key={key} flexDirection="column" marginBottom={1}>
1332
- <Text>
1333
- <Text color={colors.textDim}>{isSelected ? '>' : ' '} </Text>
1334
- <Text color={isSelected ? colors.text : colors.textMuted}>{key}</Text>
1335
- </Text>
1336
- <Text color={colors.textDim}> {option.description}</Text>
1337
- <Box>
1338
- <Text color={colors.textDim}> </Text>
1339
- {option.type === ConfigOptionType.Boolean ? (
1340
- <Text color={colors.text}>
1341
- [{values[key] ? 'x' : ' '}] {values[key] ? 'enabled' : 'disabled'}
1342
- </Text>
1343
- ) : option.type === ConfigOptionType.Select && option.options ? (
1344
- <Text color={colors.text}>
1345
- {option.options.map((opt, i) => {
1346
- const isCurrentValue = String(values[key]) === opt;
1347
- const isEditingThis = editingSelect === key;
1348
- const isHighlighted = isEditingThis && selectOptionIndex === i;
1349
- return (
1350
- <Text key={opt}>
1351
- {i > 0 && <Text color={colors.textDim}> · </Text>}
1352
- <Text
1353
- color={isHighlighted ? '#ffffff' : isCurrentValue ? colors.primary : colors.textMuted}
1354
- backgroundColor={isHighlighted ? colors.primary : undefined}
1355
- >
1356
- {isCurrentValue && !isEditingThis ? `[${opt}]` : isHighlighted ? ` ${opt} ` : opt}
1357
- </Text>
1358
- </Text>
1359
- );
1360
- })}
1361
- </Text>
1362
- ) : isEditing ? (
1363
- <Box>
1364
- <Text color={colors.textDim}>{'> '}</Text>
1365
- <TextInput
1366
- value={editValue}
1367
- onChange={setEditValue}
1368
- onSubmit={handleSubmitEdit}
1369
- />
1370
- </Box>
1371
- ) : (
1372
- <Text color={colors.text}>{String(values[key]) || '(not set)'}</Text>
1373
- )}
1374
- </Box>
1375
- </Box>
1376
- );
1377
- })}
1378
-
1379
- {/* Save button - show if in visible range */}
1380
- {showSaveButton && (
1381
- <Box marginTop={1}>
1382
- <Text>
1383
- <Text color={colors.textDim}>{selectedIndex === configKeys.length ? '>' : ' '} </Text>
1384
- <Text
1385
- backgroundColor={selectedIndex === configKeys.length ? colors.primary : undefined}
1386
- color={selectedIndex === configKeys.length ? '#ffffff' : colors.textMuted}
1387
- bold={selectedIndex === configKeys.length}
1388
- >
1389
- {' '}Save{' '}
1390
- </Text>
1391
- </Text>
1392
- </Box>
1393
- )}
1394
-
1395
- {/* Scroll down indicator */}
1396
- {showBottomIndicator && (
1397
- <Box marginTop={1}>
1398
- <Text color={colors.textDim}> ↓ {totalItems - scrollOffset - MAX_VISIBLE_CONFIG_ITEMS} more</Text>
1399
- </Box>
1400
- )}
1401
- </Box>
1402
-
1403
- <Box marginTop={1}>
1404
- <Text color={colors.textDim}>
1405
- {editingField
1406
- ? 'enter save · esc cancel'
1407
- : editingSelect
1408
- ? '←→ choose · enter select · esc cancel'
1409
- : '↑↓ select · enter toggle/edit · esc back'}
1410
- </Text>
1411
- </Box>
1412
- </Box>
1413
- );
1414
- }
28
+ // Module-level variable to store exit message (printed after leaving alternate screen)
29
+ let exitMessage: string | null = null;
1415
30
 
1416
31
  function App() {
1417
32
  const { exit } = useApp();
@@ -1429,97 +44,23 @@ function App() {
1429
44
  const [message, setMessage] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
1430
45
  const [isEditingSettings, setIsEditingSettings] = useState(false);
1431
46
  const [readmeContent, setReadmeContent] = useState<{ title: string; content: string } | null>(null);
1432
- const [selectedSetting, setSelectedSetting] = useState(0);
1433
- const [isUpdating, setIsUpdating] = useState(false);
1434
- const [isUpdatingTools, setIsUpdatingTools] = useState(false);
1435
47
  const [previousView, setPreviousView] = useState<View>('detail'); // Track where to return from readme
1436
- const [toolUpdates, setToolUpdates] = useState<ToolUpdateInfo[]>([]);
1437
- const [autoUpdatedTools, setAutoUpdatedTools] = useState<string[]>([]);
1438
-
1439
- // Check for updates once on mount
1440
- const updateInfo = useMemo(() => getUpdateInfo(), []);
1441
- const autoUpdateConfig = useMemo(() => getAutoUpdateConfig(), []);
1442
48
 
1443
- const handleUpdate = () => {
1444
- setIsUpdating(true);
1445
- // Run update in next tick to allow UI to show "Updating..."
1446
- setTimeout(() => {
1447
- const result = runUpdate();
1448
- if (result.success) {
1449
- // Store message to print after leaving alternate screen (with ANSI colors)
1450
- const blue = '\x1b[38;2;99;102;241m'; // #6366f1
1451
- const dim = '\x1b[38;2;106;106;106m';
1452
- const reset = '\x1b[0m';
1453
- exitMessage = `
1454
- ${dim}────────────────────────────────────────────────────────${reset}
1455
-
1456
- ${dim}╔═════╗${reset}
1457
- ${dim}║${reset} ${blue}●${reset} ${blue}●${reset} ${dim}║${reset} ${blue}"It's quite possible this system${reset}
1458
- ${dim}╚═╦═╦═╝${reset} ${blue}is now fully operational."${reset}
1459
-
1460
- Run ${blue}droid${reset} to start the new version.
1461
-
1462
- ${dim}────────────────────────────────────────────────────────${reset}
1463
- `;
1464
- exit();
49
+ const { updateInfo, isUpdating, handleUpdate, handleAlwaysUpdate } = useAppUpdate({
50
+ onUpdateSuccess: (msg) => {
51
+ exitMessage = msg;
52
+ },
53
+ onUpdateFailure: (error) => {
54
+ setMessage({ text: error, type: 'error' });
55
+ if (!configExists()) {
56
+ setView('setup');
1465
57
  } else {
1466
- setIsUpdating(false);
1467
- setMessage({ text: result.message, type: 'error' });
1468
- // Continue to menu on failure
1469
- if (!configExists()) {
1470
- setView('setup');
1471
- } else {
1472
- setView('menu');
1473
- }
1474
- }
1475
- }, 100);
1476
- };
1477
-
1478
- const handleAlwaysUpdate = () => {
1479
- // Enable auto-update for app in config
1480
- setAutoUpdateConfig({ app: true });
1481
- // Then run the update
1482
- handleUpdate();
1483
- };
1484
-
1485
- // Ensure system tools (marked with system: true in TOOL.yaml) are always installed and current
1486
- const ensureSystemTools = () => {
1487
- // Find all tools marked as system tools
1488
- const systemTools = tools.filter(t => (t as ToolManifest & { system?: boolean }).system === true);
1489
-
1490
- for (const systemTool of systemTools) {
1491
- const installed = isToolInstalled(systemTool.name);
1492
- const updateStatus = getToolUpdateStatus(systemTool.name);
1493
-
1494
- // Install if not installed, or update if outdated (regardless of auto-update settings)
1495
- if (!installed || updateStatus.hasUpdate) {
1496
- const primarySkill = systemTool.includes.skills.find(s => s.required)?.name || systemTool.name;
1497
- installSkill(primarySkill);
58
+ setView('menu');
1498
59
  }
1499
- }
1500
- };
1501
-
1502
- // Check for tool updates and proceed to next view
1503
- const checkToolUpdatesAndProceed = () => {
1504
- // Always ensure system tools are current (bypasses auto-update settings)
1505
- ensureSystemTools();
1506
-
1507
- const updates = getToolsWithUpdates();
1508
- setToolUpdates(updates);
1509
-
1510
- // If auto_update.tools is true, auto-update silently
1511
- if (autoUpdateConfig.tools && updates.length > 0) {
1512
- handleUpdateAllTools(updates, true);
1513
- return;
1514
- }
1515
-
1516
- // If there are updates and auto-update is off, show prompt
1517
- if (updates.length > 0) {
1518
- setView('tool-updates');
1519
- return;
1520
- }
60
+ },
61
+ });
1521
62
 
1522
- // No updates, proceed to setup or menu
63
+ const proceedToNextView = () => {
1523
64
  if (!configExists()) {
1524
65
  setView('setup');
1525
66
  } else {
@@ -1527,38 +68,15 @@ ${dim}────────────────────────
1527
68
  }
1528
69
  };
1529
70
 
1530
- const handleUpdateAllTools = (updates: ToolUpdateInfo[] = toolUpdates, silent = false) => {
1531
- if (!silent) {
1532
- setIsUpdatingTools(true);
1533
- }
1534
-
1535
- setTimeout(() => {
1536
- let successCount = 0;
1537
- let failCount = 0;
1538
- const updatedNames: string[] = [];
1539
-
1540
- for (const tool of updates) {
1541
- // Find the tool to get its primary skill
1542
- const toolManifest = tools.find(t => t.name === tool.name);
1543
- if (toolManifest) {
1544
- const primarySkill = toolManifest.includes.skills.find(s => s.required)?.name || toolManifest.name;
1545
- const result = updateSkill(primarySkill);
1546
- if (result.success) {
1547
- successCount++;
1548
- updatedNames.push(tool.name);
1549
- } else {
1550
- failCount++;
1551
- }
1552
- }
1553
- }
1554
-
1555
- setIsUpdatingTools(false);
1556
-
1557
- // Track which tools were auto-updated for visual indicator
1558
- if (silent && updatedNames.length > 0) {
1559
- setAutoUpdatedTools(updatedNames);
1560
- }
1561
-
71
+ const {
72
+ toolUpdates,
73
+ isUpdatingTools,
74
+ autoUpdatedTools,
75
+ checkForUpdates,
76
+ updateAllTools,
77
+ enableAutoUpdateAndUpdate,
78
+ } = useToolUpdates({
79
+ onUpdateComplete: ({ successCount, failCount, silent }) => {
1562
80
  if (successCount > 0) {
1563
81
  setMessage({
1564
82
  text: silent
@@ -1567,52 +85,31 @@ ${dim}────────────────────────
1567
85
  type: failCount > 0 ? 'error' : 'success',
1568
86
  });
1569
87
  }
88
+ proceedToNextView();
89
+ },
90
+ });
1570
91
 
1571
- // Proceed to next view
1572
- if (!configExists()) {
1573
- setView('setup');
1574
- } else {
1575
- setView('menu');
1576
- }
1577
- }, 100);
1578
- };
92
+ // Check for tool updates and proceed to next view
93
+ const checkToolUpdatesAndProceed = () => {
94
+ const { updates, shouldAutoUpdate } = checkForUpdates();
1579
95
 
1580
- const handleAlwaysUpdateTools = () => {
1581
- // Enable auto-update in config
1582
- setAutoUpdateConfig({ tools: true });
1583
- // Then update all
1584
- handleUpdateAllTools();
1585
- };
96
+ if (shouldAutoUpdate) {
97
+ updateAllTools(updates, true);
98
+ return;
99
+ }
1586
100
 
1587
- const handleSkipToolUpdates = () => {
1588
- if (!configExists()) {
1589
- setView('setup');
1590
- } else {
1591
- setView('menu');
101
+ if (updates.length > 0) {
102
+ setView('tool-updates');
103
+ return;
1592
104
  }
1593
- };
1594
105
 
1595
- const handleToggleAutoUpdateTools = () => {
1596
- const current = getAutoUpdateConfig();
1597
- setAutoUpdateConfig({ tools: !current.tools });
1598
- // Force re-render by setting message (auto-clears on next input)
1599
- setMessage({
1600
- text: `✓ Auto-update tools ${!current.tools ? 'enabled' : 'disabled'}`,
1601
- type: 'success',
1602
- });
106
+ proceedToNextView();
1603
107
  };
1604
108
 
1605
- const handleToggleAutoUpdateApp = () => {
1606
- const current = getAutoUpdateConfig();
1607
- setAutoUpdateConfig({ app: !current.app });
1608
- setMessage({
1609
- text: `✓ Auto-update app ${!current.app ? 'enabled' : 'disabled'}`,
1610
- type: 'success',
1611
- });
109
+ const handleSkipToolUpdates = () => {
110
+ proceedToNextView();
1612
111
  };
1613
112
 
1614
- const MAX_VISIBLE_ITEMS = 6;
1615
-
1616
113
  const tools = getBundledTools();
1617
114
  // Keep skills for configure view (tools configure via their primary skill)
1618
115
  const skills = getBundledSkills();
@@ -1668,32 +165,18 @@ ${dim}────────────────────────
1668
165
  setView('detail');
1669
166
  } else if (activeTab === 'settings') {
1670
167
  setView('detail');
1671
- setSelectedSetting(0);
1672
168
  }
1673
169
  }
1674
170
  } else if (view === 'detail') {
1675
171
  if (key.escape || key.backspace) {
1676
172
  setView('menu');
1677
173
  setSelectedAction(0);
1678
- setSelectedSetting(0);
1679
174
  }
1680
175
  if (activeTab === 'settings') {
1681
- // Settings detail view navigation
1682
- if (key.upArrow) {
1683
- setSelectedSetting((prev) => Math.max(0, prev - 1));
1684
- }
1685
- if (key.downArrow) {
1686
- setSelectedSetting((prev) => Math.min(2, prev + 1)); // 0: auto-update tools, 1: auto-update app, 2: edit profile
1687
- }
176
+ // Settings detail view - just enter to edit
1688
177
  if (key.return) {
1689
- if (selectedSetting === 0) {
1690
- handleToggleAutoUpdateTools();
1691
- } else if (selectedSetting === 1) {
1692
- handleToggleAutoUpdateApp();
1693
- } else if (selectedSetting === 2) {
1694
- setIsEditingSettings(true);
1695
- setView('setup');
1696
- }
178
+ setIsEditingSettings(true);
179
+ setView('setup');
1697
180
  }
1698
181
  }
1699
182
  if (key.leftArrow && activeTab === 'tools') {
@@ -1807,8 +290,8 @@ ${dim}────────────────────────
1807
290
  return (
1808
291
  <ToolUpdatePrompt
1809
292
  toolUpdates={toolUpdates}
1810
- onUpdateAll={() => handleUpdateAllTools()}
1811
- onAlways={handleAlwaysUpdateTools}
293
+ onUpdateAll={() => updateAllTools()}
294
+ onAlways={enableAutoUpdateAndUpdate}
1812
295
  onSkip={handleSkipToolUpdates}
1813
296
  isUpdating={isUpdatingTools}
1814
297
  />
@@ -1967,13 +450,7 @@ ${dim}────────────────────────
1967
450
 
1968
451
  {activeTab === 'settings' && (
1969
452
  <SettingsDetails
1970
- onEditSettings={() => {
1971
- setIsEditingSettings(true);
1972
- setView('setup');
1973
- }}
1974
453
  isFocused={view === 'detail'}
1975
- onToggleAutoUpdate={handleToggleAutoUpdateTools}
1976
- selectedSetting={selectedSetting}
1977
454
  />
1978
455
  )}
1979
456
  </Box>
@@ -1985,7 +462,7 @@ export async function tuiCommand(): Promise<void> {
1985
462
  process.stdout.write('\x1b[?1049h');
1986
463
  process.stdout.write('\x1b[H'); // Move cursor to top-left
1987
464
 
1988
- const { unmount, waitUntilExit } = render(<App />);
465
+ const { waitUntilExit } = render(<App />);
1989
466
 
1990
467
  await waitUntilExit();
1991
468