Terminal-based AI coding agent with interactive TUI for autonomous code generation.
Features: - Interactive TUI with React/Ink - Autonomous agent with tool calls (bash, read, write, edit, glob, grep) - Permission system with pattern-based rules - Session management with auto-compaction - Dual providers: GitHub Copilot and Ollama - MCP server integration - Todo panel and theme system - Streaming responses - GitHub-compatible project context
This commit is contained in:
68
src/services/command-suggestion/analyze.ts
Normal file
68
src/services/command-suggestion/analyze.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* File change analysis for command suggestions
|
||||
*/
|
||||
|
||||
import { basename } from "path";
|
||||
|
||||
import { detectProjectContext } from "@services/command-suggestion/context";
|
||||
import { SUGGESTION_PATTERNS } from "@services/command-suggestion/patterns";
|
||||
import {
|
||||
getProjectContext,
|
||||
setProjectContext,
|
||||
addSuggestion,
|
||||
} from "@services/command-suggestion/state";
|
||||
import type {
|
||||
CommandSuggestion,
|
||||
SuggestionPattern,
|
||||
} from "@/types/command-suggestion";
|
||||
|
||||
const matchesFilePattern = (
|
||||
pattern: SuggestionPattern,
|
||||
filePath: string,
|
||||
fileName: string,
|
||||
): boolean =>
|
||||
pattern.filePatterns.some((p) => p.test(filePath) || p.test(fileName));
|
||||
|
||||
const matchesContentPattern = (
|
||||
pattern: SuggestionPattern,
|
||||
content?: string,
|
||||
): boolean => {
|
||||
if (!pattern.contentPatterns || !content) {
|
||||
return true;
|
||||
}
|
||||
return pattern.contentPatterns.some((p) => p.test(content));
|
||||
};
|
||||
|
||||
export const analyzeFileChange = (
|
||||
filePath: string,
|
||||
content?: string,
|
||||
): CommandSuggestion[] => {
|
||||
let ctx = getProjectContext();
|
||||
if (!ctx) {
|
||||
ctx = detectProjectContext(process.cwd());
|
||||
setProjectContext(ctx);
|
||||
}
|
||||
|
||||
const newSuggestions: CommandSuggestion[] = [];
|
||||
const fileName = basename(filePath);
|
||||
|
||||
for (const pattern of SUGGESTION_PATTERNS) {
|
||||
if (!matchesFilePattern(pattern, filePath, fileName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!matchesContentPattern(pattern, content)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const suggestions = pattern.suggestions(ctx, filePath);
|
||||
|
||||
for (const suggestion of suggestions) {
|
||||
if (addSuggestion(suggestion)) {
|
||||
newSuggestions.push(suggestion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newSuggestions;
|
||||
};
|
||||
41
src/services/command-suggestion/context.ts
Normal file
41
src/services/command-suggestion/context.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Project context detection
|
||||
*/
|
||||
|
||||
import { existsSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
import { PROJECT_FILES } from "@constants/command-suggestion";
|
||||
import type { ProjectContext } from "@/types/command-suggestion";
|
||||
|
||||
export const detectProjectContext = (cwd: string): ProjectContext => ({
|
||||
hasPackageJson: existsSync(join(cwd, PROJECT_FILES.PACKAGE_JSON)),
|
||||
hasYarnLock: existsSync(join(cwd, PROJECT_FILES.YARN_LOCK)),
|
||||
hasPnpmLock: existsSync(join(cwd, PROJECT_FILES.PNPM_LOCK)),
|
||||
hasBunLock: existsSync(join(cwd, PROJECT_FILES.BUN_LOCK)),
|
||||
hasCargoToml: existsSync(join(cwd, PROJECT_FILES.CARGO_TOML)),
|
||||
hasGoMod: existsSync(join(cwd, PROJECT_FILES.GO_MOD)),
|
||||
hasPyproject: existsSync(join(cwd, PROJECT_FILES.PYPROJECT)),
|
||||
hasRequirements: existsSync(join(cwd, PROJECT_FILES.REQUIREMENTS)),
|
||||
hasMakefile: existsSync(join(cwd, PROJECT_FILES.MAKEFILE)),
|
||||
hasDockerfile: existsSync(join(cwd, PROJECT_FILES.DOCKERFILE)),
|
||||
cwd,
|
||||
});
|
||||
|
||||
const PACKAGE_MANAGER_PRIORITY: Array<{
|
||||
check: (ctx: ProjectContext) => boolean;
|
||||
manager: string;
|
||||
}> = [
|
||||
{ check: (ctx) => ctx.hasBunLock, manager: "bun" },
|
||||
{ check: (ctx) => ctx.hasPnpmLock, manager: "pnpm" },
|
||||
{ check: (ctx) => ctx.hasYarnLock, manager: "yarn" },
|
||||
];
|
||||
|
||||
export const getPackageManager = (ctx: ProjectContext): string => {
|
||||
for (const { check, manager } of PACKAGE_MANAGER_PRIORITY) {
|
||||
if (check(ctx)) {
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
return "npm";
|
||||
};
|
||||
41
src/services/command-suggestion/format.ts
Normal file
41
src/services/command-suggestion/format.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Command suggestion formatting and retrieval
|
||||
*/
|
||||
|
||||
import { PRIORITY_ORDER, PRIORITY_ICONS } from "@constants/command-suggestion";
|
||||
import {
|
||||
getPendingSuggestionsMap,
|
||||
clearSuggestions as clearStore,
|
||||
removeSuggestion as removeFromStore,
|
||||
} from "@services/command-suggestion/state";
|
||||
import type { CommandSuggestion } from "@/types/command-suggestion";
|
||||
|
||||
export const getPendingSuggestions = (): CommandSuggestion[] => {
|
||||
const suggestionsMap = getPendingSuggestionsMap();
|
||||
return Array.from(suggestionsMap.values()).sort(
|
||||
(a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority],
|
||||
);
|
||||
};
|
||||
|
||||
export const clearSuggestions = (): void => clearStore();
|
||||
|
||||
export const removeSuggestion = (command: string): void =>
|
||||
removeFromStore(command);
|
||||
|
||||
export const formatSuggestions = (suggestions: CommandSuggestion[]): string => {
|
||||
if (suggestions.length === 0) return "";
|
||||
|
||||
const lines = ["", "Suggested commands:"];
|
||||
|
||||
for (const s of suggestions) {
|
||||
const icon = PRIORITY_ICONS[s.priority];
|
||||
lines.push(` ${icon} ${s.command} (${s.reason})`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
};
|
||||
|
||||
export const hasHighPrioritySuggestions = (): boolean => {
|
||||
const suggestionsMap = getPendingSuggestionsMap();
|
||||
return Array.from(suggestionsMap.values()).some((s) => s.priority === "high");
|
||||
};
|
||||
225
src/services/command-suggestion/patterns.ts
Normal file
225
src/services/command-suggestion/patterns.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* Command suggestion patterns
|
||||
*/
|
||||
|
||||
import {
|
||||
FILE_PATTERNS,
|
||||
CONTENT_PATTERNS,
|
||||
SUGGESTION_MESSAGES,
|
||||
SUGGESTION_REASONS,
|
||||
} from "@constants/command-suggestion";
|
||||
import { getPackageManager } from "@services/command-suggestion/context";
|
||||
import type {
|
||||
SuggestionPattern,
|
||||
ProjectContext,
|
||||
CommandSuggestion,
|
||||
} from "@/types/command-suggestion";
|
||||
|
||||
const createPackageJsonSuggestions = (
|
||||
ctx: ProjectContext,
|
||||
): CommandSuggestion[] => {
|
||||
const pm = getPackageManager(ctx);
|
||||
return [
|
||||
{
|
||||
command: `${pm} install`,
|
||||
description: SUGGESTION_MESSAGES.INSTALL_DEPS,
|
||||
priority: "high",
|
||||
reason: SUGGESTION_REASONS.PACKAGE_JSON_MODIFIED,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const createTsconfigSuggestions = (
|
||||
ctx: ProjectContext,
|
||||
): CommandSuggestion[] => {
|
||||
const pm = getPackageManager(ctx);
|
||||
return [
|
||||
{
|
||||
command: `${pm} run build`,
|
||||
description: SUGGESTION_MESSAGES.REBUILD_PROJECT,
|
||||
priority: "medium",
|
||||
reason: SUGGESTION_REASONS.TSCONFIG_CHANGED,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const createSourceFileSuggestions = (
|
||||
ctx: ProjectContext,
|
||||
filePath: string,
|
||||
): CommandSuggestion[] => {
|
||||
const pm = getPackageManager(ctx);
|
||||
const isTestFile = FILE_PATTERNS.TEST_FILE.test(filePath);
|
||||
|
||||
return isTestFile
|
||||
? [
|
||||
{
|
||||
command: `${pm} test`,
|
||||
description: SUGGESTION_MESSAGES.RUN_TESTS,
|
||||
priority: "high",
|
||||
reason: SUGGESTION_REASONS.TEST_FILE_MODIFIED,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
command: `${pm} run dev`,
|
||||
description: SUGGESTION_MESSAGES.START_DEV,
|
||||
priority: "low",
|
||||
reason: SUGGESTION_REASONS.SOURCE_FILE_MODIFIED,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const createCargoSuggestions = (): CommandSuggestion[] => [
|
||||
{
|
||||
command: "cargo build",
|
||||
description: SUGGESTION_MESSAGES.BUILD_RUST,
|
||||
priority: "high",
|
||||
reason: SUGGESTION_REASONS.CARGO_MODIFIED,
|
||||
},
|
||||
];
|
||||
|
||||
const createGoModSuggestions = (): CommandSuggestion[] => [
|
||||
{
|
||||
command: "go mod tidy",
|
||||
description: SUGGESTION_MESSAGES.TIDY_GO,
|
||||
priority: "high",
|
||||
reason: SUGGESTION_REASONS.GO_MOD_MODIFIED,
|
||||
},
|
||||
];
|
||||
|
||||
const createPythonSuggestions = (ctx: ProjectContext): CommandSuggestion[] =>
|
||||
ctx.hasPyproject
|
||||
? [
|
||||
{
|
||||
command: "pip install -e .",
|
||||
description: SUGGESTION_MESSAGES.INSTALL_PYTHON_EDITABLE,
|
||||
priority: "high",
|
||||
reason: SUGGESTION_REASONS.PYTHON_DEPS_CHANGED,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
command: "pip install -r requirements.txt",
|
||||
description: SUGGESTION_MESSAGES.INSTALL_PYTHON_DEPS,
|
||||
priority: "high",
|
||||
reason: SUGGESTION_REASONS.REQUIREMENTS_MODIFIED,
|
||||
},
|
||||
];
|
||||
|
||||
const createDockerSuggestions = (
|
||||
_ctx: ProjectContext,
|
||||
filePath: string,
|
||||
): CommandSuggestion[] =>
|
||||
filePath.includes("docker-compose")
|
||||
? [
|
||||
{
|
||||
command: "docker-compose up --build",
|
||||
description: SUGGESTION_MESSAGES.DOCKER_COMPOSE_BUILD,
|
||||
priority: "medium",
|
||||
reason: SUGGESTION_REASONS.DOCKER_COMPOSE_CHANGED,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
command: "docker build -t app .",
|
||||
description: SUGGESTION_MESSAGES.DOCKER_BUILD,
|
||||
priority: "medium",
|
||||
reason: SUGGESTION_REASONS.DOCKERFILE_MODIFIED,
|
||||
},
|
||||
];
|
||||
|
||||
const createMakefileSuggestions = (): CommandSuggestion[] => [
|
||||
{
|
||||
command: "make",
|
||||
description: SUGGESTION_MESSAGES.RUN_MAKE,
|
||||
priority: "medium",
|
||||
reason: SUGGESTION_REASONS.MAKEFILE_MODIFIED,
|
||||
},
|
||||
];
|
||||
|
||||
const createMigrationSuggestions = (
|
||||
ctx: ProjectContext,
|
||||
): CommandSuggestion[] => {
|
||||
const pm = getPackageManager(ctx);
|
||||
return [
|
||||
{
|
||||
command: `${pm} run migrate`,
|
||||
description: SUGGESTION_MESSAGES.RUN_MIGRATE,
|
||||
priority: "high",
|
||||
reason: SUGGESTION_REASONS.MIGRATION_MODIFIED,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const createEnvSuggestions = (): CommandSuggestion[] => [
|
||||
{
|
||||
command: "cp .env.example .env",
|
||||
description: SUGGESTION_MESSAGES.CREATE_ENV,
|
||||
priority: "medium",
|
||||
reason: SUGGESTION_REASONS.ENV_TEMPLATE_MODIFIED,
|
||||
},
|
||||
];
|
||||
|
||||
const createLinterSuggestions = (ctx: ProjectContext): CommandSuggestion[] => {
|
||||
const pm = getPackageManager(ctx);
|
||||
return [
|
||||
{
|
||||
command: `${pm} run lint`,
|
||||
description: SUGGESTION_MESSAGES.RUN_LINT,
|
||||
priority: "low",
|
||||
reason: SUGGESTION_REASONS.LINTER_CONFIG_CHANGED,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const SUGGESTION_PATTERNS: SuggestionPattern[] = [
|
||||
{
|
||||
filePatterns: [FILE_PATTERNS.PACKAGE_JSON],
|
||||
contentPatterns: [
|
||||
CONTENT_PATTERNS.DEPENDENCIES,
|
||||
CONTENT_PATTERNS.DEV_DEPENDENCIES,
|
||||
CONTENT_PATTERNS.PEER_DEPENDENCIES,
|
||||
],
|
||||
suggestions: createPackageJsonSuggestions,
|
||||
},
|
||||
{
|
||||
filePatterns: [FILE_PATTERNS.TSCONFIG],
|
||||
suggestions: createTsconfigSuggestions,
|
||||
},
|
||||
{
|
||||
filePatterns: [FILE_PATTERNS.SOURCE_FILES],
|
||||
suggestions: createSourceFileSuggestions,
|
||||
},
|
||||
{
|
||||
filePatterns: [FILE_PATTERNS.CARGO_TOML],
|
||||
suggestions: createCargoSuggestions,
|
||||
},
|
||||
{
|
||||
filePatterns: [FILE_PATTERNS.GO_MOD],
|
||||
suggestions: createGoModSuggestions,
|
||||
},
|
||||
{
|
||||
filePatterns: [FILE_PATTERNS.PYTHON_DEPS],
|
||||
suggestions: createPythonSuggestions,
|
||||
},
|
||||
{
|
||||
filePatterns: [FILE_PATTERNS.DOCKER],
|
||||
suggestions: createDockerSuggestions,
|
||||
},
|
||||
{
|
||||
filePatterns: [FILE_PATTERNS.MAKEFILE],
|
||||
suggestions: createMakefileSuggestions,
|
||||
},
|
||||
{
|
||||
filePatterns: [FILE_PATTERNS.MIGRATIONS],
|
||||
suggestions: createMigrationSuggestions,
|
||||
},
|
||||
{
|
||||
filePatterns: [FILE_PATTERNS.ENV_EXAMPLE],
|
||||
suggestions: createEnvSuggestions,
|
||||
},
|
||||
{
|
||||
filePatterns: [FILE_PATTERNS.LINTER_CONFIG],
|
||||
suggestions: createLinterSuggestions,
|
||||
},
|
||||
];
|
||||
57
src/services/command-suggestion/state.ts
Normal file
57
src/services/command-suggestion/state.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Command suggestion state management
|
||||
*/
|
||||
|
||||
import { createStore } from "zustand/vanilla";
|
||||
|
||||
import type {
|
||||
CommandSuggestion,
|
||||
ProjectContext,
|
||||
} from "@/types/command-suggestion";
|
||||
|
||||
interface SuggestionState {
|
||||
pendingSuggestions: Map<string, CommandSuggestion>;
|
||||
projectContext: ProjectContext | null;
|
||||
}
|
||||
|
||||
const store = createStore<SuggestionState>(() => ({
|
||||
pendingSuggestions: new Map(),
|
||||
projectContext: null,
|
||||
}));
|
||||
|
||||
export const getProjectContext = (): ProjectContext | null =>
|
||||
store.getState().projectContext;
|
||||
|
||||
export const setProjectContext = (ctx: ProjectContext): void => {
|
||||
store.setState({ projectContext: ctx });
|
||||
};
|
||||
|
||||
export const addSuggestion = (suggestion: CommandSuggestion): boolean => {
|
||||
const { pendingSuggestions } = store.getState();
|
||||
if (pendingSuggestions.has(suggestion.command)) {
|
||||
return false;
|
||||
}
|
||||
const newMap = new Map(pendingSuggestions);
|
||||
newMap.set(suggestion.command, suggestion);
|
||||
store.setState({ pendingSuggestions: newMap });
|
||||
return true;
|
||||
};
|
||||
|
||||
export const removeSuggestion = (command: string): void => {
|
||||
const { pendingSuggestions } = store.getState();
|
||||
const newMap = new Map(pendingSuggestions);
|
||||
newMap.delete(command);
|
||||
store.setState({ pendingSuggestions: newMap });
|
||||
};
|
||||
|
||||
export const clearSuggestions = (): void => {
|
||||
store.setState({ pendingSuggestions: new Map() });
|
||||
};
|
||||
|
||||
export const getPendingSuggestionsMap = (): Map<string, CommandSuggestion> =>
|
||||
store.getState().pendingSuggestions;
|
||||
|
||||
export const hasSuggestion = (command: string): boolean =>
|
||||
store.getState().pendingSuggestions.has(command);
|
||||
|
||||
export const subscribeToSuggestions = store.subscribe;
|
||||
Reference in New Issue
Block a user