Skip to content

Commit c668cbf

Browse files
committed
Auto-scaffold repos on first run
gitclaw now handles all setup automatically when pointed at any directory: - Creates the directory if it doesn't exist - Runs git init if not already a repo - Scaffolds agent.yaml, SOUL.md, and memory/MEMORY.md if missing - Commits the scaffold - Prompts for directory path interactively if --dir not provided
1 parent 6ffa628 commit c668cbf

1 file changed

Lines changed: 127 additions & 3 deletions

File tree

src/index.ts

Lines changed: 127 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ import type { HooksConfig } from "./hooks.js";
1414
import { loadDeclarativeTools } from "./tool-loader.js";
1515
import { AuditLogger, isAuditEnabled } from "./audit.js";
1616
import { formatComplianceWarnings } from "./compliance.js";
17-
import { readFile } from "fs/promises";
18-
import { join } from "path";
17+
import { readFile, mkdir, writeFile, stat, access } from "fs/promises";
18+
import { join, resolve } from "path";
19+
import { execSync } from "child_process";
1920

2021
// ANSI helpers
2122
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
@@ -122,8 +123,131 @@ function summarizeArgs(args: any): string {
122123
.join(", ");
123124
}
124125

126+
function askQuestion(question: string): Promise<string> {
127+
const rl = createInterface({ input: process.stdin, output: process.stdout });
128+
return new Promise((res) => {
129+
rl.question(question, (answer) => {
130+
rl.close();
131+
res(answer.trim());
132+
});
133+
});
134+
}
135+
136+
function isGitRepo(dir: string): boolean {
137+
try {
138+
execSync("git rev-parse --is-inside-work-tree", { cwd: dir, stdio: "pipe" });
139+
return true;
140+
} catch {
141+
return false;
142+
}
143+
}
144+
145+
async function fileExists(path: string): Promise<boolean> {
146+
try {
147+
await access(path);
148+
return true;
149+
} catch {
150+
return false;
151+
}
152+
}
153+
154+
async function ensureRepo(dir: string, model?: string): Promise<string> {
155+
const absDir = resolve(dir);
156+
157+
// Create directory if it doesn't exist
158+
if (!(await fileExists(absDir))) {
159+
console.log(dim(`Creating directory: ${absDir}`));
160+
await mkdir(absDir, { recursive: true });
161+
}
162+
163+
// Git init if not a repo
164+
if (!isGitRepo(absDir)) {
165+
console.log(dim("Initializing git repository..."));
166+
execSync("git init", { cwd: absDir, stdio: "pipe" });
167+
168+
// Create .gitignore
169+
const gitignorePath = join(absDir, ".gitignore");
170+
if (!(await fileExists(gitignorePath))) {
171+
await writeFile(gitignorePath, "node_modules/\ndist/\n.gitagent/\n", "utf-8");
172+
}
173+
174+
// Initial commit so memory saves work
175+
execSync("git add -A && git commit -m 'Initial commit' --allow-empty", {
176+
cwd: absDir,
177+
stdio: "pipe",
178+
});
179+
}
180+
181+
// Scaffold agent.yaml if missing
182+
const agentYamlPath = join(absDir, "agent.yaml");
183+
if (!(await fileExists(agentYamlPath))) {
184+
const defaultModel = model || "openai:gpt-4o-mini";
185+
const agentName = absDir.split("/").pop() || "my-agent";
186+
const yaml = [
187+
'spec_version: "0.1.0"',
188+
`name: ${agentName}`,
189+
"version: 0.1.0",
190+
`description: Gitclaw agent for ${agentName}`,
191+
"model:",
192+
` preferred: "${defaultModel}"`,
193+
" fallback: []",
194+
"tools: [cli, read, write, memory]",
195+
"runtime:",
196+
" max_turns: 50",
197+
"",
198+
].join("\n");
199+
await writeFile(agentYamlPath, yaml, "utf-8");
200+
console.log(dim(`Created agent.yaml (model: ${defaultModel})`));
201+
}
202+
203+
// Scaffold memory if missing
204+
const memoryDir = join(absDir, "memory");
205+
const memoryFile = join(memoryDir, "MEMORY.md");
206+
if (!(await fileExists(memoryFile))) {
207+
await mkdir(memoryDir, { recursive: true });
208+
await writeFile(memoryFile, "# Memory\n", "utf-8");
209+
}
210+
211+
// Scaffold SOUL.md if missing
212+
const soulPath = join(absDir, "SOUL.md");
213+
if (!(await fileExists(soulPath))) {
214+
await writeFile(soulPath, [
215+
"# Identity",
216+
"",
217+
"You are a helpful AI agent. You live inside a git repository.",
218+
"You can run commands, read and write files, and remember things.",
219+
"Be concise and action-oriented.",
220+
"",
221+
].join("\n"), "utf-8");
222+
}
223+
224+
// Stage new scaffolded files
225+
try {
226+
execSync("git add -A && git diff --cached --quiet || git commit -m 'Scaffold gitclaw agent'", {
227+
cwd: absDir,
228+
stdio: "pipe",
229+
});
230+
} catch {
231+
// ok if nothing to commit
232+
}
233+
234+
return absDir;
235+
}
236+
125237
async function main(): Promise<void> {
126-
const { model, dir, prompt, env } = parseArgs(process.argv);
238+
const { model, dir: rawDir, prompt, env } = parseArgs(process.argv);
239+
240+
// If no --dir given interactively, ask for it
241+
let dir = rawDir;
242+
if (dir === process.cwd() && !prompt) {
243+
const answer = await askQuestion(green("? ") + bold("Repository path") + dim(" (. for current dir)") + green(": "));
244+
if (answer) {
245+
dir = resolve(answer === "." ? process.cwd() : answer);
246+
}
247+
}
248+
249+
// Ensure the target is a valid gitclaw repo
250+
dir = await ensureRepo(dir, model);
127251

128252
let loaded;
129253
try {

0 commit comments

Comments
 (0)