Skip to content

Commit 306e706

Browse files
authored
feat: add user confirmation to init subcommand (#25)
1 parent b195d7e commit 306e706

14 files changed

Lines changed: 308 additions & 66 deletions

File tree

apps/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "agentv",
3-
"version": "0.5.0",
3+
"version": "0.5.1",
44
"description": "CLI entry point for AgentV",
55
"type": "module",
66
"repository": {
Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,81 @@
11
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
22
import path from "node:path";
3+
import * as readline from "node:readline/promises";
34

45
import { TemplateManager } from "../../templates/index.js";
56

67
export interface InitCommandOptions {
78
targetPath?: string;
89
}
910

11+
async function promptYesNo(message: string): Promise<boolean> {
12+
const rl = readline.createInterface({
13+
input: process.stdin,
14+
output: process.stdout,
15+
});
16+
17+
try {
18+
const answer = await rl.question(`${message} (y/N): `);
19+
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
20+
} finally {
21+
rl.close();
22+
}
23+
}
24+
1025
export async function initCommand(options: InitCommandOptions = {}): Promise<void> {
1126
const targetPath = path.resolve(options.targetPath ?? ".");
1227
const githubDir = path.join(targetPath, ".github");
28+
const agentvDir = path.join(targetPath, ".agentv");
29+
30+
// Get templates
31+
const githubTemplates = TemplateManager.getGithubTemplates();
32+
const agentvTemplates = TemplateManager.getAgentvTemplates();
33+
34+
// Check if any files already exist
35+
const existingFiles: string[] = [];
36+
if (existsSync(githubDir)) {
37+
for (const template of githubTemplates) {
38+
const targetFilePath = path.join(githubDir, template.path);
39+
if (existsSync(targetFilePath)) {
40+
existingFiles.push(path.relative(targetPath, targetFilePath));
41+
}
42+
}
43+
}
44+
if (existsSync(agentvDir)) {
45+
for (const template of agentvTemplates) {
46+
const targetFilePath = path.join(agentvDir, template.path);
47+
if (existsSync(targetFilePath)) {
48+
existingFiles.push(path.relative(targetPath, targetFilePath));
49+
}
50+
}
51+
}
52+
53+
// If files exist, prompt user
54+
if (existingFiles.length > 0) {
55+
console.log("We detected an existing setup:");
56+
existingFiles.forEach((file) => console.log(` - ${file}`));
57+
console.log();
58+
59+
const shouldReplace = await promptYesNo("Do you want to replace these files?");
60+
if (!shouldReplace) {
61+
console.log("\nInit cancelled. No files were changed.");
62+
return;
63+
}
64+
console.log();
65+
}
1366

1467
// Create .github directory if it doesn't exist
1568
if (!existsSync(githubDir)) {
1669
mkdirSync(githubDir, { recursive: true });
1770
}
1871

19-
// Get templates
20-
const templates = TemplateManager.getTemplates();
72+
// Create .agentv directory if it doesn't exist
73+
if (!existsSync(agentvDir)) {
74+
mkdirSync(agentvDir, { recursive: true });
75+
}
2176

22-
// Copy each template to .github
23-
for (const template of templates) {
77+
// Copy each .github template
78+
for (const template of githubTemplates) {
2479
const targetFilePath = path.join(githubDir, template.path);
2580
const targetDirPath = path.dirname(targetFilePath);
2681

@@ -34,8 +89,28 @@ export async function initCommand(options: InitCommandOptions = {}): Promise<voi
3489
console.log(`Created ${path.relative(targetPath, targetFilePath)}`);
3590
}
3691

92+
// Copy each .agentv template
93+
for (const template of agentvTemplates) {
94+
const targetFilePath = path.join(agentvDir, template.path);
95+
const targetDirPath = path.dirname(targetFilePath);
96+
97+
// Create directory if needed
98+
if (!existsSync(targetDirPath)) {
99+
mkdirSync(targetDirPath, { recursive: true });
100+
}
101+
102+
// Write file
103+
writeFileSync(targetFilePath, template.content, "utf-8");
104+
console.log(`Created ${path.relative(targetPath, targetFilePath)}`);
105+
}
106+
37107
console.log("\nAgentV initialized successfully!");
38108
console.log(`\nFiles installed to ${path.relative(targetPath, githubDir)}:`);
39-
templates.forEach((t) => console.log(` - ${t.path}`));
40-
console.log("\nYou can now create eval files using the schema and prompt templates.");
109+
githubTemplates.forEach((t) => console.log(` - ${t.path}`));
110+
console.log(`\nFiles installed to ${path.relative(targetPath, agentvDir)}:`);
111+
agentvTemplates.forEach((t) => console.log(` - ${t.path}`));
112+
console.log("\nYou can now:");
113+
console.log(" 1. Edit .agentv/.env with your API credentials");
114+
console.log(" 2. Configure targets in .agentv/targets.yaml");
115+
console.log(" 3. Create eval files using the schema and prompt templates");
41116
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Example environment configuration for AgentV
2+
# Copy this file to .env and fill in your credentials
3+
4+
# Model Provider Selection (Optional - can be configured via targets.yaml)
5+
PROVIDER=azure
6+
7+
# Azure OpenAI Configuration
8+
# These are the default environment variable names used in the provided targets.yaml
9+
AZURE_OPENAI_ENDPOINT=https://your-endpoint.openai.azure.com/
10+
AZURE_OPENAI_API_KEY=your-api-key-here
11+
AZURE_DEPLOYMENT_NAME=gpt-4o
12+
13+
# Anthropic Configuration (if using Anthropic provider)
14+
ANTHROPIC_API_KEY=your-anthropic-api-key-here
15+
16+
# VS Code Workspace Paths for Execution Targets
17+
# Note: Using forward slashes is recommended for paths in .env files
18+
# to avoid issues with escape characters.
19+
PROJECTX_WORKSPACE_PATH=C:/Users/your-username/OneDrive - Company Pty Ltd/sample.code-workspace
20+
21+
# CLI provider sample (used by the local_cli target)
22+
PROJECT_ROOT=D:/GitHub/your-username/agentv/docs/examples/simple
23+
LOCAL_AGENT_TOKEN=your-cli-token
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
$schema: agentv-config-v2
2+
3+
# Customize which files are treated as guidelines vs regular file content
4+
5+
# Custom guideline patterns:
6+
guideline_patterns:
7+
- "**/*.instructions.md"
8+
- "**/instructions/**"
9+
- "**/*.prompt.md"
10+
- "**/prompts/**"
11+
12+
# Notes:
13+
# - Patterns use standard glob syntax (via micromatch library)
14+
# - Paths are normalized to forward slashes for cross-platform compatibility
15+
# - Only files matching these patterns are loaded as guidelines
16+
# - All other files referenced in eval cases are treated as regular file content
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
$schema: agentv-targets-v2
2+
3+
# A list of all supported evaluation targets for the project.
4+
# Each target defines a provider and its specific configuration.
5+
# Actual values for paths/keys are stored in the local .env file.
6+
7+
targets:
8+
- name: vscode_projectx
9+
provider: vscode
10+
settings:
11+
# VS Code provider requires a workspace template path.
12+
workspace_template: PROJECTX_WORKSPACE_PATH
13+
# This key makes the judge model configurable and is good practice
14+
judge_target: azure_base
15+
16+
- name: vscode_insiders_projectx
17+
provider: vscode-insiders
18+
settings:
19+
# VS Code Insiders provider (preview version) requires a workspace template path.
20+
workspace_template: PROJECTX_WORKSPACE_PATH
21+
# This key makes the judge model configurable and is good practice
22+
judge_target: azure_base
23+
24+
- name: azure_base
25+
provider: azure
26+
# The base LLM provider needs no extra settings, as the model is
27+
# defined in the .env file.
28+
settings:
29+
endpoint: AZURE_OPENAI_ENDPOINT
30+
api_key: AZURE_OPENAI_API_KEY
31+
model: AZURE_DEPLOYMENT_NAME
32+
33+
- name: default
34+
provider: azure
35+
# The base LLM provider needs no extra settings, as the model is
36+
# defined in the .env file.
37+
settings:
38+
endpoint: AZURE_OPENAI_ENDPOINT
39+
api_key: AZURE_OPENAI_API_KEY
40+
model: AZURE_DEPLOYMENT_NAME
41+
42+
- name: local_cli
43+
provider: cli
44+
judge_target: azure_base
45+
settings:
46+
# Passes the fully rendered prompt and any attached files to a local Python script
47+
# NOTE: Do not add quotes around {PROMPT} or {FILES} - they are already shell-escaped
48+
command_template: uv run ./mock_cli.py --prompt {PROMPT} {FILES}
49+
# Format for each file in {FILES}. {path} and {basename} are automatically shell-escaped, so no quotes needed
50+
files_format: --file {path}
51+
# Optional working directory and env overrides resolved from .env
52+
cwd: CLI_EVALS_DIR
53+
env:
54+
API_TOKEN: LOCAL_AGENT_TOKEN
55+
timeout_seconds: 30
56+
healthcheck:
57+
type: command
58+
command_template: uv run ./mock_cli.py --healthcheck
59+
60+
- name: codex_cli
61+
provider: codex
62+
judge_target: azure_base
63+
settings:
64+
# Uses the Codex CLI (defaults to `codex` on PATH)
65+
# executable: CODEX_CLI_PATH # Optional: override executable path
66+
# args: # Optional additional CLI arguments
67+
# - --profile
68+
# - CODEX_PROFILE
69+
# - --model
70+
# - CODEX_MODEL
71+
# - --ask-for-approval
72+
# - CODEX_APPROVAL_PRESET
73+
timeout_seconds: 180
74+
cwd: CODEX_WORKSPACE_DIR # Where scratch workspaces are created
File renamed without changes.
File renamed without changes.

apps/cli/src/templates/eval-build.prompt.md renamed to apps/cli/src/templates/github/prompts/eval-build.prompt.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ description: 'Apply when writing evals in YAML format'
1313
- Message fields: `role` (required), `content` (required)
1414
- Message roles: `system`, `user`, `assistant`, `tool`
1515
- Content types: `text` (inline), `file` (relative or absolute path)
16+
- Attachments (type: `file`) should default to the `user` role
1617
- File paths must start with "/" for absolute paths (e.g., "/prompts/file.md")
1718

1819
## Example

apps/cli/src/templates/index.ts

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { readFileSync } from "node:fs";
1+
import { readFileSync, readdirSync, statSync } from "node:fs";
22
import path from "node:path";
33
import { fileURLToPath } from "node:url";
44

@@ -8,48 +8,52 @@ export interface Template {
88
}
99

1010
export class TemplateManager {
11-
static getTemplates(): Template[] {
12-
// Resolve templates directory:
13-
// - In production (dist): templates are at dist/templates/
14-
// - In development (src): templates are at src/templates/
11+
static getGithubTemplates(): Template[] {
12+
return this.getTemplatesFromDir("github");
13+
}
14+
15+
static getAgentvTemplates(): Template[] {
16+
return this.getTemplatesFromDir("agentv");
17+
}
18+
19+
private static getTemplatesFromDir(subdir: string): Template[] {
1520
const currentDir = path.dirname(fileURLToPath(import.meta.url));
1621

1722
// Check if we're running from dist or src
1823
let templatesDir: string;
1924
if (currentDir.includes(path.sep + "dist")) {
2025
// Production: templates are at dist/templates/
21-
templatesDir = path.join(currentDir, "templates");
26+
templatesDir = path.join(currentDir, "templates", subdir);
2227
} else {
2328
// Development: templates are at src/templates/ (same directory as this file)
24-
templatesDir = currentDir;
29+
templatesDir = path.join(currentDir, subdir);
2530
}
26-
27-
const evalBuildPrompt = readFileSync(
28-
path.join(templatesDir, "eval-build.prompt.md"),
29-
"utf-8"
30-
);
31-
const evalSchema = readFileSync(
32-
path.join(templatesDir, "eval-schema.json"),
33-
"utf-8"
34-
);
35-
const configSchema = readFileSync(
36-
path.join(templatesDir, "config-schema.json"),
37-
"utf-8"
38-
);
39-
40-
return [
41-
{
42-
path: "prompts/eval-build.prompt.md",
43-
content: evalBuildPrompt,
44-
},
45-
{
46-
path: "contexts/eval-schema.json",
47-
content: evalSchema,
48-
},
49-
{
50-
path: "contexts/config-schema.json",
51-
content: configSchema,
52-
},
53-
];
31+
32+
return this.readTemplatesRecursively(templatesDir, "");
33+
}
34+
35+
private static readTemplatesRecursively(dir: string, relativePath: string): Template[] {
36+
const templates: Template[] = [];
37+
const entries = readdirSync(dir);
38+
39+
for (const entry of entries) {
40+
const fullPath = path.join(dir, entry);
41+
const stat = statSync(fullPath);
42+
const entryRelativePath = relativePath ? path.join(relativePath, entry) : entry;
43+
44+
if (stat.isDirectory()) {
45+
// Recursively read subdirectories
46+
templates.push(...this.readTemplatesRecursively(fullPath, entryRelativePath));
47+
} else {
48+
// Read file content
49+
const content = readFileSync(fullPath, "utf-8");
50+
templates.push({
51+
path: entryRelativePath.split(path.sep).join("/"), // Normalize to forward slashes
52+
content,
53+
});
54+
}
55+
}
56+
57+
return templates;
5458
}
5559
}

0 commit comments

Comments
 (0)