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) 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/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} )} - + )} ); diff --git a/source/components/update-message.tsx b/source/components/update-message.tsx new file mode 100644 index 0000000..bf9de07 --- /dev/null +++ b/source/components/update-message.tsx @@ -0,0 +1,97 @@ +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 available updates...', + }); + } + + if (status === Status.Updating) { + return React.createElement(InfoMessage, { + message: 'Downloading and installing the latest Nanocoder update...', + }); + } + + 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 has been updated to the latest version. Please restart your session to apply the update.', + }); + } + + if (status === Status.Error) { + return React.createElement(ErrorMessage, { + message: `Failed to update Nanocoder: ${error?.message}`, + }); + } + + return null; +}