Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -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)
72 changes: 20 additions & 52 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

Expand All @@ -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'
Expand All @@ -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/
Expand Down Expand Up @@ -140,4 +107,5 @@ AGENTS.md
component.md
refactor.md

.heap-snapshots
# Heap snapshots
.heap-snapshots
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions source/app/hooks/useAppInitialization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -247,6 +248,7 @@ export function useAppInitialization({
mcpCommand,
initCommand,
themeCommand,
updateCommand,
]);

// Now start with the properly initialized objects (excluding MCP)
Expand Down
1 change: 1 addition & 0 deletions source/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
11 changes: 11 additions & 0 deletions source/commands/update.tsx
Original file line number Diff line number Diff line change
@@ -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);
},
};
15 changes: 8 additions & 7 deletions source/components/status.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -83,16 +83,17 @@ export default memo(function Status({
</Text>
)}
{updateInfo?.hasUpdate && (
<Text color={colors.warning}>
<Text bold={true}>Update Available: </Text>v
{updateInfo.currentVersion} → v{updateInfo.latestVersion}
<>
<Text color={colors.warning}>
<Text bold={true}>Update Available: </Text>v
{updateInfo.currentVersion} → v{updateInfo.latestVersion}
</Text>
{updateInfo.updateCommand && (
<Text color={colors.secondary}>
{' '}
(Run: {updateInfo.updateCommand})
↳ Run: /update or {updateInfo.updateCommand}
</Text>
)}
</Text>
</>
)}
</TitledBox>
);
Expand Down
97 changes: 97 additions & 0 deletions source/components/update-message.tsx
Original file line number Diff line number Diff line change
@@ -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>(Status.Checking);
const [error, setError] = useState<Error | null>(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;
}