-
Notifications
You must be signed in to change notification settings - Fork 366
Expand file tree
/
Copy pathwriter.ts
More file actions
142 lines (119 loc) · 4 KB
/
Copy pathwriter.ts
File metadata and controls
142 lines (119 loc) · 4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import YAML from "yaml";
import { detectProject } from "./detector.ts";
import { getConfigPath, getProgressPath, getRalphyDir } from "./loader.ts";
import type { RalphyConfig } from "./types.ts";
/**
* Create the default config YAML content
*/
function createConfigContent(detected: ReturnType<typeof detectProject>): string {
return `# Ralphy Configuration
# https://github.com/michaelshimeles/ralphy
# Project info (auto-detected, edit if needed)
project:
name: "${escapeYaml(detected.name)}"
language: "${escapeYaml(detected.language || "Unknown")}"
framework: "${escapeYaml(detected.framework)}"
description: "" # Add a brief description
# Commands (auto-detected from package.json/pyproject.toml)
commands:
test: "${escapeYaml(detected.testCmd)}"
lint: "${escapeYaml(detected.lintCmd)}"
build: "${escapeYaml(detected.buildCmd)}"
# Rules - instructions the AI MUST follow
# These are injected into every prompt
rules:
# Examples:
# - "Always use TypeScript strict mode"
# - "Follow the error handling pattern in src/utils/errors.ts"
# - "All API endpoints must have input validation with Zod"
# - "Use server actions instead of API routes in Next.js"
#
# Skills/playbooks (optional):
# - "Before coding, read and follow any relevant skill/playbook docs under .opencode/skills or .claude/skills."
# Boundaries - files/folders the AI should not modify
boundaries:
never_touch:
# Examples:
# - "src/legacy/**"
# - "migrations/**"
# - "*.lock"
`;
}
/**
* Escape a value for safe YAML string
* BUG FIX: Use YAML library for proper escaping to prevent injection attacks
*/
function escapeYaml(value: string | undefined | null): string {
if (!value) return "";
// Use YAML library for proper escaping instead of simple quote replacement
// This prevents YAML injection attacks
const serialized = YAML.stringify(value).trim();
// Remove surrounding quotes added by YAML.stringify for simple strings
return serialized.replace(/^"|"$/g, "");
}
/**
* Initialize the .ralphy directory with config files
*/
export function initConfig(workDir = process.cwd()): {
created: boolean;
detected: ReturnType<typeof detectProject>;
} {
const ralphyDir = getRalphyDir(workDir);
const configPath = getConfigPath(workDir);
const progressPath = getProgressPath(workDir);
// Detect project settings
const detected = detectProject(workDir);
// Create directory if it doesn't exist
if (!existsSync(ralphyDir)) {
mkdirSync(ralphyDir, { recursive: true });
}
// Create config file
const configContent = createConfigContent(detected);
writeFileSync(configPath, configContent, "utf-8");
// Create progress file
writeFileSync(progressPath, "# Ralphy Progress Log\n\n", "utf-8");
return { created: true, detected };
}
/**
* Add a rule to the config
*/
export function addRule(rule: string, workDir = process.cwd()): void {
const configPath = getConfigPath(workDir);
if (!existsSync(configPath)) {
throw new Error(`No config found. Run 'ralphy --init' first.`);
}
const content = readFileSync(configPath, "utf-8");
const parsed = YAML.parse(content) as RalphyConfig;
// Ensure rules array exists
if (!parsed.rules) {
parsed.rules = [];
}
// Add the rule
parsed.rules.push(rule);
// Write back
writeFileSync(configPath, YAML.stringify(parsed), "utf-8");
}
/**
* Log a task to the progress file
*/
export function logTaskProgress(
task: string,
status: "completed" | "failed",
workDir = process.cwd(),
): void {
const progressPath = getProgressPath(workDir);
if (!existsSync(progressPath)) {
return;
}
const timestamp = new Date().toISOString().slice(0, 16).replace("T", " ");
const icon = status === "completed" ? "✓" : "✗";
const line = `- [${icon}] ${timestamp} - ${task}\n`;
appendFileSync(progressPath, line, "utf-8");
}
/**
* @deprecated Writes are synchronous; kept for API compatibility.
*/
export async function flushAllProgressWrites(): Promise<void> {
return Promise.resolve();
}