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;
+}