Skip to content

Commit f0bdbf3

Browse files
committed
chore: recover standardization service files from feat/modular branch
1 parent 2b96528 commit f0bdbf3

5 files changed

Lines changed: 541 additions & 0 deletions

File tree

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import { Octokit } from "octokit";
2+
import { getOctokit } from "./octokit/core";
3+
import { getGithubConfigs } from "@utils/github/configs";
4+
import { DEFAULT_TEMPLATE_REPO, DEFAULT_GITHUB_OWNER } from "@github-utils";
5+
import { getDb } from "@db";
6+
import { standardizationRules } from "@db/schemas/app/standardization";
7+
import { eq } from "drizzle-orm";
8+
import type { Agent } from "@openai/agents";
9+
import { createRunner, resolveDefaultAiModel, resolveDefaultAiProvider } from "@/ai/agents/base/agent-ai";
10+
11+
export class StandardizationService {
12+
private static STANDARD_REPO_OWNER = DEFAULT_GITHUB_OWNER;
13+
private static STANDARD_REPO_NAME = DEFAULT_TEMPLATE_REPO;
14+
15+
/**
16+
* Enforce standards on a repository
17+
* @param env Worker Environment
18+
* @param targetRepo Target repository metadata
19+
*/
20+
static async enforce(env: Env, targetRepo: { owner: { login: string }, name: string, default_branch?: string }) {
21+
console.log(`[Standardization] Enforcing standards on ${targetRepo.owner.login}/${targetRepo.name}`);
22+
23+
const octokit = await getOctokit(env) as unknown as Octokit;
24+
const config = getGithubConfigs(env);
25+
const db = getDb(env.DB);
26+
27+
// 1. Infer Infrastructure Tags for Target Repo
28+
// We need to list files to infer tags.
29+
let infraTags: string[] = ["Repository"];
30+
try {
31+
const { data: tree } = await octokit.rest.git.getTree({
32+
owner: targetRepo.owner.login,
33+
repo: targetRepo.name,
34+
tree_sha: targetRepo.default_branch || 'main',
35+
recursive: '1'
36+
});
37+
38+
const paths = (tree.tree || []).map(t => t.path || "").filter(Boolean);
39+
infraTags = this.inferProjectTags(paths);
40+
console.log(`[Standardization] Inferred tags for ${targetRepo.name}:`, infraTags);
41+
} catch (e) {
42+
console.warn(`[Standardization] Failed to infer tags (likely empty repo or no access), defaulting to basic tags.`, e);
43+
}
44+
45+
// 2. Fetch Rules from DB
46+
const rules = await db.select().from(standardizationRules).all();
47+
48+
// 3. Apply Rules
49+
for (const rule of rules) {
50+
await this.applyRule(env, octokit, config, rule, targetRepo, infraTags);
51+
}
52+
53+
console.log(`[Standardization] Completed enforcement for ${targetRepo.owner.login}/${targetRepo.name}`);
54+
}
55+
56+
/**
57+
* Apply a single rule
58+
*/
59+
private static async applyRule(
60+
env: Env,
61+
octokit: Octokit,
62+
config: any,
63+
rule: typeof standardizationRules.$inferSelect,
64+
targetRepo: { owner: { login: string }, name: string, default_branch?: string },
65+
targetTags: string[]
66+
) {
67+
// A. Check Relevance
68+
const relevantInfra = JSON.parse(rule.relevantInfra);
69+
const irrelevantInfra = JSON.parse(rule.irrelevantInfra);
70+
71+
// Logic:
72+
// - If relevantInfra is empty, it applies to ALL (unless excluded).
73+
// - If relevantInfra has items, target MUST have at least one.
74+
// - If target has ANY tag in irrelevantInfra, skip.
75+
76+
if (irrelevantInfra.length > 0 && targetTags.some(tag => irrelevantInfra.includes(tag))) {
77+
// console.debug(`[Standardization] Skipping ${rule.filePath} (Irrelevant Infra)`);
78+
return;
79+
}
80+
81+
if (relevantInfra.length > 0 && !targetTags.some(tag => relevantInfra.includes(tag))) {
82+
// console.debug(`[Standardization] Skipping ${rule.filePath} (Missing Relevant Infra)`);
83+
return;
84+
}
85+
86+
// B. Fetch Source Content
87+
let content: string | null = null;
88+
let sourceSha: string | undefined;
89+
90+
try {
91+
const [sourceOwner, sourceRepo] = rule.sourceRepo.split('/');
92+
const { data: sourceFile } = await octokit.rest.repos.getContent({
93+
owner: sourceOwner,
94+
repo: sourceRepo,
95+
path: rule.filePath
96+
});
97+
98+
if (!Array.isArray(sourceFile) && sourceFile.type === "file" && sourceFile.content) {
99+
content = Buffer.from(sourceFile.content, "base64").toString("utf8");
100+
sourceSha = sourceFile.sha;
101+
}
102+
} catch (e: any) {
103+
console.warn(`[Standardization] Source file ${rule.sourceRepo}/${rule.filePath} not found.`, e.message);
104+
return;
105+
}
106+
107+
if (!content) return;
108+
109+
// C. AI Customization
110+
if (rule.aiInstructions) {
111+
try {
112+
const provider = resolveDefaultAiProvider(env);
113+
const model = resolveDefaultAiModel(env, provider);
114+
115+
const runner = await createRunner(env, provider, model);
116+
const { Agent: OpenAIAgent } = await import("@openai/agents");
117+
const agent = new OpenAIAgent({
118+
name: "StandardizationCustomizer",
119+
model,
120+
instructions: "Customize the file content based on the instructions. Return ONLY the full customized file content. No markdown fences. Do not output any conversational text.",
121+
});
122+
123+
const prompt = `
124+
Instructions: "${rule.aiInstructions}"
125+
126+
Target Context:
127+
Repo: ${targetRepo.owner.login}/${targetRepo.name}
128+
Tags: ${targetTags.join(', ')}
129+
130+
File Content:
131+
${content}
132+
`;
133+
134+
const result = await runner.run(agent, prompt);
135+
let customized = typeof result.finalOutput === 'string' ? result.finalOutput : String(result.finalOutput);
136+
137+
// Strip markdown fences if present
138+
customized = customized.replace(/^```[a-z]*\n/i, '').replace(/\n```$/, '').trim();
139+
140+
if (customized) {
141+
content = customized;
142+
sourceSha = undefined;
143+
}
144+
145+
} catch (aiError) {
146+
console.error(`[Standardization] AI Customization failed for ${rule.filePath}`, aiError);
147+
}
148+
}
149+
150+
// D. Sync to Target
151+
try {
152+
// Check existence
153+
let targetSha: string | undefined;
154+
try {
155+
const { data: targetFile } = await octokit.rest.repos.getContent({
156+
owner: targetRepo.owner.login,
157+
repo: targetRepo.name,
158+
path: rule.filePath
159+
});
160+
161+
if (!Array.isArray(targetFile) && targetFile.type === "file") {
162+
targetSha = targetFile.sha;
163+
164+
// If we shouldn't overwrite and it exists, skip.
165+
// Exception: if we want to enforce updates, we check overWrite policy.
166+
if (!rule.shouldOverwrite) {
167+
// console.debug(`[Standardization] Skipping ${rule.filePath} (Exists & No Overwrite)`);
168+
return;
169+
}
170+
171+
// Optimization: If no AI customization happened, we can compare SHAs (if source was pure).
172+
if (sourceSha && targetFile.sha === sourceSha) {
173+
return;
174+
}
175+
}
176+
} catch (err: any) {
177+
if (err.status !== 404) throw err;
178+
}
179+
180+
// Write
181+
await octokit.rest.repos.createOrUpdateFileContents({
182+
owner: targetRepo.owner.login,
183+
repo: targetRepo.name,
184+
path: rule.filePath,
185+
message: `chore(standards): sync ${rule.filePath}`,
186+
content: Buffer.from(content || "").toString("base64"),
187+
sha: targetSha,
188+
branch: targetRepo.default_branch
189+
});
190+
191+
console.log(`[Standardization] Synced ${rule.filePath} to ${targetRepo.name}`);
192+
193+
} catch (e: any) {
194+
console.error(`[Standardization] Failed to write ${rule.filePath}`, e);
195+
}
196+
}
197+
198+
199+
/**
200+
* Infer project tags (Simplified version of logic in projects.ts)
201+
* We duplicate slightly to avoid circular dependency on "routes" logic or we should move logic to shared util.
202+
* Moving to shared util is better but for now let's keep it self-contained or use what we can.
203+
*/
204+
private static inferProjectTags(paths: string[]): string[] {
205+
const tags = new Set<string>(["Repository"]);
206+
const lowerPaths = paths.map(p => p.toLowerCase());
207+
208+
if (lowerPaths.some(p => p.endsWith("wrangler.toml") || p.endsWith("wrangler.json") || p.endsWith("wrangler.jsonc"))) {
209+
tags.add("cloudflare_worker");
210+
tags.add("cloudflare");
211+
}
212+
if (lowerPaths.some(p => p.endsWith("package.json"))) tags.add("nodejs");
213+
if (lowerPaths.some(p => p.endsWith(".py") || p.endsWith("requirements.txt"))) tags.add("python");
214+
if (lowerPaths.some(p => p.includes("next.config"))) tags.add("nextjs");
215+
if (lowerPaths.some(p => p.includes("astro.config"))) tags.add("astro");
216+
217+
return Array.from(tags);
218+
}
219+
}
220+
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
2+
import { getOctokit } from "@services/octokit/core";
3+
import { BaseAgent } from "@/ai/agents/base/BaseAgent";
4+
5+
export class AgentGenerator {
6+
static async ensureAgent(env: Env, owner: string, repo: string) {
7+
const octokit = await getOctokit(env);
8+
9+
// 1. Check if ANY .agent.md exists in .github/agents/
10+
// We look for .github/agents/*.agent.md
11+
// List contents of .github/agents
12+
let hasAgent = false;
13+
try {
14+
const { data: contents } = await octokit.rest.repos.getContent({
15+
owner,
16+
repo,
17+
path: ".github/agents",
18+
});
19+
20+
if (Array.isArray(contents)) {
21+
hasAgent = contents.some(file => file.name.endsWith(".agent.md"));
22+
}
23+
} catch (err: any) {
24+
if (err.status !== 404) {
25+
console.error("[AgentGen] Error checking agents:", err);
26+
}
27+
// 404 means directory doesn't exist, so no agent.
28+
}
29+
30+
if (hasAgent) {
31+
console.log(`[AgentGen] Agent already exists for ${owner}/${repo}. Optimizing instructions...`);
32+
try {
33+
const { data: files } = await octokit.rest.repos.getContent({
34+
owner,
35+
repo,
36+
path: ".github/agents",
37+
});
38+
39+
const agentFile = (Array.isArray(files) ? files : [files]).find((f: any) => f.name.endsWith(".agent.md"));
40+
41+
if (agentFile && agentFile.sha) {
42+
const { data: fileData } = await octokit.rest.repos.getContent({
43+
owner, repo, path: agentFile.path
44+
});
45+
46+
if (!Array.isArray(fileData) && (fileData as any).content) {
47+
const contentBytes = atob((fileData as any).content);
48+
49+
if ((env as any).AI) {
50+
const response = await (env as any).AI.run('@cf/meta/llama-3.1-8b-instruct', {
51+
messages: [
52+
{ role: 'system', content: 'You are an expert AI agent architect. Review the provided .agent.md configuration and improve its core instructions for clarity, precision, and tool utilization while strictly preserving the existing YAML frontmatter. Return ONLY the final markdown file content without conversational padding.' },
53+
{ role: 'user', content: `Here is the current agent configuration:\n\n${contentBytes}` }
54+
]
55+
});
56+
57+
const optimizedContent = (response as any).response;
58+
59+
if (optimizedContent && optimizedContent.includes('---')) {
60+
await octokit.rest.repos.createOrUpdateFileContents({
61+
owner,
62+
repo,
63+
path: agentFile.path,
64+
message: "chore(agent): optimize agent instructions via Workers AI",
65+
content: btoa(optimizedContent),
66+
sha: agentFile.sha,
67+
});
68+
console.log(`[AgentGen] Successfully optimized ${agentFile.name}`);
69+
} else {
70+
console.log(`[AgentGen] Optimization discarded (invalid output format)`);
71+
}
72+
} else {
73+
console.warn("[AgentGen] AI binding not found, skipping optimization.");
74+
}
75+
}
76+
}
77+
} catch (error) {
78+
console.error("[AgentGen] Optimization process failed:", error);
79+
}
80+
return;
81+
}
82+
83+
console.log(`[AgentGen] No agent found for ${owner}/${repo}. Generating default specialist...`);
84+
85+
// 2. Evaluate Tech Stack (Simplistic heuristic for now)
86+
// Check for package.json, wrangler.toml, etc.
87+
const stack: string[] = [];
88+
try {
89+
await octokit.rest.repos.getContent({ owner, repo, path: "wrangler.toml" });
90+
stack.push("Cloudflare Workers");
91+
} catch(e) {
92+
console.log(`[AgentGen] No wrangler.toml found for ${owner}/${repo}`, JSON.stringify(e));
93+
}
94+
try {
95+
const { data: pkgParams } = await octokit.rest.repos.getContent({ owner, repo, path: "package.json" });
96+
if ("content" in pkgParams) {
97+
const pkgJson = JSON.parse(atob(pkgParams.content));
98+
const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
99+
if (deps.hono) stack.push("Hono");
100+
if (deps.drizzle || deps["drizzle-orm"]) stack.push("Drizzle ORM");
101+
if (deps.astro) stack.push("Astro");
102+
if (deps.react) stack.push("React");
103+
}
104+
} catch(e) {
105+
console.log(`[AgentGen] No package.json found for ${owner}/${repo}`, JSON.stringify(e));
106+
}
107+
108+
const description = stack.length > 0
109+
? `Expert in ${stack.join(", ")} and repository standardization.`
110+
: "General purpose repository specialist.";
111+
112+
// 3. Generate .agent.md Content
113+
const agentName = `${repo}-specialist`;
114+
const agentContent = `---
115+
name: ${agentName}
116+
description: ${description}
117+
tools: ["*"]
118+
mcp-servers:
119+
- github
120+
- cloudflare-docs
121+
---
122+
123+
# ${agentName}
124+
125+
You are the dedicated specialist for the ${repo} repository.
126+
Your tech stack includes: ${stack.join(", ")}.
127+
128+
## Core Instructions
129+
130+
1. **Code Quality**: Enforce strictly typed TypeScript. Use Zod for validation.
131+
2. **Architecture**: Follow the Modular Backend/Fullstack structure.
132+
3. **Database**: Use Drizzle ORM for all database interactions.
133+
4. **Testing**: Ensure all critical paths are covered by tests.
134+
135+
## MCP Tools
136+
You have access to the full suite of MCP tools. Use them to:
137+
- Search documentation (cloudflare-docs)
138+
- Manage GitHub issues and PRs (github)
139+
- Execute code in the sandbox (sandbox)
140+
`;
141+
142+
// 4. Push to Repo
143+
try {
144+
await octokit.rest.repos.createOrUpdateFileContents({
145+
owner,
146+
repo,
147+
path: `.github/agents/${agentName}.agent.md`,
148+
message: "feat(agent): bootstrap default repository specialist",
149+
content: btoa(agentContent),
150+
});
151+
console.log(`[AgentGen] Created .github/agents/${agentName}.agent.md`);
152+
} catch (err) {
153+
console.error("[AgentGen] Failed to create agent file:", err);
154+
}
155+
}
156+
}

0 commit comments

Comments
 (0)