@orderful/droid 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @orderful/droid
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#74](https://github.com/Orderful/droid/pull/74) [`4e820c2`](https://github.com/Orderful/droid/commit/4e820c21b11cfbacc0fe35ee65fc98d169f065ec) Thanks [@frytyler](https://github.com/frytyler)! - Add droid meta-skill for update awareness
8
+ - New "droid" tool with `system: true` flag (auto-installs and stays current)
9
+ - SKILL.md teaches Claude to check npm for droid updates and nudge users
10
+ - System tools can't be uninstalled (Uninstall button hidden)
11
+
12
+ ## 0.12.0
13
+
14
+ ### Minor Changes
15
+
16
+ - [#72](https://github.com/Orderful/droid/pull/72) [`1611eac`](https://github.com/Orderful/droid/commit/1611eacb9076279bcc4ed552857d3c546441f80a) Thanks [@frytyler](https://github.com/frytyler)! - Add auto-update feature for app and tools
17
+ - Add `auto_update.app` and `auto_update.tools` config options
18
+ - Show tool updates prompt after welcome screen with [Update All] [Always] [Skip]
19
+ - Add [Always] button to app update prompt for consistency
20
+ - "Always" enables auto-update in config for future runs
21
+ - When auto-update is enabled, tools update silently on startup with indicator
22
+ - Add auto-update toggle to Settings screen
23
+ - Tools default to auto-update enabled, app defaults to disabled
24
+
3
25
  ## 0.11.1
4
26
 
5
27
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../../src/commands/tui.tsx"],"names":[],"mappings":"AA4lDA,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAWhD"}
1
+ {"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../../src/commands/tui.tsx"],"names":[],"mappings":"AA87DA,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAiBhD"}
@@ -6,12 +6,14 @@ import { execSync } from 'child_process';
6
6
  import { existsSync, readFileSync } from 'fs';
7
7
  import { join } from 'path';
8
8
  import { getBundledSkills, installSkill, uninstallSkill, updateSkill, } from '../lib/skills.js';
9
- import { getBundledTools, getBundledToolsDir, isToolInstalled, getToolUpdateStatus, getInstalledToolVersion } from '../lib/tools.js';
10
- import { configExists, loadConfig, saveConfig, loadSkillOverrides, saveSkillOverrides } from '../lib/config.js';
9
+ import { getBundledTools, getBundledToolsDir, isToolInstalled, getToolUpdateStatus, getInstalledToolVersion, getToolsWithUpdates } from '../lib/tools.js';
10
+ import { configExists, loadConfig, saveConfig, loadSkillOverrides, saveSkillOverrides, getAutoUpdateConfig, setAutoUpdateConfig } from '../lib/config.js';
11
11
  import { configurePlatformPermissions } from './setup.js';
12
12
  import { Platform, BuiltInOutput, ConfigOptionType } from '../lib/types.js';
13
13
  import { getVersion, getUpdateInfo, runUpdate } from '../lib/version.js';
14
14
  import { getRandomQuote } from '../lib/quotes.js';
15
+ // Module-level variable to store exit message (printed after leaving alternate screen)
16
+ let exitMessage = null;
15
17
  const colors = {
16
18
  primary: '#6366f1',
17
19
  bgSelected: '#2d2d2d',
@@ -90,20 +92,26 @@ function getOutputOptions() {
90
92
  }
91
93
  return options;
92
94
  }
93
- function WelcomeScreen({ onContinue, onUpdate, onExit, updateInfo, isUpdating }) {
95
+ function WelcomeScreen({ onContinue, onUpdate, onAlways, onExit, updateInfo, isUpdating }) {
94
96
  const [selectedButton, setSelectedButton] = useState(0);
95
97
  const welcomeQuote = useMemo(() => getRandomQuote(), []);
96
98
  useInput((input, key) => {
97
99
  if (isUpdating)
98
100
  return;
99
101
  if (updateInfo.hasUpdate) {
100
- if (key.leftArrow || key.rightArrow) {
101
- setSelectedButton((prev) => (prev === 0 ? 1 : 0));
102
+ if (key.leftArrow) {
103
+ setSelectedButton((prev) => Math.max(0, prev - 1));
104
+ }
105
+ if (key.rightArrow) {
106
+ setSelectedButton((prev) => Math.min(2, prev + 1));
102
107
  }
103
108
  if (key.return) {
104
109
  if (selectedButton === 0) {
105
110
  onUpdate();
106
111
  }
112
+ else if (selectedButton === 1) {
113
+ onAlways();
114
+ }
107
115
  else {
108
116
  onContinue();
109
117
  }
@@ -122,7 +130,40 @@ function WelcomeScreen({ onContinue, onUpdate, onExit, updateInfo, isUpdating })
122
130
  }
123
131
  });
124
132
  const hasUpdate = updateInfo.hasUpdate && updateInfo.latestVersion;
125
- return (_jsx(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", height: 18, children: _jsxs(Box, { flexDirection: "column", alignItems: "center", borderStyle: "single", borderColor: hasUpdate ? '#eab308' : colors.border, paddingX: 4, paddingY: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: colors.textDim, children: "\u2554\u2550\u2550\u2550\u2550\u2550\u2557 " }), _jsx(Text, { color: colors.text, children: "droid" }), _jsxs(Text, { color: colors.textDim, children: [" v", updateInfo.currentVersion] })] }), _jsxs(Text, { children: [_jsx(Text, { color: colors.textDim, children: "\u2551 " }), _jsx(Text, { color: hasUpdate ? '#eab308' : colors.primary, children: "\u25CF" }), _jsx(Text, { color: colors.textDim, children: " " }), _jsx(Text, { color: hasUpdate ? '#eab308' : colors.primary, children: "\u25CF" }), _jsx(Text, { color: colors.textDim, children: " \u2551 " }), _jsx(Text, { color: colors.textMuted, children: "Droid, teaching your AI new tricks" })] }), _jsxs(Text, { children: [_jsx(Text, { color: colors.textDim, children: "\u255A\u2550\u2566\u2550\u2566\u2550\u255D " }), _jsx(Text, { color: colors.textDim, children: "github.com/Orderful/droid" })] })] }), hasUpdate ? (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 2, marginBottom: 1, flexDirection: "column", alignItems: "center", children: [_jsx(Text, { color: "#eab308", italic: true, children: "\"The odds of functioning optimally without this" }), _jsx(Text, { color: "#eab308", italic: true, children: "update are approximately 3,720 to 1.\"" })] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: colors.textMuted, children: ["v", updateInfo.currentVersion, " \u2192 v", updateInfo.latestVersion] }) }), isUpdating ? (_jsx(Text, { color: "#eab308", children: "Updating..." })) : (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { backgroundColor: selectedButton === 0 ? '#eab308' : colors.bgSelected, color: selectedButton === 0 ? '#000000' : colors.textMuted, bold: selectedButton === 0, children: [' ', "Update", ' '] }), _jsx(Text, { children: " " }), _jsxs(Text, { backgroundColor: selectedButton === 1 ? colors.bgSelected : undefined, color: selectedButton === 1 ? colors.text : colors.textMuted, bold: selectedButton === 1, children: [' ', "Skip for now", ' '] })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, children: "\u2190\u2192 select \u00B7 enter" }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 2, marginBottom: 1, children: _jsx(Text, { backgroundColor: colors.primary, color: "#ffffff", bold: true, children: ` ${welcomeQuote} ` }) }), _jsx(Text, { color: colors.textDim, children: "press enter" })] }))] }) }));
133
+ return (_jsx(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", height: 18, children: _jsxs(Box, { flexDirection: "column", alignItems: "center", borderStyle: "single", borderColor: hasUpdate ? '#eab308' : colors.border, paddingX: 4, paddingY: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: colors.textDim, children: "\u2554\u2550\u2550\u2550\u2550\u2550\u2557 " }), _jsx(Text, { color: colors.text, children: "droid" }), _jsxs(Text, { color: colors.textDim, children: [" v", updateInfo.currentVersion] })] }), _jsxs(Text, { children: [_jsx(Text, { color: colors.textDim, children: "\u2551 " }), _jsx(Text, { color: hasUpdate ? '#eab308' : colors.primary, children: "\u25CF" }), _jsx(Text, { color: colors.textDim, children: " " }), _jsx(Text, { color: hasUpdate ? '#eab308' : colors.primary, children: "\u25CF" }), _jsx(Text, { color: colors.textDim, children: " \u2551 " }), _jsx(Text, { color: colors.textMuted, children: "Droid, teaching your AI new tricks" })] }), _jsxs(Text, { children: [_jsx(Text, { color: colors.textDim, children: "\u255A\u2550\u2566\u2550\u2566\u2550\u255D " }), _jsx(Text, { color: colors.textDim, children: "github.com/Orderful/droid" })] })] }), hasUpdate ? (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 2, marginBottom: 1, flexDirection: "column", alignItems: "center", children: [_jsx(Text, { color: "#eab308", italic: true, children: "\"The odds of functioning optimally without this" }), _jsx(Text, { color: "#eab308", italic: true, children: "update are approximately 3,720 to 1.\"" })] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: colors.textMuted, children: ["v", updateInfo.currentVersion, " \u2192 v", updateInfo.latestVersion] }) }), isUpdating ? (_jsx(Text, { color: "#eab308", children: "Updating..." })) : (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { backgroundColor: selectedButton === 0 ? '#eab308' : colors.bgSelected, color: selectedButton === 0 ? '#000000' : colors.textMuted, bold: selectedButton === 0, children: [' ', "Update", ' '] }), _jsx(Text, { children: " " }), _jsxs(Text, { backgroundColor: selectedButton === 1 ? '#eab308' : colors.bgSelected, color: selectedButton === 1 ? '#000000' : colors.textMuted, bold: selectedButton === 1, children: [' ', "Always", ' '] }), _jsx(Text, { children: " " }), _jsxs(Text, { backgroundColor: selectedButton === 2 ? colors.bgSelected : undefined, color: selectedButton === 2 ? colors.text : colors.textMuted, bold: selectedButton === 2, children: [' ', "Skip", ' '] })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, children: "\u2190\u2192 select \u00B7 enter \u00B7 \"Always\" enables auto-update" }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 2, marginBottom: 1, children: _jsx(Text, { backgroundColor: colors.primary, color: "#ffffff", bold: true, children: ` ${welcomeQuote} ` }) }), _jsx(Text, { color: colors.textDim, children: "press enter" })] }))] }) }));
134
+ }
135
+ function ToolUpdatePrompt({ toolUpdates, onUpdateAll, onAlways, onSkip, isUpdating }) {
136
+ const [selectedButton, setSelectedButton] = useState(0);
137
+ useInput((input, key) => {
138
+ if (isUpdating)
139
+ return;
140
+ if (key.leftArrow) {
141
+ setSelectedButton((prev) => Math.max(0, prev - 1));
142
+ }
143
+ if (key.rightArrow) {
144
+ setSelectedButton((prev) => Math.min(2, prev + 1));
145
+ }
146
+ if (key.return) {
147
+ if (selectedButton === 0) {
148
+ onUpdateAll();
149
+ }
150
+ else if (selectedButton === 1) {
151
+ onAlways();
152
+ }
153
+ else {
154
+ onSkip();
155
+ }
156
+ }
157
+ if (input === 'q') {
158
+ onSkip();
159
+ }
160
+ });
161
+ const buttons = [
162
+ { label: 'Update All', action: onUpdateAll },
163
+ { label: 'Always', action: onAlways },
164
+ { label: 'Skip', action: onSkip },
165
+ ];
166
+ return (_jsx(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", height: 18, children: _jsxs(Box, { flexDirection: "column", alignItems: "center", borderStyle: "single", borderColor: colors.primary, paddingX: 4, paddingY: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: colors.textDim, children: "\u2554\u2550\u2550\u2550\u2550\u2550\u2557 " }), _jsx(Text, { color: colors.text, children: "Tool Updates" })] }), _jsxs(Text, { children: [_jsx(Text, { color: colors.textDim, children: "\u2551 " }), _jsx(Text, { color: colors.primary, children: "\u25CF" }), _jsx(Text, { color: colors.textDim, children: " " }), _jsx(Text, { color: colors.primary, children: "\u25CF" }), _jsx(Text, { color: colors.textDim, children: " \u2551 " }), _jsxs(Text, { color: colors.textMuted, children: [toolUpdates.length, " tool", toolUpdates.length > 1 ? 's have' : ' has', " updates available"] })] }), _jsx(Text, { children: _jsx(Text, { color: colors.textDim, children: "\u255A\u2550\u2566\u2550\u2566\u2550\u255D" }) })] }), _jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [toolUpdates.slice(0, 5).map((tool) => (_jsxs(Text, { color: colors.textMuted, children: [_jsx(Text, { color: colors.primary, children: "\u2191" }), " ", tool.name, _jsxs(Text, { color: colors.textDim, children: [" ", tool.installedVersion, " \u2192 ", tool.bundledVersion] })] }, tool.name))), toolUpdates.length > 5 && (_jsxs(Text, { color: colors.textDim, children: ["...and ", toolUpdates.length - 5, " more"] }))] }), isUpdating ? (_jsx(Text, { color: colors.primary, children: "Updating tools..." })) : (_jsx(Box, { flexDirection: "row", children: buttons.map((button, index) => (_jsxs(Text, { backgroundColor: selectedButton === index ? colors.primary : colors.bgSelected, color: selectedButton === index ? '#000000' : colors.textMuted, bold: selectedButton === index, children: [' ', button.label, ' '] }, button.label))) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, children: "\u2190\u2192 select \u00B7 enter \u00B7 \"Always\" enables auto-update" }) })] }) }));
126
167
  }
127
168
  function SetupScreen({ onComplete, onSkip, initialConfig }) {
128
169
  const [step, setStep] = useState('platform');
@@ -202,11 +243,11 @@ function SetupScreen({ onComplete, onSkip, initialConfig }) {
202
243
  function TabBar({ tabs, activeTab }) {
203
244
  return (_jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: tabs.map((tab) => (_jsxs(Text, { backgroundColor: tab.id === activeTab ? colors.primary : undefined, color: tab.id === activeTab ? '#ffffff' : colors.textMuted, bold: tab.id === activeTab, wrap: "truncate", children: [' ', tab.label, ' '] }, tab.id))) }));
204
245
  }
205
- function ToolItem({ tool, isSelected, isActive, }) {
246
+ function ToolItem({ tool, isSelected, isActive, wasAutoUpdated, }) {
206
247
  const installed = isToolInstalled(tool.name);
207
248
  const installedVersion = getInstalledToolVersion(tool.name);
208
249
  const updateStatus = getToolUpdateStatus(tool.name);
209
- return (_jsx(Box, { paddingX: 1, backgroundColor: isActive ? colors.bgSelected : undefined, children: _jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: colors.textDim, children: [isSelected ? '>' : ' ', " "] }), _jsx(Text, { color: isSelected || isActive ? colors.text : colors.textMuted, children: tool.name }), installed && installedVersion && _jsxs(Text, { color: colors.textDim, children: [" v", installedVersion] }), installed && _jsx(Text, { color: colors.success, children: " \u2713" }), updateStatus.hasUpdate && _jsx(Text, { color: colors.primary, children: " \u2191" }), _jsx(Text, { children: " " }), tool.includes.skills.length > 0 && _jsx(Text, { color: colors.skill, children: "\u25CF " }), tool.includes.commands.length > 0 && _jsx(Text, { color: colors.command, children: "\u25CF " }), tool.includes.agents.length > 0 && _jsx(Text, { color: colors.agent, children: "\u25CF" })] }) }));
250
+ return (_jsx(Box, { paddingX: 1, backgroundColor: isActive ? colors.bgSelected : undefined, children: _jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: colors.textDim, children: [isSelected ? '>' : ' ', " "] }), _jsx(Text, { color: isSelected || isActive ? colors.text : colors.textMuted, children: tool.name }), installed && installedVersion && _jsxs(Text, { color: colors.textDim, children: [" v", installedVersion] }), installed && _jsx(Text, { color: colors.success, children: " \u2713" }), wasAutoUpdated && _jsx(Text, { color: colors.success, children: " \u2191" }), updateStatus.hasUpdate && !wasAutoUpdated && _jsx(Text, { color: colors.primary, children: " \u2191" }), _jsx(Text, { children: " " }), tool.includes.skills.length > 0 && _jsx(Text, { color: colors.skill, children: "\u25CF " }), tool.includes.commands.length > 0 && _jsx(Text, { color: colors.command, children: "\u25CF " }), tool.includes.agents.length > 0 && _jsx(Text, { color: colors.agent, children: "\u25CF" })] }) }));
210
251
  }
211
252
  function ToolDetails({ tool, isFocused, selectedAction, }) {
212
253
  if (!tool) {
@@ -215,6 +256,7 @@ function ToolDetails({ tool, isFocused, selectedAction, }) {
215
256
  const installed = isToolInstalled(tool.name);
216
257
  const installedVersion = getInstalledToolVersion(tool.name);
217
258
  const updateStatus = getToolUpdateStatus(tool.name);
259
+ const isSystemTool = tool.system === true;
218
260
  const actions = installed
219
261
  ? [
220
262
  { id: 'explore', label: 'Explore', variant: 'default' },
@@ -222,10 +264,12 @@ function ToolDetails({ tool, isFocused, selectedAction, }) {
222
264
  ? [{ id: 'update', label: `Update (${updateStatus.bundledVersion})`, variant: 'primary' }]
223
265
  : []),
224
266
  { id: 'configure', label: 'Configure', variant: 'default' },
225
- { id: 'uninstall', label: 'Uninstall', variant: 'danger' },
267
+ // System tools can't be uninstalled
268
+ ...(!isSystemTool ? [{ id: 'uninstall', label: 'Uninstall', variant: 'danger' }] : []),
226
269
  ]
227
270
  : [
228
271
  { id: 'explore', label: 'Explore', variant: 'default' },
272
+ // System tools auto-install, but show Install for manual trigger if needed
229
273
  { id: 'install', label: 'Install', variant: 'primary' },
230
274
  ];
231
275
  return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, flexGrow: 1, children: [_jsx(Text, { color: colors.text, bold: true, children: tool.name }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, children: [tool.version, tool.status && ` · ${tool.status}`, installed && _jsx(Text, { color: colors.success, children: " \u00B7 installed" }), updateStatus.hasUpdate && (_jsxs(Text, { color: colors.primary, children: [" \u00B7 update (", installedVersion, " \u2192 ", updateStatus.bundledVersion, ")"] }))] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textMuted, children: tool.description }) }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: colors.textDim, children: "Includes:" }), _jsx(Box, { marginTop: 1, children: _jsx(ComponentBadges, { tool: tool }) }), _jsxs(Box, { flexDirection: "column", marginTop: 1, marginLeft: 1, children: [tool.includes.skills.length > 0 && (_jsxs(Text, { children: [_jsx(Text, { color: colors.skill, children: "Skills: " }), _jsx(Text, { color: colors.textMuted, children: tool.includes.skills.map(s => s.name).join(', ') })] })), tool.includes.commands.length > 0 && (_jsxs(Text, { children: [_jsx(Text, { color: colors.command, children: "Commands: " }), _jsx(Text, { color: colors.textMuted, children: tool.includes.commands.map(c => `/${c}`).join(', ') })] })), tool.includes.agents.length > 0 && (_jsxs(Text, { children: [_jsx(Text, { color: colors.agent, children: "Agents: " }), _jsx(Text, { color: colors.textMuted, children: tool.includes.agents.join(', ') })] }))] })] }), isFocused && (_jsx(Box, { flexDirection: "row", marginTop: 1, children: actions.map((action, index) => (_jsxs(Text, { backgroundColor: selectedAction === index
@@ -400,10 +444,10 @@ function ToolExplorer({ tool, onViewSource, onClose }) {
400
444
  return (_jsx(Box, { marginRight: 1, marginBottom: 1, children: _jsx(Text, { backgroundColor: isSelected ? colors.agent : colors.bgSelected, color: isSelected ? '#000000' : colors.agent, bold: isSelected, children: ` ${item.name} ` }) }, item.name));
401
445
  }) })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, children: "\u2190\u2192 navigate \u00B7 enter view source \u00B7 esc back" }) })] }));
402
446
  }
403
- function SettingsDetails({ onEditSettings, isFocused, }) {
447
+ function SettingsDetails({ onEditSettings, isFocused, onToggleAutoUpdate, selectedSetting, }) {
404
448
  const config = loadConfig();
405
- const outputOptions = getOutputOptions();
406
- return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, flexGrow: 1, children: [_jsx(Text, { color: colors.text, bold: true, children: "Settings" }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: colors.textDim, children: "Platform: " }), _jsx(Text, { color: colors.text, children: config.platform === Platform.ClaudeCode ? 'Claude Code' : 'OpenCode' })] }), _jsxs(Text, { children: [_jsx(Text, { color: colors.textDim, children: "Your @mention: " }), _jsx(Text, { color: colors.text, children: config.user_mention })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, children: "Config: ~/.droid/config.yaml" }) }), isFocused && (_jsx(Box, { marginTop: 2, children: _jsxs(Text, { backgroundColor: colors.primary, color: "#ffffff", bold: true, children: [' ', "Edit Settings", ' '] }) })), !isFocused && (_jsx(Box, { marginTop: 2, children: _jsx(Text, { color: colors.textDim, children: "press enter to edit" }) }))] }));
449
+ const autoUpdateConfig = getAutoUpdateConfig();
450
+ return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, flexGrow: 1, children: [_jsx(Text, { color: colors.text, bold: true, children: "Settings" }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: colors.textDim, children: "Platform: " }), _jsx(Text, { color: colors.text, children: config.platform === Platform.ClaudeCode ? 'Claude Code' : 'OpenCode' })] }), _jsxs(Text, { children: [_jsx(Text, { color: colors.textDim, children: "Your @mention: " }), _jsx(Text, { color: colors.text, children: config.user_mention })] })] }), _jsxs(Box, { flexDirection: "column", marginTop: 2, children: [_jsx(Text, { color: colors.text, bold: true, children: "Auto-Update" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: colors.textDim, children: isFocused && selectedSetting === 0 ? '> ' : ' ' }), _jsxs(Text, { color: isFocused && selectedSetting === 0 ? colors.text : colors.textMuted, children: ["[", autoUpdateConfig.tools ? 'x' : ' ', "] Auto-update tools"] })] }) }), _jsx(Text, { color: colors.textDim, children: " Update tools automatically when droid starts" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: colors.textDim, children: isFocused && selectedSetting === 1 ? '> ' : ' ' }), _jsxs(Text, { color: isFocused && selectedSetting === 1 ? colors.text : colors.textMuted, children: ["[", autoUpdateConfig.app ? 'x' : ' ', "] Auto-update app"] })] }) }), _jsx(Text, { color: colors.textDim, children: " Update droid automatically when a new version is available" })] }), _jsx(Box, { marginTop: 2, children: _jsx(Text, { color: colors.textDim, children: "Config: ~/.droid/config.yaml" }) }), isFocused && (_jsx(Box, { marginTop: 2, children: _jsxs(Text, { backgroundColor: selectedSetting === 2 ? colors.primary : colors.bgSelected, color: selectedSetting === 2 ? '#ffffff' : colors.textMuted, bold: selectedSetting === 2, children: [' ', "Edit Profile", ' '] }) })), isFocused && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, children: "\u2191\u2193 select \u00B7 enter toggle/edit \u00B7 esc back" }) })), !isFocused && (_jsx(Box, { marginTop: 2, children: _jsx(Text, { color: colors.textDim, children: "press enter to configure" }) }))] }));
407
451
  }
408
452
  const MAX_VISIBLE_CONFIG_ITEMS = 4; // Each config item takes ~3 lines
409
453
  function SkillConfigScreen({ skill, onComplete, onCancel }) {
@@ -561,18 +605,36 @@ function App() {
561
605
  const [message, setMessage] = useState(null);
562
606
  const [isEditingSettings, setIsEditingSettings] = useState(false);
563
607
  const [readmeContent, setReadmeContent] = useState(null);
608
+ const [selectedSetting, setSelectedSetting] = useState(0);
564
609
  const [isUpdating, setIsUpdating] = useState(false);
610
+ const [isUpdatingTools, setIsUpdatingTools] = useState(false);
565
611
  const [previousView, setPreviousView] = useState('detail'); // Track where to return from readme
612
+ const [toolUpdates, setToolUpdates] = useState([]);
613
+ const [autoUpdatedTools, setAutoUpdatedTools] = useState([]);
566
614
  // Check for updates once on mount
567
615
  const updateInfo = useMemo(() => getUpdateInfo(), []);
616
+ const autoUpdateConfig = useMemo(() => getAutoUpdateConfig(), []);
568
617
  const handleUpdate = () => {
569
618
  setIsUpdating(true);
570
619
  // Run update in next tick to allow UI to show "Updating..."
571
620
  setTimeout(() => {
572
621
  const result = runUpdate();
573
622
  if (result.success) {
574
- // Print success message before exiting
575
- console.log('\n✅ Update complete! Run `droid` to start the new version.\n');
623
+ // Store message to print after leaving alternate screen (with ANSI colors)
624
+ const blue = '\x1b[38;2;99;102;241m'; // #6366f1
625
+ const dim = '\x1b[38;2;106;106;106m';
626
+ const reset = '\x1b[0m';
627
+ exitMessage = `
628
+ ${dim}────────────────────────────────────────────────────────${reset}
629
+
630
+ ${dim}╔═════╗${reset}
631
+ ${dim}║${reset} ${blue}●${reset} ${blue}●${reset} ${dim}║${reset} ${blue}"It's quite possible this system${reset}
632
+ ${dim}╚═╦═╦═╝${reset} ${blue}is now fully operational."${reset}
633
+
634
+ Run ${blue}droid${reset} to start the new version.
635
+
636
+ ${dim}────────────────────────────────────────────────────────${reset}
637
+ `;
576
638
  exit();
577
639
  }
578
640
  else {
@@ -588,6 +650,126 @@ function App() {
588
650
  }
589
651
  }, 100);
590
652
  };
653
+ const handleAlwaysUpdate = () => {
654
+ // Enable auto-update for app in config
655
+ setAutoUpdateConfig({ app: true });
656
+ // Then run the update
657
+ handleUpdate();
658
+ };
659
+ // Ensure system tools (marked with system: true in TOOL.yaml) are always installed and current
660
+ const ensureSystemTools = () => {
661
+ // Find all tools marked as system tools
662
+ const systemTools = tools.filter(t => t.system === true);
663
+ for (const systemTool of systemTools) {
664
+ const installed = isToolInstalled(systemTool.name);
665
+ const updateStatus = getToolUpdateStatus(systemTool.name);
666
+ // Install if not installed, or update if outdated (regardless of auto-update settings)
667
+ if (!installed || updateStatus.hasUpdate) {
668
+ const primarySkill = systemTool.includes.skills.find(s => s.required)?.name || systemTool.name;
669
+ installSkill(primarySkill);
670
+ }
671
+ }
672
+ };
673
+ // Check for tool updates and proceed to next view
674
+ const checkToolUpdatesAndProceed = () => {
675
+ // Always ensure system tools are current (bypasses auto-update settings)
676
+ ensureSystemTools();
677
+ const updates = getToolsWithUpdates();
678
+ setToolUpdates(updates);
679
+ // If auto_update.tools is true, auto-update silently
680
+ if (autoUpdateConfig.tools && updates.length > 0) {
681
+ handleUpdateAllTools(updates, true);
682
+ return;
683
+ }
684
+ // If there are updates and auto-update is off, show prompt
685
+ if (updates.length > 0) {
686
+ setView('tool-updates');
687
+ return;
688
+ }
689
+ // No updates, proceed to setup or menu
690
+ if (!configExists()) {
691
+ setView('setup');
692
+ }
693
+ else {
694
+ setView('menu');
695
+ }
696
+ };
697
+ const handleUpdateAllTools = (updates = toolUpdates, silent = false) => {
698
+ if (!silent) {
699
+ setIsUpdatingTools(true);
700
+ }
701
+ setTimeout(() => {
702
+ let successCount = 0;
703
+ let failCount = 0;
704
+ const updatedNames = [];
705
+ for (const tool of updates) {
706
+ // Find the tool to get its primary skill
707
+ const toolManifest = tools.find(t => t.name === tool.name);
708
+ if (toolManifest) {
709
+ const primarySkill = toolManifest.includes.skills.find(s => s.required)?.name || toolManifest.name;
710
+ const result = updateSkill(primarySkill);
711
+ if (result.success) {
712
+ successCount++;
713
+ updatedNames.push(tool.name);
714
+ }
715
+ else {
716
+ failCount++;
717
+ }
718
+ }
719
+ }
720
+ setIsUpdatingTools(false);
721
+ // Track which tools were auto-updated for visual indicator
722
+ if (silent && updatedNames.length > 0) {
723
+ setAutoUpdatedTools(updatedNames);
724
+ }
725
+ if (successCount > 0) {
726
+ setMessage({
727
+ text: silent
728
+ ? `↑ Auto-updated ${successCount} tool${successCount > 1 ? 's' : ''}`
729
+ : `✓ Updated ${successCount} tool${successCount > 1 ? 's' : ''}${failCount > 0 ? `, ${failCount} failed` : ''}`,
730
+ type: failCount > 0 ? 'error' : 'success',
731
+ });
732
+ }
733
+ // Proceed to next view
734
+ if (!configExists()) {
735
+ setView('setup');
736
+ }
737
+ else {
738
+ setView('menu');
739
+ }
740
+ }, 100);
741
+ };
742
+ const handleAlwaysUpdateTools = () => {
743
+ // Enable auto-update in config
744
+ setAutoUpdateConfig({ tools: true });
745
+ // Then update all
746
+ handleUpdateAllTools();
747
+ };
748
+ const handleSkipToolUpdates = () => {
749
+ if (!configExists()) {
750
+ setView('setup');
751
+ }
752
+ else {
753
+ setView('menu');
754
+ }
755
+ };
756
+ const handleToggleAutoUpdateTools = () => {
757
+ const current = getAutoUpdateConfig();
758
+ setAutoUpdateConfig({ tools: !current.tools });
759
+ // Force re-render by setting message (auto-clears on next input)
760
+ setMessage({
761
+ text: `✓ Auto-update tools ${!current.tools ? 'enabled' : 'disabled'}`,
762
+ type: 'success',
763
+ });
764
+ };
765
+ const handleToggleAutoUpdateApp = () => {
766
+ const current = getAutoUpdateConfig();
767
+ setAutoUpdateConfig({ app: !current.app });
768
+ setMessage({
769
+ text: `✓ Auto-update app ${!current.app ? 'enabled' : 'disabled'}`,
770
+ type: 'success',
771
+ });
772
+ };
591
773
  const MAX_VISIBLE_ITEMS = 6;
592
774
  const tools = getBundledTools();
593
775
  // Keep skills for configure view (tools configure via their primary skill)
@@ -642,8 +824,8 @@ function App() {
642
824
  setView('detail');
643
825
  }
644
826
  else if (activeTab === 'settings') {
645
- setIsEditingSettings(true);
646
- setView('setup');
827
+ setView('detail');
828
+ setSelectedSetting(0);
647
829
  }
648
830
  }
649
831
  }
@@ -651,19 +833,41 @@ function App() {
651
833
  if (key.escape || key.backspace) {
652
834
  setView('menu');
653
835
  setSelectedAction(0);
836
+ setSelectedSetting(0);
654
837
  }
655
- if (key.leftArrow) {
838
+ if (activeTab === 'settings') {
839
+ // Settings detail view navigation
840
+ if (key.upArrow) {
841
+ setSelectedSetting((prev) => Math.max(0, prev - 1));
842
+ }
843
+ if (key.downArrow) {
844
+ setSelectedSetting((prev) => Math.min(2, prev + 1)); // 0: auto-update tools, 1: auto-update app, 2: edit profile
845
+ }
846
+ if (key.return) {
847
+ if (selectedSetting === 0) {
848
+ handleToggleAutoUpdateTools();
849
+ }
850
+ else if (selectedSetting === 1) {
851
+ handleToggleAutoUpdateApp();
852
+ }
853
+ else if (selectedSetting === 2) {
854
+ setIsEditingSettings(true);
855
+ setView('setup');
856
+ }
857
+ }
858
+ }
859
+ if (key.leftArrow && activeTab === 'tools') {
656
860
  setSelectedAction((prev) => Math.max(0, prev - 1));
657
861
  }
658
- if (key.rightArrow) {
862
+ if (key.rightArrow && activeTab === 'tools') {
659
863
  let maxActions = 0;
660
- if (activeTab === 'tools') {
661
- const tool = tools[selectedIndex];
662
- const installed = tool ? isToolInstalled(tool.name) : false;
663
- const hasUpdate = tool ? getToolUpdateStatus(tool.name).hasUpdate : false;
664
- // Explore, [Update], Configure, Uninstall or Explore, Install
665
- maxActions = installed ? (hasUpdate ? 3 : 2) : 1;
666
- }
864
+ const tool = tools[selectedIndex];
865
+ const installed = tool ? isToolInstalled(tool.name) : false;
866
+ const hasUpdate = tool ? getToolUpdateStatus(tool.name).hasUpdate : false;
867
+ const isSystem = tool ? tool.system === true : false;
868
+ // Explore, [Update], Configure, [Uninstall] or Explore, Install
869
+ // System tools don't have Uninstall, so one less action
870
+ maxActions = installed ? (hasUpdate ? (isSystem ? 2 : 3) : (isSystem ? 1 : 2)) : 1;
667
871
  setSelectedAction((prev) => Math.min(maxActions, prev + 1));
668
872
  }
669
873
  if (key.return && activeTab === 'tools') {
@@ -671,13 +875,14 @@ function App() {
671
875
  if (tool) {
672
876
  const installed = isToolInstalled(tool.name);
673
877
  const toolUpdateStatus = getToolUpdateStatus(tool.name);
878
+ const isSystemTool = tool.system === true;
674
879
  // Build actions array to match ToolDetails
675
880
  const toolActions = installed
676
881
  ? [
677
882
  { id: 'explore' },
678
883
  ...(toolUpdateStatus.hasUpdate ? [{ id: 'update' }] : []),
679
884
  { id: 'configure' },
680
- { id: 'uninstall' },
885
+ ...(!isSystemTool ? [{ id: 'uninstall' }] : []),
681
886
  ]
682
887
  : [{ id: 'explore' }, { id: 'install' }];
683
888
  const actionId = toolActions[selectedAction]?.id;
@@ -738,22 +943,17 @@ function App() {
738
943
  }
739
944
  }
740
945
  }
741
- }, { isActive: view !== 'welcome' && view !== 'setup' && view !== 'configure' && view !== 'explorer' });
946
+ }, { isActive: view !== 'welcome' && view !== 'tool-updates' && view !== 'setup' && view !== 'configure' && view !== 'explorer' });
742
947
  const selectedTool = activeTab === 'tools' ? tools[selectedIndex] ?? null : null;
743
948
  // For configure view, we need the matching skill manifest
744
949
  const selectedSkillForConfig = selectedTool
745
950
  ? skills.find(s => s.name === (selectedTool.includes.skills.find(sk => sk.required)?.name || selectedTool.name))
746
951
  : null;
747
952
  if (view === 'welcome') {
748
- return (_jsx(WelcomeScreen, { updateInfo: updateInfo, isUpdating: isUpdating, onUpdate: handleUpdate, onExit: exit, onContinue: () => {
749
- // If no config exists, show setup first
750
- if (!configExists()) {
751
- setView('setup');
752
- }
753
- else {
754
- setView('menu');
755
- }
756
- } }));
953
+ return (_jsx(WelcomeScreen, { updateInfo: updateInfo, isUpdating: isUpdating, onUpdate: handleUpdate, onAlways: handleAlwaysUpdate, onExit: exit, onContinue: checkToolUpdatesAndProceed }));
954
+ }
955
+ if (view === 'tool-updates' && toolUpdates.length > 0) {
956
+ return (_jsx(ToolUpdatePrompt, { toolUpdates: toolUpdates, onUpdateAll: () => handleUpdateAllTools(), onAlways: handleAlwaysUpdateTools, onSkip: handleSkipToolUpdates, isUpdating: isUpdatingTools }));
757
957
  }
758
958
  if (view === 'setup') {
759
959
  return (_jsx(SetupScreen, { onComplete: () => {
@@ -787,7 +987,10 @@ function App() {
787
987
  setView('detail');
788
988
  } }));
789
989
  }
790
- return (_jsxs(Box, { flexDirection: "row", padding: 1, children: [_jsxs(Box, { flexDirection: "column", width: 44, borderStyle: "single", borderColor: colors.border, children: [_jsx(Box, { paddingX: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: colors.textDim, children: "[" }), _jsx(Text, { color: colors.primary, children: "\u25CF" }), _jsx(Text, { color: colors.textDim, children: " " }), _jsx(Text, { color: colors.primary, children: "\u25CF" }), _jsx(Text, { color: colors.textDim, children: "] " }), _jsx(Text, { color: colors.textMuted, children: "droid" }), _jsxs(Text, { color: colors.textDim, children: [" v", getVersion()] })] }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { color: colors.textDim, children: loadConfig().platform === Platform.ClaudeCode ? 'Claude Code' : 'OpenCode' }) }), _jsx(Box, { paddingX: 1, marginTop: 1, children: _jsx(TabBar, { tabs: tabs, activeTab: activeTab }) }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [activeTab === 'tools' && (_jsxs(_Fragment, { children: [scrollOffset > 0 && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, children: ["\u2191 ", scrollOffset, " more"] }) })), tools.slice(scrollOffset, scrollOffset + MAX_VISIBLE_ITEMS).map((tool, index) => (_jsx(ToolItem, { tool: tool, isSelected: scrollOffset + index === selectedIndex, isActive: scrollOffset + index === selectedIndex && view === 'detail' }, tool.name))), scrollOffset + MAX_VISIBLE_ITEMS < tools.length && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, children: ["\u2193 ", tools.length - scrollOffset - MAX_VISIBLE_ITEMS, " more"] }) })), tools.length > MAX_VISIBLE_ITEMS && (_jsx(Box, { paddingX: 1, marginTop: 1, children: _jsxs(Text, { color: colors.textDim, children: [tools.length, " tools total"] }) }))] })), activeTab === 'settings' && (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: colors.textDim, children: "View and edit config" }) }))] }), message && (_jsx(Box, { paddingX: 1, marginTop: 1, children: _jsx(Text, { color: message.type === 'success' ? colors.success : colors.error, children: message.text }) })), _jsx(Box, { paddingX: 1, marginTop: 1, children: _jsx(Text, { color: colors.textDim, children: view === 'menu' ? '←→ ↑↓ enter q' : '←→ enter esc q' }) })] }), activeTab === 'tools' && (_jsx(ToolDetails, { tool: selectedTool, isFocused: view === 'detail', selectedAction: selectedAction })), activeTab === 'settings' && (_jsx(SettingsDetails, { onEditSettings: () => setView('setup'), isFocused: false }))] }));
990
+ return (_jsxs(Box, { flexDirection: "row", padding: 1, children: [_jsxs(Box, { flexDirection: "column", width: 44, borderStyle: "single", borderColor: colors.border, children: [_jsx(Box, { paddingX: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: colors.textDim, children: "[" }), _jsx(Text, { color: colors.primary, children: "\u25CF" }), _jsx(Text, { color: colors.textDim, children: " " }), _jsx(Text, { color: colors.primary, children: "\u25CF" }), _jsx(Text, { color: colors.textDim, children: "] " }), _jsx(Text, { color: colors.textMuted, children: "droid" }), _jsxs(Text, { color: colors.textDim, children: [" v", getVersion()] })] }) }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { color: colors.textDim, children: loadConfig().platform === Platform.ClaudeCode ? 'Claude Code' : 'OpenCode' }) }), _jsx(Box, { paddingX: 1, marginTop: 1, children: _jsx(TabBar, { tabs: tabs, activeTab: activeTab }) }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [activeTab === 'tools' && (_jsxs(_Fragment, { children: [scrollOffset > 0 && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, children: ["\u2191 ", scrollOffset, " more"] }) })), tools.slice(scrollOffset, scrollOffset + MAX_VISIBLE_ITEMS).map((tool, index) => (_jsx(ToolItem, { tool: tool, isSelected: scrollOffset + index === selectedIndex, isActive: scrollOffset + index === selectedIndex && view === 'detail', wasAutoUpdated: autoUpdatedTools.includes(tool.name) }, tool.name))), scrollOffset + MAX_VISIBLE_ITEMS < tools.length && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, children: ["\u2193 ", tools.length - scrollOffset - MAX_VISIBLE_ITEMS, " more"] }) })), tools.length > MAX_VISIBLE_ITEMS && (_jsx(Box, { paddingX: 1, marginTop: 1, children: _jsxs(Text, { color: colors.textDim, children: [tools.length, " tools total"] }) }))] })), activeTab === 'settings' && (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: colors.textDim, children: "View and edit config" }) }))] }), message && (_jsx(Box, { paddingX: 1, marginTop: 1, children: _jsx(Text, { color: message.type === 'success' ? colors.success : colors.error, children: message.text }) })), _jsx(Box, { paddingX: 1, marginTop: 1, children: _jsx(Text, { color: colors.textDim, children: view === 'menu' ? '←→ ↑↓ enter q' : '←→ enter esc q' }) })] }), activeTab === 'tools' && (_jsx(ToolDetails, { tool: selectedTool, isFocused: view === 'detail', selectedAction: selectedAction })), activeTab === 'settings' && (_jsx(SettingsDetails, { onEditSettings: () => {
991
+ setIsEditingSettings(true);
992
+ setView('setup');
993
+ }, isFocused: view === 'detail', onToggleAutoUpdate: handleToggleAutoUpdateTools, selectedSetting: selectedSetting }))] }));
791
994
  }
792
995
  export async function tuiCommand() {
793
996
  // Enter alternate screen (fullscreen mode)
@@ -797,5 +1000,10 @@ export async function tuiCommand() {
797
1000
  await waitUntilExit();
798
1001
  // Leave alternate screen
799
1002
  process.stdout.write('\x1b[?1049l');
1003
+ // Print exit message if set (e.g., after successful update)
1004
+ if (exitMessage) {
1005
+ console.log(exitMessage);
1006
+ exitMessage = null;
1007
+ }
800
1008
  }
801
1009
  //# sourceMappingURL=tui.js.map