From 00f57b2b3584e2a306b91a6dc8ba7a61edbfba8f Mon Sep 17 00:00:00 2001 From: Alex Spinu Date: Fri, 19 Sep 2025 11:02:52 +0300 Subject: [PATCH 1/4] feat: add builtin update command --- .gitignore | 72 +++++------------ README.md | 2 + source/app/hooks/useAppInitialization.tsx | 2 + source/commands/index.ts | 1 + source/commands/update.tsx | 11 +++ source/components/update-message.tsx | 95 +++++++++++++++++++++++ 6 files changed, 131 insertions(+), 52 deletions(-) create mode 100644 source/commands/update.tsx create mode 100644 source/components/update-message.tsx diff --git a/.gitignore b/.gitignore index 3943ef4..8df78a7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ logs *.log npm-debug.log* yarn-debug.log* -yarn-error.log* pnpm-debug.log* lerna-debug.log* @@ -13,48 +12,44 @@ pids *.seed *.pid.lock -# Directory for instrumented libs generated by jscoverage/JSCover +# Instrumented libs lib-cov -# Coverage directory used by tools like istanbul +# Coverage coverage *.lcov - -# nyc test coverage .nyc_output -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +# Grunt intermediate storage .grunt -# Bower dependency directory (https://bower.io/) -bower_components - -# node_modules +# Dependency directories node_modules/ jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) +bower_components/ web_modules/ +# Build output +build/Release +dist/ +out +.next +.nuxt +public + # TypeScript cache *.tsbuildinfo -# Optional npm cache directory +# Optional caches .npm - -# Optional eslint cache .eslintcache - -# Optional stylelint cache .stylelintcache - -# Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ -# Optional REPL history +# REPL history .node_repl_history # Output of 'npm pack' @@ -70,46 +65,18 @@ web_modules/ .env.production.local .env.local -# parcel-bundler cache (https://parceljs.org/) +# Parcel/Snowpack/Gatsby/Storybook cache .cache .parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -public - -# Storybook build outputs -.out .storybook-out storybook-static +.out # Temporary folders +.tmp/ tmp/ temp/ -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ - -# TypeScript compiled output -dist/ - # IDE and editor files .vscode/ .idea/ @@ -140,4 +107,5 @@ AGENTS.md component.md refactor.md -.heap-snapshots \ No newline at end of file +# Heap snapshots +.heap-snapshots diff --git a/README.md b/README.md index 0d69371..156ac2b 100644 --- a/README.md +++ b/README.md @@ -247,6 +247,8 @@ Nanocoder automatically saves your preferences to remember your choices across s - `/debug` - Toggle logging levels (silent/normal/verbose) - `/custom-commands` - List all custom commands - `/exit` - Exit the application +- `/theme` - Select a theme for the Nanocoder CLI +- `/update` - Update Nanocoder to the latest version - `!command` - Execute bash commands directly without leaving Nanocoder (output becomes context for the LLM) #### Custom Commands diff --git a/source/app/hooks/useAppInitialization.tsx b/source/app/hooks/useAppInitialization.tsx index 5e03311..09007d3 100644 --- a/source/app/hooks/useAppInitialization.tsx +++ b/source/app/hooks/useAppInitialization.tsx @@ -28,6 +28,7 @@ import { mcpCommand, initCommand, themeCommand, + updateCommand, } from '../../commands/index.js'; import SuccessMessage from '../../components/success-message.js'; import ErrorMessage from '../../components/error-message.js'; @@ -247,6 +248,7 @@ export function useAppInitialization({ mcpCommand, initCommand, themeCommand, + updateCommand, ]); // Now start with the properly initialized objects (excluding MCP) diff --git a/source/commands/index.ts b/source/commands/index.ts index ad7650b..da62563 100644 --- a/source/commands/index.ts +++ b/source/commands/index.ts @@ -8,3 +8,4 @@ export * from './debug.js'; export * from './custom-commands.js'; export * from './init.js'; export * from './theme.js'; +export * from './update.js'; diff --git a/source/commands/update.tsx b/source/commands/update.tsx new file mode 100644 index 0000000..24dc865 --- /dev/null +++ b/source/commands/update.tsx @@ -0,0 +1,11 @@ +import {Command} from '../types/index.js'; +import React from 'react'; +import UpdateMessage from '../components/update-message.js'; + +export const updateCommand: Command = { + name: 'update', + description: 'Update Nanocoder to the latest version', + handler: async (_args: string[]) => { + return React.createElement(UpdateMessage); + }, +}; diff --git a/source/components/update-message.tsx b/source/components/update-message.tsx new file mode 100644 index 0000000..9ac53d0 --- /dev/null +++ b/source/components/update-message.tsx @@ -0,0 +1,95 @@ +import React, {useEffect, useState} from 'react'; +import {toolRegistry} from '../tools/index.js'; +import InfoMessage from './info-message.js'; +import SuccessMessage from './success-message.js'; +import ErrorMessage from './error-message.js'; +import {checkForUpdates} from '../utils/update-checker.js'; + +enum Status { + Checking = 'checking', + Updating = 'updating', + NoUpdate = 'no-update', + Success = 'success', + Error = 'error', +} + +export default function UpdateMessage() { + const [status, setStatus] = useState(Status.Checking); + const [error, setError] = useState(null); + + useEffect(() => { + let isMounted = true; + + const check = async () => { + try { + const updateInfo = await checkForUpdates(); + if (isMounted) { + if (updateInfo.hasUpdate) { + setStatus(Status.Updating); + update(); + } else { + setStatus(Status.NoUpdate); + } + } + } catch (e) { + if (isMounted) { + setStatus(Status.Error); + setError(e as Error); + } + } + }; + + const update = async () => { + try { + await toolRegistry.execute_bash({ + command: 'npm update -g @motesoftware/nanocoder', + }); + if (isMounted) { + setStatus(Status.Success); + } + } catch (e) { + if (isMounted) { + setStatus(Status.Error); + setError(e as Error); + } + } + }; + + check(); + + return () => { + isMounted = false; + }; + }, []); + + if (status === Status.Checking) { + return React.createElement(InfoMessage, { + message: 'Checking for updates...', + }); + } + + if (status === Status.Updating) { + return React.createElement(InfoMessage, {message: 'Updating nanocoder...'}); + } + + if (status === Status.NoUpdate) { + return React.createElement(SuccessMessage, { + message: 'Nanocoder is already up to date.', + }); + } + + if (status === Status.Success) { + return React.createElement(SuccessMessage, { + message: + 'Nanocoder was updated to the latest version, restart the session to use the new version', + }); + } + + if (status === Status.Error) { + return React.createElement(ErrorMessage, { + message: `Error updating nanocoder: ${error?.message}`, + }); + } + + return null; +} From 29c0d452676e5fb703cee1255d50dac0bf82ffda Mon Sep 17 00:00:00 2001 From: Alex Spinu Date: Fri, 19 Sep 2025 11:05:10 +0300 Subject: [PATCH 2/4] chore: add pull request template --- .github/pull_request_template.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..070e84b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,24 @@ +## Description + +Brief description of what this PR does + +## Type of Change + +- [ ] Bug fix +- [ ] New feature +- [ ] Breaking change +- [ ] Documentation update + +## Testing + +- [ ] Tested with Ollama +- [ ] Tested with OpenRouter +- [ ] Tested with OpenAI-compatible API +- [ ] Tested MCP integration (if applicable) + +## Checklist + +- [ ] Code follows project style guidelines +- [ ] Self-review completed +- [ ] Documentation updated (if needed) +- [ ] No breaking changes (or clearly documented) From d7eaa62d3a44b59faf836445f7cf033fd36ee86c Mon Sep 17 00:00:00 2001 From: Alex Spinu Date: Fri, 19 Sep 2025 11:23:03 +0300 Subject: [PATCH 3/4] chore: copy change --- source/components/status.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/source/components/status.tsx b/source/components/status.tsx index 9ff030b..89dbd50 100644 --- a/source/components/status.tsx +++ b/source/components/status.tsx @@ -1,5 +1,5 @@ import {Text} from 'ink'; -import {memo, useState, useEffect} from 'react'; +import {memo, useEffect, useState} from 'react'; import {existsSync} from 'fs'; import {useTheme} from '../hooks/useTheme.js'; @@ -83,16 +83,17 @@ export default memo(function Status({ )} {updateInfo?.hasUpdate && ( - - Update Available: v - {updateInfo.currentVersion} → v{updateInfo.latestVersion} + <> + + Update Available: v + {updateInfo.currentVersion} → v{updateInfo.latestVersion} + {updateInfo.updateCommand && ( - {' '} - (Run: {updateInfo.updateCommand}) + ↳ Run: /update or {updateInfo.updateCommand} )} - + )} ); From 0175357a23e60aefbe7d7923658b297ebcb27b81 Mon Sep 17 00:00:00 2001 From: Alex Spinu Date: Fri, 19 Sep 2025 11:24:11 +0300 Subject: [PATCH 4/4] chore: copy change --- source/components/update-message.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/source/components/update-message.tsx b/source/components/update-message.tsx index 9ac53d0..bf9de07 100644 --- a/source/components/update-message.tsx +++ b/source/components/update-message.tsx @@ -64,12 +64,14 @@ export default function UpdateMessage() { if (status === Status.Checking) { return React.createElement(InfoMessage, { - message: 'Checking for updates...', + message: 'Checking for available updates...', }); } if (status === Status.Updating) { - return React.createElement(InfoMessage, {message: 'Updating nanocoder...'}); + return React.createElement(InfoMessage, { + message: 'Downloading and installing the latest Nanocoder update...', + }); } if (status === Status.NoUpdate) { @@ -81,13 +83,13 @@ export default function UpdateMessage() { if (status === Status.Success) { return React.createElement(SuccessMessage, { message: - 'Nanocoder was updated to the latest version, restart the session to use the new version', + 'Nanocoder has been updated to the latest version. Please restart your session to apply the update.', }); } if (status === Status.Error) { return React.createElement(ErrorMessage, { - message: `Error updating nanocoder: ${error?.message}`, + message: `Failed to update Nanocoder: ${error?.message}`, }); }