@orderful/droid 0.11.1 → 0.12.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,18 @@
1
1
  # @orderful/droid
2
2
 
3
+ ## 0.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#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
8
+ - Add `auto_update.app` and `auto_update.tools` config options
9
+ - Show tool updates prompt after welcome screen with [Update All] [Always] [Skip]
10
+ - Add [Always] button to app update prompt for consistency
11
+ - "Always" enables auto-update in config for future runs
12
+ - When auto-update is enabled, tools update silently on startup with indicator
13
+ - Add auto-update toggle to Settings screen
14
+ - Tools default to auto-update enabled, app defaults to disabled
15
+
3
16
  ## 0.11.1
4
17
 
5
18
  ### 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":"AAo6DA,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) {
@@ -400,10 +441,10 @@ function ToolExplorer({ tool, onViewSource, onClose }) {
400
441
  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
442
  }) })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, children: "\u2190\u2192 navigate \u00B7 enter view source \u00B7 esc back" }) })] }));
402
443
  }
403
- function SettingsDetails({ onEditSettings, isFocused, }) {
444
+ function SettingsDetails({ onEditSettings, isFocused, onToggleAutoUpdate, selectedSetting, }) {
404
445
  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" }) }))] }));
446
+ const autoUpdateConfig = getAutoUpdateConfig();
447
+ 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
448
  }
408
449
  const MAX_VISIBLE_CONFIG_ITEMS = 4; // Each config item takes ~3 lines
409
450
  function SkillConfigScreen({ skill, onComplete, onCancel }) {
@@ -561,18 +602,36 @@ function App() {
561
602
  const [message, setMessage] = useState(null);
562
603
  const [isEditingSettings, setIsEditingSettings] = useState(false);
563
604
  const [readmeContent, setReadmeContent] = useState(null);
605
+ const [selectedSetting, setSelectedSetting] = useState(0);
564
606
  const [isUpdating, setIsUpdating] = useState(false);
607
+ const [isUpdatingTools, setIsUpdatingTools] = useState(false);
565
608
  const [previousView, setPreviousView] = useState('detail'); // Track where to return from readme
609
+ const [toolUpdates, setToolUpdates] = useState([]);
610
+ const [autoUpdatedTools, setAutoUpdatedTools] = useState([]);
566
611
  // Check for updates once on mount
567
612
  const updateInfo = useMemo(() => getUpdateInfo(), []);
613
+ const autoUpdateConfig = useMemo(() => getAutoUpdateConfig(), []);
568
614
  const handleUpdate = () => {
569
615
  setIsUpdating(true);
570
616
  // Run update in next tick to allow UI to show "Updating..."
571
617
  setTimeout(() => {
572
618
  const result = runUpdate();
573
619
  if (result.success) {
574
- // Print success message before exiting
575
- console.log('\n✅ Update complete! Run `droid` to start the new version.\n');
620
+ // Store message to print after leaving alternate screen (with ANSI colors)
621
+ const blue = '\x1b[38;2;99;102;241m'; // #6366f1
622
+ const dim = '\x1b[38;2;106;106;106m';
623
+ const reset = '\x1b[0m';
624
+ exitMessage = `
625
+ ${dim}────────────────────────────────────────────────────────${reset}
626
+
627
+ ${dim}╔═════╗${reset}
628
+ ${dim}║${reset} ${blue}●${reset} ${blue}●${reset} ${dim}║${reset} ${blue}"It's quite possible this system${reset}
629
+ ${dim}╚═╦═╦═╝${reset} ${blue}is now fully operational."${reset}
630
+
631
+ Run ${blue}droid${reset} to start the new version.
632
+
633
+ ${dim}────────────────────────────────────────────────────────${reset}
634
+ `;
576
635
  exit();
577
636
  }
578
637
  else {
@@ -588,6 +647,110 @@ function App() {
588
647
  }
589
648
  }, 100);
590
649
  };
650
+ const handleAlwaysUpdate = () => {
651
+ // Enable auto-update for app in config
652
+ setAutoUpdateConfig({ app: true });
653
+ // Then run the update
654
+ handleUpdate();
655
+ };
656
+ // Check for tool updates and proceed to next view
657
+ const checkToolUpdatesAndProceed = () => {
658
+ const updates = getToolsWithUpdates();
659
+ setToolUpdates(updates);
660
+ // If auto_update.tools is true, auto-update silently
661
+ if (autoUpdateConfig.tools && updates.length > 0) {
662
+ handleUpdateAllTools(updates, true);
663
+ return;
664
+ }
665
+ // If there are updates and auto-update is off, show prompt
666
+ if (updates.length > 0) {
667
+ setView('tool-updates');
668
+ return;
669
+ }
670
+ // No updates, proceed to setup or menu
671
+ if (!configExists()) {
672
+ setView('setup');
673
+ }
674
+ else {
675
+ setView('menu');
676
+ }
677
+ };
678
+ const handleUpdateAllTools = (updates = toolUpdates, silent = false) => {
679
+ if (!silent) {
680
+ setIsUpdatingTools(true);
681
+ }
682
+ setTimeout(() => {
683
+ let successCount = 0;
684
+ let failCount = 0;
685
+ const updatedNames = [];
686
+ for (const tool of updates) {
687
+ // Find the tool to get its primary skill
688
+ const toolManifest = tools.find(t => t.name === tool.name);
689
+ if (toolManifest) {
690
+ const primarySkill = toolManifest.includes.skills.find(s => s.required)?.name || toolManifest.name;
691
+ const result = updateSkill(primarySkill);
692
+ if (result.success) {
693
+ successCount++;
694
+ updatedNames.push(tool.name);
695
+ }
696
+ else {
697
+ failCount++;
698
+ }
699
+ }
700
+ }
701
+ setIsUpdatingTools(false);
702
+ // Track which tools were auto-updated for visual indicator
703
+ if (silent && updatedNames.length > 0) {
704
+ setAutoUpdatedTools(updatedNames);
705
+ }
706
+ if (successCount > 0) {
707
+ setMessage({
708
+ text: silent
709
+ ? `↑ Auto-updated ${successCount} tool${successCount > 1 ? 's' : ''}`
710
+ : `✓ Updated ${successCount} tool${successCount > 1 ? 's' : ''}${failCount > 0 ? `, ${failCount} failed` : ''}`,
711
+ type: failCount > 0 ? 'error' : 'success',
712
+ });
713
+ }
714
+ // Proceed to next view
715
+ if (!configExists()) {
716
+ setView('setup');
717
+ }
718
+ else {
719
+ setView('menu');
720
+ }
721
+ }, 100);
722
+ };
723
+ const handleAlwaysUpdateTools = () => {
724
+ // Enable auto-update in config
725
+ setAutoUpdateConfig({ tools: true });
726
+ // Then update all
727
+ handleUpdateAllTools();
728
+ };
729
+ const handleSkipToolUpdates = () => {
730
+ if (!configExists()) {
731
+ setView('setup');
732
+ }
733
+ else {
734
+ setView('menu');
735
+ }
736
+ };
737
+ const handleToggleAutoUpdateTools = () => {
738
+ const current = getAutoUpdateConfig();
739
+ setAutoUpdateConfig({ tools: !current.tools });
740
+ // Force re-render by setting message (auto-clears on next input)
741
+ setMessage({
742
+ text: `✓ Auto-update tools ${!current.tools ? 'enabled' : 'disabled'}`,
743
+ type: 'success',
744
+ });
745
+ };
746
+ const handleToggleAutoUpdateApp = () => {
747
+ const current = getAutoUpdateConfig();
748
+ setAutoUpdateConfig({ app: !current.app });
749
+ setMessage({
750
+ text: `✓ Auto-update app ${!current.app ? 'enabled' : 'disabled'}`,
751
+ type: 'success',
752
+ });
753
+ };
591
754
  const MAX_VISIBLE_ITEMS = 6;
592
755
  const tools = getBundledTools();
593
756
  // Keep skills for configure view (tools configure via their primary skill)
@@ -642,8 +805,8 @@ function App() {
642
805
  setView('detail');
643
806
  }
644
807
  else if (activeTab === 'settings') {
645
- setIsEditingSettings(true);
646
- setView('setup');
808
+ setView('detail');
809
+ setSelectedSetting(0);
647
810
  }
648
811
  }
649
812
  }
@@ -651,19 +814,39 @@ function App() {
651
814
  if (key.escape || key.backspace) {
652
815
  setView('menu');
653
816
  setSelectedAction(0);
817
+ setSelectedSetting(0);
654
818
  }
655
- if (key.leftArrow) {
819
+ if (activeTab === 'settings') {
820
+ // Settings detail view navigation
821
+ if (key.upArrow) {
822
+ setSelectedSetting((prev) => Math.max(0, prev - 1));
823
+ }
824
+ if (key.downArrow) {
825
+ setSelectedSetting((prev) => Math.min(2, prev + 1)); // 0: auto-update tools, 1: auto-update app, 2: edit profile
826
+ }
827
+ if (key.return) {
828
+ if (selectedSetting === 0) {
829
+ handleToggleAutoUpdateTools();
830
+ }
831
+ else if (selectedSetting === 1) {
832
+ handleToggleAutoUpdateApp();
833
+ }
834
+ else if (selectedSetting === 2) {
835
+ setIsEditingSettings(true);
836
+ setView('setup');
837
+ }
838
+ }
839
+ }
840
+ if (key.leftArrow && activeTab === 'tools') {
656
841
  setSelectedAction((prev) => Math.max(0, prev - 1));
657
842
  }
658
- if (key.rightArrow) {
843
+ if (key.rightArrow && activeTab === 'tools') {
659
844
  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
- }
845
+ const tool = tools[selectedIndex];
846
+ const installed = tool ? isToolInstalled(tool.name) : false;
847
+ const hasUpdate = tool ? getToolUpdateStatus(tool.name).hasUpdate : false;
848
+ // Explore, [Update], Configure, Uninstall or Explore, Install
849
+ maxActions = installed ? (hasUpdate ? 3 : 2) : 1;
667
850
  setSelectedAction((prev) => Math.min(maxActions, prev + 1));
668
851
  }
669
852
  if (key.return && activeTab === 'tools') {
@@ -738,22 +921,17 @@ function App() {
738
921
  }
739
922
  }
740
923
  }
741
- }, { isActive: view !== 'welcome' && view !== 'setup' && view !== 'configure' && view !== 'explorer' });
924
+ }, { isActive: view !== 'welcome' && view !== 'tool-updates' && view !== 'setup' && view !== 'configure' && view !== 'explorer' });
742
925
  const selectedTool = activeTab === 'tools' ? tools[selectedIndex] ?? null : null;
743
926
  // For configure view, we need the matching skill manifest
744
927
  const selectedSkillForConfig = selectedTool
745
928
  ? skills.find(s => s.name === (selectedTool.includes.skills.find(sk => sk.required)?.name || selectedTool.name))
746
929
  : null;
747
930
  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
- } }));
931
+ return (_jsx(WelcomeScreen, { updateInfo: updateInfo, isUpdating: isUpdating, onUpdate: handleUpdate, onAlways: handleAlwaysUpdate, onExit: exit, onContinue: checkToolUpdatesAndProceed }));
932
+ }
933
+ if (view === 'tool-updates' && toolUpdates.length > 0) {
934
+ return (_jsx(ToolUpdatePrompt, { toolUpdates: toolUpdates, onUpdateAll: () => handleUpdateAllTools(), onAlways: handleAlwaysUpdateTools, onSkip: handleSkipToolUpdates, isUpdating: isUpdatingTools }));
757
935
  }
758
936
  if (view === 'setup') {
759
937
  return (_jsx(SetupScreen, { onComplete: () => {
@@ -787,7 +965,10 @@ function App() {
787
965
  setView('detail');
788
966
  } }));
789
967
  }
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 }))] }));
968
+ 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: () => {
969
+ setIsEditingSettings(true);
970
+ setView('setup');
971
+ }, isFocused: view === 'detail', onToggleAutoUpdate: handleToggleAutoUpdateTools, selectedSetting: selectedSetting }))] }));
791
972
  }
792
973
  export async function tuiCommand() {
793
974
  // Enter alternate screen (fullscreen mode)
@@ -797,5 +978,10 @@ export async function tuiCommand() {
797
978
  await waitUntilExit();
798
979
  // Leave alternate screen
799
980
  process.stdout.write('\x1b[?1049l');
981
+ // Print exit message if set (e.g., after successful update)
982
+ if (exitMessage) {
983
+ console.log(exitMessage);
984
+ exitMessage = null;
985
+ }
800
986
  }
801
987
  //# sourceMappingURL=tui.js.map